[Core] Add MRV2 virtual-batch PCP for MLA (#46570)

Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com> Signed-off-by: Matthew Bonanni <mbonanni@redhat.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Matthew Bonanni <mbonanni@redhat.com>

Lucas Wilkinson committed Jul 19, 2026 at 04:53 UTC b6ff8a2f509cc7ac9c58176f5115a836aa1e08bd
39 files changed +1204 -196
.buildkite/test_areas/lm_eval.yaml
+22
@@ -78,6 +78,28 @@ steps:
78 commands:
79 - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-small-tp.txt
80
81 +- label: LM Eval PCP (4xB200)
82 + key: lm-eval-pcp-4xb200
83 + timeout_in_minutes: 360
84 + device: b200-k8s
85 + num_devices: 4
86 + optional: true
87 + source_file_dependencies:
88 + - csrc/
89 + - tests/evals/gsm8k/configs/GLM-5.2-NVFP4-TP2-PCP2-EP.yaml
90 + - tests/evals/gsm8k/configs/GLM-5.2-NVFP4-TP1-PCP4-EP.yaml
91 + - tests/evals/gsm8k/configs/models-pcp.txt
92 + - vllm/model_executor/layers/quantization
93 + - vllm/config/parallel.py
94 + - vllm/distributed/parallel_state.py
95 + - vllm/model_executor/layers/attention/mla_attention.py
96 + - vllm/model_executor/layers/attention/pcp.py
97 + - vllm/v1/worker/gpu/model_runner.py
98 + - vllm/v1/worker/gpu/pcp_manager.py
99 + autorun_on_main: true
100 + commands:
101 + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-pcp.txt
102 +
103 - label: LM Eval Large Models EP (2xB200)
104 key: lm-eval-large-models-ep-2xb200
105 timeout_in_minutes: 60
tests/evals/gsm8k/configs/GLM-5.2-NVFP4-TP1-PCP4-EP.yaml new
+16
@@ -0,0 +1,16 @@
1 +model_name: "nvidia/GLM-5.2-NVFP4"
2 +accuracy_threshold: 0.90
3 +num_questions: 1319
4 +num_fewshot: 5
5 +max_concurrency: 100
6 +server_args: >-
7 + --enforce-eager
8 + --max-model-len 4096
9 + --safetensors-load-strategy prefetch
10 + --moe-backend flashinfer_cutlass
11 + --prefill-context-parallel-size 4
12 + --enable-expert-parallel
13 + --kv-cache-dtype fp8
14 +env:
15 + VLLM_LOGGING_LEVEL: "DEBUG"
16 + VLLM_USE_V2_MODEL_RUNNER: "1"
tests/evals/gsm8k/configs/GLM-5.2-NVFP4-TP2-PCP2-EP.yaml new
+17
@@ -0,0 +1,17 @@
1 +model_name: "nvidia/GLM-5.2-NVFP4"
2 +accuracy_threshold: 0.90
3 +num_questions: 1319
4 +num_fewshot: 5
5 +max_concurrency: 100
6 +server_args: >-
7 + --enforce-eager
8 + --max-model-len 4096
9 + --safetensors-load-strategy prefetch
10 + --moe-backend flashinfer_cutlass
11 + --tensor-parallel-size 2
12 + --prefill-context-parallel-size 2
13 + --enable-expert-parallel
14 + --kv-cache-dtype fp8
15 +env:
16 + VLLM_LOGGING_LEVEL: "DEBUG"
17 + VLLM_USE_V2_MODEL_RUNNER: "1"
tests/evals/gsm8k/configs/models-pcp.txt new
+2
@@ -0,0 +1,2 @@
1 +GLM-5.2-NVFP4-TP2-PCP2-EP.yaml
2 +GLM-5.2-NVFP4-TP1-PCP4-EP.yaml
tests/evals/gsm8k/gsm8k_eval.py
+15 -1
@@ -217,6 +217,7 @@ def evaluate_gsm8k(
217 seed: int | None = 42,
218 request_timeout_seconds: float = 600,
219 gen_prefix: str = "",
220 + max_concurrency: int | None = None,
221 ) -> dict[str, float | int]:
222 """
223 Evaluate GSM8K accuracy using vLLM serve endpoint.
@@ -261,7 +262,14 @@ def evaluate_gsm8k(
262 return answer, tokens
263
264 timeout = aiohttp.ClientTimeout(total=request_timeout_seconds)
264 - async with aiohttp.ClientSession(timeout=timeout) as session:
265 + connector = (
266 + aiohttp.TCPConnector(limit=max_concurrency)
267 + if max_concurrency is not None
268 + else None
269 + )
270 + async with aiohttp.ClientSession(
271 + timeout=timeout, connector=connector
272 + ) as session:
273 tasks = [get_answer(session, i) for i in range(num_questions)]
274 await tqdm.gather(*tasks, desc="Evaluating")
275
@@ -343,6 +351,11 @@ def main() -> None:
351 parser.add_argument(
352 "--seed", type=int, default=42, help="Random seed for reproducibility"
353 )
354 + parser.add_argument(
355 + "--max-concurrency",
356 + type=int,
357 + help="Maximum number of concurrent requests",
358 + )
359 parser.add_argument("--save-results", type=str, help="Save results to JSON file")
360
361 args = parser.parse_args()
@@ -355,6 +368,7 @@ def main() -> None:
368 port=args.port,
369 temperature=args.temperature,
370 seed=args.seed,
371 + max_concurrency=args.max_concurrency,
372 )
373
374 # Print results to terminal
tests/evals/gsm8k/test_gsm8k_correctness.py
+1
@@ -59,6 +59,7 @@ def run_gsm8k_eval(eval_config: dict, server_url: str) -> dict:
59 seed=eval_config.get("seed", 42),
60 request_timeout_seconds=request_timeout_seconds,
61 gen_prefix=eval_config.get("gen_prefix", ""),
62 + max_concurrency=eval_config.get("max_concurrency"),
63 )
64
65 return results
tests/v1/attention/test_attention_splitting.py
+16
@@ -160,6 +160,8 @@ def apply_split_decodes_and_prefills(
160 decode_threshold: int,
161 require_uniform: bool,
162 padded_num_tokens: int | None = None,
163 + is_prefilling: list[bool] | None = None,
164 + treat_short_extends_as_decodes: bool = True,
165 ):
166 """Helper function to apply split_decodes_and_prefills and return
167 the results."""
@@ -173,11 +175,14 @@ def apply_split_decodes_and_prefills(
175
176 if padded_num_tokens is not None:
177 common_metadata.num_actual_tokens = padded_num_tokens
178 + if is_prefilling is not None:
179 + common_metadata.is_prefilling = torch.tensor(is_prefilling)
180
181 return split_decodes_and_prefills(
182 common_metadata,
183 decode_threshold=decode_threshold,
184 require_uniform=require_uniform,
185 + treat_short_extends_as_decodes=treat_short_extends_as_decodes,
186 )
187
188
@@ -236,6 +241,17 @@ def test_split_decodes_and_prefills_uniform_all_ones():
241 assert num_prefill_tokens == 0
242
243
244 +def test_split_decodes_and_prefills_uniform_short_extend():
245 + result = apply_split_decodes_and_prefills(
246 + [1, 1],
247 + decode_threshold=1,
248 + require_uniform=True,
249 + is_prefilling=[False, True],
250 + treat_short_extends_as_decodes=False,
251 + )
252 + assert result == (1, 1, 1, 1)
253 +
254 +
255 def test_split_decodes_and_prefills_uniform_all_short_decodes():
256 query_lens = [2, 2, 1, 3, 2, 1, 2]
257 num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
tests/v1/attention/test_sparse_mla_backends.py
+7
@@ -1114,6 +1114,13 @@ def test_sparse_backend_prefill_correctness(
1114 @pytest.mark.parametrize(
1115 "seq_lens,query_lens,workspace_size,max_logits_bytes,expected",
1116 [
1117 + (
1118 + torch.tensor([0]),
1119 + torch.tensor([0]),
1120 + 100,
1121 + 1000,
1122 + [],
1123 + ),
1124 # Logits constraint triggers split (M*N exceeds budget)
1125 # req0: M=10, N=100 -> 1000 elems (4000 bytes) - fits in 5000
1126 # req1: adding M=10, N=100 -> new_M=20, new_N=200 -> 4000 elems > 1250
tests/v1/kv_offload/test_factory.py
+4 -4
@@ -385,7 +385,7 @@ def test_tiering_spec_aligns_row_size():
385 assert spec.num_blocks == cpu_bytes_to_use // alignment
386
387
388 -def test_offloading_spec_resolves_prefill_context_parallel_block_sizes():
388 +def test_offloading_spec_kv_sharding_ignores_prefill_context_parallel():
389 config = _make_layout_vllm_config(
390 cpu_bytes_to_use=65536,
391 extra_config={"block_size": 64},
@@ -394,9 +394,9 @@ def test_offloading_spec_resolves_prefill_context_parallel_block_sizes():
394
395 spec = _create_spec(config, _make_kv_cache_config())
396
397 - assert spec.tokens_per_block == (32,)
398 - assert spec.tokens_per_hash == 32
399 - assert spec.blocks_per_chunk == 2
397 + assert spec.tokens_per_block == (16,)
398 + assert spec.tokens_per_hash == 16
399 + assert spec.blocks_per_chunk == 4
400
401
402 def test_offloading_config_preserves_data_parallel_index():
tests/v1/simple_kv_offload/test_scheduler.py
+16 -24
@@ -1551,9 +1551,8 @@ def _make_cp_scheduler(
1551 num_gpu_blocks: int = 16,
1552 lazy: bool = False,
1553 ) -> SchedulerFixture:
1554 - """Build a SimpleCPUOffloadScheduler with CP-scaled virtual block size."""
1555 - cp_world_size = dcp_world_size * pcp_world_size
1556 - virtual_block_size = BLOCK_SIZE * cp_world_size
1554 + """Build a SimpleCPUOffloadScheduler with DCP-scaled block size."""
1555 + virtual_block_size = BLOCK_SIZE * dcp_world_size
1556
1557 kv_cache_config = _make_kv_cache_config(num_gpu_blocks)
1558 vllm_config = _make_cp_vllm_config(dcp_world_size, pcp_world_size)
@@ -1645,17 +1644,14 @@ def _allocate_cp_gpu_blocks(
1644 ],
1645 )
1646 def test_cp_block_size_scaling(dcp_world_size: int, pcp_world_size: int) -> None:
1648 - """Verify that the scheduler's block_size and cp_world_size are correctly
1649 - scaled when context parallelism is enabled."""
1647 + """Verify block size scaling follows DCP ownership."""
1648 fix = _make_cp_scheduler(
1649 dcp_world_size=dcp_world_size, pcp_world_size=pcp_world_size
1650 )
1651 sched = fix.scheduler
1652
1655 - expected_cp = dcp_world_size * pcp_world_size
1656 - assert sched.cp_world_size == expected_cp
1657 - assert sched.block_size == BLOCK_SIZE * expected_cp
1658 - assert sched.fa_block_size == BLOCK_SIZE * expected_cp
1653 + assert sched.cp_world_size == dcp_world_size
1654 + assert sched.block_size == BLOCK_SIZE * dcp_world_size
1655
1656
1657 # ---------------------------------------------------------------------------
@@ -1682,8 +1678,7 @@ def test_cp_eager_store_and_load_roundtrip(
1678 lazy=False,
1679 )
1680 sched = fix.scheduler
1685 - cp = dcp_world_size * pcp_world_size
1686 - vbs = BLOCK_SIZE * cp
1681 + vbs = BLOCK_SIZE * dcp_world_size
1682
1683 num_blocks = 2
1684 req = _make_cp_request(num_blocks, vbs)
@@ -1749,18 +1744,16 @@ def test_cp_eager_store_and_load_roundtrip(
1744 def test_cp_effective_block_size_store_and_load(
1745 dcp_world_size: int, pcp_world_size: int
1746 ) -> None:
1752 - """Verify ready_blocks_g (store) and n_take_g (load) use the effective
1753 - (physical * cp) block size, not the per-rank physical size."""
1747 + """Verify store/load physical block counts scale with DCP, not PCP."""
1748 fix = _make_cp_scheduler(
1749 dcp_world_size=dcp_world_size, pcp_world_size=pcp_world_size
1750 )
1751 sched = fix.scheduler
1752 gpu_pool = fix.gpu_block_pool
1759 - cp = dcp_world_size * pcp_world_size
1760 - vbs = BLOCK_SIZE * cp
1753 + vbs = BLOCK_SIZE * dcp_world_size
1754 + expected_blocks = 1
1755
1762 - # Store: allocate 2 blocks, confirm only 1. Without the fix,
1763 - # ready_blocks_g = vbs / BLOCK_SIZE = 2, storing both blocks.
1756 + # Store one DCP-scaled logical block worth of tokens.
1757 req = _make_cp_request(num_blocks=2, virtual_block_size=vbs)
1758 gpu_blocks = _allocate_cp_gpu_blocks(gpu_pool, req, 2, vbs)
1759 kv = KVCacheBlocks(blocks=(gpu_blocks,))
@@ -1772,12 +1765,11 @@ def test_cp_effective_block_size_store_and_load(
1765 new_reqs={req.request_id: kv.get_block_ids()},
1766 )
1767 )
1775 - assert len(m1.store_gpu_blocks) == 1
1776 - assert len(m1.store_cpu_blocks) == 1
1768 + assert len(m1.store_gpu_blocks) == expected_blocks
1769 + assert len(m1.store_cpu_blocks) == expected_blocks
1770 simulate_store_completion(sched, m1.store_event)
1771
1779 - # Load: store 2 blocks from a second request, accept only 1 as external.
1780 - # Without the fix, n_take_g = vbs / BLOCK_SIZE = 2, loading both.
1772 + # Load one DCP-scaled logical block from a two-block CPU hit.
1773 req2 = _make_cp_request(num_blocks=2, virtual_block_size=vbs)
1774 kv2 = KVCacheBlocks(blocks=(_allocate_cp_gpu_blocks(gpu_pool, req2, 2, vbs),))
1775 req2.num_computed_tokens = 2 * vbs
@@ -1801,7 +1793,7 @@ def test_cp_effective_block_size_store_and_load(
1793 hit, _ = sched.get_num_new_matched_tokens(req3, num_computed_tokens=0)
1794 assert hit == 2 * vbs
1795
1804 - kv3 = KVCacheBlocks(blocks=(gpu_pool.get_new_blocks(2),))
1796 + kv3 = KVCacheBlocks(blocks=(gpu_pool.get_new_blocks(expected_blocks),))
1797 sched.update_state_after_alloc(req3, kv3, num_external_tokens=vbs)
1798 m3 = sched.build_connector_meta(
1799 make_scheduler_output(
@@ -1810,8 +1802,8 @@ def test_cp_effective_block_size_store_and_load(
1802 )
1803 )
1804 assert m3.load_event >= 0
1813 - assert len(m3.load_gpu_blocks) == 1
1814 - assert len(m3.load_cpu_blocks) == 1
1805 + assert len(m3.load_gpu_blocks) == expected_blocks
1806 + assert len(m3.load_cpu_blocks) == expected_blocks
1807 assert m3.load_gpu_blocks == [kv3.get_block_ids()[0][0]]
1808
1809
vllm/config/parallel.py
+37 -25
@@ -122,10 +122,11 @@ class ParallelConfig:
122 tensor_parallel_size: int = Field(default=1, ge=1)
123 """Number of tensor parallel groups."""
124 prefill_context_parallel_size: int = Field(default=1, ge=1)
125 - """Number of prefill context parallel groups."""
125 + """Number of ranks that split prefill sequence computation. PCP expands
126 + the process world size but does not increase the KV-cache shard count."""
127 data_parallel_size: int = Field(default=1, ge=1)
128 """Number of data parallel groups. MoE layers will be sharded according to
128 - the product of the tensor parallel size and data parallel size."""
129 + the product of the tensor, prefill-context, and data parallel sizes."""
130 data_parallel_size_local: int = Field(default=1, ge=0)
131 """Number of local data parallel groups. A value of 0 is a sentinel used by
132 the engine-args layer to signal that data parallelism was specified
@@ -337,9 +338,9 @@ class ParallelConfig:
338 connect as clients to exchange self-picked group ports at runtime."""
339
340 decode_context_parallel_size: int = Field(default=1, ge=1)
340 - """Number of decode context parallel groups, because the world size does
341 - not change by dcp, it simply reuse the GPUs of TP group, and tp_size
342 - needs to be divisible by dcp_size."""
341 + """Number of ranks that shard the decode KV cache. DCP does not expand
342 + the process world size. Without PCP, DCP reuses TP ranks. With PCP, DCP
343 + either spans the PCP axis or the full TP x PCP block."""
344
345 dcp_kv_cache_interleave_size: int = 1
346 """
@@ -357,13 +358,11 @@ class ParallelConfig:
358 """
359
360 cp_kv_cache_interleave_size: int = 1
360 - """Interleave size of kv_cache storage while using DCP or PCP.
361 - For `total_cp_rank = pcp_rank * dcp_world_size + dcp_rank`,
362 - and `total_cp_world_size = pcp_world_size * dcp_world_size`.
363 - store interleave_size tokens on total_cp_rank i,
364 - then store next interleave_size tokens on total_cp_rank i+1.
361 + """Interleave size of kv_cache storage while using DCP.
362 + Store interleave_size tokens on dcp_rank i, then store next
363 + interleave_size tokens on dcp_rank i+1.
364 Interleave_size=1: token-level alignment, where token `i` is stored on
366 - total_cp_rank `i % total_cp_world_size`.
365 + dcp_rank `i % dcp_world_size`.
366 Interleave_size=block_size: block-level alignment, where tokens are
367 first populated to the preceding ranks. Tokens are then stored
368 in (rank i+1, block j) only after (rank i, block j) is fully occupied.
@@ -480,11 +479,19 @@ class ParallelConfig:
479 )
480 if not self.enable_expert_parallel:
481 raise ValueError("enable_expert_parallel must be True to use EPLB.")
483 - if self.tensor_parallel_size * self.data_parallel_size <= 1:
482 + # The EP group spans the TP x PCP x DP ranks. EPLB therefore needs
483 + # TP, PCP, or DP > 1.
484 + if (
485 + self.tensor_parallel_size
486 + * self.prefill_context_parallel_size
487 + * self.data_parallel_size
488 + <= 1
489 + ):
490 raise ValueError(
485 - "EPLB requires tensor_parallel_size or data_parallel_size "
486 - f"to be greater than 1, but got "
487 - f"TP={self.tensor_parallel_size},DP={self.data_parallel_size}."
491 + "EPLB requires tensor, prefill-context, or data parallelism, "
492 + f"but got TP={self.tensor_parallel_size}, "
493 + f"PCP={self.prefill_context_parallel_size}, "
494 + f"DP={self.data_parallel_size}."
495 )
496 else:
497 if self.eplb_config.num_redundant_experts != 0:
@@ -495,15 +502,21 @@ class ParallelConfig:
502 "num_redundant_experts."
503 )
504
498 - # Note(hc): In the current implementation of decode context
499 - # parallel(DCP), tp_size needs to be divisible by dcp_size,
500 - # because the world size does not change by dcp, it simply
501 - # reuses the GPUs of TP group, and split one TP group into
502 - # tp_size//dcp_size DCP groups.
503 - if self.tensor_parallel_size % self.decode_context_parallel_size != 0:
505 + tp = self.tensor_parallel_size
506 + pcp = self.prefill_context_parallel_size
507 + dcp = self.decode_context_parallel_size
508 + if pcp > 1 and self.data_parallel_size > 1:
509 + raise ValueError("PCP does not support data parallelism yet.")
510 + if pcp == 1:
511 + # DCP reuses the TP ranks when PCP is disabled.
512 + if tp % dcp != 0:
513 + raise ValueError(f"tp_size={tp} must be divisible by dcp_size={dcp}.")
514 + elif dcp not in (1, pcp, tp * pcp):
515 raise ValueError(
505 - f"tp_size={self.tensor_parallel_size} must be divisible by"
506 - f"dcp_size={self.decode_context_parallel_size}."
516 + "When PCP is enabled, DCP must be disabled, span the PCP "
517 + "axis, or span the full TP x PCP axis. "
518 + f"Got TP={tp}, PCP={pcp}, DCP={dcp}; valid DCP sizes are "
519 + f"{sorted({1, pcp, tp * pcp})}."
520 )
521
522 if self.dcp_comm_backend == "a2a" and self.decode_context_parallel_size <= 1:
@@ -515,8 +528,7 @@ class ParallelConfig:
528
529 @property
530 def world_size_across_dp(self) -> int:
518 - """world_size_across_dp is TPxPPxDP, it is the size of the world
519 - including data parallelism."""
531 + """Process world size across TP, PCP, PP, and DP."""
532 return self.world_size * self.data_parallel_size
533
534 @property
vllm/config/vllm.py
+3 -2
@@ -2130,9 +2130,10 @@ class VllmConfig:
2130 model_config = self.model_config
2131 speculative_config = self.speculative_config
2132
2133 - if self.parallel_config.prefill_context_parallel_size > 1:
2133 + if self.parallel_config.prefill_context_parallel_size > 1 and not (
2134 + model_config is not None and model_config.use_mla
2135 + ):
2136 unsupported.append("prefill context parallelism")
2135 -
2137 if self.compilation_config.mode == CompilationMode.STOCK_TORCH_COMPILE:
2138 unsupported.append("stock torch.compile")
2139
vllm/distributed/device_communicators/all2all.py
+27 -17
@@ -8,7 +8,7 @@ import torch
8 import torch.distributed as dist
9
10 import vllm.envs as envs
11 -from vllm.distributed import get_dp_group, get_ep_group
11 +from vllm.distributed import get_dp_group, get_ep_group, get_pcp_group
12 from vllm.distributed.utils import StatelessProcessGroup
13 from vllm.forward_context import get_forward_context
14 from vllm.logger import init_logger
@@ -49,6 +49,23 @@ class AgRsAll2AllManager(All2AllManagerBase):
49 def __init__(self, cpu_group, tcp_store_group=None):
50 super().__init__(cpu_group, tcp_store_group)
51
52 + def _get_comm_group(self, is_sequence_parallel: bool) -> Any:
53 + if is_sequence_parallel:
54 + return get_ep_group()
55 + if self.dp_world_size > 1:
56 + return get_dp_group()
57 + return get_pcp_group()
58 +
59 + def _get_sizes(self, num_local_tokens: int, comm_group: Any) -> list[int]:
60 + if self.dp_world_size == 1:
61 + return [num_local_tokens] * comm_group.world_size
62 +
63 + dp_metadata = get_forward_context().dp_metadata
64 + assert dp_metadata is not None
65 + sizes = dp_metadata.get_chunk_sizes_across_dp_rank()
66 + assert sizes is not None
67 + return sizes
68 +
69 def dispatch_router_logits(
70 self,
71 hidden_states: torch.Tensor,
@@ -62,11 +79,8 @@ class AgRsAll2AllManager(All2AllManagerBase):
79 """
80 Gather hidden_states and router_logits from all dp ranks.
81 """
65 - dp_metadata = get_forward_context().dp_metadata
66 - assert dp_metadata is not None
67 - sizes = dp_metadata.get_chunk_sizes_across_dp_rank()
68 - assert sizes is not None
69 - dist_group = get_ep_group() if is_sequence_parallel else get_dp_group()
82 + dist_group = self._get_comm_group(is_sequence_parallel)
83 + sizes = self._get_sizes(hidden_states.shape[0], dist_group)
84 assert sizes[dist_group.rank_in_group] == hidden_states.shape[0]
85
86 tensors_to_gather = [hidden_states, router_logits]
@@ -97,11 +111,8 @@ class AgRsAll2AllManager(All2AllManagerBase):
111 """
112 Gather hidden_states and router_logits from all dp ranks.
113 """
100 - dp_metadata = get_forward_context().dp_metadata
101 - assert dp_metadata is not None
102 - sizes = dp_metadata.get_chunk_sizes_across_dp_rank()
103 - assert sizes is not None
104 - dist_group = get_ep_group() if is_sequence_parallel else get_dp_group()
114 + dist_group = self._get_comm_group(is_sequence_parallel)
115 + sizes = self._get_sizes(hidden_states.shape[0], dist_group)
116 assert sizes[dist_group.rank_in_group] == hidden_states.shape[0]
117
118 tensors_to_gather = [hidden_states, topk_weights, topk_ids]
@@ -129,12 +140,11 @@ class AgRsAll2AllManager(All2AllManagerBase):
140 """
141 Reduce-scatter hidden_states across all dp ranks.
142 """
132 - dp_metadata = get_forward_context().dp_metadata
133 - assert dp_metadata is not None
134 - sizes = dp_metadata.get_chunk_sizes_across_dp_rank()
135 - assert sizes is not None
136 -
137 - dist_group = get_ep_group() if is_sequence_parallel else get_dp_group()
143 + dist_group = self._get_comm_group(is_sequence_parallel)
144 + sizes = self._get_sizes(
145 + hidden_states.shape[0] // dist_group.world_size,
146 + dist_group,
147 + )
148 hidden_states = dist_group.reduce_scatterv(hidden_states, dim=0, sizes=sizes)
149 return hidden_states
150
vllm/distributed/device_communicators/base_device_communicator.py
+8 -3
@@ -176,11 +176,16 @@ class DeviceCommunicatorBase:
176 config = get_current_vllm_config_or_none()
177 if config is not None:
178 # initialize the all2all manager for DP or sequence-parallel EP.
179 + parallel_config = config.parallel_config
180 use_ep = (
180 - config.parallel_config.data_parallel_size > 1
181 - or config.parallel_config.use_sequence_parallel_moe
181 + parallel_config.data_parallel_size > 1
182 + or parallel_config.use_sequence_parallel_moe
183 + or (
184 + parallel_config.enable_expert_parallel
185 + and parallel_config.prefill_context_parallel_size > 1
186 + )
187 )
183 - all2all_backend = config.parallel_config.all2all_backend
188 + all2all_backend = parallel_config.all2all_backend
189
190 self.is_ep_communicator = unique_name.split(":")[0] == "ep"
191 self.use_all2all = self.is_ep_communicator and use_ep
vllm/distributed/kv_transfer/kv_connector/v1/offloading/config.py
+4 -5
@@ -36,13 +36,12 @@ def build_offloading_config(
36 engine_id = kv_transfer_config.engine_id
37
38 parallel_config = vllm_config.parallel_config
39 - context_parallel_factor = (
40 - parallel_config.decode_context_parallel_size
41 - * parallel_config.prefill_context_parallel_size
42 - )
39 groups = tuple(
40 OffloadingGroupConfig(
45 - tokens_per_block=(group.kv_cache_spec.block_size * context_parallel_factor),
41 + tokens_per_block=(
42 + group.kv_cache_spec.block_size
43 + * parallel_config.decode_context_parallel_size
44 + ),
45 layer_names=tuple(group.layer_names),
46 )
47 for group in kv_cache_config.kv_cache_groups
vllm/distributed/parallel_state.py
+14 -11
@@ -1784,7 +1784,7 @@ def initialize_model_parallel(
1784 get_world_group().device_group
1785 )
1786
1787 - # the layout order is: ExternalDP x DP x PP x TP
1787 + # the layout order is: ExternalDP x DP x PP x PCP x TP
1788 # ExternalDP is the data parallel group that is not part of the model,
1789 # every dp rank can generate independently (in verl integration).
1790 # DP is the data parallel group that is part of the model,
@@ -1821,17 +1821,13 @@ def initialize_model_parallel(
1821 # Build the DCP model-parallel groups.
1822 global _DCP
1823 assert _DCP is None, "decode context model parallel group is already initialized"
1824 - # Note(hc): In the current implementation of decode context parallel,
1825 - # dcp_size must not exceed tp_size, because the world size does not
1826 - # change by DCP, it simply reuses the GPUs of TP group, and split one
1827 - # TP group into tp_size//dcp_size DCP groups.
1828 - group_ranks = all_ranks.reshape(-1, decode_context_model_parallel_size).unbind(0)
1824 + dcp_size = decode_context_model_parallel_size or 1
1825 + dcp_ranks = local_all_ranks if enable_elastic_ep else all_ranks
1826 + if dcp_size > 1:
1827 + # DCP spans PCP first, then TP for full TP x PCP groups.
1828 + dcp_ranks = dcp_ranks.transpose(-1, -2)
1829 + group_ranks = dcp_ranks.reshape(-1, dcp_size).unbind(0)
1830 group_ranks = [x.tolist() for x in group_ranks]
1830 - if enable_elastic_ep:
1831 - group_ranks = local_all_ranks.reshape(
1832 - -1, decode_context_model_parallel_size
1833 - ).unbind(0)
1834 - group_ranks = [x.tolist() for x in group_ranks]
1831 _DCP = init_model_parallel_group(
1832 group_ranks,
1833 get_world_group().local_rank,
@@ -2005,6 +2001,13 @@ def ensure_model_parallel_initialized(
2001 f"{pcp_world_size=} vs. "
2002 f"{prefill_context_model_parallel_size=}"
2003 )
2004 + dcp_world_size = get_dcp_group().world_size
2005 + dcp_model_parallel_size = decode_context_model_parallel_size or 1
2006 + assert dcp_world_size == dcp_model_parallel_size, (
2007 + "decode context parallel group already initialized, but of unexpected size: "
2008 + f"{dcp_world_size=} vs. "
2009 + f"{dcp_model_parallel_size=}"
2010 + )
2011
2012
2013 def prepare_communication_buffer_for_model(model: torch.nn.Module):
vllm/model_executor/layers/attention/mla_attention.py
+60 -12
@@ -211,6 +211,7 @@ from vllm.config import (
211 from vllm.config.cache import CacheDType
212 from vllm.distributed.parallel_state import (
213 get_dcp_group,
214 + get_tp_group,
215 is_global_first_rank,
216 )
217 from vllm.forward_context import ForwardContext, get_forward_context
@@ -225,6 +226,10 @@ from vllm.model_executor.layers.attention.attention import (
226 from vllm.model_executor.layers.attention.kv_transfer_utils import (
227 maybe_transfer_kv_layer,
228 )
229 +from vllm.model_executor.layers.attention.pcp import (
230 + finalize_mla_pcp_decode,
231 + maybe_gather_mla_latent_cache_inputs,
232 +)
233 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
234 from vllm.model_executor.layers.linear import (
235 ColumnParallelLinear,
@@ -269,7 +274,7 @@ from vllm.v1.attention.backends.utils import (
274 get_dcp_local_seq_lens,
275 split_decodes_and_prefills,
276 )
272 -from vllm.v1.attention.ops.common import cp_lse_ag_out_rs
277 +from vllm.v1.attention.ops.common import cp_lse_ag_out_ar, cp_lse_ag_out_rs
278 from vllm.v1.attention.ops.dcp_alltoall import dcp_a2a_lse_reduce
279 from vllm.v1.attention.ops.merge_attn_states import merge_attn_states
280 from vllm.v1.attention.selector import get_attn_backend
@@ -499,6 +504,8 @@ class MLAAttention(nn.Module, AttentionLayerBase):
504 self.use_direct_call = not current_platform.opaque_attention_op()
505
506 vllm_config = get_current_vllm_config()
507 + parallel_config = vllm_config.parallel_config
508 + self.use_pcp = parallel_config.prefill_context_parallel_size > 1
509 compilation_config = vllm_config.compilation_config
510 if prefix in compilation_config.static_forward_context:
511 raise ValueError(f"Duplicate layer name: {prefix}")
@@ -611,11 +618,23 @@ class MLAAttention(nn.Module, AttentionLayerBase):
618 assert isinstance(slot_mapping, dict), (
619 f"Expected slot_mapping to be a dict, got {type(slot_mapping)}. "
620 )
621 + layer_slot_mapping = slot_mapping.get(self.layer_name)
622 + kv_for_cache, kpe_for_cache, layer_slot_mapping = (
623 + maybe_gather_mla_latent_cache_inputs(
624 + kv_c_normed,
625 + k_pe,
626 + layer_slot_mapping,
627 + attn_metadata.num_decode_tokens
628 + if attn_metadata is not None
629 + else None,
630 + self.use_pcp,
631 + )
632 + )
633 self.impl.do_kv_cache_update( # type: ignore[attr-defined]
615 - kv_c_normed,
616 - k_pe,
634 + kv_for_cache,
635 + kpe_for_cache,
636 self_kv_cache,
618 - slot_mapping.get(self.layer_name),
637 + layer_slot_mapping,
638 self.kv_cache_dtype,
639 self._k_scale,
640 )
@@ -710,6 +729,10 @@ class MLAAttention(nn.Module, AttentionLayerBase):
729 fp8_attention = is_quantized_kv_cache(self.kv_cache_dtype)
730
731 num_actual_toks = attn_metadata.num_actual_tokens
732 + if self.use_pcp and self.impl.dcp_world_size > 1 and quant_key is not None:
733 + raise NotImplementedError(
734 + "MRV2 MLA PCP+DCP does not support fused output quantization yet."
735 + )
736
737 # Inputs and outputs may be padded for CUDA graphs
738 output_padded = output
@@ -833,11 +856,15 @@ class MLAAttention(nn.Module, AttentionLayerBase):
856 else:
857 mqa_q = (mqa_ql_nope, mqa_q_pe)
858 if self.impl.dcp_world_size > 1:
836 - if isinstance(mqa_q, tuple):
837 - # concatenate mqa_ql_nope and mqa_q_pe -> (B, N, L + P)
838 - mqa_q = torch.cat(mqa_q, dim=-1)
839 - # mqa_q do allgather in head dim.
840 - mqa_q = get_dcp_group().all_gather(mqa_q, dim=1)
859 + if self.use_pcp:
860 + if self.impl.dcp_world_size > self.impl.pcp_world_size:
861 + if isinstance(mqa_q, tuple):
862 + mqa_q = torch.cat(mqa_q, dim=-1)
863 + mqa_q = get_tp_group().all_gather(mqa_q, dim=1)
864 + else:
865 + if isinstance(mqa_q, tuple):
866 + mqa_q = torch.cat(mqa_q, dim=-1)
867 + mqa_q = get_dcp_group().all_gather(mqa_q, dim=1)
868
869 # call decode attn
870 if not self.impl.is_sparse:
@@ -846,6 +873,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
873
874 # correct dcp attn_out with lse.
875 if self.impl.dcp_world_size > 1:
876 + assert lse is not None
877 if self.dcp_a2a:
878 attn_out = dcp_a2a_lse_reduce(
879 attn_out,
@@ -853,6 +881,13 @@ class MLAAttention(nn.Module, AttentionLayerBase):
881 get_dcp_group(),
882 is_lse_base_on_e=self.impl.lse_base_on_e,
883 )
884 + elif self.use_pcp:
885 + attn_out = cp_lse_ag_out_ar(
886 + attn_out,
887 + lse,
888 + get_dcp_group(),
889 + is_lse_base_on_e=self.impl.lse_base_on_e,
890 + )
891 else:
892 attn_out = cp_lse_ag_out_rs(
893 attn_out,
@@ -860,6 +895,8 @@ class MLAAttention(nn.Module, AttentionLayerBase):
895 get_dcp_group(),
896 is_lse_base_on_e=self.impl.lse_base_on_e,
897 )
898 + if self.use_pcp:
899 + attn_out = finalize_mla_pcp_decode(attn_out, self.num_heads)
900
901 # v_up projection
902 self._v_up_proj(attn_out, out=mqa_output_slice)
@@ -903,6 +940,8 @@ class MLAAttention(nn.Module, AttentionLayerBase):
940 raise ValueError(f"Unsupported quant_key: {quant_key}")
941 return quant_output
942
943 + if self.use_pcp and output_padded.shape[0] > num_actual_toks:
944 + output_padded[num_actual_toks:].zero_()
945 return output_padded
946
947 def process_weights_after_loading(self, act_dtype: torch.dtype):
@@ -1082,8 +1121,17 @@ def unified_mla_kv_cache_update(
1121 the data dependency between them to ensure torch.compile preserves ordering.
1122 """
1123 layer_name = _resolve_layer_name(layer_name)
1085 - _, attn_layer, kv_cache, layer_slot_mapping = get_attention_context(layer_name)
1124 + attn_metadata, attn_layer, kv_cache, layer_slot_mapping = get_attention_context(
1125 + layer_name
1126 + )
1127 if layer_slot_mapping is not None:
1128 + kv_c_normed, k_pe, layer_slot_mapping = maybe_gather_mla_latent_cache_inputs(
1129 + kv_c_normed,
1130 + k_pe,
1131 + layer_slot_mapping,
1132 + attn_metadata.num_decode_tokens if attn_metadata is not None else None,
1133 + attn_layer.use_pcp,
1134 + )
1135 attn_layer.impl.do_kv_cache_update( # type: ignore[attr-defined]
1136 kv_c_normed,
1137 k_pe,
@@ -1723,6 +1771,7 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1771 self.compilation_config = vllm_config.compilation_config
1772 self.vllm_config = vllm_config
1773 self.device = device
1774 + self.use_pcp = parallel_config.prefill_context_parallel_size > 1
1775
1776 self.num_heads = self.model_config.get_num_attention_heads(parallel_config)
1777 self.mla_dims = get_mla_dims(self.model_config)
@@ -1735,11 +1784,9 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1784
1785 try:
1786 self.dcp_world_size = get_dcp_group().world_size
1738 - self.dcp_rank = get_dcp_group().rank_in_group
1787 except AssertionError:
1788 # DCP might not be initialized in testing
1789 self.dcp_world_size = 1
1742 - self.dcp_rank = 0
1790 self.dcp_local_block_size = parallel_config.cp_kv_cache_interleave_size
1791 self.dcp_virtual_block_size = self.dcp_local_block_size * self.dcp_world_size
1792 self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
@@ -1857,6 +1904,7 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1904 common_attn_metadata,
1905 decode_threshold=self.reorder_batch_threshold,
1906 require_uniform=(self.query_len_support != QueryLenSupport.VARLEN),
1907 + treat_short_extends_as_decodes=not self.use_pcp,
1908 )
1909 )
1910
vllm/model_executor/layers/attention/pcp.py new
+92
@@ -0,0 +1,92 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import torch
4 +
5 +from vllm.distributed.parallel_state import (
6 + get_pcp_group,
7 + get_tp_group,
8 +)
9 +
10 +
11 +def _gather_prefill_cache_inputs(
12 + tensors: tuple[torch.Tensor, ...],
13 + slot_mapping: torch.Tensor,
14 + num_decode_tokens: int,
15 +) -> tuple[tuple[torch.Tensor, ...], torch.Tensor]:
16 + """Keep replicated decode writes local and gather partitioned prefills."""
17 + local_num_tokens = tensors[0].shape[0]
18 + assert all(tensor.shape[0] == local_num_tokens for tensor in tensors)
19 + assert 0 <= num_decode_tokens <= local_num_tokens
20 +
21 + if num_decode_tokens == local_num_tokens:
22 + return tensors, slot_mapping[:num_decode_tokens]
23 +
24 + pcp_group = get_pcp_group()
25 + gathered_prefills = tuple(
26 + pcp_group.all_gather(tensor[num_decode_tokens:].contiguous(), dim=0)
27 + for tensor in tensors
28 + )
29 + pcp_size = pcp_group.world_size
30 + gathered_slot_mapping = slot_mapping[: pcp_size * local_num_tokens]
31 + if num_decode_tokens == 0:
32 + return gathered_prefills, gathered_slot_mapping
33 +
34 + cache_inputs = tuple(
35 + torch.cat((tensor[:num_decode_tokens], gathered_prefill), dim=0)
36 + for tensor, gathered_prefill in zip(tensors, gathered_prefills)
37 + )
38 + rank_slot_mappings = gathered_slot_mapping.view(pcp_size, local_num_tokens)
39 + cache_slot_mapping = torch.cat(
40 + (
41 + rank_slot_mappings[0, :num_decode_tokens],
42 + rank_slot_mappings[:, num_decode_tokens:].flatten(),
43 + )
44 + )
45 + return cache_inputs, cache_slot_mapping
46 +
47 +
48 +def maybe_gather_mla_latent_cache_inputs(
49 + kv_c_normed: torch.Tensor,
50 + k_pe: torch.Tensor,
51 + slot_mapping: torch.Tensor | None,
52 + num_decode_tokens: int | None,
53 + use_pcp: bool,
54 +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
55 + if not use_pcp or num_decode_tokens is None:
56 + return kv_c_normed, k_pe, slot_mapping
57 + assert slot_mapping is not None
58 + num_tokens = kv_c_normed.shape[0]
59 + k_pe_flat = k_pe.reshape(num_tokens, -1)
60 + (cache_kv_c, cache_k_pe_flat), cache_slot_mapping = _gather_prefill_cache_inputs(
61 + (kv_c_normed, k_pe_flat),
62 + slot_mapping,
63 + num_decode_tokens,
64 + )
65 + cache_k_pe = cache_k_pe_flat.view(-1, *k_pe.shape[1:])
66 + return cache_kv_c, cache_k_pe, cache_slot_mapping
67 +
68 +
69 +def maybe_gather_indexer_k(
70 + k: torch.Tensor,
71 + slot_mapping: torch.Tensor,
72 + num_decode_tokens: int,
73 + use_pcp: bool,
74 +) -> tuple[torch.Tensor, torch.Tensor]:
75 + if not use_pcp:
76 + return k, slot_mapping
77 + (cache_k,), cache_slot_mapping = _gather_prefill_cache_inputs(
78 + (k,), slot_mapping, num_decode_tokens
79 + )
80 + return cache_k, cache_slot_mapping
81 +
82 +
83 +def finalize_mla_pcp_decode(
84 + output: torch.Tensor,
85 + num_heads: int,
86 +) -> torch.Tensor:
87 + if output.shape[1] < num_heads:
88 + output = get_pcp_group().all_gather(output, dim=1)
89 + elif output.shape[1] > num_heads:
90 + head_start = get_tp_group().rank_in_group * num_heads
91 + output = output[:, head_start : head_start + num_heads]
92 + return output
vllm/model_executor/layers/attention/sparse_mla_attention.py
+2 -2
@@ -59,13 +59,12 @@ class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
59 device=device,
60 )
61 parallel_config = vllm_config.parallel_config
62 + self.use_pcp = parallel_config.prefill_context_parallel_size > 1
63 try:
64 self.dcp_world_size = get_dcp_group().world_size
64 - self.dcp_rank = get_dcp_group().rank_in_group
65 except AssertionError:
66 # DCP might not be initialized in testing
67 self.dcp_world_size = 1
68 - self.dcp_rank = 0
68 self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
69 self.dcp_local_block_size = self.cp_kv_cache_interleave_size
70 self.dcp_virtual_block_size = self.dcp_local_block_size * self.dcp_world_size
@@ -174,6 +173,7 @@ class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
173 num_decodes, num_prefills, num_decode_tokens, _ = split_decodes_and_prefills(
174 common_attn_metadata,
175 decode_threshold=self.reorder_batch_threshold or 1,
176 + treat_short_extends_as_decodes=not self.use_pcp,
177 require_uniform=self.require_uniform_decodes,
178 )
179 (
vllm/model_executor/layers/fused_moe/config.py
+3 -1
@@ -1050,7 +1050,9 @@ class FusedMoEParallelConfig:
1050
1051 @property
1052 def use_all2all_kernels(self):
1053 - return self.use_ep and (self.dp_size > 1 or self.is_sequence_parallel)
1053 + return self.use_ep and (
1054 + self.dp_size > 1 or self.pcp_size > 1 or self.is_sequence_parallel
1055 + )
1056
1057 @property
1058 def use_deepep_ht_kernels(self):
vllm/model_executor/layers/fused_moe/runner/moe_runner.py
+11 -17
@@ -752,18 +752,12 @@ class MoERunner(MoERunnerInterface):
752 assert len(result) == 2
753 hidden_states, router_logits = result
754
755 - # NOTE: Similar with DP, PCP also needs dispatch and combine. For
756 - # simplicity, AgRsAll2All was added separately for PCP here. Maybe
757 - # we should modify All2AllManager abstraction to better support PCP.
758 - if self.moe_config.pcp_size > 1:
759 - hidden_states = get_pcp_group().all_gather(
760 - hidden_states,
761 - dim=0,
762 - )
763 - router_logits = get_pcp_group().all_gather(
764 - router_logits,
765 - dim=0,
766 - )
755 + if (
756 + self.moe_config.pcp_size > 1
757 + and not self.moe_config.moe_parallel_config.use_all2all_kernels
758 + ):
759 + hidden_states = get_pcp_group().all_gather(hidden_states, dim=0)
760 + router_logits = get_pcp_group().all_gather(router_logits, dim=0)
761
762 return hidden_states, router_logits
763
@@ -777,11 +771,11 @@ class MoERunner(MoERunnerInterface):
771 hidden_states, self.moe_config.is_sequence_parallel
772 )
773
780 - if self.moe_config.pcp_size > 1:
781 - hidden_states = get_pcp_group().reduce_scatter(
782 - hidden_states,
783 - dim=0,
784 - )
774 + if (
775 + self.moe_config.pcp_size > 1
776 + and not self.moe_config.moe_parallel_config.use_all2all_kernels
777 + ):
778 + hidden_states = get_pcp_group().reduce_scatter(hidden_states, dim=0)
779
780 if self.shared_experts is not None:
781 assert shared_output is not None
vllm/model_executor/layers/sparse_attn_indexer.py
+26 -3
@@ -9,10 +9,11 @@ from vllm import _custom_ops as ops
9 from vllm._aiter_ops import rocm_aiter_ops
10 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
11 from vllm.config import get_current_vllm_config
12 -from vllm.distributed import get_dcp_group
12 +from vllm.distributed import get_dcp_group, get_pcp_group
13 from vllm.forward_context import get_forward_context
14 from vllm.logger import init_logger
15 from vllm.model_executor.custom_op import CustomOp
16 +from vllm.model_executor.layers.attention.pcp import maybe_gather_indexer_k
17 from vllm.model_executor.layers.quantization.utils.quant_utils import (
18 get_fp8_min_max,
19 )
@@ -308,6 +309,7 @@ def sparse_attn_indexer(
309 total_seq_lens: int,
310 topk_indices_buffer: torch.Tensor,
311 skip_k_cache_insert: bool,
312 + use_pcp: bool,
313 use_fp4_cache: bool = False,
314 dcp_rank: int = 0,
315 dcp_world_size: int = 1,
@@ -354,6 +356,7 @@ def sparse_attn_indexer(
356 total_seq_lens,
357 topk_indices_buffer,
358 skip_k_cache_insert,
359 + use_pcp,
360 use_fp4_cache,
361 )
362 attn_metadata_narrowed = attn_metadata[k_cache_prefix]
@@ -373,18 +376,28 @@ def sparse_attn_indexer(
376 # During speculative decoding, k may be padded to the CUDA graph batch
377 # size while slot_mapping only covers actual tokens. Truncate k to avoid
378 # out-of-bounds reads in the kernel.
379 + # Keep PCP padding so every rank contributes the same all-gather shape.
380 num_tokens = slot_mapping.shape[0]
381 + if use_pcp:
382 + num_tokens //= get_pcp_group().world_size
383 if k is not None:
384 k = k[:num_tokens]
385
386 if not skip_k_cache_insert:
387 + assert k is not None
388 + k, slot_mapping_for_cache = maybe_gather_indexer_k(
389 + k,
390 + slot_mapping,
391 + num_decode_tokens,
392 + use_pcp,
393 + )
394 # scale_fmt can be None, but the function expects str
395 assert scale_fmt is not None
396 assert not use_fp4_cache, "Unfused FP4 Insert is not supported yet"
397 ops.indexer_k_quant_and_cache(
398 k,
399 kv_cache,
387 - slot_mapping,
400 + slot_mapping_for_cache,
401 quant_block_size,
402 scale_fmt,
403 )
@@ -498,7 +511,14 @@ def sparse_attn_indexer(
511 assert decode_metadata is not None
512 kv_cache = kv_cache_as_quant_view(kv_cache, head_dim, use_fp4_cache)
513 decode_lens = decode_metadata.decode_lens
501 - if decode_metadata.requires_padding:
514 + if num_decode_tokens == 0:
515 + padded_q_quant_decode_tokens = q_quant[:1].reshape(1, 1, *q_quant.shape[1:])
516 + padded_q_scale = (
517 + q_scale[:1].reshape(1, 1, *q_scale.shape[1:])
518 + if q_scale is not None
519 + else None
520 + )
521 + elif decode_metadata.requires_padding:
522 # pad in edge case where we have short chunked prefill length <
523 # decode_threshold since we unstrictly split
524 # prefill and decode by decode_threshold
@@ -663,6 +683,7 @@ def sparse_attn_indexer_fake(
683 total_seq_lens: int,
684 topk_indices_buffer: torch.Tensor | None,
685 skip_k_cache_insert: bool,
686 + use_pcp: bool,
687 use_fp4_cache: bool = False,
688 dcp_rank: int = 0,
689 dcp_world_size: int = 1,
@@ -725,6 +746,7 @@ class SparseAttnIndexer(CustomOp):
746 self.dcp_world_size = parallel_config.decode_context_parallel_size
747 self.dcp_rank = get_dcp_group().rank_in_group if self.dcp_world_size > 1 else 0
748 self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
749 + self.use_pcp = parallel_config.prefill_context_parallel_size > 1
750 if current_platform.is_cuda() and not has_deep_gemm():
751 raise RuntimeError(
752 "Sparse Attention Indexer CUDA op requires DeepGEMM support in "
@@ -777,6 +799,7 @@ class SparseAttnIndexer(CustomOp):
799 self.max_total_seq_len,
800 self.topk_indices_buffer,
801 self.skip_k_cache_insert,
802 + self.use_pcp,
803 self.use_fp4_cache,
804 self.dcp_rank,
805 self.dcp_world_size,
vllm/v1/attention/backend.py
+14 -2
@@ -281,6 +281,13 @@ class AttentionBackend(ABC):
281 def supports_kv_connector(cls) -> bool:
282 return True
283
284 + @classmethod
285 + def supports_pcp(cls) -> bool:
286 + try:
287 + return cls.get_impl_cls().supports_pcp
288 + except NotImplementedError:
289 + return False
290 +
291 @classmethod
292 def supports_attn_type(cls, attn_type: str) -> bool:
293 """Check if backend supports a given attention type.
@@ -327,6 +334,7 @@ class AttentionBackend(ABC):
334 use_non_causal: bool = False,
335 use_batch_invariant: bool = False,
336 use_kv_connector: bool = False,
337 + use_pcp: bool = False,
338 ) -> list[str]:
339 invalid_reasons = []
340 if not cls.supports_head_size(head_size):
@@ -367,6 +375,8 @@ class AttentionBackend(ABC):
375 invalid_reasons.append("batch invariance not supported")
376 if use_kv_connector and not cls.supports_kv_connector():
377 invalid_reasons.append("KV connector not supported")
378 + if use_pcp and not cls.supports_pcp():
379 + invalid_reasons.append("PCP not supported")
380 combination_reason = cls.supports_combination(
381 head_size,
382 dtype,
@@ -858,8 +868,8 @@ class AttentionImplBase(ABC, Generic[T]):
868 except AssertionError:
869 self.pcp_world_size = 1
870 self.pcp_rank = 0
861 - self.total_cp_world_size = self.pcp_world_size * self.dcp_world_size
862 - self.total_cp_rank = self.pcp_rank * self.dcp_world_size + self.dcp_rank
871 + self.total_cp_world_size = self.dcp_world_size
872 + self.total_cp_rank = self.dcp_rank
873
874 self.need_to_return_lse_for_decode = (
875 self.dcp_world_size > 1 and self.can_return_lse_for_decode
@@ -987,6 +997,8 @@ class AttentionImpl(AttentionImplBase[T], Generic[T]):
997 class MLAAttentionImpl(AttentionImplBase[T], Generic[T]):
998 """MLA attention implementation with forward_mqa and forward_mha methods."""
999
1000 + supports_pcp: bool = True
1001 +
1002 @abstractmethod
1003 def __init__(
1004 self,
vllm/v1/attention/backends/mla/indexer.py
+19 -4
@@ -6,7 +6,7 @@ import torch
6
7 import vllm.envs as envs
8 from vllm.config import VllmConfig
9 -from vllm.distributed import get_dcp_group
9 +from vllm.distributed import get_dcp_group, get_pcp_group
10 from vllm.logger import init_logger
11 from vllm.platforms import current_platform
12 from vllm.triton_utils import tl, triton
@@ -29,7 +29,7 @@ from vllm.v1.attention.backends.utils import (
29 split_decodes_and_prefills,
30 )
31 from vllm.v1.kv_cache_interface import AttentionSpec, MLAAttentionSpec
32 -from vllm.v1.worker.cp_utils import get_total_cp_world_size
32 +from vllm.v1.worker.cp_utils import get_kv_cache_shard_count
33
34 logger = init_logger(__name__)
35
@@ -109,7 +109,7 @@ def split_indexer_prefill_chunks(
109 end += 1
110
111 req_slice = slice(start + request_offset, end + request_offset)
112 - max_q = max(1, max_logits_elems // chunk_n) if chunk_n > 0 else chunk_m
112 + max_q = max(1, max_logits_elems // chunk_n) if chunk_n > 0 else max(1, chunk_m)
113 for q_off in range(0, chunk_m, max_q):
114 sub_m = min(max_q, chunk_m - q_off)
115 chunks.append((req_slice, slice(q_off, q_off + sub_m)))
@@ -118,6 +118,10 @@ def split_indexer_prefill_chunks(
118
119
120 class DeepseekV32IndexerBackend(AttentionBackend):
121 + @classmethod
122 + def supports_pcp(cls) -> bool:
123 + return True
124 +
125 @staticmethod
126 def get_name() -> str:
127 return "DEEPSEEK_V32_INDEXER"
@@ -256,6 +260,8 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
260 parallel_config = self.vllm_config.parallel_config
261 self.dcp_world_size = parallel_config.decode_context_parallel_size
262 self.dcp_rank = get_dcp_group().rank_in_group if self.dcp_world_size > 1 else 0
263 + self.pcp_world_size = parallel_config.prefill_context_parallel_size
264 + self.use_pcp = self.pcp_world_size > 1
265 self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
266 # The DCP sparse-indexer code is parameterized by interleave size, but
267 # interleave > 1 is not yet validated end-to-end (gsm8k parity fails),
@@ -339,7 +345,7 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
345 )
346 max_num_blocks_per_req = cdiv(
347 self.vllm_config.model_config.max_model_len,
342 - self.kv_cache_spec.block_size * get_total_cp_world_size(),
348 + self.kv_cache_spec.block_size * get_kv_cache_shard_count(),
349 )
350 self.expanded_block_table_buffer = torch.zeros(
351 (
@@ -570,6 +576,7 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
576 common_attn_metadata,
577 decode_threshold=self.decode_threshold,
578 require_uniform=not self.use_flattening,
579 + treat_short_extends_as_decodes=not self.use_pcp,
580 )
581 )
582
@@ -579,6 +586,9 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
586 compressed_slot_mapping = slot_mapping
587 compressed_seq_lens = seq_lens
588 if self.compress_ratio > 1:
589 + padded_num_tokens = num_tokens
590 + if self.pcp_world_size > 1:
591 + padded_num_tokens = slot_mapping.shape[0] // self.pcp_world_size
592 compressed_slot_mapping = get_compressed_slot_mapping(
593 num_tokens,
594 query_start_loc,
@@ -588,6 +598,11 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
598 self.compress_ratio,
599 out=self.compressed_slot_mapping_buffer,
600 )
601 + if self.pcp_world_size > 1:
602 + compressed_slot_mapping = get_pcp_group().all_gather(
603 + self.compressed_slot_mapping_buffer[:padded_num_tokens],
604 + dim=0,
605 + )
606 compressed_seq_lens = seq_lens // self.compress_ratio
607
608 prefill_metadata = None
vllm/v1/attention/backends/utils.py
+3 -1
@@ -612,7 +612,9 @@ def split_decodes_and_prefills(
612 # check if we are in a padded uniform batch; this is used for full-CGs, some
613 # requests may have a query length of 0 but since they are padding its fine
614 # to treat them as decodes (ensures num_decodes matches the captured size)
615 - if torch.all((query_lens == query_lens[0]) | (query_lens == 0)):
615 + if treat_short_extends_as_decodes and torch.all(
616 + (query_lens == query_lens[0]) | (query_lens == 0)
617 + ):
618 return num_reqs, 0, num_tokens, 0 # all decodes
619 is_prefill = query_lens != query_lens[0]
620 else:
vllm/v1/attention/selector.py
+4 -1
@@ -36,6 +36,7 @@ class AttentionSelectorConfig(NamedTuple):
36 use_non_causal: bool = False
37 use_batch_invariant: bool = False
38 use_kv_connector: bool = False
39 + use_pcp: bool = False
40
41 def __repr__(self):
42 return (
@@ -52,7 +53,8 @@ class AttentionSelectorConfig(NamedTuple):
53 f"has_sliding_window={self.has_sliding_window}, "
54 f"use_non_causal={self.use_non_causal}, "
55 f"use_batch_invariant={self.use_batch_invariant}, "
55 - f"use_kv_connector={self.use_kv_connector})"
56 + f"use_kv_connector={self.use_kv_connector}, "
57 + f"use_pcp={self.use_pcp})"
58 )
59
60
@@ -150,6 +152,7 @@ def get_attn_backend(
152 use_non_causal=vllm_config.attention_config.use_non_causal,
153 use_batch_invariant=envs.VLLM_BATCH_INVARIANT,
154 use_kv_connector=use_kv_connector,
155 + use_pcp=vllm_config.parallel_config.prefill_context_parallel_size > 1,
156 )
157
158 # A per-KV-group override (keyed by KVCacheSpecKind) takes precedence over
vllm/v1/core/kv_cache_coordinator.py
-2
@@ -472,8 +472,6 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
472 self.pcp_world_size = pcp_world_size
473 if dcp_world_size > 1:
474 self.block_size *= dcp_world_size
475 - if pcp_world_size > 1:
476 - self.block_size *= pcp_world_size
475 # For models using only Mamba, block_size is set to max_model_len when
476 # prefix caching is disabled, and hash_block_size validation is skipped.
477 assert not enable_caching or (hash_block_size == self.block_size), (
vllm/v1/core/kv_cache_utils.py
+5 -6
@@ -631,8 +631,8 @@ def resolve_kv_cache_block_sizes(
631
632 - ``scheduler_block_size`` is the token-alignment invariant used by the
633 scheduler (e.g. for ``num_computed_tokens`` rounding). Single group:
634 - ``cache_config.block_size * dcp * pcp``. Multiple groups: LCM of every
635 - group's effective block size. Attention groups are scaled by DCP/PCP;
634 + ``cache_config.block_size * dcp``. Multiple groups: LCM of every
635 + group's effective block size. Attention groups are scaled by DCP;
636 Mamba groups keep their full per-rank state and are not scaled.
637 - ``hash_block_size`` is the granularity at which ``Request.block_hashes``
638 is computed. Single group: equals scheduler block size. Multiple groups:
@@ -644,15 +644,14 @@ def resolve_kv_cache_block_sizes(
644 """
645 cache_config = vllm_config.cache_config
646 dcp = vllm_config.parallel_config.decode_context_parallel_size
647 - pcp = vllm_config.parallel_config.prefill_context_parallel_size
647 groups = kv_cache_config.kv_cache_groups
648
650 - if len(groups) <= 1: # Single group: block_size * dcp * pcp
651 - bs = cache_config.block_size * dcp * pcp
649 + if len(groups) <= 1:
650 + bs = cache_config.block_size * dcp
651 return bs, bs
652
653 group_block_sizes = [
655 - g.kv_cache_spec.block_size * dcp * pcp
654 + g.kv_cache_spec.block_size * dcp
655 if isinstance(g.kv_cache_spec, AttentionSpec)
656 else g.kv_cache_spec.block_size
657 for g in groups
vllm/v1/core/sched/scheduler.py
+1 -1
@@ -271,7 +271,7 @@ class Scheduler(SchedulerInterface):
271 log_stats=self.log_stats,
272 enable_kv_cache_events=self.enable_kv_cache_events,
273 dcp_world_size=self.dcp_world_size,
274 - pcp_world_size=self.pcp_world_size,
274 + pcp_world_size=1,
275 scheduler_block_size=self.block_size,
276 hash_block_size=hash_block_size,
277 metrics_collector=self.kv_metrics_collector,
vllm/v1/core/single_type_kv_cache_manager.py
+6 -6
@@ -75,8 +75,8 @@ class SingleTypeKVCacheManager(ABC):
75 self.block_size = kv_cache_spec.block_size
76 self.dcp_world_size = dcp_world_size
77 self.pcp_world_size = pcp_world_size
78 - if dcp_world_size * pcp_world_size > 1:
79 - self.block_size *= dcp_world_size * pcp_world_size
78 + if dcp_world_size > 1:
79 + self.block_size *= dcp_world_size
80 self.kv_cache_spec = kv_cache_spec
81 self.block_pool = block_pool
82 self.enable_caching = enable_caching
@@ -674,10 +674,10 @@ class FullAttentionManager(SingleTypeKVCacheManager):
674 "and chunked local attention groups"
675 )
676 block_size = kv_cache_spec.block_size
677 - if dcp_world_size * pcp_world_size > 1:
678 - # DCP/PCP shard each block's KV across ranks; hashes must be
679 - # viewed at the sharded (scaled) block size.
680 - block_size *= dcp_world_size * pcp_world_size
677 + if dcp_world_size > 1:
678 + # DCP shards each block's KV across ranks; hashes must be viewed at
679 + # the sharded block size.
680 + block_size *= dcp_world_size
681 block_hashes = resolve_block_hashes(
682 block_hashes,
683 block_pool.hash_block_size,
vllm/v1/kv_cache_interface.py
+4 -12
@@ -218,14 +218,9 @@ class AttentionSpec(KVCacheSpec):
218 )
219
220 def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
221 - # Attention KV is token-interleaved across DCP/PCP ranks, so each rank
222 - # only stores max_len // (dcp * pcp) tokens per request.
221 parallel_config = vllm_config.parallel_config
224 - total_cp_size = (
225 - parallel_config.decode_context_parallel_size
226 - * parallel_config.prefill_context_parallel_size
227 - )
228 - return cdiv(max_len, self.block_size * total_cp_size)
222 + kv_shard_count = parallel_config.decode_context_parallel_size
223 + return cdiv(max_len, self.block_size * kv_shard_count)
224
225
226 @dataclass(frozen=True, kw_only=True)
@@ -263,11 +258,8 @@ class FullAttentionSpec(AttentionSpec):
258 def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
259 max_model_len = vllm_config.model_config.max_model_len
260 dcp_world_size = vllm_config.parallel_config.decode_context_parallel_size
266 - pcp_world_size = vllm_config.parallel_config.prefill_context_parallel_size
267 - # Note(hc): each dcp rank only need save
268 - # (max_model_len//dcp_world_size) tokens locally.
269 - if dcp_world_size * pcp_world_size > 1:
270 - max_model_len = cdiv(max_model_len, dcp_world_size * pcp_world_size)
261 + if dcp_world_size > 1:
262 + max_model_len = cdiv(max_model_len, dcp_world_size)
263 return cdiv(max_model_len, self.block_size) * self.page_size_bytes
264
265 @classmethod
vllm/v1/simple_kv_offload/manager.py
+3 -4
@@ -83,8 +83,7 @@ class SimpleCPUOffloadScheduler:
83 and vllm_config.kv_events_config.enable_kv_cache_events
84 )
85 dcp_world_size = vllm_config.parallel_config.decode_context_parallel_size
86 - pcp_world_size = vllm_config.parallel_config.prefill_context_parallel_size
87 - self.cp_world_size = dcp_world_size * pcp_world_size
86 + self.cp_world_size = dcp_world_size
87 self.block_size = scheduler_block_size
88 self.hash_block_size = hash_block_size
89 assert self.block_size % self.hash_block_size == 0
@@ -127,7 +126,7 @@ class SimpleCPUOffloadScheduler:
126 enable_caching=True,
127 enable_kv_cache_events=self.enable_kv_cache_events,
128 dcp_world_size=dcp_world_size,
130 - pcp_world_size=pcp_world_size,
129 + pcp_world_size=1,
130 scheduler_block_size=self.block_size,
131 hash_block_size=self.hash_block_size,
132 )
@@ -354,7 +353,7 @@ class SimpleCPUOffloadScheduler:
353 )
354 assert num_external_tokens % g_block_size == 0, (
355 f"num_external_tokens={num_external_tokens} not aligned to "
357 - f"group {g} effective block_size={g_block_size}"
356 + f"group {g} block_size={g_block_size}"
357 )
358 n_take_g = num_external_tokens // g_block_size
359 cpu_hit_blocks.append(cpu_hit_blocks_full[g][:n_take_g])
vllm/v1/worker/block_table.py
+2 -4
@@ -163,8 +163,6 @@ class BlockTable:
163 return
164 assert self.slot_mapping_mode == SlotMappingMode.TOKEN_TO_KV_SLOT
165
166 - total_cp_world_size = self.pcp_world_size * self.dcp_world_size
167 - total_cp_rank = self.pcp_rank * self.dcp_world_size + self.dcp_rank
166 _compute_slot_mapping_kernel[(num_reqs + 1,)](
167 num_tokens,
168 self.max_num_batched_tokens,
@@ -176,8 +174,8 @@ class BlockTable:
174 self.slot_mapping.gpu,
175 KV_CACHE_BLOCK_SIZE=self.kv_cache_block_size,
176 BLOCKS_PER_KV_BLOCK=self.blocks_per_kv_block,
179 - TOTAL_CP_WORLD_SIZE=total_cp_world_size,
180 - TOTAL_CP_RANK=total_cp_rank,
177 + TOTAL_CP_WORLD_SIZE=self.dcp_world_size,
178 + TOTAL_CP_RANK=self.dcp_rank,
179 CP_KV_CACHE_INTERLEAVE_SIZE=self.cp_kv_cache_interleave_size,
180 PAD_ID=PAD_SLOT_ID,
181 BLOCK_SIZE=1024,
vllm/v1/worker/cp_utils.py
+10 -15
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, cast
6 import torch
7
8 from vllm.config import VllmConfig, get_layers_from_vllm_config
9 -from vllm.distributed import get_dcp_group, get_pcp_group
9 +from vllm.distributed import get_dcp_group
10 from vllm.logger import init_logger
11 from vllm.v1.attention.backend import CommonAttentionMetadata
12 from vllm.v1.attention.backends.utils import split_decodes_prefills_and_extends
@@ -27,6 +27,13 @@ def check_attention_cp_compatibility(vllm_config: VllmConfig) -> None:
27 layer_type = cast(type[Any], AttentionLayerBase)
28 layers = get_layers_from_vllm_config(vllm_config, layer_type)
29 for layer in layers.values():
30 + get_attn_backend = getattr(layer, "get_attn_backend", None)
31 + if pcp_size > 1 and get_attn_backend is not None:
32 + backend = get_attn_backend()
33 + assert backend.supports_pcp(), (
34 + "PCP requires attention backend support, "
35 + f"but {backend.get_name()} does not support PCP."
36 + )
37 layer_impl = getattr(layer, "impl", None)
38 if layer_impl is None:
39 continue
@@ -44,26 +51,14 @@ def check_attention_cp_compatibility(vllm_config: VllmConfig) -> None:
51 "--attention-backend or disable DCP."
52 )
53
47 - if pcp_size > 1:
48 - assert layer_impl.supports_pcp, (
49 - "PCP requires attention impls' support, "
50 - f"but the impl {layer_impl.__class__.__name__} "
51 - "does not support PCP."
52 - )
53 -
54
55 -def get_total_cp_world_size():
56 - try:
57 - pcp_world_size = get_pcp_group().world_size
58 - except AssertionError:
59 - # PCP might not be initialized in testing
60 - pcp_world_size = 1
55 +def get_kv_cache_shard_count() -> int:
56 try:
57 dcp_world_size = get_dcp_group().world_size
58 except AssertionError:
59 # DCP might not be initialized in testing
60 dcp_world_size = 1
66 - return dcp_world_size * pcp_world_size
61 + return dcp_world_size
62
63
64 def get_dcp_dummy_context_len(
vllm/v1/worker/gpu/attn_utils.py
+7
@@ -578,6 +578,7 @@ def build_attn_metadata(
578 seq_lens_cpu_upper_bound: torch.Tensor | None = None,
579 dcp_local_seq_lens: torch.Tensor | None = None,
580 positions: torch.Tensor | None = None,
581 + is_prefilling: torch.Tensor | None = None,
582 mm_req_doc_ranges: dict[int, list[tuple[int, int]]] | None = None,
583 model_specific_attn_metadata: ModelSpecificAttnMetadata | None = None,
584 for_cudagraph_capture: bool = False,
@@ -605,6 +606,11 @@ def build_attn_metadata(
606 if model_specific_attn_metadata is not None
607 else {}
608 )
609 + # Model-specific metadata (e.g. Mamba hybrid) may supply its own
610 + # padding-aware is_prefilling, which takes precedence over the default.
611 + group_is_prefilling = common_attn_metadata_extra_kwargs.pop(
612 + "is_prefilling", is_prefilling
613 + )
614 common_attn_metadata = CommonAttentionMetadata(
615 query_start_loc=query_start_loc_gpu,
616 query_start_loc_cpu=query_start_loc_cpu,
@@ -619,6 +625,7 @@ def build_attn_metadata(
625 causal=group_causal,
626 dcp_local_seq_lens=dcp_local_seq_lens,
627 positions=positions,
628 + is_prefilling=group_is_prefilling,
629 mm_req_doc_ranges=mm_req_doc_ranges,
630 rswa_prefix_lens=rswa_prefix_lens,
631 **common_attn_metadata_extra_kwargs,
vllm/v1/worker/gpu/block_table.py
+16 -6
@@ -135,20 +135,28 @@ class BlockTables:
135 self,
136 idx_mapping: torch.Tensor,
137 num_reqs_padded: int,
138 + out: tuple[torch.Tensor, ...] | None = None,
139 + out_ptrs: torch.Tensor | None = None,
140 ) -> tuple[torch.Tensor, ...]:
141 + if out is None:
142 + out = tuple(self.input_block_tables)
143 + out_ptrs = self.input_block_table_ptrs
144 + else:
145 + assert out_ptrs is not None
146 + assert len(out) == self.num_kv_cache_groups
147 num_reqs = idx_mapping.shape[0]
148 # Launch kernel with num_reqs_padded to fuse zeroing of padded rows.
149 _gather_block_tables_kernel[(self.num_kv_cache_groups, num_reqs_padded)](
150 idx_mapping,
151 self.block_table_ptrs,
144 - self.input_block_table_ptrs,
152 + out_ptrs,
153 self.block_table_strides,
154 self.num_blocks.gpu,
155 self.num_blocks.gpu.stride(0),
156 num_reqs,
157 BLOCK_SIZE=1024, # type: ignore
158 )
151 - return tuple(bt[:num_reqs_padded] for bt in self.input_block_tables)
159 + return tuple(bt[:num_reqs_padded] for bt in out)
160
161 def get_dummy_block_tables(self, num_reqs: int) -> tuple[torch.Tensor, ...]:
162 # NOTE(woosuk): The output may be used for CUDA graph capture.
@@ -163,26 +171,28 @@ class BlockTables:
171 query_start_loc: torch.Tensor,
172 positions: torch.Tensor,
173 num_tokens_padded: int,
174 + out: torch.Tensor | None = None,
175 ) -> torch.Tensor:
176 num_reqs = idx_mapping.shape[0]
177 num_groups = self.num_kv_cache_groups
178 + slot_mappings = self.slot_mappings if out is None else out
179 _compute_slot_mappings_kernel[(num_groups, num_reqs + 1)](
170 - self.max_num_batched_tokens,
180 + slot_mappings.shape[1],
181 idx_mapping,
182 query_start_loc,
183 positions,
184 self.block_table_ptrs,
185 self.block_table_strides,
186 self.block_sizes_tensor,
177 - self.slot_mappings,
178 - self.slot_mappings.stride(0),
187 + slot_mappings,
188 + slot_mappings.stride(0),
189 self.cp_rank,
190 CP_SIZE=self.cp_size,
191 CP_INTERLEAVE=self.cp_interleave,
192 PAD_ID=PAD_SLOT_ID,
193 TRITON_BLOCK_SIZE=1024, # type: ignore
194 )
185 - return self.slot_mappings[:, :num_tokens_padded]
195 + return slot_mappings[:, :num_tokens_padded]
196
197 def get_dummy_slot_mappings(self, num_tokens: int) -> torch.Tensor:
198 # Fill the entire slot_mappings tensor, not just the first `num_tokens` entries.
vllm/v1/worker/gpu/model_runner.py
+21 -3
@@ -52,6 +52,7 @@ from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
52 from vllm.v1.kv_cache_interface import KVCacheConfig, MambaSpec
53 from vllm.v1.outputs import DraftTokenIds, ModelRunnerOutput
54 from vllm.v1.worker.cp_utils import check_attention_cp_compatibility
55 +from vllm.v1.worker.gpu import pcp_manager as pcp
56 from vllm.v1.worker.gpu.async_utils import AsyncOutput, AsyncPoolingOutput
57 from vllm.v1.worker.gpu.attn_utils import (
58 build_slot_mappings_by_layer,
@@ -205,6 +206,8 @@ class GPUModelRunner(LoRAModelRunnerMixin):
206 # Draft tokens propagation - for spec-dec + struct outputs.
207 self.draft_tokens_handler = DraftTokensHandler(self.device)
208
209 + self.pcp_manager: pcp.PCPManager | None = None
210 +
211 # Pooling models.
212 self.is_pooling_model = self.model_config.runner_type == "pooling"
213 self.pooling_runner: PoolingRunner | None = None
@@ -456,6 +459,13 @@ class GPUModelRunner(LoRAModelRunnerMixin):
459 cp_rank=self.dcp_rank,
460 cp_interleave=self.cp_interleave,
461 )
462 + self.pcp_manager = pcp.maybe_build_pcp_manager(
463 + self.vllm_config,
464 + self.device,
465 + self.supports_mm_inputs,
466 + self.req_states,
467 + self.block_tables,
468 + )
469 initialize_mamba_ssu_backend(
470 self.vllm_config.mamba_config, self.kv_cache_config
471 )
@@ -1008,7 +1018,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1018 # prompt_lens is only used in R-SWA case.
1019 prompt_lens = self.req_states.prompt_len.gpu[idx_mapping]
1020
1011 - return InputBatch(
1021 + input_batch = InputBatch(
1022 req_ids=req_ids,
1023 num_reqs=num_reqs,
1024 num_reqs_after_padding=num_reqs_padded,
@@ -1040,10 +1050,14 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1050 has_structured_output_reqs=scheduler_output.has_structured_output_requests,
1051 prompt_lens=prompt_lens,
1052 )
1053 + return pcp.maybe_partition_pcp_batch(self.pcp_manager, input_batch)
1054
1055 def prepare_attn(
1056 self, input_batch: InputBatch
1057 ) -> tuple[tuple[torch.Tensor, ...], torch.Tensor]:
1058 + if self.pcp_manager is not None:
1059 + return self.pcp_manager.prepare_attn(input_batch)
1060 +
1061 # Block tables: num_kv_cache_groups x [num_reqs_padded, max_num_blocks].
1062 block_tables = self.block_tables.gather_block_tables(
1063 input_batch.idx_mapping,
@@ -1063,8 +1077,8 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1077 self, input_batch: InputBatch
1078 ) -> tuple[tuple[torch.Tensor, ...], torch.Tensor]:
1079 block_tables = self.block_tables.get_dummy_block_tables(input_batch.num_reqs)
1066 - slot_mappings = self.block_tables.get_dummy_slot_mappings(
1067 - input_batch.num_tokens
1080 + slot_mappings = pcp.maybe_get_pcp_dummy_slot_mappings(
1081 + self.pcp_manager, self.block_tables, input_batch.num_tokens
1082 )
1083 return block_tables, slot_mappings
1084
@@ -1412,6 +1426,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1426 return ModelRunnerOutput.with_kv_conn_output_only(kv_connector_output)
1427
1428 # Last rank: sample tokens
1429 + hidden_states, input_batch = pcp.maybe_restore_pcp_for_sampling(
1430 + self.pcp_manager, hidden_states, input_batch
1431 + )
1432 +
1433 sampler_output, num_sampled, num_rejected = self.sample(
1434 hidden_states, input_batch, grammar_output
1435 )
vllm/v1/worker/gpu/model_states/default.py
+6 -2
@@ -151,7 +151,10 @@ class DefaultModelState(ModelState):
151 # For piecewise cudagraphs and eager, use unpadded sizes.
152 num_reqs = input_batch.num_reqs
153 num_tokens = input_batch.num_tokens
154 - query_start_loc_cpu = torch.from_numpy(input_batch.query_start_loc_np)
154 + query_start_loc_cpu = torch.from_numpy(
155 + input_batch.query_start_loc_np[: num_reqs + 1]
156 + )
157 + query_start_loc_gpu = input_batch.query_start_loc[: num_reqs + 1]
158 max_query_len = input_batch.num_scheduled_tokens.max().item()
159 seq_lens_cpu_upper_bound = input_batch.seq_lens_cpu_upper_bound
160 if for_capture:
@@ -174,7 +177,7 @@ class DefaultModelState(ModelState):
177 attn_groups=attn_groups,
178 num_reqs=num_reqs,
179 num_tokens=num_tokens,
177 - query_start_loc_gpu=input_batch.query_start_loc,
180 + query_start_loc_gpu=query_start_loc_gpu,
181 query_start_loc_cpu=query_start_loc_cpu,
182 max_query_len=max_query_len,
183 seq_lens=input_batch.seq_lens,
@@ -185,6 +188,7 @@ class DefaultModelState(ModelState):
188 seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound,
189 dcp_local_seq_lens=input_batch.dcp_local_seq_lens,
190 positions=input_batch.positions,
191 + is_prefilling=torch.from_numpy(input_batch.is_prefilling_np),
192 mm_req_doc_ranges=req_doc_ranges,
193 for_cudagraph_capture=for_capture,
194 rswa_prefix_lens=input_batch.prompt_lens,
vllm/v1/worker/gpu/pcp_manager.py new
+680
@@ -0,0 +1,680 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from dataclasses import dataclass, replace
4 +
5 +import numpy as np
6 +import torch
7 +
8 +from vllm.config import CUDAGraphMode, VllmConfig
9 +from vllm.distributed.parallel_state import get_dcp_group, get_pcp_group
10 +from vllm.logger import init_logger
11 +from vllm.v1.attention.backends.utils import PAD_SLOT_ID
12 +from vllm.v1.worker.gpu.block_table import BlockTables
13 +from vllm.v1.worker.gpu.buffer_utils import async_copy_to_gpu
14 +from vllm.v1.worker.gpu.cp_utils import prepare_dcp_local_seq_lens
15 +from vllm.v1.worker.gpu.input_batch import (
16 + InputBatch,
17 + InputBuffers,
18 + combine_sampled_and_draft_tokens,
19 + prepare_pos_seq_lens,
20 +)
21 +from vllm.v1.worker.gpu.states import RequestState
22 +
23 +logger = init_logger(__name__)
24 +
25 +
26 +@dataclass(frozen=True)
27 +class RankSegment:
28 + global_batch_req_idx: int
29 + global_batch_slice: slice
30 + rank_local_batch_slice: slice
31 +
32 + @property
33 + def num_tokens(self) -> int:
34 + return self.global_batch_slice.stop - self.global_batch_slice.start
35 +
36 +
37 +class PCPManager:
38 + """MRV2 PC batch manager.
39 +
40 + The model runner keeps the global scheduled batch. This manager rewrites only
41 + the per-step InputBatch into rank-local DualChunkSwap rows and keeps the
42 + global-batch view private to restore to the global batch shape before
43 + sampling/postprocess.
44 + """
45 +
46 + def __init__(
47 + self,
48 + pcp_world_size: int,
49 + pcp_rank: int,
50 + device: torch.device,
51 + req_states: RequestState | None = None,
52 + max_num_reqs: int | None = None,
53 + max_num_tokens: int | None = None,
54 + block_tables: BlockTables | None = None,
55 + dcp_world_size: int = 1,
56 + dcp_rank: int = 0,
57 + cp_interleave: int = 1,
58 + ) -> None:
59 + self.pcp_world_size = pcp_world_size
60 + self.pcp_rank = pcp_rank
61 + self.device = device
62 + self.dcp_world_size = dcp_world_size
63 + self.dcp_rank = dcp_rank
64 + self.cp_interleave = cp_interleave
65 +
66 + self._global_batch: InputBatch | None = None
67 + self._req_states = req_states
68 + self._block_tables = block_tables
69 + self._hidden_restore_idx: torch.Tensor | None = None
70 + self._padded_gather_idx: torch.Tensor | None = None
71 + self._gathered_kv_write_mask: torch.Tensor | None = None
72 + self._pad_slot_id = torch.tensor(PAD_SLOT_ID, dtype=torch.int64, device=device)
73 +
74 + max_num_local_reqs = 2 * max_num_reqs if max_num_reqs is not None else None
75 + self._input_buffers = (
76 + InputBuffers(max_num_local_reqs, max_num_tokens, device)
77 + if max_num_local_reqs is not None and max_num_tokens is not None
78 + else None
79 + )
80 + self._local_req_idx = (
81 + torch.arange(max_num_local_reqs, dtype=torch.int32, device=device)
82 + if max_num_local_reqs is not None
83 + else None
84 + )
85 + self._local_block_tables: tuple[torch.Tensor, ...] | None
86 + self._local_block_table_ptrs: torch.Tensor | None
87 + if block_tables is not None and max_num_local_reqs is not None:
88 + self._local_block_tables = tuple(
89 + table.new_zeros((max_num_local_reqs, table.shape[1]))
90 + for table in block_tables.input_block_tables
91 + )
92 + self._local_block_table_ptrs = torch.tensor(
93 + [table.data_ptr() for table in self._local_block_tables],
94 + dtype=torch.uint64,
95 + device=device,
96 + )
97 + else:
98 + self._local_block_tables = None
99 + self._local_block_table_ptrs = None
100 + num_kv_cache_groups = (
101 + block_tables.num_kv_cache_groups if block_tables is not None else 0
102 + )
103 + self._global_batch_slot_mappings = (
104 + torch.empty(
105 + num_kv_cache_groups,
106 + max_num_tokens,
107 + dtype=torch.int64,
108 + device=device,
109 + )
110 + if max_num_tokens is not None and num_kv_cache_groups > 0
111 + else None
112 + )
113 + self._gathered_kv_slot_mappings = (
114 + torch.empty(
115 + num_kv_cache_groups,
116 + max_num_tokens * pcp_world_size,
117 + dtype=torch.int64,
118 + device=device,
119 + )
120 + if max_num_tokens is not None and num_kv_cache_groups > 0
121 + else None
122 + )
123 +
124 + @staticmethod
125 + def validate_config(
126 + vllm_config: VllmConfig,
127 + supports_mm_inputs: bool,
128 + ) -> None:
129 + parallel_config = vllm_config.parallel_config
130 + model_config = vllm_config.model_config
131 + pcp_size = parallel_config.prefill_context_parallel_size
132 + if pcp_size <= 1:
133 + return
134 +
135 + if not model_config.use_mla:
136 + raise NotImplementedError("MRV2 PCP currently supports MLA models only.")
137 + if parallel_config.pipeline_parallel_size > 1:
138 + raise NotImplementedError("MRV2 PCP does not support PP yet.")
139 + if model_config.is_encoder_decoder:
140 + raise NotImplementedError(
141 + "MRV2 PCP does not support encoder-decoder models yet."
142 + )
143 + if supports_mm_inputs:
144 + raise NotImplementedError("MRV2 PCP does not support MM inputs yet.")
145 + if vllm_config.lora_config is not None:
146 + raise NotImplementedError("MRV2 PCP does not support LoRA yet.")
147 + if vllm_config.speculative_config is not None:
148 + raise NotImplementedError(
149 + "MRV2 PCP does not support speculative decoding yet."
150 + )
151 + is_sparse_mla = hasattr(model_config.hf_text_config, "index_topk")
152 + if (
153 + is_sparse_mla
154 + and vllm_config.compilation_config.cudagraph_mode != CUDAGraphMode.NONE
155 + ):
156 + raise NotImplementedError(
157 + "MRV2 sparse MLA PCP does not support CUDA graphs yet. "
158 + "Set -cc.cudagraph_mode=NONE."
159 + )
160 + if vllm_config.compilation_config.cudagraph_mode.has_full_cudagraphs():
161 + raise NotImplementedError("MRV2 PCP supports PIECEWISE CUDA graphs only.")
162 +
163 + @staticmethod
164 + def _reorder_segments(
165 + segments: list[RankSegment],
166 + num_computed_tokens: np.ndarray,
167 + is_prefilling: np.ndarray,
168 + query_start_loc_np: np.ndarray,
169 + ) -> list[RankSegment]:
170 + """Move pure prefills last to match the batch ordering expected by
171 + attention backends like MLA and sparse MLA.
172 + """
173 +
174 + def is_pure_prefill(segment: RankSegment) -> bool:
175 + req_idx = segment.global_batch_req_idx
176 + start_pos = (
177 + num_computed_tokens[req_idx]
178 + + segment.global_batch_slice.start
179 + - query_start_loc_np[req_idx]
180 + )
181 + return is_prefilling[req_idx] and start_pos == 0
182 +
183 + segments.sort(key=is_pure_prefill)
184 + rank_offset = 0
185 + for index, segment in enumerate(segments):
186 + segments[index] = replace(
187 + segment,
188 + rank_local_batch_slice=slice(
189 + rank_offset, rank_offset + segment.num_tokens
190 + ),
191 + )
192 + rank_offset += segment.num_tokens
193 + return segments
194 +
195 + def _get_rank_segments(
196 + self,
197 + rank: int,
198 + num_scheduled_tokens: np.ndarray,
199 + num_computed_tokens: np.ndarray,
200 + is_prefilling: np.ndarray,
201 + query_start_loc_np: np.ndarray,
202 + ) -> list[RankSegment]:
203 + """Build one rank's attention-compatible DualChunkSwap rows.
204 +
205 + PCP=4 partitions each prefill into eight chunks:
206 +
207 + full: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
208 + rank 0: 0 7
209 + rank 1: 1 6
210 + rank 2: 2 5
211 + rank 3: 3 4
212 + """
213 + rank_segments = []
214 + rank_offset = 0
215 + num_chunks = 2 * self.pcp_world_size
216 + for global_batch_req_idx, num_tokens in enumerate(num_scheduled_tokens):
217 + query_len = int(num_tokens)
218 + if query_len == 0:
219 + continue
220 + global_batch_start = int(query_start_loc_np[global_batch_req_idx])
221 + chunk_indices: tuple[int, ...]
222 + if bool(is_prefilling[global_batch_req_idx]):
223 + chunk_size = (query_len + num_chunks - 1) // num_chunks
224 + chunk_indices = (rank, num_chunks - 1 - rank)
225 + else: # decodes are replicated
226 + chunk_size = query_len
227 + chunk_indices = (0,)
228 +
229 + for chunk_idx in chunk_indices:
230 + chunk_offset = chunk_idx * chunk_size
231 + chunk_len = min(chunk_size, query_len - chunk_offset)
232 + if chunk_len <= 0:
233 + continue
234 + chunk_start = global_batch_start + chunk_offset
235 + rank_segments.append(
236 + RankSegment(
237 + global_batch_req_idx=global_batch_req_idx,
238 + global_batch_slice=slice(chunk_start, chunk_start + chunk_len),
239 + rank_local_batch_slice=slice(
240 + rank_offset, rank_offset + chunk_len
241 + ),
242 + )
243 + )
244 + rank_offset += chunk_len
245 + return self._reorder_segments(
246 + rank_segments,
247 + num_computed_tokens,
248 + is_prefilling,
249 + query_start_loc_np,
250 + )
251 +
252 + def _build_batch_layout(
253 + self,
254 + num_scheduled_tokens: np.ndarray,
255 + num_computed_tokens: np.ndarray,
256 + is_prefilling: np.ndarray,
257 + query_start_loc_np: np.ndarray,
258 + ) -> tuple[list[list[RankSegment]], list[int]]:
259 + segments_by_rank = []
260 + per_rank_num_tokens = []
261 + for rank in range(self.pcp_world_size):
262 + segments = self._get_rank_segments(
263 + rank,
264 + num_scheduled_tokens,
265 + num_computed_tokens,
266 + is_prefilling,
267 + query_start_loc_np,
268 + )
269 + num_rank_tokens = sum(segment.num_tokens for segment in segments)
270 + segments_by_rank.append(segments)
271 + per_rank_num_tokens.append(num_rank_tokens)
272 +
273 + # PCP=2 example:
274 + # global batch: [A B C D E F G]
275 + # rank 0 / rank 1: [A B G] / [C D E F]
276 + # padded gathered: [A B G _ | C D E F]
277 + # hidden_restore_idx: [0, 1, 4, 5, 6, 7, 2]
278 + # padded_gather_idx: [0, 1, 6, 0, 2, 3, 4, 5]
279 + # Therefore global = gathered[hidden_restore_idx] and
280 + # padded_gathered = global[padded_gather_idx].
281 + hidden_restore_idx = np.empty(int(query_start_loc_np[-1]), dtype=np.int64)
282 + padded_num_tokens = max(per_rank_num_tokens)
283 + num_expanded_tokens = padded_num_tokens * self.pcp_world_size
284 + padded_gather_idx = np.zeros(num_expanded_tokens, dtype=np.int64)
285 + gathered_kv_write_mask = np.zeros(num_expanded_tokens, dtype=np.bool_)
286 + for rank, segments in enumerate(segments_by_rank):
287 + expanded_rank_offset = rank * padded_num_tokens
288 + for segment in segments:
289 + padded_gathered_slice = slice(
290 + expanded_rank_offset + segment.rank_local_batch_slice.start,
291 + expanded_rank_offset + segment.rank_local_batch_slice.stop,
292 + )
293 + padded_gather_idx[padded_gathered_slice] = np.arange(
294 + segment.global_batch_slice.start,
295 + segment.global_batch_slice.stop,
296 + dtype=np.int64,
297 + )
298 + # Cache insertion pairs one slot entry with each rank's local decode.
299 + if not bool(is_prefilling[segment.global_batch_req_idx]) and rank != 0:
300 + continue
301 + gathered_kv_write_mask[padded_gathered_slice] = True
302 + hidden_restore_idx[segment.global_batch_slice] = np.arange(
303 + padded_gathered_slice.start,
304 + padded_gathered_slice.stop,
305 + dtype=np.int64,
306 + )
307 +
308 + self._hidden_restore_idx = async_copy_to_gpu(
309 + hidden_restore_idx, device=self.device
310 + )
311 + self._padded_gather_idx = async_copy_to_gpu(
312 + padded_gather_idx, device=self.device
313 + )
314 + self._gathered_kv_write_mask = async_copy_to_gpu(
315 + gathered_kv_write_mask, device=self.device
316 + )
317 + return segments_by_rank, per_rank_num_tokens
318 +
319 + def partition_batch(self, input_batch: InputBatch) -> InputBatch:
320 + assert self._req_states is not None
321 + assert self._input_buffers is not None
322 + req_states = self._req_states
323 + input_buffers = self._input_buffers
324 + if input_batch.num_draft_tokens > 0:
325 + raise NotImplementedError("MRV2 PCP does not support spec decode yet.")
326 +
327 + global_batch = input_batch
328 + self._global_batch = global_batch
329 +
330 + num_scheduled_tokens = global_batch.num_scheduled_tokens
331 + num_computed_tokens = global_batch.num_computed_tokens_np
332 + is_prefilling = global_batch.is_prefilling_np
333 +
334 + segments_by_rank, per_rank_num_tokens = self._build_batch_layout(
335 + num_scheduled_tokens,
336 + num_computed_tokens,
337 + is_prefilling,
338 + global_batch.query_start_loc_np,
339 + )
340 +
341 + local_segments = segments_by_rank[self.pcp_rank]
342 + if not local_segments:
343 + local_segments = [
344 + RankSegment(
345 + global_batch_req_idx=0,
346 + global_batch_slice=slice(0, 0),
347 + rank_local_batch_slice=slice(0, 0),
348 + )
349 + ]
350 +
351 + num_local_reqs = len(local_segments)
352 + if num_local_reqs > input_buffers.max_num_reqs:
353 + raise RuntimeError(
354 + "PCP local request count exceeds the MRV2 input buffer size: "
355 + f"{num_local_reqs} > {input_buffers.max_num_reqs}."
356 + )
357 +
358 + local_to_global_batch_req_idx_np = np.fromiter(
359 + (segment.global_batch_req_idx for segment in local_segments),
360 + dtype=np.int32,
361 + count=num_local_reqs,
362 + )
363 + local_start_pos_np = np.fromiter(
364 + (
365 + num_computed_tokens[segment.global_batch_req_idx]
366 + + segment.global_batch_slice.start
367 + - global_batch.query_start_loc_np[segment.global_batch_req_idx]
368 + for segment in local_segments
369 + ),
370 + dtype=np.int32,
371 + count=num_local_reqs,
372 + )
373 + local_num_scheduled_tokens = np.fromiter(
374 + (segment.num_tokens for segment in local_segments),
375 + dtype=np.int32,
376 + count=num_local_reqs,
377 + )
378 + local_to_global_req_idx_np = global_batch.idx_mapping_np[
379 + local_to_global_batch_req_idx_np
380 + ]
381 + local_req_ids = [
382 + global_batch.req_ids[global_batch_req_idx]
383 + for global_batch_req_idx in local_to_global_batch_req_idx_np
384 + ]
385 +
386 + num_local_tokens = int(local_num_scheduled_tokens.sum())
387 + num_local_tokens_padded = max(per_rank_num_tokens)
388 + fresh_prefills = int(
389 + np.count_nonzero(is_prefilling & (num_computed_tokens == 0))
390 + )
391 + continued_prefills = int(
392 + np.count_nonzero(is_prefilling & (num_computed_tokens > 0))
393 + )
394 + logger.debug(
395 + "PCP batch: rank=%d global_batch_reqs=%d fresh_prefills=%d "
396 + "continued_prefills=%d decodes=%d local_reqs=%d "
397 + "local_tokens=%d per_rank_tokens=%s",
398 + self.pcp_rank,
399 + global_batch.num_reqs,
400 + fresh_prefills,
401 + continued_prefills,
402 + global_batch.num_reqs - fresh_prefills - continued_prefills,
403 + num_local_reqs,
404 + num_local_tokens,
405 + per_rank_num_tokens,
406 + )
407 + if num_local_tokens_padded > input_buffers.max_num_tokens:
408 + raise RuntimeError(
409 + "PCP local token count exceeds the MRV2 input buffer size: "
410 + f"{num_local_tokens_padded} > {input_buffers.max_num_tokens}."
411 + )
412 + rank_token_start = self.pcp_rank * num_local_tokens_padded
413 + assert self._padded_gather_idx is not None
414 + local_gather_idx = self._padded_gather_idx[
415 + rank_token_start : rank_token_start + num_local_tokens_padded
416 + ]
417 + torch.index_select(
418 + global_batch.input_ids,
419 + 0,
420 + local_gather_idx,
421 + out=input_buffers.input_ids[:num_local_tokens_padded],
422 + )
423 +
424 + local_query_start_loc_np = np.empty(
425 + input_buffers.max_num_reqs + 1, dtype=np.int32
426 + )
427 + local_query_start_loc_np[0] = 0
428 + local_query_start_loc_out = local_query_start_loc_np[1 : num_local_reqs + 1]
429 + np.cumsum(local_num_scheduled_tokens, out=local_query_start_loc_out)
430 + local_query_start_loc_np[num_local_reqs + 1 :] = num_local_tokens
431 + async_copy_to_gpu(local_query_start_loc_np, out=input_buffers.query_start_loc)
432 + local_query_start_loc = input_buffers.query_start_loc[: num_local_reqs + 1]
433 +
434 + local_to_global_req_idx = async_copy_to_gpu(
435 + local_to_global_req_idx_np, device=self.device
436 + )
437 + local_start_pos = async_copy_to_gpu(local_start_pos_np, device=self.device)
438 +
439 + assert self._local_req_idx is not None
440 + prepare_pos_seq_lens(
441 + self._local_req_idx[:num_local_reqs],
442 + local_query_start_loc,
443 + local_start_pos,
444 + input_buffers.positions,
445 + input_buffers.seq_lens[:num_local_reqs],
446 + )
447 + seq_lens = input_buffers.seq_lens[:num_local_reqs]
448 + is_padding = input_buffers.is_padding[:num_local_tokens_padded]
449 + is_padding[:num_local_tokens].fill_(False)
450 + is_padding[num_local_tokens:].fill_(True)
451 + if num_local_tokens_padded > num_local_tokens:
452 + input_buffers.input_ids[:num_local_tokens_padded].masked_fill_(
453 + is_padding, 0
454 + )
455 + input_buffers.positions[:num_local_tokens_padded].masked_fill_(
456 + is_padding, 0
457 + )
458 +
459 + total_num_logits = num_local_reqs if num_local_tokens > 0 else 0
460 + if total_num_logits > 0:
461 + cu_num_logits_np = np.arange(num_local_reqs + 1, dtype=np.int32)
462 + cu_num_logits = torch.arange(
463 + num_local_reqs + 1, device=self.device, dtype=torch.int32
464 + )
465 + else:
466 + cu_num_logits_np = np.zeros(num_local_reqs + 1, dtype=np.int32)
467 + cu_num_logits = torch.zeros(
468 + num_local_reqs + 1, device=self.device, dtype=torch.int32
469 + )
470 + logits_indices = combine_sampled_and_draft_tokens(
471 + input_buffers.input_ids,
472 + local_to_global_req_idx,
473 + req_states.last_sampled_tokens,
474 + local_query_start_loc,
475 + seq_lens,
476 + req_states.prefill_len.gpu,
477 + req_states.draft_tokens,
478 + cu_num_logits,
479 + total_num_logits,
480 + 1,
481 + )
482 +
483 + local_prefill_len_np = global_batch.prefill_len_np[
484 + local_to_global_batch_req_idx_np
485 + ]
486 + local_num_computed_prefill_tokens_np = np.minimum(
487 + local_start_pos_np, local_prefill_len_np
488 + )
489 + local_is_prefilling_np = (
490 + local_num_computed_prefill_tokens_np < local_prefill_len_np
491 + )
492 + seq_lens_cpu_upper_bound_np = np.zeros(num_local_reqs, dtype=np.int32)
493 + seq_lens_cpu_upper_bound_np[:] = local_start_pos_np + local_num_scheduled_tokens
494 +
495 + dcp_local_seq_lens = None
496 + if self.dcp_world_size > 1:
497 + prepare_dcp_local_seq_lens(
498 + input_buffers.dcp_local_seq_lens,
499 + seq_lens,
500 + num_local_reqs,
501 + self.dcp_world_size,
502 + self.dcp_rank,
503 + self.cp_interleave,
504 + )
505 + dcp_local_seq_lens = input_buffers.dcp_local_seq_lens[:num_local_reqs]
506 +
507 + return replace(
508 + input_batch,
509 + req_ids=local_req_ids,
510 + num_reqs=num_local_reqs,
511 + num_reqs_after_padding=num_local_reqs,
512 + idx_mapping=local_to_global_req_idx,
513 + idx_mapping_np=local_to_global_req_idx_np,
514 + expanded_idx_mapping=local_to_global_req_idx,
515 + expanded_local_pos=torch.zeros(
516 + num_local_reqs, dtype=torch.int32, device=self.device
517 + ),
518 + num_scheduled_tokens=local_num_scheduled_tokens,
519 + num_tokens=num_local_tokens,
520 + num_tokens_after_padding=num_local_tokens_padded,
521 + num_draft_tokens=0,
522 + num_draft_tokens_per_req=None,
523 + query_start_loc=local_query_start_loc,
524 + query_start_loc_np=local_query_start_loc_np[: num_local_reqs + 1],
525 + seq_lens=seq_lens,
526 + seq_lens_cpu_upper_bound=torch.from_numpy(seq_lens_cpu_upper_bound_np),
527 + dcp_local_seq_lens=dcp_local_seq_lens,
528 + num_computed_tokens_np=local_start_pos_np,
529 + prefill_len_np=local_prefill_len_np,
530 + num_computed_prefill_tokens_np=local_num_computed_prefill_tokens_np,
531 + is_prefilling_np=local_is_prefilling_np,
532 + max_seq_len_np=global_batch.max_seq_len_np[local_to_global_batch_req_idx_np]
533 + if global_batch.max_seq_len_np is not None
534 + else None,
535 + input_ids=input_buffers.input_ids[:num_local_tokens_padded],
536 + positions=input_buffers.positions[:num_local_tokens_padded],
537 + is_padding=is_padding,
538 + logits_indices=logits_indices,
539 + cu_num_logits=cu_num_logits,
540 + cu_num_logits_np=cu_num_logits_np,
541 + prompt_lens=None,
542 + )
543 +
544 + def prepare_attn(
545 + self, input_batch: InputBatch
546 + ) -> tuple[tuple[torch.Tensor, ...], torch.Tensor]:
547 + assert self._block_tables is not None
548 + assert self._local_block_tables is not None
549 + assert self._local_block_table_ptrs is not None
550 + block_tables = self._block_tables.gather_block_tables(
551 + input_batch.idx_mapping,
552 + input_batch.num_reqs_after_padding,
553 + out=self._local_block_tables,
554 + out_ptrs=self._local_block_table_ptrs,
555 + )
556 + slot_mappings = self.prepare_slot_mappings()
557 + return block_tables, slot_mappings
558 +
559 + def prepare_slot_mappings(self) -> torch.Tensor:
560 + assert self._block_tables is not None
561 + assert self._global_batch_slot_mappings is not None
562 + assert self._global_batch is not None
563 + global_batch = self._global_batch
564 + global_batch_slot_mappings = self._block_tables.compute_slot_mappings(
565 + global_batch.idx_mapping,
566 + global_batch.query_start_loc,
567 + global_batch.positions,
568 + global_batch.num_tokens,
569 + out=self._global_batch_slot_mappings,
570 + )
571 + return self._convert_to_gathered_slot_mappings(global_batch_slot_mappings)
572 +
573 + def get_dummy_slot_mappings(self, num_tokens: int) -> torch.Tensor:
574 + assert self._gathered_kv_slot_mappings is not None
575 + self._gathered_kv_slot_mappings.fill_(PAD_SLOT_ID)
576 + return self._gathered_kv_slot_mappings[:, : num_tokens * self.pcp_world_size]
577 +
578 + def _convert_to_gathered_slot_mappings(
579 + self,
580 + global_batch_slot_mappings: torch.Tensor,
581 + ) -> torch.Tensor:
582 + assert self._padded_gather_idx is not None
583 + assert self._gathered_kv_write_mask is not None
584 + padded_gather_idx = self._padded_gather_idx
585 + num_expanded_tokens = padded_gather_idx.shape[0]
586 + if self._gathered_kv_slot_mappings is None:
587 + self._gathered_kv_slot_mappings = global_batch_slot_mappings.new_empty(
588 + global_batch_slot_mappings.shape[0], num_expanded_tokens
589 + )
590 + gathered_kv_slot_mappings = self._gathered_kv_slot_mappings[
591 + :, :num_expanded_tokens
592 + ]
593 + torch.index_select(
594 + global_batch_slot_mappings,
595 + 1,
596 + padded_gather_idx,
597 + out=gathered_kv_slot_mappings,
598 + )
599 + torch.where(
600 + self._gathered_kv_write_mask.unsqueeze(0),
601 + gathered_kv_slot_mappings,
602 + self._pad_slot_id,
603 + out=gathered_kv_slot_mappings,
604 + )
605 + return gathered_kv_slot_mappings
606 +
607 + def restore_hidden_states(self, hidden_states: torch.Tensor) -> torch.Tensor:
608 + if self._hidden_restore_idx is None:
609 + return hidden_states
610 + gathered = get_pcp_group().all_gather(hidden_states, dim=0)
611 + return gathered[self._hidden_restore_idx]
612 +
613 + def restore_for_sampling(
614 + self,
615 + hidden_states: torch.Tensor,
616 + ) -> tuple[torch.Tensor, InputBatch]:
617 + assert self._global_batch is not None
618 + return self.restore_hidden_states(hidden_states), self._global_batch
619 +
620 +
621 +def maybe_partition_pcp_batch(
622 + manager: PCPManager | None,
623 + input_batch: InputBatch,
624 +) -> InputBatch:
625 + if manager is None:
626 + return input_batch
627 + return manager.partition_batch(input_batch)
628 +
629 +
630 +def maybe_get_pcp_dummy_slot_mappings(
631 + manager: PCPManager | None,
632 + block_tables: BlockTables,
633 + num_tokens: int,
634 +) -> torch.Tensor:
635 + if manager is None:
636 + return block_tables.get_dummy_slot_mappings(num_tokens)
637 + return manager.get_dummy_slot_mappings(num_tokens)
638 +
639 +
640 +def maybe_restore_pcp_for_sampling(
641 + manager: PCPManager | None,
642 + hidden_states: torch.Tensor | None,
643 + input_batch: InputBatch,
644 +) -> tuple[torch.Tensor, InputBatch]:
645 + assert hidden_states is not None
646 + if manager is None:
647 + return hidden_states, input_batch
648 + return manager.restore_for_sampling(hidden_states)
649 +
650 +
651 +def maybe_build_pcp_manager(
652 + vllm_config: VllmConfig,
653 + device: torch.device,
654 + supports_mm_inputs: bool,
655 + req_states: RequestState,
656 + block_tables: BlockTables,
657 +) -> PCPManager | None:
658 + parallel_config = vllm_config.parallel_config
659 + pcp_size = parallel_config.prefill_context_parallel_size
660 + if pcp_size <= 1:
661 + return None
662 +
663 + PCPManager.validate_config(vllm_config, supports_mm_inputs)
664 +
665 + pcp_rank = get_pcp_group().rank_in_group
666 + dcp_size = parallel_config.decode_context_parallel_size
667 + dcp_rank = get_dcp_group().rank_in_group if dcp_size > 1 else 0
668 +
669 + return PCPManager(
670 + pcp_world_size=pcp_size,
671 + pcp_rank=pcp_rank,
672 + device=device,
673 + req_states=req_states,
674 + max_num_reqs=vllm_config.scheduler_config.max_num_seqs,
675 + max_num_tokens=vllm_config.scheduler_config.max_num_batched_tokens,
676 + block_tables=block_tables,
677 + dcp_world_size=dcp_size,
678 + dcp_rank=dcp_rank,
679 + cp_interleave=parallel_config.cp_kv_cache_interleave_size,
680 + )