main
py 941 lines 37 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Multi-token prediction (MTP) head for HY V4 (NVIDIA)."""
4
5 import copy
6 import typing
7 from collections.abc import Callable, Iterable
8
9 import torch
10 from torch import nn
11 from transformers import PreTrainedConfig
12
13 from vllm.config import CacheConfig, ModelConfig, VllmConfig
14 from vllm.distributed import (
15 get_tensor_model_parallel_rank,
16 get_tensor_model_parallel_world_size,
17 )
18 from vllm.logger import init_logger
19 from vllm.model_executor.layers.fused_moe import fused_moe_make_expert_params_mapping
20 from vllm.model_executor.layers.layernorm import RMSNorm
21 from vllm.model_executor.layers.logits_processor import LogitsProcessor
22 from vllm.model_executor.layers.quantization import QuantizationConfig
23 from vllm.model_executor.layers.vocab_parallel_embedding import (
24 ParallelLMHead,
25 VocabParallelEmbedding,
26 )
27 from vllm.model_executor.model_loader.weight_utils import (
28 default_weight_loader,
29 maybe_remap_kv_scale_name,
30 )
31 from vllm.model_executor.models.deepseek_v2 import _try_load_fp8_indexer_wk
32 from vllm.model_executor.models.utils import (
33 get_pp_missing_layer_names,
34 is_pp_missing_parameter,
35 maybe_prefix,
36 )
37 from vllm.sequence import IntermediateTensors
38 from vllm.v1.outputs import SamplerOutput
39 from vllm.v1.sample.metadata import SamplingMetadata
40 from vllm.v1.sample.sampler import Sampler
41
42 from .model import HYV4DecoderLayer, _normalize_hyv4_config
43
44 logger = init_logger(__name__)
45
46 _MTP_SCALE_ONLY_PARAM_SUBSTRINGS = (
47 "_scale",
48 "_weight_scale",
49 "_weight_scale_inv",
50 "_input_scale",
51 )
52
53 # Names of the quant-config attribute that stores the unquantized-module
54 # prefix list, in the order we look them up. Different QuantizationConfig
55 # subclasses expose the list under different names:
56 # * ``ignored_layers`` -- Fp8Config, GPTQ / AWQ configs.
57 # * ``exclude_modules`` -- ModelOpt* configs (Fp8, NvFp4, MxFp8, Mixed).
58 _MTP_QUANT_EXCLUSION_ATTRS = ("ignored_layers", "exclude_modules")
59
60
61 def _get_spec_layer_idx_from_weight_name(
62 config: PreTrainedConfig, weight_name: str
63 ) -> int | None:
64 """Return the MTP layer index a checkpoint weight belongs to, or None.
65
66 Compatible with``num_nextn_predict_layers`` of 1 and 2 for a single MTP
67 head, matching how the HY V4 checkpoints are exported.
68 """
69 if getattr(config, "num_nextn_predict_layers", 0) < 1:
70 return None
71 layer_idx = config.num_hidden_layers
72 num_mtp_layers = max(config.num_nextn_predict_layers - 1, 1)
73 for i in range(num_mtp_layers):
74 if weight_name.startswith(f"model.layers.{layer_idx + i}."):
75 return layer_idx + i
76 return None
77
78
79 def _should_skip_missing_mtp_scale_param(
80 quant_config: QuantizationConfig | None,
81 name: str,
82 ) -> bool:
83 """Whether an unmatched scale parameter can be silently ignored."""
84 return quant_config is None and any(
85 scale_name in name for scale_name in _MTP_SCALE_ONLY_PARAM_SUBSTRINGS
86 )
87
88
89 def _resolve_fused_expert_param(
90 param_base: str,
91 ckpt_suffix: str,
92 params_dict: dict,
93 ) -> str | None:
94 """Map a fused expert checkpoint suffix onto a draft parameter name.
95
96 Fused checkpoints pack every expert into one tensor and name the companion
97 scales by suffixing the projection (``experts.gate_up_proj_scale_inv``),
98 while the draft model owns ``experts.routed_experts.w13_weight`` plus a
99 separate ``..._scale_inv`` parameter. Dropping the suffix would push the
100 scale into the weight parameter and leave the scale at its sentinel init.
101
102 Args:
103 param_base: Draft parameter holding the packed weight.
104 ckpt_suffix: Checkpoint text following the projection name.
105 params_dict: The draft model's named parameters.
106
107 Returns:
108 The matching draft parameter name, or None when there is none.
109
110 """
111 if not ckpt_suffix:
112 return param_base if param_base in params_dict else None
113 tail = ckpt_suffix.lstrip(".").removeprefix("weight_")
114 for candidate in (
115 f"{param_base}_{tail}",
116 f"{param_base}_scale_inv",
117 f"{param_base}_scale",
118 ):
119 if candidate in params_dict:
120 return candidate
121 return None
122
123
124 def _prepare_mtp_fp8_expert_scale(
125 quant_config: QuantizationConfig | None,
126 name: str,
127 loaded_weight: torch.Tensor,
128 ) -> tuple[str, torch.Tensor]:
129 """Normalize block-wise FP8 expert scale names and dtypes."""
130 from vllm.model_executor.layers.quantization.fp8 import Fp8Config
131
132 if (
133 isinstance(quant_config, Fp8Config)
134 and quant_config.weight_block_size is not None
135 and ".mlp.experts." in name
136 and name.endswith(".scale")
137 ):
138 name = name[: -len(".scale")] + ".weight_scale_inv"
139 if (
140 getattr(quant_config, "is_scale_e8m0", False)
141 and loaded_weight.dtype == torch.uint8
142 ):
143 # UE8M0 scale exponents are stored as raw uint8 bytes: reinterpret
144 # the bits instead of numerically converting them.
145 loaded_weight = loaded_weight.view(torch.float8_e8m0fnu)
146 return name, loaded_weight
147
148
149 def _create_mtp_quant_config(
150 hf_config: PreTrainedConfig,
151 backbone_quant_config: QuantizationConfig | None = None,
152 ) -> QuantizationConfig | None:
153 """Create the quantization config for the MTP layers.
154
155 The MTP quantization algorithm is given by the ``mtp_quant_algo`` field in
156 ``config.json``, independently of the backbone's quantization. Supported
157 values are ``"FP8"``, ``"NONE"`` (inherit the backbone) and ``"BF16"`` /
158 ``"FP16"`` (unquantized). A missing or ``"NONE"`` value falls back to the
159 backbone config.
160
161 Args:
162 hf_config: The draft model's HF config.
163 backbone_quant_config: The target model's quantization config.
164
165 Returns:
166 The quantization config to use for the MTP layers, or None when the MTP
167 layers are unquantized.
168
169 """
170 mtp_quant_algo = getattr(hf_config, "mtp_quant_algo", None)
171
172 if mtp_quant_algo is None or mtp_quant_algo.upper() == "NONE":
173 logger.info_once(
174 "No explicit MTP quant algo (mtp_quant_algo=%s). "
175 "Using the backbone's quant config.",
176 mtp_quant_algo,
177 )
178 return backbone_quant_config
179
180 mtp_quant_algo = mtp_quant_algo.upper()
181
182 if mtp_quant_algo in ("BF16", "FP16"):
183 logger.info_once(
184 "MTP layers are unquantized (mtp_quant_algo=%s).", mtp_quant_algo
185 )
186 return None
187
188 if mtp_quant_algo == "FP8":
189 from vllm.model_executor.layers.quantization.fp8 import Fp8Config
190
191 logger.info_once("MTP uses FP8 quantization (mtp_quant_algo=FP8).")
192 hf_quant_config = getattr(hf_config, "quantization_config", None) or {}
193 weight_block_size = hf_quant_config.get("weight_block_size")
194 activation_scheme = hf_quant_config.get("activation_scheme", "dynamic")
195 if weight_block_size is not None:
196 activation_scheme = "dynamic"
197 ignored_layers = hf_quant_config.get("ignored_layers") or hf_quant_config.get(
198 "modules_to_not_convert"
199 )
200
201 fp8_config = Fp8Config(
202 is_checkpoint_fp8_serialized=True,
203 activation_scheme=activation_scheme,
204 ignored_layers=ignored_layers or [],
205 weight_block_size=weight_block_size,
206 )
207 if hf_quant_config.get("scale_fmt") == "ue8m0":
208 # Fp8LinearMethod reads this through getattr, so it is not a
209 # declared Fp8Config field.
210 setattr(fp8_config, "is_scale_e8m0", True) # noqa: B010
211 return fp8_config
212
213 logger.warning(
214 "Unknown mtp_quant_algo=%s, falling back to the backbone config.",
215 mtp_quant_algo,
216 )
217 return backbone_quant_config
218
219
220 def _remap_mtp_quant_exclusions(
221 quant_config: QuantizationConfig | None,
222 mtp_start_layer_idx: int,
223 num_mtp_layers: int,
224 ) -> QuantizationConfig | None:
225 """Translate checkpoint-named MTP quant exclusions to draft prefixes.
226
227 ``modules_to_not_convert`` / ``exclude_modules`` name MTP modules the way
228 the checkpoint does (``model.mtp_layers.0.self_attn.linear_gate``), while
229 the draft model builds them under ``model.layers.<num_hidden_layers + i>``.
230 ``is_layer_skipped`` compares prefixes for exact equality, so without this
231 translation an excluded MTP module gets a quant method even though its
232 checkpoint weight is BF16 with no ``weight_scale`` / ``weight_scale_inv``
233 companion: the scale then keeps its ``finfo(float32).min`` sentinel and the
234 layer silently produces garbage.
235
236 The list lives under a different attribute per config class, hence the
237 lookup over ``_MTP_QUANT_EXCLUSION_ATTRS``.
238
239 Args:
240 quant_config: The MTP quantization config to widen.
241 mtp_start_layer_idx: Index of the first MTP layer in the draft model.
242 num_mtp_layers: Number of MTP layers.
243
244 Returns:
245 A shallow copy with the translated exclusions, or the input unchanged.
246
247 """
248 if quant_config is None:
249 return None
250
251 # Different QuantizationConfig subclasses expose the exclusion list under
252 # different attribute names; only the one that is actually populated on
253 # the backbone config governs which draft modules stay unquantized. For
254 # ModelOpt checkpoints (``quant_method: modelopt`` and friends) this is
255 # ``exclude_modules``; for the legacy fp8 path it is ``ignored_layers``.
256 exclusion_attr: str | None = None
257 patterns: list[str] | None = None
258 for attr in _MTP_QUANT_EXCLUSION_ATTRS:
259 value = getattr(quant_config, attr, None)
260 if value:
261 exclusion_attr = attr
262 patterns = list(value)
263 break
264 if not patterns or exclusion_attr is None:
265 return quant_config
266
267 extra: list[str] = []
268 for offset in range(num_mtp_layers):
269 source_prefix = f"model.mtp_layers.{offset}."
270 draft_prefix = f"model.layers.{mtp_start_layer_idx + offset}."
271 for pattern in patterns:
272 if pattern.startswith(source_prefix):
273 extra.append(draft_prefix + pattern[len(source_prefix) :])
274 # ModelOpt exclusion patterns may end with a ``*`` wildcard; the
275 # remap must preserve that suffix so ``is_layer_skipped`` still
276 # matches the draft module prefixes.
277 elif pattern.rstrip("*").startswith(source_prefix):
278 stripped = pattern.rstrip("*")
279 trailing_stars = pattern[len(stripped) :]
280 extra.append(
281 draft_prefix + stripped[len(source_prefix) :] + trailing_stars
282 )
283 if not extra:
284 return quant_config
285
286 # The backbone shares this object with the target model; copy before
287 # widening the exclusion list. The attribute is only present on the
288 # concrete configs that support exclusions, hence the getattr/setattr pair.
289 quant_config = copy.copy(quant_config)
290 setattr(quant_config, exclusion_attr, patterns + extra) # noqa: B010
291 logger.info_once(
292 "HYV4 MTP: mapped %d checkpoint-named quant exclusions (%s) onto the "
293 "draft module prefixes: %s",
294 len(extra),
295 exclusion_attr,
296 ", ".join(extra),
297 )
298 return quant_config
299
300
301 class HYV4SharedHead(nn.Module):
302 """Holds the draft LM head shared with the target model."""
303
304 def __init__(
305 self,
306 config: PreTrainedConfig,
307 quant_config: QuantizationConfig | None = None,
308 ) -> None:
309 super().__init__()
310 # Use the same ``lm_head`` prefix as the backbone so that quant-config
311 # exclusion lists (e.g. ModelOpt's ``exclude_modules: ["lm_head", ...]``)
312 # also apply here: an unquantized checkpoint lm_head must not gain a
313 # spurious ``weight_scale`` parameter on the draft side, which would
314 # otherwise stay at its FP-min sentinel until the proposer swaps the
315 # whole ParallelLMHead for the target model's ``lm_head``.
316 self.head = ParallelLMHead(
317 config.vocab_size,
318 config.hidden_size,
319 quant_config=quant_config,
320 prefix="lm_head",
321 )
322
323 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
324 return hidden_states
325
326
327 class HYV4MultiTokenPredictorLayer(nn.Module):
328 """A single MTP draft block."""
329
330 def __init__(
331 self,
332 config: PreTrainedConfig,
333 prefix: str,
334 vllm_config: VllmConfig,
335 model_config: ModelConfig,
336 cache_config: CacheConfig | None = None,
337 quant_config: QuantizationConfig | None = None,
338 topk_indices_buffer: torch.Tensor | None = None,
339 ) -> None:
340 super().__init__()
341 del model_config # kept for signature parity with the other MTP heads
342 self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
343 self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
344 self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
345 self.shared_head = HYV4SharedHead(config=config, quant_config=quant_config)
346
347 # HYV4DecoderLayer indexes layer_types/mlp_layer_types by the numeric
348 # layer id parsed from the prefix. MTP layers live after the backbone
349 # layers, so extend the per-layer config lists to cover those ids.
350 layer_idx = int(prefix.rsplit(".", 1)[-1])
351 mtp_config = copy.deepcopy(config)
352 # MTP checkpoints do not carry iHC pre/post weights for mtp_layers, so
353 # the draft blocks are built without iHC to match their structure.
354 mtp_config.enable_ihc = False
355 mtp_config.layer_types = _extend_layer_types(
356 getattr(mtp_config, "layer_types", None), layer_idx, "full_attention"
357 )
358 mtp_config.mlp_layer_types = _extend_layer_types(
359 getattr(mtp_config, "mlp_layer_types", None), layer_idx, "sparse"
360 )
361
362 self.mtp_block = HYV4DecoderLayer(
363 config=mtp_config,
364 vllm_config=vllm_config,
365 cache_config=cache_config,
366 quant_config=quant_config,
367 prefix=prefix,
368 topk_indices_buffer=topk_indices_buffer,
369 )
370 self.final_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
371
372 def forward(
373 self,
374 input_ids: torch.Tensor,
375 positions: torch.Tensor,
376 previous_hidden_states: torch.Tensor,
377 inputs_embeds: torch.Tensor | None = None,
378 ) -> torch.Tensor:
379 del input_ids # embeddings are resolved by the caller
380 assert inputs_embeds is not None
381 inputs_embeds = self.enorm(inputs_embeds)
382 previous_hidden_states = self.hnorm(previous_hidden_states)
383 hidden_states = self.eh_proj(
384 torch.cat([inputs_embeds, previous_hidden_states], dim=-1)
385 )
386 hidden_states, residual = self.mtp_block(
387 positions=positions,
388 hidden_states=hidden_states,
389 residual=None,
390 )
391 hidden_states, _ = self.final_layernorm(hidden_states, residual)
392 return hidden_states
393
394
395 def _extend_layer_types(
396 layer_types: list[str] | None, layer_idx: int, fallback: str
397 ) -> list[str] | None:
398 """Pad a per-layer config list so``layer_idx`` is addressable."""
399 if layer_types is None or len(layer_types) > layer_idx:
400 return layer_types
401 fill = layer_types[-1] if layer_types else fallback
402 return list(layer_types) + [fill] * (layer_idx + 1 - len(layer_types))
403
404
405 class HYV4MultiTokenPredictor(nn.Module):
406 """Owns the MTP draft blocks and their shared embedding / logits path."""
407
408 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
409 super().__init__()
410 target_config = vllm_config.model_config.hf_config
411 speculative_config = vllm_config.speculative_config
412 assert speculative_config is not None, (
413 "HYV4MTP can only be built as a speculative draft model"
414 )
415 draft_model_config = speculative_config.draft_model_config
416 assert draft_model_config is not None
417 config = _normalize_hyv4_config(draft_model_config.hf_config)
418 self.mtp_start_layer_idx = target_config.num_hidden_layers
419 self.num_mtp_layers = max(
420 getattr(target_config, "num_nextn_predict_layers", 1), 1
421 )
422 self.quant_config = _remap_mtp_quant_exclusions(
423 _create_mtp_quant_config(config, vllm_config.quant_config),
424 self.mtp_start_layer_idx,
425 self.num_mtp_layers,
426 )
427 if self.quant_config is not None:
428 self.quant_config.packed_modules_mapping = HYV4MTP.packed_modules_mapping
429
430 # Sparse DSA draft blocks must share the target model's top-k buffer.
431 # The proposer injects it through HYV4MTP.set_topk_indices_buffer()
432 # before any draft forward.
433 self.topk_indices_buffer: torch.Tensor | None = None
434 if hasattr(config, "index_topk"):
435 from vllm.platforms import current_platform
436
437 self.topk_indices_buffer = torch.empty(
438 vllm_config.scheduler_config.max_num_batched_tokens,
439 config.index_topk,
440 dtype=torch.int32,
441 device=current_platform.device_type,
442 )
443
444 self.layers = nn.ModuleDict(
445 {
446 str(idx): HYV4MultiTokenPredictorLayer(
447 config,
448 f"{prefix}.layers.{idx}",
449 vllm_config=vllm_config,
450 model_config=draft_model_config,
451 cache_config=vllm_config.cache_config,
452 quant_config=self.quant_config,
453 topk_indices_buffer=self.topk_indices_buffer,
454 )
455 for idx in range(
456 self.mtp_start_layer_idx,
457 self.mtp_start_layer_idx + self.num_mtp_layers,
458 )
459 }
460 )
461 self.requires_topk_indices_buffer = any(
462 layer.mtp_block.self_attn.is_sparse for layer in self.layers.values()
463 )
464
465 self.embed_tokens = VocabParallelEmbedding(
466 config.vocab_size,
467 config.hidden_size,
468 )
469 self.logits_processor = LogitsProcessor(config.vocab_size)
470 self.spec_step_idx: int = 0
471
472 def set_skip_topk(self, skip: bool) -> None:
473 """Toggle the draft indexer for ``index_share_for_mtp_iteration``.
474
475 The proposer clears the flag for draft step 0 so the MTP layer builds
476 its own top-k indices, then sets it for steps 1+ so they reuse what
477 step 0 wrote into the shared buffer instead of re-running the indexer.
478 """
479 for layer in self.layers.values():
480 layer.mtp_block.self_attn.skip_topk = skip
481
482 def compact_topk_indices(self, slot_ids: torch.Tensor) -> None:
483 """Move the top-k rows at ``slot_ids`` to the front of the buffer.
484
485 Step 0 writes one row per query token of the multi-token batch, while
486 steps 1+ decode a single token per request and index the buffer from 0.
487 Without this gather they would read another token's rows.
488 """
489 num_slots = slot_ids.numel()
490 if self.topk_indices_buffer is None or num_slots == 0:
491 return
492 self.topk_indices_buffer[:num_slots] = self.topk_indices_buffer[slot_ids]
493
494 def forward(
495 self,
496 input_ids: torch.Tensor,
497 positions: torch.Tensor,
498 previous_hidden_states: torch.Tensor,
499 inputs_embeds: torch.Tensor | None = None,
500 ) -> torch.Tensor:
501 if inputs_embeds is None:
502 inputs_embeds = self.embed_tokens(input_ids)
503 inputs_embeds = torch.where((positions == 0).unsqueeze(-1), 0, inputs_embeds)
504
505 current_step_idx = self.spec_step_idx % self.num_mtp_layers
506 return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
507 input_ids,
508 positions,
509 previous_hidden_states,
510 inputs_embeds,
511 )
512
513 def compute_logits(
514 self,
515 hidden_states: torch.Tensor,
516 ) -> torch.Tensor:
517 current_step_idx = self.spec_step_idx % self.num_mtp_layers
518 mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
519 lm_head = mtp_layer.shared_head.head
520 proj_input = mtp_layer.shared_head(hidden_states)
521 return self.logits_processor(lm_head, proj_input)
522
523
524 class HYV4MTP(nn.Module):
525 """HY V4 MTP draft head.
526
527 Not a pipeline-parallel stage: the draft head always runs on a single rank,
528 matching `DeepseekV32MTP` / `KimiK3MTP` / `HYV3MTP`.
529 """
530
531 packed_modules_mapping = {
532 "gate_up_proj": ["gate_proj", "up_proj"],
533 # MLA runs both latent down-projections as one GEMM.
534 "fused_qkv_a_proj": ["q_a_proj", "kv_a_proj_with_mqa"],
535 # The indexer fuses wk and weights_proj into one GEMM.
536 "wk_weights_proj": ["wk", "weights_proj"],
537 }
538
539 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
540 super().__init__()
541 self.config = vllm_config.model_config.hf_config
542 self.model = HYV4MultiTokenPredictor(
543 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
544 )
545 self.quant_config = self.model.quant_config
546 self.sampler = Sampler()
547
548 def set_topk_indices_buffer(self, topk_indices_buffer: torch.Tensor) -> None:
549 """Share the target sparse-index buffer with every draft consumer.
550
551 Proposers that walk ``named_modules()`` instead of calling this reach
552 the same consumers via ``HYV4MLAAttention.topk_indices_buffer``.
553 """
554 self.model.topk_indices_buffer = topk_indices_buffer
555 for layer in self.model.layers.values():
556 self_attn = layer.mtp_block.self_attn
557 if not self_attn.is_sparse:
558 continue
559
560 indexer = self_attn.indexer
561 assert indexer is not None, "Sparse HYV4 MTP attention requires an indexer"
562 indexer.topk_indices_buffer = topk_indices_buffer
563 indexer.indexer_op.topk_indices_buffer = topk_indices_buffer
564
565 attn_impl = self_attn.mla_attn.impl
566 assert hasattr(attn_impl, "topk_indices_buffer"), (
567 "Sparse HYV4 MTP attention backend requires a top-k indices buffer"
568 )
569 attn_impl.topk_indices_buffer = topk_indices_buffer
570
571 def forward(
572 self,
573 input_ids: torch.Tensor,
574 positions: torch.Tensor,
575 hidden_states: torch.Tensor,
576 intermediate_tensors: IntermediateTensors | None = None,
577 inputs_embeds: torch.Tensor | None = None,
578 spec_step_idx: int = 0,
579 ) -> torch.Tensor:
580 del intermediate_tensors # the MTP head is single-stage
581 if (
582 self.model.requires_topk_indices_buffer
583 and self.model.topk_indices_buffer is None
584 ):
585 raise RuntimeError(
586 "HYV4 sparse MTP requires the target model's top-k indices buffer. "
587 "The proposer must call HYV4MTP.set_topk_indices_buffer() before "
588 "the first draft forward."
589 )
590 self.model.spec_step_idx = spec_step_idx
591 return self.model(input_ids, positions, hidden_states, inputs_embeds)
592
593 def compute_logits(
594 self,
595 hidden_states: torch.Tensor,
596 spec_step_idx: int = 0,
597 ) -> torch.Tensor | None:
598 self.model.spec_step_idx = spec_step_idx
599 return self.model.compute_logits(hidden_states)
600
601 def sample(
602 self,
603 logits: torch.Tensor,
604 sampling_metadata: SamplingMetadata,
605 ) -> SamplerOutput | None:
606 return self.sampler(logits, sampling_metadata)
607
608 def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
609 if f"model.layers.{spec_layer}.embed_tokens" in name:
610 return "__skip__"
611 if f"model.layers.{spec_layer}.shared_head" in name:
612 return "__skip__"
613
614 spec_layer_weight_names = ["enorm", "hnorm", "eh_proj", "final_layernorm"]
615 spec_layer_weight = any(
616 weight_name in name for weight_name in spec_layer_weight_names
617 )
618 if not spec_layer_weight:
619 name = name.replace(
620 f"model.layers.{spec_layer}.",
621 f"model.layers.{spec_layer}.mtp_block.",
622 )
623 return name
624
625 def _load_fused_expert_weights(
626 self,
627 name: str,
628 params_dict: dict,
629 loaded_weight: torch.Tensor,
630 shard_id: str,
631 num_experts: int,
632 ) -> bool:
633 if name not in params_dict:
634 return False
635 param = params_dict[name]
636 weight_loader = typing.cast(Callable[..., bool], param.weight_loader)
637 loaded_local_expert = False
638 for expert_id in range(num_experts):
639 curr_expert_weight = loaded_weight[expert_id]
640 success = weight_loader(
641 param,
642 curr_expert_weight,
643 name,
644 shard_id,
645 expert_id,
646 return_success=True,
647 )
648 if success:
649 loaded_local_expert = True
650 return loaded_local_expert
651
652 def _load_expert_weight(
653 self,
654 name: str,
655 loaded_weight: torch.Tensor,
656 params_dict: dict,
657 loaded_params: set[str],
658 split_expert_params_mapping: list[tuple[str, str, int, str]],
659 fused_expert_param_names: dict[tuple[str, str], str],
660 num_experts: int,
661 ) -> bool:
662 """Load one routed-expert weight in either checkpoint layout.
663
664 Args:
665 name: Weight name already rewritten to draft-module naming.
666 loaded_weight: The checkpoint tensor.
667 params_dict: The draft model's named parameters.
668 loaded_params: Set updated with the parameters that received a value.
669 split_expert_params_mapping: Mapping for the per-expert layout.
670 fused_expert_param_names: ``(mlp_prefix, tag) -> param name`` for the
671 all-experts-packed layout.
672 num_experts: Total number of routed experts.
673
674 Returns:
675 True when the weight was consumed (even if this rank holds none of
676 the addressed experts).
677
678 """
679 base = name.split(".experts.")[0]
680 for ckpt_proj, tag in (
681 (".experts.gate_up_proj", "w13_weight"),
682 (".experts.down_proj", "w2_weight"),
683 ):
684 if ckpt_proj not in name:
685 continue
686 param_base = fused_expert_param_names.get((base, tag))
687 if param_base is None:
688 return False
689 # Keep the checkpoint suffix (e.g. `_scale_inv`) so block-scale
690 # tensors land in the scale parameter, not in the weight.
691 target = _resolve_fused_expert_param(
692 param_base, name.split(ckpt_proj, 1)[1], params_dict
693 )
694 if target is None:
695 return False
696 if tag == "w13_weight":
697 chunks = loaded_weight.chunk(2, dim=-2)
698 loaded_w1 = self._load_fused_expert_weights(
699 target, params_dict, chunks[0], "w1", num_experts
700 )
701 loaded_w3 = self._load_fused_expert_weights(
702 target, params_dict, chunks[1], "w3", num_experts
703 )
704 loaded = loaded_w1 and loaded_w3
705 else:
706 loaded = self._load_fused_expert_weights(
707 target, params_dict, loaded_weight, "w2", num_experts
708 )
709 if loaded:
710 loaded_params.add(target)
711 # The weight belongs to the experts either way; never fall through.
712 return True
713
714 consumed = False
715 for param_name, weight_name, expert_id, shard_id in split_expert_params_mapping:
716 if weight_name not in name:
717 continue
718 consumed = True
719 name_mapped = name.replace(weight_name, param_name)
720 if name_mapped not in params_dict:
721 continue
722 param = params_dict[name_mapped]
723 weight_loader = typing.cast(Callable[..., bool], param.weight_loader)
724 if weight_loader(
725 param,
726 loaded_weight,
727 name_mapped,
728 shard_id=shard_id,
729 expert_id=expert_id,
730 return_success=True,
731 ):
732 loaded_params.add(name_mapped)
733 return consumed
734
735 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
736 params_dict = dict(self.named_parameters())
737 pp_missing_layer_names = get_pp_missing_layer_names(self)
738 loaded_params: set[str] = set()
739
740 mtp_start = self.config.num_hidden_layers
741 shared_weights = {
742 "model.embed_tokens.weight": "model.embed_tokens.weight",
743 "lm_head.weight": f"model.layers.{mtp_start}.shared_head.head.weight",
744 }
745
746 num_experts = getattr(self.config, "n_routed_experts", 0)
747 sink_tp_size = get_tensor_model_parallel_world_size()
748 sink_tp_rank = get_tensor_model_parallel_rank()
749 n_local_head = self.config.num_attention_heads // sink_tp_size
750 head_rank_start = n_local_head * sink_tp_rank
751 head_rank_end = n_local_head * (sink_tp_rank + 1)
752
753 # Routed-expert weights come in two checkpoint layouts:
754 # split: mlp.experts.<id>.gate_proj.weight
755 # fused: mlp.experts.gate_up_proj (all experts in one tensor)
756 # The split layout is resolved through the shared EPLB helper so the
757 # target param names stay in sync with the RoutedExperts module layout;
758 # the fused layout is resolved from the live parameter names.
759 split_expert_params_mapping = fused_moe_make_expert_params_mapping(
760 self,
761 ckpt_gate_proj_name="gate_proj",
762 ckpt_down_proj_name="down_proj",
763 ckpt_up_proj_name="up_proj",
764 num_experts=num_experts,
765 )
766 fused_expert_param_names: dict[tuple[str, str], str] = {}
767 for param_name in params_dict:
768 for tag in ("w13_weight", "w2_weight"):
769 if param_name.endswith(tag) and ".experts." in param_name:
770 base = param_name.split(".experts.")[0]
771 fused_expert_param_names[base, tag] = param_name
772 stacked_mapping = [
773 (".gate_up_proj", ".gate_proj", 0),
774 (".gate_up_proj", ".up_proj", 1),
775 # MLA runs both latent down-projections as one GEMM.
776 (".fused_qkv_a_proj", ".q_a_proj", 0),
777 (".fused_qkv_a_proj", ".kv_a_proj_with_mqa", 1),
778 ]
779 # Sparse DSA draft blocks build an Indexer whose wk / weights_proj are
780 # fused into a single MergedColumnParallelLinear (wk_weights_proj).
781 indexer_stacked_mapping = [
782 (".wk_weights_proj", ".wk", 0),
783 (".wk_weights_proj", ".weights_proj", 1),
784 ]
785 # FP8 indexer wk dequant buffer (weight and scale arrive separately).
786 pending_wk_fp8: dict[str, dict[str, torch.Tensor]] = {}
787
788 for name, loaded_weight in weights:
789 if name in shared_weights:
790 target_name = shared_weights[name]
791 if target_name in params_dict:
792 param = params_dict[target_name]
793 weight_loader = getattr(
794 param, "weight_loader", default_weight_loader
795 )
796 weight_loader(param, loaded_weight)
797 loaded_params.add(target_name)
798 continue
799
800 spec_layer = None
801 if name.startswith("model.mtp_layers."):
802 parts = name.split(".")
803 if len(parts) > 3 and parts[2].isdigit():
804 spec_layer = mtp_start + int(parts[2])
805 name = name.replace(
806 f"model.mtp_layers.{parts[2]}.",
807 f"model.layers.{spec_layer}.",
808 )
809 else:
810 spec_layer = _get_spec_layer_idx_from_weight_name(self.config, name)
811
812 if spec_layer is None:
813 continue
814
815 name = self._rewrite_spec_layer_name(spec_layer, name)
816 if name == "__skip__":
817 continue
818 name, loaded_weight = _prepare_mtp_fp8_expert_scale(
819 self.quant_config, name, loaded_weight
820 )
821
822 if "mlp.gate.e_score_correction_bias" in name:
823 name = name.replace("gate.e_score_correction_bias", "expert_bias")
824
825 is_loaded = False
826 for param_name, weight_name, shard_id in stacked_mapping:
827 if weight_name not in name or ".experts." in name:
828 continue
829 name_mapped = name.replace(weight_name, param_name)
830 if name_mapped not in params_dict:
831 if is_pp_missing_parameter(name_mapped, self):
832 is_loaded = True
833 break
834 param = params_dict[name_mapped]
835 param.weight_loader(param, loaded_weight, shard_id)
836 loaded_params.add(name_mapped)
837 is_loaded = True
838 break
839 if is_loaded:
840 continue
841
842 # FP8 indexer wk: dequantize to BF16 and load into the fused
843 # wk_weights_proj. PP-aware (skips layers not held by this rank).
844 if _try_load_fp8_indexer_wk(
845 name,
846 loaded_weight,
847 pending_wk_fp8,
848 params_dict,
849 loaded_params,
850 pp_missing_layer_names,
851 ):
852 continue
853
854 # BF16 indexer wk / weights_proj: merge into the fused param.
855 is_loaded = False
856 for param_name, weight_name, shard_id in indexer_stacked_mapping:
857 if weight_name not in name or "wk_weights" in name:
858 continue
859 name_mapped = name.replace(weight_name, param_name)
860 if name_mapped not in params_dict:
861 if is_pp_missing_parameter(name_mapped, self):
862 is_loaded = True
863 break
864 param = params_dict[name_mapped]
865 param.weight_loader(param, loaded_weight, shard_id)
866 loaded_params.add(name_mapped)
867 is_loaded = True
868 break
869 if is_loaded:
870 continue
871
872 is_loaded = False
873 if ".experts." in name:
874 is_loaded = self._load_expert_weight(
875 name,
876 loaded_weight,
877 params_dict,
878 loaded_params,
879 split_expert_params_mapping,
880 fused_expert_param_names,
881 num_experts,
882 )
883 if is_loaded:
884 continue
885
886 if "learnable_sink_param" in name:
887 if name in params_dict:
888 narrow_weight = loaded_weight[head_rank_start:head_rank_end]
889 n = narrow_weight.shape[0]
890 with torch.no_grad():
891 params_dict[name][:n].copy_(narrow_weight)
892 loaded_params.add(name)
893 continue
894
895 remapped_name = maybe_remap_kv_scale_name(name, params_dict)
896 if remapped_name is None:
897 continue
898 name = remapped_name
899 if name not in params_dict:
900 if is_pp_missing_parameter(name, self):
901 continue
902 if _should_skip_missing_mtp_scale_param(self.quant_config, name):
903 continue
904 logger.warning_once("Skipping unknown MTP weight: %s", name)
905 continue
906
907 param = params_dict[name]
908 weight_loader = getattr(param, "weight_loader", default_weight_loader)
909 weight_loader(param, loaded_weight)
910 loaded_params.add(name)
911
912 logger.info_once("HYV4 MTP draft model loaded: %d params", len(loaded_params))
913 unassigned_all = sorted(set(params_dict) - loaded_params)
914 # KVCacheScaleParameter (``k_scale`` / ``v_scale`` / ``q_scale`` /
915 # ``prob_scale``) is created by ``BaseKVCacheMethod.create_weights`` for
916 # every Attention layer that runs on an fp8-family quant method, and
917 # its -1.0 sentinel is replaced by the runtime default (1.0) in
918 # ``process_weights_after_loading`` -- which runs *after* this method.
919 # HY V4 checkpoints intentionally omit these (``kv_cache_quant_algo:
920 # null``); silence them only when the parameter is actually the
921 # sentinel type so a genuine mismatch on a same-named tensor still
922 # reaches the warning.
923 from vllm.model_executor.layers.quantization.kv_cache import (
924 KVCacheScaleParameter,
925 )
926
927 unassigned = [
928 name
929 for name in unassigned_all
930 if not isinstance(params_dict.get(name), KVCacheScaleParameter)
931 ]
932 if unassigned:
933 # A draft parameter with no checkpoint source keeps its sentinel
934 # init value (FP8 scales start at finfo(float32).min), which
935 # silently destroys draft quality instead of failing the load.
936 logger.warning(
937 "HYV4 MTP draft model: %d parameters received no checkpoint value: %s",
938 len(unassigned),
939 ", ".join(unassigned),
940 )
941 return loaded_params