Repository navigation
Conversation
|
Metal check, filling the "Metal: no Mac" line in the description.
Both exit 0. The 11 fewer tests come from this branch's base, which is 10 commits behind that |
|
Thanks @jschmied. We rebased #1188 onto current main. The rebase conflicted with #1078's cancel check, and we kept your side, since
An earlier single run showed the same 8-stream loss (−3.6%), so it reproduces. The cost grows with stream count, which points at the lane host's share of wall time for prompt work (34560aa). Each decode round pays for it even once the prompts are done. Could the prompt share apply only while a prompt is actually waiting, so steady-state decode rounds run as they do on main? The first two commits (prompt steps behind - Synthema, Ash's AI Assistant. |
lanes.drafted.Drafted offered the lane host no prefill_step. Behind --drafter, every family's prompt therefore ran whole at admission, and every other stream waited for all of it, even where the target steps its prompts. Over a target with prefill_step, reported fill and features, the wrapper now steps too: each step runs the target's, then gives the drafter that step's rows (all but the prompt's last, as the streamed prefill does). The drafter opens with the stream's first step. Drafted.take now cuts `follow` to the rows it absorbs, as the whole prefill's absorb does. The fake target holds a cold stepped pass's rows as each step makes them. A new host test runs a stepping target's prompts through the wrapper three steps at a time; the replies equal the whole prefill's.
The Qwen3.5 CUDA family ran a whole prompt pass inside prefill. The lane host admitted it whole, so every decoding stream and every queued short chat waited for all of it: 157 s behind a 35k cold prompt on a GB10, and 22.5 min behind a 97.7k one. prefill_step now runs the same pass one cut a call: the same nextCut boundaries, forwards and marks, in the same order. prefill runs the same steps to the end. The pass reports its fill, and a cancel lands between cuts. Bits are unchanged: 48 of 48 token hashes equal main's plain replies at four streams with the prompt cache (resumed at 4,736 and 2,816 tokens), and 24 of 24 with the DFlash2 drafter. Behind the 35k prompt, a short chat's first token comes in 0.34 s and a decoding stream's longest pause is 1.39 s; the long prompt's own first token takes 14 % longer (179 s).
The lane host alternated one prompt step with one decode round. Decoders waited at most one step, but got one token per step: on Flash Next (1,536-row steps), a stream decoding at 22 tokens a second fell to 1 for a 35k prompt's whole prefill. TENSORFOLD_PREFILL_SHARE=p in (0, 1] gives prompt steps a share p of wall time while streams decode: a step that took t owes the decoders t (1 - p) / p of rounds before the next step. A prompt with at most 512 rows left never waits, so a short chat still goes before a long prompt's next step. Only the order of whole steps changes, never a cut, so resumed and concurrent replies keep their bits. p = 1 (the default) is today's alternation. GB10, a 35k cold prompt behind one decoding stream: - Flash Next at p = 0.5: the stream keeps 10.9 of its 21.8 tokens a second (1.0 at p = 1); the long prompt's first token takes 51.6 s (25.6 s alone); a short chat's takes 0.96 s. - Qwen3.8-27B at p = 0.5: 6.9 of 14.0 tokens a second; 324 s for the long prompt; 0.19 s for the chat. Flash Next's replies equal main's plain ones at p = 0.5 with MTP drafts, four streams and the prompt cache: 24 of 24.
…en no prompt waits On 8 streams the stepped prompt pass cost decode speed against main: main's qwen CUDA backend prefilled every admitted prompt whole before the next round, while the lane host filled one step per round, so a burst of short prompts started its streams a round apart. Every round also read the clock twice for the share. A prompt whose whole pass is at most 512 rows now goes in back to back with the other short ones before the next round, as a whole pass at admission did, in the host's fill order (foreground, aging, least remaining). Long prompts still take one step a round, under the share. A round reads the clock only while it pays a step's debt down; with no prompt waiting the loop is main's. GB10, Qwen3.8-27B MLX 4-bit, --no-drafts, bench_concurrent (256 tokens, 4 cells, 3 reps), fresh servers in the order main/new/new/main/old/main: - aggregate tok/s, main (3 runs) -> this commit (2 runs): 1 stream 15.07 -> 15.00, 2: 28.27 -> 28.26, 4: 52.97 -> 52.77, 8: 93.72 -> 93.51 (-0.2 %; main's runs span 93.25 to 94.35). The previous head: 90.22 at 8 (-3.7 %). - token hashes equal main's in every cell. - a 69-token chat sent 3 s into a cold 12k-token prompt: first token in 0.26 to 0.30 s, against 19.6 to 19.9 s on main; the long prompt's own first token 23.5 to 23.7 s, against 22.5 to 22.7 s.
34560aa to
8154927
Compare
|
Thanks for the rebase and the runs. I rebased onto current main the same way (our side; GB10, Qwen3.8-27B MLX 4-bit,
|
… a restored fake pass holds its rows The prompt share counted paused streams as decoders, so with every stream paused a long prompt's step created debt that nothing paid down; it now counts runnable streams. A share below 0.01 falls back to 1 and a step's debt saturates instead of overflowing. The fake backend holds a restored stream's kept rows from its first step, so the drafter wrapper's per-step feature reads find them.
|
One more commit (8de3712) from a second review pass, no change to the scheduling policy or the numbers above:
|
|
Landed on main in 2ff0191..464824f, with your five records under your authorship. Thank you, @jschmied, and thanks for chasing down the 8-stream loss. We replayed it onto current main, where #1228's interrupted-prefill rules had landed in the meantime. One maintainer child reconciles the two: a backend failure during a stepped prompt still ends On a GB10 with Qwen3.8-27B MLX 4-bit, alternating base/new/new/base:
- Synthema, Ash's AI Assistant. |
What this changes
A long cold prompt on the native CUDA engine stops every other stream. Two causes, three commits.
3aa8718d(lanes.drafted). The drafter's wrapper offered the lane host noprefill_step. Behind--drafter, every family's prompt therefore ran whole at admission, even where the target steps its prompts. Over a target withprefill_step, reported fill andfeatures, the wrapper now steps too: it runs the target's step, then gives the drafter that step's rows (all but the prompt's last, as the streamed prefill does).bd64eb6d(qwen CUDA). The Qwen3.5 family ran a whole prompt pass insideprefill.prefill_stepnow runs the same pass one cut a call: the samenextCutboundaries, forwards and marks, in the same order. The pass reports its fill, and a cancel lands between cuts.34560aa8(lane host). The host alternated one prompt step with one decode round.TENSORFOLD_PREFILL_SHARE=pin (0, 1] gives prompt steps a share p of wall time while streams decode:Stacking. On
main@f8d254c5e(it contains chaog992's lone-driver halt fix,010850d74). Branchprefill-interleave.Receipt
nvidia/Qwen3.8-Flash-Next-FP4-Local@b9ce18a6, served from a symlink folder with a rewritten index (see 1.1.2 refuses nvidia/Qwen3.8-Flash-Next-FP4-Local: its index names shards under tensors/ #1187).--parallel 4, no prompt cache,--no-draftsunless noted.Baseline (
main).The 27B stops everyone for the whole prompt. Flash Next already steps, but its decoders get one token per 1.1 s step: 4 % of their rate for the whole prefill.
This PR.
Predictions written before each run:
Every result after the change landed in its predicted range.
The 27B baseline missed high. I predicted 15-45 s at 35k and 450-900 s at 98k; main's 128-row prompt chunks run at about 224 tokens a second at 35k, and slower further in.
Exactness. Token hashes against
main's plain run (1 stream, no drafts, no cache):Tests.
drafted_step_test.zig) runs a stepping target's prompts through the wrapper three steps at a time; the replies equal the whole prefill's.zig build test -Dnvcc=…: 276/276 steps, 3454/3469 passed, 15 skipped (missing libjpeg/libwebp and hardware), exit 0.Contract. No new kernel.
Not run, and why
Gotchas and overlap
families/qwen3_5/cuda_native.zig.bd64eb6dis a thin split of that loop: whichever lands first, the other rebases. With 4096-row chunks, a 27B step becomes the whole wait a decoder sees (about 4 s).Drafted.takenow cutsfollowto the rows it absorbs, as the whole prefill's absorb does.Questions for @ashhart
--prefill-share) rather than an environment variable?