Skip to content

Commit b2f683c

Browse files
authored
[Feature][MRv2] pcp/dcp public logic modification for MRv2 (vllm-project#12595)
### What this PR does / why we need it? Modifications to the public logic of PCP DCP in Model Runner v2. 1. Removed assertion error reporting. 2. Added a new subclass, pcp_manager, to construct the Ascend PCP manager. 3. In Model Runner, the pcp_manager is called to split a request into two separate requests: a header and a tail. ### Does this PR introduce _any_ user-facing change? No ### How was this patch tested? - vLLM version: v0.25.1 - vLLM main: vllm-project/vllm@d02df74 --------- Signed-off-by: weiguihua2 <weiguihua2@huawei.com>
1 parent 32a59d4 commit b2f683c

4 files changed

Lines changed: 381 additions & 9 deletions

File tree

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Adapt from https://github.com/vllm-project/vllm/blob/main/vllm/v1/worker/gpu/model_runner.py
2+
# SPDX-License-Identifier: Apache-2.0
3+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4+
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
# This file is a part of the vllm-ascend project.
18+
#
19+
from types import SimpleNamespace
20+
from unittest.mock import MagicMock, patch
21+
22+
import numpy as np
23+
import pytest
24+
import torch
25+
from vllm.v1.worker.gpu.input_batch import InputBatch
26+
27+
from vllm_ascend.utils import vllm_version_is
28+
29+
if not vllm_version_is("0.25.1"):
30+
from vllm.v1.worker.gpu.pcp_manager import PCPManager
31+
32+
import vllm_ascend.worker.v2.pcp_manager as pcp_manager_module
33+
from vllm_ascend.worker.v2.input_batch import AscendInputBatch, AscendInputBuffers
34+
from vllm_ascend.worker.v2.pcp_manager import (
35+
AscendPCPManager,
36+
maybe_build_ascend_pcp_manager,
37+
)
38+
39+
40+
def _mock_async_copy_to_cpu(value, out=None, device=None):
41+
"""Copy PCP metadata without requiring device hooks in CPU-only UTs."""
42+
if isinstance(value, np.ndarray):
43+
value = torch.from_numpy(value)
44+
elif not isinstance(value, torch.Tensor):
45+
value = torch.as_tensor(value)
46+
47+
if out is not None:
48+
out.copy_(value)
49+
return out
50+
51+
return value.to(device="cpu")
52+
53+
54+
def _make_local_pcp_batch():
55+
"""Build a local batch in the shape returned by the community PCP manager."""
56+
input_buffers = AscendInputBuffers(
57+
max_num_reqs=4,
58+
max_num_tokens=16,
59+
device=torch.device("cpu"),
60+
)
61+
base_batch = InputBatch.make_dummy(
62+
num_reqs=2,
63+
num_tokens=6,
64+
input_buffers=input_buffers,
65+
)
66+
67+
# Local PCP rows: one starts at position 6 and contains two tokens; the
68+
# other starts at position 13 and contains four tokens.
69+
base_batch.req_ids = ["req-head", "req-tail"]
70+
base_batch.idx_mapping = torch.tensor([3, 7], dtype=torch.int32)
71+
base_batch.idx_mapping_np = np.array([3, 7], dtype=np.int32)
72+
base_batch.expanded_idx_mapping = base_batch.idx_mapping
73+
base_batch.num_scheduled_tokens = np.array([2, 4], dtype=np.int32)
74+
base_batch.query_start_loc_np = np.array([0, 2, 6], dtype=np.int32)
75+
base_batch.query_start_loc.copy_(torch.tensor([0, 2, 6], dtype=torch.int32))
76+
base_batch.num_computed_tokens_np = np.array([6, 13], dtype=np.int32)
77+
base_batch.prefill_len_np = np.array([32, 32], dtype=np.int32)
78+
base_batch.num_computed_prefill_tokens_np = np.array([6, 13], dtype=np.int32)
79+
base_batch.is_prefilling_np = np.array([True, True])
80+
base_batch.seq_lens.copy_(torch.tensor([8, 17], dtype=torch.int32))
81+
base_batch.seq_lens_cpu_upper_bound = torch.tensor([500, 600], dtype=torch.int32)
82+
base_batch.input_ids.copy_(torch.tensor([10, 11, 20, 21, 22, 23], dtype=torch.int32))
83+
base_batch.positions.copy_(torch.tensor([6, 7, 13, 14, 15, 16], dtype=torch.int64))
84+
base_batch.is_padding.fill_(False)
85+
86+
return AscendInputBatch(
87+
**base_batch.__dict__,
88+
seq_lens_np=np.array([101, 102], dtype=np.int32),
89+
attn_state="global-attn-state",
90+
)
91+
92+
93+
def _make_global_pcp_batch():
94+
"""Build the global batch that is passed into PCPManager.partition_batch."""
95+
input_buffers = AscendInputBuffers(
96+
max_num_reqs=4,
97+
max_num_tokens=32,
98+
device=torch.device("cpu"),
99+
)
100+
base_batch = InputBatch.make_dummy(
101+
num_reqs=1,
102+
num_tokens=18,
103+
input_buffers=input_buffers,
104+
)
105+
base_batch.req_ids = ["global-req"]
106+
base_batch.idx_mapping = torch.tensor([3], dtype=torch.int32)
107+
base_batch.idx_mapping_np = np.array([3], dtype=np.int32)
108+
base_batch.expanded_idx_mapping = base_batch.idx_mapping
109+
base_batch.num_scheduled_tokens = np.array([18], dtype=np.int32)
110+
base_batch.query_start_loc_np = np.array([0, 18], dtype=np.int32)
111+
base_batch.query_start_loc.copy_(torch.tensor([0, 18], dtype=torch.int32))
112+
base_batch.num_computed_tokens_np = np.array([0], dtype=np.int32)
113+
base_batch.prefill_len_np = np.array([18], dtype=np.int32)
114+
base_batch.num_computed_prefill_tokens_np = np.array([0], dtype=np.int32)
115+
base_batch.is_prefilling_np = np.array([True])
116+
base_batch.seq_lens.copy_(torch.tensor([18], dtype=torch.int32))
117+
base_batch.seq_lens_cpu_upper_bound = torch.tensor([18], dtype=torch.int32)
118+
base_batch.input_ids.copy_(torch.arange(18, dtype=torch.int32))
119+
base_batch.positions.copy_(torch.arange(18, dtype=torch.int64))
120+
base_batch.is_padding.fill_(False)
121+
122+
return AscendInputBatch(
123+
**base_batch.__dict__,
124+
seq_lens_np=np.array([18], dtype=np.int32),
125+
attn_state="global-attn-state",
126+
)
127+
128+
129+
@pytest.mark.skipif(vllm_version_is("0.25.1"), reason="requires vllm main branch")
130+
def test_partition_batch_refreshes_local_ascend_input_batch_metadata():
131+
"""Refresh Ascend metadata after the real PCP local-batch rewrite."""
132+
vllm_config = object()
133+
global_batch = _make_global_pcp_batch()
134+
req_states = SimpleNamespace(
135+
last_sampled_tokens=torch.zeros(4, dtype=torch.int64),
136+
prefill_len=SimpleNamespace(gpu=torch.zeros(4, dtype=torch.int32)),
137+
draft_tokens=torch.empty((4, 0), dtype=torch.int64),
138+
)
139+
manager = AscendPCPManager(
140+
pcp_world_size=2,
141+
pcp_rank=0,
142+
device=torch.device("cpu"),
143+
vllm_config=vllm_config,
144+
req_states=req_states,
145+
max_num_reqs=1,
146+
max_num_tokens=18,
147+
)
148+
attn_state = MagicMock()
149+
150+
with (
151+
# This Triton helper is unrelated to PCP partitioning and has no CPU
152+
# implementation. Stub only it; AscendPCPManager.partition_batch and
153+
# PCPManager.partition_batch both execute unmocked below.
154+
patch(
155+
"vllm.v1.worker.gpu.pcp_manager.prepare_pos_seq_lens",
156+
return_value=None,
157+
),
158+
patch(
159+
"vllm.v1.worker.gpu.pcp_manager.combine_sampled_and_draft_tokens",
160+
return_value=torch.zeros(2, dtype=torch.int64),
161+
),
162+
patch(
163+
"vllm.v1.worker.gpu.pcp_manager.async_copy_to_gpu",
164+
side_effect=_mock_async_copy_to_cpu,
165+
),
166+
patch.object(pcp_manager_module, "build_attn_state", return_value=attn_state) as build_attn_state,
167+
):
168+
result = manager.partition_batch(global_batch)
169+
170+
assert isinstance(result, AscendInputBatch)
171+
assert result is not global_batch
172+
assert manager._global_batch is global_batch
173+
np.testing.assert_array_equal(global_batch.seq_lens_np, np.array([18], dtype=np.int32))
174+
assert global_batch.attn_state == "global-attn-state"
175+
176+
# PCP=2 rank 0 owns the tail chunk then the head chunk; the real base
177+
# implementation produces this local row order and pads to rank 1's size.
178+
assert result.req_ids == ["global-req", "global-req"]
179+
np.testing.assert_array_equal(result.idx_mapping_np, np.array([3, 3], dtype=np.int32))
180+
np.testing.assert_array_equal(result.num_scheduled_tokens, np.array([3, 5], dtype=np.int32))
181+
np.testing.assert_array_equal(result.query_start_loc_np, np.array([0, 3, 8], dtype=np.int32))
182+
assert result.num_tokens == 8
183+
assert result.num_tokens_after_padding == 10
184+
assert torch.equal(result.input_ids[:8], torch.tensor([15, 16, 17, 0, 1, 2, 3, 4], dtype=torch.int32))
185+
186+
# dataclasses.replace() retains the global Ascend-only fields by default;
187+
# the override must refresh them from real PCP-local CPU rows.
188+
expected_seq_lens = np.array([18, 5], dtype=np.int32)
189+
np.testing.assert_array_equal(result.seq_lens_np, expected_seq_lens)
190+
assert result.attn_state is attn_state
191+
192+
args = build_attn_state.call_args.args
193+
assert args[0] is vllm_config
194+
np.testing.assert_array_equal(args[1], expected_seq_lens)
195+
assert args[2] == 2
196+
np.testing.assert_array_equal(args[3], np.array([3, 5], dtype=np.int32))
197+
np.testing.assert_array_equal(args[4], np.array([3, 5], dtype=np.int32))
198+
199+
200+
@pytest.mark.skipif(vllm_version_is("0.25.1"), reason="requires vllm main branch")
201+
def test_maybe_build_ascend_pcp_manager_returns_none_when_pcp_is_disabled():
202+
vllm_config = SimpleNamespace(
203+
parallel_config=SimpleNamespace(prefill_context_parallel_size=1),
204+
)
205+
206+
assert (
207+
maybe_build_ascend_pcp_manager(
208+
vllm_config,
209+
torch.device("cpu"),
210+
supports_mm_inputs=False,
211+
req_states=MagicMock(),
212+
block_tables=MagicMock(),
213+
)
214+
is None
215+
)
216+
217+
218+
@pytest.mark.skipif(vllm_version_is("0.25.1"), reason="requires vllm main branch")
219+
def test_maybe_build_ascend_pcp_manager_uses_ascend_subclass():
220+
vllm_config = SimpleNamespace(
221+
parallel_config=SimpleNamespace(
222+
prefill_context_parallel_size=2,
223+
decode_context_parallel_size=2,
224+
cp_kv_cache_interleave_size=4,
225+
),
226+
scheduler_config=SimpleNamespace(max_num_seqs=8, max_num_batched_tokens=32),
227+
)
228+
pcp_group = SimpleNamespace(rank_in_group=1)
229+
dcp_group = SimpleNamespace(rank_in_group=0)
230+
req_states = MagicMock()
231+
232+
with (
233+
patch.object(PCPManager, "validate_config") as validate_config,
234+
patch.object(pcp_manager_module, "get_pcp_group", return_value=pcp_group),
235+
patch.object(pcp_manager_module, "get_dcp_group", return_value=dcp_group),
236+
):
237+
manager = maybe_build_ascend_pcp_manager(
238+
vllm_config,
239+
torch.device("cpu"),
240+
supports_mm_inputs=False,
241+
req_states=req_states,
242+
block_tables=None,
243+
)
244+
245+
assert isinstance(manager, AscendPCPManager)
246+
assert manager.vllm_config is vllm_config
247+
assert manager.pcp_world_size == 2
248+
assert manager.pcp_rank == 1
249+
assert manager.dcp_world_size == 2
250+
assert manager.dcp_rank == 0
251+
assert manager.cp_interleave == 4
252+
validate_config.assert_called_once_with(vllm_config, False)

vllm_ascend/worker/v2/block_table.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,27 +68,29 @@ def compute_slot_mappings(
6868
query_start_loc: torch.Tensor,
6969
positions: torch.Tensor,
7070
num_tokens_padded: int,
71+
out: torch.Tensor | None = None,
7172
) -> torch.Tensor:
7273
num_reqs = idx_mapping.shape[0]
7374
num_groups = self.num_kv_cache_groups
75+
slot_mappings = self.slot_mappings if out is None else out
7476
_compute_slot_mappings_kernel[(num_groups, num_reqs + 1)](
75-
self.max_num_batched_tokens,
77+
slot_mappings.shape[1],
7678
idx_mapping,
7779
query_start_loc,
7880
positions,
7981
self.block_table_ptrs,
8082
self.block_table_strides,
8183
self.block_sizes_tensor,
82-
self.slot_mappings,
83-
self.slot_mappings.stride(0),
84+
slot_mappings,
85+
slot_mappings.stride(0),
8486
self.cp_rank,
8587
CP_SIZE=self.cp_size,
8688
CP_INTERLEAVE=self.cp_interleave,
8789
PAD_ID=PAD_SLOT_ID,
8890
TRITON_BLOCK_SIZE=1024, # type: ignore
8991
TOTAL_BLOCK_SIZE=4096,
9092
)
91-
return self.slot_mappings[:, :num_tokens_padded]
93+
return slot_mappings[:, :num_tokens_padded]
9294

9395

9496
@triton.jit

vllm_ascend/worker/v2/model_runner.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
if not vllm_version_is("0.25.1"):
4242
from vllm.v1.worker.gpu.model_runner import sort_batch_req_ids
4343

44+
from vllm_ascend.worker.v2.pcp_manager import maybe_build_ascend_pcp_manager
45+
4446
from vllm_ascend.ascend_config import get_ascend_config
4547
from vllm_ascend.ascend_forward_context import (
4648
MoECommType,
@@ -67,12 +69,7 @@ def __init__(self, vllm_config: VllmConfig, device: torch.device):
6769
# Ascend-specific configurations
6870
self.ascend_config = get_ascend_config()
6971
# The following features are not yet supported in Ascend NPU model runner v2:
70-
# - Context parallelism (prefill or decode)
7172
# - Dynamic EPLB
72-
parallel_config = vllm_config.parallel_config
73-
if parallel_config.prefill_context_parallel_size > 1 or parallel_config.decode_context_parallel_size > 1:
74-
raise NotImplementedError("Context parallelism is not supported by Ascend NPU model runner v2.")
75-
7673
if self.ascend_config.eplb_config.dynamic_eplb:
7774
raise NotImplementedError("dynamic_eplb is not supported by Ascend NPU model runner v2.")
7875

@@ -136,6 +133,17 @@ def initialize_kv_cache(self, kv_cache_config: KVCacheConfig) -> None:
136133
with graph_manager_wrapper(self):
137134
super().initialize_kv_cache(kv_cache_config)
138135

136+
# GPUModelRunner constructs the community PCP manager while initializing
137+
# the KV cache. Replace it with the Ascend subclass.
138+
if not vllm_version_is("0.25.1"):
139+
self.pcp_manager = maybe_build_ascend_pcp_manager(
140+
self.vllm_config,
141+
self.device,
142+
self.supports_mm_inputs,
143+
self.req_states,
144+
self.block_tables,
145+
)
146+
139147
@torch.inference_mode()
140148
def profile_run(self) -> None:
141149
"""Override GPUModelRunner.profile_run for Ascend NPUs.
@@ -361,6 +369,9 @@ def prepare_inputs(
361369
attn_state=attn_state,
362370
)
363371

372+
if not vllm_version_is("0.25.1"):
373+
input_batch = vllm_model_runner.pcp.maybe_partition_pcp_batch(self.pcp_manager, input_batch)
374+
364375
# For mla/sfa, update cos/sin. Here is for execute_model.
365376
update_cos_sin(input_batch.positions)
366377

0 commit comments

Comments
 (0)