Skip to content

[Bugfix][Spec Decode] Honour the draft's attention_backend on Model Runner V2 - #54826

Merged
njhill merged 3 commits into
vllm-project:mainfrom
stecasta:fix/v2-draft-attention-backend
Sep 4, 2026
Merged

njhill merged 3 commits into
vllm-project:mainfrom
stecasta:fix/v2-draft-attention-backend

Conversation

@stecasta

@stecasta stecasta commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Purpose

--speculative-config '{"attention_backend": ...}' is silently ignored on Model Runner V2.

The three draft overrides have deliberately different contracts, per SpeculativeConfig:

field when unset
moe_backend inherits the target's
kv_cache_dtype inherits the target's
attention_backend cleared, so the draft autoselects independently

The V1 proposer implements this in _create_draft_vllm_config by assigning the draft's backend
unconditionally, so a None erases the target's rather than inheriting it:

# Note (matt): Never inherit the attention backend from base, because there are
# many opportunities for incompatibility, so we always independently autoselect
# unless explicitly specified in the speculative config.

V2 implements neither half: an explicit backend is dropped, and an unset one inherits. It affects
every speculator family that does not resolve the backend itself, which is eagle, mtp,
gemma4, autoregressive, multi_module_mtp and dflash2. Only dflash (which overrides
attn_vllm_config) and dspark (which resolves it in dspark/utils.py) were unaffected.

MTP is the common case and published recipes use it, e.g.
--speculative-config '{"method":"mtp","attention_backend":"TRITON_ATTN", ...}'. This became
user-visible when MRV2 was made the default in #53183.

Fix

Apply the override in load_eagle_model, before get_model(), alongside the existing
kv_cache_dtype override.

Placement is the crux. init_attn_backend resolves the backend from each already-constructed layer:

attn_backend = attn_layers[layer_name].get_attn_backend()   # v1/worker/gpu/attn_utils.py

and uses vllm_config only to look those layers up. So the backend is fixed at construction time and
any config mutated afterwards is ignored. The assignment is unconditional, mirroring V1, so the unset
case clears the target's backend instead of inheriting it.

Validation

Unit, tests/v1/spec_decode/test_draft_attention_backend_override.py, which captures the config
load_eagle_model actually hands to get_model:

without the fix:  2 failed, 1 passed
with the fix:     3 passed

Both behavioural tests fail when the fix is reverted, so they are not vacuous.

Hardware, Qwen3.6-35B-A3B-NVFP4 with MTP-3 on an RTX PRO 6000 Blackwell Max-Q, V2, default MoE
backend, real routing, one node:

arm draft backend resolved output tput
without fix, attention_backend: TRITON_ATTN FlashInferBackend (request dropped) 214.91
with fix, attention_backend: TRITON_ATTN TritonAttentionBackend 193.69
with fix, attention_backend unset serves, autoselects n/a

Note the throughput drops 9.9% in the middle row, and that is the point: the draft now runs the
backend the user asked for. Previously the request was discarded and the draft silently ran
FlashInfer. A no-op change could not move throughput at all, so this is also the proof the override
takes effect. The third row is the safety check on the unconditional assignment: clearing the backend
when unset does not break the default MTP path.

Why this is not a duplicate

I ran the checks in AGENTS.md. Nothing open touches the V2 draft attention-config path. #53450 pins
backends for components that autoselect, which is a different mechanism and config surface.

Model evaluation

Not applicable. This changes which attention backend the draft uses. Under real rejection sampling
the target verifies every drafted token, so the draft's kernel choice cannot alter output; it can only
move acceptance length. Affected configurations today run a backend the user did not request, so there
is no prior baseline to regress against.

AI assistance

AI assistance was used to author this change. I have reviewed every changed line, ran the tests and
the hardware validation above myself, and can defend the design.

…unner V2

The V1 proposer sets the draft's attention backend from the speculative config
and deliberately never inherits the target's:

  # Note (matt): Never inherit the attention backend from base, because there
  # are many opportunities for incompatibility, so we always independently
  # autoselect unless explicitly specified in the speculative config.

The V2 base speculator returned the target's VllmConfig unchanged from
attn_vllm_config, which init_attn_backend then uses to build the draft's
attention groups. So an explicit attention_backend in --speculative-config was
silently dropped for every speculator family that does not override the
property itself. dflash overrides it (for causality) and dspark resolves it in
its own loader, but eagle, mtp, gemma4, autoregressive and multi_module_mtp all
inherited the target's backend instead.

Apply the override in the base property so every family picks it up. dflash
already chains through super(), so it keeps its own behaviour on top, and
dspark is unaffected because it resolves the backend before this point.

Unset stays unchanged: the target's config is returned as before.

Signed-off-by: Stefano Castagnetta <scastagnetta@nvidia.com>
@mergify mergify Bot added speculative-decoding mrv2 Model Runner V2 specific bug Something isn't working labels Sep 1, 2026
…draft is built

The previous approach overrode attention_config in the base speculator's
attn_vllm_config property. That cannot work: init_attn_backend resolves the
backend from each constructed layer via get_attn_backend(), and only uses
vllm_config to look the layers up, so a config mutated after construction is
ignored. Measured on an MTP draft asking for TRITON_ATTN: the property ran and
the draft still resolved FlashInferBackend.

Apply the override in load_eagle_model instead, before get_model(), alongside
the existing kv_cache_dtype override, so the draft's layers are constructed
with the requested backend.

Assign it unconditionally, mirroring the V1 proposer: a None backend clears the
target's so the draft autoselects independently rather than inheriting, which
V1 does deliberately because draft and target attention shapes differ and not
every backend serves both.

Signed-off-by: Stefano Castagnetta <scastagnetta@nvidia.com>
@stecasta
stecasta marked this pull request as ready for review September 2, 2026 13:13

@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.

@stecasta

stecasta commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Hi @njhill @TheEpicDolphin, could you take a look? Currently we can't specify the attention backend of the drafter using runner v2

@TheEpicDolphin TheEpicDolphin 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.

Sorry for the delay. Thanks for the fix!

@TheEpicDolphin TheEpicDolphin added the ready ONLY add when PR is ready to merge/full CI is needed label Sep 3, 2026
@TheEpicDolphin

Copy link
Copy Markdown
Collaborator

/ci run

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

@stecasta, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #87129 for commit 0f94a119424c.

@TheEpicDolphin

Copy link
Copy Markdown
Collaborator

@stecasta the test_with_eagle3_spec_decoding failure seems related to this change. The drafter is auto-selecting a different backend than before. Plz take a look when you get the chance

Assigning unconditionally cleared the target's backend when unset, letting the
draft autoselect. Target and draft must share one KV cache layout, and the
declared sets do not all intersect: FLEX_ATTENTION and TurboQuant accept only
LBNHC, while b12x and SM100 FlashInfer exclude it. On ROCm this left no common
layout and failed engine init in test_with_eagle3_spec_decoding.

Restrict the override to the explicitly set case, which is the reported bug, and
cover the unset case with a test.

Signed-off-by: Stefano Castagnetta <scastagnetta@nvidia.com>
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: e2142f91-d662-427b-9275-5ca45ac607e5

📥 Commits

Reviewing files that changed from the base of the PR and between 0ad5652 and a341a84.

📒 Files selected for processing (2)
  • tests/v1/spec_decode/test_draft_attention_backend_override.py
  • vllm/v1/worker/gpu/spec_decode/eagle/utils.py

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Speculative draft models now honor an explicitly configured attention backend.
    • Draft and target models use matching attention backends and KV cache layouts.
    • Target model configuration remains unchanged when applying draft-model settings.

Walkthrough

load_eagle_model now propagates an explicitly configured speculative attention backend to the draft model. New tests cover backend override, fallback to the target backend, and preservation of the caller's configuration.

Changes

Eagle attention backend

Layer / File(s) Summary
Propagate and validate the draft attention backend
vllm/v1/worker/gpu/spec_decode/eagle/utils.py, tests/v1/spec_decode/test_draft_attention_backend_override.py
load_eagle_model applies speculative_config.attention_backend before model construction. Tests verify explicit override, None fallback, and no mutation of the original configuration.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to a341a

Draft models now honor an explicitly configured attention backend while retaining existing unset behavior. Coverage confirms the override and configuration preservation, with no current merge-blocking risk identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the bug fix and specifies that Model Runner V2 must honor the draft's attention_backend.
Description check ✅ Passed The description directly explains the affected configuration, implementation, behavioral change, tests, and hardware validation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@stecasta

stecasta commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @TheEpicDolphin!

I had applied the override unconditionally, mirroring the V1 proposer, which clears the target's backend when attention_backend is unset and lets the draft autoselect. On ROCm the target is pinned to FLEX_ATTENTION ({LBNHC}) and the draft resolved to ROCM_ATTN ({LHBNC, LBHNC}), leaving no shared KV cache layout:

ValueError: No KV cache layout satisfies every supported set: [['LBNHC'], ['LHBNC', 'LBHNC']]
I have scope-limited the PR to the explicitly set case, which is the actual bug, and left the unset case inheriting as before.

I do not think inheriting is the right long term answer though. The draft should autoselect subject to the target's layout constraint rather than the two being intersected after both are built. I opened #55312 for that, with a note on where the constraint would need to be threaded.

Also in flight and related: #54788 (draft moe_backend dropped on V2, same class of bug) do you mind a quick review also there? :)

@stecasta

stecasta commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #87232 for commit a341a84de74a.

@stecasta

stecasta commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Ok it's passing now @TheEpicDolphin

@njhill njhill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @stecasta

@njhill
njhill merged commit 2524051 into vllm-project:main Sep 4, 2026
108 checks passed
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 ready ONLY add when PR is ready to merge/full CI is needed speculative-decoding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants