ggml-cuda : round growing pool requests up to a power of two - #78
Open
michal-zurkowski wants to merge 1 commit into
Open
michal-zurkowski wants to merge 1 commit into
michal-zurkowski wants to merge 1 commit into
Conversation
ggml_cuda_pool_leg reuses a cached block only when it is >= the request, so a cached block smaller than the request can serve neither this request nor any larger later one. A caller whose request grows every step therefore misses the cache and strands its predecessor once per step, for as long as it grows. qsa-prefill.cu is such a caller: it sizes packed K/V scratch from the KV cache, which gains one ubatch per prefill step (8 MiB per 8192-token ubatch). Round the request up to a power of two when the pool's largest block sits just below it, which is that pattern's signature, so the series reuses one block per octave. Nothing is freed, so no address the pool has handed out is invalidated; that matters because pool scratch is returned to the pool during CUDA graph capture while its pointer is already baked into a captured kernel node. The OOM retry falls back to the unrounded size, so it can never fail where it would previously have succeeded. f = 2 is the optimum, not just the obvious choice: a series growing to M costs about f*M live plus M/(f-1) stranded, and f + 1/(f-1) is minimised at f = 2. Assisted-by: Claude Opus 5 (Claude Code) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
michal-zurkowski
marked this pull request as ready for review
September 20, 2026 12:54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
ggml_cuda_pool_leg::allocreuses a cached block only when it is>= size, so a smaller cached block can serve neither this request nor any larger later one, and nothing shrinks the pool in normal operation. A caller whose request grows every step therefore misses the cache and strands its predecessor once per step.qsa-prefill.cuis such a caller: it sizes packed K/V scratch from the KV cache, which gains one ubatch per prefill step (8 MiB per 8192-token ubatch).This PR rounds the request up to a power of two when the pool's largest block sits just below it, so the series reuses one block per octave instead. One file, +21/-1. Nothing is freed.
Mechanism
Not freeing is the point: pool scratch is returned to the pool during a CUDA graph capture while its pointer is already baked into a captured kernel node (
ggml-cuda.cu:5244,:5502), and replay never re-enters the allocator, so releasing a cached block could invalidate a pointer a later replay still uses. The gate matters too: rounding unconditionally also inflates large batch-sized scratch, which at ubatch 16384 cancelled the entire saving.f = 2is the optimum rather than the obvious choice, since a series growing toMcosts aboutf*Mlive plusM/(f-1)stranded andf + 1/(f-1)is minimised atf = 2. The OOM retry falls back to the unrounded size, so it can never fail where it would previously have succeeded.Measurements
Both binaries are
masterplus a temporary patch that widens QSA block indices to 32 bits, which is what lets the prefill reach 512Ki; that patch is not part of this PR and is identical in both. They differ only by this commit: separate source and build directories, prefixes and RPATHs, same session, no runtime switch.Memory
GTT growth during one prefill, MiB.
llama-server, native 262144 context for the 256Ki rows, 524288 with YaRN x2 for the 512Ki rows.athe unpatched run never finished: it was stopped by the probe's own guard when free system memory fell below 2048 MiB, between 0.58 and 0.75 of the prompt. Those figures are lower bounds, so the saving is understated. Every PR run completed the full 504,281-token prefill, with 12.8 to 19.1 GB of GTT still free. GTT and system RAM are one pool on this part, so the ceiling is real.Unpatched cost falls as ubatch grows, because a larger ubatch means fewer, larger growth steps. The PR column does not move, because its footprint is set by the number of power-of-two size classes, not by how many steps were taken to reach them.
Memory, multi-turn
The same effect with the cache grown by conversation turns instead of one long prefill, at ubatch 16384 and native context,
cache_prompton. Turn sizes are generated once and replayed against both builds, so the two runs see an identical sequence. Measured prefix reuse was 97.5% and 98.4%.A turn smaller than one ubatch makes the turn, not the ubatch, the growth step. At ubatch 16384 the unpatched build goes from 4,146 MiB over 16 steps to 12,792 MiB over 80, while the PR stays at 2,476.
Throughput
Prompt processing, tok/s, 256Ki prefill. The 512Ki rows have no unpatched figure because no unpatched run finished.
Correctness
test-backend-ops, full suite, both builds: 29695/29695, rc=0 each.Complete final logits after prefill, compared with
cmpbetween the two builds: byte-identical on all eight prompts, token ids identical. Four corpora (tests/corpus/correctness-{prose,code,structured,numeric}.txt) at native length, and each repeated deterministically to 210,000 chars, giving prompt depths of 40,347 / 72,811 / 85,508 / 170,815 tokens.Requirements
GGML_HIP_NO_VMM=ON, gfx1151), the only caller with a growing request is this fork's QSA prefill, and GTT and system RAM being one pool is what turns stranding into a failed run.cudaFree, which is unsafe against CUDA graph replay; an env-var gate that was only test scaffolding; an unconditional rounding that inflated batch-sized scratch and cancelled its own saving at ubatch 16384; and a first round of measurements taken against amasterthat was 86 commits stale. Each of those was caught by the repository owner or by review, and redone. The owner set the objective, chose the base, and directed the corrections.