main
py 562 lines 22.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """MTP draft model for DeepSeek V4 (internal codename: DeepseekV4).
4
5 Split from ``deepseek_mtp.py`` because the V4 architecture introduces several
6 pieces that have no analogue in V3/V32:
7 * separate ``e_proj`` / ``h_proj`` with fp8 linear quantization (instead of
8 the fused ``eh_proj``);
9 * ``hc_head`` hypercompressed vocab projection applied in ``compute_logits``;
10 * ``DeepseekV4DecoderLayer`` with its own aux-stream management;
11 * V4-specific checkpoint weight-name remapping in ``load_weights``.
12 """
13
14 import typing
15 from collections.abc import Callable, Iterable
16
17 import regex as re
18 import torch
19 import torch.nn as nn
20
21 from vllm.config import VllmConfig
22 from vllm.distributed import (
23 get_tensor_model_parallel_rank,
24 get_tensor_model_parallel_world_size,
25 )
26 from vllm.logger import init_logger
27 from vllm.model_executor.layers.fused_moe import fused_moe_make_expert_params_mapping
28 from vllm.model_executor.layers.layernorm import RMSNorm
29 from vllm.model_executor.layers.linear import ReplicatedLinear
30 from vllm.model_executor.layers.logits_processor import LogitsProcessor
31 from vllm.model_executor.layers.mhc import HCHeadOp
32 from vllm.model_executor.layers.vocab_parallel_embedding import (
33 VocabParallelEmbedding,
34 )
35 from vllm.model_executor.model_loader.mtp_validation import (
36 is_mtp_completeness_check_enabled,
37 )
38 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
39 from vllm.model_executor.models.deepseek_mtp import SharedHead
40 from vllm.model_executor.models.deepseek_v2 import get_spec_layer_idx_from_weight_name
41 from vllm.model_executor.models.utils import maybe_prefix
42 from vllm.models.deepseek_v4.common.ops.fused_mtp_input_rmsnorm import (
43 _FUSED_MTP_INPUT_RMSNORM_KERNEL,
44 _MTP_SHARED_HEAD_RMSNORM_KERNEL,
45 )
46 from vllm.platforms import current_platform
47 from vllm.sequence import IntermediateTensors
48
49 from .model import DeepseekV4DecoderLayer
50
51 logger = init_logger(__name__)
52
53 # MoE expert scales are fused into per-layer w13/w2 tensors. The exact
54 # parameter suffix depends on which MoERunner method handles the experts:
55 # - fp4 experts (Mxfp4MoEMethod) register ``w{1,2,3}_weight_scale``;
56 # - fp8 experts (Fp8MoEMethod with block_quant=True) register
57 # ``w{1,2,3}_weight_scale_inv``.
58 # Other FP8 linear scales (including shared experts) always use
59 # ``.weight_scale_inv``. Mirrors the per-instance mapper built by
60 # ``_make_deepseek_v4_weights_mapper`` in deepseek_v4.py.
61 _EXPERT_SCALE_RE = re.compile(r"\.experts\.\d+\.w[123]\.scale$")
62
63
64 class DeepSeekV4MultiTokenPredictorLayer(nn.Module):
65 def __init__(
66 self,
67 vllm_config: VllmConfig,
68 topk_indices_buffer: torch.Tensor,
69 prefix: str,
70 aux_stream_list: list[torch.cuda.Stream] | None = None,
71 ) -> None:
72 super().__init__()
73
74 assert vllm_config.speculative_config is not None
75 config = vllm_config.speculative_config.draft_model_config.hf_config
76 self.config = config
77 quant_config = vllm_config.quant_config
78 self.rms_norm_eps = config.rms_norm_eps
79
80 self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
81 self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
82
83 # V4 keeps e_ and h_ proj separate (with fp8 linear quant) rather than
84 # fusing them the way V3 does with eh_proj.
85 self.e_proj = ReplicatedLinear(
86 config.hidden_size,
87 config.hidden_size,
88 bias=False,
89 return_bias=False,
90 quant_config=quant_config,
91 prefix=f"{prefix}.e_proj",
92 )
93 self.h_proj = ReplicatedLinear(
94 config.hidden_size,
95 config.hidden_size,
96 bias=False,
97 return_bias=False,
98 quant_config=quant_config,
99 prefix=f"{prefix}.h_proj",
100 )
101
102 self.hc_eps = config.hc_eps
103 self.hc_mult = config.hc_mult
104 self.hc_dim = self.hc_mult * config.hidden_size
105 self.hc_head_fn = nn.Parameter(
106 torch.empty(self.hc_mult, self.hc_dim, dtype=torch.float32),
107 requires_grad=False,
108 )
109 self.hc_head_base = nn.Parameter(
110 torch.empty(self.hc_mult, dtype=torch.float32),
111 requires_grad=False,
112 )
113 self.hc_head_scale = nn.Parameter(
114 torch.empty(1, dtype=torch.float32),
115 requires_grad=False,
116 )
117
118 self.shared_head = SharedHead(
119 config=config, prefix=prefix, quant_config=quant_config
120 )
121 self.mtp_block = DeepseekV4DecoderLayer(
122 vllm_config,
123 prefix,
124 topk_indices_buffer=topk_indices_buffer,
125 aux_stream_list=aux_stream_list,
126 )
127
128 self.hc_head_op = HCHeadOp()
129
130 if vllm_config.kernel_config.enable_jit_warmup:
131 _FUSED_MTP_INPUT_RMSNORM_KERNEL.register_warmup()
132 _MTP_SHARED_HEAD_RMSNORM_KERNEL.register_warmup()
133
134 def forward(
135 self,
136 input_ids: torch.Tensor,
137 positions: torch.Tensor,
138 previous_hidden_states: torch.Tensor,
139 inputs_embeds: torch.Tensor | None = None,
140 spec_step_index: int = 0,
141 ) -> torch.Tensor:
142 assert inputs_embeds is not None
143 # Target stashes pre-hc_head residual as flat (T, hc_mult * D);
144 # reshape to (T, hc_mult, D) — the training-time layout — before
145 # the fused norm pass so both inputs are 3D-friendly.
146 previous_hidden_states = previous_hidden_states.view(
147 -1, self.hc_mult, self.config.hidden_size
148 )
149 # Fused: mask inputs at position 0 (not needed by MTP), enorm, hnorm.
150 inputs_embeds, previous_hidden_states = _FUSED_MTP_INPUT_RMSNORM_KERNEL(
151 inputs_embeds,
152 positions,
153 previous_hidden_states,
154 self.enorm.weight.data,
155 self.hnorm.weight.data,
156 self.enorm.variance_epsilon,
157 self.hc_mult,
158 )
159 hidden_states = self.h_proj(previous_hidden_states) + self.e_proj(
160 inputs_embeds
161 ).unsqueeze(-2)
162 hidden_states, residual, post_mix, res_mix = self.mtp_block(
163 positions=positions, x=hidden_states, input_ids=input_ids
164 )
165 if self.mtp_block.use_fused_mhc:
166 hidden_states = self.mtp_block.hc_post(
167 hidden_states, residual, post_mix, res_mix
168 )
169 # Return the flat pre-hc_head residual so it can be re-fed as the
170 # next spec step's `previous_hidden_states` when
171 # num_speculative_tokens > 1. hc_head is deferred to compute_logits.
172 return hidden_states.flatten(1)
173
174
175 class DeepSeekV4MultiTokenPredictor(nn.Module):
176 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
177 super().__init__()
178 config = vllm_config.model_config.hf_config
179 self.mtp_start_layer_idx = config.num_hidden_layers
180 self.num_mtp_layers = config.num_nextn_predict_layers
181 self.device = current_platform.device_type
182
183 topk_tokens = config.index_topk
184 self.topk_indices_buffer = torch.empty(
185 vllm_config.scheduler_config.max_num_batched_tokens,
186 topk_tokens,
187 dtype=torch.int32,
188 device=self.device,
189 )
190
191 # Three aux streams shared across all MTP layers, mirroring
192 # DeepseekV4Model. ROCm runs the same work serially for now.
193 aux_stream_list = (
194 None
195 if current_platform.is_rocm()
196 else [torch.cuda.Stream() for _ in range(3)]
197 )
198
199 # to map the exact layer index from weights
200 self.layers = torch.nn.ModuleDict(
201 {
202 str(idx): DeepSeekV4MultiTokenPredictorLayer(
203 vllm_config,
204 self.topk_indices_buffer,
205 f"{prefix}.layers.{idx}",
206 aux_stream_list=aux_stream_list,
207 )
208 for idx in range(
209 self.mtp_start_layer_idx,
210 self.mtp_start_layer_idx + self.num_mtp_layers,
211 )
212 }
213 )
214 self.embed_tokens = VocabParallelEmbedding(
215 config.vocab_size,
216 config.hidden_size,
217 prefix=maybe_prefix(prefix, "embed_tokens"),
218 )
219 self.logits_processor = LogitsProcessor(config.vocab_size)
220
221 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
222 return self.embed_tokens(input_ids)
223
224 def forward(
225 self,
226 input_ids: torch.Tensor,
227 positions: torch.Tensor,
228 previous_hidden_states: torch.Tensor,
229 inputs_embeds: torch.Tensor | None = None,
230 spec_step_idx: int = 0,
231 ) -> torch.Tensor:
232 if inputs_embeds is None:
233 inputs_embeds = self.embed_tokens(input_ids)
234 current_step_idx = spec_step_idx % self.num_mtp_layers
235 return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
236 input_ids,
237 positions,
238 previous_hidden_states,
239 inputs_embeds,
240 current_step_idx,
241 )
242
243 def _pre_head(
244 self,
245 mtp_layer: DeepSeekV4MultiTokenPredictorLayer,
246 hidden_states: torch.Tensor,
247 ) -> torch.Tensor:
248 # MTP forward returns the pre-hc_head residual (T, hc_mult * D); apply
249 # hc_head here so logits are computed from the dense hidden state.
250 hidden_states = hidden_states.view(
251 -1, mtp_layer.hc_mult, mtp_layer.config.hidden_size
252 )
253 hidden_states = mtp_layer.hc_head_op(
254 hidden_states,
255 mtp_layer.hc_head_fn,
256 mtp_layer.hc_head_scale,
257 mtp_layer.hc_head_base,
258 mtp_layer.rms_norm_eps,
259 mtp_layer.hc_eps,
260 )
261 return _MTP_SHARED_HEAD_RMSNORM_KERNEL(
262 hidden_states,
263 mtp_layer.shared_head.norm.weight.data,
264 mtp_layer.shared_head.norm.variance_epsilon,
265 )
266
267 def compute_logits(
268 self,
269 hidden_states: torch.Tensor,
270 spec_step_idx: int = 0,
271 ) -> torch.Tensor:
272 current_step_idx = spec_step_idx % self.num_mtp_layers
273 mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
274 logits = self.logits_processor(
275 mtp_layer.shared_head.head, self._pre_head(mtp_layer, hidden_states)
276 )
277 return logits
278
279 def get_top_tokens(
280 self,
281 hidden_states: torch.Tensor,
282 spec_step_idx: int = 0,
283 ) -> torch.Tensor:
284 current_step_idx = spec_step_idx % self.num_mtp_layers
285 mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
286 # Vocab-parallel argmax for the greedy draft: per-rank head projection
287 # + local argmax + a [batch, 2*tp] (value, index) reduce, instead of
288 # all-gathering the full vocab logits.
289 return self.logits_processor.get_top_tokens(
290 mtp_layer.shared_head.head, self._pre_head(mtp_layer, hidden_states)
291 )
292
293
294 class DeepSeekV4MTP(nn.Module):
295 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
296 super().__init__()
297 self.config = vllm_config.model_config.hf_config
298 self.quant_config = vllm_config.quant_config
299 self.model = DeepSeekV4MultiTokenPredictor(
300 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
301 )
302
303 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
304 return self.model.embed_input_ids(input_ids)
305
306 def forward(
307 self,
308 input_ids: torch.Tensor | None,
309 positions: torch.Tensor,
310 hidden_states: torch.Tensor,
311 intermediate_tensors: IntermediateTensors | None = None,
312 inputs_embeds: torch.Tensor | None = None,
313 spec_step_idx: int = 0,
314 ) -> torch.Tensor:
315 hidden_states = self.model(
316 input_ids, positions, hidden_states, inputs_embeds, spec_step_idx
317 )
318 return hidden_states
319
320 def compute_logits(
321 self,
322 hidden_states: torch.Tensor,
323 spec_step_idx: int = 0,
324 ) -> torch.Tensor | None:
325 return self.model.compute_logits(hidden_states, spec_step_idx)
326
327 def get_top_tokens(
328 self,
329 hidden_states: torch.Tensor,
330 spec_step_idx: int = 0,
331 ) -> torch.Tensor:
332 # Greedy-draft path used when use_local_argmax_reduction is enabled:
333 # vocab-parallel argmax, no full-vocab logits.
334 return self.model.get_top_tokens(hidden_states, spec_step_idx)
335
336 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
337 # Weight name remapping for checkpoint compatibility.
338 # Maps checkpoint weight paths to model parameter paths.
339 WEIGHT_NAME_REMAPPING: dict[str, str] = {
340 ".emb.tok_emb.weight": ".embed_tokens.weight",
341 ".head.weight": ".shared_head.head.weight",
342 ".norm.weight": ".shared_head.norm.weight",
343 }
344
345 def _remap_weight_name(name: str) -> str:
346 """Remap checkpoint weight names to model parameter names."""
347 for old_pattern, new_pattern in WEIGHT_NAME_REMAPPING.items():
348 if old_pattern in name:
349 name = name.replace(old_pattern, new_pattern)
350 return name
351
352 def _find_mtp_layer_idx(name: str) -> int:
353 subnames = name.split(".")
354 for subname in subnames:
355 try:
356 # we return the first encountered integer
357 return int(subname)
358 except ValueError:
359 continue
360 return 0
361
362 stacked_params_mapping = [
363 # (param_name, shard_name, shard_id)
364 ("gate_up_proj", "w1", 0),
365 ("gate_up_proj", "w3", 1),
366 ("attn.fused_wqa_wkv", "attn.wq_a", 0),
367 ("attn.fused_wqa_wkv", "attn.wkv", 1),
368 ]
369 params_dict = dict(self.named_parameters())
370 loaded_params: set[str] = set()
371
372 def _resolve_scale_name(name: str) -> str:
373 # Quark checkpoints and QuarkW8A8Fp8PerBlock use ``.weight_scale``.
374 # Native block-FP8 layers register ``.weight_scale_inv``. Rename
375 # to ``_inv`` only when that variant exists and the plain one
376 # does not.
377 if name.endswith(".weight_scale") and name not in params_dict:
378 inv = name.removesuffix(".weight_scale") + ".weight_scale_inv"
379 if inv in params_dict:
380 return inv
381 # Otherwise leave the name unchanged: either it already matches a
382 # param, or it is genuinely unknown and should surface the normal
383 # KeyError downstream rather than be silently rewritten.
384 return name
385
386 # TP for attention
387 tp_size = get_tensor_model_parallel_world_size()
388 tp_rank = get_tensor_model_parallel_rank()
389 n_head = self.config.num_attention_heads
390 n_local_head = n_head // tp_size
391 head_rank_start = n_local_head * tp_rank
392 head_rank_end = n_local_head * (tp_rank + 1)
393
394 # Pre-compute expert mapping ONCE.
395 expert_mapping = fused_moe_make_expert_params_mapping(
396 self,
397 ckpt_gate_proj_name="w1",
398 ckpt_down_proj_name="w2",
399 ckpt_up_proj_name="w3",
400 num_experts=self.config.n_routed_experts,
401 )
402
403 # FP8 experts register ``..._weight_scale_inv`` (block_quant) while
404 # FP4/MXFP4 experts register ``..._weight_scale``. Choose the suffix
405 # for the rename below based on the model's expert dtype.
406 expert_scale_suffix = (
407 ".weight_scale"
408 if getattr(self.config, "expert_dtype", "fp4") == "fp4"
409 else ".weight_scale_inv"
410 )
411
412 for name, loaded_weight in weights:
413 mtp_layer_idx = _find_mtp_layer_idx(name)
414 # V4 checkpoints store MTP weights as `mtp.{i}.*`; remap to
415 # `model.layers.{num_hidden_layers + i}.*` so that
416 # get_spec_layer_idx_from_weight_name can identify them.
417 name = name.replace(
418 f"mtp.{mtp_layer_idx}.",
419 f"model.layers.{self.config.num_hidden_layers + mtp_layer_idx}.",
420 )
421
422 spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
423 if spec_layer is None:
424 continue
425
426 name = _remap_weight_name(name)
427 name = self._rewrite_spec_layer_name(spec_layer, name)
428
429 if spec_layer != self.model.mtp_start_layer_idx and ".layers" not in name:
430 continue
431 if name.endswith(".scale"):
432 suffix = (
433 expert_scale_suffix
434 if _EXPERT_SCALE_RE.search(name)
435 else ".weight_scale_inv"
436 )
437 name = name.removesuffix(".scale") + suffix
438 for param_name, weight_name, shard_id in stacked_params_mapping:
439 # Skip non-stacked layers and experts (experts handled below).
440 if ".experts." in name:
441 continue
442 if weight_name not in name:
443 continue
444 name = name.replace(weight_name, param_name)
445 name = _resolve_scale_name(name)
446
447 param = params_dict[name]
448 weight_loader = param.weight_loader
449 weight_loader(param, loaded_weight, shard_id)
450 loaded_params.add(name)
451 break
452 else:
453 if ".experts." in name:
454 # Reinterpret E8M0 scales as uint8 to preserve raw
455 # exponent bytes; numeric copy_() would zero them.
456 # Mirrors the main DeepseekV4 loader.
457 if (
458 "weight_scale" in name
459 and loaded_weight.dtype == torch.float8_e8m0fnu
460 ):
461 loaded_weight = loaded_weight.view(torch.uint8)
462 for mapping in expert_mapping:
463 param_name, weight_name, expert_id, expert_shard_id = mapping
464 if weight_name not in name:
465 continue
466 name_mapped = name.replace(weight_name, param_name)
467 param = params_dict[name_mapped]
468 # We should ask the weight loader to return success or not
469 # here since otherwise we may skip experts with other
470 # available replicas.
471 weight_loader = typing.cast(
472 Callable[..., bool], param.weight_loader
473 )
474 success = weight_loader(
475 param,
476 loaded_weight,
477 name_mapped,
478 shard_id=expert_shard_id,
479 expert_id=expert_id,
480 return_success=True,
481 )
482 if success:
483 name = name_mapped
484 loaded_params.add(name_mapped)
485 break
486 continue
487 elif "attn_sink" in name:
488 narrow_weight = loaded_weight[head_rank_start:head_rank_end]
489 n = narrow_weight.shape[0]
490 params_dict[name][:n].copy_(narrow_weight)
491 loaded_params.add(name)
492 continue
493 else:
494 if ".shared_experts.w2" in name:
495 name = name.replace(
496 ".shared_experts.w2", ".shared_experts.down_proj"
497 )
498 if name.endswith(".ffn.gate.bias"):
499 name = name.replace(".bias", ".e_score_correction_bias")
500 name = _resolve_scale_name(name)
501 param = params_dict[name]
502 weight_loader = getattr(
503 param, "weight_loader", default_weight_loader
504 )
505 weight_loader(param, loaded_weight)
506 loaded_params.add(name)
507 continue
508
509 loaded_layers: set[int] = set()
510 for param_name in loaded_params:
511 spec_layer = get_spec_layer_idx_from_weight_name(self.config, param_name)
512 if spec_layer is not None:
513 loaded_layers.add(spec_layer)
514 for layer_idx in range(
515 self.model.mtp_start_layer_idx,
516 self.model.mtp_start_layer_idx + self.model.num_mtp_layers,
517 ):
518 if layer_idx not in loaded_layers and is_mtp_completeness_check_enabled():
519 raise ValueError(
520 f"MTP speculative decoding layer {layer_idx} weights "
521 f"missing from checkpoint. The checkpoint may have "
522 f"been quantized without including the MTP layers. "
523 f"Use a checkpoint that includes MTP layer weights, "
524 f"or disable speculative decoding."
525 )
526 logger.info_once("MTP draft model loaded: %d params", len(loaded_params))
527 return loaded_params
528
529 def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
530 """Rewrite the weight name to match the format of the original model.
531 Add .mtp_block for modules in transformer layer block for spec layer
532 and rename shared layer weights to be top level.
533 """
534 spec_layer_weight_names = [
535 "embed_tokens",
536 "enorm",
537 "hnorm",
538 "h_proj",
539 "e_proj",
540 "shared_head",
541 "hc_head_fn",
542 "hc_head_base",
543 "hc_head_scale",
544 ]
545 shared_weight_names = ["embed_tokens"]
546 spec_layer_weight = False
547 shared_weight = False
548 for weight_name in spec_layer_weight_names:
549 if weight_name in name:
550 spec_layer_weight = True
551 if weight_name in shared_weight_names:
552 shared_weight = True
553 break
554 if not spec_layer_weight:
555 # treat rest weights as weights for transformer layer block
556 name = name.replace(
557 f"model.layers.{spec_layer}.", f"model.layers.{spec_layer}.mtp_block."
558 )
559 elif shared_weight:
560 # treat shared weights as top level weights
561 name = name.replace(f"model.layers.{spec_layer}.", "model.")
562 return name