Repository navigation
cuda qwen3_5: the prompt cache keeps KV pages on the device (TF_QWEN_KV_SHARE, #1156) - #1357
Conversation
a1804ab to
f3d5455
Compare
|
Follow-up: fault injection on the share's failure paths found two holes; both are fixed in a new commit on this PR, "cuda qwen3_5: TF_QWEN_KV_SHARE's pages through its failure paths". The new tests (cuda_prefix_fault_test.zig,
Stream sync in keep(): a 9k-token turn (8981 tokens, share on, fresh server, 2 runs per model) calls keep once, at 8192, and the wait there took 0 µs on 35B-A3B + MTP and on 27B + DFlash2. The chunk's forward has already waited for its last row's logits, so the stream is idle by the time keep runs. That is 0 % of TTFT (cold turn 4.76 s / 15.8 s; warm turn 0.70 s / 2.21 s, resumed at 8192). I kept the wait as the ordering guarantee and left it in place. Gates (35B-A3B + MTP and 27B + DFlash2, bf16 KV, --parallel 8 at 16384, TF_QWEN_KV_PAGES=256 TF_QWEN_KV_SHARE=8): share == the original stack's share and == every stream on pages without share (37/37 each, which includes the resumes at 8192, 8292, 8448 and 4796); drafted == plain (37/37); concurrent == solo (15/15); no lane page is held at idle. zig build test passes, and lean shows 0 new problems. |
|
Tested at
The last row is #1344's split: a batch with a paged stream takes the partial-only attention while a lone first stream takes the tensor-core path, and which near-ties part depends on how requests share rounds (6/8 on an earlier run of this stack at Timing, one stream: at this size the server has no room for a prompt cache on 24 GB ("no prompt cache" at startup), so SHARE is the only way this card resumes a turn. Turn 2 of the 6k-token chat: 1.34 s with SHARE (4,096 resumed) against 3.2 s with all pages and no cache (fp8: 1.40 s against 3.3 s); cold turn 1: 8.3 s against 3.2 s (fp8: 10.3 s against 3.3 s), the slower paged prompt path you describe. Not run: the 35B with MTP, more than three streams, windows past 8,192 (none fit beside the drafter with whole windows on this card), and the share's injected failure paths on the device. |
f3d5455 to
59258d2
Compare
|
Agreed to hold this until paged prompt chunks have a tensor-core path. Here is the memory receipt you asked for, plus what changed on the branch. Changes on the branch (no change to the share's code path):
Memory, share off vs on,
So at Sizing caveat found while measuring. With Your other two points:
|
|
The tensor-core path this was held for is up as #1418: a one-part paged prompt chunk now runs on paged twins of the contiguous prompt tiles, bit-equal to contiguous. Measured with this PR stacked on it (27B, TTFT s): 8k 6.38 / 6.50 with |
59258d2 to
735a7f2
Compare
|
Rebased on main |
|
Re-measured on this head ( |
|
Thank you for the memory receipt, the fault tests and the re-measurement. With #1373 in, the reason we held this is Two things to settle, here or in a follow-up. First, the sizing caveat you found: The gate table in the description is from main |
… in the disk facts, a shell-free disk test zig/build/cuda.zig takes the engine_api import exactly as ashhart#1357 writes it, one line with its comment, so the file stays at 600 lines and the two no longer conflict in family(). The disk identity's facts name whether int8 prompt projections are on. The restart test runs in std.testing.tmpDir and cuts a block or flips a state's byte through the file API: no shell, no fixed /tmp path, no Windows skip.
|
Thanks. Pushed
Checks: lean 0 new. |
… in the disk facts, a shell-free disk test zig/build/cuda.zig takes the engine_api import exactly as ashhart#1357 writes it, one line with its comment, so the file stays at 600 lines and the two no longer conflict in family(). The disk identity's facts name whether int8 prompt projections are on. The restart test runs in std.testing.tmpDir and cuts a block or flips a state's byte through the file API: no shell, no fixed /tmp path, no Windows skip.
|
Thanks @jschmied. Our GB10 run on the 27B: the default path is clean (0 of 20 prompt rows differ, sweep within ±0.4%), and with The cause: with the share on, Could you give the pool |
… disk facts, a shell-free disk test zig/build/cuda.zig takes the engine_api import exactly as #1357 writes it, one line with its comment, so the file stays at 600 lines and the two no longer conflict in family(). The disk identity's facts name whether int8 prompt projections are on. The restart test runs in std.testing.tmpDir and cuts a block or flips a state's byte through the file API: no shell, no fixed /tmp path, no Windows skip.
968c177 to
7db39e4
Compare
|
Thanks for catching it. Fixed and pushed as Fix. When every stream runs on pages ( Host test. Served, GB10. Qwen3.8-27B MLX-4bit + DFlash2, bf16 KV,
One cosmetic thing is left: the startup line still describes the plan ("the engine's own at a 16384-token window, 7 more taking 959 pages"). With the share on, all 8 run on those pages. I can change the wording here or in a follow-up, whichever you prefer. Checks on main |
|
Thanks @jschmied. Giving the pool a lane for each of One thing breaks: the new test doesn't link on Windows. On the startup line: since you'll push for the Windows fix anyway, please fold it in here. With the share on, it Then we'll rerun on a GB10: the 27B with DFlash2 and the 35B with MTP, at
|
|
Thanks. Both are in
Checks: |
|
Thanks @jschmied. Both are in on
They run once the box finishes its kernel contracts, and the results will be here. |
…KV_SHARE) qwen3_5's prompt cache copies a kept state's recurrences and KV rows to pinned host memory and back into the next turn's own pages, so an agent session holds its prefix twice while a turn runs (on GB10 the host copy is the same physical memory). TF_QWEN_KV_SHARE=n (with TF_QWEN_KV_PAGES dividing the 4096-row prompt chunk) puts the prompt cache over the pool's pages instead: core/prompt_radix.zig, the store the HIP backend uses, with up to n recurrent-state snapshots. A prompt pass keeps whole pages where it cuts a chunk anyway (the store's marks taken down to the chunk grid; the tree retains the stream's pages, ones it already held for the same tokens replace the stream's own); a later request resumes the deepest kept boundary, its lane adopts those pages and only the recurrent state comes back from the host. Store pages are reclaimable: a promise evicts idle ones before it fails. With the share every stream runs on pool pages (the reserved window stays idle, as with TF_QWEN_KV_PAGES_ALL), so a stream's bytes do not depend on the lane it got. Bytes: a resume point is a cold pass's chunk cut and every later chunk cuts where a cold pass does; adopted rows are a cold pass's, read in order. Kept only from prompt passes; prompts with images or request chunk starts keep their own grid and skip the store. Resumes land on the host cache's grid. The host makes no prompt cache of its own for such a family (cuda_kv_pool.ownCache) and its startup line says so; the qwen35 module imports the engine_api module the server links (build/cuda.zig). --learn-conversations writes the host cache's states, so it is refused with the share. GB10, TF_QWEN_KV_PAGES=256 TF_QWEN_KV_SHARE=8, --parallel 8 at 16384, Qwen3.6-35B-A3B + MTP and Qwen3.8-27B + DFlash2, bf16 and fp8 KV: every output equals main's with every stream on pages and no cache (37/37 each), resumed == fresh at 8192 (on the grid), 8292 (mid page), 8448 (a page off the grid) and 4796 (mid chunk), drafted == plain, concurrent == solo at 1/2/4/8, no lane page held at idle. Flags unset: outputs equal main's. TTFT of a warm second turn (9k-token prompt, fresh servers, 3 pairs): share off 0.20-0.21 s (35B) and 0.74-0.76 s (27B), share on 0.69-0.78 s and 2.19-2.22 s, and the cold first turn 1.7 -> 4.7-5.4 s and 7.3-7.4 -> 15.6-15.8 s: every stream on pages takes the scalar paged attention, where the reserved window takes the tensor-core prompt attention. Both resume at 8192.
Two holes the new fault tests found: - Prefix.open: begin retains the resumed pages for the lane, but a failed onGrid (an allocation, when a grid cut above the resume is left to keep) returned before openWith took them, so each kept one reference forever. The pool's idle census missed it (a page the store holds twice counts as kept); the store's pages never came back. They go back there now. - Pages.swap moved the lane's references to the store's pages before the table upload; a failed upload left the device table on pages the lane had given back, and the pass went on (keep's errors are logged). The table goes up first; QwenLaneTable, its upload failed, ends the stream with the lane still holding its own pages. cuda_prefix_fault_test.zig runs the real Pages and Prefix on the fake device (test-qwen35-share-faults-host): every allocation of a resume failing in turn, each wait of a resume failing (the adopted pages' table, the state put back), no free lane, a failed promise, and a swap's failed upload; after each, no page is leased or promised and none is held twice, and after the store closes every page is free and every pinned copy gone. The fake device holds several pinned buffers, copies to the host and fails the nth wait.
…tream TF_QWEN_KV_SHARE=8 with 8 live conversations resumed nothing: each stream's saves evicted the others' before a resume reached them. The server now logs a warning when n is below 4 x --parallel and names the value to set. n keeps its meaning (the snapshots the store keeps); nothing is sized behind it.
… of --parallel's streams With TF_QWEN_KV_SHARE (or TF_QWEN_KV_PAGES_ALL's checks) attach sets paged_only, so every stream runs on pool pages and the reserved window stays idle, but the pool was still made with the plan's --parallel - 1 lanes and fits refuses a stream once they are taken: --parallel 8 served 7 at once, the eighth request waiting a whole reply. The pool now gets the idle window's stream as one more lane in that mode. cuda_admit_test.zig (in test-qwen35-share-faults-host) attaches the pool as the server does at --parallel 2 and 8 and opens streams while fits admits them: N with the share and with the checks; N - 1 before.
…s.env, and the startup line says every stream is on pages The test declared setenv and unsetenv as extern C functions, which lld-link can't resolve on Windows; sys.env has both on every platform. With TF_QWEN_KV_SHARE or TF_QWEN_KV_PAGES_ALL every stream runs on the pool's pages, so the startup line now says all of --parallel's streams are on pages and the engine's own window is idle.
65ef4ff to
fb1d3e8
Compare
Six 8k conversations sent twice at --parallel 4 resumed none at n=4 and all at n=16, with pages to spare: the store limits states as well as pages, and each new conversation evicted the state the next turn wanted. The server now logs a warning below 4 x --parallel and names the value to set, as ashhart#1357 does for TF_QWEN_KV_SHARE.
|
Thank you @jschmied. This is in TensorFold 1.2.0, released today: https://github.com/ashhart/TensorFold/releases/tag/v1.2.0 It landed with your commits under your own name, so you're listed as a TensorFold contributor, and it's credited to you in the release notes and the changelog. |
Six 8k conversations sent twice at --parallel 4 resumed none at n=4 and all at n=16, with pages to spare: the store limits states as well as pages, and each new conversation evicted the state the next turn wanted. The server now logs a warning below 4 x --parallel and names the value to set, as ashhart#1357 does for TF_QWEN_KV_SHARE.
Six 8k conversations sent twice at --parallel 4 resumed none at n=4 and all at n=16, with pages to spare: the store limits states as well as pages, and each new conversation evicted the state the next turn wanted. The server now logs a warning below 4 x --parallel and names the value to set, as ashhart#1357 does for TF_QWEN_KV_SHARE.
Six 8k conversations sent twice at --parallel 4 resumed none at n=4 and all at n=16, with pages to spare: the store limits states as well as pages, and each new conversation evicted the state the next turn wanted. The server now logs a warning below 4 x --parallel and names the value to set, as #1357 does for TF_QWEN_KV_SHARE.
Part of #1156 (was
0566be8athere). Stacked on #1356; only the last commit is new here.TF_QWEN_KV_SHARE=n: the prompt cache keeps KV as pool pages on the device instead of host copies. It is off by default. Changed in the port to current main:TF_QWEN_KV_PAGES_ALL), and the reserved window stays idle;cuda_prefill.zigandcuda_prefix.zig.Gates on GB10: Qwen3.6-35B-A3B MLX-4bit with
--drafter(its MTP layer) and Qwen3.8-27B MLX-4bit with DFlash2, bf16 and fp8 KV,--parallel 8, 16384,TF_QWEN_KV_PAGES=256, 37 greedy outputs per arm:On main
21db2c4c9,zig build test -Dnvcc=/usr/local/cuda/bin/nvccexits 0, and lean shows 0 new.Warm second-turn TTFT at
735a7f20a, 9k-token chat, fresh servers alternating off/on, 3 pairs (both resume at 8192), bf16 KV:Turn-2 outputs are equal off and on (same hash in all 6 runs per model).
Since main
084333955(#1373) paged prompt chunks run on the tensor-core tiles, so the share no longer costs prefill time. Turn 2 is level with the share off (within 5 ms), and cold turn 1 is 3–6 % faster with it on. The earlier numbers (turn 2 0.69–0.78 s and 2.19–2.22 s, turn 1 4.7–5.4 s and 15.7 s) came from the scalar paged attention.