[Model Runner V2][Spec Decode] Support spec decode with draft model (#43091)

Signed-off-by: Icey <1790571317@qq.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Xiaoshuang Wang committed Sep 29, 2026 at 03:38 UTC ef42093a36b7d7cc74ecfc0533099e01f2aa8286
6 files changed +552 -4
vllm/config/vllm.py
-2
@@ -3067,8 +3067,6 @@ class VllmConfig:
3067 # https://github.com/vllm-project/vllm/pull/40704
3068 "ngram",
3069 "ngram_gpu",
3070 - # https://github.com/vllm-project/vllm/pull/43091
3071 - "draft_model",
3070 "suffix",
3071 "medusa",
3072 "mlp_speculator",
vllm/v1/worker/gpu/model_runner.py
+12 -1
@@ -799,7 +799,11 @@ class GPUModelRunner(LoRAModelRunnerMixin):
799 "skip_attn must only be True for initial memory profiling."
800 )
801
802 - # Create a dummy scheduler output.
802 + # Create a dummy scheduler output. Plain draft-model speculation adds
803 + # one correction slot per request during prefill. The scheduler
804 + # accounts for these slots, while dummy runs bypass the scheduler.
805 + # Adjust the dummy token count only when the expanded draft batch
806 + # would exceed the token budget.
807 num_reqs = min(num_tokens, self.max_num_reqs)
808 if uniform_decode:
809 # HACK(lucas): for now since the worker is shared between MRV1 and MRV2,
@@ -813,8 +817,15 @@ class GPUModelRunner(LoRAModelRunnerMixin):
817 num_reqs = min(
818 num_reqs, self.max_num_tokens // self.speculator.num_query_per_req
819 )
820 + num_tokens = min(
821 + num_tokens,
822 + self.max_num_tokens
823 + - num_reqs * self.speculator.num_extra_query_per_req,
824 + )
825 +
826 # Distribute the remainder evenly so no dummy request exceeds
827 # ceil(num_tokens / num_reqs) <= max_model_len tokens.
828 +
829 num_tokens_per_request = [
830 num_tokens // num_reqs + (i >= num_reqs - num_tokens % num_reqs)
831 for i in range(num_reqs)
vllm/v1/worker/gpu/spec_decode/__init__.py
+6
@@ -60,5 +60,11 @@ def init_speculator(vllm_config: VllmConfig, device: torch.device):
60 )
61
62 return EagleSpeculator(vllm_config, device)
63 + elif speculative_config.uses_draft_model():
64 + from vllm.v1.worker.gpu.spec_decode.draft_model.speculator import (
65 + PlainDraftModelSpeculator,
66 + )
67 +
68 + return PlainDraftModelSpeculator(vllm_config, device)
69 else:
70 raise NotImplementedError(f"{speculative_config.method} is not supported yet.")
vllm/v1/worker/gpu/spec_decode/draft_model/__init__.py new
+2
@@ -0,0 +1,2 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/v1/worker/gpu/spec_decode/draft_model/speculator.py new
+527
@@ -0,0 +1,527 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from typing import Any
4 +
5 +import torch
6 +import torch.nn as nn
7 +
8 +from vllm.compilation.backends import set_model_tag
9 +from vllm.config import VllmConfig, replace
10 +from vllm.config.compilation import CUDAGraphMode
11 +from vllm.forward_context import BatchDescriptor, set_forward_context
12 +from vllm.logger import init_logger
13 +from vllm.model_executor.model_loader import get_model
14 +from vllm.triton_utils import tl, triton
15 +from vllm.v1.kv_cache_interface import KVCacheConfig
16 +from vllm.v1.spec_decode.utils import next_power_of_2
17 +from vllm.v1.worker.gpu.attn_utils import build_slot_mappings_by_layer
18 +from vllm.v1.worker.gpu.block_table import BlockTables
19 +from vllm.v1.worker.gpu.cudagraph_utils import BatchExecutionDescriptor
20 +from vllm.v1.worker.gpu.dp_utils import DPSyncState
21 +from vllm.v1.worker.gpu.input_batch import InputBatch, InputBuffers
22 +from vllm.v1.worker.gpu.model_states.interface import ModelState
23 +from vllm.v1.worker.gpu.spec_decode.speculator import DraftModelSpeculator
24 +from vllm.v1.worker.utils import AttentionGroup
25 +
26 +logger = init_logger(__name__)
27 +
28 +
29 +class PlainDraftModelSpeculator(DraftModelSpeculator):
30 + """Speculative decoding using a separate smaller draft LM.
31 +
32 + Unlike Eagle, the draft model runs fully independently of the target model.
33 + Step 0 builds an expanded buffer (accepted + correction token + rejected
34 + slots) via a Triton kernel; steps 1..k-1 are single-token decode steps.
35 + """
36 +
37 + # Plain draft model needs one extra slot per request for correction.
38 + num_extra_query_per_req = 1
39 +
40 + def __init__(self, vllm_config: VllmConfig, device: torch.device):
41 + super().__init__(vllm_config, device)
42 +
43 + # draft_max_seq_len is read by the parent's attention metadata builder.
44 + # Plain draft model doesn't do per-batch adjustment; cap at max.
45 + self.draft_max_seq_len = self.max_model_len
46 +
47 + self.last_token_indices = torch.zeros(
48 + self.max_num_reqs, dtype=torch.int64, device=device
49 + )
50 + # GPU-side arange for decode query_start_loc initialisation.
51 + self.arange_gpu = torch.arange(
52 + self.max_num_reqs + 1, dtype=torch.int32, device=device
53 + )
54 + # Scalar step counter used as column index into draft_logits.
55 + self.current_draft_step = torch.tensor(0, dtype=torch.int64, device=device)
56 +
57 + _expanded_max = self.max_num_tokens + self.max_num_reqs
58 + self.expanded_input_ids = torch.zeros(
59 + _expanded_max, dtype=torch.int32, device=device
60 + )
61 + self.expanded_positions = torch.zeros(
62 + _expanded_max, dtype=torch.int64, device=device
63 + )
64 + self.expanded_slot_mappings: torch.Tensor
65 + self.supports_mm_inputs = False
66 +
67 + def init_cudagraph_manager(self, cudagraph_mode: CUDAGraphMode) -> None:
68 + pass # CUDA graph not yet supported for plain draft model speculator.
69 +
70 + def capture(self) -> None:
71 + pass # CUDA graph not yet supported for plain draft model speculator.
72 +
73 + def set_attn(
74 + self,
75 + model_state: ModelState,
76 + kv_cache_config: KVCacheConfig,
77 + block_tables: BlockTables,
78 + target_input_buffers: InputBuffers,
79 + target_attn_groups: list[list[AttentionGroup]],
80 + ) -> None:
81 + super().set_attn(
82 + model_state,
83 + kv_cache_config,
84 + block_tables,
85 + target_input_buffers,
86 + target_attn_groups,
87 + )
88 + self.expanded_slot_mappings = torch.empty(
89 + block_tables.num_kv_cache_groups,
90 + self.max_num_tokens + self.max_num_reqs,
91 + dtype=torch.int64,
92 + device=self.device,
93 + )
94 +
95 + def load_draft_model(
96 + self,
97 + target_model: nn.Module,
98 + target_attn_layer_names: set[str],
99 + ) -> nn.Module:
100 + spec = self.speculative_config
101 + assert spec is not None
102 + draft_vllm_config = replace(
103 + self.vllm_config,
104 + model_config=self.draft_model_config,
105 + quant_config=None,
106 + parallel_config=replace(
107 + spec.draft_parallel_config,
108 + rank=self.vllm_config.parallel_config.rank,
109 + ),
110 + )
111 + with set_model_tag("draft_model"):
112 + return get_model(
113 + vllm_config=draft_vllm_config,
114 + prefix="draft_model",
115 + )
116 +
117 + @torch.inference_mode()
118 + def _run_model(
119 + self,
120 + input_ids: torch.Tensor,
121 + positions: torch.Tensor,
122 + attn_metadata: dict[str, Any] | None,
123 + slot_mappings: dict[str, torch.Tensor] | None,
124 + num_tokens_across_dp: torch.Tensor | None,
125 + ) -> torch.Tensor:
126 + num_tokens = input_ids.shape[0]
127 + batch_descriptor = BatchDescriptor(num_tokens=num_tokens)
128 + with set_forward_context(
129 + attn_metadata,
130 + self.vllm_config,
131 + num_tokens=num_tokens,
132 + cudagraph_runtime_mode=CUDAGraphMode.NONE,
133 + num_tokens_across_dp=num_tokens_across_dp,
134 + slot_mapping=slot_mappings,
135 + batch_descriptor=batch_descriptor,
136 + ):
137 + hidden_states = self.model( # type: ignore[misc]
138 + input_ids=input_ids,
139 + positions=positions,
140 + )
141 + if isinstance(hidden_states, tuple):
142 + hidden_states = hidden_states[0]
143 + return hidden_states
144 +
145 + def _accepted_last_indices(
146 + self,
147 + input_batch: InputBatch,
148 + num_rejected: torch.Tensor,
149 + num_reqs: int,
150 + ) -> torch.Tensor:
151 + qsl = input_batch.query_start_loc
152 + adjusted_lens = qsl[1 : num_reqs + 1] - qsl[:num_reqs] - num_rejected[:num_reqs]
153 + return qsl[:num_reqs] + adjusted_lens - 1
154 +
155 + def _prefill(
156 + self,
157 + input_batch: InputBatch,
158 + num_rejected: torch.Tensor,
159 + last_sampled: torch.Tensor,
160 + num_reqs: int,
161 + skip_attn: bool,
162 + num_tokens_across_dp: torch.Tensor | None,
163 + ) -> None:
164 + if skip_attn:
165 + src_tokens = input_batch.num_tokens
166 + self.last_token_indices[:num_reqs] = self._accepted_last_indices(
167 + input_batch, num_rejected, num_reqs
168 + )
169 + hidden_states = self._run_model(
170 + input_batch.input_ids[:src_tokens],
171 + input_batch.positions[:src_tokens],
172 + None,
173 + None,
174 + num_tokens_across_dp,
175 + )
176 + positions_buf = input_batch.positions
177 + else:
178 + block_tables = self.block_tables
179 + kv_cache_config = self.kv_cache_config
180 + assert block_tables is not None
181 + assert kv_cache_config is not None
182 +
183 + total_expanded = prepare_prefill_inputs(
184 + input_buffers=self.input_buffers,
185 + input_batch=input_batch,
186 + last_sampled=last_sampled,
187 + num_rejected=num_rejected,
188 + expanded_input_ids=self.expanded_input_ids,
189 + expanded_positions=self.expanded_positions,
190 + last_token_indices=self.last_token_indices,
191 + max_num_reqs=self.max_num_reqs,
192 + max_model_len=self.max_model_len,
193 + )
194 +
195 + prefill_slot_mappings = block_tables.compute_slot_mappings(
196 + input_batch.idx_mapping,
197 + self.input_buffers.query_start_loc[: num_reqs + 1],
198 + self.expanded_positions[:total_expanded],
199 + total_expanded,
200 + out=self.expanded_slot_mappings,
201 + )
202 + qsl_np = input_batch.query_start_loc_np
203 + query_start_loc_cpu_expanded = (
204 + torch.from_numpy(qsl_np[: num_reqs + 1]).int()
205 + + torch.from_numpy(self.arange_np[: num_reqs + 1]).int()
206 + )
207 + prefill_attn_md = self._build_attn_metadata(
208 + batch_desc=BatchExecutionDescriptor(
209 + cg_mode=CUDAGraphMode.NONE,
210 + num_tokens=total_expanded,
211 + num_reqs=num_reqs,
212 + ),
213 + num_reqs=num_reqs,
214 + query_start_loc_np=query_start_loc_cpu_expanded.numpy(),
215 + seq_lens_cpu_upper_bound=input_batch.seq_lens_cpu_upper_bound,
216 + step=0,
217 + slot_mappings=prefill_slot_mappings,
218 + )
219 + prefill_slot_maps_by_layer = build_slot_mappings_by_layer(
220 + prefill_slot_mappings, kv_cache_config
221 + )
222 + hidden_states = self._run_model(
223 + self.expanded_input_ids[:total_expanded],
224 + self.expanded_positions[:total_expanded],
225 + prefill_attn_md,
226 + prefill_slot_maps_by_layer,
227 + num_tokens_across_dp,
228 + )
229 + positions_buf = self.expanded_positions
230 +
231 + last_indices = self.last_token_indices[:num_reqs]
232 + self.current_draft_step.fill_(0)
233 + self.draft_tokens[:num_reqs, 0] = self.sample_draft(
234 + hidden_states[last_indices],
235 + positions_buf[last_indices],
236 + self.idx_mapping[:num_reqs],
237 + self.temperature,
238 + self.seeds,
239 + self.current_draft_step,
240 + self.draft_logits,
241 + )
242 +
243 + def _multi_step_decode(
244 + self,
245 + input_batch: InputBatch,
246 + num_rejected: torch.Tensor,
247 + num_reqs: int,
248 + skip_attn: bool,
249 + num_tokens_across_dp: torch.Tensor | None,
250 + ) -> None:
251 + if skip_attn:
252 + last_positions = input_batch.positions[
253 + self._accepted_last_indices(input_batch, num_rejected, num_reqs)
254 + ]
255 + else:
256 + last_positions = self.expanded_positions[self.last_token_indices[:num_reqs]]
257 +
258 + self.input_buffers.positions[:num_reqs].copy_(last_positions)
259 + # The decode loop increments seq_lens BEFORE each forward.
260 + # Initial seq_lens = target_seq_lens - num_rejected + 1.
261 + # Step 1 forward uses target_seq_lens - num_rejected + 2.
262 + # Step 2 forward uses target_seq_lens - num_rejected + 3.
263 + self.input_buffers.seq_lens[:num_reqs].copy_(
264 + torch.clamp(
265 + input_batch.seq_lens[:num_reqs] - num_rejected[:num_reqs].int() + 1,
266 + max=self.max_model_len,
267 + )
268 + )
269 + self.input_buffers.query_start_loc[: num_reqs + 1].copy_(
270 + self.arange_gpu[: num_reqs + 1]
271 + )
272 +
273 + idx_mapping = input_batch.idx_mapping
274 +
275 + for step in range(1, self.num_speculative_steps):
276 + self.input_buffers.input_ids[:num_reqs].copy_(
277 + self.draft_tokens[:num_reqs, step - 1].int()
278 + )
279 + torch.clamp(
280 + self.input_buffers.positions[:num_reqs] + 1,
281 + max=self.max_model_len - 1,
282 + out=self.input_buffers.positions[:num_reqs],
283 + )
284 + # Increment seq_lens BEFORE the forward so the attention reads from
285 + # the KV slot written by the previous step.
286 + torch.clamp(
287 + self.input_buffers.seq_lens[:num_reqs] + 1,
288 + max=self.max_model_len,
289 + out=self.input_buffers.seq_lens[:num_reqs],
290 + )
291 +
292 + decode_attn_md = None
293 + step_slot_maps_by_layer = None
294 + if not skip_attn:
295 + block_tables = self.block_tables
296 + kv_cache_config = self.kv_cache_config
297 + assert block_tables is not None
298 + assert kv_cache_config is not None
299 + q_start = self.input_buffers.query_start_loc[: num_reqs + 1]
300 + positions = self.input_buffers.positions[:num_reqs]
301 + step_slot_mappings = block_tables.compute_slot_mappings(
302 + idx_mapping, q_start, positions, num_reqs
303 + )
304 + step_slot_maps_by_layer = build_slot_mappings_by_layer(
305 + step_slot_mappings, kv_cache_config
306 + )
307 + decode_attn_md = self._build_uniform_attn_metadata(
308 + batch_desc=BatchExecutionDescriptor(
309 + cg_mode=CUDAGraphMode.NONE,
310 + num_tokens=num_reqs,
311 + num_reqs=num_reqs,
312 + uniform_token_count=1,
313 + ),
314 + num_reqs=num_reqs,
315 + num_query_per_req=1,
316 + seq_lens_cpu_upper_bound=input_batch.seq_lens_cpu_upper_bound,
317 + # Include the correction token inserted during prefill.
318 + step=step + 1,
319 + )
320 + hidden_states = self._run_model(
321 + self.input_buffers.input_ids[:num_reqs],
322 + self.input_buffers.positions[:num_reqs],
323 + decode_attn_md,
324 + step_slot_maps_by_layer,
325 + num_tokens_across_dp,
326 + )
327 +
328 + self.current_draft_step.fill_(step)
329 + self.draft_tokens[:num_reqs, step] = self.sample_draft(
330 + hidden_states[:num_reqs],
331 + self.input_buffers.positions[:num_reqs],
332 + self.idx_mapping[:num_reqs],
333 + self.temperature,
334 + self.seeds,
335 + self.current_draft_step,
336 + self.draft_logits,
337 + )
338 +
339 + @torch.inference_mode()
340 + def propose(
341 + self,
342 + input_batch: InputBatch,
343 + attn_metadata: dict[str, Any],
344 + slot_mappings: dict[str, torch.Tensor],
345 + last_hidden_states: torch.Tensor, # unused
346 + aux_hidden_states: list[torch.Tensor] | None, # unused
347 + num_sampled: torch.Tensor,
348 + num_rejected: torch.Tensor,
349 + last_sampled: torch.Tensor,
350 + next_prefill_tokens: torch.Tensor,
351 + temperature: torch.Tensor,
352 + seeds: torch.Tensor,
353 + dp_sync: DPSyncState | None = None,
354 + dummy_run: bool = False,
355 + skip_attn_for_dummy_run: bool = False,
356 + mm_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None,
357 + is_profile: bool = False,
358 + ) -> torch.Tensor:
359 + assert self.model is not None
360 +
361 + num_reqs = input_batch.num_reqs
362 + skip_attn = dummy_run and skip_attn_for_dummy_run
363 + num_tokens_across_dp = (
364 + dp_sync.num_tokens_across_dp if dp_sync is not None else None
365 + )
366 +
367 + # Copy per-request temperature/seeds/idx_mapping into pre-allocated
368 + # buffers so sample_draft can read them without extra slicing.
369 + self._copy_request_inputs(
370 + num_reqs,
371 + input_batch.idx_mapping,
372 + temperature,
373 + seeds,
374 + )
375 +
376 + self._prefill(
377 + input_batch,
378 + num_rejected,
379 + last_sampled,
380 + num_reqs,
381 + skip_attn,
382 + num_tokens_across_dp,
383 + )
384 +
385 + if self.num_speculative_steps == 1:
386 + return self.draft_tokens[:num_reqs, :1]
387 +
388 + self._multi_step_decode(
389 + input_batch,
390 + num_rejected,
391 + num_reqs,
392 + skip_attn,
393 + num_tokens_across_dp,
394 + )
395 +
396 + return self.draft_tokens[:num_reqs]
397 +
398 +
399 +@triton.jit
400 +def _prepare_prefill_inputs_kernel(
401 + target_input_ids_ptr, # [src_tokens] int32
402 + target_positions_ptr, # [src_tokens] int64
403 + last_sampled_ptr, # [max_num_reqs] int32
404 + idx_mapping_ptr, # [num_reqs] int32
405 + out_input_ids_ptr, # [src_tokens + num_reqs] int32
406 + out_positions_ptr, # [src_tokens + num_reqs] int64
407 + last_token_indices_ptr, # [max_num_reqs] int64
408 + out_query_start_loc_ptr, # [max_num_reqs + 1] int32
409 + out_seq_lens_ptr, # [max_num_reqs] int32
410 + query_start_loc_ptr, # [num_reqs + 1] int32
411 + seq_lens_ptr, # [num_reqs] int32
412 + num_rejected_ptr, # [num_reqs] int32
413 + max_num_reqs,
414 + max_model_len,
415 + BLOCK_SIZE: tl.constexpr,
416 +):
417 + """Per-request step-0 input preparation for the plain draft-model speculator.
418 +
419 + Output layout for request i (out_start = query_start_loc[i] + i):
420 + [out_start, out_start + num_valid) accepted tokens
421 + [out_start + num_valid] correction token
422 + (out_start + num_valid, out_start + total_out) rejected slots
423 +
424 + where num_valid = query_lens[i] - num_rejected[i]
425 + total_out = query_lens[i] + 1
426 + """
427 + req_idx = tl.program_id(0)
428 + num_reqs = tl.num_programs(0)
429 +
430 + req_state_idx = tl.load(idx_mapping_ptr + req_idx)
431 +
432 + q_start = tl.load(query_start_loc_ptr + req_idx)
433 + q_next = tl.load(query_start_loc_ptr + req_idx + 1)
434 + seq_len = tl.load(seq_lens_ptr + req_idx)
435 + num_rejected = tl.load(num_rejected_ptr + req_idx)
436 +
437 + num_valid = q_next - q_start - num_rejected
438 + correction_token = tl.load(last_sampled_ptr + req_state_idx).to(tl.int32)
439 + start_pos = tl.load(target_positions_ptr + q_start)
440 + out_start = q_start + req_idx
441 + total_out = q_next - q_start + 1
442 +
443 + for i in range(0, total_out, BLOCK_SIZE):
444 + j = i + tl.arange(0, BLOCK_SIZE)
445 + in_bounds = j < total_out
446 +
447 + is_valid = j < num_valid
448 + is_correction = j == num_valid
449 + src_idx = q_start + j
450 + token_ids = tl.load(target_input_ids_ptr + src_idx, mask=is_valid, other=0)
451 + positions = tl.minimum(start_pos + j, max_model_len - 1)
452 + token_ids = tl.where(is_correction, correction_token, token_ids)
453 +
454 + out_idx = out_start + j
455 + tl.store(out_input_ids_ptr + out_idx, token_ids, mask=in_bounds)
456 + tl.store(out_positions_ptr + out_idx, positions, mask=in_bounds)
457 +
458 + tl.store(last_token_indices_ptr + req_idx, out_start + num_valid)
459 + tl.store(out_query_start_loc_ptr + req_idx, out_start)
460 + # seqlen_k for the expanded prefill attention:
461 + # pre_existing = seq_len - query_len (draft KV before this round)
462 + # expanded_query_len = query_len + 1 (adds correction token)
463 + # seqlen_k = pre_existing + expanded_query_len = seq_len + 1
464 + # num_rejected does NOT change seqlen_k — the expanded query always has
465 + # query_len+1 slots. Rejected positions write temporary KV entries that
466 + # are overwritten with valid tokens in a later round.
467 + new_seq_len = tl.minimum(seq_len + 1, max_model_len)
468 + tl.store(out_seq_lens_ptr + req_idx, new_seq_len)
469 +
470 + if req_idx == num_reqs - 1:
471 + total_expanded = out_start + total_out
472 + tl.store(out_query_start_loc_ptr + num_reqs, total_expanded)
473 + for i in range(num_reqs + 1, max_num_reqs + 1, BLOCK_SIZE):
474 + block = i + tl.arange(0, BLOCK_SIZE)
475 + mask = block <= max_num_reqs
476 + tl.store(out_query_start_loc_ptr + block, total_expanded, mask=mask)
477 + for i in range(num_reqs, max_num_reqs, BLOCK_SIZE):
478 + block = i + tl.arange(0, BLOCK_SIZE)
479 + mask = block < max_num_reqs
480 + tl.store(out_seq_lens_ptr + block, 0, mask=mask)
481 + tl.store(last_token_indices_ptr + block, 0, mask=mask)
482 +
483 +
484 +def prepare_prefill_inputs(
485 + input_buffers: InputBuffers,
486 + input_batch: InputBatch,
487 + last_sampled: torch.Tensor, # [max_num_reqs] int64
488 + num_rejected: torch.Tensor, # [num_reqs] int64
489 + expanded_input_ids: torch.Tensor, # [max_tokens + max_reqs] int32
490 + expanded_positions: torch.Tensor, # [max_tokens + max_reqs] int64
491 + last_token_indices: torch.Tensor, # [max_num_reqs] int64
492 + max_num_reqs: int,
493 + max_model_len: int,
494 +) -> int:
495 + """Call _prepare_prefill_inputs_kernel and return total_expanded tokens.
496 +
497 + Side-effects (kernel writes):
498 + - expanded_input_ids, expanded_positions
499 + - last_token_indices
500 + - input_buffers.query_start_loc (expanded: original[i] + i)
501 + - input_buffers.seq_lens (= target_seq_lens - num_rejected + 1)
502 + """
503 + num_reqs = input_batch.num_reqs
504 + src_tokens = input_batch.num_tokens
505 + qsl_np = input_batch.query_start_loc_np
506 + query_lens = qsl_np[1 : num_reqs + 1] - qsl_np[:num_reqs]
507 + max_total_out = int(query_lens.max()) + 2 # +1 correction, +1 alignment
508 + BLOCK_SIZE = min(512, next_power_of_2(max_total_out))
509 +
510 + _prepare_prefill_inputs_kernel[(num_reqs,)](
511 + input_batch.input_ids,
512 + input_batch.positions,
513 + last_sampled.int(),
514 + input_batch.idx_mapping,
515 + expanded_input_ids,
516 + expanded_positions,
517 + last_token_indices,
518 + input_buffers.query_start_loc,
519 + input_buffers.seq_lens,
520 + input_batch.query_start_loc,
521 + input_batch.seq_lens,
522 + num_rejected.int(),
523 + max_num_reqs,
524 + max_model_len,
525 + BLOCK_SIZE=BLOCK_SIZE,
526 + )
527 + return src_tokens + num_reqs
vllm/v1/worker/gpu/spec_decode/speculator.py
+5 -1
@@ -58,6 +58,8 @@ def _target_feeds_hc_residual(vllm_config: VllmConfig) -> bool:
58
59 class BaseSpeculator(ABC):
60 num_query_per_req: int = 1
61 + # Extra query slots reserved per request outside the regular queries.
62 + num_extra_query_per_req: int = 0
63
64 @abstractmethod
65 def init_cudagraph_manager(self, cudagraph_mode: CUDAGraphMode) -> None:
@@ -302,6 +304,7 @@ class DraftModelSpeculator(BaseSpeculator):
304 step: int,
305 causal: bool | Mapping[int, bool] = True,
306 dcp_local_seq_lens: torch.Tensor | None = None,
307 + slot_mappings: torch.Tensor | None = None,
308 ) -> dict[str, Any] | None:
309 num_reqs_padded = batch_desc.num_reqs or num_reqs
310 # A FULL graph replays a captured shape whose padded requests each hold
@@ -323,7 +326,8 @@ class DraftModelSpeculator(BaseSpeculator):
326 block_tables = [
327 x[:num_reqs_padded] for x in self.block_tables.input_block_tables
328 ]
326 - slot_mappings = self.block_tables.slot_mappings[:, :num_tokens]
329 + if slot_mappings is None:
330 + slot_mappings = self.block_tables.slot_mappings[:, :num_tokens]
331 draft_seq_lens_cpu_upper_bound = torch.zeros(
332 num_reqs_padded, dtype=torch.int32, device="cpu"
333 )