Skip to content

[Bugfix][MRV2] Require all requests to be decoding for uniform-decode dispatch - #51865

Merged
njhill merged 5 commits into
vllm-project:mainfrom
njhill:rahul/k3-pr6-uniform-decode
Aug 12, 2026
Merged

[Bugfix][MRV2] Require all requests to be decoding for uniform-decode dispatch#51865
njhill merged 5 commits into
vllm-project:mainfrom
njhill:rahul/k3-pr6-uniform-decode

Conversation

@njhill

@njhill njhill commented Aug 11, 2026

Copy link
Copy Markdown
Member

This is a copy of #50532 from @rchalamala with some additional rework to avoid redundant computation.

Part of the prepare_inputs logic is split into a separate gather_batch_req_state method which runs before the dp token count / cuda-graph mode synchronization.

rchalamala and others added 5 commits August 11, 2026 19:38
… dispatch

Model runner v2 classified a batch as a uniform decode batch from its shape
alone -- num_tokens == num_reqs * max_query_len, `get_uniform_token_count` in
vllm/v1/worker/gpu/cudagraph_utils.py -- and nothing about whether the requests
were decoding. A prompt chunk of exactly `1 + num_speculative_tokens` tokens has
that shape, so a prefill was dispatched with `cg_mode=FULL` and replayed the
captured spec-verify decode graph over prompt tokens. Without spec decoding the
same collision exists between a 1-token prompt chunk and the q=1 decode graph.

Observed on this base with Qwen/Qwen3.5-0.8B (GDN hybrid) + MTP, K=7,
FULL_AND_PIECEWISE, VLLM_USE_V2_MODEL_RUNNER=1: an 8-token prompt dispatches
`reqs=1 toks=8 uniform_token_count=8 -> FULL` on its prefill step and generates
deterministic garbage, while the same request at 4, 5, 6, 7 or 9 prompt tokens
dispatches PIECEWISE and is coherent. After this change the 8-token prefill
dispatches PIECEWISE and answers the prompt; the control outputs are unchanged
byte for byte, and genuine decode steps still replay their FULL graphs
(uniform_token_count=8 for the verify step, 1 for the drafter steps).

Mechanism, traced at the metadata level on this base: the mis-dispatched batch
still builds prefill metadata, because `MambaHybridModelState.prepare_attn`
passes is_prefilling and a -1 draft count for the chunk, so
`GDNAttentionMetadataBuilder.build` reports num_prefills > 0. Both
persistent-buffer refresh blocks in that builder are gated on num_prefills == 0,
so neither runs, and the replayed graph reads the state indices its buffers held
when the last uniform decode batch was built -- NULL(0) from the capture-time
dummy block tables if none has run yet. `state_idx <= 0` makes the fused GDN
kernels skip the recurrent state read and the store
(third_party/flash_linear_attention/ops/fused_sigmoid_gating.py), so the
prompt's conv/SSM state is never created and every later step computes from a
zero state.

The gate is per batch, not per request, so genuine decodes sharing a batch with
such a chunk lose their refresh too: building metadata for 6 spec decodes plus 2
colliding chunks leaves all 8 rows of spec_state_indices_tensor at their
capture-time values while the real block table sits elsewhere. Dense models
survive the mis-dispatch because spec-verify attention over a fresh sequence is
the same computation as prefill attention; only recurrent-state models lose
state. Issue vllm-project#49918 REPORTS this as 0/6 correct control requests, reproduced
independently of proposer (MTP and ngram), quantization and batch size, with
PIECEWISE or enforce-eager as workarounds.

The classification rule existed twice and drifted, which is why fixing it in one
runner leaves the other broken. `get_uniform_decode_token_count` in
vllm/v1/worker/utils.py is now the single source of truth: every request has the
same query length AND no request has prompt tokens left to compute. utils.py is
already the module that both gpu_model_runner.py and the gpu/ package import
from, so neither runner has to depend on the other. Both v2 call sites use it:
the target model in `execute_model`, and the draft prefill graph in
`AutoRegressiveSpeculator.propose`, whose comment already claimed "when all
requests are decoding" while testing only the shape -- that dispatch also
replayed FULL for the 8-token prompt above.

Capture paths keep the shape-only test. Dummy batches (DP padding, memory
profiling, warmup) are uniform by construction and their synthetic request ids
have no state to consult, matching `InputBatch.make_dummy`, which marks every
dummy request as not prefilling; the v1 runner expresses the same exemption as
`force_uniform_decode`. Startup on the run above exercised the profile run, the
warmup steps and prefill/decode graph capture with the stricter predicate in
place, and the captured decode graphs are still selected at runtime.

