Skip to content

[Bugfix][V1] Keep still-prefilling rows out of full decode graphs - #60512

Open
atty57 wants to merge 3 commits into
vllm-project:mainfrom
atty57:fix/57720-decode-graph-eligible-mrv1
Open

atty57 wants to merge 3 commits into
vllm-project:mainfrom
atty57:fix/57720-decode-graph-eligible-mrv1

Conversation

@atty57

@atty57 atty57 commented Oct 8, 2026

Copy link
Copy Markdown

Overview

Full CUDA-graph dispatch in Model Runner V1 is decided by batch shape alone, so a one-token prompt looks like a decode step and replays a decode graph. On Mamba-hybrid models the result is garbled output that differs on every run. This gates full graphs on the batch actually being a decode, the way Model Runner V2 already does. Fixes #57720.

Claims

Validation

End-to-end reproducer from the issue, tiiuae/Falcon-H1-Tiny-90M-Instruct, VLLM_USE_V2_MODEL_RUNNER=0, cudagraph_mode=FULL_AND_PIECEWISE, temperature=0, 6 runs per prompt, RTX 4070 Laptop (8 GB):

prompt prompt tokens before after
Hello, Hi, Yes, No, The 1 6 distinct in 6, garbled (e.g. ' ->[ resilDAE') 1 distinct, coherent
Hello there 2 1 distinct 1 distinct (identical text)
The capital of France is 5 1 distinct 1 distinct (identical text)

Totals: unstable=5 of 7 before, unstable=0 of 7 after.

Reproducer
import collections
from vllm import LLM, SamplingParams

if __name__ == "__main__":
    llm = LLM(model="tiiuae/Falcon-H1-Tiny-90M-Instruct", max_model_len=4096)
    sp = SamplingParams(temperature=0.0, max_tokens=32)
    for p in ["Hello", "Hi", "Yes", "No", "The", "Hello there"]:
        outs = [llm.generate([p], sp, use_tqdm=False)[0].outputs[0].text for _ in range(6)]
        print(len(collections.Counter(outs)), "distinct in 6:", repr(p), "->", repr(outs[0][:40]))

Run with VLLM_USE_V2_MODEL_RUNNER=0.

Test suites, all passing on the same machine:

command result
pytest tests/v1/worker/test_gpu_model_runner.py 56 passed, 1 skipped
pytest tests/v1/worker/test_gpu_batch_ordering.py 13 passed
pytest tests/v1/attention/test_mamba_metadata_builder.py 1 passed
pytest tests/v1/cudagraph/test_cudagraph_dispatch.py 15 passed

Also ran facebook/opt-125m on MRV1 end-to-end to confirm the attention-only path is unaffected (stable, coherent output). pre-commit run --files <changed files> passes, including ruff check, ruff format, typos, mypy, and the test-tethering hook. tools/pre_commit/mypy.py 3.12 reports the same 48 pre-existing errors in gpu_model_runner.py with and without the change, so no new type errors.

Validation constraints: single consumer GPU, so no TP/PP, no disaggregated-prefill and no speculative-decoding end-to-end run. The spec-decode shapes are covered by unit tests instead. The local flashinfer JIT cannot build in this environment (CUDA toolkit/compiler header mismatch), so the reproducer ran with VLLM_USE_FLASHINFER_SAMPLER=0; that is unrelated to the dispatch path being changed.

Details

Root cause:

  1. GPUModelRunner._is_uniform_decode classifies a batch purely by shape: max_num_scheduled_tokens == uniform_decode_query_len and num_tokens == max * num_reqs. A fresh one-token prompt satisfies both.
  2. The batch therefore dispatches to a full graph. For Mamba, AttentionCGSupport.UNIFORM_BATCH resolves the default mode to FULL_AND_PIECEWISE, so full graphs cover exactly these "uniform decode" batches.
  3. BaseMambaAttentionMetadataBuilder._compute_common_metadata classifies the same row correctly as a prefill: it only reclassifies one-token rows that have prior state (has_prior_state = seq_lens > query_lens), and explicitly keeps first prompt chunks as prefills.
  4. It does not matter, because has_prefill / has_decode in MambaMixer2.forward are Python branches. Graph replay re-runs neither; the captured graph holds only the decode kernels.
  5. Those kernels (causal_conv1d_update, selective_state_update) update conv_state / ssm_state in place. The prefill path is what seeds them. A first prompt chunk therefore reads whatever the freshly allocated Mamba block contains, which is why the answer is fluent, unrelated to the prompt, and different on every run.

This also explains the workarounds reported on the issue: PIECEWISE and --enforce-eager re-run the Python branch each step, prompts of two or more tokens are not uniform-shaped, and --mamba-cache-mode and prefix caching are orthogonal.

Design: rather than a Mamba-specific gate, _is_decode_graph_eligible mirrors decode_graph_eligible in MRV2's gather_batch_req_state — a batch may take a full graph only if every row is done prefilling or is a single new token over existing context, with placeholder drafts discounted so padded prompt tails keep their graph. Keeping one semantics across the two runners seemed better than inventing a narrower MRV1-only rule.

MRV1 deprecation: MRV2 already has this guard, so nothing there changes. MRV1 still needs it because it remains selectable via VLLM_USE_V2_MODEL_RUNNER=0 and is still the default for some configurations (ROCm architectures in ROCM_DEFAULT_MRV1_ARCHITECTURES, environments without Triton, and models using features MRV2 does not support).

Tradeoff: as in MRV2, the guard is backend-agnostic, so an attention-only model with a lone fresh one-token prompt in an otherwise decode-shaped batch now runs piecewise instead of full. That shape is rare, the fast path for real decode batches is unchanged, and the early return keeps the check to two array comparisons when no row is prefilling.

Testing approach: the three new cases sit beside test_is_uniform_decode and exercise the predicate directly with a lightweight stub, matching how tests/v1/worker/test_gpu_batch_ordering.py covers the MRV2 equivalent. They mirror the MRV2 assertions: first prompt chunk rejected, one-token tail with prior context kept, draft-padded tail kept while a bare K+1 first chunk is rejected.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

Full-graph dispatch in Model Runner V1 is shape based, so a first prompt
chunk of one token looks like a decode step and replays a graph captured
over decodes. That graph holds only the decode kernels, which update
recurrent state in place rather than seeding it, so a one-token prompt
reads an unseeded Mamba cache block and comes back garbled, differently
on every run.

Gate full graphs on every row being either done prefilling or a single
new token over existing context, matching decode_graph_eligible in Model
Runner V2.

Fixes vllm-project#57720

Assisted-by: AI coding agent
Signed-off-by: atty57 <[email protected]>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@mergify mergify Bot added the bug Something isn't working label Oct 8, 2026
@mergify

mergify Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @atty57.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 8, 2026
@mergify mergify Bot removed the needs-rebase label Oct 8, 2026
@atty57
atty57 marked this pull request as ready for review October 8, 2026 21:43
@atty57
atty57 requested a review from njhill as a code owner October 8, 2026 21:43

@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 commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @atty57.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 12, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working needs-rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Mamba-hybrid models (granite-4.0-h) give a different, garbled greedy answer every time for a ONE-token prompt under full CUDA graphs

1 participant