Found while migrating Qwen3MoeDecodeGraph and DeepseekV2DecodeGraph onto the
break-point capture seam (ENG-CUDAGRAPH-BREAK W3, #1291). It is NOT caused by
that migration — the same shape is present at the migration's base commit
5d9fe332c — and W3 deliberately did not close it. This issue records it with an
owner rather than leaving it to be rediscovered.
What
src/vllm/model_executor/models/qwen3.cpp:1106 declines its decode graph
outright whenever ModelForwardInput::device_token_ids is non-null:
if (input.device_token_ids != nullptr) {
return std::nullopt;
}
Its comment records the measured battery that forced it (#323, #1179): depth-1 graph ON PASS 78/78, depth-2 graph OFF PASS 82/82, depth-2 graph ON FAIL, slots 1-3 degenerate. The cause is that Step() replays against the persistent
HOST token_ids, while the asynchronous device mirror has patched the DEVICE
ids and deliberately left the host vector stale for decode rows
(src/vllm/v1/worker/gpu/runner.cpp:1265-1309,1523). That comment also says the
hazard is "latent for EVERY classic-dense model, since the graph is default-ON".
Three other decode-graph registrations route a pure-decode step into a driver
with the same host-vector replay and carry no such check:
| Registration |
Driver |
device_token_ids read? |
Decline? |
src/vllm/model_executor/models/qwen3_moe_registry.cpp:107 |
Qwen3MoeDecodeGraph |
no |
no |
src/vllm/model_executor/models/deepseek_v2_registry.cpp:106 |
DeepseekV2DecodeGraph |
no |
no |
src/vllm/model_executor/models/glm4_moe_lite_registry.cpp:125 |
DeepseekV2DecodeGraph |
no |
no |
grep -c StepDevInputs returns 0 for qwen3_moe.cpp, deepseek_v2.cpp and
voxtral.cpp, and 41 for qwen3_5.cpp — the persistent DEVICE input path exists
in exactly one driver, which is the divergence
.agents/specs/eng-cudagraph-break.md
## Our baseline already records as the cost of nine drivers.
VoxtralDecodeGraph is NOT affected: its only construction site is
VoxtralGenerateGreedy, the single-sequence multimodal greedy loop, which is not
reached from runner.cpp and has no asynchronous device mirror.
Why W3 did not fix it in flow
AGENTS.md's in-flow rule covers a small and clear fix. This is neither, in two
ways.
- Adding a decline is a behaviour change that trades a shipped, default-ON
capability away. qwen3.cpp's decline is justified by a measured
four-concurrent battery on real hardware; W3 has no such measurement for these
two models and would be copying a mitigation on the strength of a structural
argument alone.
- The correct fix is already assigned.
qwen3.cpp's own comment names it:
read the identifiers at REPLAY time from a stable device buffer, which is what
StepDevInputs (qwen3_5.cpp:3894) already is.
eng-cudagraph-break.md
## Work breakdown W4 is where that becomes a SEAM capability rather than one
driver's private code, and that is the only version of the fix that reaches
these registrations at all. Hand-copying it into each would be exactly the
duplication row ENG-CUDAGRAPH-BREAK exists to remove.
Owner
ENG-CUDAGRAPH-BREAK W4, with #1179 as the standing tracker for the decline
itself. Recorded under ## Owed in
eng-cudagraph-break.md.
What would settle it
Run the same four-concurrent battery qwen3.cpp's comment records against
Qwen3-Coder and DeepSeek-V2-Lite with VT_ASYNC_DEVICE_MIRROR live and the
decode graph ON. Either it reproduces the depth-2 degeneration, in which case
these two registrations need the same mitigation until W4 lands, or it does not,
in which case the reason has to be found and written down rather than assumed.
Found while migrating
Qwen3MoeDecodeGraphandDeepseekV2DecodeGraphonto thebreak-point capture seam (ENG-CUDAGRAPH-BREAK W3, #1291). It is NOT caused by
that migration — the same shape is present at the migration's base commit
5d9fe332c— and W3 deliberately did not close it. This issue records it with anowner rather than leaving it to be rediscovered.
What
src/vllm/model_executor/models/qwen3.cpp:1106declines its decode graphoutright whenever
ModelForwardInput::device_token_idsis non-null:Its comment records the measured battery that forced it (#323, #1179):
depth-1 graph ON PASS 78/78,depth-2 graph OFF PASS 82/82,depth-2 graph ON FAIL, slots 1-3 degenerate. The cause is thatStep()replays against the persistentHOST
token_ids, while the asynchronous device mirror has patched the DEVICEids and deliberately left the host vector stale for decode rows
(
src/vllm/v1/worker/gpu/runner.cpp:1265-1309,1523). That comment also says thehazard is "latent for EVERY classic-dense model, since the graph is default-ON".
Three other decode-graph registrations route a pure-decode step into a driver
with the same host-vector replay and carry no such check:
device_token_idsread?src/vllm/model_executor/models/qwen3_moe_registry.cpp:107Qwen3MoeDecodeGraphsrc/vllm/model_executor/models/deepseek_v2_registry.cpp:106DeepseekV2DecodeGraphsrc/vllm/model_executor/models/glm4_moe_lite_registry.cpp:125DeepseekV2DecodeGraphgrep -c StepDevInputsreturns 0 forqwen3_moe.cpp,deepseek_v2.cppandvoxtral.cpp, and 41 forqwen3_5.cpp— the persistent DEVICE input path existsin exactly one driver, which is the divergence
.agents/specs/eng-cudagraph-break.md## Our baselinealready records as the cost of nine drivers.VoxtralDecodeGraphis NOT affected: its only construction site isVoxtralGenerateGreedy, the single-sequence multimodal greedy loop, which is notreached from
runner.cppand has no asynchronous device mirror.Why W3 did not fix it in flow
AGENTS.md's in-flow rule covers a small and clear fix. This is neither, in two
ways.
capability away.
qwen3.cpp's decline is justified by a measuredfour-concurrent battery on real hardware; W3 has no such measurement for these
two models and would be copying a mitigation on the strength of a structural
argument alone.
qwen3.cpp's own comment names it:read the identifiers at REPLAY time from a stable device buffer, which is what
StepDevInputs(qwen3_5.cpp:3894) already is.eng-cudagraph-break.md## Work breakdownW4 is where that becomes a SEAM capability rather than onedriver's private code, and that is the only version of the fix that reaches
these registrations at all. Hand-copying it into each would be exactly the
duplication row
ENG-CUDAGRAPH-BREAKexists to remove.Owner
ENG-CUDAGRAPH-BREAKW4, with #1179 as the standing tracker for the declineitself. Recorded under
## Owedineng-cudagraph-break.md.What would settle it
Run the same four-concurrent battery
qwen3.cpp's comment records againstQwen3-Coder and DeepSeek-V2-Lite with
VT_ASYNC_DEVICE_MIRRORlive and thedecode graph ON. Either it reproduces the depth-2 degeneration, in which case
these two registrations need the same mitigation until W4 lands, or it does not,
in which case the reason has to be found and written down rather than assumed.