DFlash2 speculative decoding serves one sequence. At concurrency 2 the engine refuses on its own invariant and EngineCore stops. This is the blocking defect for the #1574 three-engine comparison: vLLM and SGLang were both measured at c=1..32 with the same DFlash2 draft at k=8, and we cannot produce a rung above c=1.
Measured
integ4/1574 @ 3d137890f (main + #1994 + #1997 + #1999 + #2000), artifact-gated (flash_fwd=1792, SpecDecodeFA2Bf16=1, FA2 manifest [121a]), on an idle leased GB10. --num-blocks 3744 --max-num-seqs 16 --max-model-len 8192 --no-enable-prefix-caching --speculative-config '{"method":"dflash","model":"/draft","num_speculative_tokens":8}', via vllm bench serve --backend openai-chat, input 1024 / output 512:
| rung |
result |
| c=1 |
24.70 out tok/s, 75.40 total, TTFT mean 1363.78 ms / p99 1388.08, TPOT 37.90 ms, 8/8 ok |
| c=2 |
VOID — ok=1, failed=7 |
The c=2 failure:
engine-fatal: EngineCore busy loop threw: vt: propose_drafts_block: context position
discontinuity (accumulation out of sync with the target's committed positions)
at src/vllm/v1/worker/gpu/runner.cpp:2961
Every later request returns 500 [request submitted to a stopped AsyncLLM]. The ladder's failed==0 assertion recorded the rung as VOID and wrote no row, which is the harness working.
The invariant, and why it is right to assert
src/vllm/v1/worker/gpu/runner.cpp:2955-2963:
// Invariant: this step's first committed token sits at absolute position L
// (== current context length). A violation means the accumulation lost sync
// (the I5e async-input-combine bug class) — assert rather than corrupt.
const int64_t L = dflash_ctx_len_[static_cast<size_t>(i)];
VT_CHECK(step.positions[static_cast<size_t>(rows[0])] == L,
"propose_drafts_block: context position discontinuity (accumulation "
"out of sync with the target's committed positions)");
This is a guarded refusal, not a crash — the code deliberately stops rather than drafting from a desynchronised context, and the comment names the failure class it is defending against. The defect is that the accumulation desynchronises at all once more than one request is in flight.
The matching half is the capture gate at src/vllm/model_executor/models/qwen3_dflash.cpp:1577, which admits the capture-safe paged path only when P == 1. Above that the fallback re-materialises each request's context from the paged store every propose step, and dflash_ctx_len_[i] no longer tracks what the target committed for request i.
Relationship to the other rows
Distinct from all four fixes landed today, and it is what remains after them:
It is also the surviving candidate for the crash half of #1983: the same P > 1 region, reached under a configuration where the position accumulation desynchronises rather than the assertion catching it first.
What c=1 says about the rest
Worth recording, because it changes the priority. At c=1 on this build we are competitive: 24.70 out tok/s against vLLM's 24.36 and SGLang's 25.20, with TPOT 37.90 ms against SGLang's 37.93 — per-token decode at parity with the engine we are chasing. Caveats: ours ran at --max-model-len 8192 where the recorded competitor rows used 32768, and at --max-num-seqs 16 with an explicit 3744-block pool against their defaults, so this is a same-band result rather than a like-for-like one.
The gap to close is therefore concurrency, not per-token cost.
Owed
- A red-before test that drives two concurrent requests through
propose_drafts_block and reproduces the discontinuity on CPU, so the fix is gated rather than measured.
- Whether the same configuration serves at c>=2 with
--speculative-config omitted. If it does, that isolates the defect to the draft path and gives an interim comparison point.
- Reproduction is one command; the binary is at
/usr/local/vcpp/build14 and the ladder at /usr/local/vcpp/ladder.sh.
DFlash2 speculative decoding serves one sequence. At concurrency 2 the engine refuses on its own invariant and
EngineCorestops. This is the blocking defect for the #1574 three-engine comparison: vLLM and SGLang were both measured at c=1..32 with the same DFlash2 draft at k=8, and we cannot produce a rung above c=1.Measured
integ4/1574@3d137890f(main+ #1994 + #1997 + #1999 + #2000), artifact-gated (flash_fwd=1792,SpecDecodeFA2Bf16=1, FA2 manifest[121a]), on an idle leased GB10.--num-blocks 3744 --max-num-seqs 16 --max-model-len 8192 --no-enable-prefix-caching --speculative-config '{"method":"dflash","model":"/draft","num_speculative_tokens":8}', viavllm bench serve --backend openai-chat, input 1024 / output 512:The c=2 failure:
Every later request returns 500
[request submitted to a stopped AsyncLLM]. The ladder'sfailed==0assertion recorded the rung as VOID and wrote no row, which is the harness working.The invariant, and why it is right to assert
src/vllm/v1/worker/gpu/runner.cpp:2955-2963:This is a guarded refusal, not a crash — the code deliberately stops rather than drafting from a desynchronised context, and the comment names the failure class it is defending against. The defect is that the accumulation desynchronises at all once more than one request is in flight.
The matching half is the capture gate at
src/vllm/model_executor/models/qwen3_dflash.cpp:1577, which admits the capture-safe paged path only whenP == 1. Above that the fallback re-materialises each request's context from the paged store every propose step, anddflash_ctx_len_[i]no longer tracks what the target committed for requesti.Relationship to the other rows
Distinct from all four fixes landed today, and it is what remains after them:
--max-num-seqs(#1983) #1999 (recurrent state in the KV budget) — verified on device: residency slope 1.22-1.50 GiB/seq to 0.0000,--max-num-seqs 28from 0.23 to 17.37 tok/s.It is also the surviving candidate for the crash half of #1983: the same
P > 1region, reached under a configuration where the position accumulation desynchronises rather than the assertion catching it first.What c=1 says about the rest
Worth recording, because it changes the priority. At c=1 on this build we are competitive: 24.70 out tok/s against vLLM's 24.36 and SGLang's 25.20, with TPOT 37.90 ms against SGLang's 37.93 — per-token decode at parity with the engine we are chasing. Caveats: ours ran at
--max-model-len 8192where the recorded competitor rows used 32768, and at--max-num-seqs 16with an explicit 3744-block pool against their defaults, so this is a same-band result rather than a like-for-like one.The gap to close is therefore concurrency, not per-token cost.
Owed
propose_drafts_blockand reproduces the discontinuity on CPU, so the fix is gated rather than measured.--speculative-configomitted. If it does, that isolates the defect to the draft path and gives an interim comparison point./usr/local/vcpp/build14and the ladder at/usr/local/vcpp/ladder.sh.