Skip to content

perf: Conditional gallocr caching for Fast-AR decode on pure CPU runs (%4~ faster)#43

Open
Skyrion9 wants to merge 1 commit into
rodrigomatta:mainfrom
Skyrion9:gallocr-cache
Open

perf: Conditional gallocr caching for Fast-AR decode on pure CPU runs (%4~ faster)#43
Skyrion9 wants to merge 1 commit into
rodrigomatta:mainfrom
Skyrion9:gallocr-cache

Conversation

@Skyrion9

@Skyrion9 Skyrion9 commented Jul 15, 2026

Copy link
Copy Markdown
  • Implemented ggml_gallocr_t with proper lifecycle management.

  • We check if we're running purely on CPU and use the cached codepath, or fallback to old behavior when any GPU backend is involved.

  • This effectively eliminates redundant allocation cycles in Fast-AR decoding loop for pure CPU configs.

Before/After PR test

CPU: AMD Ryzen 7 5700X3D (8C/16T, pinned to physical cores)
Model: s2-pro-q8_0.gguf | Backend: CPU (-ngl 0) | Allocator: mimalloc
Prompt: "hello world, how are you today?" (~204 tokens prefill)

Metric Without gallocr With gallocr Delta
Gen Loop Avg 294.36 ms/frame 274.90 ms/frame -6.6%
Total Avg 399.09 ms/frame 382.14 ms/frame -4.2%
Gen RTF 7.66 7.27 -5.1%
Total RTF 8.59 8.23 -4.2%
Prefill 3675 ms 3503 ms -4.7%
Decode 2592 ms 2485 ms -4.1%
Max RSS 6351 MB 6329 MB -22 MB
My build script:
#!/bin/bash

# ==============================================================================
# s2.cpp CPU Build Script - Optimized for AMD Ryzen 5700X3D (Zen 3)
# ==============================================================================

# Duke nukem
# rm -rf build/

export CC=clang
export CXX=clang++

export OPT_FLAGS="-O3 -march=znver3 -flto=thin -ffast-math -fno-finite-math-only -funroll-loops -mprefer-vector-width=256"

export LINK_FLAGS="-fuse-ld=lld -flto=thin -lmimalloc"

# Configure via Ninja
cmake -S . -B build -G Ninja \
  -DCMAKE_C_COMPILER_LAUNCHER=ccache \
  -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_C_FLAGS="$OPT_FLAGS" \
  -DCMAKE_CXX_FLAGS="$OPT_FLAGS" \
  -DCMAKE_EXE_LINKER_FLAGS="$LINK_FLAGS" \
  -DCMAKE_SHARED_LINKER_FLAGS="$LINK_FLAGS" \
  -DS2_VULKAN=OFF \
  -DS2_AUTO_APPLY_LOCAL_PATCHES=OFF \
  -DGGML_VULKAN=OFF \
  -DGGML_NATIVE=OFF \
  -DGGML_AVX2=ON \
  -DGGML_FMA=ON \
  -DGGML_F16C=ON \
  -DGGML_OPENMP=OFF \
  -DBUILD_SHARED_LIBS=OFF \
  -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \
  -DGGML_LTO=OFF

# GGML_LTO / CMAKE_INTERPROCEDURAL_OPTIMIZATION:
# we are manually forcing "-flto=thin" and "-fuse-ld=lld" through our raw compilation flags, redundant.
cmake --build build --config Release --parallel

echo ""
echo "Build complete. Binary: build/s2"
echo "Remember to set at runtime:"
echo "  export GGML_CPU_ALL_CORES=0"
echo "  export GGML_CPU_STRICT=1"
echo "  export MIMALLOC_PURGE_DELAY=0"
My runtime setup:
export GGML_CPU_ALL_CORES=0
export GGML_CPU_STRICT=1
export MIMALLOC_PURGE_DELAY=0
export MIMALLOC_ALLOW_LARGE_OS_PAGES=1

./s2_CPU \
  -m models/s2-pro-q8_0.gguf \
  -t models/tokenizer.json \
  -v -1 \
  --threads 8 \
  --temp 0.8 \
  --top-p 0.95 \
  --voice 69 \
  --codec-context-frames 128 \
  --codec-follow-backend \
  -text "hello world, how are you today?" \
  -o output.wav

Summary by CodeRabbit

  • Performance

    • Improved CPU-only model decoding efficiency by reusing allocated resources during repeated inference.
    • Reduced overhead when processing responses without GPU acceleration.
  • Bug Fixes

    • Improved reliability of fast decoding across both CPU-only and GPU-accelerated configurations.
    • Added safer handling for resource allocation and graph execution failures.

… %4~ faster CPU inference.

- Implemented ggml_gallocr_t with proper lifecycle management.

- We check if we're running purely on CPU and use the cached codepath, or fallback to old behavior when any GPU backend is involved.

- We effectively eliminate redundant allocation cycles in Fast-AR decoding loop for pure CPU configs.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

SlowARModel now owns a cached graph allocator. fast_decode uses the allocator for CPU-only graph execution and continues using the scheduler for GPU-offloaded execution, with corresponding cleanup and error handling.

Changes

SlowARModel execution

Layer / File(s) Summary
Cached allocator lifecycle
include/s2_model.h, src/s2_model.cpp
SlowARModel declares and initializes fast_gallocr_, then frees it during destruction.
Conditional fast decode execution
src/s2_model.cpp
CPU-only decoding allocates and computes graphs through fast_gallocr_ and backend_cpu_; GPU-offloaded decoding retains fast_sched_ execution and branch-specific error handling.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Suggested reviewers: rodrigomatta

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: CPU-only conditional gallocr caching for Fast-AR decode, and it matches the performance-focused diff.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/s2_model.cpp (1)

1238-1289: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deduplicate the tensor-set calls shared by both branches.

The ggml_backend_tensor_set calls for hidden0, positions, and prefix_ids (Lines 1254-1258) are byte-for-byte identical to the ones in the GPU-offload branch (Lines 1275-1279). Only the allocation and compute calls actually differ between branches; the input-setting step can be pulled out into a single shared block after allocation succeeds in either branch, avoiding the two copies drifting apart later.

♻️ Proposed refactor
     if (is_cpu_only) {
         if (!fast_gallocr_) {
             fast_gallocr_ = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend_cpu_));
             if (!fast_gallocr_) {
                 std::fprintf(stderr, "[fast_decode] gallocr creation failed\n");
                 ggml_free(ctx0);
                 return false;
             }
         }

         if (!ggml_gallocr_alloc_graph(fast_gallocr_, gf)) {
             std::fprintf(stderr, "[fast_decode] gallocr alloc failed\n");
             ggml_free(ctx0);
             return false;
         }
-
-        ggml_backend_tensor_set(hidden0, hidden_in.data(), 0, hidden_in.size() * sizeof(float));
-        ggml_backend_tensor_set(positions, pos_vals.data(), 0, pos_vals.size() * sizeof(int32_t));
-        if (prefix_ids) {
-            ggml_backend_tensor_set(prefix_ids, prefix_tokens.data(), 0, prefix_tokens.size() * sizeof(int32_t));
-        }
-
         if (ggml_backend_graph_compute(backend_cpu_, gf) != GGML_STATUS_SUCCESS) {
             std::fprintf(stderr, "[fast_decode] cpu compute failed\n");
             ggml_free(ctx0);
             return false;
         }
     } else {
         ggml_backend_sched_reset(fast_sched_);

         if (!ggml_backend_sched_alloc_graph(fast_sched_, gf)) {
             std::fprintf(stderr, "[fast_decode] sched alloc failed\n");
             ggml_backend_sched_reset(fast_sched_);
             ggml_free(ctx0);
             return false;
         }
-
-        ggml_backend_tensor_set(hidden0,   hidden_in.data(),    0, hidden_in.size() * sizeof(float));
-        ggml_backend_tensor_set(positions, pos_vals.data(),     0, pos_vals.size() * sizeof(int32_t));
-        if (prefix_ids) {
-            ggml_backend_tensor_set(prefix_ids, prefix_tokens.data(), 0, prefix_tokens.size() * sizeof(int32_t));
-        }
-
+
+    }
+
+    ggml_backend_tensor_set(hidden0,   hidden_in.data(),    0, hidden_in.size() * sizeof(float));
+    ggml_backend_tensor_set(positions, pos_vals.data(),     0, pos_vals.size() * sizeof(int32_t));
+    if (prefix_ids) {
+        ggml_backend_tensor_set(prefix_ids, prefix_tokens.data(), 0, prefix_tokens.size() * sizeof(int32_t));
+    }
+
+    if (is_cpu_only) {
+        if (ggml_backend_graph_compute(backend_cpu_, gf) != GGML_STATUS_SUCCESS) {
+            std::fprintf(stderr, "[fast_decode] cpu compute failed\n");
+            ggml_free(ctx0);
+            return false;
+        }
+    } else {
         if (ggml_backend_sched_graph_compute(fast_sched_, gf) != GGML_STATUS_SUCCESS) {
             std::fprintf(stderr, "[fast_decode] sched compute failed\n");
             ggml_backend_sched_reset(fast_sched_);
             ggml_free(ctx0);
             return false;
         }

         ggml_backend_sched_reset(fast_sched_);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/s2_model.cpp` around lines 1238 - 1289, Deduplicate the input tensor
setup in the fast decode flow: remove the identical ggml_backend_tensor_set
calls for hidden0, positions, and optional prefix_ids from both is_cpu_only
branches, and place one shared block after the branch-specific graph allocation
succeeds and before computation. Preserve the existing allocation, compute,
cleanup, and failure behavior for both CPU and scheduled backends.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/s2_model.cpp`:
- Around line 1238-1289: Deduplicate the input tensor setup in the fast decode
flow: remove the identical ggml_backend_tensor_set calls for hidden0, positions,
and optional prefix_ids from both is_cpu_only branches, and place one shared
block after the branch-specific graph allocation succeeds and before
computation. Preserve the existing allocation, compute, cleanup, and failure
behavior for both CPU and scheduled backends.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 95e6695c-8e20-49e1-a5ed-373709072048

📥 Commits

Reviewing files that changed from the base of the PR and between 2c33261 and 6d320ac.

📒 Files selected for processing (2)
  • include/s2_model.h
  • src/s2_model.cpp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant