main
py 922 lines 33.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import typing
4 from collections.abc import Callable, Iterable
5 from itertools import islice
6
7 import regex as re
8 import torch
9 import torch.nn as nn
10
11 from vllm.compilation.decorators import support_torch_compile
12 from vllm.config import VllmConfig
13 from vllm.distributed import (
14 get_pp_group,
15 get_tensor_model_parallel_rank,
16 get_tensor_model_parallel_world_size,
17 )
18 from vllm.model_executor.layers.activation import SiluAndMul, SiluAndMulWithClamp
19 from vllm.model_executor.layers.fused_moe import (
20 FusedMoEFactory,
21 GateLinear,
22 fused_moe_make_expert_params_mapping,
23 )
24 from vllm.model_executor.layers.fused_moe.utils import (
25 is_model_fused_shared_expert_compatible,
26 )
27 from vllm.model_executor.layers.layernorm import RMSNorm
28 from vllm.model_executor.layers.linear import (
29 MergedColumnParallelLinear,
30 RowParallelLinear,
31 )
32 from vllm.model_executor.layers.logits_processor import LogitsProcessor
33 from vllm.model_executor.layers.mhc import (
34 HAS_AITER_MHC,
35 HAS_TILELANG_MHC,
36 HCHeadOp,
37 MHCFusedPostPreOp,
38 MHCPostOp,
39 MHCPreOp,
40 )
41 from vllm.model_executor.layers.quantization import QuantizationConfig
42 from vllm.model_executor.layers.vocab_parallel_embedding import (
43 ParallelLMHead,
44 VocabParallelEmbedding,
45 )
46 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
47 from vllm.model_executor.models.interfaces import (
48 EagleModelMixin,
49 SupportsEagle3,
50 SupportsPP,
51 )
52 from vllm.model_executor.models.utils import (
53 AutoWeightsLoader,
54 PPMissingLayer,
55 WeightsMapper,
56 extract_layer_index,
57 is_pp_missing_parameter,
58 make_layers,
59 maybe_prefix,
60 )
61 from vllm.models.deepseek_v4.cpu.cpu_sparse import DeepseekV4CPUAttention
62 from vllm.platforms import current_platform
63 from vllm.sequence import IntermediateTensors
64
65
66 class DeepseekV4MLP(nn.Module):
67 def __init__(
68 self,
69 hidden_size: int,
70 intermediate_size: int,
71 hidden_act: str,
72 swiglu_limit: float | None = None,
73 quant_config: QuantizationConfig | None = None,
74 reduce_results: bool = True,
75 is_sequence_parallel: bool = False,
76 prefix: str = "",
77 ) -> None:
78 super().__init__()
79
80 # is_sequence_parallel: weights are replicated, no collective ops
81 # needed. Otherwise: standard TP with an allreduce at the end.
82 self.gate_up_proj = MergedColumnParallelLinear(
83 hidden_size,
84 [intermediate_size] * 2,
85 bias=False,
86 quant_config=quant_config,
87 disable_tp=is_sequence_parallel,
88 prefix=f"{prefix}.gate_up_proj",
89 )
90 self.down_proj = RowParallelLinear(
91 intermediate_size,
92 hidden_size,
93 bias=False,
94 quant_config=quant_config,
95 reduce_results=reduce_results,
96 disable_tp=is_sequence_parallel,
97 prefix=f"{prefix}.down_proj",
98 )
99 if hidden_act != "silu":
100 raise ValueError(
101 f"Unsupported activation: {hidden_act}. Only silu is supported for now."
102 )
103 if swiglu_limit is not None:
104 self.act_fn = SiluAndMulWithClamp(swiglu_limit)
105 else:
106 self.act_fn = SiluAndMul()
107
108 def forward(self, x):
109 gate_up, _ = self.gate_up_proj(x)
110 x = self.act_fn(gate_up)
111 x, _ = self.down_proj(x)
112 return x
113
114
115 class DeepseekV4MoE(nn.Module):
116 def __init__(
117 self,
118 vllm_config: VllmConfig,
119 prefix: str = "",
120 ):
121 super().__init__()
122
123 self.tp_size = get_tensor_model_parallel_world_size()
124 config = vllm_config.model_config.hf_config
125 quant_config = vllm_config.quant_config
126 self.prefix = prefix
127
128 self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
129 self.hidden_size = config.hidden_size
130
131 self.n_routed_experts = config.n_routed_experts
132 self.n_activated_experts = config.num_experts_per_tok
133 self.moe_intermediate_size = config.moe_intermediate_size
134 self.swiglu_limit = config.swiglu_limit
135 self.renormalize = config.norm_topk_prob
136 self.scoring_func = getattr(config, "scoring_func", "sqrtsoftplus")
137
138 self.gate = GateLinear(
139 input_size=config.hidden_size,
140 output_size=config.n_routed_experts,
141 bias=False,
142 out_dtype=torch.float32,
143 prefix=f"{prefix}.gate",
144 )
145
146 self.gate.e_score_correction_bias = None
147 self.gate.tid2eid = None
148 is_hash_moe = extract_layer_index(prefix) < config.num_hash_layers
149 self.hash_indices_dtype = torch.int32
150 if is_hash_moe:
151 # hash MoE doesn't use e_score_correction_bias
152 # Use randint instead of empty to avoid garbage values causing
153 # invalid memory access in dummy mode (--load-format="dummy")
154 self.gate.tid2eid = nn.Parameter(
155 torch.randint(
156 0,
157 config.n_routed_experts,
158 (config.vocab_size, config.num_experts_per_tok),
159 dtype=self.hash_indices_dtype,
160 ),
161 requires_grad=False,
162 )
163 elif getattr(config, "topk_method", None) == "noaux_tc":
164 self.gate.e_score_correction_bias = nn.Parameter(
165 torch.empty(config.n_routed_experts, dtype=torch.float32),
166 requires_grad=False,
167 )
168
169 self.n_shared_experts = config.n_shared_experts
170
171 # No CPU fused-shared-expert grouped-GEMM path yet.
172 self.is_fused_shared_expert_enabled = False
173
174 if config.n_shared_experts is None or self.is_fused_shared_expert_enabled:
175 self.shared_experts = None
176 else:
177 intermediate_size = config.moe_intermediate_size * config.n_shared_experts
178
179 self.shared_experts = DeepseekV4MLP(
180 hidden_size=config.hidden_size,
181 intermediate_size=intermediate_size,
182 hidden_act=config.hidden_act,
183 swiglu_limit=self.swiglu_limit,
184 quant_config=quant_config,
185 reduce_results=False,
186 prefix=f"{prefix}.shared_experts",
187 )
188
189 self.tp_rank = get_tensor_model_parallel_rank()
190 assert config.n_routed_experts % self.tp_size == 0
191
192 self.n_local_experts = config.n_routed_experts // self.tp_size
193 self.experts_start_idx = self.tp_rank * self.n_local_experts
194 self.experts_end_idx = self.experts_start_idx + self.n_local_experts
195
196 self.experts = FusedMoEFactory(
197 shared_experts=self.shared_experts,
198 n_shared_experts=(
199 config.n_shared_experts if self.is_fused_shared_expert_enabled else None
200 ),
201 fuse_shared_experts=self.is_fused_shared_expert_enabled,
202 gate=self.gate,
203 num_experts=config.n_routed_experts,
204 top_k=config.num_experts_per_tok,
205 hidden_size=config.hidden_size,
206 intermediate_size=config.moe_intermediate_size,
207 renormalize=config.norm_topk_prob,
208 quant_config=quant_config,
209 prefix=f"{prefix}.experts",
210 scoring_func=self.scoring_func,
211 routed_scaling_factor=self.routed_scaling_factor,
212 e_score_correction_bias=self.gate.e_score_correction_bias,
213 hash_indices_table=self.gate.tid2eid,
214 swiglu_limit=self.swiglu_limit,
215 router_logits_dtype=torch.float32,
216 )
217 if is_hash_moe:
218 # apply_monolithic() never sees hash_indices_table via the
219 # router path (modular-only) -- stash it on RoutedExperts
220 # directly. object.__setattr__ avoids nn.Module re-registering
221 # (and duplicating in the state_dict) this alias.
222 object.__setattr__(
223 self.experts.routed_experts, "hash_indices_table", self.gate.tid2eid
224 )
225
226 def forward(
227 self, hidden_states: torch.Tensor, input_ids: torch.Tensor | None = None
228 ) -> torch.Tensor:
229 if self.gate.tid2eid is not None and input_ids is None:
230 raise ValueError("DeepSeek V4 hash MoE routing requires input_ids.")
231
232 org_shape = hidden_states.shape
233 final_hidden_states = self.experts(
234 hidden_states=hidden_states,
235 router_logits=hidden_states,
236 input_ids=input_ids,
237 )
238
239 return final_hidden_states.view(org_shape)
240
241
242 class DeepseekV4DecoderLayer(nn.Module):
243 def __init__(
244 self,
245 vllm_config,
246 prefix,
247 topk_indices_buffer: torch.Tensor | None = None,
248 ):
249 super().__init__()
250
251 # Lazy import to avoid top-level tilelang dependency.
252 # Registers both torch.ops.vllm.mhc_pre and mhc_post
253 import vllm.model_executor.layers.mhc # noqa: F401
254
255 config = vllm_config.model_config.hf_config
256 self.hidden_size = config.hidden_size
257
258 self.rms_norm_eps = config.rms_norm_eps
259 self.attn = DeepseekV4CPUAttention(
260 vllm_config,
261 prefix=f"{prefix}.attn",
262 topk_indices_buffer=topk_indices_buffer,
263 )
264 self.ffn = DeepseekV4MoE(vllm_config, prefix=f"{prefix}.ffn")
265
266 self.attn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
267 self.ffn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
268 self.hc_mult = config.hc_mult
269 self.hc_sinkhorn_iters = config.hc_sinkhorn_iters
270 self.hc_eps = config.hc_eps
271 self.hc_post_alpha = 2.0
272 mix_hc = (2 + self.hc_mult) * self.hc_mult
273 hc_dim = self.hc_mult * self.hidden_size
274 self.hc_attn_fn = nn.Parameter(
275 torch.empty(
276 (mix_hc, hc_dim),
277 dtype=torch.float32,
278 ),
279 requires_grad=False,
280 )
281 self.hc_ffn_fn = nn.Parameter(
282 torch.empty(
283 (mix_hc, hc_dim),
284 dtype=torch.float32,
285 ),
286 requires_grad=False,
287 )
288 self.hc_attn_base = nn.Parameter(
289 torch.empty(
290 mix_hc,
291 dtype=torch.float32,
292 ),
293 requires_grad=False,
294 )
295 self.hc_ffn_base = nn.Parameter(
296 torch.empty(
297 mix_hc,
298 dtype=torch.float32,
299 ),
300 requires_grad=False,
301 )
302 self.hc_attn_scale = nn.Parameter(
303 torch.empty(
304 3,
305 dtype=torch.float32,
306 ),
307 requires_grad=False,
308 )
309 self.hc_ffn_scale = nn.Parameter(
310 torch.empty(
311 3,
312 dtype=torch.float32,
313 ),
314 requires_grad=False,
315 )
316 self.mhc_pre = MHCPreOp()
317 self.mhc_post = MHCPostOp()
318 self.mhc_fused_post_pre = MHCFusedPostPreOp()
319 self.use_fused_mhc = HAS_TILELANG_MHC and not (
320 HAS_AITER_MHC and self.hidden_size % 256 == 0
321 )
322
323 def hc_pre(
324 self,
325 x: torch.Tensor,
326 hc_fn: torch.Tensor,
327 hc_scale: torch.Tensor,
328 hc_base: torch.Tensor,
329 ):
330 post_mix, res_mix, layer_input = self.mhc_pre(
331 residual=x,
332 fn=hc_fn,
333 hc_scale=hc_scale,
334 hc_base=hc_base,
335 rms_eps=self.rms_norm_eps,
336 hc_pre_eps=self.hc_eps,
337 hc_sinkhorn_eps=self.hc_eps,
338 hc_post_mult_value=self.hc_post_alpha,
339 sinkhorn_repeat=self.hc_sinkhorn_iters,
340 )
341 return layer_input, post_mix, res_mix
342
343 def hc_post(
344 self,
345 x: torch.Tensor,
346 residual: torch.Tensor,
347 post: torch.Tensor,
348 comb: torch.Tensor,
349 ):
350 return self.mhc_post(x, residual, post, comb)
351
352 def _forward_fused_post_pre(
353 self,
354 x: torch.Tensor,
355 positions: torch.Tensor,
356 input_ids: torch.Tensor | None,
357 post_mix: torch.Tensor | None = None,
358 res_mix: torch.Tensor | None = None,
359 residual: torch.Tensor | None = None,
360 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
361 if residual is None:
362 # Run standalone hc_pre on first layer
363 residual = x
364 x, post_mix, res_mix = self.hc_pre(
365 x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base
366 )
367 else:
368 residual, post_mix, res_mix, x = self.mhc_fused_post_pre(
369 x,
370 residual,
371 post_mix,
372 res_mix,
373 self.hc_attn_fn,
374 self.hc_attn_scale,
375 self.hc_attn_base,
376 self.rms_norm_eps,
377 self.hc_eps,
378 self.hc_eps,
379 self.hc_post_alpha,
380 self.hc_sinkhorn_iters,
381 )
382
383 x = self.attn_norm(x)
384 x = self.attn(positions, x, None)
385
386 residual, post_mix, res_mix, x = self.mhc_fused_post_pre(
387 x,
388 residual,
389 post_mix,
390 res_mix,
391 self.hc_ffn_fn,
392 self.hc_ffn_scale,
393 self.hc_ffn_base,
394 self.rms_norm_eps,
395 self.hc_eps,
396 self.hc_eps,
397 self.hc_post_alpha,
398 self.hc_sinkhorn_iters,
399 )
400 x = self.ffn_norm(x)
401 x = self.ffn(x, input_ids)
402 return x, residual, post_mix, res_mix
403
404 def _forward_unfused_post_pre(
405 self,
406 x: torch.Tensor,
407 positions: torch.Tensor,
408 input_ids: torch.Tensor | None,
409 post_mix: torch.Tensor | None = None,
410 res_mix: torch.Tensor | None = None,
411 residual: torch.Tensor | None = None,
412 ) -> tuple[
413 torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None
414 ]:
415 residual = x
416 x, post, comb = self.hc_pre(
417 x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base
418 )
419 x = self.attn_norm(x)
420 x = self.attn(positions, x, None)
421 x = self.hc_post(x, residual, post, comb)
422
423 residual = x
424 x, post, comb = self.hc_pre(
425 x, self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base
426 )
427 x = self.ffn_norm(x)
428 x = self.ffn(x, input_ids)
429 x = self.hc_post(x, residual, post, comb)
430 return x, None, None, None
431
432 def forward(
433 self,
434 x: torch.Tensor,
435 positions: torch.Tensor,
436 input_ids: torch.Tensor | None,
437 post_mix: torch.Tensor | None = None,
438 res_mix: torch.Tensor | None = None,
439 residual: torch.Tensor | None = None,
440 ) -> tuple[
441 torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None
442 ]:
443 if not self.use_fused_mhc:
444 return self._forward_unfused_post_pre(
445 x, positions, input_ids, post_mix, res_mix, residual
446 )
447 return self._forward_fused_post_pre(
448 x, positions, input_ids, post_mix, res_mix, residual
449 )
450
451
452 @support_torch_compile(
453 dynamic_arg_dims={
454 "input_ids": 0,
455 "positions": -1,
456 "intermediate_tensors": 0,
457 "inputs_embeds": 0,
458 }
459 )
460 class DeepseekV4Model(nn.Module, EagleModelMixin):
461 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
462 super().__init__()
463
464 config = vllm_config.model_config.hf_config
465 quant_config = vllm_config.quant_config
466 self.config = config
467 self.vocab_size = config.vocab_size
468 self.hc_eps = config.hc_eps
469 self.hc_mult = config.hc_mult
470 self.hc_dim = self.hc_mult * config.hidden_size
471 self.rms_norm_eps = config.rms_norm_eps
472
473 self.device = current_platform.device_type
474 # Reserved topk indices buffer for all Indexer layers to reuse.
475 self.topk_indices_buffer = torch.empty(
476 vllm_config.scheduler_config.max_num_batched_tokens,
477 config.index_topk,
478 dtype=torch.int32,
479 device=self.device,
480 )
481
482 if get_pp_group().is_first_rank:
483 self.embed_tokens = VocabParallelEmbedding(
484 config.vocab_size,
485 config.hidden_size,
486 quant_config=quant_config,
487 prefix=f"{prefix}.embed_tokens",
488 )
489 else:
490 self.embed_tokens = PPMissingLayer()
491
492 self.start_layer, self.end_layer, self.layers = make_layers(
493 config.num_hidden_layers,
494 lambda prefix: DeepseekV4DecoderLayer(
495 vllm_config,
496 prefix=prefix,
497 topk_indices_buffer=self.topk_indices_buffer,
498 ),
499 prefix=f"{prefix}.layers",
500 )
501 self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
502 self.layers,
503 DeepseekV4MoE,
504 "ffn",
505 )
506
507 if get_pp_group().is_last_rank:
508 self.norm = RMSNorm(config.hidden_size, self.rms_norm_eps)
509 else:
510 self.norm = PPMissingLayer()
511
512 self.hc_head_fn = nn.Parameter(
513 torch.empty(
514 self.hc_mult,
515 self.hc_dim,
516 dtype=torch.float32,
517 ),
518 requires_grad=False,
519 )
520 self.hc_head_base = nn.Parameter(
521 torch.empty(
522 self.hc_mult,
523 dtype=torch.float32,
524 ),
525 requires_grad=False,
526 )
527 self.hc_head_scale = nn.Parameter(
528 torch.empty(1, dtype=torch.float32),
529 requires_grad=False,
530 )
531 self.hc_head_op = HCHeadOp()
532 spec_config = vllm_config.speculative_config
533 needs_mtp_hidden_states = spec_config is not None and (
534 spec_config.use_eagle() or spec_config.uses_draft_model()
535 )
536 if get_pp_group().is_last_rank and needs_mtp_hidden_states:
537 self._mtp_hidden_buffer = torch.empty(
538 vllm_config.scheduler_config.max_num_batched_tokens,
539 self.hc_dim,
540 dtype=vllm_config.model_config.dtype,
541 device=self.device,
542 )
543 else:
544 self._mtp_hidden_buffer = None
545
546 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
547 return self.embed_tokens(input_ids)
548
549 def make_empty_intermediate_tensors(
550 self,
551 batch_size: int,
552 dtype: torch.dtype,
553 device: torch.device,
554 ) -> IntermediateTensors:
555 # PP intermediate tensors keep the (num_tokens, hc_mult, hidden_size)
556 # multi-stream shape until hc_head() collapses it.
557 return IntermediateTensors(
558 {
559 "hidden_states": torch.zeros(
560 (batch_size, self.hc_mult, self.config.hidden_size),
561 dtype=dtype,
562 device=device,
563 ),
564 }
565 )
566
567 def forward(
568 self,
569 input_ids: torch.Tensor,
570 positions: torch.Tensor,
571 intermediate_tensors: IntermediateTensors | None,
572 inputs_embeds: torch.Tensor | None = None,
573 ) -> torch.Tensor | IntermediateTensors:
574 if get_pp_group().is_first_rank:
575 if inputs_embeds is not None:
576 hidden_states = inputs_embeds
577 else:
578 hidden_states = self.embed_input_ids(input_ids)
579 hidden_states = hidden_states.unsqueeze(-2).repeat(1, self.hc_mult, 1)
580 else:
581 assert intermediate_tensors is not None
582 hidden_states = intermediate_tensors["hidden_states"]
583
584 residual, post_mix, res_mix = None, None, None
585 # EAGLE3/DSpark/DFlash aux hidden states: post-mhc hidden state at
586 # each target layer, averaged over hc_mult streams to [T,
587 # hidden_size]. Empty unless a draft model set aux_hidden_state_layers.
588 aux_hidden_states: list[torch.Tensor] = []
589 # Reused below to avoid computing hc_post twice if the last layer is
590 # also an aux layer.
591 final_aux_recon: torch.Tensor | None = None
592 for idx, layer in enumerate(
593 islice(self.layers, self.start_layer, self.end_layer),
594 start=self.start_layer,
595 ):
596 hidden_states, residual, post_mix, res_mix = layer(
597 hidden_states,
598 positions,
599 input_ids,
600 post_mix,
601 res_mix,
602 residual,
603 )
604 if (idx + 1) in self.aux_hidden_state_layers:
605 # Unfused path already applied hc_post; fused path needs it
606 # applied here before averaging.
607 if layer.use_fused_mhc:
608 aux_recon = layer.hc_post(
609 hidden_states, residual, post_mix, res_mix
610 )
611 final_aux_recon = aux_recon
612 else:
613 aux_recon = hidden_states
614 aux_hidden_states.append(aux_recon.mean(dim=1))
615 if layer is not None and layer.use_fused_mhc:
616 # Reuse the last layer's hc_post output if already computed above.
617 if (
618 final_aux_recon is not None
619 and self.end_layer in self.aux_hidden_state_layers
620 ):
621 hidden_states = final_aux_recon
622 else:
623 hidden_states = layer.hc_post(
624 hidden_states, residual, post_mix, res_mix
625 )
626
627 if not get_pp_group().is_last_rank:
628 return IntermediateTensors({"hidden_states": hidden_states})
629
630 if self._mtp_hidden_buffer is not None:
631 num_tokens = hidden_states.shape[0]
632 self._mtp_hidden_buffer[:num_tokens].copy_(hidden_states.flatten(1))
633
634 hidden_states = self.hc_head_op(
635 hidden_states,
636 self.hc_head_fn,
637 self.hc_head_scale,
638 self.hc_head_base,
639 self.rms_norm_eps,
640 self.hc_eps,
641 )
642 hidden_states = self.norm(hidden_states)
643 if len(aux_hidden_states) > 0:
644 return hidden_states, aux_hidden_states
645 return hidden_states
646
647 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
648 stacked_params_mapping = [
649 # (param_name, shard_name, shard_id)
650 ("gate_up_proj", "w1", 0),
651 ("gate_up_proj", "w3", 1),
652 ("attn.fused_wqa_wkv", "attn.wq_a", 0),
653 ("attn.fused_wqa_wkv", "attn.wkv", 1),
654 ("compressor.fused_wkv_wgate", "compressor.wkv", 0),
655 ("compressor.fused_wkv_wgate", "compressor.wgate", 1),
656 ]
657 params_dict = dict(self.named_parameters())
658 loaded_params: set[str] = set()
659
660 def _resolve_param_name(name: str) -> str:
661 inv_name = f"{name}_inv"
662 if name not in params_dict and inv_name in params_dict:
663 return inv_name
664 return name
665
666 # TP for attention
667 tp_size = get_tensor_model_parallel_world_size()
668 tp_rank = get_tensor_model_parallel_rank()
669 n_head = self.config.num_attention_heads
670 n_local_head = n_head // tp_size
671 head_rank_start = n_local_head * tp_rank
672 head_rank_end = n_local_head * (tp_rank + 1)
673
674 # Pre-compute expert mapping ONCE.
675 expert_mapping = self.get_expert_mapping()
676
677 # Each MoE's own per-layer fusion decision is the source of truth,
678 # so the redirect below can't diverge from how it was built.
679 fuse_by_layer = {
680 extract_layer_index(mod_name): mod.is_fused_shared_expert_enabled
681 for mod_name, mod in self.named_modules()
682 if isinstance(mod, DeepseekV4MoE)
683 }
684 n_routed = self.config.n_routed_experts
685 # Maps the single shared-expert tensor group to one appended slot;
686 # multiple shared experts would need per-expert slicing (see
687 # deepseek_v2.py). DeepSeek-V4 has n_shared_experts == 1.
688 if any(fuse_by_layer.values()) and self.config.n_shared_experts != 1:
689 raise NotImplementedError(
690 "deepseek-v4 fused shared-expert loading supports only "
691 f"n_shared_experts == 1, got {self.config.n_shared_experts}"
692 )
693
694 for name, loaded_weight in weights:
695 # Shared-expert fusion: redirect shared_experts.w{1,2,3} into
696 # the appended routed-expert slot so it loads through the
697 # routed expert (grouped GEMM) loader.
698 if ".ffn.shared_experts.w" in name and fuse_by_layer.get(
699 extract_layer_index(name), False
700 ):
701 name = name.replace(
702 ".ffn.shared_experts.w",
703 f".ffn.experts.{n_routed}.w",
704 )
705
706 for param_name, weight_name, shard_id in stacked_params_mapping:
707 # Skip non-stacked layers and experts (experts handled below).
708 if ".experts." in name:
709 continue
710 if weight_name not in name:
711 continue
712 name = name.replace(weight_name, param_name)
713
714 if is_pp_missing_parameter(name, self):
715 break
716 param_name = _resolve_param_name(name)
717 param = params_dict[param_name]
718 weight_loader = param.weight_loader
719 weight_loader(param, loaded_weight, shard_id)
720 loaded_params.add(param_name)
721 break
722 else:
723 if ".experts." in name:
724 # E8M0 scales are float8_e8m0fnu in checkpoints but the
725 # MoE param is uint8; copy_() would numerically convert
726 # (e.g. 2^-7 -> 0) instead of preserving raw bytes.
727 if (
728 "weight_scale" in name
729 and loaded_weight.dtype == torch.float8_e8m0fnu
730 ):
731 loaded_weight = loaded_weight.view(torch.uint8)
732 for mapping in expert_mapping:
733 param_name, weight_name, expert_id, expert_shard_id = mapping
734 if weight_name not in name:
735 continue
736 name_mapped = name.replace(weight_name, param_name)
737 if is_pp_missing_parameter(name_mapped, self):
738 continue
739 param = params_dict[name_mapped]
740 # return_success lets us skip experts that have
741 # another replica available.
742 weight_loader = typing.cast(
743 Callable[..., bool], param.weight_loader
744 )
745 success = weight_loader(
746 param,
747 loaded_weight,
748 name_mapped,
749 shard_id=expert_shard_id,
750 expert_id=expert_id,
751 return_success=True,
752 )
753 if success:
754 name = name_mapped
755 break
756 loaded_params.add(name_mapped)
757 continue
758 elif "attn_sink" in name:
759 if is_pp_missing_parameter(name, self):
760 continue
761 narrow_weight = loaded_weight[head_rank_start:head_rank_end]
762 n = narrow_weight.shape[0]
763 params_dict[name][:n].copy_(narrow_weight)
764 loaded_params.add(name)
765 continue
766 else:
767 if is_pp_missing_parameter(name, self):
768 continue
769 param_name = _resolve_param_name(name)
770 param = params_dict[param_name]
771 weight_loader = getattr(
772 param, "weight_loader", default_weight_loader
773 )
774 weight_loader(param, loaded_weight)
775 loaded_params.add(param_name)
776 continue
777
778 return loaded_params
779
780 def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
781 # (param_name, weight_name, expert_id, shard_id). When fusing shared
782 # experts, include the appended slots so redirected shared-expert
783 # weights route through the expert loader.
784 n_shared = getattr(self.config, "n_shared_experts", 0) or 0
785 num_experts = self.config.n_routed_experts + (
786 n_shared if self.is_fused_shared_expert_enabled else 0
787 )
788 return fused_moe_make_expert_params_mapping(
789 self,
790 ckpt_gate_proj_name="w1",
791 ckpt_down_proj_name="w2",
792 ckpt_up_proj_name="w3",
793 num_experts=num_experts,
794 )
795
796
797 def _make_deepseek_v4_weights_mapper(
798 expert_dtype: str, fuse_shared_experts: bool = False
799 ) -> WeightsMapper:
800 if expert_dtype == "fp4":
801 # MXFP4 experts register scales as ``w{1,2,3}_weight_scale`` (no
802 # _inv); FP8 linear/shared-expert scales register as
803 # ``weight_scale_inv``. Route DeepSeek-native ``.scale`` and
804 # AMD-Quark ``.weight_scale`` keys accordingly, leaving expert/
805 # shared-expert ``w{1,2,3}.weight_scale`` untouched for the MXFP4
806 # loader.
807 scale_regex = {
808 re.compile(r"(\.experts\.\d+\.w[123])\.scale$"): r"\1.weight_scale",
809 re.compile(r"\.scale$"): ".weight_scale_inv",
810 re.compile(r"(?<!\.w[123])\.weight_scale$"): ".weight_scale_inv",
811 }
812 else:
813 # FP8 experts use Fp8MoEMethod (block_quant=True), which registers
814 # scales as ``w{13,2}_weight_scale_inv``. Map all ``.scale`` keys
815 # there.
816 scale_regex = {
817 re.compile(r"\.scale$"): ".weight_scale_inv",
818 }
819 # Fused shared experts are redirected to routed expert slots elsewhere;
820 # leave their names untouched here.
821 substr_map: dict[str, str | None] = (
822 {}
823 if fuse_shared_experts
824 else {".shared_experts.w2": ".shared_experts.down_proj"}
825 )
826 # No MTP support on CPU yet: drop mtp.* weights (no model.mtp submodule).
827 substr_map["mtp."] = None
828 return WeightsMapper(
829 orig_to_new_prefix={
830 "layers.": "model.layers.",
831 "embed.": "model.embed.",
832 "norm.": "model.norm.",
833 "hc_head": "model.hc_head",
834 "mtp.": "model.mtp.",
835 },
836 orig_to_new_regex=scale_regex,
837 orig_to_new_suffix={
838 "head.weight": "lm_head.weight",
839 "embed.weight": "embed_tokens.weight",
840 ".ffn.gate.bias": ".ffn.gate.e_score_correction_bias",
841 ".input_scale": ".input_scale_2",
842 },
843 orig_to_new_substr=substr_map,
844 )
845
846
847 class DeepseekV4ForCausalLM(nn.Module, SupportsPP, SupportsEagle3):
848 model_cls = DeepseekV4Model
849
850 # Default mapper assumes the original FP4-expert checkpoint layout.
851 # Overridden per-instance in __init__ when expert_dtype != "fp4".
852 hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper("fp4")
853 packed_modules_mapping = {
854 "gate_up_proj": ["w1", "w3"],
855 "fused_wqa_wkv": ["wq_a", "wkv"],
856 }
857
858 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
859 super().__init__()
860
861 config = vllm_config.model_config.hf_config
862 self.config = config
863 expert_dtype = getattr(config, "expert_dtype", "fp4")
864 self.model = self.model_cls(
865 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
866 )
867 if expert_dtype != "fp4" or self.model.is_fused_shared_expert_enabled:
868 self.hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper(
869 expert_dtype,
870 fuse_shared_experts=self.model.is_fused_shared_expert_enabled,
871 )
872 if get_pp_group().is_last_rank:
873 self.lm_head = ParallelLMHead(
874 config.vocab_size,
875 config.hidden_size,
876 prefix=maybe_prefix(prefix, "lm_head"),
877 )
878 else:
879 self.lm_head = PPMissingLayer()
880 self.logits_processor = LogitsProcessor(config.vocab_size)
881 self.make_empty_intermediate_tensors = ( # type: ignore[method-assign]
882 self.model.make_empty_intermediate_tensors
883 )
884
885 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
886 return self.model.embed_input_ids(input_ids)
887
888 def compute_logits(
889 self,
890 hidden_states: torch.Tensor,
891 ) -> torch.Tensor | None:
892 logits = self.logits_processor(self.lm_head, hidden_states)
893 return logits
894
895 def forward(
896 self,
897 input_ids: torch.Tensor,
898 positions: torch.Tensor,
899 intermediate_tensors: IntermediateTensors | None = None,
900 inputs_embeds: torch.Tensor | None = None,
901 ) -> torch.Tensor | IntermediateTensors:
902 hidden_states = self.model(
903 input_ids, positions, intermediate_tensors, inputs_embeds
904 )
905 return hidden_states
906
907 def get_mtp_target_hidden_states(self) -> torch.Tensor | None:
908 """Pre-hc_head residual stream buffer (max_num_batched_tokens,
909 hc_mult * hidden_size) for the MTP draft model. Populated by
910 forward(); valid after each target step."""
911 return getattr(self.model, "_mtp_hidden_buffer", None)
912
913 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
914 loader = AutoWeightsLoader(self)
915 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
916
917 def process_weights_after_loading(self) -> None:
918 # No CPU AMX weight-packing hook yet.
919 pass
920
921 def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
922 return self.model.get_expert_mapping()