main
py 1,873 lines 82.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import dataclasses
4 from importlib.util import find_spec
5 from typing import TYPE_CHECKING, Any, cast
6
7 import numpy as np
8 import torch
9 import torch.nn as nn
10
11 from vllm.compilation.breakable_cudagraph import BreakableCUDAGraphWrapper
12 from vllm.config import (
13 CUDAGraphMode,
14 VllmConfig,
15 get_layers_from_vllm_config,
16 replace,
17 )
18
19 if TYPE_CHECKING:
20 from vllm.v1.spec_decode.vocab_mapping import VocabMapping
21
22 from vllm.distributed.eplb.eplb_state import EplbState
23 from vllm.distributed.parallel_state import get_pp_group
24 from vllm.forward_context import set_forward_context
25 from vllm.logger import init_logger
26 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
27 from vllm.model_executor.model_loader import get_model
28 from vllm.model_executor.model_loader.utils import get_draft_load_config
29 from vllm.model_executor.models import (
30 supports_multimodal,
31 supports_multimodal_embeddings,
32 )
33 from vllm.model_executor.models.deepseek_eagle3 import Eagle3DeepseekV2ForCausalLM
34 from vllm.model_executor.models.interfaces import SupportsMultiModal
35 from vllm.model_executor.models.laguna_dflash import DFlashLagunaForCausalLM
36 from vllm.model_executor.models.llama_eagle3 import Eagle3LlamaForCausalLM
37 from vllm.model_executor.models.qwen3_dflash import DFlashQwen3ForCausalLM
38 from vllm.model_executor.models.qwen3_eagle3 import Eagle3Qwen3ForCausalLM
39 from vllm.platforms import current_platform
40 from vllm.utils.torch_utils import PIN_MEMORY, async_tensor_h2d
41 from vllm.v1.attention.backend import CommonAttentionMetadata
42 from vllm.v1.attention.backends.registry import AttentionBackendEnum
43 from vllm.v1.attention.backends.triton_attn import TritonAttentionMetadata
44 from vllm.v1.cudagraph_dispatcher import CudagraphDispatcher
45 from vllm.v1.kv_cache_interface import KVCacheConfig, UniformTypeKVCacheSpecs
46 from vllm.v1.sample.metadata import SamplingMetadata
47 from vllm.v1.sample.ops.topk_topp_sampler import (
48 empty_exponential_noise_like,
49 sample_with_exponential_noise,
50 )
51 from vllm.v1.sample.sampler import _SAMPLING_EPS
52 from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
53 from vllm.v1.spec_decode.utils import (
54 PADDING_SLOT_ID,
55 _copy_and_expand_eagle_inputs,
56 _eagle_prepare_inputs_padded,
57 _eagle_prepare_next_token_padded,
58 _eagle_step_slot_mapping_metadata,
59 compute_new_slot_mapping,
60 eagle_step_update_slot_mapping_and_metadata,
61 extend_all_queries_by_N,
62 next_power_of_2,
63 )
64 from vllm.v1.utils import CpuGpuBuffer
65 from vllm.v1.worker.dp_utils import coordinate_batch_across_dp
66 from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
67 from vllm.v1.worker.utils import AttentionGroup
68
69 logger = init_logger(__name__)
70
71
72 class SpecDecodeBaseProposer:
73 def __init__(
74 self,
75 vllm_config: VllmConfig,
76 device: torch.device,
77 pass_hidden_states_to_model: bool,
78 runner=None,
79 ):
80 self.vllm_config = vllm_config
81 assert vllm_config.speculative_config is not None
82 self.speculative_config = vllm_config.speculative_config
83 self.draft_model_config = self.speculative_config.draft_model_config
84 self.method = self.speculative_config.method
85 self.pass_hidden_states_to_model = pass_hidden_states_to_model
86 self._share_mtp_indices = False
87
88 self.device = device
89 self.dtype = vllm_config.model_config.dtype
90 self.max_model_len = vllm_config.model_config.max_model_len
91 self.dp_rank = vllm_config.parallel_config.data_parallel_rank
92 self.eplb_state: EplbState | None = None
93 self.num_speculative_tokens = self.speculative_config.num_speculative_tokens
94
95 # We need to get the hidden size from the draft model config because
96 # the draft model's hidden size can be different from the target model's
97 # hidden size (e.g., Llama 3.3 70B).
98 self.hidden_size = self.draft_model_config.get_hidden_size()
99 self.inputs_embeds_size = self.draft_model_config.get_inputs_embeds_size()
100
101 # DeepSeek V4 MTP consumes the target's pre-hc_head residual stream,
102 # shape (T, hc_mult * hidden_size). Expand the hidden_states buffer
103 # so target_hidden_states fits; detect DeepseekV4 via draft hf_config.
104 draft_hf_config = self.draft_model_config.hf_config
105 if hasattr(draft_hf_config, "compress_ratios") and hasattr(
106 draft_hf_config, "hc_mult"
107 ):
108 self.hidden_size = self.hidden_size * draft_hf_config.hc_mult
109
110 # Unifying eagle, draft model, and parallel drafting support.
111 # DFlash always uses parallel drafting (all tokens in one pass),
112 # but has an additional slot for the next_token_id (does not shift like EAGLE)
113 self.parallel_drafting: bool = self.speculative_config.parallel_drafting
114 self.extra_slots_per_request = (
115 1 if not self.parallel_drafting else self.num_speculative_tokens
116 )
117 self.net_num_new_slots_per_request = self.extra_slots_per_request - (
118 1 if (self.pass_hidden_states_to_model and self.method != "dflash") else 0
119 )
120 self.needs_extra_input_slots = self.net_num_new_slots_per_request > 0
121
122 # When True, all draft steps reuse the same position as the
123 # first step instead of advancing by one each iteration.
124 # Used by draft models with Q-only attention that share KV
125 # with the target and always predict from the same position.
126 self.constant_draft_positions: bool = False
127
128 self.parallel_drafting_token_id: int = 0
129 self.parallel_drafting_hidden_state_tensor: torch.Tensor | None = None
130 if self.parallel_drafting:
131 self._init_parallel_drafting_params()
132 self.use_local_argmax_reduction: bool = (
133 self.speculative_config.use_local_argmax_reduction
134 )
135 self.use_fp64_gumbel = vllm_config.model_config.use_fp64_gumbel
136
137 self.use_heterogeneous_vocab: bool = (
138 self.speculative_config.use_heterogeneous_vocab
139 )
140 self.vocab_mapping: VocabMapping | None = None
141
142 self.max_batch_size = vllm_config.scheduler_config.max_num_seqs
143 self.max_num_tokens = vllm_config.scheduler_config.max_num_batched_tokens
144 _eagle_step_slot_mapping_metadata.register_warmup(
145 max_model_len=self.max_model_len,
146 max_batch_size=self.max_batch_size,
147 )
148 _eagle_prepare_inputs_padded.register_warmup(max_batch_size=self.max_batch_size)
149 _eagle_prepare_next_token_padded.register_warmup(
150 vocab_size=vllm_config.model_config.get_vocab_size(),
151 num_sampled_tokens_per_req=self.num_speculative_tokens + 1,
152 max_batch_size=self.max_batch_size,
153 )
154 if self.needs_extra_input_slots:
155 _copy_and_expand_eagle_inputs.register_warmup(
156 parallel_drafting_token_id=self.parallel_drafting_token_id,
157 num_padding_slots_per_request=self.extra_slots_per_request,
158 shift_input_ids=self.pass_hidden_states_to_model,
159 max_num_tokens=self.max_num_tokens,
160 )
161 self.token_arange_np = np.arange(self.max_num_tokens, dtype=np.int32)
162
163 # Can be specialized by methods like DFlash to reduce the limit
164 self.max_query_tokens = self.max_num_tokens
165 self.max_positions = self.max_num_tokens
166
167 # Multi-modal data support
168 self.supports_mm_inputs = vllm_config.model_config.supports_multimodal_inputs
169
170 self.draft_attn_groups: list[AttentionGroup] = []
171 self.kv_cache_gid: int = -1
172 self.eagle3_use_aux_hidden_state: bool = (
173 self._get_eagle3_use_aux_hidden_state_from_config()
174 )
175
176 self.compilation_config = self.vllm_config.compilation_config
177
178 # Cudagraph dispatcher for PIECEWISE-only dispatching in eagle.
179 # Keys are initialized later via initialize_cudagraph_keys() called from
180 # gpu_model_runner._check_and_update_cudagraph_mode after
181 # adjust_cudagraph_sizes_for_spec_decode is called.
182 self.cudagraph_dispatcher = CudagraphDispatcher(self.vllm_config)
183
184 # persistent buffers for cuda graph
185 self.input_ids = torch.zeros(
186 self.max_num_tokens, dtype=torch.int32, device=device
187 )
188 # Use draft model's M-RoPE setting, not target model's
189 # Draft models may be text-only even if target is multimodal
190 self.uses_mrope = self.draft_model_config.uses_mrope
191 if self.uses_mrope:
192 # NOTE: `mrope_positions` is implemented with one additional dummy
193 # position on purpose to make it non-contiguous so that it can work
194 # with torch compile.
195 # See detailed explanation in https://github.com/vllm-project/vllm/pull/12128#discussion_r1926431923
196
197 # NOTE: For text-only inputs, each dimension has identical position
198 # IDs, making M-RoPE functionally equivalent to 1D-RoPE.
199 # See page 5 of https://arxiv.org/abs/2409.12191
200 self.mrope_positions = torch.zeros(
201 (self.draft_model_config.mrope_num_dims, self.max_positions + 1),
202 dtype=torch.int64,
203 device=device,
204 )
205 else:
206 # RoPE need (max_num_tokens,)
207 self.positions = torch.zeros(
208 self.max_positions,
209 dtype=torch.int64,
210 device=device,
211 )
212 self.hidden_states = torch.zeros(
213 (self.max_num_tokens, self.hidden_size), dtype=self.dtype, device=device
214 )
215
216 # Will be set when we initialize the attention backend
217 self.block_size: int = -1
218
219 # We need +1 here because the arange is used to set query_start_loc,
220 # which has one more element than batch_size.
221 max_num_slots_for_arange = max(self.max_batch_size + 1, self.max_num_tokens)
222 self.arange = torch.arange(
223 max_num_slots_for_arange, device=device, dtype=torch.int32
224 )
225
226 if self.needs_extra_input_slots:
227 self._raise_if_padded_drafter_batch_disabled()
228 self._warn_if_multimodal()
229 self._raise_if_mrope()
230
231 self.is_rejected_token_mask: torch.Tensor | None = None
232 self.is_masked_token_mask: torch.Tensor | None = None
233 if self.needs_extra_input_slots:
234 # For draft models and parallel drafting, we need to keep track of
235 # which tokens are rejected to update the slot mapping with padding slots.
236 self.is_rejected_token_mask = torch.zeros(
237 (self.max_num_tokens,), dtype=torch.bool, device=device
238 )
239 # For parallel drafting, we also need to keep track of which tokens
240 # are parallel-padding tokens used to sample at later positions.
241 # We populate this tensor even when using draft models for simplicity.
242 self.is_masked_token_mask = torch.zeros(
243 (self.max_num_tokens,), dtype=torch.bool, device=device
244 )
245
246 self.inputs_embeds = torch.zeros(
247 (self.max_num_tokens, self.inputs_embeds_size),
248 dtype=self.dtype,
249 device=device,
250 )
251
252 self.backup_next_token_ids = CpuGpuBuffer(
253 self.max_batch_size,
254 dtype=torch.int32,
255 device=device,
256 with_numpy=True,
257 )
258 self._enable_probabilistic_draft_probs = (
259 self.speculative_config.rejection_sample_method == "standard"
260 and self.speculative_config.draft_sample_method == "probabilistic"
261 )
262 self._last_draft_probs: torch.Tensor | None = None
263
264 self._slot_mapping_buffer = torch.zeros(
265 self.max_positions, dtype=torch.int64, device=device
266 )
267
268 # Determine allowed attention backends once during initialization.
269 self.allowed_attn_types: tuple | None = None
270 if current_platform.is_rocm():
271 from vllm.models.deepseek_v4.amd.rocm import (
272 DeepseekV4ROCMAiterMLASparseMetadata,
273 DeepseekV4ROCMAiterSparseSWAMetadata,
274 )
275
276 # MiniMax-M3 sparse (lightning-indexer) attention. The multi-step
277 # drafting machinery is shared code at num_speculative_tokens>1.
278 # this just opts the metadata into the ROCm allowlist.
279 from vllm.models.minimax_m3.common.sparse_attention import (
280 MiniMaxM3SparseMetadata,
281 )
282 from vllm.v1.attention.backends.mla.indexer import (
283 DeepseekV32IndexerMetadata,
284 )
285 from vllm.v1.attention.backends.mla.rocm_aiter_mla_sparse import (
286 ROCMAiterMLASparseMetadata,
287 )
288 from vllm.v1.attention.backends.rocm_attn import RocmAttentionMetadata
289
290 rocm_types = [
291 TritonAttentionMetadata,
292 RocmAttentionMetadata,
293 ROCMAiterMLASparseMetadata,
294 DeepseekV4ROCMAiterMLASparseMetadata,
295 DeepseekV4ROCMAiterSparseSWAMetadata,
296 DeepseekV32IndexerMetadata,
297 MiniMaxM3SparseMetadata,
298 ]
299 # ROCM_AITER_FA is an optional backend
300 # We check is_enabled() here to avoid importing the backend module during
301 # auto-discovery when VLLM_ROCM_USE_AITER=0, which would trigger aiter
302 # import and JIT compilation warnings. Explicit backend selection via
303 # attention_config still works because the backend module is loaded
304 # directly when selected, not through this auto-discovery path.
305 # Check if backend module exists to allow explicit selection
306 if find_spec(
307 AttentionBackendEnum.ROCM_AITER_FA.get_path(include_classname=False)
308 ):
309 from vllm.v1.attention.backends.rocm_aiter_fa import (
310 AiterFlashAttentionMetadata,
311 )
312
313 rocm_types.append(AiterFlashAttentionMetadata)
314
315 # TRITON_MLA backend support for MLA models (e.g., DeepSeek)
316 from vllm.model_executor.layers.attention.mla_attention import (
317 MLACommonMetadata,
318 )
319
320 rocm_types.append(MLACommonMetadata)
321
322 # FlexAttention backend support
323 from vllm.v1.attention.backends.flex_attention import FlexAttentionMetadata
324
325 rocm_types.append(FlexAttentionMetadata)
326
327 self.allowed_attn_types = tuple(rocm_types)
328
329 def _raise_if_padded_drafter_batch_disabled(self):
330 if self.speculative_config.disable_padded_drafter_batch:
331 raise NotImplementedError(
332 "Speculative Decoding with draft models or parallel drafting only "
333 "supports padded drafter batch. Please unset "
334 "disable_padded_drafter_batch in the speculative_config."
335 )
336
337 def _warn_if_multimodal(self):
338 if self.supports_mm_inputs:
339 logger.warning(
340 "Speculative Decoding with draft models or parallel drafting "
341 "does not fully support multimodal models yet. "
342 "Proceeding with text-only speculative decoding."
343 )
344
345 def _raise_if_mrope(self):
346 if self.draft_model_config.uses_mrope:
347 raise NotImplementedError(
348 "Speculative Decoding with draft models or parallel drafting "
349 "does not support M-RoPE yet"
350 )
351
352 def set_eplb_state(self, eplb_state: EplbState) -> None:
353 """Inject EPLB state after construction."""
354 self.eplb_state = eplb_state
355
356 def _init_parallel_drafting_params(self):
357 # For parallel drafting, we need the token ID to use for masked slots
358 # And for EAGLE + parallel drafting, we need the hidden state tensor to use
359 # for those masked slots.
360
361 model_hf_config = self.draft_model_config.hf_config
362 # DFlash stores mask_token_id in dflash_config
363 dflash_config = getattr(model_hf_config, "dflash_config", None)
364 if dflash_config and "mask_token_id" in dflash_config:
365 self.parallel_drafting_token_id = dflash_config["mask_token_id"]
366 elif getattr(model_hf_config, "mask_token_id", None) is not None:
367 self.parallel_drafting_token_id = model_hf_config.mask_token_id
368 elif hasattr(model_hf_config, "dspark_noise_token_id"):
369 self.parallel_drafting_token_id = model_hf_config.dspark_noise_token_id
370 elif hasattr(model_hf_config, "pard_token"):
371 self.parallel_drafting_token_id = model_hf_config.pard_token
372 elif hasattr(model_hf_config, "ptd_token_id"):
373 self.parallel_drafting_token_id = model_hf_config.ptd_token_id
374 else:
375 raise ValueError(
376 "For parallel drafting, the draft model config must have "
377 "`dflash_config.mask_token_id`, `mask_token_id`, "
378 "`dspark_noise_token_id`, `pard_token`, or `ptd_token_id` "
379 "specified in its config.json."
380 )
381
382 if self.pass_hidden_states_to_model:
383 self.parallel_drafting_hidden_state_tensor = torch.empty(
384 self.hidden_size, dtype=self.dtype, device=self.device
385 )
386
387 def _get_positions(self, num_tokens: int):
388 if self.uses_mrope:
389 return self.mrope_positions[:, :num_tokens]
390 return self.positions[:num_tokens]
391
392 def _set_positions(self, num_tokens: int, positions: torch.Tensor):
393 if self.uses_mrope:
394 self.mrope_positions[:, :num_tokens] = positions
395 else:
396 # Convert M-RoPE positions if target model uses M-RoPE
397 # but draft doesn't, For text inputs, all M-RoPE
398 # dimensions are identical
399 if self.vllm_config.model_config.uses_mrope:
400 positions = positions[0]
401 self.positions[:num_tokens] = positions
402
403 def _get_slot_mapping(
404 self,
405 num_tokens: int,
406 slot_mapping: torch.Tensor | None = None,
407 ) -> dict[str, torch.Tensor]:
408 """Return slot_mapping dict for EAGLE layers.
409
410 If slot_mapping is provided, copies it into the buffer first.
411 """
412 if slot_mapping is not None:
413 num_actual = slot_mapping.shape[0]
414 self._slot_mapping_buffer[:num_actual].copy_(slot_mapping)
415 if num_tokens > num_actual:
416 self._slot_mapping_buffer[num_actual:num_tokens].fill_(PADDING_SLOT_ID)
417
418 view = self._slot_mapping_buffer[:num_tokens]
419 return {name: view for name in self._draft_attn_layer_names}
420
421 def initialize_cudagraph_keys(self, cudagraph_mode: CUDAGraphMode) -> None:
422 """Initialize cudagraph dispatcher keys for the drafter.
423
424 Only supports PIECEWISE cudagraphs (via mixed_mode).
425 This should be called after adjust_cudagraph_sizes_for_spec_decode.
426 """
427 if (
428 not self.speculative_config.enforce_eager
429 and cudagraph_mode.mixed_mode()
430 in [CUDAGraphMode.PIECEWISE, CUDAGraphMode.FULL]
431 ):
432 eagle_cudagraph_mode = CUDAGraphMode.PIECEWISE
433 else:
434 eagle_cudagraph_mode = CUDAGraphMode.NONE
435
436 self.cudagraph_dispatcher.initialize_cudagraph_keys(eagle_cudagraph_mode)
437
438 def _greedy_sample(self, hidden_states: torch.Tensor) -> torch.Tensor:
439 """Greedy-sample draft tokens from hidden states."""
440 if self.use_local_argmax_reduction:
441 return self.model.get_top_tokens(hidden_states)
442 if self.use_heterogeneous_vocab:
443 logits = self.model.compute_logits(hidden_states)
444 assert self.vocab_mapping is not None
445 logits = self.vocab_mapping.constrain_draft_logits(logits)
446 draft_token_ids = logits.argmax(dim=-1)
447 return self.vocab_mapping.map_draft_to_target_ids(draft_token_ids)
448 return self.model.compute_logits(hidden_states).argmax(dim=-1)
449
450 def _sample_from_logits(
451 self,
452 logits: torch.Tensor,
453 sampling_metadata: SamplingMetadata,
454 ) -> tuple[torch.Tensor, torch.Tensor | None]:
455 if not self._enable_probabilistic_draft_probs:
456 return logits.argmax(dim=-1), None
457 if sampling_metadata.all_greedy:
458 return logits.argmax(dim=-1), None
459
460 # Parallel drafting (e.g. DFlash) samples num_speculative_tokens rows
461 # per request in a single pass, so logits has batch_size * K rows while
462 # the sampling metadata is per-request. The rows are request-major
463 # (K consecutive slots per request), so repeat_interleave the
464 # per-request temperature to match before probabilistic sampling.
465 temperature = sampling_metadata.temperature
466 if temperature is not None and temperature.shape[0] != logits.shape[0]:
467 assert logits.shape[0] % temperature.shape[0] == 0
468 factor = logits.shape[0] // temperature.shape[0]
469 sampling_metadata = dataclasses.replace(
470 sampling_metadata,
471 temperature=temperature.repeat_interleave(factor, dim=0),
472 )
473
474 return compute_probs_and_sample_next_token(
475 logits, sampling_metadata, self.use_fp64_gumbel
476 )
477
478 def _sample_draft_tokens(
479 self,
480 hidden_states: torch.Tensor,
481 sampling_metadata: SamplingMetadata,
482 ) -> tuple[torch.Tensor, torch.Tensor | None]:
483 if not self._enable_probabilistic_draft_probs or sampling_metadata.all_greedy:
484 return self._greedy_sample(hidden_states), None
485 logits = self.model.compute_logits(hidden_states)
486 if self.use_heterogeneous_vocab:
487 assert self.vocab_mapping is not None
488 logits = self.vocab_mapping.constrain_draft_logits(logits)
489 draft_token_ids, draft_probs = self._sample_from_logits(
490 logits, sampling_metadata
491 )
492 if self.use_heterogeneous_vocab:
493 assert self.vocab_mapping is not None
494 draft_token_ids = self.vocab_mapping.map_draft_to_target_ids(
495 draft_token_ids
496 )
497 # Config validation ensures draft_sample_method == "greedy" when
498 # use_heterogeneous_vocab is True, so this branch should never be
499 # reached. Kept as a safety fallback until probabilistic rejection
500 # sampling with heterogeneous vocabularies is implemented.
501 # TODO: remap draft_probs to target-vocab space for lossless
502 # probabilistic rejection sampling with heterogeneous vocabularies.
503 assert draft_probs is None, (
504 "probabilistic draft sampling is not supported with "
505 "use_heterogeneous_vocab"
506 )
507 return draft_token_ids, draft_probs
508
509 def take_last_draft_probs(self) -> torch.Tensor | None:
510 return self._last_draft_probs
511
512 def propose(
513 self,
514 num_speculative_tokens,
515 # [num_tokens]
516 target_token_ids: torch.Tensor,
517 # [num_tokens] or [3, num_tokens] when M-RoPE is enabled
518 target_positions: torch.Tensor,
519 # [num_tokens, hidden_size]
520 target_hidden_states: torch.Tensor,
521 # [batch_size]
522 next_token_ids: torch.Tensor,
523 token_indices_to_sample: torch.Tensor | None,
524 common_attn_metadata: CommonAttentionMetadata,
525 sampling_metadata: SamplingMetadata,
526 mm_embed_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None,
527 num_rejected_tokens_gpu: torch.Tensor | None = None,
528 slot_mappings: dict[str, torch.Tensor]
529 | list[dict[str, torch.Tensor]]
530 | None = None,
531 ) -> torch.Tensor:
532 self.num_speculative_tokens = num_speculative_tokens
533 self._last_draft_probs = None
534 batch_size = common_attn_metadata.batch_size()
535
536 if self.method in ("eagle3", "dflash"):
537 model = self.model
538 if isinstance(model, BreakableCUDAGraphWrapper):
539 model = model.unwrap()
540 assert isinstance(
541 model,
542 (
543 Eagle3LlamaForCausalLM,
544 Eagle3DeepseekV2ForCausalLM,
545 DFlashQwen3ForCausalLM,
546 Eagle3Qwen3ForCausalLM,
547 DFlashLagunaForCausalLM,
548 ),
549 )
550 target_hidden_states = self.model.combine_hidden_states(
551 target_hidden_states
552 )
553 assert target_hidden_states.shape[-1] == self.hidden_size
554
555 num_tokens, token_indices_to_sample, common_attn_metadata = (
556 self.set_inputs_first_pass(
557 target_token_ids=target_token_ids,
558 next_token_ids=next_token_ids,
559 target_positions=target_positions,
560 target_hidden_states=target_hidden_states,
561 token_indices_to_sample=token_indices_to_sample,
562 cad=common_attn_metadata,
563 num_rejected_tokens_gpu=num_rejected_tokens_gpu,
564 )
565 )
566
567 per_group_attn_metadata, per_layer_attn_metadata = (
568 self.build_per_group_and_layer_attn_metadata(common_attn_metadata)
569 )
570
571 cudagraph_runtime_mode, num_input_tokens, num_tokens_across_dp = (
572 self._determine_batch_execution_and_padding(num_tokens)
573 )
574
575 model_kwargs, slot_mapping_size = self.build_model_inputs_first_pass(
576 num_tokens, num_input_tokens, mm_embed_inputs
577 )
578 # Step 0 of index_share_for_mtp_iteration: let the MTP layer
579 # compute its own indices (skip_topk=False) so subsequent steps
580 # can reuse them.
581 if self._share_mtp_indices and hasattr(self.model.model, "set_skip_topk"):
582 self.model.model.set_skip_topk(False)
583
584 if self.eplb_state is not None:
585 self.eplb_state.prepare_forward(
586 self.draft_model_config,
587 num_tokens,
588 )
589
590 with set_forward_context(
591 per_layer_attn_metadata,
592 self.vllm_config,
593 num_tokens=num_input_tokens,
594 num_tokens_across_dp=num_tokens_across_dp,
595 cudagraph_runtime_mode=cudagraph_runtime_mode,
596 slot_mapping=self._get_slot_mapping(
597 slot_mapping_size, common_attn_metadata.slot_mapping
598 ),
599 ):
600 ret_hidden_states = self.model(**model_kwargs)
601 if not self.model_returns_tuple():
602 last_hidden_states = ret_hidden_states
603 hidden_states = last_hidden_states
604 else:
605 last_hidden_states, hidden_states = ret_hidden_states
606
607 # After step 0: switch to reuse mode so steps 1+ skip the indexer
608 # and read the indices that step 0 just wrote into the shared buffer.
609 if self._share_mtp_indices and hasattr(self.model.model, "set_skip_topk"):
610 self.model.model.set_skip_topk(True)
611 # The topk indices were written for each query token in the multi-token
612 # batch. Compact the topk indices for each request's last token.
613 self.model.model.compact_topk_indices(token_indices_to_sample)
614
615 sample_hidden_states = last_hidden_states[token_indices_to_sample]
616
617 # No draft tokens requested (e.g. Dynamic SD decided K=0).
618 # The prefill forward pass above already ran to keep the drafter
619 # KV cache in sync, so just return an empty tensor.
620 if self.num_speculative_tokens == 0:
621 return torch.empty(
622 batch_size,
623 0,
624 device=sample_hidden_states.device,
625 dtype=torch.int64,
626 )
627
628 # Early exit if there is only one draft token to be generated.
629 if self.num_speculative_tokens == 1 or self.parallel_drafting:
630 draft_token_ids, draft_probs = self._sample_draft_tokens(
631 sample_hidden_states, sampling_metadata
632 )
633 if draft_probs is not None:
634 self._last_draft_probs = draft_probs.view(
635 -1, self.num_speculative_tokens, draft_probs.shape[-1]
636 ).contiguous()
637 return draft_token_ids.view(-1, self.num_speculative_tokens)
638
639 if self.uses_mrope:
640 positions = self.mrope_positions[:, token_indices_to_sample]
641 else:
642 positions = self.positions[token_indices_to_sample]
643 hidden_states = hidden_states[token_indices_to_sample]
644
645 if self.constant_draft_positions:
646 # Write the sampling positions into the front of the
647 # positions buffer so that subsequent loop iterations
648 # (which read via _get_positions) use the correct values.
649 self.positions[:batch_size] = positions
650
651 draft_token_ids, draft_probs = self._sample_draft_tokens(
652 sample_hidden_states, sampling_metadata
653 )
654 draft_probs_list = None if draft_probs is None else [draft_probs]
655
656 if self.allowed_attn_types is not None:
657 for group_md in per_group_attn_metadata:
658 if not isinstance(group_md, self.allowed_attn_types):
659 raise ValueError(
660 f"Unsupported attention metadata type for speculative "
661 "decoding with num_speculative_tokens > 1: "
662 f"{type(group_md)}. Supported types are: "
663 f"{self.allowed_attn_types}"
664 )
665
666 # Generate the remaining draft tokens.
667 draft_token_ids_list = [draft_token_ids]
668
669 cudagraph_runtime_mode, input_batch_size, batch_size_across_dp = (
670 self._determine_batch_execution_and_padding(batch_size)
671 )
672
673 common_attn_metadata.num_actual_tokens = batch_size
674 common_attn_metadata.max_query_len = 1
675 common_attn_metadata.query_start_loc = self.arange[: batch_size + 1]
676 common_attn_metadata.query_start_loc_cpu = torch.from_numpy(
677 self.token_arange_np[: batch_size + 1]
678 ).clone()
679
680 # In padded drafter batch, we need to adjust the sequence lengths
681 # to remove the "padding" (i.e. rejected tokens).
682 # Only apply this adjustment when we have rejected tokens
683 # (i.e., not the first proposal).
684 if self.num_speculative_tokens > 1 and num_rejected_tokens_gpu is not None:
685 common_attn_metadata.seq_lens -= num_rejected_tokens_gpu
686
687 block_size = self.block_size
688 assert block_size > 0, "block_size has not been initialized."
689 for token_index in range(self.num_speculative_tokens - 1):
690 # Update the inputs.
691 # cast to int32 is crucial when eagle model is compiled.
692 # tensor.argmax() returns int64 by default.
693 input_ids = draft_token_ids_list[-1].int()
694
695 if self.use_heterogeneous_vocab:
696 # Map target token IDs to draft vocab space (TLI algorithm)
697 assert self.vocab_mapping is not None
698 input_ids = self.vocab_mapping.map_target_to_draft_ids(input_ids)
699
700 if not self.constant_draft_positions:
701 positions = self._update_positions_dependent_metadata(
702 positions,
703 common_attn_metadata,
704 batch_size,
705 input_batch_size,
706 block_size,
707 )
708
709 # Rebuild attention metadata. When draft positions are constant
710 # (e.g. Gemma4 MTP), common_attn_metadata is invariant across
711 # loop iterations so we build once and reuse.
712 if not self.constant_draft_positions or token_index == 0:
713 _, per_layer_attn_metadata = (
714 self.build_per_group_and_layer_attn_metadata(
715 common_attn_metadata, draft_index=token_index + 1
716 )
717 )
718
719 # copy inputs to buffer for cudagraph
720 self.input_ids[:batch_size] = input_ids
721 self.hidden_states[:batch_size] = hidden_states
722 if self.supports_mm_inputs:
723 self.inputs_embeds[:batch_size] = self.model.embed_input_ids(input_ids)
724
725 input_ids = None
726 inputs_embeds = self.inputs_embeds[:input_batch_size]
727 else:
728 input_ids = self.input_ids[:input_batch_size]
729 inputs_embeds = None
730
731 # Run the model.
732 model_kwargs = {
733 "input_ids": input_ids,
734 "positions": self._get_positions(input_batch_size),
735 "inputs_embeds": inputs_embeds,
736 }
737 if self.pass_hidden_states_to_model:
738 model_kwargs["hidden_states"] = self.hidden_states[:input_batch_size]
739
740 if self.eplb_state is not None:
741 self.eplb_state.prepare_forward(
742 self.draft_model_config,
743 batch_size,
744 )
745
746 with set_forward_context(
747 per_layer_attn_metadata,
748 self.vllm_config,
749 num_tokens=input_batch_size,
750 num_tokens_across_dp=batch_size_across_dp,
751 cudagraph_runtime_mode=cudagraph_runtime_mode,
752 slot_mapping=self._get_slot_mapping(input_batch_size),
753 ):
754 ret_hidden_states = self.model(**model_kwargs)
755 if not self.model_returns_tuple():
756 last_hidden_states = ret_hidden_states
757 hidden_states = ret_hidden_states
758 else:
759 last_hidden_states, hidden_states = ret_hidden_states
760
761 hidden_states = hidden_states[:batch_size]
762 draft_token_ids, draft_probs = self._sample_draft_tokens(
763 last_hidden_states[:batch_size], sampling_metadata
764 )
765 if draft_probs is not None:
766 assert draft_probs_list is not None
767 draft_probs_list.append(draft_probs)
768 draft_token_ids_list.append(draft_token_ids)
769
770 # [batch_size, num_speculative_tokens]
771 draft_token_ids = torch.stack(draft_token_ids_list, dim=1)
772 if draft_probs_list is not None:
773 self._last_draft_probs = torch.stack(draft_probs_list, dim=1).contiguous()
774 return draft_token_ids
775
776 def _update_positions_dependent_metadata(
777 self,
778 positions: torch.Tensor,
779 common_attn_metadata,
780 batch_size: int,
781 input_batch_size: int,
782 block_size: int,
783 ) -> torch.Tensor:
784 """Update positions, slot mappings, and sequence metadata for the
785 next draft step. Returns the updated positions tensor."""
786 positions_1d = positions[0] if self.uses_mrope else positions
787 if self.uses_mrope:
788 out_pos = self.mrope_positions[0, :batch_size]
789 else:
790 out_pos = self.positions[:batch_size]
791 eagle_step_update_slot_mapping_and_metadata(
792 positions_1d=positions_1d,
793 block_table_tensor=common_attn_metadata.block_table_tensor,
794 seq_lens=common_attn_metadata.seq_lens,
795 block_size=block_size,
796 max_model_len=self.max_model_len,
797 out_clamped_positions=out_pos,
798 out_slot_mapping=self._slot_mapping_buffer[:input_batch_size],
799 input_batch_size=input_batch_size,
800 )
801 common_attn_metadata.slot_mapping = self._slot_mapping_buffer[:batch_size]
802 if self.uses_mrope:
803 self.mrope_positions[1:, :batch_size] = self.mrope_positions[0, :batch_size]
804 positions = self.mrope_positions[:, :batch_size]
805 else:
806 positions = self.positions[:batch_size]
807 common_attn_metadata.max_seq_len = min(
808 common_attn_metadata.max_seq_len + 1,
809 self.max_model_len,
810 )
811
812 if common_attn_metadata.seq_lens_cpu_upper_bound is not None:
813 common_attn_metadata.seq_lens_cpu_upper_bound += 1
814
815 return positions
816
817 def set_inputs_first_pass(
818 self,
819 target_token_ids: torch.Tensor,
820 next_token_ids: torch.Tensor,
821 target_positions: torch.Tensor,
822 target_hidden_states: torch.Tensor,
823 token_indices_to_sample: torch.Tensor | None,
824 cad: CommonAttentionMetadata,
825 num_rejected_tokens_gpu: torch.Tensor | None,
826 ) -> tuple[int, torch.Tensor, CommonAttentionMetadata]:
827 # Map target token IDs to draft vocab space (TLI algorithm)
828 if self.use_heterogeneous_vocab:
829 assert self.vocab_mapping is not None
830 target_token_ids = self.vocab_mapping.map_target_to_draft_ids(
831 target_token_ids
832 )
833 next_token_ids = self.vocab_mapping.map_target_to_draft_ids(next_token_ids)
834 if not self.needs_extra_input_slots:
835 # Default EAGLE pathway: no reshaping of input tensors needed.
836 # Simply rotate the input ids and leave the positions unchanged,
837 # Inserting the next token ids at the last slot in each request.
838 if token_indices_to_sample is None:
839 token_indices_to_sample = cad.query_start_loc[1:] - 1
840
841 num_tokens = target_token_ids.shape[0]
842 # Shift the input ids by one token.
843 # E.g., [a1, b1, b2, c1, c2, c3] -> [b1, b2, c1, c2, c3, c3]
844 self.input_ids[: num_tokens - 1] = target_token_ids[1:]
845 # Replace the last token with the next token.
846 # E.g., [b1, b2, c1, c2, c3, c3] -> [a2, b2, b3, c2, c3, c4]
847 self.input_ids[token_indices_to_sample] = next_token_ids
848
849 # copy inputs to buffer for cudagraph
850 self._set_positions(num_tokens, target_positions)
851
852 self.hidden_states[:num_tokens] = target_hidden_states
853
854 return num_tokens, token_indices_to_sample, cad
855 else:
856 assert self.is_rejected_token_mask is not None
857 assert self.is_masked_token_mask is not None
858 # 1.
859 # Call a custom triton kernel to copy input_ids and positions
860 # into the correct slots in the preallocated buffers self.input_ids,
861 # self.positions.
862 batch_size = cad.batch_size()
863 # Since we might have to copy a lot of data for prefills, we select the
864 # block size based on the max query length and limit to max 256 slots/block.
865 max_num_tokens_per_request = (
866 cad.max_query_len + self.net_num_new_slots_per_request
867 )
868 BLOCK_SIZE_TOKENS = min(256, next_power_of_2(max_num_tokens_per_request))
869 num_blocks = (
870 max_num_tokens_per_request + BLOCK_SIZE_TOKENS - 1
871 ) // BLOCK_SIZE_TOKENS
872 total_num_input_tokens = target_token_ids.shape[0]
873 total_num_output_tokens = total_num_input_tokens + (
874 self.net_num_new_slots_per_request * batch_size
875 )
876
877 token_indices_to_sample = torch.empty(
878 batch_size * self.extra_slots_per_request,
879 dtype=torch.int32,
880 device=self.device,
881 )
882
883 # Destination indices to write target_hidden_states into drafting buffer.
884 out_hidden_state_mapping = torch.empty(
885 total_num_input_tokens, dtype=torch.int32, device=self.device
886 )
887
888 # Kernel grid: one program per request (row)
889 query_start_loc = cad.query_start_loc
890 query_end_loc = cad.query_start_loc[1:] - 1
891 if num_rejected_tokens_gpu is not None:
892 query_end_loc = query_end_loc - num_rejected_tokens_gpu
893
894 _copy_and_expand_eagle_inputs(
895 # (Padded) Inputs from the target model
896 target_token_ids_ptr=target_token_ids,
897 target_positions_ptr=target_positions,
898 next_token_ids_ptr=next_token_ids, # sampled tokens, one per request
899 # Outputs to the drafting buffers
900 out_input_ids_ptr=self.input_ids,
901 out_positions_ptr=self.positions, # Doesn't support mrope for now
902 out_is_rejected_token_mask_ptr=self.is_rejected_token_mask,
903 out_is_masked_token_mask_ptr=self.is_masked_token_mask,
904 out_new_token_indices_ptr=token_indices_to_sample,
905 out_hidden_state_mapping_ptr=out_hidden_state_mapping,
906 # Input metadata
907 query_start_loc_ptr=query_start_loc,
908 query_end_loc_ptr=query_end_loc,
909 padding_token_id=0,
910 parallel_drafting_token_id=self.parallel_drafting_token_id,
911 # Sizing info
912 # Note that we can deduce batch_size for free from the grid size
913 total_input_tokens=total_num_input_tokens,
914 num_padding_slots_per_request=self.extra_slots_per_request,
915 shift_input_ids=self.pass_hidden_states_to_model,
916 BLOCK_SIZE_TOKENS=BLOCK_SIZE_TOKENS,
917 batch_size=batch_size,
918 num_blocks=num_blocks,
919 )
920 if self.pass_hidden_states_to_model:
921 assert self.parallel_drafting_hidden_state_tensor is not None
922 self.hidden_states[out_hidden_state_mapping] = target_hidden_states
923 # Use torch.where to avoid DtoH sync from boolean indexing
924 mask = self.is_masked_token_mask[:total_num_output_tokens]
925 torch.where(
926 mask.unsqueeze(1),
927 self.parallel_drafting_hidden_state_tensor,
928 self.hidden_states[:total_num_output_tokens],
929 out=self.hidden_states[:total_num_output_tokens],
930 )
931
932 # 2.
933 # Recompute the slot mapping based on the new positions and
934 # rejection mask.
935 assert self.block_size > 0, "block_size has not been initialized."
936 new_slot_mapping = compute_new_slot_mapping(
937 cad=cad,
938 new_positions=self.positions[:total_num_output_tokens],
939 is_rejected_token_mask=self.is_rejected_token_mask[
940 :total_num_output_tokens
941 ],
942 block_size=self.block_size,
943 num_new_tokens=self.net_num_new_slots_per_request,
944 max_model_len=self.max_model_len,
945 )
946
947 # 3. Update the common attention metadata with the new (meta)data
948 new_cad = extend_all_queries_by_N(
949 cad,
950 N=self.net_num_new_slots_per_request,
951 arange=self.arange,
952 new_slot_mapping=new_slot_mapping,
953 )
954
955 return total_num_output_tokens, token_indices_to_sample, new_cad
956
957 def build_model_inputs_first_pass(
958 self,
959 num_tokens: int,
960 num_input_tokens: int,
961 mm_embed_inputs: tuple[list[torch.Tensor], torch.Tensor] | None,
962 ) -> tuple[dict[str, Any], int]:
963 if self.supports_mm_inputs:
964 mm_embeds, is_mm_embed = mm_embed_inputs or (None, None)
965
966 self.inputs_embeds[:num_tokens] = self.model.embed_input_ids(
967 self.input_ids[:num_tokens],
968 multimodal_embeddings=mm_embeds,
969 is_multimodal=is_mm_embed,
970 )
971
972 input_ids = None
973 inputs_embeds = self.inputs_embeds[:num_input_tokens]
974 else:
975 input_ids = self.input_ids[:num_input_tokens]
976 inputs_embeds = None
977
978 model_kwargs = {
979 "input_ids": input_ids,
980 "positions": self._get_positions(num_input_tokens),
981 "inputs_embeds": inputs_embeds,
982 }
983 if self.pass_hidden_states_to_model:
984 model_kwargs["hidden_states"] = self.hidden_states[:num_input_tokens]
985
986 return model_kwargs, num_input_tokens
987
988 def build_per_group_and_layer_attn_metadata(
989 self, common_attn_metadata: CommonAttentionMetadata, draft_index: int = 0
990 ) -> tuple[list[object], dict[str, object]]:
991 per_group_attn_metadata: list[object] = []
992 per_layer_attn_metadata: dict[str, object] = {}
993 for attn_group in self.draft_attn_groups:
994 attn_metadata = attn_group.get_metadata_builder().build_for_drafting(
995 common_attn_metadata=common_attn_metadata, draft_index=draft_index
996 )
997 per_group_attn_metadata.append(attn_metadata)
998 for layer_name in attn_group.layer_names:
999 per_layer_attn_metadata[layer_name] = attn_metadata
1000 return per_group_attn_metadata, per_layer_attn_metadata
1001
1002 def model_returns_tuple(self) -> bool:
1003 if self.method == "mtp":
1004 # These models return separate hidden states for logits and for
1005 # feedback into the next draft step.
1006 architectures = self.draft_model_config.hf_config.architectures or []
1007 return bool(
1008 {
1009 "DeepSeekMTPModel",
1010 "DeepseekV32MTPModel",
1011 "Glm5NextMTPModel",
1012 "KimiK3MTPModel",
1013 }.intersection(architectures)
1014 )
1015 return self.method not in ("mtp", "draft_model", "dflash")
1016
1017 def prepare_next_token_ids_cpu(
1018 self,
1019 sampled_token_ids: list[list[int]],
1020 requests: dict[str, CachedRequestState],
1021 gpu_input_batch: InputBatch,
1022 num_scheduled_tokens: dict[str, int],
1023 ) -> torch.Tensor:
1024 """This function is used to prepare the inputs for speculative decoding.
1025 It calculates the next token ids for each request based on the sampled
1026 token ids from the CPU. If a request has no sampled token ids (e.g.,
1027 during the initial decoding steps), it falls back to using the request
1028 state to get the next token id.
1029 """
1030 req_ids = gpu_input_batch.req_ids
1031 next_token_ids: list[int] = []
1032 for i, token_ids in enumerate(sampled_token_ids):
1033 if token_ids:
1034 # Common case.
1035 next_token_id = token_ids[-1]
1036 else:
1037 # Partial prefill (rare case).
1038 # Get the next token id from the request state.
1039 req_id = req_ids[i]
1040 req_state = requests[req_id]
1041 seq_len = req_state.num_computed_tokens + num_scheduled_tokens[req_id]
1042 next_token_id = req_state.get_token_id(seq_len)
1043 next_token_ids.append(next_token_id)
1044 next_token_ids = torch.tensor(
1045 next_token_ids, dtype=torch.int32, device=self.input_ids.device
1046 )
1047 return next_token_ids
1048
1049 def prepare_next_token_ids_padded(
1050 self,
1051 sampled_token_ids: torch.Tensor,
1052 requests: dict[str, CachedRequestState],
1053 gpu_input_batch: InputBatch,
1054 discard_request_mask: torch.Tensor,
1055 ) -> tuple[torch.Tensor, torch.Tensor]:
1056 """This function is used to prepare the inputs for speculative decoding.
1057 It calculates the next token ids and the number of valid sampled tokens
1058 for each request, considering the "discarded" requests whose next token
1059 is not sampled and comes from `request.get_token_id()` instead. This is denoted
1060 the "backup" token id. It also counts rejected tokens via `sampled_token_ids`.
1061 """
1062 # Precompute backup token IDs for discarded requests.
1063 num_reqs = gpu_input_batch.num_reqs
1064 for i in range(num_reqs):
1065 self.backup_next_token_ids.np[i] = requests[
1066 gpu_input_batch.req_ids[i]
1067 ].get_token_id(gpu_input_batch.num_tokens_no_spec[i] - 1)
1068 self.backup_next_token_ids.copy_to_gpu(num_reqs)
1069 backup_tokens_gpu = self.backup_next_token_ids.gpu
1070
1071 batch_size, num_tokens = sampled_token_ids.shape
1072 device = sampled_token_ids.device
1073
1074 assert discard_request_mask.dtype == torch.bool
1075 assert backup_tokens_gpu.dtype == torch.int32
1076
1077 next_token_ids = torch.empty(batch_size, dtype=torch.int32, device=device)
1078 valid_sampled_tokens_count = next_token_ids.new_empty(batch_size)
1079
1080 # Kernel grid: one program per request (row)
1081
1082 # Find the next power of 2 for block sizes
1083 BLOCK_SIZE_TOKENS = next_power_of_2(num_tokens)
1084 _eagle_prepare_next_token_padded(
1085 sampled_token_ids,
1086 discard_request_mask,
1087 backup_tokens_gpu,
1088 next_token_ids,
1089 valid_sampled_tokens_count,
1090 gpu_input_batch.vocab_size,
1091 num_tokens,
1092 batch_size,
1093 sampled_token_ids.stride(0),
1094 BLOCK_SIZE_TOKENS=BLOCK_SIZE_TOKENS,
1095 )
1096
1097 return next_token_ids, valid_sampled_tokens_count
1098
1099 def prepare_inputs_padded(
1100 self,
1101 common_attn_metadata: CommonAttentionMetadata,
1102 spec_decode_metadata: SpecDecodeMetadata,
1103 valid_sampled_tokens_count: torch.Tensor,
1104 ) -> tuple[CommonAttentionMetadata, torch.Tensor, torch.Tensor]:
1105 """This function is used to prepare the inputs for speculative decoding
1106 It updates the common_attn_metadata for speculative decoding,
1107 but does not consider the rejected tokens. Instead, all tokens
1108 are included as inputs to the speculator, with the rejected tokens
1109 used as padding and filtered out later by `token_indices_to_sample`.
1110 No blocking CPU operations should be introduced in this function.
1111 """
1112 num_reqs = common_attn_metadata.num_reqs
1113 device = valid_sampled_tokens_count.device
1114
1115 token_indices_to_sample = torch.empty(
1116 (num_reqs,), dtype=torch.int32, device=device
1117 )
1118 num_rejected_tokens_gpu = torch.empty(
1119 (num_reqs,), dtype=torch.int32, device=device
1120 )
1121
1122 _eagle_prepare_inputs_padded(
1123 spec_decode_metadata.cu_num_draft_tokens,
1124 valid_sampled_tokens_count,
1125 common_attn_metadata.query_start_loc,
1126 token_indices_to_sample,
1127 num_rejected_tokens_gpu,
1128 num_reqs,
1129 )
1130
1131 query_start_loc_cpu = common_attn_metadata.query_start_loc_cpu
1132 new_query_len_per_req = query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]
1133
1134 total_num_tokens = query_start_loc_cpu[-1].item()
1135
1136 spec_common_attn_metadata = CommonAttentionMetadata(
1137 query_start_loc=common_attn_metadata.query_start_loc,
1138 seq_lens=common_attn_metadata.seq_lens,
1139 query_start_loc_cpu=query_start_loc_cpu,
1140 seq_lens_cpu_upper_bound=common_attn_metadata.seq_lens_cpu_upper_bound,
1141 num_reqs=common_attn_metadata.num_reqs,
1142 num_actual_tokens=total_num_tokens,
1143 max_query_len=new_query_len_per_req.max().item(),
1144 max_seq_len=common_attn_metadata.max_seq_len,
1145 block_table_tensor=common_attn_metadata.block_table_tensor,
1146 slot_mapping=common_attn_metadata.slot_mapping[:total_num_tokens],
1147 causal=True,
1148 dcp_local_seq_lens=common_attn_metadata.dcp_local_seq_lens,
1149 )
1150
1151 return (
1152 spec_common_attn_metadata,
1153 token_indices_to_sample,
1154 num_rejected_tokens_gpu,
1155 )
1156
1157 def prepare_inputs(
1158 self,
1159 common_attn_metadata: CommonAttentionMetadata,
1160 sampled_token_ids: list[list[int]],
1161 num_draft_tokens: list[int],
1162 ) -> tuple[CommonAttentionMetadata, torch.Tensor]:
1163 """This function is used to prepare the inputs for speculative decoding.
1164 It updates to the common_attn_metadata to account for the rejected
1165 tokens (and newly sampled tokens). It also returns the token indices
1166 of the tokens that should be fed to the speculator.
1167 """
1168 # E.g.
1169 # common_attn_metadata.query_start_loc{_cpu}:
1170 # [0, q1, q1 + q2, q1 + q2 + q3]
1171 # common_attn_metadata.seq_lens{_cpu}: [s1, s2, s3]
1172 # num_rejected_tokens: [n1, n2, n3]
1173 # This function computes the intermediate values:
1174 # num_tokens_per_req: [q1 - n1, q2 - n2, q3 - n3]
1175 # And returns:
1176 # common_attn_metadata.query_start_loc{_cpu}:
1177 # [0, q1 - n1, q1 + q2 - n1 - n2, q1 + q2 + q3 - n1 - n2 - n3]
1178 # common_attn_metadata.seq_lens{_cpu}:
1179 # [s1 - n1 + 1, s2 - n2 + 1, s3 - n3 + 1]
1180 # token_indices: [0, 1, ..., q1 - n1 - 1,
1181 # q1, q1 + 1, ..., q1 + q2 - n2 - 1,
1182 # q1 + q2, q1 + q2 + 1, ..., q1 + q2 + q3 - n3 - 1]
1183
1184 num_rejected_tokens = [
1185 n + 1 - len(sampled_token_ids[i]) if n > 0 else 0
1186 for i, n in enumerate(num_draft_tokens)
1187 ]
1188 num_rejected_tokens = torch.tensor(num_rejected_tokens, dtype=torch.int32)
1189
1190 device = common_attn_metadata.query_start_loc.device
1191 query_start_loc_cpu = common_attn_metadata.query_start_loc_cpu
1192 # upper_bound - rejected = actual post-rejection seq_lens (no D2H sync).
1193 assert common_attn_metadata.seq_lens_cpu_upper_bound is not None
1194 new_seq_lens_cpu = (
1195 common_attn_metadata.seq_lens_cpu_upper_bound - num_rejected_tokens
1196 )
1197
1198 # [0, q1, q1 + q2, q1 + q2 + q3] -> [q1, q2, q3]
1199 new_query_len_per_req = query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]
1200 # [q1, q2, q3] -> [q1 - n1, q2 - n2, q3 - n3]
1201 new_num_tokens_per_req = new_query_len_per_req - num_rejected_tokens
1202 new_num_tokens_per_req_np = new_num_tokens_per_req.numpy()
1203
1204 # [q1 - n1, q2 - n2, q3 - n3] ->
1205 # [0, q1 - n1, q1 + q2 - n1 - n2, q1 + q2 + q3 - n1 - n2 - n3]
1206 new_query_start_loc_cpu = torch.zeros(
1207 query_start_loc_cpu.shape,
1208 dtype=torch.int32,
1209 pin_memory=PIN_MEMORY,
1210 )
1211 new_query_start_loc_np = new_query_start_loc_cpu.numpy()
1212 np.cumsum(new_num_tokens_per_req_np, out=new_query_start_loc_np[1:])
1213
1214 total_num_tokens = new_query_start_loc_np[-1]
1215 # Example assuming num_tokens_per_req_np = [2, 4, 3]
1216 # this implies that `new_query_start_locs` is:
1217 # [0, 2, 6, 9] ->
1218 # [0, 0, 2, 2, 2, 2, 6, 6, 6]
1219 # _r1_ ____r2____ ___r3__
1220 new_query_start_locs_expanded = np.repeat(
1221 new_query_start_loc_np[:-1], new_num_tokens_per_req_np
1222 )
1223 # [0, 1, 2, 3, 4, 5, 6, 7, 8] ->
1224 # [0, 1, 0, 1, 2, 3, 0, 1, 2]
1225 # _r1_ ____r2____ ___r3__
1226 token_offsets = (
1227 self.token_arange_np[:total_num_tokens] - new_query_start_locs_expanded
1228 )
1229
1230 # Expand starting positions to match token pattern
1231 # [0, q1, q1 + q2] ->
1232 # [0, 0, q1, q1, q1, q1, q1 + q2, q1 + q2, q1 + q2]
1233 # _r1_ _____r2_______ ___________r3____________
1234 old_query_start_locs_expanded = np.repeat(
1235 query_start_loc_cpu[:-1].numpy(), new_num_tokens_per_req_np
1236 )
1237 # Final token indices are:
1238 # [0, 1, // req 1
1239 # q1 + 0, q1 + 1, q1 + 2, q1 + 3, // req 2
1240 # q1 + q2 + 0, q1 + q2 + 1, q1 + q2 + 2] // req 3
1241 token_indices_np = token_offsets + old_query_start_locs_expanded
1242 token_indices = async_tensor_h2d(token_indices_np, device=device)
1243
1244 spec_common_attn_metadata = CommonAttentionMetadata(
1245 query_start_loc=async_tensor_h2d(new_query_start_loc_cpu, device=device),
1246 seq_lens=async_tensor_h2d(new_seq_lens_cpu, device=device),
1247 query_start_loc_cpu=new_query_start_loc_cpu,
1248 seq_lens_cpu_upper_bound=new_seq_lens_cpu,
1249 num_reqs=common_attn_metadata.num_reqs,
1250 num_actual_tokens=total_num_tokens,
1251 max_query_len=new_query_len_per_req.max().item(),
1252 max_seq_len=new_seq_lens_cpu.max().item(),
1253 block_table_tensor=common_attn_metadata.block_table_tensor,
1254 slot_mapping=common_attn_metadata.slot_mapping[token_indices],
1255 causal=True,
1256 dcp_local_seq_lens=common_attn_metadata.dcp_local_seq_lens,
1257 )
1258
1259 return spec_common_attn_metadata, token_indices
1260
1261 def get_model_name(self, model: nn.Module) -> str:
1262 if hasattr(model, "module"): # multi-GPU
1263 model = model.module
1264 return model.__class__.__name__
1265
1266 def _create_draft_vllm_config(self) -> VllmConfig:
1267 """Return a VllmConfig with kernel-level overrides for the proposer.
1268 Subclasses may override to apply additional config changes.
1269 """
1270 spec_cfg = self.speculative_config
1271 base = self.vllm_config
1272
1273 if spec_cfg.moe_backend is not None:
1274 base = replace(
1275 base,
1276 kernel_config=replace(
1277 base.kernel_config,
1278 moe_backend=spec_cfg.moe_backend,
1279 ),
1280 )
1281
1282 # Note (matt): Never inherit the attention backend from base, because there are
1283 # many opportunities for incompatibility, so we always independently autoselect
1284 # unless explicitly specified in the speculative config.
1285 base = replace(
1286 base,
1287 attention_config=replace(
1288 base.attention_config,
1289 backend=spec_cfg.attention_backend,
1290 ),
1291 )
1292
1293 if spec_cfg.kv_cache_dtype is not None:
1294 base = replace(
1295 base,
1296 cache_config=replace(
1297 base.cache_config,
1298 cache_dtype=spec_cfg.kv_cache_dtype,
1299 ),
1300 )
1301
1302 return base
1303
1304 def _get_model(self) -> nn.Module:
1305 """Default method to call get_model(). Can be overridden by subclasses which
1306 need to customize model loading.
1307 """
1308 from vllm.compilation.backends import set_model_tag
1309
1310 draft_vllm_config = self._create_draft_vllm_config()
1311 with set_model_tag("eagle_head"):
1312 model = get_model(
1313 vllm_config=draft_vllm_config,
1314 model_config=self.speculative_config.draft_model_config,
1315 load_config=get_draft_load_config(draft_vllm_config),
1316 )
1317 return model
1318
1319 def load_model(self, target_model: nn.Module) -> None:
1320 target_attn_layer_names = set(
1321 get_layers_from_vllm_config(
1322 self.vllm_config,
1323 AttentionLayerBase, # type: ignore[type-abstract]
1324 ).keys()
1325 )
1326
1327 self.model = self._get_model()
1328
1329 # Find draft layers (attention layers added by draft model)
1330 all_attn_layers = get_layers_from_vllm_config(
1331 self.vllm_config,
1332 AttentionLayerBase, # type: ignore[type-abstract]
1333 )
1334 # Filter to only layers that have KV cache specs.
1335 self._draft_attn_layer_names = {
1336 name
1337 for name in (set(all_attn_layers.keys()) - target_attn_layer_names)
1338 if all_attn_layers[name].get_kv_cache_spec(self.vllm_config) is not None
1339 }
1340
1341 # Even if the target model is multimodal, we can also use
1342 # text-only draft models
1343 if self.supports_mm_inputs and not supports_multimodal_embeddings(self.model):
1344 logger.warning_once(
1345 "Draft model %s does not support external multimodal embeddings. "
1346 "Embeddings from the target model will not be passed to the "
1347 "drafter; using text-only draft inputs instead.",
1348 type(self.model).__name__,
1349 )
1350 self.supports_mm_inputs = False
1351
1352 if supports_multimodal(target_model):
1353 # handle multimodality
1354 assert hasattr(target_model, "config")
1355 if self.get_model_name(target_model) in [
1356 "BailingMoeV3VLForConditionalGeneration",
1357 "Cohere2VisionForConditionalGeneration",
1358 "Exaone4_5_ForConditionalGeneration",
1359 "GlmOcrForConditionalGeneration",
1360 "HunYuanVLForConditionalGeneration",
1361 "InternS2PreviewForConditionalGeneration",
1362 "MiMoV2OmniForCausalLM",
1363 "Qwen2_5_VLForConditionalGeneration",
1364 "Qwen3_5ForConditionalGeneration",
1365 "Qwen3_5MoeForConditionalGeneration",
1366 "Qwen3VLForConditionalGeneration",
1367 "Qwen3VLMoeForConditionalGeneration",
1368 "Gemma4ForConditionalGeneration",
1369 "Gemma4UnifiedForConditionalGeneration",
1370 "Step3p7ForConditionalGeneration",
1371 ]:
1372 self.model.config.image_token_index = target_model.config.image_token_id
1373 elif self.get_model_name(target_model) == "PixtralForConditionalGeneration":
1374 self.model.config.image_token_index = (
1375 target_model.config.vision_config.image_token_id
1376 )
1377 elif self.get_model_name(target_model) in (
1378 "KimiK25ForConditionalGeneration",
1379 "KimiK3ForConditionalGeneration",
1380 ):
1381 self.model.config.image_token_index = (
1382 target_model.config.media_placeholder_token_id
1383 )
1384 elif self.get_model_name(target_model) == "NemotronH_Nano_VL_V2":
1385 self.model.config.image_token_index = (
1386 target_model.config.img_context_token_id
1387 )
1388 else:
1389 self.model.config.image_token_index = (
1390 target_model.config.image_token_index
1391 )
1392 target_language_model = cast(
1393 SupportsMultiModal, target_model
1394 ).get_language_model()
1395 else:
1396 target_language_model = target_model
1397
1398 self._maybe_share_embeddings(target_language_model)
1399 self._maybe_share_lm_head(target_language_model)
1400
1401 if (
1402 self.parallel_drafting
1403 and self.pass_hidden_states_to_model
1404 and self.parallel_drafting_hidden_state_tensor is not None
1405 ):
1406 flat_mask = self.model.mask_hidden.view(-1)
1407 if self.eagle3_use_aux_hidden_state:
1408 # EAGLE3: mask_hidden stores all aux hidden states,
1409 # project through combine_hidden_states
1410 self.parallel_drafting_hidden_state_tensor.copy_(
1411 self.model.combine_hidden_states(flat_mask)
1412 )
1413 else:
1414 self.parallel_drafting_hidden_state_tensor.copy_(flat_mask)
1415
1416 def _maybe_share_embeddings(self, target_language_model: nn.Module) -> None:
1417 """Some draft models may not have their own embedding layers, and some may
1418 have a duplicate copy of the target model's embedding layers. In these cases,
1419 we share the target model's embedding layers with the draft model to save
1420 memory.
1421 """
1422 if get_pp_group().world_size == 1:
1423 inner_model = getattr(target_language_model, "model", None)
1424 if inner_model is None:
1425 raise AttributeError("Target model does not have 'model' attribute")
1426 if hasattr(inner_model, "embed_tokens"):
1427 target_embed_tokens = inner_model.embed_tokens
1428 elif hasattr(inner_model, "embedding"):
1429 target_embed_tokens = inner_model.embedding
1430 else:
1431 raise AttributeError(
1432 "Target model does not have 'embed_tokens' or 'embedding' attribute"
1433 )
1434
1435 share_embeddings = False
1436 if hasattr(self.model, "has_own_embed_tokens"):
1437 # EAGLE model
1438 if not self.model.has_own_embed_tokens:
1439 share_embeddings = True
1440 logger.info(
1441 "Detected EAGLE model without its own embed_tokens in the"
1442 " checkpoint. Sharing target model embedding weights with the"
1443 " draft model."
1444 )
1445 elif (
1446 isinstance(target_embed_tokens.weight, torch.Tensor)
1447 and isinstance(self.model.model.embed_tokens.weight, torch.Tensor)
1448 # TODO: Offload to CPU for comparison to avoid extra GPU memory
1449 # usage in CI testing environments with limited GPU memory
1450 and torch.equal(
1451 target_embed_tokens.weight.cpu(),
1452 self.model.model.embed_tokens.weight.cpu(),
1453 )
1454 ):
1455 share_embeddings = True
1456 logger.info(
1457 "Detected EAGLE model with embed_tokens identical to the target"
1458 " model. Sharing target model embedding weights with the draft"
1459 " model."
1460 )
1461 else:
1462 logger.info(
1463 "Detected EAGLE model with distinct embed_tokens weights. "
1464 "Keeping separate embedding weights from the target model."
1465 )
1466 else:
1467 # MTP model
1468 share_embeddings = True
1469 logger.info(
1470 "Detected MTP model. "
1471 "Sharing target model embedding weights with the draft model."
1472 )
1473
1474 if share_embeddings and hasattr(self.model, "has_own_embed_tokens"):
1475 # EAGLE drafts consume input embeddings at their own hidden
1476 # size, so only share when the widths match. MTP drafts
1477 # project target-width embeddings (e.g. Gemma4 MTP's
1478 # pre_projection takes 2 * backbone_hidden_size), so the
1479 # width check does not apply to them.
1480 draft_embed = self.model.model.embed_tokens
1481 # Guard with isinstance so non-Tensor weights (e.g. in tests)
1482 # are not affected — mirrors the weight-equality check above.
1483 if isinstance(target_embed_tokens.weight, torch.Tensor) and isinstance(
1484 draft_embed.weight, torch.Tensor
1485 ):
1486 target_dim = target_embed_tokens.weight.shape[-1]
1487 draft_dim = draft_embed.weight.shape[-1]
1488 if target_dim != draft_dim:
1489 share_embeddings = False
1490 logger.info(
1491 "Target embedding dim (%d) differs from draft "
1492 "embedding dim (%d). Keeping separate embedding "
1493 "weights.",
1494 target_dim,
1495 draft_dim,
1496 )
1497
1498 if share_embeddings:
1499 if hasattr(self.model.model, "embed_tokens"):
1500 del self.model.model.embed_tokens
1501 self.model.model.embed_tokens = target_embed_tokens
1502 else:
1503 logger.info(
1504 "The draft model's vocab embedding will be loaded separately"
1505 " from the target model."
1506 )
1507
1508 def _maybe_share_lm_head(self, target_language_model: nn.Module) -> None:
1509 """Some draft models may not have their own LM head, and some may have a
1510 duplicate copy of the target model's LM head. In these cases, we share
1511 the target model's LM head with the draft model to save memory.
1512 """
1513 share_lm_head = False
1514 if hasattr(self.model, "has_own_lm_head"):
1515 # EAGLE model
1516 if not self.model.has_own_lm_head:
1517 share_lm_head = True
1518 logger.info(
1519 "Detected EAGLE model without its own lm_head in the checkpoint. "
1520 "Sharing target model lm_head weights with the draft model."
1521 )
1522 elif (
1523 hasattr(target_language_model, "lm_head")
1524 and hasattr(target_language_model.lm_head, "weight")
1525 and hasattr(self.model.lm_head, "weight")
1526 and isinstance(target_language_model.lm_head.weight, torch.Tensor)
1527 and isinstance(self.model.lm_head.weight, torch.Tensor)
1528 # TODO: Offload to CPU for comparison to avoid extra GPU memory
1529 # usage in CI testing environments with limited GPU memory
1530 and torch.equal(
1531 target_language_model.lm_head.weight.cpu(),
1532 self.model.lm_head.weight.cpu(),
1533 )
1534 ):
1535 share_lm_head = True
1536 logger.info(
1537 "Detected EAGLE model with lm_head identical to the target model. "
1538 "Sharing target model lm_head weights with the draft model."
1539 )
1540 else:
1541 logger.info(
1542 "Detected EAGLE model with distinct lm_head weights. "
1543 "Keeping separate lm_head weights from the target model."
1544 )
1545 else:
1546 # MTP model
1547 share_lm_head = True
1548 logger.info(
1549 "Detected MTP model. "
1550 "Sharing target model lm_head weights with the draft model."
1551 )
1552
1553 if share_lm_head and hasattr(target_language_model, "lm_head"):
1554 if hasattr(self.model, "lm_head"):
1555 del self.model.lm_head
1556 self.model.lm_head = target_language_model.lm_head
1557
1558 # MTP models call compute_logits via shared_head.head (a
1559 # ParallelLMHead inside each MTP layer), not self.model.lm_head.
1560 # If the checkpoint omits a copy of the lm_head weights at the
1561 # MTP layer path, shared_head.head stays uninitialised and
1562 # produces NaN logits. Always share it explicitly.
1563 inner = getattr(self.model, "model", None)
1564 layers = getattr(inner, "layers", None) if inner else None
1565 if layers is not None:
1566 items = layers.values() if isinstance(layers, nn.ModuleDict) else layers
1567 for layer in items:
1568 sh = getattr(layer, "shared_head", None)
1569 if sh is not None and hasattr(sh, "head"):
1570 del sh.head
1571 sh.head = target_language_model.lm_head
1572 logger.info(
1573 "Shared target model lm_head with MTP shared_head.head."
1574 )
1575
1576 if hasattr(target_language_model.model, "topk_indices_buffer"):
1577 target_buffer = target_language_model.model.topk_indices_buffer
1578 if hasattr(self.model.model, "topk_indices_buffer"):
1579 del self.model.model.topk_indices_buffer
1580 self.model.model.topk_indices_buffer = target_buffer
1581 # Also share at per-module level so that the indexer and
1582 # sparse-attention backends in each MTP layer read from
1583 # the target model's buffer.
1584 for _, module in self.model.model.named_modules():
1585 if hasattr(module, "topk_indices_buffer"):
1586 module.topk_indices_buffer = target_buffer
1587 logger.info(
1588 "Detected MTP model with topk_indices_buffer. "
1589 "Sharing target model topk_indices_buffer with the draft model."
1590 )
1591
1592 # Detect index_share_for_mtp_iteration: when True, the proposer
1593 # toggles skip_topk so step 0 computes MTP's own indices and
1594 # steps 1+ reuse them.
1595 spec_config = self.vllm_config.speculative_config
1596 draft_hf_config = (
1597 spec_config.draft_model_config.hf_config
1598 if spec_config is not None
1599 else None
1600 )
1601 self._share_mtp_indices = getattr(
1602 draft_hf_config, "index_share_for_mtp_iteration", False
1603 )
1604
1605 if self.use_local_argmax_reduction:
1606 if not hasattr(self.model, "get_top_tokens"):
1607 raise ValueError(
1608 "use_local_argmax_reduction is enabled but draft model "
1609 f"{self.model.__class__.__name__} does not implement "
1610 "get_top_tokens()."
1611 )
1612 logger.info(
1613 "Using local argmax reduction for draft token generation "
1614 "(communication: O(2*tp_size) vs O(vocab_size))."
1615 )
1616
1617 @torch.inference_mode()
1618 def dummy_run(
1619 self,
1620 num_tokens: int,
1621 use_cudagraphs: bool = True,
1622 is_graph_capturing: bool = False,
1623 slot_mappings: dict[str, torch.Tensor] | None = None,
1624 ) -> None:
1625 # FIXME: when using tree-based specdec, adjust number of forward-passes
1626 # according to the depth of the tree.
1627 only_one_forward_pass = is_graph_capturing or self.parallel_drafting
1628 for fwd_idx in range(
1629 1 if only_one_forward_pass else self.num_speculative_tokens
1630 ):
1631 if fwd_idx <= 1:
1632 cudagraph_runtime_mode, num_input_tokens, num_tokens_across_dp = (
1633 self._determine_batch_execution_and_padding(
1634 num_tokens, use_cudagraphs=use_cudagraphs
1635 )
1636 )
1637
1638 # Make sure to use EAGLE's own buffer during cudagraph capture.
1639 if (
1640 self._draft_attn_layer_names
1641 and slot_mappings is not None
1642 and next(iter(self._draft_attn_layer_names)) in slot_mappings
1643 ):
1644 slot_mapping_dict = self._get_slot_mapping(num_input_tokens)
1645 else:
1646 slot_mapping_dict = slot_mappings or {}
1647
1648 with set_forward_context(
1649 None,
1650 self.vllm_config,
1651 num_tokens=num_input_tokens,
1652 num_tokens_across_dp=num_tokens_across_dp,
1653 cudagraph_runtime_mode=cudagraph_runtime_mode,
1654 slot_mapping=slot_mapping_dict,
1655 ):
1656 if self.supports_mm_inputs:
1657 input_ids = None
1658 inputs_embeds = self.inputs_embeds[:num_input_tokens]
1659 else:
1660 input_ids = self.input_ids[:num_input_tokens]
1661 inputs_embeds = None
1662
1663 kwargs = dict(
1664 input_ids=input_ids,
1665 positions=self._get_positions(num_input_tokens),
1666 inputs_embeds=inputs_embeds,
1667 )
1668 if self.pass_hidden_states_to_model:
1669 kwargs["hidden_states"] = self.hidden_states[:num_input_tokens]
1670 self.model(**kwargs)
1671
1672 def _get_eagle3_use_aux_hidden_state_from_config(self) -> bool:
1673 """Some eagle3 heads (e.g., nvidia/gpt-oss-120b-Eagle3-v2) do not use auxiliary
1674 hidden states and directly uses the last layer output just like eagle1.
1675 They might indicate this by setting "use_aux_hidden_state" to False
1676 inside the "eagle_config" dict of their hf_config.
1677 """
1678 if self.method != "eagle3":
1679 return False
1680 # Assume that eagle3 heads use aux hidden states by default
1681 use_aux_hidden_state = True
1682 eagle_config = getattr(self.draft_model_config.hf_config, "eagle_config", None)
1683 if eagle_config is not None:
1684 use_aux_hidden_state = eagle_config.get("use_aux_hidden_state", True)
1685 return use_aux_hidden_state
1686
1687 def validate_same_kv_cache_group(self, kv_cache_config: KVCacheConfig) -> None:
1688 """Validate that all drafting layers belong to the same KVCacheGroup.
1689 Need this assumption to ensure all drafting layers can use the
1690 same AttentionMetadata.
1691 May extend to multiple AttentionMetadata in the future.
1692 """
1693 kv_cache_groups: dict[str, int] = {}
1694 for id, kv_cache_group in enumerate(kv_cache_config.kv_cache_groups):
1695 for layer_name in kv_cache_group.layer_names:
1696 kv_cache_groups[layer_name] = id
1697 assert (
1698 len(
1699 set(
1700 [
1701 kv_cache_groups[layer_name]
1702 for layer_name in self._draft_attn_layer_names
1703 ]
1704 )
1705 )
1706 == 1
1707 ), "All drafting layers should belong to the same kv cache group"
1708
1709 def initialize_attn_backend(
1710 self,
1711 kv_cache_config: KVCacheConfig,
1712 kernel_block_sizes: list[int] | None = None,
1713 ) -> None:
1714 """Initialize AttentionGroups for draft layers using kv_cache_config.
1715 Called from the model runner's initialize_metadata_builders.
1716 """
1717 all_attn_layers = get_layers_from_vllm_config(
1718 self.vllm_config,
1719 AttentionLayerBase, # type: ignore[type-abstract]
1720 )
1721
1722 # Find which kv_cache_group the draft layers belong to
1723 self.validate_same_kv_cache_group(kv_cache_config)
1724 kv_cache_spec = None
1725 for gid, group in enumerate(kv_cache_config.kv_cache_groups):
1726 if self._draft_attn_layer_names & set(group.layer_names):
1727 self.kv_cache_gid = gid
1728 kv_cache_spec = group.kv_cache_spec
1729 break
1730
1731 attention_groups: dict[tuple[str, str], AttentionGroup] = {}
1732 if kv_cache_spec is not None:
1733 # _draft_attn_layer_names is a set; iterate in sorted order so
1734 # that attention_groups (and anything derived from its first
1735 # element) is deterministic across processes.
1736 for layer_name in sorted(self._draft_attn_layer_names):
1737 attn_backend = all_attn_layers[layer_name].get_attn_backend()
1738 backend_key = attn_backend.full_cls_name()
1739 if backend_key not in attention_groups:
1740 layer_kv_cache_spec = kv_cache_spec
1741 if isinstance(layer_kv_cache_spec, UniformTypeKVCacheSpecs):
1742 layer_kv_cache_spec = layer_kv_cache_spec.kv_cache_specs[
1743 layer_name
1744 ]
1745
1746 kernel_block_size = (
1747 kernel_block_sizes[self.kv_cache_gid]
1748 if kernel_block_sizes is not None
1749 and self.kv_cache_gid < len(kernel_block_sizes)
1750 else None
1751 )
1752 attn_group = AttentionGroup(
1753 backend=attn_backend,
1754 layer_names=[layer_name],
1755 kv_cache_spec=layer_kv_cache_spec,
1756 kv_cache_group_id=self.kv_cache_gid,
1757 )
1758 attn_group.create_metadata_builders(
1759 self.vllm_config,
1760 self.device,
1761 kernel_block_size=kernel_block_size,
1762 )
1763 attention_groups[backend_key] = attn_group
1764 else:
1765 attention_groups[backend_key].layer_names.append(layer_name)
1766
1767 self.draft_attn_groups = list(attention_groups.values())
1768 if kernel_block_sizes is not None and 0 <= self.kv_cache_gid < len(
1769 kernel_block_sizes
1770 ):
1771 # Slot mappings are computed against the block table, which is
1772 # stored at kernel-block granularity. Use the kernel block size
1773 # rather than the KV cache manager's block size; the two differ
1774 # when manager blocks are split for the attention kernel.
1775 self.block_size = kernel_block_sizes[self.kv_cache_gid]
1776 else:
1777 self.block_size = (
1778 self.draft_attn_groups[0]
1779 .get_metadata_builder()
1780 .kv_cache_spec.block_size
1781 )
1782 logger.debug("Using block size %d for drafting layers", self.block_size)
1783
1784 def _determine_batch_execution_and_padding(
1785 self,
1786 num_tokens: int,
1787 use_cudagraphs: bool = True,
1788 ) -> tuple[CUDAGraphMode, int, torch.Tensor | None]:
1789 cudagraph_mode, batch_desc = self.cudagraph_dispatcher.dispatch(
1790 num_tokens,
1791 valid_modes=({CUDAGraphMode.NONE} if not use_cudagraphs else None),
1792 )
1793 num_tokens_padded = batch_desc.num_tokens
1794
1795 # Extra coordination when running data-parallel since we need to
1796 # coordinate across ranks
1797 # TODO(Flechman): support DBO ubatching
1798 should_ubatch, num_tokens_across_dp = False, None
1799 if self.vllm_config.parallel_config.data_parallel_size > 1:
1800 should_ubatch, num_tokens_across_dp, synced_cudagraph_mode = (
1801 coordinate_batch_across_dp(
1802 num_tokens_unpadded=num_tokens,
1803 parallel_config=self.vllm_config.parallel_config,
1804 allow_microbatching=False,
1805 num_tokens_padded=num_tokens_padded,
1806 cudagraph_mode=cudagraph_mode.value,
1807 )
1808 )
1809 assert not should_ubatch, "DBO ubatching not implemented for EAGLE"
1810
1811 # Extract DP-synced values
1812 if num_tokens_across_dp is not None:
1813 dp_rank = self.dp_rank
1814 num_tokens_padded = int(num_tokens_across_dp[dp_rank].item())
1815 # Re-dispatch with DP padding so we have the correct
1816 # batch_descriptor
1817 cudagraph_mode, batch_desc = self.cudagraph_dispatcher.dispatch(
1818 num_tokens_padded,
1819 valid_modes={CUDAGraphMode(synced_cudagraph_mode)},
1820 )
1821 # Assert to make sure the agreed upon token count is correct
1822 # otherwise num_tokens_across_dp will no-longer be valid
1823 assert batch_desc.num_tokens == num_tokens_padded
1824 num_tokens_across_dp[dp_rank] = num_tokens_padded
1825
1826 return cudagraph_mode, num_tokens_padded, num_tokens_across_dp
1827
1828
1829 # NOTE(woosuk): Currently, the below code is not used and we always use argmax
1830 # to sample the draft tokens. We will use this after we find a way to manage
1831 # the draft prob tensor.
1832 # Refer to https://github.com/vllm-project/vllm/pull/16899 for the details.
1833 # FIXME(woosuk): The logic here is duplicated with the main sampling code.
1834 # We should refactor this to reuse the same sampling implementation.
1835 def compute_probs_and_sample_next_token(
1836 logits: torch.Tensor,
1837 sampling_metadata: SamplingMetadata,
1838 use_fp64_gumbel: bool = False,
1839 ) -> tuple[torch.Tensor, torch.Tensor]:
1840 if sampling_metadata.all_greedy:
1841 # For greedy requests, draft_probs is not used in rejection sampling.
1842 # Therefore, we can just return the logits.
1843 probs = logits
1844 next_token_ids = logits.argmax(dim=-1)
1845 return next_token_ids, probs
1846
1847 assert sampling_metadata.temperature is not None
1848
1849 # Use epsilon comparison to detect greedy sampling (temperature ~ 0.0)
1850 # consistent with sampler.py's _SAMPLING_EPS threshold
1851 temperature = sampling_metadata.temperature
1852 # Avoid division by zero if there are greedy requests.
1853 if not sampling_metadata.all_random:
1854 is_greedy = temperature < _SAMPLING_EPS
1855 temperature = torch.where(is_greedy, 1.0, temperature)
1856 logits.div_(temperature.view(-1, 1))
1857 probs = logits.softmax(dim=-1, dtype=torch.float32)
1858
1859 # NOTE(woosuk): Currently, we ignore most of the sampling parameters in
1860 # generating the draft tokens. We only use the temperature. While this
1861 # could degrade the acceptance rate, it does not affect the distribution
1862 # of the generated tokens after rejection sampling.
1863
1864 # TODO(woosuk): Consider seeds.
1865 q = empty_exponential_noise_like(probs, use_fp64_gumbel)
1866 q.exponential_()
1867 # NOTE(woosuk): We shouldn't use `probs.div_(q)` because the draft_probs
1868 # will be used later for rejection sampling.
1869 next_token_ids = sample_with_exponential_noise(probs.clone(), q)
1870 if not sampling_metadata.all_random:
1871 greedy_token_ids = probs.argmax(dim=-1)
1872 next_token_ids = torch.where(is_greedy, greedy_token_ids, next_token_ids)
1873 return next_token_ids, probs