Repository navigation
qwen3_5 cuda: warm turns prefill the next turn's prompt and keep only a 4096-row-multiple end - #2824
Conversation
|
Thanks @devopsfortunato, this is a careful find. The
We'll write the CHANGELOG line when it lands.
|
|
CI on head Local runs in this branch's tree, for the record:
|
|
@devopsfortunato thanks for the green CI. Tonight #2838 landed the Your enablement is the copy we'll land, and #2873's foreground yield goes first: without it, a request arriving during a warm pass waits for the whole prefill. Warm turns follow the prompt cache, as on Nemotron, so no new switch is needed. Our other asks still stand (the body notes, #2686 first, receipts), and please check that
|
|
@devopsfortunato a correction to my 11 Nov comment, sorry: #2838 has not landed yet. It's in the landing queue (its checks are green), and the The other asks from that comment are unchanged.
|
5a4867d to
e6af058
Compare
|
@devopsfortunato your new head (
|
|
Rebased and reshaped per your comment. Head is The Line budget. Where the wrappers sit is not arbitrary and I measured it: Body notes are in: what a warm pass actually keeps here (only at an exact multiple of the 4096-row grid, a pass that would keep nothing is cancelled before it runs, so a turn under 4096 rows keeps nothing and a 5000-row turn keeps its 4096 mark but not its end), that #2686 first is stated in the stack section, and #2804 is Receipts are the one thing I cannot post yet. The GB10 here is still serving another model, so Host side, all with |
|
Thanks @devopsfortunato, the reshaped branch reads well. We have no more code asks. Here's what we checked on
To answer your two questions:
Please post the numbers for both sides here. Once they match, we'll write the changelog line crediting you.
|
GB10 receipt — warm turns on the 4096 boundary (dense)The device run the 11 Nov review asked for, with one substitution: the checkpoint staged on this machine is Qwen3.5-2B MLX 4-bit (
Not covered here: the Qwen3.8-27B NVFP4 cell (no |
|
@ashhart @devopsfortunato the receipt above covers the dense path on Qwen3.5-2B. If the larger cells help before landing, I can run the same |
|
Yes please, @jschmied — those are exactly the two cells I flagged as not covered. main |
|
@devopsfortunato @ashhart here are the two larger cells on a GB10, run as main
Qwen3.6-35B-A3B (
Qwen3.8-27B dense,
|
|
Thanks @jschmied for the two larger cells, and @devopsfortunato for asking for them. They settle the order. On @devopsfortunato, one question: your 2B run kept 4096 on what should be the same head, where the 27B and 35B kept nothing. Was #2838 merged into that build, or was it a different binary? The served binary's md5 would settle it. What's left is unchanged:
The cell for a request that arrives during a warm pass belongs to #2873.
|
@ashhart answering your question: #2838 was not in that build. The branch worktree served So jschmied's larger cells are right and my 2B "qualified: true" line is wrong. I could not reproduce it, and with the logs in hand I can see why it should not happen at all:
One small lesson for the tool if it helps: a |
… a 4096-row-multiple end The Qwen3.5 CUDA family declared no warm pass, so the server never ran one for it. Both entry points now declare `warm_quiet` (150 ms) and `warm_usable`, `cuda_snaps.cacheRules` reads a family's `warm_usable`, and a CUDA host takes the family's own wait instead of Nemotron's. What the pass keeps is the narrow part: a request's chunk starts never reach its own end, and this family's grid is 4096 rows, so the end survives only when the prompt is an exact multiple of it. A turn under 4096 rows keeps nothing and is cancelled before it runs; a 5000-row turn keeps its 4096 mark but not its end. `prefillFn`'s whole text pass is fixed on main (ashhart#2838); this branch carries the enablement only, and ashhart#2873's foreground yield goes first. Host tests: the declarations on both entry points, and a family's warm rules and its own wait. Refs ashhart#2812
e6af058 to
f5d9ad1
Compare
|
Rebased onto What I re-verified on the rebased head (fresh builds,
|
|
CI is now green on rebased head I also corrected the receipt record in place: my original positive 2B result is withdrawn, and the earlier proposed race explanation was not demonstrated. The retained-binary checksums and later #2838 is in the new base. #2686 and #2873 are still open; after they land, your requested fresh dense/MoE comparisons and the Clef request remain outstanding. This rebase/green CI is not a claim that those device requirements are complete. |
|
Thanks @devopsfortunato, that is a model correction. Answering the binary question with the md5s, rerunning the cell three times on each build, and withdrawing the 2B line is exactly how we want receipts handled, and the false keep you describe is a good find for the tool. We rechecked the rebased head here. It applies to today's main with one adjacent-lines hunk in cuda_snaps.zig, which we resolved by hand, and the two new warm steps plus the prefill, request-starts and cost-open steps pass. Lean adds nothing and cuda_native.zig stays under the cap. Nothing more from you right now. This waits on the order we set: when #2686 and #2873 are in we will post here, then the fresh 27B and 35B comparisons and the one Clef readout on that head, and it queues.
|
What this changes
cache had the warm-turn path (a prompt-only pass over the next turn's prompt,
lane_host_warm.zig:6)and Nemotron was the only family using it.
qwen3_5andqwen3_5_moedeclarewarm_quiet(150 ms) andwarm_usable,cuda_snaps.cacheRulesreads a family'swarm_usable, and the CUDA host takes the family's own waitinstead of Nemotron's constant. No other CUDA family declares either, so their rules stay
warm = .off,warm_usable = false.Store.usableaccepts that position (prompt_cache.zig:216-218) — for a planned family, only at achunk start. A request's starts never reach its own end (
chunk_plan.zig:50returns beforeids.len),and this family's grid is
snap_grid = E.max_rows= 4096 rows (cuda_native.zig:55), so in practicethe end is kept only when the prompt is an exact multiple of 4096 rows. A pass that would keep nothing
is cancelled before it runs (
lane_host_warm.zig:9), so a turn under 4096 rows keeps nothing at all,and a turn of, say, 5000 rows keeps its 4096 mark but not its end. The gain is a follow-up that
resumes from the last full 4096-row row rather than recomputing it, not a resume from the prompt end.
prefillFndispatch fix and its host test are Qwen 3.5 CUDA: a stream's whole prompt fill runs once (fixes #2804) #2838's, andthe foreground yield that stops a request arriving during a warm pass waiting for a whole prefill is
fix(qwen3.5/cuda): yield background prefill between chunks #2873's. This branch no longer touches
prefillFn.--prompt-cache-diskwithTF_QWEN_KV_PAGES_ALLrefusing at startup) was the same
prefillFnshort-circuit and Qwen 3.5 CUDA: a stream's whole prompt fill runs once (fixes #2804) #2838 fixed it, not here.223da7659, which includes Qwen 3.5 CUDA: a stream's whole prompt fill runs once (fixes #2804) #2838. Landing prerequisites still open: server: a warm pass keeps the state the next turn resumes (chunk cut, tool-result opener, reasoning) #2686 and fix(qwen3.5/cuda): yield background prefill between chunks #2873. This branch contains the enablement only; nothing inprefillFnchanges here. This rebase does not mean the remaining landing prerequisites or requested receipts are complete.atc overlap --jsonon this head: 51 overlaps among the open pull requests sharing these files, noconflicts; 7 merge clean (1295, 1632, 1411, 1879, 1767, 2156, 2054) and the rest report
unknownbecause the run hit its own limits (
pullRequestsLimit,pullFilesUnavailable,timeLimit).atc whoon the most-touched files shows no active agent and no pull-request holder.
Checks on this head
f5d9ad1b8, one commit onupstream/main223da7659.prefillFnis main'sprefill.whole; this diff is the declarations, the cache rules, the host wait and their tests.python3 tools/zig/lean_check.py:341 problems / 6309 fileson223da7659and341 / 6310at head — no new finding.zig fmt --checkon the changed files andgit diff --checkareclean.
zig/src/families/qwen3_5/cuda_native.zigis 597 lines against the checker's 600-line cap; main's copy isexactly 600, so the four declaration lines had to be paid for. This branch pays by moving the two
Qwen share faults:/Qwen pages default:wrappers at the file's tail intocuda_admit_test.zig, thehelper that already owns
admitsandcapsand already importscuda_native.zig, and leaving oneanonymous wrapper in
cuda_native.zigto keep both edges alive. Where the wrappers sit is measured, notassumed: hanging them off
cuda.ziginstead hides them from the roots that reachcuda_native.zigdirectly, and
test-qwen35-modelopt-admission-hostthen silently loses the ten tests those wrappersreach, because a Zig 0.17 test filter that matches nothing exits 0.
zig build -Dfixed-cpu <step> --summary all, all exit 0:test-qwen35-warm-turns-host16/16 andtest-cuda-warm-rules-host11/11 (the two new steps)test-qwen35-prefill-host19/19,test-qwen35-request-starts-host17/17,test-qwen35-cost-open-host4/4test-qwen35-modelopt-admission-host145/145 here and 145/145 on a checkout of223da7659, so thewrapper move changes no test count.
test-cuda-hostwas not re-run after the rebase; it was clean on the pre-rebase head (0 tests failed,5983/6013passed, 30 skipped, exit 0) and CI runs it on this head.f5d9ad1b8: 8 checks pass, 2 scheduled-only jobs skipped. No GPU run and format run are green.model keeps a row; with Qwen 3.5 CUDA: a stream's whole prompt fill runs once (fixes #2804) #2838 on top both keep the 4096 row, continuation prefill 0.653→0.045 s (35B
MoE) and ~3.03→0.116 s (27B), every
token_sha256andoutput_sha256equal to main. My own 2B cell iswithdrawn: the original positive result was not reproduced. Three later runs on the retained binary returned
cached_tokens: 0withUnknownStreamwhen saving the warm state. The cause of the original discrepancy is unproven; the earlier race explanation was speculation, not a demonstrated mechanism.Receipt
eab305db0, old heade6af05814, and that head with Qwen 3.5 CUDA: a stream's whole prompt fill runs once (fixes #2804) #2838 applied. Those are not measurements of current headf5d9ad1b8. After server: a warm pass keeps the state the next turn resumes (chunk cut, tool-result opener, reasoning) #2686 and fix(qwen3.5/cuda): yield background prefill between chunks #2873 land, the maintainer still requests a rebase and fresh main-versus-head dense/MoE comparisons, plus one Clef readout request that must not fail withInvalidQwenPrompt. My original positive 2B receipt remains withdrawn.this family. No other family changes.
--prompt-cache-gib 0, as on Nemotron;TF_QWEN_REQUEST_STARTSstill governs message starts and isunchanged.
warm_turnsre-routes everymax_tokens: 0request through the warm pass(
lane_host.zig:403), and that pass ends the request.cancelledwhen it would keep nothing(
lane_host.zig:493). So a client's explicit prefill-only call on Qwen3.5 CUDA with a prompt under4096 rows now returns cancelled instead of prefilled. That is what the capability already means on
Nemotron, and embeddings and readouts are exempt (
engine_api.zig:100), but it is user-visible.warm_quietalso sets the cache rulewarm = .waits, solookupUserAtnow prices andkeeps an endpoint that is the prompt end for prompt-only requests too (the same rule Nemotron
already has), not only for a reply's warm pass. With
snap_lookahead = 0the only reachabledifference is that endpoint charge, and only when the history is exactly the prompt and 4096-aligned;
cold-pass cache behavior for Qwen is otherwise unchanged.
warm_usable = truethe end is kept only whereStore.usableaccepts it, as the third bulletabove spells out: an exact multiple of the 4096-row grid, or a request chunk start below the end.
Interior grid marks go through
lane_cache_queue.keep, which never consultswarm_usable— that isdeliberate, since an off-grid end is not resumable and keeping it would only hold budget.
warm_quietalso keeps the family off the KV ledger (cuda_ledger.wantedrefuses anyfamily that declares it). A no-op today: no CUDA family declares
per_request, sowantedalreadyreturned false. Noted in the comment where it is enforced.
what a turn keeps.
--prompt-cache-gibis set.Kernel contract, only when a GPU kernel is new or changed
n/a — no kernel is new or changed.
Checklist
two new steps check declarations and rules that do not exist on main, so they do not compile there.
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. — full matrix not verified on this head; historical main-versus-branch hash equality alone does not prove these contracts.