-
-
Notifications
You must be signed in to change notification settings - Fork 21k
[Spec][V2] Support MTP speculative decoding under pipeline parallelism #46994
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
eastwood-c
wants to merge
15
commits into
vllm-project:main
Choose a base branch
from
eastwood-c:v2-mtp-pp-rebase
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+434
−17
Open
Changes from 14 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
74c56ed
[Spec][V2] Support MTP speculative decoding under pipeline parallelism
eastwood-c c175667
[Spec][V2] Fix stale topk_indices_buffer in sparse MLA backends under…
eastwood-c f99ab4c
[Spec][V2] Apply fc projection on last PP rank for Qwen3.5 MTP
eastwood-c f3b6c0b
[Spec][V2] Fix stale topk_indices_buffer in remaining sparse MLA back…
eastwood-c 01d006b
[Spec][V2] Add tests for MTP+PP fixes
eastwood-c f56a4a8
Merge remote-tracking branch 'origin/main' into v2-mtp-pp-rebase
eastwood-c 1cf76e9
[Spec][V2] Fix test imports and clean up MTP+PP test docstrings
eastwood-c 517b981
[Spec][V2] Fix propose() kwarg name and PP-group access at PP=1
eastwood-c 7f5a315
Merge remote-tracking branch 'origin/main' into v2-mtp-pp-rebase
eastwood-c f881617
Merge remote-tracking branch 'origin/main' into v2-mtp-pp-rebase
eastwood-c 6f54d3c
[Spec][V2] Load the MTP draft's own embed_tokens under pipeline paral…
eastwood-c 17cf146
Merge remote-tracking branch 'origin/main' into v2-mtp-pp-rebase
eastwood-c 89d2788
Merge remote-tracking branch 'origin/main' into v2-mtp-pp-rebase
eastwood-c e349a56
Merge remote-tracking branch 'origin/main' into v2-mtp-pp-rebase
eastwood-c 56866a1
Update tests/v1/worker/test_pp_utils.py
eastwood-c File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """Unit tests for the PPHandler sampled-token / draft-token relay under PP.""" | ||
|
|
||
| from types import SimpleNamespace | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
| import torch | ||
|
|
||
| from vllm.v1.worker.gpu import pp_utils | ||
| from vllm.v1.worker.gpu.pp_utils import PPHandler | ||
|
|
||
| requires_cuda = pytest.mark.skipif( | ||
| not torch.cuda.is_available(), reason="PPHandler drives a side CUDA stream" | ||
| ) | ||
|
|
||
|
|
||
| def make_handler( | ||
| monkeypatch, | ||
| *, | ||
| is_last_rank: bool, | ||
| num_speculative_steps: int, | ||
| relay_draft_tokens: bool, | ||
| world_size: int = 2, | ||
| ) -> PPHandler: | ||
| """Build a real PPHandler with the PP group stubbed out.""" | ||
| pp_group = SimpleNamespace( | ||
| is_last_rank=is_last_rank, | ||
| last_rank=world_size - 1, | ||
| world_size=world_size, | ||
| make_sibling_device_group=lambda group_desc: object(), | ||
| ) | ||
| monkeypatch.setattr(pp_utils, "get_pp_group", lambda: pp_group) | ||
| return PPHandler( | ||
| max_num_reqs=8, | ||
| num_speculative_steps=num_speculative_steps, | ||
| device=torch.device("cuda"), | ||
| relay_draft_tokens=relay_draft_tokens, | ||
| ) | ||
|
|
||
|
|
||
| def record_broadcasts(monkeypatch) -> list[torch.Tensor]: | ||
| """Capture every tensor handed to the collective, in call order.""" | ||
| calls: list[torch.Tensor] = [] | ||
| monkeypatch.setattr( | ||
| torch.distributed, "broadcast", lambda t, src, group: calls.append(t) | ||
| ) | ||
| return calls | ||
|
|
||
|
|
||
| def make_input_batch(num_reqs: int = 3, *, needs_sample: bool = True): | ||
| # compute_need_sampled_mask only reads these fields. With needs_sample=False | ||
| # every request is already at max_seq_len, so no sample is needed next step. | ||
| return SimpleNamespace( | ||
| num_reqs=num_reqs, | ||
| num_computed_tokens_np=np.zeros(num_reqs, dtype=np.int32), | ||
| prefill_len_np=np.full(num_reqs, 4, dtype=np.int32), | ||
| num_scheduled_tokens=np.full(num_reqs, 4, dtype=np.int32), | ||
| max_seq_len_np=np.full(num_reqs, 100 if needs_sample else 1, dtype=np.int32), | ||
| idx_mapping=torch.arange(num_reqs, device="cuda"), | ||
| idx_mapping_np=np.arange(num_reqs, dtype=np.int32), | ||
| ) | ||
|
|
||
|
|
||
| def send_step(handler, input_batch, *, width: int, with_draft: bool): | ||
| num_reqs = input_batch.num_reqs | ||
| sampled = torch.zeros(num_reqs, width, dtype=torch.int64, device="cuda") | ||
| counts = torch.zeros(num_reqs, dtype=torch.int32, device="cuda") | ||
| handler.broadcast(sampled, counts, counts, input_batch) | ||
| if with_draft: | ||
| draft = torch.zeros( | ||
| num_reqs, handler.max_sample_len - 1, dtype=torch.int64, device="cuda" | ||
| ) | ||
| handler.broadcast_draft(draft, input_batch) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # broadcast() pads so send/recv element counts match on every step | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| @requires_cuda | ||
| @pytest.mark.parametrize("width,num_spec", [(1, 1), (1, 3), (2, 3)]) | ||
| def test_broadcast_pads_sampled_tokens_to_max_sample_len(monkeypatch, width, num_spec): | ||
| """The sampler emits width 1 on steps with no draft tokens (prefill, first | ||
| decode) and num_spec+1 only after rejection sampling. The receiver always | ||
| posts a max_sample_len buffer, so an unpadded send is a count mismatch.""" | ||
| handler = make_handler( | ||
| monkeypatch, | ||
| is_last_rank=True, | ||
| num_speculative_steps=num_spec, | ||
| relay_draft_tokens=True, | ||
| ) | ||
| calls = record_broadcasts(monkeypatch) | ||
| input_batch = make_input_batch() | ||
|
|
||
| send_step(handler, input_batch, width=width, with_draft=False) | ||
|
|
||
| sent_sampled = calls[0] | ||
| assert sent_sampled.shape == (input_batch.num_reqs, handler.max_sample_len) | ||
| # Placeholder columns are ignored by post_update, which advances each | ||
| # request by its own num_sampled count. | ||
| assert (sent_sampled[:, width:] == -1).all() | ||
| assert (sent_sampled[:, :width] == 0).all() | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Sender and receiver must post the same number of collectives per step | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| @requires_cuda | ||
| def test_send_and_recv_op_counts_match_with_speculator(monkeypatch): | ||
| """With a speculator the step is three broadcasts: sampled, combined, draft.""" | ||
| sender = make_handler( | ||
| monkeypatch, is_last_rank=True, num_speculative_steps=3, relay_draft_tokens=True | ||
| ) | ||
| calls = record_broadcasts(monkeypatch) | ||
| send_step(sender, make_input_batch(), width=1, with_draft=True) | ||
| assert len(calls) == 3 | ||
|
|
||
| receiver = make_handler( | ||
| monkeypatch, | ||
| is_last_rank=False, | ||
| num_speculative_steps=3, | ||
| relay_draft_tokens=True, | ||
| ) | ||
| calls.clear() | ||
| assert receiver.receive(make_input_batch()) | ||
| assert len(calls) == 3 | ||
| assert calls[2].shape == (3, sender.max_sample_len - 1) | ||
|
|
||
|
|
||
| @requires_cuda | ||
| def test_send_and_recv_op_counts_match_without_speculator(monkeypatch): | ||
| """Diffusion LLMs set num_speculative_steps > 0 but have no speculator, so | ||
| the last rank never relays draft tokens. Gating the receiver's third recv on | ||
| num_speculative_steps instead of on the speculator hangs the non-last ranks | ||
| waiting for a broadcast that is never issued.""" | ||
| sender = make_handler( | ||
| monkeypatch, | ||
| is_last_rank=True, | ||
| num_speculative_steps=3, | ||
| relay_draft_tokens=False, | ||
| ) | ||
| calls = record_broadcasts(monkeypatch) | ||
| send_step(sender, make_input_batch(), width=1, with_draft=False) | ||
| assert len(calls) == 2 | ||
|
|
||
| receiver = make_handler( | ||
| monkeypatch, | ||
| is_last_rank=False, | ||
| num_speculative_steps=3, | ||
| relay_draft_tokens=False, | ||
| ) | ||
| calls.clear() | ||
| assert receiver.receive(make_input_batch()) | ||
| assert len(calls) == 2 | ||
| assert receiver.queue[-1].draft_tokens is None | ||
|
|
||
|
|
||
| @requires_cuda | ||
| def test_both_ranks_skip_when_no_request_needs_sampling(monkeypatch): | ||
| """The skip gate must be symmetric, or the ranks desynchronize.""" | ||
| sender = make_handler( | ||
| monkeypatch, is_last_rank=True, num_speculative_steps=3, relay_draft_tokens=True | ||
| ) | ||
| calls = record_broadcasts(monkeypatch) | ||
| send_step(sender, make_input_batch(needs_sample=False), width=1, with_draft=True) | ||
| assert calls == [] | ||
|
|
||
| receiver = make_handler( | ||
| monkeypatch, | ||
| is_last_rank=False, | ||
| num_speculative_steps=3, | ||
| relay_draft_tokens=True, | ||
| ) | ||
| calls.clear() | ||
| assert not receiver.receive(make_input_batch(needs_sample=False)) | ||
| assert calls == [] | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Relayed draft tokens survive the deferred consume | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| @requires_cuda | ||
| def test_relayed_draft_tokens_reach_get_prev_sampled_outputs(monkeypatch): | ||
| """Draft tokens received at step T must come back out pp_size steps later, | ||
| so the next combine_sampled_and_draft_tokens reads real values rather than | ||
| zero-init.""" | ||
| receiver = make_handler( | ||
| monkeypatch, | ||
| is_last_rank=False, | ||
| num_speculative_steps=3, | ||
| relay_draft_tokens=True, | ||
| world_size=2, | ||
| ) | ||
| record_broadcasts(monkeypatch) | ||
| receiver.receive(make_input_batch()) | ||
|
|
||
| # The pre-seeded placeholders drain first; the entry lands pp_size steps on. | ||
| outputs = None | ||
| for _ in range(3): | ||
| outputs = receiver.get_prev_sampled_outputs() | ||
| if outputs is not None: | ||
| break | ||
| assert outputs is not None | ||
| assert outputs["draft_tokens"] is not None | ||
| assert outputs["draft_tokens"].shape == (3, receiver.max_sample_len - 1) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # DeepSeekMTP under pipeline parallelism | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_deepseek_mtp_passes_supports_pp_gate(): | ||
| """DeepSeekMTP must pass the supports_pp() gate used at model resolution; | ||
| otherwise the engine refuses to build it under pipeline parallelism. The | ||
| gate covers both the SupportsPP MRO entry and the forward() signature.""" | ||
| from vllm.model_executor.models.deepseek_mtp import DeepSeekMTP | ||
| from vllm.model_executor.models.interfaces import supports_pp | ||
|
|
||
| assert supports_pp(DeepSeekMTP) | ||
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.