[PCP][Spec Decode] Adds PCP support for single-module MTP and replicated DSpark. (#56107)
Signed-off-by: Summer Yang <girasoleyang@gmail.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com> Co-authored-by: Summer Yang <girasoleyang@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: OpenAI Codex <noreply@openai.com>
Lucas Wilkinson committed
Sep 11, 2026 at 01:07 UTC
980c16c8e4c66dce0bc4d355e4ec7fa2ae7b5141
11 files changed
+159
-79
tests/v1/spec_decode/test_eagle_draft_attn_metadata.py
+1
@@ -48,6 +48,7 @@ def _make_fake_speculator(
48
)
49
return SimpleNamespace(
50
arange=torch.arange(max_num_reqs + 1, dtype=torch.int32, device="cpu"),
51
+ draft_is_prefilling=torch.zeros(max_num_reqs, dtype=torch.bool),
52
block_tables=fake_block_tables,
53
input_buffers=fake_input_buffers,
54
attn_groups=[],
tests/v1/worker/test_gpu_autoregressive_speculator.py
+1
@@ -94,6 +94,7 @@ def _make_speculator(
94
95
speculator = object.__new__(_TestSpeculator)
96
speculator.supports_mm_inputs = False
97
+ speculator.pcp_manager = None
98
speculator.vllm_config = None
99
speculator.input_buffers = SimpleNamespace(
100
input_ids=torch.arange(4),
tests/v1/worker/test_gpu_pcp_manager.py
+1
-10
@@ -11,7 +11,6 @@ from vllm.v1.worker.gpu import cp_utils as gpu_cp_utils
11
from vllm.v1.worker.gpu import pcp_manager as pcp_manager_module
12
from vllm.v1.worker.gpu.input_batch import InputBatch, InputBuffers
13
from vllm.v1.worker.gpu.pcp_manager import PCPManager
14
-from vllm.v1.worker.gpu.states import RequestState
14
15
16
def _copy_to_cpu(value, out=None, device=None):
@@ -125,6 +124,7 @@ def _make_global_decode_batch(
124
125
base = InputBatch.make_dummy(num_reqs, num_tokens, buffers)
126
buffers.seq_lens[:num_reqs] = torch.from_numpy(seq_lens_np).to(device)
127
+ buffers.positions[:num_reqs] = torch.tensor(num_computed_tokens, device=device)
128
query_start_loc_np = np.arange(num_reqs + 1, dtype=np.int32)
129
buffers.query_start_loc[: num_reqs + 1] = torch.from_numpy(query_start_loc_np).to(
130
device
@@ -166,19 +166,10 @@ def test_partition_defers_dcp_metadata_to_post_partition_batch():
166
the field afterwards from the PCP-owned buffers.
167
"""
168
device = torch.device("cuda:0")
169
- req_states = RequestState(
170
- max_num_reqs=4,
171
- max_model_len=64,
172
- max_num_batched_tokens=8,
173
- num_speculative_steps=1,
174
- vocab_size=8,
175
- device=device,
176
- )
169
manager = PCPManager(
170
pcp_world_size=2,
171
pcp_rank=0,
172
device=device,
181
- req_states=req_states,
173
max_num_reqs=4,
174
max_num_tokens=8,
175
dcp_world_size=2,
vllm/model_executor/layers/sparse_attn_indexer.py
+3
-2
@@ -401,9 +401,10 @@ def sparse_attn_indexer(
401
# out-of-bounds reads in the kernel.
402
# Keep PCP padding so every rank contributes the same all-gather shape.
403
num_tokens = slot_mapping.shape[0]
404
- if use_pcp:
405
- num_tokens //= get_pcp_group().world_size
404
if k is not None:
405
+ # MTP draft decodes are replicated and use an unexpanded slot mapping.
406
+ if use_pcp and num_tokens > k.shape[0]:
407
+ num_tokens //= get_pcp_group().world_size
408
k = k[:num_tokens]
409
410
if not skip_k_cache_insert:
vllm/v1/worker/cp_utils.py
+7
-3
@@ -18,16 +18,20 @@ else:
18
logger = init_logger(__name__)
19
20
21
-def check_attention_cp_compatibility(vllm_config: VllmConfig) -> None:
21
+def check_attention_cp_compatibility(
22
+ vllm_config: VllmConfig,
23
+ target_layer_names: set[str] | None = None,
24
+) -> None:
25
pcp_size = vllm_config.parallel_config.prefill_context_parallel_size
26
dcp_size = vllm_config.parallel_config.decode_context_parallel_size
27
interleave_size = vllm_config.parallel_config.cp_kv_cache_interleave_size
28
if pcp_size * dcp_size > 1:
29
layer_type = cast(type[Any], AttentionLayerBase)
30
layers = get_layers_from_vllm_config(vllm_config, layer_type)
28
- for layer in layers.values():
31
+ for layer_name, layer in layers.items():
32
+ check_pcp = target_layer_names is None or layer_name in target_layer_names
33
get_attn_backend = getattr(layer, "get_attn_backend", None)
30
- if pcp_size > 1 and get_attn_backend is not None:
34
+ if pcp_size > 1 and check_pcp and get_attn_backend is not None:
35
backend = get_attn_backend()
36
assert backend.supports_pcp(), (
37
"PCP requires attention backend support, "
vllm/v1/worker/gpu/model_runner.py
+12
-4
@@ -656,7 +656,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
656
self.vllm_config,
657
self.device,
658
self.supports_mm_inputs,
659
- self.req_states,
659
self.block_tables,
660
cls=self.pcp_manager_cls,
661
)
@@ -668,6 +667,8 @@ class GPUModelRunner(LoRAModelRunnerMixin):
667
self.kv_cache_config,
668
self.max_num_reqs,
669
)
670
+ if self.speculator is not None:
671
+ self.speculator.pcp_manager = self.pcp_manager
672
initialize_mamba_ssu_backend(
673
self.vllm_config.mamba_config,
674
self.kv_cache_config,
@@ -697,7 +698,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
698
self.fast_prefill = FastPrefillHelper(
699
self.cudagraph_manager, self.max_num_tokens
700
)
700
- check_attention_cp_compatibility(self.vllm_config)
701
+ check_attention_cp_compatibility(self.vllm_config, target_attn_layer_names)
702
if isinstance(self.speculator, DraftModelSpeculator):
703
# HACK(woosuk)
704
self.speculator.set_attn(
@@ -1964,9 +1965,16 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1965
return ModelRunnerOutput.with_ec_conn_output(output, ec_connector_output)
1966
1967
# Last rank: sample tokens
1968
+ draft_hidden_states = hidden_states
1969
+ assert draft_hidden_states is not None
1970
hidden_states, input_batch = pcp.maybe_restore_pcp_for_sampling(
1971
self.pcp_manager, hidden_states, input_batch
1972
)
1973
+ if self.pcp_manager is not None and aux_hidden_states is not None:
1974
+ aux_hidden_states = [
1975
+ self.pcp_manager.restore_hidden_states(states)
1976
+ for states in aux_hidden_states
1977
+ ]
1978
1979
sampler_output, num_sampled, num_rejected = self.sample(
1980
hidden_states, input_batch, grammar_output
@@ -2043,10 +2051,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
2051
# (e.g. DeepSeek V4 MTP needs the pre-hc_head residual). The
2052
# target returns a persistent buffer sized at max_num_batched_tokens;
2053
# slice to the active token count that propose() expects.
2046
- spec_hidden_states = hidden_states
2054
+ spec_hidden_states = draft_hidden_states
2055
if hasattr(self.model, "get_mtp_target_hidden_states"):
2056
pre_hc_hidden_states = self.model.get_mtp_target_hidden_states()
2049
- spec_hidden_states = pre_hc_hidden_states[: hidden_states.shape[0]] # type: ignore[union-attr]
2057
+ spec_hidden_states = pre_hc_hidden_states[: draft_hidden_states.size(0)]
2058
with use_workspace_lane(self._draft_workspace_lane):
2059
draft_tokens = self.speculator.propose(
2060
input_batch,
vllm/v1/worker/gpu/pcp_manager.py
+84
-53
@@ -6,7 +6,7 @@ from dataclasses import dataclass, replace
6
import numpy as np
7
import torch
8
9
-from vllm.config import CUDAGraphMode, VllmConfig
9
+from vllm.config import VllmConfig
10
from vllm.distributed.parallel_state import get_dcp_group, get_pcp_group
11
from vllm.logger import init_logger
12
from vllm.v1.attention.backends.utils import PAD_SLOT_ID
@@ -15,10 +15,7 @@ from vllm.v1.worker.gpu.buffer_utils import async_copy_to_gpu
15
from vllm.v1.worker.gpu.input_batch import (
16
InputBatch,
17
InputBuffers,
18
- combine_sampled_and_draft_tokens,
19
- prepare_pos_seq_lens,
18
)
21
-from vllm.v1.worker.gpu.states import RequestState
19
20
logger = init_logger(__name__)
21
@@ -48,7 +45,6 @@ class PCPManager:
45
pcp_world_size: int,
46
pcp_rank: int,
47
device: torch.device,
51
- req_states: RequestState | None = None,
48
max_num_reqs: int | None = None,
49
max_num_tokens: int | None = None,
50
block_tables: BlockTables | None = None,
@@ -64,7 +60,9 @@ class PCPManager:
60
self.cp_interleave = cp_interleave
61
62
self._global_batch: InputBatch | None = None
67
- self._req_states = req_states
63
+ self._local_batch: InputBatch | None = None
64
+ self._local_gather_idx: torch.Tensor | None = None
65
+ self.draft_prefill_batch: InputBatch | None = None
66
self._block_tables = block_tables
67
self._hidden_restore_idx: torch.Tensor | None = None
68
self._padded_gather_idx: torch.Tensor | None = None
@@ -77,11 +75,6 @@ class PCPManager:
75
if max_num_local_reqs is not None and max_num_tokens is not None
76
else None
77
)
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
- )
78
self._local_block_tables: tuple[torch.Tensor, ...] | None
79
self._local_block_table_ptrs: torch.Tensor | None
80
if block_tables is not None and max_num_local_reqs is not None:
@@ -144,19 +137,23 @@ class PCPManager:
137
raise NotImplementedError("MRV2 PCP does not support MM inputs yet.")
138
if vllm_config.lora_config is not None:
139
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
- )
140
+ speculative_config = vllm_config.speculative_config
141
+ if speculative_config is not None:
142
+ if speculative_config.use_dspark():
143
+ dcp_size = parallel_config.decode_context_parallel_size
144
+ if dcp_size not in (1, pcp_size):
145
+ raise NotImplementedError(
146
+ "MRV2 PCP DSpark requires DCP=1 or DCP=PCP; got "
147
+ f"DCP={dcp_size}, PCP={pcp_size}."
148
+ )
149
+ elif (
150
+ speculative_config.method != "mtp"
151
+ or speculative_config.use_multi_module_mtp()
152
+ ):
153
+ raise NotImplementedError(
154
+ "MRV2 PCP only supports DSpark or single-module MTP "
155
+ "speculative decoding."
156
+ )
157
if vllm_config.compilation_config.cudagraph_mode.has_full_cudagraphs():
158
raise NotImplementedError("MRV2 PCP supports PIECEWISE CUDA graphs only.")
159
@@ -358,12 +355,8 @@ class PCPManager:
355
input_batch: InputBatch,
356
padded_num_tokens: int | None = None,
357
) -> InputBatch:
361
- assert self._req_states is not None
358
assert self._input_buffers is not None
363
- req_states = self._req_states
359
input_buffers = self._input_buffers
365
- if input_batch.num_draft_tokens > 0:
366
- raise NotImplementedError("MRV2 PCP does not support spec decode yet.")
360
361
global_batch = input_batch
362
self._global_batch = global_batch
@@ -458,12 +451,21 @@ class PCPManager:
451
local_gather_idx = self._padded_gather_idx[
452
rank_token_start : rank_token_start + num_local_tokens_padded
453
]
454
+ self._local_gather_idx = local_gather_idx
455
torch.index_select(
456
global_batch.input_ids,
457
0,
458
local_gather_idx,
459
out=input_buffers.input_ids[:num_local_tokens_padded],
460
)
461
+ # Keep the GPU request-state cursor materialized by prepare_inputs().
462
+ # The CPU cursor can lag after speculative rejection.
463
+ torch.index_select(
464
+ global_batch.positions,
465
+ 0,
466
+ local_gather_idx,
467
+ out=input_buffers.positions[:num_local_tokens_padded],
468
+ )
469
470
local_query_start_loc_np = np.empty(
471
input_buffers.max_num_reqs + 1, dtype=np.int32
@@ -478,17 +480,16 @@ class PCPManager:
480
local_to_global_req_idx = async_copy_to_gpu(
481
local_to_global_req_idx_np, device=self.device
482
)
481
- local_start_pos = async_copy_to_gpu(local_start_pos_np, device=self.device)
482
-
483
- assert self._local_req_idx is not None
484
- prepare_pos_seq_lens(
485
- self._local_req_idx[:num_local_reqs],
486
- local_query_start_loc,
487
- local_start_pos,
488
- input_buffers.positions,
489
- input_buffers.seq_lens[:num_local_reqs],
490
- )
483
seq_lens = input_buffers.seq_lens[:num_local_reqs]
484
+ if num_local_tokens > 0:
485
+ local_end_positions = torch.index_select(
486
+ input_buffers.positions,
487
+ 0,
488
+ local_query_start_loc[1:] - 1,
489
+ )
490
+ seq_lens.copy_(local_end_positions + 1)
491
+ else:
492
+ seq_lens.zero_()
493
is_padding = input_buffers.is_padding[:num_local_tokens_padded]
494
is_padding[:num_local_tokens].fill_(False)
495
is_padding[num_local_tokens:].fill_(True)
@@ -511,18 +512,9 @@ class PCPManager:
512
cu_num_logits = torch.zeros(
513
num_local_reqs + 1, device=self.device, dtype=torch.int32
514
)
514
- logits_indices = combine_sampled_and_draft_tokens(
515
- input_buffers.input_ids,
516
- local_to_global_req_idx,
517
- req_states.last_sampled_tokens,
518
- local_query_start_loc,
519
- seq_lens,
520
- req_states.prefill_len.gpu,
521
- req_states.draft_tokens,
522
- cu_num_logits,
523
- total_num_logits,
524
- 1,
525
- )
515
+ # Local logits are never sampled. The complete hidden-state tensor is
516
+ # restored first and sampled with the untouched global InputBatch.
517
+ logits_indices = local_query_start_loc[1:] - 1
518
519
local_prefill_len_np = global_batch.prefill_len_np[
520
local_to_global_batch_req_idx_np
@@ -536,7 +528,7 @@ class PCPManager:
528
seq_lens_cpu_upper_bound_np = np.zeros(num_local_reqs, dtype=np.int32)
529
seq_lens_cpu_upper_bound_np[:] = local_start_pos_np + local_num_scheduled_tokens
530
539
- return replace(
531
+ self._local_batch = replace(
532
input_batch,
533
req_ids=local_req_ids,
534
num_reqs=num_local_reqs,
@@ -570,6 +562,7 @@ class PCPManager:
562
cu_num_logits_np=cu_num_logits_np,
563
prompt_lens=None,
564
)
565
+ return self._local_batch
566
567
def prepare_attn(
568
self, input_batch: InputBatch
@@ -640,6 +633,46 @@ class PCPManager:
633
gathered = get_pcp_group().all_gather(hidden_states, dim=0)
634
return gathered[self._hidden_restore_idx]
635
636
+ def get_draft_input_buffers(
637
+ self, input_buffers: InputBuffers
638
+ ) -> InputBatch | InputBuffers:
639
+ return self.draft_prefill_batch or input_buffers
640
+
641
+ def prepare_draft_prefill(
642
+ self,
643
+ input_batch: InputBatch,
644
+ input_ids: torch.Tensor,
645
+ ) -> None:
646
+ if input_batch is not self._global_batch or self._local_batch is None:
647
+ return
648
+ local_batch = self._local_batch
649
+ assert self._local_gather_idx is not None
650
+ num_local_tokens = self._local_gather_idx.shape[0]
651
+ torch.index_select(
652
+ input_ids,
653
+ 0,
654
+ self._local_gather_idx,
655
+ out=local_batch.input_ids[:num_local_tokens],
656
+ )
657
+ self.draft_prefill_batch = local_batch
658
+
659
+ def restore_draft_prefill(
660
+ self,
661
+ last_hidden_states: torch.Tensor,
662
+ hidden_states: torch.Tensor,
663
+ ) -> tuple[torch.Tensor, torch.Tensor]:
664
+ if self.draft_prefill_batch is None:
665
+ return last_hidden_states, hidden_states
666
+ local_last_hidden_states = last_hidden_states
667
+ last_hidden_states = self.restore_hidden_states(local_last_hidden_states)
668
+ hidden_states = (
669
+ last_hidden_states
670
+ if local_last_hidden_states is hidden_states
671
+ else self.restore_hidden_states(hidden_states)
672
+ )
673
+ self.draft_prefill_batch = None
674
+ return last_hidden_states, hidden_states
675
+
676
def restore_for_sampling(
677
self,
678
hidden_states: torch.Tensor,
@@ -686,7 +719,6 @@ def maybe_build_pcp_manager(
719
vllm_config: VllmConfig,
720
device: torch.device,
721
supports_mm_inputs: bool,
689
- req_states: RequestState,
722
block_tables: BlockTables,
723
cls: type[PCPManager] = PCPManager,
724
) -> PCPManager | None:
@@ -705,7 +737,6 @@ def maybe_build_pcp_manager(
737
pcp_world_size=pcp_size,
738
pcp_rank=pcp_rank,
739
device=device,
708
- req_states=req_states,
740
max_num_reqs=vllm_config.scheduler_config.max_num_seqs,
741
max_num_tokens=vllm_config.scheduler_config.max_num_batched_tokens,
742
block_tables=block_tables,
vllm/v1/worker/gpu/spec_decode/autoregressive/speculator.py
+29
-5
@@ -250,8 +250,6 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
250
)
251
else:
252
hidden_states = last_hidden_states
253
- self.hidden_states[:num_tokens_padded].copy_(hidden_states)
254
-
253
self._copy_request_inputs(
254
num_reqs,
255
input_batch.idx_mapping,
@@ -272,6 +270,17 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
270
self.max_num_reqs,
271
)
272
273
+ if self.pcp_manager is not None:
274
+ self.pcp_manager.prepare_draft_prefill(
275
+ input_batch,
276
+ self.input_buffers.input_ids[:num_tokens_padded],
277
+ )
278
+ prefill = self.pcp_manager.draft_prefill_batch
279
+ if prefill is not None:
280
+ num_tokens = prefill.num_tokens
281
+ num_tokens_padded = prefill.num_tokens_after_padding
282
+ self.hidden_states[:num_tokens_padded].copy_(hidden_states)
283
+
284
# When all requests are decoding (no true prefills), each has
285
# num_speculative_steps + 1 tokens, enabling FULL graph replay.
286
uniform_token_count = get_uniform_decode_token_count(
@@ -324,6 +333,11 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
333
# Early exit.
334
return self.draft_tokens[:num_reqs, :1]
335
336
+ if self.pcp_manager is not None and not dummy_run:
337
+ self.block_tables.gather_block_tables(
338
+ input_batch.idx_mapping, num_reqs_padded=num_reqs
339
+ )
340
+
341
# Prepare the inputs for the decode steps.
342
prepare_decode_inputs(
343
self.draft_tokens[:num_reqs, 0],
@@ -387,6 +401,11 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
401
cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
402
mm_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None,
403
) -> tuple[torch.Tensor, torch.Tensor]:
404
+ input_buffers: InputBatch | InputBuffers = self.input_buffers
405
+ is_padding = None
406
+ if self.pcp_manager is not None:
407
+ input_buffers = self.pcp_manager.get_draft_input_buffers(self.input_buffers)
408
+ is_padding = input_buffers.is_padding[:num_tokens]
409
batch_descriptor = BatchDescriptor(num_tokens=num_tokens)
410
with set_forward_context(
411
attn_metadata,
@@ -396,6 +415,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
415
num_tokens_across_dp=num_tokens_across_dp,
416
slot_mapping=slot_mappings,
417
batch_descriptor=batch_descriptor,
418
+ is_padding=is_padding,
419
):
420
inputs_embeds = None
421
if self.supports_mm_inputs:
@@ -406,15 +426,15 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
426
is_mm_embed.shape[0] if is_mm_embed is not None else num_tokens
427
)
428
self.inputs_embeds[:num_input_tokens] = self.model.embed_input_ids(
409
- self.input_buffers.input_ids[:num_input_tokens],
429
+ input_buffers.input_ids[:num_input_tokens],
430
multimodal_embeddings=mm_embeds,
431
is_multimodal=is_mm_embed,
432
)
433
inputs_embeds = self.inputs_embeds[:num_tokens]
434
435
model_inputs = dict(
416
- input_ids=self.input_buffers.input_ids[:num_tokens],
417
- positions=self.input_buffers.positions[:num_tokens],
436
+ input_ids=input_buffers.input_ids[:num_tokens],
437
+ positions=input_buffers.positions[:num_tokens],
438
hidden_states=self.hidden_states[:num_tokens],
439
inputs_embeds=inputs_embeds,
440
)
@@ -463,6 +483,10 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
483
cudagraph_runtime_mode=cudagraph_runtime_mode,
484
mm_inputs=mm_inputs,
485
)
486
+ if self.pcp_manager is not None:
487
+ last_hidden_states, hidden_states = self.pcp_manager.restore_draft_prefill(
488
+ last_hidden_states, hidden_states
489
+ )
490
491
sample_hidden_states = last_hidden_states[last_token_indices]
492
self.draft_tokens[:num_reqs, 0] = self.sample_draft(
vllm/v1/worker/gpu/spec_decode/dflash/speculator.py
+12
@@ -38,6 +38,13 @@ class DFlashSpeculator(DraftModelSpeculator):
38
_speculator_name = "DFlash" # For logging, so we can share methods with subclasses
39
40
def __init__(self, vllm_config: VllmConfig, device: torch.device):
41
+ parallel_config = vllm_config.parallel_config
42
+ if parallel_config.prefill_context_parallel_size > 1:
43
+ vllm_config = copy.copy(vllm_config)
44
+ vllm_config.parallel_config = replace(
45
+ parallel_config,
46
+ prefill_context_parallel_size=1,
47
+ )
48
super().__init__(vllm_config, device)
49
50
self.hidden_states = torch.zeros(
@@ -388,6 +395,11 @@ class DFlashSpeculator(DraftModelSpeculator):
395
)
396
return self.draft_tokens[:num_reqs]
397
398
+ if self.pcp_manager is not None and not dummy_run:
399
+ self.block_tables.gather_block_tables(
400
+ input_batch.idx_mapping, num_reqs_padded=num_reqs
401
+ )
402
+
403
# The query slot mapping is written into the shared BlockTables slot_mappings.
404
# That buffer's address is what the captured CUDA graph reads from at replay.
405
assert self.draft_kv_cache_group_id >= 0
vllm/v1/worker/gpu/spec_decode/mtp/speculator.py
+2
-1
@@ -28,7 +28,8 @@ class MTPSpeculator(AutoRegressiveSpeculator):
28
# toggles skip_topk so step 0 computes MTP's own indices and
29
# steps 1+ reuse them.
30
self.share_mtp_topk_indices = (
31
- getattr(draft_hf_config, "index_share_for_mtp_iteration", False)
31
+ self.vllm_config.parallel_config.prefill_context_parallel_size == 1
32
+ and getattr(draft_hf_config, "index_share_for_mtp_iteration", False)
33
and hasattr(draft_model.model, "set_skip_topk")
34
and hasattr(draft_model.model, "compact_topk_indices")
35
)
vllm/v1/worker/gpu/spec_decode/speculator.py
+7
-1
@@ -3,7 +3,7 @@
3
from abc import ABC, abstractmethod
4
from collections.abc import Mapping
5
from dataclasses import replace
6
-from typing import Any
6
+from typing import TYPE_CHECKING, Any
7
8
import numpy as np
9
import torch
@@ -29,6 +29,9 @@ from vllm.v1.worker.gpu.model_states.interface import ModelState
29
from vllm.v1.worker.gpu.sample.gumbel import gumbel_sample
30
from vllm.v1.worker.utils import AttentionGroup
31
32
+if TYPE_CHECKING:
33
+ from vllm.v1.worker.gpu.pcp_manager import PCPManager
34
+
35
logger = init_logger(__name__)
36
37
@@ -149,6 +152,7 @@ class DraftModelSpeculator(BaseSpeculator):
152
self.arange = torch.arange(
153
self.max_num_reqs + 1, dtype=torch.int32, device="cpu"
154
)
155
+ self.draft_is_prefilling = torch.zeros(self.max_num_reqs, dtype=torch.bool)
156
157
self.draft_logits: torch.Tensor | None = None
158
if self.speculative_config.draft_sample_method == "probabilistic":
@@ -166,6 +170,7 @@ class DraftModelSpeculator(BaseSpeculator):
170
)
171
172
self.supports_mm_inputs = False
173
+ self.pcp_manager: PCPManager | None = None
174
175
@abstractmethod
176
def load_draft_model(
@@ -326,6 +331,7 @@ class DraftModelSpeculator(BaseSpeculator):
331
kv_cache_config=self.kv_cache_config,
332
causal=causal,
333
seq_lens_cpu_upper_bound=draft_seq_lens_cpu_upper_bound,
334
+ is_prefilling=self.draft_is_prefilling[:num_reqs],
335
)
336
return attn_metadata
337