main
py 861 lines 34.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from collections.abc import Iterator
4 from dataclasses import dataclass, replace
5 from typing import TYPE_CHECKING
6
7 import numpy as np
8 import torch
9
10 from vllm.config import CUDAGraphMode, VllmConfig
11 from vllm.distributed.parallel_state import get_dcp_group, get_pcp_group
12 from vllm.logger import init_logger
13 from vllm.utils.torch_utils import async_tensor_h2d
14 from vllm.v1.attention.backends.utils import PAD_SLOT_ID, get_dcp_local_seq_lens
15 from vllm.v1.worker.gpu.block_table import BlockTables
16 from vllm.v1.worker.gpu.input_batch import (
17 InputBatch,
18 InputBuffers,
19 )
20
21 if TYPE_CHECKING:
22 from vllm.v1.worker.gpu.cudagraph_utils import BatchExecutionDescriptor
23
24 logger = init_logger(__name__)
25
26
27 @dataclass(frozen=True)
28 class RankSegment:
29 global_batch_req_idx: int
30 global_batch_slice: slice
31 rank_local_batch_slice: slice
32
33 @property
34 def num_tokens(self) -> int:
35 return self.global_batch_slice.stop - self.global_batch_slice.start
36
37
38 class PCPManager:
39 """MRV2 PC batch manager.
40
41 The model runner keeps the global scheduled batch. This manager rewrites only
42 the per-step InputBatch into rank-local DualChunkSwap rows and keeps the
43 global-batch view private to restore to the global batch shape before
44 sampling/postprocess.
45 """
46
47 def __init__(
48 self,
49 pcp_world_size: int,
50 pcp_rank: int,
51 device: torch.device,
52 shard_decode_requests: bool,
53 max_num_reqs: int | None = None,
54 max_num_tokens: int | None = None,
55 block_tables: BlockTables | None = None,
56 dcp_world_size: int = 1,
57 dcp_rank: int = 0,
58 cp_interleave: int = 1,
59 ) -> None:
60 self.pcp_world_size = pcp_world_size
61 self.pcp_rank = pcp_rank
62 self.device = device
63 self.dcp_world_size = dcp_world_size
64 self.dcp_rank = dcp_rank
65 self.cp_interleave = cp_interleave
66 self.shard_decode_requests = shard_decode_requests
67
68 self._global_batch: InputBatch | None = None
69 self._local_batch: InputBatch | None = None
70 self._local_gather_idx: torch.Tensor | None = None
71 self.draft_prefill_batch: InputBatch | None = None
72 self._block_tables = block_tables
73 self._hidden_restore_idx: torch.Tensor | None = None
74 self._padded_gather_idx: torch.Tensor | None = None
75 self._gathered_kv_write_mask: torch.Tensor | None = None
76 self._pad_slot_id = torch.tensor(PAD_SLOT_ID, dtype=torch.int64, device=device)
77
78 max_num_local_reqs = 2 * max_num_reqs if max_num_reqs is not None else None
79 self._input_buffers = (
80 InputBuffers(max_num_local_reqs, max_num_tokens, device)
81 if max_num_local_reqs is not None and max_num_tokens is not None
82 else None
83 )
84 self._local_block_tables: tuple[torch.Tensor, ...] | None
85 self._local_block_table_ptrs: torch.Tensor | None
86 if block_tables is not None and max_num_local_reqs is not None:
87 self._local_block_tables = tuple(
88 table.new_zeros((max_num_local_reqs, table.shape[1]))
89 for table in block_tables.input_block_tables
90 )
91 self._local_block_table_ptrs = torch.tensor(
92 [table.data_ptr() for table in self._local_block_tables],
93 dtype=torch.uint64,
94 device=device,
95 )
96 else:
97 self._local_block_tables = None
98 self._local_block_table_ptrs = None
99 num_kv_cache_groups = (
100 block_tables.num_kv_cache_groups if block_tables is not None else 0
101 )
102 self._global_batch_slot_mappings = (
103 torch.empty(
104 num_kv_cache_groups,
105 max_num_tokens,
106 dtype=torch.int64,
107 device=device,
108 )
109 if max_num_tokens is not None and num_kv_cache_groups > 0
110 else None
111 )
112 self._gathered_kv_slot_mappings = (
113 torch.empty(
114 num_kv_cache_groups,
115 max_num_tokens * pcp_world_size,
116 dtype=torch.int64,
117 device=device,
118 )
119 if max_num_tokens is not None and num_kv_cache_groups > 0
120 else None
121 )
122
123 @staticmethod
124 def validate_config(
125 vllm_config: VllmConfig,
126 supports_mm_inputs: bool,
127 ) -> None:
128 parallel_config = vllm_config.parallel_config
129 model_config = vllm_config.model_config
130 pcp_size = parallel_config.prefill_context_parallel_size
131 if pcp_size <= 1:
132 return
133
134 if not model_config.use_mla:
135 raise NotImplementedError("MRV2 PCP currently supports MLA models only.")
136 if model_config.is_encoder_decoder:
137 raise NotImplementedError(
138 "MRV2 PCP does not support encoder-decoder models yet."
139 )
140 if supports_mm_inputs:
141 raise NotImplementedError("MRV2 PCP does not support MM inputs yet.")
142 if vllm_config.lora_config is not None:
143 raise NotImplementedError("MRV2 PCP does not support LoRA yet.")
144 speculative_config = vllm_config.speculative_config
145 if speculative_config is not None:
146 if speculative_config.use_dspark():
147 dcp_size = parallel_config.decode_context_parallel_size
148 if (
149 dcp_size not in (1, pcp_size)
150 and speculative_config.draft_model_config.use_mla
151 ):
152 raise NotImplementedError(
153 "MRV2 PCP DSpark requires DCP=1 or DCP=PCP; got "
154 f"DCP={dcp_size}, PCP={pcp_size}."
155 )
156 elif (
157 speculative_config.method != "mtp"
158 or speculative_config.use_multi_module_mtp()
159 ):
160 raise NotImplementedError(
161 "MRV2 PCP only supports DSpark or single-module MTP "
162 "speculative decoding."
163 )
164 cudagraph_mode = vllm_config.compilation_config.cudagraph_mode
165 is_sparse_mla = hasattr(model_config.hf_text_config, "index_topk")
166 if parallel_config.decode_context_parallel_size > 1 and not is_sparse_mla:
167 # Dense MLA prefill sizes its DCP KV gather from each rank's own
168 # chunk rows, so the ranks' collectives diverge (#53573).
169 raise NotImplementedError("MRV2 PCP + DCP supports sparse MLA models only.")
170 if (
171 is_sparse_mla
172 and parallel_config.decode_context_parallel_size == 1
173 and cudagraph_mode != CUDAGraphMode.NONE
174 ):
175 raise NotImplementedError(
176 "MRV2 sparse MLA PCP does not support CUDA graphs yet. "
177 "Set -cc.cudagraph_mode=NONE."
178 )
179 if (
180 cudagraph_mode.has_full_cudagraphs()
181 and not cudagraph_mode.separate_routine()
182 ):
183 raise NotImplementedError(
184 "MRV2 PCP supports full CUDA graphs for decode-only routines. "
185 "Use FULL_DECODE_ONLY, FULL_AND_PIECEWISE, PIECEWISE, or NONE."
186 )
187 if (
188 parallel_config.decode_context_parallel_size > 1
189 and parallel_config.dcp_comm_backend != "ag_rs"
190 ):
191 raise NotImplementedError(
192 "MRV2 PCP + DCP requires dcp_comm_backend='ag_rs'; got "
193 f"'{parallel_config.dcp_comm_backend}'."
194 )
195
196 @staticmethod
197 def _reorder_segments(
198 segments: list[RankSegment], is_prefilling: np.ndarray
199 ) -> list[RankSegment]:
200 """Order this rank's rows decodes-first, then prefills, canonically."""
201
202 def sort_key(segment: RankSegment) -> tuple[bool, int, int]:
203 req_idx = segment.global_batch_req_idx
204 return (
205 bool(is_prefilling[req_idx]),
206 req_idx,
207 segment.global_batch_slice.start,
208 )
209
210 segments.sort(key=sort_key)
211 rank_offset = 0
212 for index, segment in enumerate(segments):
213 segments[index] = replace(
214 segment,
215 rank_local_batch_slice=slice(
216 rank_offset, rank_offset + segment.num_tokens
217 ),
218 )
219 rank_offset += segment.num_tokens
220 return segments
221
222 def replicated_requests(
223 self, num_scheduled_tokens: np.ndarray, is_prefilling: np.ndarray
224 ) -> np.ndarray:
225 """Per global request, whether every PCP rank gets the whole query."""
226 num_chunks = 2 * self.pcp_world_size
227 query_lens = np.asarray(num_scheduled_tokens, dtype=np.int64)
228 replicated = ~np.asarray(is_prefilling, dtype=np.bool_)
229 if self.dcp_world_size > 1:
230 chunk_sizes = (query_lens + num_chunks - 1) // num_chunks
231 drops_a_chunk = (num_chunks - 1) * chunk_sizes >= query_lens
232 replicated |= drops_a_chunk
233 return replicated
234
235 def _iter_rank_chunks(
236 self,
237 rank: int,
238 num_scheduled_tokens: np.ndarray,
239 is_prefilling: np.ndarray,
240 ) -> Iterator[tuple[int, int, int]]:
241 """Yield ``(request index, query offset, length)`` for one PCP rank.
242
243 PCP=4 partitions each prefill into eight chunks:
244
245 full: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
246 rank 0: 0 7
247 rank 1: 1 6
248 rank 2: 2 5
249 rank 3: 3 4
250
251 Decodes, and prefills too short to fill all eight, are replicated instead.
252 """
253 decode_ordinal = 0
254 num_chunks = 2 * self.pcp_world_size
255 replicated = self.replicated_requests(num_scheduled_tokens, is_prefilling)
256 for global_batch_req_idx, num_tokens in enumerate(num_scheduled_tokens):
257 query_len = int(num_tokens)
258 if query_len == 0:
259 continue
260 chunk_indices: tuple[int, ...]
261 if not replicated[global_batch_req_idx]:
262 chunk_size = (query_len + num_chunks - 1) // num_chunks
263 chunk_indices = (rank, num_chunks - 1 - rank)
264 elif self.shard_decode_requests:
265 chunk_size = query_len
266 # KV and hidden states are gathered back to every PCP rank, so
267 # decode ownership does not need to persist across steps. Use a
268 # compact decode-only ordinal to keep each step exactly
269 # balanced even when prefills and zero-token rows are present.
270 owner_rank = decode_ordinal % self.pcp_world_size
271 decode_ordinal += 1
272 chunk_indices = (0,) if rank == owner_rank else ()
273 else: # DCP requires decode queries on every participating rank.
274 chunk_size = query_len
275 chunk_indices = (0,)
276
277 for chunk_idx in chunk_indices:
278 chunk_offset = chunk_idx * chunk_size
279 chunk_len = min(chunk_size, query_len - chunk_offset)
280 if chunk_len <= 0:
281 continue
282 yield global_batch_req_idx, chunk_offset, chunk_len
283
284 def _get_rank_segments(
285 self,
286 rank: int,
287 num_scheduled_tokens: np.ndarray,
288 is_prefilling: np.ndarray,
289 query_start_loc_np: np.ndarray,
290 ) -> list[RankSegment]:
291 rank_segments = []
292 rank_offset = 0
293 for global_batch_req_idx, chunk_offset, chunk_len in self._iter_rank_chunks(
294 rank, num_scheduled_tokens, is_prefilling
295 ):
296 global_batch_start = int(query_start_loc_np[global_batch_req_idx])
297 chunk_start = global_batch_start + chunk_offset
298 rank_segments.append(
299 RankSegment(
300 global_batch_req_idx=global_batch_req_idx,
301 global_batch_slice=slice(chunk_start, chunk_start + chunk_len),
302 rank_local_batch_slice=slice(rank_offset, rank_offset + chunk_len),
303 )
304 )
305 rank_offset += chunk_len
306 return self._reorder_segments(rank_segments, is_prefilling)
307
308 def _build_batch_layout(
309 self,
310 num_scheduled_tokens: np.ndarray,
311 num_computed_tokens: np.ndarray,
312 is_prefilling: np.ndarray,
313 query_start_loc_np: np.ndarray,
314 padded_num_tokens: int | None = None,
315 ) -> tuple[list[list[RankSegment]], list[int]]:
316 replicated = self.replicated_requests(num_scheduled_tokens, is_prefilling)
317 segments_by_rank = []
318 per_rank_num_tokens = []
319 for rank in range(self.pcp_world_size):
320 segments = self._get_rank_segments(
321 rank,
322 num_scheduled_tokens,
323 is_prefilling,
324 query_start_loc_np,
325 )
326 num_rank_tokens = sum(segment.num_tokens for segment in segments)
327 segments_by_rank.append(segments)
328 per_rank_num_tokens.append(num_rank_tokens)
329
330 # PCP=2 example:
331 # global batch: [A B C D E F G]
332 # rank 0 / rank 1: [A B G] / [C D E F]
333 # padded gathered: [A B G _ | C D E F]
334 # hidden_restore_idx: [0, 1, 4, 5, 6, 7, 2]
335 # padded_gather_idx: [0, 1, 6, 0, 2, 3, 4, 5]
336 # Therefore global = gathered[hidden_restore_idx] and
337 # padded_gathered = global[padded_gather_idx].
338 hidden_restore_idx = np.empty(int(query_start_loc_np[-1]), dtype=np.int64)
339 if padded_num_tokens is None:
340 padded_num_tokens = max(per_rank_num_tokens)
341 elif padded_num_tokens < max(per_rank_num_tokens):
342 raise ValueError(
343 "PCP padded token count is smaller than the largest rank-local "
344 f"batch: {padded_num_tokens} < {max(per_rank_num_tokens)}."
345 )
346 num_expanded_tokens = padded_num_tokens * self.pcp_world_size
347 padded_gather_idx = np.zeros(num_expanded_tokens, dtype=np.int64)
348 gathered_kv_write_mask = np.zeros(num_expanded_tokens, dtype=np.bool_)
349 for rank, segments in enumerate(segments_by_rank):
350 expanded_rank_offset = rank * padded_num_tokens
351 for segment in segments:
352 padded_gathered_slice = slice(
353 expanded_rank_offset + segment.rank_local_batch_slice.start,
354 expanded_rank_offset + segment.rank_local_batch_slice.stop,
355 )
356 padded_gather_idx[padded_gathered_slice] = np.arange(
357 segment.global_batch_slice.start,
358 segment.global_batch_slice.stop,
359 dtype=np.int64,
360 )
361 # Replicated rows contain identical KV, so rank 0 is the
362 # canonical writer. A sharded decode has exactly one owner and
363 # therefore needs no de-duplication here.
364 is_sharded_decode = (
365 not bool(is_prefilling[segment.global_batch_req_idx])
366 and self.shard_decode_requests
367 )
368 if (
369 replicated[segment.global_batch_req_idx]
370 and not is_sharded_decode
371 and rank != 0
372 ):
373 continue
374 gathered_kv_write_mask[padded_gathered_slice] = True
375 hidden_restore_idx[segment.global_batch_slice] = np.arange(
376 padded_gathered_slice.start,
377 padded_gathered_slice.stop,
378 dtype=np.int64,
379 )
380
381 self._hidden_restore_idx = async_tensor_h2d(
382 hidden_restore_idx, device=self.device
383 )
384 self._padded_gather_idx = async_tensor_h2d(
385 padded_gather_idx, device=self.device
386 )
387 self._gathered_kv_write_mask = async_tensor_h2d(
388 gathered_kv_write_mask, device=self.device
389 )
390 return segments_by_rank, per_rank_num_tokens
391
392 def get_num_tokens_for_dispatch(
393 self,
394 num_scheduled_tokens: np.ndarray,
395 is_prefilling: np.ndarray,
396 ) -> int:
397 """Return the largest real rank-local batch before graph padding."""
398 return max(
399 sum(
400 chunk_len
401 for _, _, chunk_len in self._iter_rank_chunks(
402 rank, num_scheduled_tokens, is_prefilling
403 )
404 )
405 for rank in range(self.pcp_world_size)
406 )
407
408 @staticmethod
409 def _resolve_num_reqs_after_padding(
410 input_batch: InputBatch,
411 padded_num_reqs: int | None,
412 num_local_reqs: int,
413 ) -> int:
414 if padded_num_reqs is None:
415 return num_local_reqs
416 if input_batch.has_prefill:
417 raise RuntimeError("PCP FULL graphs require a decode-only batch.")
418 assert padded_num_reqs >= num_local_reqs, (
419 "PCP graph request capacity must cover the rank-local batch: "
420 f"{padded_num_reqs} < {num_local_reqs}."
421 )
422 return padded_num_reqs
423
424 @property
425 def input_buffers(self) -> InputBuffers:
426 assert self._input_buffers is not None
427 return self._input_buffers
428
429 @property
430 def global_batch(self) -> InputBatch:
431 assert self._global_batch is not None
432 return self._global_batch
433
434 def partition_batch(
435 self, input_batch: InputBatch, batch_desc: "BatchExecutionDescriptor"
436 ) -> InputBatch:
437 assert self._input_buffers is not None
438 input_buffers = self._input_buffers
439
440 global_batch = input_batch
441 self._global_batch = global_batch
442
443 num_scheduled_tokens = global_batch.num_scheduled_tokens
444 num_computed_tokens = global_batch.num_computed_tokens_np
445 is_prefilling = global_batch.is_prefilling_np
446
447 padded_num_tokens = None
448 padded_num_reqs = None
449 if batch_desc.cg_mode != CUDAGraphMode.NONE:
450 padded_num_tokens = batch_desc.num_tokens
451 if batch_desc.cg_mode == CUDAGraphMode.FULL:
452 padded_num_reqs = batch_desc.num_reqs
453
454 segments_by_rank, per_rank_num_tokens = self._build_batch_layout(
455 num_scheduled_tokens,
456 num_computed_tokens,
457 is_prefilling,
458 global_batch.query_start_loc_np,
459 padded_num_tokens=padded_num_tokens,
460 )
461
462 local_segments = segments_by_rank[self.pcp_rank]
463 if not local_segments:
464 local_segments = [
465 RankSegment(
466 global_batch_req_idx=0,
467 global_batch_slice=slice(0, 0),
468 rank_local_batch_slice=slice(0, 0),
469 )
470 ]
471
472 num_local_reqs = len(local_segments)
473 num_reqs_after_padding = self._resolve_num_reqs_after_padding(
474 global_batch, padded_num_reqs, num_local_reqs
475 )
476 if num_reqs_after_padding > input_buffers.max_num_reqs:
477 raise RuntimeError(
478 "PCP padded local request count exceeds the MRV2 input buffer size: "
479 f"{num_reqs_after_padding} > {input_buffers.max_num_reqs}."
480 )
481
482 local_to_global_batch_req_idx_np = np.fromiter(
483 (segment.global_batch_req_idx for segment in local_segments),
484 dtype=np.intp,
485 count=num_local_reqs,
486 )
487 local_start_pos_np = np.fromiter(
488 (
489 num_computed_tokens[segment.global_batch_req_idx]
490 + segment.global_batch_slice.start
491 - global_batch.query_start_loc_np[segment.global_batch_req_idx]
492 for segment in local_segments
493 ),
494 dtype=np.int32,
495 count=num_local_reqs,
496 )
497 local_num_scheduled_tokens = np.fromiter(
498 (segment.num_tokens for segment in local_segments),
499 dtype=np.int32,
500 count=num_local_reqs,
501 )
502 local_to_global_req_idx_np = global_batch.idx_mapping_np[
503 local_to_global_batch_req_idx_np
504 ]
505 local_req_ids = [
506 global_batch.req_ids[global_batch_req_idx]
507 for global_batch_req_idx in local_to_global_batch_req_idx_np
508 ]
509
510 num_local_tokens = int(local_num_scheduled_tokens.sum())
511 num_local_tokens_padded = (
512 max(per_rank_num_tokens) if padded_num_tokens is None else padded_num_tokens
513 )
514 fresh_prefills = int(
515 np.count_nonzero(is_prefilling & (num_computed_tokens == 0))
516 )
517 continued_prefills = int(
518 np.count_nonzero(is_prefilling & (num_computed_tokens > 0))
519 )
520 logger.debug(
521 "PCP batch: rank=%d global_batch_reqs=%d fresh_prefills=%d "
522 "continued_prefills=%d decodes=%d local_reqs=%d "
523 "local_tokens=%d per_rank_tokens=%s",
524 self.pcp_rank,
525 global_batch.num_reqs,
526 fresh_prefills,
527 continued_prefills,
528 global_batch.num_reqs - fresh_prefills - continued_prefills,
529 num_local_reqs,
530 num_local_tokens,
531 per_rank_num_tokens,
532 )
533 if num_local_tokens_padded > input_buffers.max_num_tokens:
534 raise RuntimeError(
535 "PCP local token count exceeds the MRV2 input buffer size: "
536 f"{num_local_tokens_padded} > {input_buffers.max_num_tokens}."
537 )
538 rank_token_start = self.pcp_rank * num_local_tokens_padded
539 assert self._padded_gather_idx is not None
540 local_gather_idx = self._padded_gather_idx[
541 rank_token_start : rank_token_start + num_local_tokens_padded
542 ]
543 self._local_gather_idx = local_gather_idx
544 torch.index_select(
545 global_batch.input_ids,
546 0,
547 local_gather_idx,
548 out=input_buffers.input_ids[:num_local_tokens_padded],
549 )
550 # Keep the GPU request-state cursor materialized by prepare_inputs().
551 # The CPU cursor can lag after speculative rejection.
552 torch.index_select(
553 global_batch.positions,
554 0,
555 local_gather_idx,
556 out=input_buffers.positions[:num_local_tokens_padded],
557 )
558
559 local_query_start_loc_np = np.empty(
560 input_buffers.max_num_reqs + 1, dtype=np.int32
561 )
562 local_query_start_loc_np[0] = 0
563 local_query_start_loc_out = local_query_start_loc_np[1 : num_local_reqs + 1]
564 np.cumsum(local_num_scheduled_tokens, out=local_query_start_loc_out)
565 local_query_start_loc_np[num_local_reqs + 1 :] = num_local_tokens
566 async_tensor_h2d(local_query_start_loc_np, out=input_buffers.query_start_loc)
567 local_query_start_loc = input_buffers.query_start_loc[
568 : num_reqs_after_padding + 1
569 ]
570
571 local_to_global_req_idx = async_tensor_h2d(
572 local_to_global_req_idx_np, device=self.device
573 )
574 seq_lens = input_buffers.seq_lens[:num_reqs_after_padding]
575 real_seq_lens = seq_lens[:num_local_reqs]
576 if num_local_tokens > 0:
577 local_end_positions = torch.index_select(
578 input_buffers.positions,
579 0,
580 local_query_start_loc[1 : num_local_reqs + 1] - 1,
581 )
582 real_seq_lens.copy_(local_end_positions + 1)
583 else:
584 real_seq_lens.zero_()
585 seq_lens[num_local_reqs:].zero_()
586 is_padding = input_buffers.is_padding[:num_local_tokens_padded]
587 is_padding[:num_local_tokens].fill_(False)
588 is_padding[num_local_tokens:].fill_(True)
589 if num_local_tokens_padded > num_local_tokens:
590 input_buffers.input_ids[:num_local_tokens_padded].masked_fill_(
591 is_padding, 0
592 )
593 input_buffers.positions[:num_local_tokens_padded].masked_fill_(
594 is_padding, 0
595 )
596
597 total_num_logits = num_local_reqs if num_local_tokens > 0 else 0
598 if total_num_logits > 0:
599 cu_num_logits_np = np.arange(num_local_reqs + 1, dtype=np.int32)
600 cu_num_logits = torch.arange(
601 num_local_reqs + 1, device=self.device, dtype=torch.int32
602 )
603 else:
604 cu_num_logits_np = np.zeros(num_local_reqs + 1, dtype=np.int32)
605 cu_num_logits = torch.zeros(
606 num_local_reqs + 1, device=self.device, dtype=torch.int32
607 )
608 # Local logits are never sampled. The complete hidden-state tensor is
609 # restored first and sampled with the untouched global InputBatch.
610 logits_indices = local_query_start_loc[1:] - 1
611
612 local_prefill_len_np = global_batch.prefill_len_np[
613 local_to_global_batch_req_idx_np
614 ]
615 local_num_computed_prefill_tokens_np = np.minimum(
616 local_start_pos_np, local_prefill_len_np
617 )
618 real_local_is_prefilling_np = (
619 local_num_computed_prefill_tokens_np < local_prefill_len_np
620 )
621 local_is_prefilling_np = np.zeros(num_reqs_after_padding, dtype=np.bool_)
622 local_is_prefilling_np[:num_local_reqs] = real_local_is_prefilling_np
623 local_has_prefill = bool(local_is_prefilling_np.any())
624 seq_lens_cpu_upper_bound_np = np.zeros(num_reqs_after_padding, dtype=np.int32)
625 seq_lens_cpu_upper_bound_np[:num_local_reqs] = (
626 local_start_pos_np + local_num_scheduled_tokens
627 )
628 dcp_local_seq_lens_cpu_upper_bound = None
629 if self.dcp_world_size > 1:
630 # The largest DCP shard of each row's whole request, identical on
631 # every PCP rank: the sparse backends pad their KV gather to it.
632 request_seq_lens = (num_computed_tokens + num_scheduled_tokens)[
633 local_to_global_batch_req_idx_np
634 ]
635 real_dcp_local_seq_lens_cpu_upper_bound = get_dcp_local_seq_lens(
636 torch.from_numpy(request_seq_lens.astype(np.int32)),
637 self.dcp_world_size,
638 0,
639 self.cp_interleave,
640 )
641 dcp_local_seq_lens_cpu_upper_bound = torch.zeros(
642 num_reqs_after_padding, dtype=torch.int32
643 )
644 dcp_local_seq_lens_cpu_upper_bound[:num_local_reqs].copy_(
645 real_dcp_local_seq_lens_cpu_upper_bound
646 )
647
648 self._local_batch = replace(
649 input_batch,
650 req_ids=local_req_ids,
651 num_reqs=num_local_reqs,
652 num_reqs_after_padding=num_reqs_after_padding,
653 idx_mapping=local_to_global_req_idx,
654 idx_mapping_np=local_to_global_req_idx_np,
655 expanded_idx_mapping=local_to_global_req_idx,
656 expanded_local_pos=torch.zeros(
657 num_local_reqs, dtype=torch.int32, device=self.device
658 ),
659 num_scheduled_tokens=local_num_scheduled_tokens,
660 num_tokens=num_local_tokens,
661 num_tokens_after_padding=num_local_tokens_padded,
662 num_draft_tokens=0,
663 num_draft_tokens_per_req=None,
664 query_start_loc=local_query_start_loc,
665 query_start_loc_np=local_query_start_loc_np[: num_reqs_after_padding + 1],
666 seq_lens=seq_lens,
667 seq_lens_cpu_upper_bound=torch.from_numpy(seq_lens_cpu_upper_bound_np),
668 dcp_local_seq_lens=None,
669 dcp_local_seq_lens_cpu_upper_bound=dcp_local_seq_lens_cpu_upper_bound,
670 num_computed_tokens_np=local_start_pos_np,
671 prefill_len_np=local_prefill_len_np,
672 num_computed_prefill_tokens_np=local_num_computed_prefill_tokens_np,
673 is_prefilling_np=local_is_prefilling_np,
674 max_seq_len_np=None,
675 has_prefill=local_has_prefill,
676 decode_graph_eligible=not local_has_prefill,
677 prefill_runs_as_decode_np=None,
678 input_ids=input_buffers.input_ids[:num_local_tokens_padded],
679 positions=input_buffers.positions[:num_local_tokens_padded],
680 is_padding=is_padding,
681 logits_indices=logits_indices,
682 cu_num_logits=cu_num_logits,
683 cu_num_logits_np=cu_num_logits_np,
684 prompt_lens=None,
685 )
686 return self._local_batch
687
688 def prepare_inputs_to_capture(self, input_batch: InputBatch) -> InputBatch:
689 """Stage a capture or dummy batch in persistent PCP input buffers."""
690 input_buffers = self.input_buffers
691 num_reqs = input_batch.num_reqs_after_padding
692 num_tokens = input_batch.num_tokens_after_padding
693 input_batch = replace(
694 input_batch,
695 input_ids=input_buffers.input_ids[:num_tokens].copy_(input_batch.input_ids),
696 positions=input_buffers.positions[:num_tokens].copy_(input_batch.positions),
697 is_padding=input_buffers.is_padding[:num_tokens].copy_(
698 input_batch.is_padding
699 ),
700 query_start_loc=input_buffers.query_start_loc[: num_reqs + 1].copy_(
701 input_batch.query_start_loc
702 ),
703 seq_lens=input_buffers.seq_lens[:num_reqs].copy_(input_batch.seq_lens),
704 )
705 return input_batch
706
707 def get_dummy_block_tables(self, num_reqs: int) -> tuple[torch.Tensor, ...]:
708 assert self._local_block_tables is not None
709 return tuple(
710 block_table[:num_reqs].zero_() for block_table in self._local_block_tables
711 )
712
713 def prepare_attn(
714 self, input_batch: InputBatch
715 ) -> tuple[tuple[torch.Tensor, ...], torch.Tensor]:
716 assert self._block_tables is not None
717 assert self._local_block_tables is not None
718 assert self._local_block_table_ptrs is not None
719 block_tables = self._block_tables.gather_block_tables(
720 input_batch.idx_mapping,
721 input_batch.num_reqs_after_padding,
722 out=self._local_block_tables,
723 out_ptrs=self._local_block_table_ptrs,
724 )
725 slot_mappings = self.prepare_slot_mappings()
726 return block_tables, slot_mappings
727
728 def prepare_slot_mappings(self) -> torch.Tensor:
729 assert self._block_tables is not None
730 assert self._global_batch_slot_mappings is not None
731 assert self._global_batch is not None
732 global_batch = self._global_batch
733 global_batch_slot_mappings = self._block_tables.compute_slot_mappings(
734 global_batch.idx_mapping,
735 global_batch.query_start_loc,
736 global_batch.positions,
737 global_batch.num_tokens,
738 out=self._global_batch_slot_mappings,
739 )
740 return self._convert_to_gathered_slot_mappings(global_batch_slot_mappings)
741
742 def get_dummy_slot_mappings(self, num_tokens: int) -> torch.Tensor:
743 assert self._gathered_kv_slot_mappings is not None
744 self._gathered_kv_slot_mappings.fill_(PAD_SLOT_ID)
745 return self._gathered_kv_slot_mappings[:, : num_tokens * self.pcp_world_size]
746
747 def _convert_to_gathered_slot_mappings(
748 self, global_batch_slot_mappings: torch.Tensor
749 ) -> torch.Tensor:
750 assert self._padded_gather_idx is not None
751 assert self._gathered_kv_write_mask is not None
752 padded_gather_idx = self._padded_gather_idx
753 num_expanded_tokens = padded_gather_idx.shape[0]
754 if self._gathered_kv_slot_mappings is None:
755 self._gathered_kv_slot_mappings = global_batch_slot_mappings.new_empty(
756 global_batch_slot_mappings.shape[0], num_expanded_tokens
757 )
758 gathered_kv_slot_mappings = self._gathered_kv_slot_mappings[
759 :, :num_expanded_tokens
760 ]
761 torch.index_select(
762 global_batch_slot_mappings,
763 1,
764 padded_gather_idx,
765 out=gathered_kv_slot_mappings,
766 )
767 torch.where(
768 self._gathered_kv_write_mask.unsqueeze(0),
769 gathered_kv_slot_mappings,
770 self._pad_slot_id,
771 out=gathered_kv_slot_mappings,
772 )
773 return gathered_kv_slot_mappings
774
775 def restore_hidden_states(self, hidden_states: torch.Tensor) -> torch.Tensor:
776 if self._hidden_restore_idx is None:
777 return hidden_states
778 gathered = get_pcp_group().all_gather(hidden_states, dim=0)
779 return gathered[self._hidden_restore_idx]
780
781 def get_draft_input_buffers(
782 self, input_buffers: InputBuffers
783 ) -> InputBatch | InputBuffers:
784 return self.draft_prefill_batch or input_buffers
785
786 def prepare_draft_prefill(
787 self, input_batch: InputBatch, input_ids: torch.Tensor
788 ) -> None:
789 self.draft_prefill_batch = None
790 if input_batch is not self._global_batch or self._local_batch is None:
791 return
792 local_batch = self._local_batch
793 assert self._local_gather_idx is not None
794 num_local_tokens = self._local_gather_idx.shape[0]
795 torch.index_select(
796 input_ids,
797 0,
798 self._local_gather_idx,
799 out=local_batch.input_ids[:num_local_tokens],
800 )
801 self.draft_prefill_batch = local_batch
802
803 def restore_draft_prefill(
804 self,
805 last_hidden_states: torch.Tensor,
806 hidden_states: torch.Tensor,
807 ) -> tuple[torch.Tensor, torch.Tensor]:
808 if self.draft_prefill_batch is None:
809 return last_hidden_states, hidden_states
810 local_last_hidden_states = last_hidden_states
811 last_hidden_states = self.restore_hidden_states(local_last_hidden_states)
812 hidden_states = (
813 last_hidden_states
814 if local_last_hidden_states is hidden_states
815 else self.restore_hidden_states(hidden_states)
816 )
817 self.draft_prefill_batch = None
818 return last_hidden_states, hidden_states
819
820 def restore_for_sampling(
821 self, hidden_states: torch.Tensor, aux_hidden_states: list[torch.Tensor] | None
822 ) -> tuple[torch.Tensor, list[torch.Tensor] | None, InputBatch]:
823 assert self._global_batch is not None
824 hidden_states = self.restore_hidden_states(hidden_states)
825 if aux_hidden_states is not None:
826 aux_hidden_states = [
827 self.restore_hidden_states(states) for states in aux_hidden_states
828 ]
829 return hidden_states, aux_hidden_states, self._global_batch
830
831
832 def maybe_build_pcp_manager(
833 vllm_config: VllmConfig,
834 device: torch.device,
835 supports_mm_inputs: bool,
836 block_tables: BlockTables,
837 cls: type[PCPManager] = PCPManager,
838 ) -> PCPManager | None:
839 parallel_config = vllm_config.parallel_config
840 pcp_size = parallel_config.prefill_context_parallel_size
841 if pcp_size <= 1:
842 return None
843
844 cls.validate_config(vllm_config, supports_mm_inputs)
845
846 pcp_rank = get_pcp_group().rank_in_group
847 dcp_size = parallel_config.decode_context_parallel_size
848 dcp_rank = get_dcp_group().rank_in_group if dcp_size > 1 else 0
849
850 return cls(
851 pcp_world_size=pcp_size,
852 pcp_rank=pcp_rank,
853 device=device,
854 shard_decode_requests=parallel_config.pcp_shard_decode_requests,
855 max_num_reqs=vllm_config.scheduler_config.max_num_seqs,
856 max_num_tokens=vllm_config.scheduler_config.max_num_batched_tokens,
857 block_tables=block_tables,
858 dcp_world_size=dcp_size,
859 dcp_rank=dcp_rank,
860 cp_interleave=parallel_config.cp_kv_cache_interleave_size,
861 )