Skip to content

feat: Backend-specific prefill chunk sizes#39

Open
Skyrion9 wants to merge 2 commits into
rodrigomatta:mainfrom
Skyrion9:prefill-chunk
Open

feat: Backend-specific prefill chunk sizes#39
Skyrion9 wants to merge 2 commits into
rodrigomatta:mainfrom
Skyrion9:prefill-chunk

Conversation

@Skyrion9

@Skyrion9 Skyrion9 commented Jul 15, 2026

Copy link
Copy Markdown
  • The default adaptive_gpu_chunk is too conservative, For example it'd fire up 28 times for 204 tokens causing large delays in prefill with 800 ms~ latency if used with feat: Flash Attention and modernized KV Cache layout for Slow-AR #40 only in blocks of 8.
  • By using larger chunk sizes that are adjusted for each backend, we can safely decrease this delay (from 872 ms to 378 ms, both with FA) to increase performance.

Before PR (FA disabled, no chunking for some reason, optimal prefill delay):

[Generate] Prefilling 204 tokens...
[Generate] Generating (max 1024 tokens)...
[Generate] 50 / 1024 tokens...
[Generate] Done: 55 frames generated.
[Metrics] Generate: prefill=274.399 ms, loop=3596.74 ms, total=3873.83 ms, avg=65.3952 ms/frame

After PR (FA disabled, chunk preset of 64 for Vulkan):

Generate] Prefilling 204 tokens...
[Model] Chunked prefill: 204 tokens in blocks of 64
[Generate] Generating (max 1024 tokens)...
[Generate] 50 / 1024 tokens...
[Generate] Done: 53 frames generated.
[Metrics] Generate: prefill=384.687 ms, loop=3426.41 ms, total=3813.65 ms, avg=64.6492 ms/frame

Before PR (FA enabled):

[Generate] Prefilling 204 tokens...
[Model] Chunked prefill: 204 tokens in blocks of 8
[Generate] Generating (max 1024 tokens)...
[Generate] 50 / 1024 tokens...
[Generate] Done: 60 frames generated.
[Metrics] Generate: prefill=872.475 ms, loop=3251.56 ms, total=4126.58 ms, avg=54.1927 ms/frame

After PR (FA enabled):

[Generate] Prefilling 204 tokens...
[Model] Chunked prefill: 204 tokens in blocks of 64
[Generate] Generating (max 1024 tokens)...
[Generate] 50 / 1024 tokens...
[Generate] Done: 58 frames generated.
[Metrics] Generate: prefill=378.19 ms, loop=3241.61 ms, total=3622.34 ms, avg=55.8898 ms/frame

Don't mind the small difference in average and loop. There was some background usage so not a neat/clean comparison, the massive 500 ms~ prefill difference is what I wanted to showcase.

I'm unable to test on cuda or metal, and some higher numbers causes segmentation faults on Vulkan perhaps a driver or ggml-vulkan issue. However those should be more stable?

Needs testing by anyone with the hardware to determine if they're stable and which values might benefit performance more.

Summary by CodeRabbit

Summary by CodeRabbit

  • Performance Improvements
    • Improved prompt processing by selecting more appropriate prefill chunk sizes based on available CPU threads and the active hardware backend.
    • Adjusted chunking behavior for special prompt scenarios to ensure correct and efficient prefill handling.

- The default adaptive_gpu_chunk is too conservative, For example it'd fire up 28 times for 204 tokens causing large delays in prefill with 800 ms~ prefill latency if used with FA.
- By using larger chunk sizes that are adjusted for each backend, we can safely decrease this delay (378 ms with FA) to increase performance.
@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: 0523ef65-cb2e-470e-81c4-4a986dc02c9a

📥 Commits

Reviewing files that changed from the base of the PR and between c3ffaf0 and 42e1a93.

📒 Files selected for processing (1)
  • src/s2_model.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/s2_model.cpp

📝 Walkthrough

Walkthrough

SlowARModel::prefill now selects adaptive chunk sizes from CPU thread parallelism, backend-specific defaults, and the single-token semantic prefill requirement before deciding whether to chunk tokens.

Changes

Prefill chunk sizing

Layer / File(s) Summary
Adaptive chunk selection and chunking decision
src/s2_model.cpp
Replaces GPU-layer-based sizing with a thread-scaled value bounded to 64–256, uses 512 for CUDA/Metal and 64 for Vulkan, forces 1 for the applicable semantic prompt case, and applies the result to chunk_tokens.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • rodrigomatta/s2.cpp#38: Changes Vulkan generation to call SlowARModel::prefill(...), which uses the updated chunk selection logic.
🚥 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 and concisely describes the main change: backend-specific prefill chunk sizing.
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/s2_model.cpp (1)

800-837: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Use runtime backend identification and fix unreachable CPU chunking.

