Conversation
DeepseekV4Indexer.forward took the "every candidate is selected" shortcut when this step's max_seq_len // compress_ratio fit in topk_tokens. That predicate is a host value, and the indexer runs inside a captured cudagraph segment under breakable PIECEWISE, so it is evaluated once against the capture-time dummy batch and baked in. The dummy batch has max_seq_len 1-2 (piecewise capture builds attention metadata with for_capture=False, so the host bound is the dummy's own length rather than the worst case), so every replayed step takes the short-context path. With a 3268-token context that writes indices 0..topk-1 instead of the score-based top-k, while any step that exceeds max_cudagraph_capture_size falls back to eager and runs the real indexer -- two different sparse-attention patterns for the same request. self.max_model_len is already divided by compress_ratio, so bounding on it takes the shortcut only when it holds for every request the engine can serve, which is capture-safe in every cudagraph mode. FULL was already safe because its capture path passes for_capture=True and DefaultModelState then uses max_model_len as the host bound. Verified on DeepSeek-V4-Flash-Base, TP4/EP4, 4xGB200, VLLM_BATCH_INVARIANT=1, cudagraph_mode=PIECEWISE, with batch composition pinned by a hand-driven LLMEngine: the victim's logprobs went from 8/8 and 2/9 filler counts differing to 0/8 and 0/9, and now match the FULL/eager values bit-for-bit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Closing: superseded upstream. vllm-project#52492 ("[Bugfix][DSv4] Keep Per AGENTS.md we do not carry a competing PR, and the upstream fix is the better One difference worth recording for whoever picks this up, because it is not Evidence and reasoning: |
What
DeepseekV4Indexer.forwardshort-circuits the Lightning indexer when the wholecandidate set fits in top-k. The predicate reads this step's
indexer_metadata.max_seq_len. Bound it on the configured maximum instead:One file, +10/-1 (
vllm/models/deepseek_v4/attention.py), 9 of those lines acomment explaining the hazard.
Why
That
ifis a host-side branch that runs inside a captured cudagraphsegment. DeepSeek-V4 auto-enables
VLLM_USE_BREAKABLE_CUDAGRAPH=1, whichforces
CompilationMode.NONE— no torch.compile, so no Dynamo guards — and themodel's only eager break is
DeepseekV4Attention._sparse_indexer_and_attn(43 breaks for 43 layers, measured). The indexer runs before that break, so
the predicate is evaluated exactly once, at capture, against the dummy batch,
and the chosen side is baked into the graph.
Logged predicate, capture vs. real step (SM100 / GB200, TP4/EP4,
0.26.1rc1.dev668+g3ee2df303):max_seq_lenmode=PIECEWISE)short=Truemode=NONE)short=FalseSo every graph-replayed step runs
_fill_short_context_topk_indices, whichwrites
— the earliest compressed positions in order, not the top-k by score. At
3268 tokens that is 817 candidates against
topk=512: the step sees roughly thefirst 2k tokens of the prompt; the rest of the context is invisible to the
sparse-attention path (the model still sees it through everything the indexer
does not gate). Steps
that exceed
max_cudagraph_capture_sizefall back to eager and run the realindexer, so the same request alternates between two different sparse patterns
depending on how large its step happened to be.
This is a correctness bug on its own, independent of batch invariance.
Ordinary long-context serving under piecewise silently uses the wrong sparse
pattern; without a batch-invariance checker it just looks like unexplained
quality loss.
cudagraph_mode=FULLis already safe under the capture configuration wetested (the defaults; we did not probe an explicitly shrunk capture set), for a
reason worth recording: its
capture path passes
for_capture=True(
v1/worker/gpu/cudagraph_utils.py:643), andDefaultModelStatethen usesmax_model_lenas the host bound (v1/worker/gpu/model_states/default.py:149)— measured
msl=8192at FULL capture, so the long branch is what gets baked.PIECEWISE passes
for_capture=False, under a comment that already flags therisk: "We assume that attention-like operations intended for capture will still
produce capturable metadata, even when for_capture=False. While this assumption
is brittle, it currently works in practice." Breakable cudagraphs capture every
non-breakpoint op, so it no longer holds. The one-line model fix is the safe
change; whether PIECEWISE capture should also use worst-case host bounds is a
broader call.
Tests
Batch composition pinned by driving
LLMEnginewithadd_request/stepbyhand (HTTP-level scheduling changes composition and confounds the measurement).
Victim ~3.3k-token prompt (the probe logs show
max_seq_len3268), N short orlong fillers, compare the victim's first four logprobs against the solo
reference.
After the fix, PIECEWISE values are bit-identical to FULL and eager
(
-0.6408482789993286). PIECEWISE was confirmed live in the fixed run (zeromode-pinning messages, two PIECEWISE capture bars) — it is not passing because
the graph was skipped.
Related configuration suite:
tests/test_config.py -k batch_invariant→ 7passed (run in the pinned container; the host venv's
huggingface_hubis tooold for that file's imports). Those cover the cudagraph-mode policy change, not
this predicate — this PR still has no unit test that exercises the shortcut
directly; the evidence for it is the pinned-composition probe above.
Model evaluation
The fix is an identity transform for
cudagraph_mode=FULLon this config —before:
8192 // 4 = 2048 <= 512is False; after:self.max_model_len (=2048) <= 512is also False — same branch. Confirmed empirically by the 19/19 rowabove. The accuracy and throughput numbers below were measured in the
FULL-pinned configuration and carry over by that identity argument -- they are
not a fresh full-eval rerun under PIECEWISE:
agreement 1319/1319
(baseline single-round hit rate 38.7%)
Notes
AI assistance was used to produce this change. ⚠ Before this goes upstream the
AGENTS.md duplicate checks (
gh issue view,gh pr list --searchby issue andby area) still have to be run and their results pasted here. Related but, on the
evidence we have, not duplicated:
vllm-project#52109 is a different defect (gfx942 indexer K-cache
FLAT-write / SHUFFLE-read layout mismatch; that reporter rules out
capture/replay with an
--enforce-eagercontrol).