[Bugfix][Qwen4Exp] Keep the MTP drafter stage-local under pipeline parallelism - #573
Open
Peuqui wants to merge 1 commit into
Open
[Bugfix][Qwen4Exp] Keep the MTP drafter stage-local under pipeline parallelism#573Peuqui wants to merge 1 commit into
Peuqui wants to merge 1 commit into
Conversation
…rallelism
Qwen4Exp with `--speculative-config method=mtp` and `--pipeline-parallel-size`
greater than one does not boot at all. Every worker on the final pipeline rank
dies while compiling:
torch._dynamo.exc.Unsupported: Data-dependent assertion failed
(cannot compile partial graph)
assert intermediate_tensors is not None
File "vllm/models/qwen4_exp/nvidia/mtp.py", line 333, in forward
The drafter is stage-local. `gpu_model_runner.execute_model` returns the
IntermediateTensors on every non-final pipeline rank before speculation is
reached, so `Qwen4ExpMultiTokenPredictor` only ever runs on the last rank, and
its weights are replicated rather than partitioned: embed_tokens,
fc_embedding, fc_hidden and every MTP layer are built on all ranks.
`forward` nevertheless branched on `get_pp_group().is_first_rank`, which is the
TARGET model's pipeline position. On the last rank that is False, so the
drafter took the "receive from the previous stage" path and asserted on
intermediate tensors that nobody sends. Under the fullgraph AOT compile this is
a compile error rather than a runtime one, so it takes the boot down instead of
one step.
Drop both pipeline branches: always build the embedding locally, and never
return IntermediateTensors for a next stage that does not exist for this
module. Single-rank behaviour is unchanged -- there `is_first_rank` and
`is_last_rank` are both True and the removed branches were already dead.
Measured on 2x Quadro RTX 8000 + 2x Tesla V100, Qwen3.8-Flash-Next-180B-NVFP4,
TP2 x PP2, k=4. Before: the boot dies as quoted above, on both final-rank
workers. After: the engine comes up and serves; three questions of 30 sentences
each behind 13004 tokens of context return 1200 tokens apiece at 25.2 to 28.0
tok/s with an acceptance length of 2.70 to 3.23.
Two tests cover the drafter's forward on a pipeline group that reports a
preceding and a following stage; without the fix both fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Peuqui <peuqui@github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Qwen4Exp with
--speculative-config method=mtpand--pipeline-parallel-sizegreater than one does not boot at all. Every worker on the final pipeline rank
dies while compiling:
The drafter is stage-local.
gpu_model_runner.execute_modelreturns theIntermediateTensors on every non-final pipeline rank before speculation is
reached, so
Qwen4ExpMultiTokenPredictoronly ever runs on the last rank. Itsweights are replicated rather than partitioned:
embed_tokens,fc_embedding,fc_hiddenand every MTP layer are built on all ranks.forwardnevertheless branched onget_pp_group().is_first_rank— thetarget model's pipeline position. On the last rank that is False, so the
drafter took the "receive from the previous stage" path and asserted on
intermediate tensors that nobody sends. Under the fullgraph AOT compile this is
a compile error rather than a runtime one, so it takes the whole boot down
instead of a single step.
The fix drops both pipeline branches in that module: always build the embedding
locally, and never return IntermediateTensors for a next stage that does not
exist for this module. Single-rank behaviour is unchanged — there
is_first_rankandis_last_rankare both True and the removed branches werealready dead code.
Results
2x Quadro RTX 8000 (sm75) + 2x Tesla V100 (sm70),
CUDA_VISIBLE_DEVICES=0,2,1,3,Qwen3.8-Flash-Next-180B-A4B-NVFP4 with a quantized MTP block, TP2 x PP2,
partition 24,24, k=4,
--max-model-len 16384.The "after" row is three questions of 30 sentences each behind 13004 tokens of
unrelated context, 1200 output tokens apiece, greedy with a fixed seed. All
three answers are complete and were read; the deliberately misspelled probe
term is recognized as a misspelling rather than hallucinated into a new
phenomenon.
Test Plan
Environment: checkout at
origin/main4f19ef7 with the compiled extensions ofa 1Cat-vLLM 1.5.0 wheel linked in (see Limitations).
Test Result
New file: 2 passed.
Directory without the AMD test: 280 passed, 10 failed, 3 skipped.
tests/models/qwen4_exp/test_qsa_amd.pyalone: 10 skipped.The failures are all
test_sm70_gdn_projection_split.py:: test_cuda_public_op_graph_replay_is_bitwise, and they are pre-existing andunrelated: the same file on the unmodified tree gives the identical
7 failed, 28 passed, before and after this change. They assert that a fusedpublic op route is taken (
assert route_hits == [rows, rows], actual[]),which our bench cannot satisfy because it runs main's Python against the
compiled ops of a 1.5.0 wheel.
Counter-check with
vllm/models/qwen4_exp/nvidia/mtp.pyreverted toorigin/main, both tests kept:pre-commit over both files: ruff check, ruff format, typos, mypy-local, SPDX
headers, root lazy imports, forbidden imports, torch.cuda-call check,
config-docstring check, attention-backend docs and the boolean-ops check all
Passed.
pre-commit run mypy-3.10 --hook-stage manual: Passed.Not a duplicate
Checked on 2026-09-08 against every open PR. One other open PR touches
vllm/models/qwen4_exp/nvidia/mtp.py: #553 ("NVIDIA Qwen3.8 Flash-Next MTPloader"). Its hunks in that file are at lines 22, 45, 67, 138, 152, 166, 209
and 493; this change is at 309–354.
is_first_rank,is_last_rankandintermediate_tensorsdo not occur in #553 at all. No open PR or issuementions pipeline parallelism together with the Qwen4Exp drafter.
Limitations
The unit tests and linting run against this tree. The boot and throughput
numbers come from a 1Cat-vLLM 1.5.0 wheel deployment carrying the same change,
because building current main from source on this hardware is a multi-hour CUDA
build. For the code that carries the measurement the two trees differ only in
this fix:
vllm/models/qwen4_exp/nvidia/mtp.pyonorigin/mainand thedeployed file are otherwise identical, and the "before" run above was produced
by putting main's file into that deployment unchanged.
AI assistance: this change was developed with Claude (Anthropic) as a coding
assistant. Every changed line was reviewed by me and the test runs above were
executed on my hardware; I can defend the change end to end.
🤖 Generated with Claude Code