The current chunk sizing implementation has two critical issues:

  1. Multi-backend segfault risk: Relying on compile-time macros (#if defined(GGML_USE_CUDA)) ignores the actual runtime backend. In a binary compiled with both CUDA and Vulkan, selecting Vulkan at runtime will incorrectly use the CUDA chunk size (512), causing segmentation faults.
  2. Dead-code CPU chunking: The CPU fallback block (#else) is nested inside the if (backend_gpu_ != nullptr ...) block, making it unreachable for CPU-only runs. As a result, CPU executions process all tokens in a single giant chunk, circumventing the intended memory bandwidth optimizations.

Pulling the adaptive_chunk calculation outside the GPU guard and checking the backend name at runtime resolves both issues. As an added benefit, wrapping n_threads with resolve_n_threads() ensures the chunk size scales correctly when n_threads is passed as 0 (auto-detect).

🔒️ Proposed fix to apply runtime checks and fix CPU sizing
-    int32_t chunk_tokens = n_tokens;
-    if (backend_gpu_ != nullptr && n_gpu_layers_ > 0) {
-        int32_t semantic_prompt_tokens = 0;
-        for (int32_t t = 0; t < n_tokens; ++t) {
-            const int32_t semantic = flat_tokens[static_cast<size_t>(t) * codebook_dim];
-            if (semantic >= hparams_.semantic_begin_id &&
-                semantic <= hparams_.semantic_end_id) {
-                semantic_prompt_tokens++;
-                if (semantic_prompt_tokens > 1) {
-                    break;
-                }
-            }
-        }
-
-        if (semantic_prompt_tokens > 1 &&
-            backend_requires_single_token_semantic_prefill(backend_gpu_)) {
-            chunk_tokens = 1;
-        } else {
-            // Backend-specific prefill chunk sizes for naive attention path.
-            // Vulkan: Limited by ggml_soft_max shader workgroup/shared memory limits
-            // CUDA/Metal: Highly optimized soft_max kernels handle large batches
-            // CPU: Memory bandwidth bound, scale with available parallelism
-            int32_t adaptive_gpu_chunk = 64; // Safe fallback
-
-#if defined(GGML_USE_CUDA) || defined(GGML_USE_METAL)
-            adaptive_gpu_chunk = 512;
-#elif defined(GGML_USE_VULKAN)
-            adaptive_gpu_chunk = 64;
-#else
-            // CPU or unknown backend: scale with thread count, capped at 256
-            adaptive_gpu_chunk = std::min(256, std::max(64, n_threads * 16));
-#endif
-
-            if (n_tokens > adaptive_gpu_chunk) {
-                chunk_tokens = adaptive_gpu_chunk;
-            }
-        }
-    }
+    // Backend-specific prefill chunk sizes for naive attention path.
+    // Vulkan: Limited by ggml_soft_max shader workgroup/shared memory limits
+    // CUDA/Metal: Highly optimized soft_max kernels handle large batches
+    // CPU: Memory bandwidth bound, scale with available parallelism
+    int32_t adaptive_chunk = std::min<int32_t>(256, std::max<int32_t>(64, resolve_n_threads(n_threads) * 16));
+
+    if (backend_gpu_ != nullptr && n_gpu_layers_ > 0) {
+        const char * backend_name = ggml_backend_name(backend_gpu_);
+        if (backend_name != nullptr) {
+            std::string name(backend_name);
+            if (name.find("CUDA") != std::string::npos ||
+                name.find("Metal") != std::string::npos) {
+                adaptive_chunk = 512;
+            } else if (name.find("Vulkan") != std::string::npos) {
+                adaptive_chunk = 64;
+            }
+        }
+
+        int32_t semantic_prompt_tokens = 0;
+        for (int32_t t = 0; t < n_tokens; ++t) {
+            const int32_t semantic = flat_tokens[static_cast<size_t>(t) * codebook_dim];
+            if (semantic >= hparams_.semantic_begin_id &&
+                semantic <= hparams_.semantic_end_id) {
+                semantic_prompt_tokens++;
+                if (semantic_prompt_tokens > 1) {
+                    break;
+                }
+            }
+        }
+
+        if (semantic_prompt_tokens > 1 &&
+            backend_requires_single_token_semantic_prefill(backend_gpu_)) {
+            adaptive_chunk = 1;
+        }
+    }
+
+    int32_t chunk_tokens = n_tokens;
+    if (n_tokens > adaptive_chunk) {
+        chunk_tokens = adaptive_chunk;
+    }
🤖 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 800 - 837, Move adaptive chunk-size
calculation out of the backend_gpu_ guard so CPU execution also applies it, and
replace compile-time GGML_USE_* selection with runtime backend identification
using the available backend name/type symbols. Select CUDA/Metal, Vulkan, or the
CPU fallback based on the active backend, and compute the CPU size with
resolve_n_threads() so an n_threads value of 0 is auto-detected. Preserve the
existing single-token semantic prefill override and apply the resulting
chunk_tokens limit consistently.
🤖 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.

Outside diff comments:
In `@src/s2_model.cpp`:
- Around line 800-837: Move adaptive chunk-size calculation out of the
backend_gpu_ guard so CPU execution also applies it, and replace compile-time
GGML_USE_* selection with runtime backend identification using the available
backend name/type symbols. Select CUDA/Metal, Vulkan, or the CPU fallback based
on the active backend, and compute the CPU size with resolve_n_threads() so an
n_threads value of 0 is auto-detected. Preserve the existing single-token
semantic prefill override and apply the resulting chunk_tokens limit
consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c78d33e-8578-4358-b2a9-de79ad48114f

📥 Commits

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

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

…hunking

- The CPU fallback was nested inside (backend_gpu_ != nullptr) guard. We can instead establish a CPU baseline first and adapt it to backend down the line.
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