Skip to content

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

Open
Skyrion9 wants to merge 2 commits 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 2 commits 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 decoding efficiency by reusing a cached allocator across repeated inference runs.
    • Reduced decoding overhead by using an optimized CPU execution path when GPU offload is inactive.
  • Bug Fixes

    • Improved reliability of fast decoding across CPU-only and GPU-accelerated configurations.
    • Added safer handling for allocator/scheduler allocation and graph execution failures, with earlier failure returns.

… %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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 032299b7-a24f-46b7-9e98-1b8f8ee4426f

📥 Commits

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

📒 Files selected for processing (1)
  • src/s2_model.cpp

📝 Walkthrough

Walkthrough

SlowARModel now owns a cached graph allocator. fast_decode uses it for CPU-only graph execution and retains scheduler-based execution for GPU-offloaded decoding.

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 with 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 accurately summarizes the main change: conditional gallocr caching for Fast-AR decode on CPU-only runs.
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

- In the GPU path of fast_decode, ggml_backend_sched_reset() was called at the end of the else block, before ggml_backend_tensor_get() read the output logits.
..This released the scheduler's buffer ownership while the output tensor was still being read.

- Moved the reset to after tensor_get, guarded by !is_cpu_only since the CPU gallocr path does not use the scheduler.
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