Skip to content

Three decode-graph registrations route async steps into a host-vector replay with no device_token_ids decline #1305

Description

@localai-bot

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.

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions