@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / cde8d24710
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 4, 17:30 UTC

[Spec Decode] Move `SpecDecodeBaseProposer` out of `eagle.py` (#40732)

Signed-off-by: Matthew Bonanni <mbonanni@redhat.com>

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