main
py 1,995 lines 86.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import copy
5 import functools
6 from collections.abc import Callable, Mapping
7 from typing import TYPE_CHECKING, Any, Literal, get_args
8
9 from pydantic import Field, SkipValidation, field_validator, model_validator
10 from typing_extensions import Self
11
12 from vllm.config import LoadConfig
13 from vllm.config.cache import CacheDType
14 from vllm.config.kernel import MoEBackend
15 from vllm.config.model import HfOverrides, ModelConfig
16 from vllm.config.parallel import ParallelConfig
17 from vllm.config.utils import config, replace
18 from vllm.logger import init_logger
19 from vllm.transformers_utils.config import get_hf_text_config
20 from vllm.utils.hashing import safe_hash
21 from vllm.utils.import_utils import LazyLoader, has_arctic_inference
22 from vllm.v1.attention.backends.registry import AttentionBackendEnum
23
24 if TYPE_CHECKING:
25 from transformers import PreTrainedConfig
26
27 import vllm.model_executor.layers.quantization as me_quant
28 from vllm.config.vllm import VllmConfig
29 else:
30 PreTrainedConfig = Any
31 VllmConfig = Any
32
33 me_quant = LazyLoader(
34 "model_executor", globals(), "vllm.model_executor.layers.quantization"
35 )
36
37 logger = init_logger(__name__)
38
39 MTPModelTypes = Literal[
40 "deepseek_mtp",
41 "dots3_note_mtp",
42 "mimo_mtp",
43 "mimo_v2_mtp",
44 "glm4_moe_mtp",
45 "glm4_moe_lite_mtp",
46 "glm_ocr_mtp",
47 "ernie_mtp",
48 "nemotron_h_mtp",
49 "exaone_moe_mtp",
50 "exaone4_5_mtp",
51 "qwen3_next_mtp",
52 "qwen4_exp_mtp",
53 "qwen3_5_mtp",
54 "longcat_flash_mtp",
55 "bailing_hybrid_v3_mtp",
56 "minimax_m3_mtp",
57 "bailing_hybrid_mtp",
58 "mtp",
59 "kimi_k3_mtp",
60 "pangu_ultra_moe_mtp",
61 "step3p5_mtp",
62 "hy_v3_mtp",
63 "hy_v4_mtp",
64 "gemma4_mtp",
65 "inkling_mtp",
66 "glm5_next_mtp",
67 ]
68 NgramGPUTypes = Literal["ngram_gpu"]
69 DFlashModelTypes = Literal["dflash"]
70 DSparkModelTypes = Literal["dspark"]
71 EagleModelTypes = Literal[
72 "eagle", "eagle3", "extract_hidden_states", MTPModelTypes, DFlashModelTypes
73 ]
74 SpeculativeMethod = Literal[
75 "ngram",
76 "medusa",
77 "mlp_speculator",
78 "draft_model",
79 "suffix",
80 "custom_class",
81 EagleModelTypes,
82 NgramGPUTypes,
83 DSparkModelTypes,
84 ]
85 RejectionSampleMethod = Literal["standard", "synthetic", "block"]
86 DraftSampleMethod = Literal["greedy", "probabilistic"]
87
88 _QWEN3_OMNI_TARGET_ARCHITECTURES = frozenset(
89 {
90 "Qwen3OmniMoeForConditionalGeneration",
91 "Qwen3OmniMoeThinkerForConditionalGeneration",
92 }
93 )
94 _QWEN3_OMNI_DSPARK_ARCHITECTURE = "Qwen3OmniDSparkModel"
95
96
97 def _is_qwen3_omni_target(model_config: ModelConfig) -> bool:
98 hf_config = model_config.hf_config
99 architectures = set(getattr(model_config, "architectures", ()) or ())
100 architectures.update(getattr(hf_config, "architectures", ()) or ())
101 return getattr(hf_config, "model_type", None) == "qwen3_omni_moe" or bool(
102 architectures & _QWEN3_OMNI_TARGET_ARCHITECTURES
103 )
104
105
106 def _get_qwen3_omni_text_config(model_config: ModelConfig) -> Any:
107 thinker_config = getattr(model_config.hf_config, "thinker_config", None)
108 text_config = getattr(thinker_config, "text_config", None)
109 if text_config is None:
110 text_config = getattr(model_config, "hf_text_config", None)
111 return text_config
112
113
114 def _get_attention_head_dim(config: Any) -> int | None:
115 head_dim = getattr(config, "head_dim", None)
116 if head_dim is not None:
117 return head_dim
118 hidden_size = getattr(config, "hidden_size", None)
119 num_attention_heads = getattr(config, "num_attention_heads", None)
120 if (
121 isinstance(hidden_size, int)
122 and isinstance(num_attention_heads, int)
123 and num_attention_heads > 0
124 and hidden_size % num_attention_heads == 0
125 ):
126 return hidden_size // num_attention_heads
127 return None
128
129
130 def _get_nested_config_value(config: Any, section: str, name: str) -> Any:
131 nested_config = getattr(config, section, None)
132 if isinstance(nested_config, Mapping):
133 return nested_config.get(name)
134 return getattr(nested_config, name, None)
135
136
137 def _get_qwen3_dspark_value(config: Any, name: str) -> Any:
138 # Match DFlashQwen3Model's precedence: dflash_config overrides
139 # eagle_config, and top-level values are the modern fallback.
140 value = _get_nested_config_value(config, "dflash_config", name)
141 if value is None:
142 value = _get_nested_config_value(config, "eagle_config", name)
143 if value is None:
144 value = getattr(config, name, None)
145 return value
146
147
148 def _validate_qwen3_omni_dspark(
149 target_model_config: ModelConfig,
150 draft_model_config: ModelConfig,
151 num_speculative_tokens: int,
152 ) -> None:
153 """Validate the standalone Qwen3-Omni DSpark checkpoint contract."""
154 if not _is_qwen3_omni_target(target_model_config):
155 return
156
157 draft_hf_config = draft_model_config.hf_config
158 draft_architectures = set(getattr(draft_model_config, "architectures", ()) or ())
159 draft_architectures.update(getattr(draft_hf_config, "architectures", ()) or ())
160 if _QWEN3_OMNI_DSPARK_ARCHITECTURE not in draft_architectures:
161 raise ValueError(
162 "Qwen3-Omni DSpark requires a standalone draft checkpoint with "
163 f"architectures=['{_QWEN3_OMNI_DSPARK_ARCHITECTURE}']; generic "
164 "Qwen3DSparkModel checkpoints must be converted first."
165 )
166
167 block_size = _get_qwen3_dspark_value(draft_hf_config, "block_size")
168 if block_size is None:
169 block_size = _get_qwen3_dspark_value(draft_hf_config, "dspark_block_size")
170 if (
171 not isinstance(block_size, int)
172 or isinstance(block_size, bool)
173 or block_size <= 0
174 ):
175 raise ValueError(
176 "Qwen3-Omni DSpark requires a positive integer block_size in the "
177 "draft config."
178 )
179 if num_speculative_tokens != block_size:
180 raise ValueError(
181 "Qwen3-Omni DSpark requires num_speculative_tokens to match the "
182 f"trained block_size ({block_size}); got {num_speculative_tokens}."
183 )
184
185 target_text_config = _get_qwen3_omni_text_config(target_model_config)
186 if target_text_config is None:
187 raise ValueError(
188 "Qwen3-Omni DSpark could not resolve the target thinker text_config."
189 )
190 target_hidden_size = getattr(
191 target_text_config,
192 "hidden_size",
193 target_model_config.get_hidden_size(),
194 )
195 draft_target_hidden_size = getattr(draft_hf_config, "target_hidden_size", None)
196 if draft_target_hidden_size != target_hidden_size:
197 raise ValueError(
198 "Qwen3-Omni DSpark draft target_hidden_size must match the target "
199 f"text hidden size ({target_hidden_size}); got {draft_target_hidden_size}."
200 )
201 draft_hidden_size = getattr(draft_hf_config, "hidden_size", None)
202 if draft_hidden_size != target_hidden_size:
203 raise ValueError(
204 "Qwen3-Omni DSpark draft hidden_size must match the target thinker "
205 f"hidden size ({target_hidden_size}); got {draft_hidden_size}."
206 )
207
208 target_attention = {
209 "num_attention_heads": getattr(target_text_config, "num_attention_heads", None),
210 "num_key_value_heads": getattr(
211 target_text_config,
212 "num_key_value_heads",
213 getattr(target_text_config, "num_attention_heads", None),
214 ),
215 "head_dim": _get_attention_head_dim(target_text_config),
216 }
217 draft_attention = {
218 "num_attention_heads": getattr(draft_hf_config, "num_attention_heads", None),
219 "num_key_value_heads": getattr(
220 draft_hf_config,
221 "num_key_value_heads",
222 getattr(draft_hf_config, "num_attention_heads", None),
223 ),
224 "head_dim": _get_attention_head_dim(draft_hf_config),
225 }
226 for name, target_value in target_attention.items():
227 if (
228 not isinstance(target_value, int)
229 or isinstance(target_value, bool)
230 or target_value <= 0
231 ):
232 raise ValueError(
233 "Qwen3-Omni DSpark requires a positive integer "
234 f"{name} in the target thinker text_config; got {target_value}."
235 )
236 draft_value = draft_attention[name]
237 if draft_value != target_value:
238 raise ValueError(
239 f"Qwen3-Omni DSpark draft {name} must match the target thinker "
240 f"value ({target_value}); got {draft_value}."
241 )
242
243 target_layer_ids = _get_nested_config_value(
244 draft_hf_config, "dflash_config", "target_layer_ids"
245 )
246 if target_layer_ids is None:
247 target_layer_ids = getattr(draft_hf_config, "dspark_target_layer_ids", None)
248 if target_layer_ids is None:
249 target_layer_ids = getattr(draft_hf_config, "target_layer_ids", None)
250 if not isinstance(target_layer_ids, (list, tuple)) or not target_layer_ids:
251 raise ValueError(
252 "Qwen3-Omni DSpark requires a non-empty target_layer_ids list in "
253 "the draft config."
254 )
255 if any(
256 not isinstance(layer_id, int) or isinstance(layer_id, bool)
257 for layer_id in target_layer_ids
258 ):
259 raise ValueError(
260 "Qwen3-Omni DSpark target_layer_ids must contain only integers."
261 )
262 if list(target_layer_ids) != sorted(set(target_layer_ids)):
263 raise ValueError(
264 "Qwen3-Omni DSpark target_layer_ids must be unique and strictly increasing."
265 )
266 target_num_layers = getattr(
267 target_text_config,
268 "num_hidden_layers",
269 target_model_config.get_total_num_hidden_layers(),
270 )
271 if target_layer_ids[0] < 0 or target_layer_ids[-1] >= target_num_layers:
272 raise ValueError(
273 "Qwen3-Omni DSpark target_layer_ids must be zero-based text-layer "
274 f"indices in [0, {target_num_layers - 1}]; got {target_layer_ids}."
275 )
276
277 if _get_qwen3_dspark_value(draft_hf_config, "use_aux_hidden_state") is not True:
278 raise ValueError(
279 "Qwen3-Omni DSpark requires use_aux_hidden_state=true because Omni "
280 "conditioning is supplied through thinker hidden states."
281 )
282
283 markov_rank = getattr(draft_hf_config, "markov_rank", None)
284 if (
285 not isinstance(markov_rank, int)
286 or isinstance(markov_rank, bool)
287 or markov_rank <= 0
288 ):
289 raise ValueError(
290 "Qwen3-Omni DSpark requires a positive integer markov_rank in the "
291 "draft config."
292 )
293 markov_head_type = getattr(draft_hf_config, "markov_head_type", None)
294 if markov_head_type != "vanilla":
295 raise ValueError(
296 "Qwen3-Omni DSpark currently requires markov_head_type='vanilla'; "
297 f"got {markov_head_type!r}."
298 )
299
300 sample_from_anchor = getattr(draft_hf_config, "sample_from_anchor", True)
301 bonus_anchor = getattr(draft_hf_config, "dspark_bonus_anchor", False)
302 if sample_from_anchor is not True or bonus_anchor is not False:
303 raise ValueError(
304 "Qwen3-Omni DSpark requires sample_from_anchor=true and "
305 "dspark_bonus_anchor=false."
306 )
307
308 target_vocab_size = getattr(
309 target_text_config,
310 "vocab_size",
311 target_model_config.get_vocab_size(),
312 )
313 draft_input_vocab_size = getattr(draft_hf_config, "vocab_size", None)
314 if (
315 not isinstance(draft_input_vocab_size, int)
316 or isinstance(draft_input_vocab_size, bool)
317 or draft_input_vocab_size <= 0
318 ):
319 raise ValueError(
320 "Qwen3-Omni DSpark input vocab_size must be a positive integer; "
321 f"got {draft_input_vocab_size}."
322 )
323 draft_output_vocab_size = getattr(draft_hf_config, "draft_vocab_size", None)
324 if draft_output_vocab_size is None:
325 draft_output_vocab_size = draft_input_vocab_size
326 if (
327 not isinstance(draft_output_vocab_size, int)
328 or isinstance(draft_output_vocab_size, bool)
329 or not 0 < draft_output_vocab_size <= target_vocab_size
330 ):
331 raise ValueError(
332 "Qwen3-Omni DSpark draft_vocab_size must be a positive integer no "
333 f"larger than target vocab_size ({target_vocab_size}); got "
334 f"{draft_output_vocab_size}."
335 )
336
337 noise_token_id = _get_nested_config_value(
338 draft_hf_config, "dflash_config", "mask_token_id"
339 )
340 for name in (
341 "mask_token_id",
342 "dspark_noise_token_id",
343 "pard_token",
344 "ptd_token_id",
345 ):
346 if noise_token_id is None:
347 noise_token_id = getattr(draft_hf_config, name, None)
348 if noise_token_id is not None:
349 break
350 if (
351 not isinstance(noise_token_id, int)
352 or isinstance(noise_token_id, bool)
353 or not 0 <= noise_token_id < draft_input_vocab_size
354 ):
355 raise ValueError(
356 "Qwen3-Omni DSpark requires a valid mask/noise token id within the "
357 f"draft input vocabulary [0, {draft_input_vocab_size - 1}]."
358 )
359
360 rope_configs = (
361 getattr(draft_hf_config, "rope_parameters", None),
362 getattr(draft_hf_config, "rope_scaling", None),
363 )
364 has_mrope = getattr(draft_hf_config, "mrope_section", None) is not None
365 has_mrope = has_mrope or any(
366 isinstance(rope_config, Mapping) and "mrope_section" in rope_config
367 for rope_config in rope_configs
368 )
369 if has_mrope:
370 raise ValueError(
371 "Qwen3-Omni DSpark draft checkpoints must use logical 1-D RoPE and "
372 "must not define mrope_section."
373 )
374
375
376 # (SpeculativeConfig field, VllmConfig sub-config, overridden field)
377 _DRAFT_VLLM_CONFIG_OVERRIDES = (
378 # Otherwise the draft inherits the target's --moe-backend, which fails
379 # when the draft is unquantized and that backend is not.
380 ("moe_backend", "kernel_config", "moe_backend"),
381 # Only when set, so the draft keeps a KV cache layout the target shares.
382 ("attention_backend", "attention_config", "backend"),
383 ("kv_cache_dtype", "cache_config", "cache_dtype"),
384 )
385
386
387 @config
388 class SpeculativeConfig:
389 """Configuration for speculative decoding."""
390
391 enforce_eager: bool | None = None
392 """Override the default enforce_eager from model_config"""
393 # General speculative decoding control
394 num_speculative_tokens: int = Field(default=None, gt=0) # type: ignore[assignment]
395 """The number of speculative tokens, if provided. It will default to the
396 number in the draft model config if present, otherwise, it is required."""
397 model: str | None = None
398 """The name of the draft model, eagle head, or additional weights, if
399 provided."""
400 method: SpeculativeMethod | None = None
401 """The name of the speculative method to use. If users provide and set the
402 `model` param, the speculative method type will be detected automatically
403 if possible, if `model` param is not provided, the method name must be
404 provided.
405
406 If using `ngram` method, the related configuration `prompt_lookup_max` and
407 `prompt_lookup_min` should be considered."""
408 draft_tensor_parallel_size: int | None = Field(default=None, ge=1)
409 """The degree of the tensor parallelism for the draft model. Can only be 1
410 or the same as the target model's tensor parallel size."""
411 tensor_parallel_size: int | None = None
412 """Users should pass "draft_tensor_parallel_size". This parameter's purpose is to
413 warn users when they mistakenly provide the wrong argument."""
414
415 # Draft model configuration
416 quantization: me_quant.QuantizationMethods | str | None = None
417 """Quantization method that was used to quantize the draft model weights.
418 If `None`, we assume the model weights are not quantized. Note that it only
419 takes effect when using the draft model-based speculative method."""
420 moe_backend: MoEBackend | None = None
421 """MoE backend to use for the draft model. When `None`, the draft model
422 inherits the target model's `--moe-backend` setting. Useful when the
423 drafter and generator require different MoE kernels (e.g. quantized
424 generator with unquantized drafter)."""
425 attention_backend: AttentionBackendEnum | None = None
426 """Attention backend to use for the draft model. When `None`, the backend is
427 automatically selected. Useful when the drafter requires a different attention
428 backend (e.g. DFlash needs a backend that supports non-causal attention)."""
429 kv_cache_dtype: CacheDType | None = None
430 """KV cache dtype for the draft model. When `None`, the draft inherits the
431 target model's `--kv-cache-dtype`."""
432 max_model_len: int | None = Field(default=None, ge=1)
433 """The maximum model length of the draft model. Used when testing the
434 ability to skip speculation for some sequences."""
435 revision: str | None = None
436 """The specific model version to use for the draft model. It can be a
437 branch name, a tag name, or a commit id. If unspecified, will use the
438 default version."""
439 code_revision: str | None = None
440 """The specific revision to use for the draft model code on Hugging Face
441 Hub. It can be a branch name, a tag name, or a commit id. If unspecified,
442 will use the default version."""
443 index_share_for_mtp_iteration: bool | None = None
444 """Override whether MTP iterations reuse the first step's sparse indices.
445 If `None`, use the value from the draft model's Hugging Face config."""
446
447 # Advanced control
448 disable_padded_drafter_batch: bool = False
449 """Disable input padding for speculative decoding. If set to True,
450 speculative input batches can contain sequences of different lengths,
451 which may only be supported by certain attention backends. This currently
452 only affects the EAGLE method of speculation."""
453 disable_eagle_block_drop: bool = False
454 """Disable dropping the trailing prefix-cache block for EAGLE-like
455 speculative methods. This is an experimental option for measuring the
456 acceptance-rate impact of reusing that block. It does not disable the
457 speculative drafter itself."""
458 use_local_argmax_reduction: bool = False
459 """Use vocab-parallel local argmax instead of all-gathering full logits
460 for draft token generation. Reduces communication from O(vocab_size) to
461 O(2 * tp_size) per token. Only applies to greedy draft selection in
462 non-tree speculation."""
463
464 use_heterogeneous_vocab: bool = False
465 """Allow draft and target models to use different vocabularies.
466 When enabled, builds a token-level intersection at init and constrains
467 draft logits to shared tokens only (TLI algorithm). Requires
468 method='draft_model'."""
469
470 # Ngram proposer configuration
471 prompt_lookup_max: int | None = Field(default=None, ge=1)
472 """Maximum size of ngram token window when using Ngram proposer, required
473 when method is set to ngram."""
474 prompt_lookup_min: int | None = Field(default=None, ge=1)
475 """Minimum size of ngram token window when using Ngram proposer, if
476 provided. Defaults to 1."""
477
478 # Alternative drafting strategies
479 parallel_drafting: bool = False
480 """Enable parallel drafting, where all speculative tokens are generated
481 in parallel rather than sequentially. This can improve performance but
482 requires the speculative model be trained to support parallel drafting.
483 Only compatible with EAGLE and draft model methods."""
484
485 # required configuration params passed from engine
486 target_model_config: SkipValidation[ModelConfig] = None # type: ignore
487 """The configuration of the target model."""
488 target_parallel_config: SkipValidation[ParallelConfig] = None # type: ignore
489 """The parallel configuration for the target model."""
490
491 # dynamic speculative decoding control
492 num_speculative_tokens_per_batch_size: list[tuple[int, int, int]] | None = None
493 """Batch-size schedule used to dynamically choose speculative-token count.
494
495 Each entry is ``(range_start, range_end, num_speculative_tokens)`` with an
496 inclusive batch-size range.
497 """
498
499 # params generated in the post-init stage
500 draft_model_config: SkipValidation[ModelConfig] = None # type: ignore
501 """The configuration of the draft model initialized internal."""
502 draft_parallel_config: SkipValidation[ParallelConfig] = None # type: ignore
503 """The parallel configuration for the draft model initialized internal."""
504
505 # Suffix decoding configuration
506 suffix_decoding_max_tree_depth: int = 24
507 """The maximum depth of the suffix decoding global and prompt trees. The
508 tree depth limits the sum of the prefix match and speculation lengths."""
509
510 suffix_decoding_max_cached_requests: int = 10000
511 """The maximum number of requests to cache in the global suffix tree. If
512 exceeded, will trigger eviction in FIFO order. If set to 0, the global
513 suffix tree is disabled and past responses are not cached (prompt trees
514 are still used)."""
515
516 suffix_decoding_max_spec_factor: float = 1.0
517 """The maximum spec factor for suffix decoding. The spec factor controls
518 speculation lengths based on the prefix match length: max_spec_tokens =
519 max_spec_factor * prefix_match_length."""
520
521 suffix_decoding_min_token_prob: float = 0.1
522 """The minimum token probability for suffix decoding. Will only speculate
523 tokens with estimated probability (based on frequency counts) greater than
524 or equal to this value."""
525
526 draft_load_config: LoadConfig | None = None
527 """Load config for the draft model. If not specified, will use the load
528 config from the target model."""
529
530 rejection_sample_method: RejectionSampleMethod = "standard"
531 """The rejection sampling method to use. 'standard' uses probabilistic
532 rejection sampling (with or without cached draft logits, controlled by
533 draft_sample_method). 'synthetic' accepts draft tokens with a decaying
534 probability calibrated to synthetic_acceptance_rate. 'block' uses block
535 verification (Sun et al.), which jointly verifies the draft tokens as a
536 block instead of one at a time."""
537
538 synthetic_acceptance_rates: list[float] | None = None
539 """Per-position *unconditional* acceptance rates for synthetic rejection
540 sampling. Position i's entry is the marginal probability that the first
541 i+1 draft tokens are all accepted; the list must have length
542 num_speculative_tokens, each entry in [0, 1], and be monotonically
543 non-increasing. Only valid when rejection_sample_method is 'synthetic'.
544 Mutually exclusive with synthetic_acceptance_length."""
545
546 synthetic_acceptance_length: float | None = None
547 """Target mean acceptance length for synthetic rejection sampling, in
548 [1, num_speculative_tokens + 1]. Resolved internally to
549 synthetic_acceptance_rates. Only valid when rejection_sample_method is 'synthetic'.
550 Mutually exclusive with synthetic_acceptance_rates."""
551
552 enable_adaptive_verification: bool = False
553 """Whether to adaptively size the draft-verification budget from per-request
554 confidence. Currently only supported for method="dspark"."""
555
556 @staticmethod
557 def _acceptance_length_to_rates(length: float, n: int) -> list[float]:
558 """Mean acceptance length to unconditional per-position rates, using
559 the minimum-variance schedule."""
560 num_drafts = length - 1 # expected number of accepted draft tokens
561 num_full = int(num_drafts)
562 return (
563 [1.0] * num_full + [num_drafts - num_full] + [0.0] * (n - num_full - 1)
564 )[:n]
565
566 @staticmethod
567 def _resolve_synthetic_acceptance_rates(
568 n: int,
569 rates: list[float] | None,
570 length: float | None,
571 ) -> list[float]:
572 """Return per-position unconditional acceptance rates from exactly one
573 of `rates` or `length` (validates range, length, and monotonicity)."""
574 if (rates is None) == (length is None):
575 raise ValueError(
576 "rejection_sample_method='synthetic' requires exactly one of "
577 "synthetic_acceptance_rates or synthetic_acceptance_length."
578 )
579 if rates is not None:
580 if len(rates) != n:
581 raise ValueError(
582 f"synthetic_acceptance_rates must have length {n}, got {rates}."
583 )
584 if not all(0.0 <= r <= 1.0 for r in rates):
585 raise ValueError(
586 f"synthetic_acceptance_rates entries must be in [0, 1], "
587 f"got {rates}."
588 )
589 if any(rates[i] > rates[i - 1] for i in range(1, n)):
590 raise ValueError(
591 f"synthetic_acceptance_rates must be non-increasing, got {rates}."
592 )
593 return list(rates)
594 assert length is not None
595 if not 1.0 <= length <= float(n + 1):
596 raise ValueError(
597 f"synthetic_acceptance_length must be in [1, {n + 1}], got {length}."
598 )
599 return SpeculativeConfig._acceptance_length_to_rates(length, n)
600
601 draft_sample_method: DraftSampleMethod = "greedy"
602 """How the draft model samples tokens. 'greedy' always picks the argmax
603 token, and the draft probabilities are treated as one-hot during rejection
604 sampling. 'probabilistic' samples stochastically from the draft
605 distribution and uses the full draft logits for the probability ratio test
606 during rejection sampling. This comes at the cost of additional GPU memory
607 usage."""
608
609 dspark_draft_topk: int | None = Field(default=None, ge=1)
610 """For Qwen3 DSpark drafting, evaluate the Markov projection only for the
611 top-k base-logit candidates. Requires draft tensor parallel size 1."""
612
613 def compute_hash(self) -> str:
614 """WARNING: Whenever a new field is added to this config,
615 ensure that it is included in the factors list if
616 it affects the computation graph.
617
618 Provide a hash that uniquely identifies all the configs
619 that affect the structure of the computation
620 graph from input ids/embeddings to the final hidden states,
621 excluding anything before input ids/embeddings and after
622 the final hidden states.
623 """
624 factors: list[Any] = []
625 # Eagle3 and extract_hidden_states affect the computation graph because
626 # they return intermediate hidden states in addition to the final hidden state.
627 uses_aux_hidden_states = self.method in (
628 "eagle3",
629 "extract_hidden_states",
630 "dflash",
631 "dspark",
632 )
633 factors.append(uses_aux_hidden_states)
634 if self.method == "dspark":
635 factors.append(self.enable_adaptive_verification)
636
637 if self.draft_model_config is not None:
638 factors.append(self.draft_model_config.compute_hash())
639 # The specific layers used also affect the computation graph.
640 layer_ids = getattr(
641 self.draft_model_config.hf_config,
642 "eagle_aux_hidden_state_layer_ids",
643 None,
644 )
645 if layer_ids is not None and uses_aux_hidden_states:
646 # Convert to tuple to make it hashable
647 factors.append(tuple(layer_ids))
648
649 if self.method == "mtp" and self.draft_model_config is not None:
650 factors.append(
651 getattr(
652 self.draft_model_config.hf_config,
653 "index_share_for_mtp_iteration",
654 False,
655 )
656 )
657
658 hash_str = safe_hash(str(factors).encode(), usedforsecurity=False).hexdigest()
659 return hash_str
660
661 @staticmethod
662 def hf_config_override(hf_config: PreTrainedConfig) -> PreTrainedConfig:
663 initial_architecture = hf_config.architectures[0]
664 use_v32_mtp = hf_config.model_type in ("deepseek_v32", "glm_moe_dsa")
665 if hf_config.model_type == "dots3_note":
666 n_predict = getattr(hf_config, "num_nextn_predict_layers", 1)
667 mtp_layer_types = getattr(hf_config, "mtp_layer_types", None)
668 if mtp_layer_types is None:
669 mtp_layer_types = ["sliding_attention"] * n_predict
670 if len(mtp_layer_types) != n_predict:
671 raise ValueError(
672 "mtp_layer_types must have one entry per MTP layer: "
673 f"got {len(mtp_layer_types)} for {n_predict} layers"
674 )
675 hf_config.layer_types = [*hf_config.layer_types, *mtp_layer_types]
676 hf_config.model_type = "dots3_note_mtp"
677 hf_config.update(
678 {"n_predict": n_predict, "architectures": ["Dots3NoteMTPModel"]}
679 )
680 if hf_config.model_type in (
681 "deepseek_v3",
682 "deepseek_v32",
683 "glm_moe_dsa",
684 ):
685 hf_config.model_type = "deepseek_mtp"
686 if hf_config.model_type == "deepseek_mtp":
687 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
688 hf_config.update(
689 {
690 "n_predict": n_predict,
691 "architectures": [
692 "DeepseekV32MTPModel" if use_v32_mtp else "DeepSeekMTPModel"
693 ],
694 }
695 )
696 if hf_config.model_type in ("deepseek_v4", "deepseek_v41"):
697 # V4.1 has no classic-MTP draft: its checkpoints ship DSpark stages
698 # under ``mtp.*``, so only V4 gets an MTP architecture here. The
699 # DSpark path rewrites ``architectures`` itself;
700 # ``method="mtp"`` on V4.1 is rejected below.
701 is_v41 = hf_config.model_type == "deepseek_v41"
702 hf_config.model_type = "deepseek_mtp"
703 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
704 overrides = {"n_predict": n_predict}
705 if not is_v41:
706 overrides["architectures"] = ["DeepSeekV4MTPModel"]
707 hf_config.update(overrides)
708 if hf_config.model_type in ("pangu_ultra_moe"):
709 hf_config.model_type = "pangu_ultra_moe_mtp"
710 if hf_config.model_type == "pangu_ultra_moe_mtp":
711 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
712 hf_config.update(
713 {"n_predict": n_predict, "architectures": ["OpenPanguMTPModel"]}
714 )
715
716 if hf_config.model_type == "kimi_k3":
717 # Kimi-K3 keeps the text-model fields (incl. the MTP layer count)
718 # nested under ``text_config`` (a KimiLinearConfig).
719 text_config = getattr(hf_config, "text_config", hf_config)
720 n_predict = getattr(text_config, "num_nextn_predict_layers", None)
721 hf_config.model_type = "kimi_k3_mtp"
722 hf_config.update(
723 {"n_predict": n_predict, "architectures": ["KimiK3MTPModel"]}
724 )
725
726 if hf_config.architectures[0] == "MiMoForCausalLM":
727 hf_config.model_type = "mimo_mtp"
728 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
729 hf_config.update(
730 {
731 "num_hidden_layers": 0,
732 "n_predict": n_predict,
733 "architectures": ["MiMoMTPModel"],
734 }
735 )
736
737 if (arch := hf_config.architectures[0]) in (
738 "MiMoV2ForCausalLM",
739 "MiMoV2OmniForCausalLM",
740 ):
741 from vllm.model_executor.models.mimo_v2_mtp import (
742 _MIMO_V2_PRO_NUM_MTP_LAYERS,
743 )
744
745 mtp_arch_maps = {
746 "MiMoV2ForCausalLM": "MiMoV2MTPModel",
747 "MiMoV2OmniForCausalLM": "MiMoV2OmniMTPModel",
748 }
749
750 hf_config.model_type = "mimo_v2_mtp"
751 # vLLM currently supports only the first MiMo-V2 MTP layer.
752 n_predict = _MIMO_V2_PRO_NUM_MTP_LAYERS
753 hf_config.update(
754 {
755 "num_hidden_layers": 0,
756 "n_predict": n_predict,
757 "num_nextn_predict_layers": n_predict,
758 "architectures": [mtp_arch_maps[arch]],
759 }
760 )
761
762 if hf_config.architectures[0] == "MiMoV2FlashForCausalLM":
763 from vllm.model_executor.models.mimo_v2_mtp import (
764 _MIMO_V2_FLASH_NUM_MTP_LAYERS,
765 )
766
767 hf_config.model_type = "mimo_v2_mtp"
768 # vLLM currently supports only the first MiMo-V2 MTP layer.
769 n_predict = _MIMO_V2_FLASH_NUM_MTP_LAYERS
770 hf_config.update(
771 {
772 "num_hidden_layers": 0,
773 "n_predict": n_predict,
774 "num_nextn_predict_layers": n_predict,
775 "architectures": ["MiMoV2MTPModel"],
776 }
777 )
778
779 if hf_config.architectures[0] == "Glm4MoeForCausalLM":
780 hf_config.model_type = "glm4_moe_mtp"
781 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
782 hf_config.update(
783 {
784 "n_predict": n_predict,
785 "architectures": ["Glm4MoeMTPModel"],
786 }
787 )
788
789 if hf_config.architectures[0] == "Glm4MoeLiteForCausalLM":
790 hf_config.model_type = "glm4_moe_lite_mtp"
791 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
792 hf_config.update(
793 {
794 "num_hidden_layers": 0,
795 "n_predict": n_predict,
796 "architectures": ["Glm4MoeLiteMTPModel"],
797 }
798 )
799
800 if hf_config.architectures[0] == "GlmOcrForConditionalGeneration":
801 hf_config.model_type = "glm_ocr_mtp"
802 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
803 hf_config.update(
804 {
805 "num_hidden_layers": 0,
806 "n_predict": n_predict,
807 "architectures": ["GlmOcrMTPModel"],
808 }
809 )
810
811 if hf_config.model_type == "ernie4_5_moe":
812 hf_config.model_type = "ernie_mtp"
813 if hf_config.model_type == "ernie_mtp":
814 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
815 hf_config.update(
816 {"n_predict": n_predict, "architectures": ["ErnieMTPModel"]}
817 )
818
819 if hf_config.architectures[0] in (
820 "NemotronH_Super_Omni_Reasoning_V3",
821 "NemotronH_Omni_Reasoning_V3",
822 ):
823 # Promote VLM's text_config so MTP detection below fires correctly
824 hf_config = hf_config.text_config
825
826 if (
827 hf_config.model_type in {"nemotron_h", "nemotron_h_puzzle"}
828 and hasattr(hf_config, "num_nextn_predict_layers")
829 and hf_config.num_nextn_predict_layers > 0
830 ):
831 # Check if this is an MTP variant
832 hf_config.model_type = "nemotron_h_mtp"
833 if hf_config.model_type == "nemotron_h_mtp":
834 n_predict = getattr(hf_config, "num_nextn_predict_layers", 1)
835 hf_config.update(
836 {"n_predict": n_predict, "architectures": ["NemotronHMTPModel"]}
837 )
838
839 if hf_config.model_type == "qwen3_next":
840 hf_config.model_type = "qwen3_next_mtp"
841 if hf_config.model_type == "qwen3_next_mtp":
842 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
843 hf_config.update(
844 {"n_predict": n_predict, "architectures": ["Qwen3NextMTP"]}
845 )
846 if hf_config.model_type in {"qwen4_exp", "qwen4_exp_text"}:
847 hf_config.model_type = "qwen4_exp_mtp"
848 if hf_config.model_type == "qwen4_exp_mtp":
849 text_config = get_hf_text_config(hf_config)
850 n_predict = getattr(
851 text_config,
852 "mtp_num_hidden_layers",
853 getattr(text_config, "num_nextn_predict_layers", None),
854 )
855 share_mtp_indices = getattr(
856 text_config, "index_share_for_mtp_iteration", False
857 )
858 hf_config.update(
859 {
860 # hc_count is the HC stream multiplier for Qwen MTP feedback.
861 "hc_mult": int(text_config.hc_count),
862 "n_predict": n_predict,
863 "architectures": ["Qwen4ExpMTP"],
864 "index_share_for_mtp_iteration": share_mtp_indices,
865 }
866 )
867
868 architectures = getattr(hf_config, "architectures", []) or []
869 if initial_architecture == "BailingMoeV3VLForConditionalGeneration":
870 quantization_config = getattr(hf_config, "quantization_config", None)
871 hf_config = copy.deepcopy(hf_config.text_config)
872 if (
873 quantization_config is not None
874 and getattr(hf_config, "quantization_config", None) is None
875 ):
876 hf_config.quantization_config = copy.deepcopy(quantization_config)
877 if initial_architecture in (
878 "BailingMoeV3ForCausalLM",
879 "BailingMoeV3VLForConditionalGeneration",
880 ):
881 hf_config.model_type = "bailing_hybrid_v3_mtp"
882 elif (
883 hf_config.model_type == "bailing_hybrid"
884 or "BailingMoeV2_5ForCausalLM" in architectures
885 ):
886 hf_config.model_type = "bailing_hybrid_mtp"
887 if hf_config.model_type == "bailing_hybrid_mtp":
888 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
889 hf_config.update(
890 {
891 "n_predict": n_predict,
892 "architectures": ["BailingMoeV25MTPModel"],
893 }
894 )
895 if hf_config.model_type == "bailing_hybrid_v3_mtp":
896 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
897 hf_config.update(
898 {
899 "n_predict": n_predict,
900 "architectures": ["BailingMoeV3MTPModel"],
901 }
902 )
903
904 if hf_config.model_type == "exaone_moe":
905 hf_config.model_type = "exaone_moe_mtp"
906 if hf_config.model_type == "exaone_moe_mtp":
907 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
908 hf_config.update(
909 {"n_predict": n_predict, "architectures": ["ExaoneMoeMTP"]}
910 )
911 if "exaone4_5" in hf_config.model_type:
912 hf_config.model_type = "exaone4_5_mtp"
913 if hf_config.model_type == "exaone4_5_mtp":
914 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
915 hf_config.update(
916 {"n_predict": n_predict, "architectures": ["Exaone4_5_MTP"]}
917 )
918 if hf_config.model_type in (
919 "qwen3_5",
920 "qwen3_5_moe",
921 "qwen3_5_text",
922 "qwen3_5_moe_text",
923 ):
924 # Checkpoints that ship only the text config resolve to the
925 # `qwen3_5_text` / `qwen3_5_moe_text` model types and carry the
926 # same `mtp_num_hidden_layers` field as the multimodal ones.
927 is_moe = hf_config.model_type in ("qwen3_5_moe", "qwen3_5_moe_text")
928 hf_config.model_type = "qwen3_5_mtp"
929 n_predict = getattr(hf_config, "mtp_num_hidden_layers", None)
930 if n_predict is None:
931 text_config = get_hf_text_config(hf_config)
932 n_predict = getattr(text_config, "mtp_num_hidden_layers", None)
933 hf_config.update(
934 {
935 "n_predict": n_predict,
936 "architectures": ["Qwen3_5MoeMTP" if is_moe else "Qwen3_5MTP"],
937 }
938 )
939 if hf_config.model_type in ("intern_s2_preview", "interns2_mobius"):
940 is_mobius = hf_config.model_type == "interns2_mobius"
941 text_config = getattr(hf_config, "text_config", None)
942 is_moe = is_mobius or (
943 getattr(text_config, "model_type", None) == "qwen3_5_moe_text"
944 )
945 if is_mobius:
946 assert text_config is not None
947 text_config.model_type = "qwen3_5_moe_text"
948 text_config.num_experts = text_config.mtp_num_experts
949 text_config.num_experts_per_tok = text_config.mtp_num_experts_per_tok
950 hf_config.model_type = "qwen3_5_mtp"
951 n_predict = getattr(text_config, "mtp_num_hidden_layers", None)
952 architecture = (
953 "InternS2MobiusMTP"
954 if is_mobius
955 else ("Qwen3_5MoeMTP" if is_moe else "Qwen3_5MTP")
956 )
957 hf_config.update(
958 {
959 "n_predict": n_predict,
960 "architectures": [architecture],
961 }
962 )
963 if hf_config.model_type in ("longcat_flash", "longcat_flash_ngram"):
964 hf_config.model_type = "longcat_flash_mtp"
965 n_predict = getattr(hf_config, "num_nextn_predict_layers", 1)
966 hf_config.update(
967 {"n_predict": n_predict, "architectures": ["LongCatFlashMTPModel"]}
968 )
969
970 if hf_config.model_type in ("step3p5", "step3p7") or hf_config.architectures[
971 0
972 ] in ("Step3p5ForCausalLM", "Step3p7ForConditionalGeneration"):
973 quantization_config = getattr(hf_config, "quantization_config", None)
974 hf_config = getattr(hf_config, "text_config", hf_config)
975 if (
976 quantization_config is not None
977 and getattr(hf_config, "quantization_config", None) is None
978 ):
979 hf_config.update({"quantization_config": quantization_config})
980 hf_config.model_type = "step3p5_mtp"
981 n_predict = getattr(hf_config, "num_nextn_predict_layers", 1)
982 hf_config.update({"n_predict": n_predict, "architectures": ["Step3p5MTP"]})
983
984 if initial_architecture == "MistralLarge3ForCausalLM":
985 hf_config.update({"architectures": ["EagleMistralLarge3ForCausalLM"]})
986
987 if hf_config.model_type == "hy_v3":
988 hf_config.model_type = "hy_v3_mtp"
989 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
990 hf_config.update(
991 {"n_predict": n_predict, "architectures": ["HYV3MTPModel"]}
992 )
993
994 if hf_config.model_type == "hy_v4":
995 hf_config.model_type = "hy_v4_mtp"
996 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
997 hf_config.update(
998 {"n_predict": n_predict, "architectures": ["HYV4MTPModel"]}
999 )
1000
1001 if hf_config.model_type in ("inkling_mm_model", "inkling_model"):
1002 mtp_config = getattr(hf_config, "mtp_config", None) or {}
1003 hf_config = getattr(hf_config, "text_config", hf_config)
1004 checkpoint_depths = mtp_config.get("num_nextn_predict_layers", 0)
1005 if checkpoint_depths < 1:
1006 raise ValueError("The Inkling checkpoint does not contain MTP weights")
1007 hf_config.model_type = "inkling_mtp"
1008 hf_config.update(
1009 {
1010 "n_predict": checkpoint_depths,
1011 "num_nextn_predict_layers": checkpoint_depths,
1012 "chain_hidden_post_norm": mtp_config.get(
1013 "chain_hidden_post_norm", False
1014 ),
1015 "local_layer_ids": mtp_config.get("local_layer_ids", []),
1016 "architectures": ["InklingMTPModel"],
1017 }
1018 )
1019
1020 if hf_config.model_type in ("gemma4_assistant", "gemma4_unified_assistant"):
1021 hf_config.model_type = "gemma4_mtp"
1022 text_config = getattr(hf_config, "text_config", hf_config)
1023 # The assistant runs all decoder layers in a single forward
1024 # call to produce one draft token, so n_predict=1.
1025 # num_kv_shared_layers must be 0: cross-model KV sharing is
1026 # set up by the proposer after model construction.
1027 if hasattr(text_config, "num_kv_shared_layers"):
1028 text_config.num_kv_shared_layers = 0
1029 hf_config.update({"n_predict": 1, "architectures": ["Gemma4MTPModel"]})
1030
1031 if (
1032 hf_config.model_type == "minimax_m3_vl"
1033 or initial_architecture == "MiniMaxM3SparseForConditionalGeneration"
1034 ):
1035 # MTP modules live on the language model of this VL checkpoint, so
1036 # promote text_config before rewriting it into an MTP config.
1037 quantization_config = getattr(hf_config, "quantization_config", None)
1038 hf_config = getattr(hf_config, "text_config", hf_config)
1039 if (
1040 quantization_config is not None
1041 and getattr(hf_config, "quantization_config", None) is None
1042 ):
1043 hf_config.update({"quantization_config": quantization_config})
1044 hf_config.model_type = "minimax_m3_mtp"
1045 n_predict = getattr(hf_config, "num_mtp_modules", 1)
1046 hf_config.update(
1047 {"n_predict": n_predict, "architectures": ["MiniMaxM3MTP"]}
1048 )
1049 elif (
1050 hf_config.model_type == "minimax_m3_mtp"
1051 or initial_architecture == "MiniMaxM3MTP"
1052 ):
1053 # Standalone MTP checkpoints already use a flat MTP config with no
1054 # VL wrapper / text_config to promote, so just normalize the
1055 # architecture and derive n_predict from num_mtp_modules.
1056 n_predict = getattr(hf_config, "num_mtp_modules", 1)
1057 hf_config.update(
1058 {"n_predict": n_predict, "architectures": ["MiniMaxM3MTP"]}
1059 )
1060 if hf_config.model_type == "glm5_next":
1061 hf_config.model_type = "glm5_next_mtp"
1062 n_predict = hf_config.get_text_config().num_nextn_predict_layers
1063 hf_config.update(
1064 {"n_predict": n_predict, "architectures": ["Glm5NextMTPModel"]}
1065 )
1066
1067 return hf_config
1068
1069 @staticmethod
1070 def _apply_composed_hf_override(
1071 target_hf_overrides: Callable[[PreTrainedConfig], PreTrainedConfig],
1072 hf_config: PreTrainedConfig,
1073 ) -> PreTrainedConfig:
1074 hf_config = SpeculativeConfig.hf_config_override(hf_config)
1075 return target_hf_overrides(hf_config)
1076
1077 @staticmethod
1078 def compose_draft_hf_overrides(
1079 target_hf_overrides: HfOverrides | None,
1080 ) -> Callable[[PreTrainedConfig], PreTrainedConfig]:
1081 """Build the ``hf_overrides`` for the draft ``ModelConfig``.
1082
1083 Callable overrides on the target are config-to-config transforms
1084 (e.g. test harnesses shrinking ``num_hidden_layers``) and must also
1085 reach the draft config — otherwise a draft belonging to a large
1086 target is instantiated at full size even when the target is shrunk.
1087 Dict overrides are target-specific key patches and are not applied
1088 to the draft.
1089
1090 The composed override must stay picklable: the draft ``ModelConfig``
1091 is sent to spawned engine-core processes, so a local closure would
1092 fail with ``Can't get local object`` during pickling. Bind the
1093 target via ``functools.partial`` over a module-referenceable static
1094 method instead.
1095 """
1096 if not callable(target_hf_overrides):
1097 return SpeculativeConfig.hf_config_override
1098
1099 return functools.partial(
1100 SpeculativeConfig._apply_composed_hf_override, target_hf_overrides
1101 )
1102
1103 @staticmethod
1104 def _is_custom_proposer_path(model: str | None) -> bool:
1105 """True if ``model`` is a dotted import path (e.g. ``pkg.MyProposer``)."""
1106 if model is None:
1107 return False
1108 if model.startswith(("http://", "https://", "file://")):
1109 return False
1110 if "/" in model:
1111 return False
1112 parts = model.split(".")
1113 return len(parts) >= 2 and all(part.isidentifier() for part in parts)
1114
1115 def __post_init__(self):
1116 # Note: "method" is a new parameter that helps to extend the
1117 # configuration of non-model-based proposers, and the "model" parameter
1118 # will be used to set the draft model, eagle head, or additional weight
1119 # when needed. If users do not specify "method", the speculative method
1120 # will be detected automatically if possible. If the speculative method
1121 # can not be detected, it will be considered as the "draft_model" by
1122 # default.
1123
1124 # infer method from user args
1125 if self.method is None and SpeculativeConfig._is_custom_proposer_path(
1126 self.model
1127 ):
1128 self.method = "custom_class"
1129 elif self.method is None:
1130 if self.model in ("ngram", "[ngram]"):
1131 self.method = "ngram"
1132 else:
1133 self.method = "draft_model"
1134
1135 if self.method in get_args(MTPModelTypes) and self.method != "mtp":
1136 logger.warning(
1137 "method `%s` is deprecated and replaced with mtp.", self.method
1138 )
1139 self.method = "mtp"
1140
1141 if self.model is None and self.num_speculative_tokens is not None:
1142 if self.method == "mtp":
1143 if self.target_model_config is None:
1144 raise ValueError("target_model_config must be present for mtp")
1145 # use the draft model from the same model:
1146 # Use model_weights if set (e.g. runai_streamer replaces
1147 # model with a local cache dir but keeps the original path
1148 # in model_weights), so the draft model can load weights
1149 # from the same source as the target model.
1150 self.model = (
1151 self.target_model_config.model_weights
1152 or self.target_model_config.model
1153 )
1154 # Align the quantization of draft model for cases such as
1155 # --quantization fp8 with a bf16 checkpoint.
1156 if not self.quantization:
1157 self.quantization = self.target_model_config.quantization
1158 elif self.method == "dspark":
1159 # DeepSeek DSpark can ship the weights inside the target checkpoint
1160 if self.target_model_config is None:
1161 raise ValueError("target_model_config must be present for dspark")
1162 self.model = self.target_model_config.model
1163 if not self.quantization:
1164 self.quantization = self.target_model_config.quantization
1165 elif self.method in ("ngram", "[ngram]"):
1166 self.model = "ngram"
1167 elif self.method == "ngram_gpu":
1168 self.model = "ngram_gpu"
1169 elif self.method == "suffix":
1170 self.model = "suffix"
1171 elif self.method == "extract_hidden_states":
1172 self.model = "extract_hidden_states"
1173 elif self.method == "custom_class":
1174 # method was set explicitly, but model should already contain the
1175 # custom module path. If not, this is a configuration error.
1176 if self.model is None:
1177 raise ValueError(
1178 "method='custom_class' requires 'model' to contain the "
1179 "custom proposer module path (e.g., 'my_module.MyProposer')."
1180 )
1181 else:
1182 raise ValueError(
1183 "num_speculative_tokens was provided but without speculative model."
1184 )
1185
1186 if self.method in ("ngram", "[ngram]"):
1187 self.method = "ngram"
1188
1189 if self.method in ("ngram", "ngram_gpu"):
1190 # Set default values if not provided
1191 if self.prompt_lookup_min is None and self.prompt_lookup_max is None:
1192 # TODO(woosuk): Tune these values. They are arbitrarily chosen.
1193 self.prompt_lookup_min = 5
1194 self.prompt_lookup_max = 5
1195 elif self.prompt_lookup_min is None:
1196 if self.prompt_lookup_max is None:
1197 raise ValueError(
1198 "Either prompt_lookup_max or prompt_lookup_min must be "
1199 "provided when using the ngram method."
1200 )
1201 self.prompt_lookup_min = self.prompt_lookup_max
1202 elif self.prompt_lookup_max is None:
1203 if self.prompt_lookup_min is None:
1204 raise ValueError(
1205 "Either prompt_lookup_max or prompt_lookup_min must be "
1206 "provided when using the ngram method."
1207 )
1208 self.prompt_lookup_max = self.prompt_lookup_min
1209
1210 # Validate values
1211 if self.prompt_lookup_min > self.prompt_lookup_max:
1212 raise ValueError(
1213 f"prompt_lookup_min={self.prompt_lookup_min} must "
1214 f"be <= prompt_lookup_max={self.prompt_lookup_max}"
1215 )
1216
1217 # TODO: current we still need extract vocab_size from target model
1218 # config, in future, we may try refactor it out, and set
1219 # draft related config as None here.
1220 self.draft_model_config = self.target_model_config
1221 self.draft_parallel_config = self.target_parallel_config
1222 elif self.method == "suffix":
1223 self._validate_suffix_decoding()
1224 elif self.method == "custom_class":
1225 # Custom class proposer does not need a draft model.
1226 # It will dynamically load the user-provided class at runtime.
1227 logger.warning_once(
1228 "Using a custom class-based proposer backend. This is an "
1229 "experimental feature and the proposer interface is subject to "
1230 "breaking changes in future vLLM releases."
1231 )
1232 self.prompt_lookup_max = 0
1233 self.prompt_lookup_min = 0
1234 self.draft_model_config = self.target_model_config
1235 self.draft_parallel_config = self.target_parallel_config
1236 elif self.method == "extract_hidden_states":
1237 from vllm.transformers_utils.configs.extract_hidden_states import (
1238 ExtractHiddenStatesConfig,
1239 )
1240
1241 # ExtractHiddenStatesModel is instantiated manually in load_model()
1242 # We just need to store the target model config for KV cache shape info
1243 self.model = "extract_hidden_states"
1244 self.prompt_lookup_max = 0
1245 self.prompt_lookup_min = 0
1246
1247 if hasattr(self.draft_model_config, "hf_config"):
1248 hf_config = self.draft_model_config.hf_config.to_dict()
1249 elif (
1250 isinstance(self.draft_model_config, dict)
1251 and "hf_config" in self.draft_model_config
1252 ):
1253 hf_config = self.draft_model_config["hf_config"]
1254 else:
1255 hf_config = {}
1256
1257 self.draft_model_config = copy.copy(self.target_model_config)
1258 self.draft_model_config.hf_config = ExtractHiddenStatesConfig(
1259 self.draft_model_config.hf_config, **hf_config
1260 )
1261 self.update_arch_()
1262 self.draft_parallel_config = self.target_parallel_config
1263
1264 else:
1265 self.prompt_lookup_max = 0
1266 self.prompt_lookup_min = 0
1267
1268 if self.model is not None:
1269 # Old-format Medusa checkpoints (e.g. FasterDecoding/medusa-*)
1270 # lack a model_type key in config.json, so AutoConfig cannot
1271 # detect them. When the method is explicitly "medusa", inject
1272 # model_type so MedusaConfig.from_pretrained is used instead.
1273 draft_hf_overrides: HfOverrides
1274 if self.method == "medusa":
1275 draft_hf_overrides = {"model_type": "medusa"}
1276 else:
1277 # Compose any callable hf_overrides set on the target so the
1278 # draft config receives the same transform (e.g. the test
1279 # shrink). Dict overrides stay target-only.
1280 draft_hf_overrides = SpeculativeConfig.compose_draft_hf_overrides(
1281 self.target_model_config.hf_overrides
1282 )
1283 self.draft_model_config = ModelConfig(
1284 model=self.model,
1285 runner="draft",
1286 tokenizer=(
1287 self.model
1288 if self.use_heterogeneous_vocab
1289 else self.target_model_config.tokenizer
1290 ),
1291 tokenizer_mode=self.target_model_config.tokenizer_mode,
1292 trust_remote_code=self.target_model_config.trust_remote_code,
1293 allowed_local_media_path=self.target_model_config.allowed_local_media_path,
1294 allowed_media_domains=self.target_model_config.allowed_media_domains,
1295 dtype=self.target_model_config.dtype,
1296 seed=self.target_model_config.seed,
1297 revision=self.revision,
1298 code_revision=self.code_revision,
1299 tokenizer_revision=self.target_model_config.tokenizer_revision,
1300 max_model_len=self.max_model_len, # type: ignore[arg-type]
1301 spec_target_max_model_len=self.target_model_config.max_model_len,
1302 quantization=self.quantization,
1303 enforce_eager=self.target_model_config.enforce_eager,
1304 max_logprobs=self.target_model_config.max_logprobs,
1305 hf_overrides=draft_hf_overrides,
1306 config_format=self.target_model_config.config_format,
1307 )
1308
1309 # Old-format Medusa checkpoints (e.g. FasterDecoding/medusa-*)
1310 # omit vocab_size in config.json, so MedusaConfig falls back to
1311 # its default (32001). Align with the target model's vocab size
1312 # to avoid shape mismatches when loading LM-head weights.
1313 if self.method == "medusa":
1314 target_vocab = self.target_model_config.hf_config.vocab_size
1315 draft_hf = self.draft_model_config.hf_config
1316 if draft_hf.vocab_size != target_vocab:
1317 draft_hf.vocab_size = target_vocab
1318 draft_hf.truncated_vocab_size = target_vocab
1319
1320 # Automatically detect the method
1321 if self.method in ("eagle", "eagle3", "dflash", "dspark"):
1322 pass
1323 # examples:
1324 # yuhuili/EAGLE-LLaMA3-Instruct-8B
1325 # yuhuili/EAGLE3-LLaMA3.1-Instruct-8B
1326 # AngelSlim/Qwen3-8B_eagle3
1327 # deepseek-ai/dspark_qwen3_8b_block7
1328 elif "eagle-" in self.draft_model_config.model.lower():
1329 self.method = "eagle"
1330 elif "eagle3" in self.draft_model_config.model.lower():
1331 self.method = "eagle3"
1332 elif (
1333 "dflash" in self.draft_model_config.model.lower()
1334 or "MuseGlimmerAssistantModel"
1335 in self.draft_model_config.architectures
1336 ):
1337 self.method = "dflash"
1338 elif (
1339 "dspark" in self.draft_model_config.model.lower()
1340 or "Qwen3DSparkModel" in self.draft_model_config.architectures
1341 or _QWEN3_OMNI_DSPARK_ARCHITECTURE
1342 in self.draft_model_config.architectures
1343 or "Gemma4DSparkModel" in self.draft_model_config.architectures
1344 or (
1345 "DSparkDraftModel" in self.draft_model_config.architectures
1346 and self.draft_model_config.hf_config.model_type == "qwen3"
1347 )
1348 ):
1349 self.method = "dspark"
1350 elif self.draft_model_config.hf_config.model_type == "medusa":
1351 self.method = "medusa"
1352 elif self.draft_model_config.hf_config.model_type == "mlp_speculator":
1353 self.method = "mlp_speculator"
1354 elif self.draft_model_config.hf_config.model_type in get_args(
1355 MTPModelTypes
1356 ):
1357 self.method = "mtp"
1358 if (
1359 self.target_model_config is not None
1360 and self.target_model_config.hf_config.model_type
1361 == "deepseek_v41"
1362 ):
1363 raise ValueError(
1364 "DeepSeek V4.1 has no classic-MTP draft: its "
1365 "checkpoints ship DSpark stages under mtp.* "
1366 "(main_proj/markov_head/confidence_head) and carry "
1367 "no e_proj/h_proj/enorm/hnorm/hc_head weights. Use "
1368 "speculative method 'dspark' instead of 'mtp'."
1369 )
1370 if (
1371 self.num_speculative_tokens > 1
1372 and self.draft_model_config.hf_config.model_type
1373 not in ("step3p5_mtp", "inkling_mtp")
1374 ):
1375 logger.warning(
1376 "Enabling num_speculative_tokens > 1 will run "
1377 "multiple times of forward on same MTP layer"
1378 ",which may result in lower acceptance rate"
1379 )
1380 elif self.method == "draft_model":
1381 pass
1382 else:
1383 raise NotImplementedError(
1384 f"Unsupported speculative method: '{self.method}'"
1385 )
1386
1387 if self.method in ("eagle", "eagle3"):
1388 # EAGLE drafts share the target's positional space; a
1389 # draft checkpoint with a smaller max_position_embeddings
1390 # than the target under-sizes its rotary cache (#48894).
1391 SpeculativeConfig._maybe_override_draft_max_position_embeddings(
1392 self.draft_model_config.hf_config,
1393 self.target_model_config.max_model_len,
1394 )
1395
1396 # Replace hf_config for EAGLE draft_model
1397 if self.method in ("eagle", "eagle3", "dflash"):
1398 from vllm.transformers_utils.configs.eagle import EAGLEConfig
1399 from vllm.transformers_utils.configs.speculators import (
1400 SpeculatorsConfig,
1401 )
1402
1403 if isinstance(
1404 self.draft_model_config.hf_config,
1405 (EAGLEConfig, SpeculatorsConfig),
1406 ):
1407 pass
1408 else:
1409 eagle_config = EAGLEConfig(
1410 self.draft_model_config.hf_config,
1411 method=self.method,
1412 model_type="eagle",
1413 )
1414 self.draft_model_config.hf_config = eagle_config
1415 self.update_arch_()
1416
1417 if (
1418 self.method == "dspark"
1419 and "DSparkDraftModel" in self.draft_model_config.architectures
1420 and self.draft_model_config.hf_config.model_type == "qwen3"
1421 ):
1422 self.draft_model_config.hf_config.architectures = [
1423 "Qwen3DSparkModel"
1424 ]
1425 self.update_arch_()
1426 elif self.method == "dspark" and (
1427 "Qwen3DSparkModel" not in self.draft_model_config.architectures
1428 and _QWEN3_OMNI_DSPARK_ARCHITECTURE
1429 not in self.draft_model_config.architectures
1430 and "Gemma4DSparkModel" not in self.draft_model_config.architectures
1431 and "K3DSparkModel" not in self.draft_model_config.architectures
1432 ):
1433 # DeepSeek-V4(.1) DSpark reuses the full target config
1434 # and its weights ship in the target checkpoint.
1435 is_v41 = (
1436 self.target_model_config.hf_config.model_type == "deepseek_v41"
1437 )
1438 draft_hf_config = self.draft_model_config.hf_config
1439 draft_hf_config.model_type = (
1440 "deepseek_v41" if is_v41 else "deepseek_v4"
1441 )
1442 draft_hf_config.architectures = [
1443 "DSparkV41DraftModel" if is_v41 else "DSparkDraftModel"
1444 ]
1445 self.draft_model_config.quantization = (
1446 self.target_model_config.quantization
1447 )
1448 self.update_arch_()
1449 elif (
1450 self.method == "dspark"
1451 and "Gemma4DSparkModel" in self.draft_model_config.architectures
1452 ):
1453 # Normalize the self-contained Gemma4 draft's config keys to
1454 # the DSpark conventions.
1455 hf = self.draft_model_config.hf_config
1456 if (
1457 getattr(hf, "dspark_target_layer_ids", None) is None
1458 and getattr(hf, "target_layer_ids", None) is not None
1459 ):
1460 hf.dspark_target_layer_ids = hf.target_layer_ids
1461
1462 if self.method in ("dflash", "dspark"):
1463 self.parallel_drafting = True
1464
1465 if self.num_speculative_tokens is not None and hasattr(
1466 self.draft_model_config.hf_config, "num_lookahead_tokens"
1467 ):
1468 self.draft_model_config.hf_config.num_lookahead_tokens = (
1469 self.num_speculative_tokens
1470 )
1471
1472 n_predict = getattr(
1473 self.draft_model_config.hf_config, "n_predict", None
1474 )
1475 if self.use_dspark():
1476 if self.num_speculative_tokens is None:
1477 # DSpark's parallel width is independent of MTP stages.
1478 hf_config = self.draft_model_config.hf_config
1479 block_size = getattr(hf_config, "block_size", None)
1480 if block_size is None:
1481 block_size = getattr(hf_config, "dspark_block_size", None)
1482 self.num_speculative_tokens = block_size
1483 elif n_predict is not None:
1484 if self.num_speculative_tokens is None:
1485 # Default to max value defined in draft model config.
1486 self.num_speculative_tokens = n_predict
1487 elif (
1488 self.num_speculative_tokens > n_predict
1489 and self.num_speculative_tokens % n_predict != 0
1490 ):
1491 # Ensure divisibility for MTP module reuse.
1492 raise ValueError(
1493 f"num_speculative_tokens:{self.num_speculative_tokens}"
1494 f" must be divisible by {n_predict=}"
1495 )
1496
1497 if self.num_speculative_tokens is None:
1498 raise ValueError(
1499 "A speculative model was provided, but "
1500 "`num_speculative_tokens` was not provided"
1501 )
1502
1503 if self.dspark_draft_topk is not None and self.method != "dspark":
1504 raise ValueError("dspark_draft_topk is only supported by DSpark")
1505
1506 dspark_draft_topk = None
1507 if self.method == "dspark":
1508 hf_config = self.draft_model_config.hf_config
1509 dspark_draft_topk = self.dspark_draft_topk
1510 if dspark_draft_topk is None:
1511 dspark_draft_topk = getattr(
1512 hf_config, "dspark_draft_topk", None
1513 )
1514 if dspark_draft_topk is not None:
1515 draft_vocab_size = (
1516 getattr(hf_config, "draft_vocab_size", None)
1517 or hf_config.vocab_size
1518 )
1519 if not 1 <= dspark_draft_topk <= draft_vocab_size:
1520 raise ValueError(
1521 "dspark_draft_topk must be between 1 and the "
1522 f"draft vocabulary size ({draft_vocab_size})"
1523 )
1524 if (
1525 "Qwen3DSparkModel"
1526 not in self.draft_model_config.architectures
1527 and _QWEN3_OMNI_DSPARK_ARCHITECTURE
1528 not in self.draft_model_config.architectures
1529 ):
1530 raise ValueError(
1531 "dspark_draft_topk is only supported by "
1532 "Qwen3DSparkModel and Qwen3OmniDSparkModel"
1533 )
1534 hf_config.dspark_draft_topk = dspark_draft_topk
1535
1536 assert self.target_model_config is not None
1537 _validate_qwen3_omni_dspark(
1538 self.target_model_config,
1539 self.draft_model_config,
1540 self.num_speculative_tokens,
1541 )
1542
1543 self.draft_tensor_parallel_size = (
1544 SpeculativeConfig._verify_and_get_draft_tp(
1545 self.target_parallel_config,
1546 self.draft_tensor_parallel_size,
1547 self.draft_model_config.hf_config,
1548 )
1549 )
1550 self.draft_model_config.max_model_len = (
1551 SpeculativeConfig._maybe_override_draft_max_model_len(
1552 self.max_model_len,
1553 self.draft_model_config.max_model_len,
1554 self.target_model_config.max_model_len,
1555 )
1556 )
1557
1558 # Use the final draft config, after any architecture overrides,
1559 # so EP agrees with ModelConfig's expert-count validation.
1560 self.draft_parallel_config = (
1561 SpeculativeConfig.create_draft_parallel_config(
1562 self.target_parallel_config,
1563 self.draft_tensor_parallel_size,
1564 draft_model_config=self.draft_model_config,
1565 )
1566 )
1567
1568 if self.index_share_for_mtp_iteration is not None:
1569 if self.method != "mtp" or self.draft_model_config is None:
1570 raise ValueError(
1571 "index_share_for_mtp_iteration is only supported with method='mtp'"
1572 )
1573 self.draft_model_config.hf_config.index_share_for_mtp_iteration = (
1574 self.index_share_for_mtp_iteration
1575 )
1576
1577 if (
1578 self.method != "dspark"
1579 and self.enable_adaptive_verification
1580 and self.use_local_argmax_reduction
1581 ):
1582 raise ValueError(
1583 "Adaptive verification estimates per-position acceptance from "
1584 "the draft logits, which use_local_argmax_reduction never "
1585 "materializes. Disable one of them."
1586 )
1587
1588 return self
1589
1590 def _validate_suffix_decoding(self):
1591 if not has_arctic_inference():
1592 raise ImportError(
1593 "Arctic Inference is required for suffix decoding. "
1594 "Install via `pip install arctic-inference==0.1.1`."
1595 )
1596 if self.num_speculative_tokens is None:
1597 # Suffix decoding decides the actual number of speculative tokens
1598 # dynamically and treats num_speculative_tokens as a maximum limit.
1599 self.num_speculative_tokens = self.suffix_decoding_max_tree_depth
1600 logger.warning(
1601 "Defaulted num_speculative_tokens to %s for suffix decoding.",
1602 self.num_speculative_tokens,
1603 )
1604 # Validate values
1605 if self.suffix_decoding_max_tree_depth < 1:
1606 raise ValueError(
1607 f"suffix_decoding_max_tree_depth="
1608 f"{self.suffix_decoding_max_tree_depth} must be >= 1"
1609 )
1610 if self.suffix_decoding_max_cached_requests < 0:
1611 raise ValueError(
1612 f"suffix_decoding_max_cached_requests="
1613 f"{self.suffix_decoding_max_cached_requests} must be >= 0"
1614 )
1615 if self.suffix_decoding_max_spec_factor < 0:
1616 raise ValueError(
1617 f"suffix_decoding_max_spec_factor="
1618 f"{self.suffix_decoding_max_spec_factor} must be >= 0"
1619 )
1620 if not 0 <= self.suffix_decoding_min_token_prob <= 1:
1621 raise ValueError(
1622 f"suffix_decoding_min_token_prob="
1623 f"{self.suffix_decoding_min_token_prob} must be in [0, 1]"
1624 )
1625
1626 @staticmethod
1627 def _maybe_override_draft_max_model_len(
1628 speculative_max_model_len: int | None,
1629 draft_max_model_len: int,
1630 target_max_model_len: int,
1631 ) -> int:
1632 """Determine the max sequence len for the draft model. This is usually
1633 the draft_max_model_len, but may be the target_max_model_len if it is
1634 less than the draft_max_model_len, or may be speculative_max_model_len
1635 if it is specified.
1636
1637 This is necessary so that sequences do not exceed the capacity of the
1638 draft model or the target model.
1639
1640 speculative_max_model_len is mainly used for testing that sequences can
1641 skip speculation.
1642 """
1643 if speculative_max_model_len is not None:
1644 if speculative_max_model_len > draft_max_model_len:
1645 raise ValueError(
1646 f"{speculative_max_model_len=} cannot be "
1647 f"larger than {draft_max_model_len=}"
1648 )
1649
1650 if speculative_max_model_len > target_max_model_len:
1651 raise ValueError(
1652 f"{speculative_max_model_len=} cannot be "
1653 f"larger than {target_max_model_len=}"
1654 )
1655
1656 return speculative_max_model_len
1657
1658 result = min(
1659 draft_max_model_len,
1660 target_max_model_len,
1661 )
1662 if result != draft_max_model_len:
1663 logger.info(
1664 "Overriding draft model max model len from %d to %d",
1665 draft_max_model_len,
1666 result,
1667 )
1668 return result
1669
1670 @staticmethod
1671 def _maybe_override_draft_max_position_embeddings(
1672 draft_hf_config: PreTrainedConfig,
1673 target_max_model_len: int,
1674 ) -> None:
1675 """Raise an EAGLE draft's max_position_embeddings up to the target's.
1676
1677 The proposer feeds the draft positions up to the target's
1678 max_model_len, while max_position_embeddings sizes the draft's
1679 rotary cos_sin_cache. A smaller checkpoint value (e.g. 2048 for
1680 yuhuili/EAGLE3-LLaMA3.1-Instruct-8B) makes that cache gather go
1681 out of bounds (#48894).
1682
1683 Args:
1684 draft_hf_config: The draft model's HF config, mutated in place.
1685 target_max_model_len: The target model's max_model_len.
1686
1687 """
1688 draft_max_position_embeddings = getattr(
1689 draft_hf_config, "max_position_embeddings", None
1690 )
1691 if (
1692 draft_max_position_embeddings is None
1693 or draft_max_position_embeddings >= target_max_model_len
1694 ):
1695 return
1696 logger.info(
1697 "Overriding draft model max_position_embeddings from %d to the "
1698 "target model's max_model_len (%d); EAGLE drafts share the "
1699 "target's positional space.",
1700 draft_max_position_embeddings,
1701 target_max_model_len,
1702 )
1703 draft_hf_config.max_position_embeddings = target_max_model_len
1704
1705 @staticmethod
1706 def _verify_and_get_draft_tp(
1707 target_parallel_config: ParallelConfig,
1708 speculative_draft_tensor_parallel_size: int | None,
1709 draft_hf_config: PreTrainedConfig,
1710 ) -> int:
1711 """Verifies and adjusts the tensor parallel size for a draft model
1712 specified using speculative_draft_tensor_parallel_size.
1713 """
1714 # If speculative_draft_tensor_parallel_size is unset then set it
1715 # appropriately else verify that it is set correctly.
1716 if speculative_draft_tensor_parallel_size is None:
1717 if draft_hf_config.model_type == "mlp_speculator":
1718 speculative_draft_tensor_parallel_size = 1
1719 if target_parallel_config.tensor_parallel_size > 1:
1720 logger.warning(
1721 "%s cannot currently be run with tp>1; "
1722 "setting speculative_draft_tensor_parallel_size=1",
1723 draft_hf_config.model_type,
1724 )
1725 else:
1726 speculative_draft_tensor_parallel_size = (
1727 target_parallel_config.tensor_parallel_size
1728 )
1729 elif speculative_draft_tensor_parallel_size not in (
1730 1,
1731 target_parallel_config.tensor_parallel_size,
1732 ):
1733 raise ValueError(
1734 f"{speculative_draft_tensor_parallel_size=} cannot be "
1735 f"other value than 1 or target model tensor_parallel_size"
1736 )
1737 return speculative_draft_tensor_parallel_size
1738
1739 def update_arch_(self):
1740 """EagleConfig and ExtractHiddenStatesConfig update architectures, so update all
1741 architectures-related fields in self.draft_model_config
1742 """
1743 self.draft_model_config.hf_text_config = get_hf_text_config(
1744 self.draft_model_config.hf_config
1745 )
1746 self.draft_model_config.model_arch_config = (
1747 self.draft_model_config.get_model_arch_config()
1748 )
1749 model_info, arch = self.draft_model_config.registry.inspect_model_cls(
1750 self.draft_model_config.architectures,
1751 self.draft_model_config,
1752 )
1753 self.draft_model_config._model_info = model_info
1754 self.draft_model_config._architecture = arch
1755
1756 @staticmethod
1757 def create_draft_parallel_config(
1758 target_parallel_config: ParallelConfig,
1759 speculative_draft_tensor_parallel_size: int,
1760 draft_model_config: ModelConfig | None = None,
1761 ) -> ParallelConfig:
1762 """Create a parallel config for use by the draft worker.
1763
1764 Use the draft TP size and disable inherited EP for known dense drafts.
1765 Without a draft model config, preserve the previous EP inheritance.
1766 """
1767 enable_ep = target_parallel_config.enable_expert_parallel
1768 if draft_model_config is not None:
1769 enable_ep = enable_ep and draft_model_config.is_moe
1770
1771 draft_parallel_config = ParallelConfig(
1772 pipeline_parallel_size=1,
1773 tensor_parallel_size=speculative_draft_tensor_parallel_size,
1774 enable_expert_parallel=enable_ep,
1775 distributed_executor_backend=target_parallel_config.distributed_executor_backend,
1776 max_parallel_loading_workers=target_parallel_config.max_parallel_loading_workers,
1777 disable_custom_all_reduce=target_parallel_config.disable_custom_all_reduce,
1778 ray_workers_use_nsight=target_parallel_config.ray_workers_use_nsight,
1779 placement_group=target_parallel_config.placement_group,
1780 )
1781
1782 return draft_parallel_config
1783
1784 def apply_draft_overrides(self, vllm_config: VllmConfig) -> VllmConfig:
1785 """Overlay this config's kernel overrides onto a target VllmConfig.
1786
1787 Only non-None fields override, so an unset field keeps whatever the
1788 target resolved.
1789 """
1790 for src, config_name, dst in _DRAFT_VLLM_CONFIG_OVERRIDES:
1791 if (value := getattr(self, src)) is not None:
1792 sub_config = replace(getattr(vllm_config, config_name), **{dst: value})
1793 vllm_config = replace(vllm_config, **{config_name: sub_config})
1794 return vllm_config
1795
1796 @field_validator("attention_backend", mode="before")
1797 @classmethod
1798 def _parse_attention_backend(cls, value: Any) -> Any:
1799 if isinstance(value, str):
1800 if value.lower() == "auto":
1801 return None
1802 return AttentionBackendEnum[value.upper()]
1803 return value
1804
1805 @model_validator(mode="after")
1806 def _verify_args(self) -> Self:
1807 if self.tensor_parallel_size is not None:
1808 raise ValueError(
1809 "'tensor_parallel_size' is not a valid argument in the "
1810 "speculative_config. Please pass 'draft_tensor_parallel_size' instead."
1811 )
1812
1813 if self.num_speculative_tokens is None:
1814 raise ValueError(
1815 "num_speculative_tokens must be provided with "
1816 "speculative model unless the draft model config contains an "
1817 "n_predict parameter."
1818 )
1819
1820 if self.num_speculative_tokens <= 0:
1821 raise ValueError(
1822 "Expected num_speculative_tokens to be greater "
1823 f"than zero ({self.num_speculative_tokens})."
1824 )
1825
1826 if self.rejection_sample_method == "synthetic":
1827 # Consolidate to per-position rates
1828 self.synthetic_acceptance_rates = self._resolve_synthetic_acceptance_rates(
1829 self.num_speculative_tokens,
1830 self.synthetic_acceptance_rates,
1831 self.synthetic_acceptance_length,
1832 )
1833 self.synthetic_acceptance_length = None
1834 elif (
1835 self.synthetic_acceptance_rates is not None
1836 or self.synthetic_acceptance_length is not None
1837 ):
1838 raise ValueError(
1839 "synthetic_acceptance_rates / synthetic_acceptance_length "
1840 "are only valid with rejection_sample_method='synthetic'."
1841 )
1842
1843 if self.draft_model_config:
1844 self.draft_model_config.verify_with_parallel_config(
1845 self.draft_parallel_config
1846 )
1847
1848 if self.use_heterogeneous_vocab and not self.uses_draft_model():
1849 raise ValueError(
1850 "use_heterogeneous_vocab only works with method='draft_model'"
1851 )
1852
1853 if self.use_heterogeneous_vocab and self.draft_sample_method != "greedy":
1854 raise ValueError(
1855 "use_heterogeneous_vocab currently only supports greedy draft "
1856 "sampling. Set draft_sample_method='greedy' (the default) or "
1857 "omit it."
1858 )
1859
1860 if not self.use_heterogeneous_vocab:
1861 self.verify_equal_vocab_size_if_draft_model()
1862 return self
1863
1864 def verify_equal_vocab_size_if_draft_model(self):
1865 if (
1866 self.method == "draft_model"
1867 and self.target_model_config is not None
1868 and self.draft_model_config is not None
1869 ):
1870 target_vocab_size = self.target_model_config.get_vocab_size()
1871 draft_vocab_size = self.draft_model_config.get_vocab_size()
1872 if target_vocab_size != draft_vocab_size:
1873 raise ValueError(
1874 f"Target and draft model should have the same vocabulary size. "
1875 f"Target model vocab_size={target_vocab_size}. "
1876 f"Draft model vocab_size={draft_vocab_size}. "
1877 f"Using models with different tokenizers can cause out-of-bounds "
1878 f"errors during speculative decoding."
1879 )
1880
1881 @property
1882 def max_num_new_slots_for_drafting(self) -> int:
1883 """Return the maximum additional drafting slots per request.
1884
1885 The scheduler budget already includes one query slot per decoding request.
1886 Let K be ``num_speculative_tokens``. Standard configurations require:
1887
1888 ==================== ============= ======== ================
1889 Algorithm Method Parallel Additional slots
1890 ==================== ============= ======== ================
1891 EAGLE3 eagle3 No 0
1892 P-EAGLE eagle3 Yes K - 1
1893 DFlash dflash Yes K
1894 DSpark dspark Yes K - 1
1895 MTP mtp No 0
1896 N-gram ngram No 0
1897 Draft model draft_model No 1
1898 PARD draft_model Yes K
1899 ==================== ============= ======== ================
1900 """
1901 num_draft_tokens = self.num_speculative_tokens
1902
1903 if self.use_dflash():
1904 # DFlash uses one bonus query followed by K mask queries.
1905 return num_draft_tokens
1906
1907 if self.parallel_drafting:
1908 if self.uses_draft_model():
1909 # PARD does not shift the existing input, so all K query
1910 # positions require additional slots.
1911 return num_draft_tokens
1912
1913 # The existing query is reused; only masked queries need new slots.
1914 return num_draft_tokens - 1
1915
1916 if self.uses_draft_model():
1917 # The autoregressive draft-model input retains one unsliced token.
1918 return 1
1919
1920 return 0
1921
1922 def use_gemma4_mtp(self) -> bool:
1923 return (
1924 self.method == "mtp"
1925 and self.draft_model_config is not None
1926 and getattr(self.draft_model_config.hf_config, "model_type", None)
1927 == "gemma4_mtp"
1928 )
1929
1930 def use_step3p5_mtp(self) -> bool:
1931 return (
1932 self.method == "mtp"
1933 and self.draft_model_config is not None
1934 and getattr(self.draft_model_config.hf_config, "model_type", None)
1935 == "step3p5_mtp"
1936 )
1937
1938 def use_eagle(self) -> bool:
1939 # NOTE: This method is usually a stand-in for "speculative decoding using
1940 # target model hidden states"
1941 # TODO(ben): Refactor this so the naming is clearer
1942 return self.method in ("eagle", "eagle3", "mtp", "dflash", "dspark")
1943
1944 def use_eagle_block_drop(self) -> bool:
1945 """Whether volatile trailing cache blocks should be discarded."""
1946 return self.use_eagle() and not self.disable_eagle_block_drop
1947
1948 def use_dflash(self) -> bool:
1949 return self.method == "dflash"
1950
1951 def use_dspark(self) -> bool:
1952 return self.method == "dspark"
1953
1954 def uses_dynamic_speculative_decoding(self) -> bool:
1955 return self.num_speculative_tokens_per_batch_size is not None
1956
1957 def uses_draft_model(self) -> bool:
1958 return self.method == "draft_model"
1959
1960 def uses_draft_kv_cache(self) -> bool:
1961 return (
1962 self.use_eagle()
1963 or self.uses_draft_model()
1964 or self.uses_extract_hidden_states()
1965 )
1966
1967 def uses_extract_hidden_states(self) -> bool:
1968 return self.method == "extract_hidden_states"
1969
1970 def use_ngram_gpu(self) -> bool:
1971 return self.method == "ngram_gpu"
1972
1973 def use_multi_module_mtp(self) -> bool:
1974 if self.method != "mtp" or self.draft_model_config is None:
1975 return False
1976 num_mtp_layers = getattr(
1977 self.draft_model_config.hf_config, "num_nextn_predict_layers", 1
1978 )
1979 return min(num_mtp_layers, self.num_speculative_tokens) > 1
1980
1981 def __repr__(self) -> str:
1982 method = self.method
1983 model = (
1984 None
1985 if method
1986 in (
1987 "ngram",
1988 "suffix",
1989 "extract_hidden_states",
1990 "custom_class",
1991 )
1992 else self.draft_model_config.model
1993 )
1994 num_spec_tokens = self.num_speculative_tokens
1995 return f"SpeculativeConfig({method=}, {model=}, {num_spec_tokens=})"