Repository navigation
core kv_pool: an asked prompt cache comes before every window but the engine's own - #2220
Merged
Merged
Conversation
Bizuayeu
marked this pull request as ready for review
November 5, 2026 03:51
Owner
|
Thanks @Bizuayeu, and @MiaAI-Lab for finding it. Unset, the order is main's, and a named cache now gets what it names. It's in the queue; we'll add the changelog line, which says that a command already passing |
ashhart
added a commit
to Bizuayeu/TensorFold
that referenced
this pull request
Nov 5, 2026
ashhart
force-pushed
the
pr/kv-pool-cache-ask
branch
from
November 5, 2026 07:53
4b59f59 to
6c51cbe
Compare
… engine's own With the engine's own rows on pages, plan() sets as many windows as the room holds aside before the prompt cache (553c8bb), so the pool never holds fewer rows than windows in the same room. The cache gets what those windows leave, and a room that grows past the next window's threshold leaves it next to nothing: at TP=3 with a 1M window and four streams (291 MiB fixed each), the planner gives the default 16 GiB cache 12.5 GiB at a 38.8 GiB room and 0.5 GiB at 39.6. --prompt-cache-gib could not help, since its bytes came after those windows too. A cache --prompt-cache-gib names (Budget.asked) now comes after the engine's own window and a fixed part and page for each other stream, before any other window, and the pool takes the rest; unasked, the windows still come first. holds() counts the same way. Plan.taken is what the windows took from the cache, and the warning that a cache cannot keep one state names it and --prompt-cache-gib instead of saying the flag only caps the budget. Mia's AI Lab found the drop on their GLM-5.3-Flash recipe (MiaAI-Lab/GLM-5.3-Flash-DGX-Sparks-1104: no prompt cache at a 19.8 GiB room, 3.6 GiB at 18.9) and traced it to held(), with a cache set aside before the extra windows as the fix.
With the engine's own rows on pages, the startup line said the pool's pages were "enough for one at a N-token window" whatever they held: docs/receipts/glm-kv-pages-8e705835 shows 803 pages at a 131072-token window, three windows, and at TP=3 with a 1M window and four streams the planner gives 6,147 pages, three. The line names the whole windows the pages hold, the plan's window and verify rows each.
ashhart
force-pushed
the
pr/kv-pool-cache-ask
branch
from
November 5, 2026 08:51
6c51cbe to
3f31502
Compare
Owner
|
Thank you @Bizuayeu. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes. |
Owner
|
Thanks @Bizuayeu and @MiaAI-Lab! This is in TensorFold 1.2.3, out now: https://github.com/ashhart/TensorFold/releases/tag/v1.2.3 |
This was referenced Nov 5, 2026
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.
With the engine's own rows on pages, the prompt cache gets what the windows the room holds leave, so a room just past a
window's threshold leaves it next to nothing. By the planner's arithmetic on the new test's layout, at TP=3 with a 1M
window and four streams (291 MiB fixed each):
--prompt-cache-gib 4on main--prompt-cache-gib 4on this headOn two GB10s at TP=2 with a 262,144-token window and four streams, the prompt cache off the pool's pages so the startup
line gives its size (the room each start found is in the first column):
token_sha256(128 greedy tokens)44be1c0c6b3d3911--prompt-cache-gib 4(16.1 GiB)44be1c0c6b3d391144be1c0c6b3d3911--prompt-cache-gib 4(16.1 GiB)44be1c0c6b3d3911Unasked, the four windows still come first and the cache gets what they leave; asked, main still gave it what the
windows left, and this head gives it the 4 GiB and the pool the rest.
The startup line counts the whole windows the pool's pages hold: a 262,144-token window and a decode window's 8 rows
take 513 pages of 512 rows, so 2,052 pages are 4 windows and 1,785 are 3.48, shown as 3. All four streams run either
way, each taking pages as it needs them; the count is how many can be at a whole window at once.
What this changes
553c8bb set as many windows as the room holds aside before the prompt cache, so the pool never holds fewer rows
than windows in the same room, and left
--prompt-cache-gibafter them too. This keeps that order when the cache is notasked for, and lets an operator who names a size trade the extra windows for it: a cache
--prompt-cache-gibnames(
Budget.asked) comes after the engine's own window and a fixed part and page for each other stream, before any otherwindow, and the pool takes the rest, so the streams still take pages as they need them.
holds()counts the same way,and
Plan.takenis what the windows took from the cache.The one place this changes 553c8bb's order is an asked cache, and so the one test it changes is its edge in
cuda_kv_pool.zig: the unasked 16 GiB edge keeps the four windows first, and the asked 4 GiB edge is the engine'swindow and a fixed part and page each, as before 553c8bb.
kv_pool_test.zig's tests of 553c8bb pass unchanged.The warning that a cache cannot keep one state said
--prompt-cache-gibonly caps the budget; on pages whose windowstook the cache first, it now names what they took and that
--prompt-cache-gib Nkeeps N GiB before them. A secondcommit makes the startup line count the whole windows the pages hold, where it said "enough for one" whatever they held.
@MiaAI-Lab found the drop on their GLM-5.3-Flash recipe
(MiaAI-Lab/GLM-5.3-Flash-DGX-Sparks-1104: no prompt cache
at a 19.8 GiB room, 3.6 GiB at 18.9) and traced it to
held(), with a cache set aside before the extra windows as thefix.
c085aab04, where the receipt ran; it merges cleanly with main at8abb30f6f), two commits(
3a52172fb,4b59f59cb).atc overlap --json(2026-11-05 03:10 UTC, basefb0895099;complete: false, as atc could not read every open pullrequest's files): 31 open pull requests share files with this one. atc test-merged ten: nine merge cleanly and glm:
TF_GLM_KV_LIVE_HALF=1keeps the live half of the KDA state alone in a kept prompt state #2181conflicts in
docs/recipes/glm-5.3-flash-cuda.md, where each adds a bullet at the same place, so both stay. Itest-merged the other 21 with
git merge-tree: sixteen merge cleanly in this branch's files, and Muse Spark Open on CUDA: EXL3, DFlash2, two GPUs #1295, lfm2_moe CUDA: attention pages from the shared KV pool, and the prompt cache (stacked on #1492) #1503, lfm3: serve LFM3-1.7B on CUDA: NVFP4 and FP8 weight-only, MLX 4-bit (stacked on #1513, #1492 and #1503) #1508,lfm3 CUDA: serve the block FP8 export weight-only and the MLX 4-bit export as stored (stacked on #1508) #1634 and lfm3-vl: chat-template thinking suffix, fitGrid geometry, and deployment recipe #2045 conflict in
native/cuda.zigas they already do with main (one conflict block each, with this branchor without it; this branch's one line there merges).
Checks on this head
4b59f59cbzig build test-kv-pool38/38 (37/37 on the base; the new test is in thisstep),
test-cuda-host4,795 of 4,818 (23 skipped),test2,215 of 2,251 (36 skipped; Zig's cache kept the stepsanother run had built from the same inputs),
test-glm-prompts-host7/7,test-glm-prompt-settings-host6/6,test-glm-tp3-host8/8,test-glm-prompt-arena-host1/1,test-glm-startup-host,test-glm-pages-host,test-golden,compile-cuda-tests(also aarch64),compile-config-server: no failures.zig fmt --check build.zig zigandgit diff --check: clean.release-paths).
arithmetic there; the device run shows the same order at a 256K window); the other families whose engine's own rows
are pages, whose checkpoints this pair does not hold.
Receipt
c085aab04and this head, each built with Zig 0.17.0 and nvcc 13.3.73 for sm_121; two GB10s (128 GBunified memory each, Ubuntu 24.04.5, kernel 7.0.0-1019-nvidia, driver 580.178.04) at TP=2 over RoCE;
Bizuayeu/GLM-5.3-Flash-XSFP4at88736f91;--context 262144 --parallel 4,TF_KV_PAGESunset (pages),TF_GLM_KV_SHAREunset.--prompt-cache-gibis given, for the familieswhose engine's own rows are pages (GLM at two or more ranks, Gemma 4, Kolibri-1, Dory, Spark2.5, and Qwen3.5 with
TF_QWEN_KV_PAGES_ALL=1); Metal's pool plans (metal_admit.zig,qwen35.zig) pass no cache and are unchanged. Run:GLM at TP=2 on two GB10s (sm_121), unasked and asked. Not run: the other families, other SMs, TP=3.
token_sha25644be1c0c6b3d3911). On this head with--prompt-cache-gib 4:bench_concurrent.py --tokens 256 --reps 3 --temperatures 0 --levels 1,4 --alone --serialfound every reply at one and four streams equal to its request alone (code and chat, 3 and 12 of 3 and 12) and the
alone runs equal to
"draft": false; chat prompts of about 7K, 15K and 30K tokens and the decode check's count prompt(256 greedy tokens) gave the same
token_sha256drafted and with"draft": false, the second resuming the first'skept prompt (6,144 to 28,672 tokens cached).
bench_cache.py's raw-token completions found no kept state at thesesettings on main or on this head (their repeats ran cold, with the first request's tokens), so the resumed check is the
chat one.
Raw results
Startup lines (rank 0), TP=2,
--context 262144 --parallel 4,TF_GLM_KV_SHAREunset:bench_concurrent.py --tokens 256 --reps 3 --temperatures 0 --levels 1,4 --alone --serial, this head with--prompt-cache-gib 4:bench_cache.py --require-cold --require-reuseas each server's first requests,--prompt-cache-gib 4: no kept state for these raw-token completions on main or on this head:Checklist
kv_pool_test.zigandcuda_cache_capacity_test.zigon the base fail to build:kv_pool.Planhas notakenthereand
warnCapacitytakes one argument fewer; they have no dependency to skip on)."draft": false, a resumed prompt equals a fresh one, and concurrent equals solo (Exactnessabove).
on rows changes).
--drafter,no slower than without it, greedy and sampled (the plan sizes memory only: no kernel, round or drafter path changes).