Skip to content

Commit beb54c2

Browse files
LostFox11LostFox11
andauthored
[BugFix]support PP MTP mixed deployment (vllm-project#11076)
### What this PR does / why we need it? This PR supports V1 Pipeline Parallelism (PP) + MTP mixed deployment and fixes PP token handoff accuracy issues in batch-queue / multi-in-flight paths. Before this change, PP async sampled tokens were propagated through a direct GPU broadcast path between PP ranks. On Ascend this broadcast can introduce a forced synchronization and significantly reduce PP throughput. At the same time, PP + MTP needs draft/spec tokens to be written back to the exact scheduler output that produced them; otherwise a newer scheduled batch can make bookkeeping observe stale or mismatched request state. The updated design separates the two responsibilities: - PP confirmed sampled tokens use scheduler IPC consistently through `CachedRequestData.new_token_ids`. - Non-last PP ranks rebuild the local `prev_sampled_token_ids` / `prev_req_id_to_index` fast-path state from scheduler IPC instead of GPU broadcast. - PP + MTP draft/spec tokens are attached to `ModelRunnerOutput.spec_token_ids` and written back in `Scheduler.update_from_output`. - PP request-level in-flight fence applies to decode-ready requests, independent of whether MTP is enabled. - Intermediate prefill chunks are not fenced, so P/D mixed batches can still use prefill chunks to fill the PP pipeline. - PD-disaggregated prefill producer nodes and V2 model runner are explicitly excluded. #### Previous PP sampled-token handoff ```mermaid sequenceDiagram autonumber participant S as Scheduler participant P0 as Non-last PP rank participant PN as Last PP rank participant R as Request state S->>P0: schedule batch N P0->>PN: pipeline forward PN->>PN: sample confirmed token N PN-->>P0: GPU broadcast sampled token N P0->>P0: receive broadcast and patch local input_batch state PN-->>R: MTP draft tokens remain tied to live runner/request state S->>P0: schedule batch N+1 may happen before output N writeback S->>R: update output N later ``` #### New unified PP sampled-token IPC ```mermaid sequenceDiagram autonumber participant S as Scheduler participant P0 as Non-last PP rank participant PN as Last PP rank participant R as Request state S->>P0: schedule batch N P0->>PN: pipeline forward PN->>PN: sample confirmed token N PN-->>S: ModelRunnerOutput(sampled_token_ids, optional spec_token_ids) S->>R: update confirmed output token N S->>R: write spec_token_ids when PP + MTP is enabled S->>P0: schedule batch N+1 with CachedRequestData.new_token_ids P0->>P0: rebuild prev_sampled_token_ids / prev_req_id_to_index from IPC P0->>P0: build next input without PP GPU broadcast ``` #### Runtime scope and isolation ```mermaid flowchart LR C["vLLM config"] --> PP{"V1 PP?"} PP -- "no" --> SKIP["skip PP runtime patches"] PP -- "yes" --> PD{"PD prefill producer?"} PD -- "yes" --> SKIP PD -- "no" --> V2{"V2 model runner?"} V2 -- "yes" --> SKIP V2 -- "no" --> IPC["enable PP sampled-token IPC"] IPC --> FENCE["fence decode-ready requests"] IPC --> PACK["pack CachedRequestData.new_token_ids"] IPC --> LOCAL["non-last rank rebuilds local input state"] IPC --> SPEC{"speculative_config?"} SPEC -- "no" --> ORD["ordinary PP: sampled-token IPC only"] SPEC -- "yes" --> MTP["PP + MTP: sampled-token IPC + spec writeback"] MTP --> SPECWB["ModelRunnerOutput.spec_token_ids -> request.spec_token_ids"] ``` #### P/D mixed batch handling ```mermaid flowchart TD A["Scheduler._update_after_schedule()"] --> B{"request.is_prefill_chunk after scheduling?"} B -- "yes: intermediate prefill chunk" --> C["no fence"] C --> D["can be scheduled again to fill PP pipeline"] B -- "no: final prefill or decode" --> E["set request-level in-flight fence"] E --> F["release fence in update_from_output"] G["NPUModelRunner._update_states() on non-last PP rank"] --> H["read CachedRequestData.new_token_ids"] H --> I{"this request has real output token?"} I -- "no: intermediate prefill / empty token" --> J["do not add prev mapping"] I -- "yes: final prefill / decode" --> K["add prev_sampled_token_ids + prev_req_id_to_index"] K --> L["_prepare_input_ids() copies sampled token from IPC state"] ``` #### Full call flow ```mermaid flowchart TD A["EngineCore step"] --> B["Scheduler.schedule()"] subgraph SCHED["Scheduler side"] B --> C["_update_after_schedule()"] C --> D{"V1 PP, non-PD-P, non-V2?"} D -- "yes" --> E{"request still intermediate prefill chunk?"} E -- "yes" --> F["no fence"] E -- "no" --> G["set next_decode_eligible_step fence"] D -- "no" --> H["keep upstream behavior"] B --> I["_make_cached_request_data()"] I --> J{"PP runtime patch enabled?"} J -- "yes" --> K["pack confirmed sampled tokens into new_token_ids"] J -- "no" --> H end K --> L["NPUModelRunner.execute_model()"] H --> L subgraph RUNNER["Model runner"] L --> M["_update_states()"] M --> N{"non-last PP rank with new_token_ids?"} N -- "yes" --> O["rebuild prev_sampled_token_ids / prev_req_id_to_index"] N -- "no" --> P["no local PP IPC mapping"] O --> Q["_prepare_input_ids()"] P --> Q Q --> R["forward + sample_tokens()"] R --> S{"PP + speculative decoding?"} S -- "yes" --> T["attach spec_token_ids to ModelRunnerOutput"] S -- "no" --> U["ordinary ModelRunnerOutput"] end T --> V["Scheduler.update_from_output()"] U --> V subgraph WRITEBACK["Output writeback"] V --> W["upstream confirmed-token update"] V --> X["release PP in-flight fence"] V --> Y{"PP + MTP?"} Y -- "yes" --> Z["write request.spec_token_ids"] Y -- "no" --> AA["skip spec-token writeback"] end ``` ### Does this PR introduce _any_ user-facing change? No new CLI option or environment variable is introduced. Behaviorally, PP + MTP mixed deployment is now allowed and supported. The old platform validation that rejected non-PD-P PP + MTP configurations is removed. ### How was this patch tested? - `python3 -m py_compile vllm_ascend/worker/model_runner_v1.py vllm_ascend/patch/platform/patch_pp_mtp.py` - `bash format.sh ci` - Local `pytest` command was attempted earlier, but the local macOS Python environment does not have `torch`, so unit tests could not be executed locally. - CI should cover: - `tests/ut/test_platform.py` - `tests/ut/patch/platform/test_patch_pp_mtp.py` - `tests/e2e/pull_request/four_card/test_pipeline_parallel.py` - PP + MTP mixed-deployment accuracy workloads - vLLM version: v0.23.0 - vLLM main: vllm-project/vllm@b9a7cd4 --------- Signed-off-by: LostFox11 <wangziyue17@huawei.com> Co-authored-by: LostFox11 <wangziyue17@huawei.com>
1 parent 2eaff29 commit beb54c2

6 files changed

Lines changed: 725 additions & 97 deletions

File tree

tests/ut/patch/platform/test_patch_pp_mtp.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@
22

33
from types import SimpleNamespace
44

5+
import numpy as np
56
import pytest
7+
import torch
68
from vllm.config.model import ModelConfig
9+
from vllm.v1.core.sched.scheduler import Scheduler
10+
from vllm.v1.sample.rejection_sampler import PLACEHOLDER_TOKEN_ID
11+
12+
from vllm_ascend.patch.platform.patch_pp_mtp import (
13+
_update_pp_mtp_spec_token_ids,
14+
_use_pp_ipc_runtime_patch,
15+
)
16+
from vllm_ascend.worker.model_runner_v1 import NPUModelRunner
717

818

919
def test_model_config_validates_local_mtp_drafter_as_single_pp_rank(monkeypatch):
@@ -55,3 +65,259 @@ def test_model_config_keeps_target_model_pp_validation(monkeypatch):
5565

5666
with pytest.raises(NotImplementedError):
5767
ModelConfig.verify_with_parallel_config(model_config, parallel_config)
68+
69+
70+
@pytest.mark.parametrize(
71+
(
72+
"use_pp",
73+
"speculative_config",
74+
"async_scheduling",
75+
"use_v2_model_runner",
76+
"expected",
77+
),
78+
[
79+
(True, object(), False, False, True),
80+
(True, None, True, False, True),
81+
(True, None, False, False, True),
82+
(False, object(), True, False, False),
83+
(True, object(), True, True, False),
84+
],
85+
)
86+
def test_pp_ipc_runtime_patch_enabled_for_all_v1_pp(
87+
use_pp,
88+
speculative_config,
89+
async_scheduling,
90+
use_v2_model_runner,
91+
expected,
92+
):
93+
vllm_config = SimpleNamespace(
94+
kv_transfer_config=None,
95+
scheduler_config=SimpleNamespace(async_scheduling=async_scheduling),
96+
speculative_config=speculative_config,
97+
use_v2_model_runner=use_v2_model_runner,
98+
)
99+
100+
assert _use_pp_ipc_runtime_patch(vllm_config, use_pp) is expected
101+
102+
103+
def test_pp_ipc_runtime_patch_skips_pd_prefill_node():
104+
vllm_config = SimpleNamespace(
105+
kv_transfer_config=SimpleNamespace(
106+
is_kv_producer=True,
107+
is_kv_consumer=False,
108+
),
109+
scheduler_config=SimpleNamespace(async_scheduling=True),
110+
speculative_config=object(),
111+
use_v2_model_runner=False,
112+
)
113+
114+
assert _use_pp_ipc_runtime_patch(vllm_config, use_pp=True) is False
115+
116+
117+
@pytest.mark.parametrize("async_scheduling", [False, True])
118+
def test_pp_ipc_cached_request_data_carries_confirmed_token_for_sync_and_async(
119+
async_scheduling,
120+
):
121+
scheduler = Scheduler.__new__(Scheduler)
122+
scheduler.use_pp = True
123+
scheduler.use_v2_model_runner = False
124+
scheduler.scheduler_config = SimpleNamespace(async_scheduling=async_scheduling)
125+
scheduler.vllm_config = SimpleNamespace(
126+
kv_transfer_config=None,
127+
speculative_config=object(),
128+
use_v2_model_runner=False,
129+
)
130+
scheduler.prev_step_scheduled_req_ids = set()
131+
132+
request = SimpleNamespace(
133+
request_id="req-0",
134+
all_token_ids=[11, 12, 13],
135+
num_computed_tokens=2,
136+
num_output_tokens=1,
137+
num_output_placeholders=0,
138+
)
139+
blocks = SimpleNamespace(get_block_ids=lambda allow_none: ([0],))
140+
141+
cached_reqs_data = Scheduler._make_cached_request_data(
142+
scheduler,
143+
running_reqs=[request],
144+
resumed_reqs=[],
145+
num_scheduled_tokens={"req-0": 3},
146+
spec_decode_tokens={"req-0": [101, 102]},
147+
req_to_new_blocks={"req-0": blocks},
148+
)
149+
150+
assert cached_reqs_data.req_ids == ["req-0"]
151+
assert cached_reqs_data.new_token_ids == [[13]]
152+
assert scheduler.scheduler_config.async_scheduling is async_scheduling
153+
154+
155+
@pytest.mark.parametrize(
156+
("async_scheduling", "expected_new_token_ids"),
157+
[(False, [[]]), (True, [[13]])],
158+
)
159+
def test_pp_ipc_cached_request_data_fills_empty_confirmed_token_only_for_async(
160+
async_scheduling,
161+
expected_new_token_ids,
162+
):
163+
scheduler = Scheduler.__new__(Scheduler)
164+
scheduler.use_pp = True
165+
scheduler.use_v2_model_runner = False
166+
scheduler.scheduler_config = SimpleNamespace(async_scheduling=async_scheduling)
167+
scheduler.vllm_config = SimpleNamespace(
168+
kv_transfer_config=None,
169+
speculative_config=object(),
170+
use_v2_model_runner=False,
171+
)
172+
scheduler.prev_step_scheduled_req_ids = set()
173+
174+
request = SimpleNamespace(
175+
request_id="req-0",
176+
all_token_ids=[11, 12, 13],
177+
num_computed_tokens=3,
178+
num_output_tokens=1,
179+
num_output_placeholders=0,
180+
)
181+
blocks = SimpleNamespace(get_block_ids=lambda allow_none: ([0],))
182+
183+
cached_reqs_data = Scheduler._make_cached_request_data(
184+
scheduler,
185+
running_reqs=[request],
186+
resumed_reqs=[],
187+
num_scheduled_tokens={"req-0": 2},
188+
spec_decode_tokens={"req-0": [101, 102]},
189+
req_to_new_blocks={"req-0": blocks},
190+
)
191+
192+
assert cached_reqs_data.new_token_ids == expected_new_token_ids
193+
assert scheduler.scheduler_config.async_scheduling is async_scheduling
194+
195+
196+
def test_pp_ipc_sampled_token_handoff_advances_async_non_last_rank_state(
197+
monkeypatch,
198+
):
199+
monkeypatch.setattr(
200+
"vllm_ascend.worker.model_runner_v1.get_pp_group",
201+
lambda: SimpleNamespace(is_last_rank=False),
202+
)
203+
204+
runner = NPUModelRunner.__new__(NPUModelRunner)
205+
runner.is_kv_producer = False
206+
runner.is_kv_consumer = False
207+
runner.use_async_scheduling = True
208+
runner.device = torch.device("cpu")
209+
runner.discard_request_mask = SimpleNamespace(
210+
np=np.zeros(2, dtype=bool),
211+
)
212+
runner.input_batch = SimpleNamespace(
213+
num_reqs=2,
214+
req_ids=["req-0", "req-1"],
215+
prev_sampled_token_ids=None,
216+
prev_req_id_to_index={},
217+
num_tokens_no_spec=np.array([3, 5], dtype=np.int64),
218+
is_token_ids=np.zeros((2, 8), dtype=bool),
219+
)
220+
runner.requests = {
221+
"req-0": SimpleNamespace(output_token_ids=[31]),
222+
"req-1": SimpleNamespace(output_token_ids=[41, 42]),
223+
}
224+
scheduler_output = SimpleNamespace(
225+
scheduled_cached_reqs=SimpleNamespace(
226+
req_ids=["req-0", "req-1"],
227+
new_token_ids=[[101], [202]],
228+
num_output_tokens=[1, 2],
229+
),
230+
)
231+
232+
runner._apply_pp_sampled_tokens_from_scheduler_output(scheduler_output)
233+
234+
assert runner.input_batch.prev_req_id_to_index == {
235+
"req-0": 0,
236+
"req-1": 1,
237+
}
238+
assert runner.input_batch.prev_sampled_token_ids.tolist() == [[101], [202]]
239+
assert runner.requests["req-0"].output_token_ids == [
240+
31,
241+
PLACEHOLDER_TOKEN_ID,
242+
]
243+
assert runner.requests["req-1"].output_token_ids == [
244+
41,
245+
42,
246+
PLACEHOLDER_TOKEN_ID,
247+
]
248+
assert runner.input_batch.is_token_ids[0, 3]
249+
assert runner.input_batch.is_token_ids[1, 5]
250+
assert runner.input_batch.num_tokens_no_spec.tolist() == [4, 6]
251+
252+
253+
def test_pp_ipc_sampled_token_handoff_keeps_sync_path_on_scheduler_tokens(
254+
monkeypatch,
255+
):
256+
monkeypatch.setattr(
257+
"vllm_ascend.worker.model_runner_v1.get_pp_group",
258+
lambda: SimpleNamespace(is_last_rank=False),
259+
)
260+
261+
runner = NPUModelRunner.__new__(NPUModelRunner)
262+
runner.is_kv_producer = False
263+
runner.is_kv_consumer = False
264+
runner.use_async_scheduling = False
265+
runner.device = torch.device("cpu")
266+
runner.input_batch = SimpleNamespace(
267+
num_reqs=1,
268+
req_ids=["req-0"],
269+
prev_sampled_token_ids="keep",
270+
prev_req_id_to_index={"keep": 0},
271+
num_tokens_no_spec=np.array([3], dtype=np.int64),
272+
is_token_ids=np.zeros((1, 8), dtype=bool),
273+
)
274+
runner.requests = {
275+
"req-0": SimpleNamespace(output_token_ids=[31]),
276+
}
277+
scheduler_output = SimpleNamespace(
278+
scheduled_cached_reqs=SimpleNamespace(
279+
req_ids=["req-0"],
280+
new_token_ids=[[101]],
281+
num_output_tokens=[1],
282+
),
283+
)
284+
285+
runner._apply_pp_sampled_tokens_from_scheduler_output(scheduler_output)
286+
287+
assert runner.input_batch.prev_req_id_to_index == {"keep": 0}
288+
assert runner.input_batch.prev_sampled_token_ids == "keep"
289+
assert runner.requests["req-0"].output_token_ids == [31]
290+
assert not runner.input_batch.is_token_ids[0, 3]
291+
assert runner.input_batch.num_tokens_no_spec.tolist() == [3]
292+
293+
294+
@pytest.mark.parametrize("async_scheduling", [False, True])
295+
def test_pp_mtp_spec_tokens_are_written_from_model_runner_output_for_sync_and_async(
296+
async_scheduling,
297+
):
298+
request = SimpleNamespace(
299+
spec_token_ids=[],
300+
structured_output_request=None,
301+
is_finished=lambda: False,
302+
)
303+
scheduler = SimpleNamespace(
304+
scheduler_config=SimpleNamespace(async_scheduling=async_scheduling),
305+
requests={"req-0": request},
306+
structured_output_manager=SimpleNamespace(
307+
should_advance=lambda _request: False,
308+
),
309+
)
310+
scheduler_output = SimpleNamespace(num_scheduled_tokens={"req-0": 1})
311+
model_runner_output = SimpleNamespace(
312+
req_id_to_index={"req-0": 0},
313+
sampled_token_ids=[[200]],
314+
spec_token_ids=[[301, 302]],
315+
)
316+
317+
_update_pp_mtp_spec_token_ids(
318+
scheduler,
319+
scheduler_output,
320+
model_runner_output,
321+
)
322+
323+
assert request.spec_token_ids == [301, 302]

tests/ut/test_platform.py

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -783,48 +783,6 @@ def test_validate_parallel_config_accepts_neither(self):
783783

784784
self.platform._validate_parallel_config(vllm_config)
785785

786-
def test_validate_pd_pp_mtp_config_accepts_prefill_producer(self):
787-
vllm_config = TestNPUPlatform.mock_vllm_config()
788-
vllm_config.speculative_config = MagicMock(method="mtp")
789-
vllm_config.parallel_config.pipeline_parallel_size = 2
790-
vllm_config.kv_transfer_config = MagicMock(is_kv_producer=True, kv_role="kv_producer")
791-
792-
self.platform._validate_pd_pp_mtp_config(vllm_config)
793-
794-
def test_validate_pd_pp_mtp_config_accepts_decode_dp_mtp(self):
795-
vllm_config = TestNPUPlatform.mock_vllm_config()
796-
vllm_config.speculative_config = MagicMock(method="mtp")
797-
vllm_config.parallel_config.pipeline_parallel_size = 1
798-
vllm_config.kv_transfer_config = MagicMock(is_kv_producer=False, kv_role="kv_consumer")
799-
800-
self.platform._validate_pd_pp_mtp_config(vllm_config)
801-
802-
def test_validate_pd_pp_mtp_config_rejects_decode_pp_mtp(self):
803-
vllm_config = TestNPUPlatform.mock_vllm_config()
804-
vllm_config.speculative_config = MagicMock(method="mtp")
805-
vllm_config.parallel_config.pipeline_parallel_size = 2
806-
vllm_config.kv_transfer_config = MagicMock(is_kv_producer=False, kv_role="kv_consumer")
807-
808-
with pytest.raises(ValueError, match=r"PP\+MTP.*P nodes.*D nodes.*pipeline_parallel_size=1"):
809-
self.platform._validate_pd_pp_mtp_config(vllm_config)
810-
811-
def test_validate_pd_pp_mtp_config_rejects_non_pd_pp_mtp(self):
812-
vllm_config = TestNPUPlatform.mock_vllm_config()
813-
vllm_config.speculative_config = MagicMock(method="mtp")
814-
vllm_config.parallel_config.pipeline_parallel_size = 2
815-
vllm_config.kv_transfer_config = None
816-
817-
with pytest.raises(ValueError, match=r"PP\+MTP.*PD-disaggregated P nodes"):
818-
self.platform._validate_pd_pp_mtp_config(vllm_config)
819-
820-
def test_validate_pd_pp_mtp_config_allows_non_mtp_spec_decode(self):
821-
vllm_config = TestNPUPlatform.mock_vllm_config()
822-
vllm_config.speculative_config = MagicMock(method="eagle")
823-
vllm_config.parallel_config.pipeline_parallel_size = 2
824-
vllm_config.kv_transfer_config = None
825-
826-
self.platform._validate_pd_pp_mtp_config(vllm_config)
827-
828786
@patch("vllm_ascend.quantization.utils.maybe_auto_detect_quantization")
829787
@patch("vllm_ascend.utils.get_ascend_device_type", return_value=AscendDeviceType.A3)
830788
@patch("vllm_ascend.ascend_config.init_ascend_config")

0 commit comments

Comments
 (0)