Deliberately not done: the v1 site, `GPUModelRunner._is_uniform_decode`, is left
untouched. PR vllm-project#47123 has been open since 2026-06-30 to fix exactly that call
site and this change must not supersede it. The helper is shaped so that
`_is_uniform_decode` can collapse onto it by passing
`input_batch.num_computed_tokens_cpu` and `input_batch.num_prompt_tokens` and
comparing the result with uniform_decode_query_len. vllm/v1/worker/gpu/warmup.py
also still pads its warmup prompts to `decode_query_len + 1` to dodge the
misclassification; that workaround is now redundant, but it is harmless and it
keeps the warmup shapes diverse.

The added cost is a per-step `np.fromiter` over the scheduled request ids,
one dict lookup each, then a gather of two int32 numpy views. No CPU-GPU
sync. It duplicates an index vector `prepare_inputs` rebuilds shortly
after; folding the two together is a worthwhile follow-up.

Tests: with the decoding clause removed from the predicate (i.e. the pre-fix
rule), test_prompt_chunks_shaped_like_spec_decode_miss_the_full_graph and
test_prompt_chunk_of_decode_query_len_is_not_uniform_decode fail while the
uniform-decode and dummy-run cases still pass.

AI assistance was used for this change.

Co-authored-by: Janelle Cai <janelle.cai@modal.com>
Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
_get_uniform_token_count built a per-request index mapping with a Python
dict-lookup loop before calling get_uniform_decode_token_count, which runs
the O(1) shape test first and returns None for any mixed prefill/decode
batch. Most batches are mixed, so the gather was built and discarded one
line later on the common path, where the code it replaced was a single
arithmetic comparison.

Runs the same shape test in the caller and returns early, so the gather is
built only for batches that can still turn out to be uniform decode. The
predicate is the one the callee already applies, so classification is
unchanged.

Co-authored-by: Janelle Cai <janelle.cai@modal.com>
Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
The multi-module MTP speculator picks its cudagraph with
`get_uniform_token_count`, which tests batch shape alone. A prompt chunk of
`1 + num_speculative_tokens` tokens has a spec-decode step's shape, so that
test classifies it as a decode batch and replays a decode-captured graph over
prompt tokens, whose kernels read per-request state a prefill metadata build
does not refresh. Route it through `get_uniform_decode_token_count`, which
additionally requires that no request is still prefilling.

This speculator is a copy of the autoregressive one, comment included, and
arrived after the other call sites were already fixed, so the guard added here
is written over every speculator in the package rather than over this one. It
fails on `multi_module_mtp/speculator.py:201` without the change and passes
with it, and will fail again on the next speculator added by copy.

Co-authored-by: Janelle Cai <janelle.cai@modal.com>
Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Keep the main-branch imports required by the autoregressive speculator while
removing the replaced shape-only helper.

Co-authored-by: Janelle Cai <janelle.cai@modal.com>
Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Signed-off-by: Nick Hill <nickhill123@gmail.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify mergify Bot added nvidia speculative-decoding mrv2 Model Runner V2 specific labels Aug 11, 2026
@mergify mergify Bot added the bug Something isn't working label Aug 11, 2026
@njhill njhill changed the title [Bugfix][MRV2] Require all requests to be decoding for uniform-decode dispatch- #50532 [Bugfix][MRV2] Require all requests to be decoding for uniform-decode dispatch Aug 11, 2026
@njhill

njhill commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #83430 for commit 7201f59250f9.

@WoosukKwon WoosukKwon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks!

@github-project-automation github-project-automation Bot moved this to Ready in NVIDIA Aug 11, 2026
@njhill
njhill merged commit 0a94d85 into vllm-project:main Aug 12, 2026
112 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to Done in NVIDIA Aug 12, 2026
@njhill
njhill deleted the rahul/k3-pr6-uniform-decode branch August 12, 2026 00:15
Comment thread vllm/v1/worker/utils.py
return num_reqs > 0 and num_tokens == max_query_len * num_reqs


def get_uniform_decode_token_count(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: i find having get_uniform_token_count and get_uniform_decode_token_count confusing, why can't we deprecate get_uniform_token_count and use get_uniform_decode_token_count exclusively?

zyp2014 pushed a commit to zyp2014/vllm that referenced this pull request Aug 21, 2026
… dispatch (vllm-project#51865)

Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Co-authored-by: Janelle Cai <janelle.cai@modal.com>
garrygale added a commit to garrygale/vllm that referenced this pull request Sep 5, 2026
A chunked-prefill chunk of exactly decode_query_len tokens has the same
shape as a uniform spec-decode batch. Shape-only classification then
selects a FULL uniform-decode graph for a batch that still contains a
prefill, corrupting GDN/Mamba state and causing intermittent acceptance
drops whenever a request finishes and a new prefill tail is admitted.
Require that every scheduled request is past its prefill before uniform
decode dispatch (vLLM vllm-project#51865).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working mrv2 Model Runner V2 specific nvidia speculative-decoding

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants