main
py 1,138 lines 46.3 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 import vllm.envs as envs
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.forward_context import get_forward_context, is_forward_context_available
19 from vllm.logger import init_logger
20 from vllm.model_executor.layers.fused_moe import (
21 fused_moe_make_expert_params_mapping,
22 )
23 from vllm.model_executor.layers.layernorm import RMSNorm
24 from vllm.model_executor.layers.logits_processor import LogitsProcessor
25 from vllm.model_executor.layers.mhc import (
26 HAS_AITER_MHC_FUSED_POST_PRE_DELAYED_RMS_NORM,
27 MHCPostOp,
28 MHCPreDelayedOp,
29 )
30 from vllm.model_executor.layers.quantization import QuantizationConfig
31 from vllm.model_executor.layers.vocab_parallel_embedding import (
32 ParallelLMHead,
33 VocabParallelEmbedding,
34 )
35 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
36 from vllm.model_executor.models.interfaces import (
37 EagleModelMixin,
38 MixtureOfExperts,
39 SupportsEagle3,
40 SupportsLoRA,
41 SupportsPP,
42 )
43 from vllm.model_executor.models.utils import (
44 AutoWeightsLoader,
45 PPMissingLayer,
46 WeightsMapper,
47 extract_layer_index,
48 is_pp_missing_parameter,
49 make_layers,
50 maybe_prefix,
51 )
52 from vllm.models.common.ops.sequence_parallel import (
53 sp_all_gather,
54 sp_padding_mask,
55 sp_reduce_scatter,
56 sp_shard,
57 )
58 from vllm.models.deepseek_v4.amd.model import (
59 DeepseekV4MoE as DeepseekV4MoEBase,
60 )
61 from vllm.models.deepseek_v41.amd.rocm import DeepseekV41ROCMAiterMLAAttention
62 from vllm.models.deepseek_v41.attention import DeepseekV4Attention
63 from vllm.sequence import IntermediateTensors
64 from vllm.utils.math_utils import cdiv
65 from vllm.v1.attention.backends.registry import AttentionBackendEnum
66 from vllm.v1.worker.ubatching import dbo_current_ubatch_id
67
68 from ..common.engram import Engram, EngramLayout, NgramHashState
69 from ..common.mm_preprocess import IMAGE_SENTINEL_BASE_ID, image_sentinel_mask
70
71 if typing.TYPE_CHECKING:
72 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
73
74 logger = init_logger(__name__)
75
76
77 class DeepseekV4MoE(DeepseekV4MoEBase):
78 def __init__(
79 self,
80 vllm_config: VllmConfig,
81 prefix: str = "",
82 use_sequence_parallel: bool = False,
83 ):
84 config = vllm_config.model_config.hf_config
85 n_routed_experts = config.n_routed_experts
86 n_activated_experts = config.num_experts_per_tok
87 if extract_layer_index(prefix) >= config.num_hidden_layers:
88 n_routed_experts = (
89 getattr(config, "dspark_n_routed_experts", 0) or n_routed_experts
90 )
91 n_activated_experts = (
92 getattr(config, "dspark_num_experts_per_tok", 0) or n_activated_experts
93 )
94 # The reused ROCm V4 MoE reads expert counts from the HF config. Draft
95 # layers use V4.1's smaller DSpark expert set, so temporarily expose
96 # the per-layer counts while constructing the module, then restore the
97 # shared config for the remaining backbone layers.
98 old_routed = config.n_routed_experts
99 old_activated = config.num_experts_per_tok
100 old_hash_layers = getattr(config, "num_hash_layers", None)
101 config.n_routed_experts = n_routed_experts
102 config.num_experts_per_tok = n_activated_experts
103 config.num_hash_layers = 0
104 try:
105 super().__init__(vllm_config, prefix=prefix)
106 finally:
107 config.n_routed_experts = old_routed
108 config.num_experts_per_tok = old_activated
109 if old_hash_layers is None:
110 delattr(config, "num_hash_layers")
111 else:
112 config.num_hash_layers = old_hash_layers
113
114 # The V4.1 checkpoint contains a second per-expert routing bias for
115 # image sentinel tokens. The reused ROCm V4 MoE predates this field,
116 # so register it here and attach the same Parameter to the router,
117 # matching the CUDA V4.1 implementation. Text tokens continue to use
118 # e_score_correction_bias.
119 self.gate.bias_vl = None
120 self.image_sentinel_lo = 0
121 if getattr(config, "vision_n_layers", 0) > 0:
122 self.gate.bias_vl = nn.Parameter(
123 torch.empty(self.n_routed_experts, dtype=torch.float32),
124 requires_grad=False,
125 )
126 self.image_sentinel_lo = IMAGE_SENTINEL_BASE_ID
127 self.experts.router.bias_vl = self.gate.bias_vl
128 self.experts.router.image_sentinel_lo = self.image_sentinel_lo
129 self.use_mega_moe = False
130 # Expose the MixtureOfExperts protocol fields expected by the V4.1
131 # outer model. The current ROCm runner has no redundant EPLB copies,
132 # so logical and physical expert counts are identical.
133 self.n_logical_experts = self.n_routed_experts
134 self.n_physical_experts = self.n_routed_experts
135 self.n_local_physical_experts = self.n_local_experts
136 self.n_redundant_experts = 0
137
138
139 def _select_dsv4_attn_cls(vllm_config: VllmConfig) -> type[DeepseekV4Attention]:
140 """Pick the ROCm sparse-MLA implementation for DeepSeek V4.1."""
141 backend = vllm_config.attention_config.backend
142 if backend is not None and (
143 backend is not AttentionBackendEnum.ROCM_FLASHMLA_SPARSE_DSV4
144 ):
145 raise ValueError(
146 f"{backend.name} is not supported for DeepSeek V4.1 on ROCm; "
147 "use ROCM_FLASHMLA_SPARSE_DSV4."
148 )
149 return DeepseekV41ROCMAiterMLAAttention
150
151
152 def _use_sequence_parallel(vllm_config: VllmConfig) -> bool:
153 # The ROCm fused-MoE path currently uses ordinary tensor/expert parallel.
154 return False
155
156
157 class DeepseekV4DecoderLayer(nn.Module):
158 def __init__(
159 self,
160 vllm_config,
161 prefix,
162 topk_indices_buffer: torch.Tensor | None = None,
163 aux_stream_list: list[torch.cuda.Stream] | None = None,
164 candidate_block_buffer: torch.Tensor | None = None,
165 engram_layout: EngramLayout | None = None,
166 ):
167 super().__init__()
168 config = vllm_config.model_config.hf_config
169 self.hidden_size = config.hidden_size
170 self.use_sequence_parallel = _use_sequence_parallel(vllm_config)
171
172 self.engram: Engram | None = None
173 if engram_layout is not None:
174 layer_id = extract_layer_index(prefix)
175 if layer_id in engram_layout.layer_ids:
176 self.engram = Engram(
177 config,
178 vllm_config.quant_config,
179 engram_layout,
180 engram_layout.layer_ids.index(layer_id),
181 use_sequence_parallel=self.use_sequence_parallel,
182 prefix=f"{prefix}.engram",
183 )
184
185 self.rms_norm_eps = config.rms_norm_eps
186 self.attn = _select_dsv4_attn_cls(vllm_config)(
187 vllm_config,
188 prefix=f"{prefix}.attn",
189 topk_indices_buffer=topk_indices_buffer,
190 aux_stream_list=aux_stream_list,
191 candidate_block_buffer=candidate_block_buffer,
192 )
193 if self.use_sequence_parallel:
194 self.attn.wo_b.reduce_results = False
195 self.ffn = DeepseekV4MoE(
196 vllm_config,
197 prefix=f"{prefix}.ffn",
198 use_sequence_parallel=self.use_sequence_parallel,
199 )
200
201 self.attn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
202 self.ffn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
203 self.hc_mult = config.hc_mult
204 self.hc_sinkhorn_iters = config.hc_sinkhorn_iters
205 self.hc_eps = config.hc_eps
206 self.hc_post_alpha = 2.0
207 mix_hc = (2 + self.hc_mult) * self.hc_mult
208 hc_dim = self.hc_mult * self.hidden_size
209 self.hc_attn_fn = nn.Parameter(
210 torch.empty(
211 (mix_hc, hc_dim),
212 dtype=torch.float32,
213 ),
214 requires_grad=False,
215 )
216 self.hc_attn_fn_broadcast: torch.Tensor | None = None
217 self.hc_ffn_fn = nn.Parameter(
218 torch.empty(
219 (mix_hc, hc_dim),
220 dtype=torch.float32,
221 ),
222 requires_grad=False,
223 )
224 self.hc_attn_base = nn.Parameter(
225 torch.empty(
226 mix_hc,
227 dtype=torch.float32,
228 ),
229 requires_grad=False,
230 )
231 self.hc_ffn_base = nn.Parameter(
232 torch.empty(
233 mix_hc,
234 dtype=torch.float32,
235 ),
236 requires_grad=False,
237 )
238 self.hc_attn_scale = nn.Parameter(
239 torch.empty(
240 3,
241 dtype=torch.float32,
242 ),
243 requires_grad=False,
244 )
245 self.hc_ffn_scale = nn.Parameter(
246 torch.empty(
247 3,
248 dtype=torch.float32,
249 ),
250 requires_grad=False,
251 )
252 self.mhc_pre_delayed = MHCPreDelayedOp()
253 self.mhc_post = MHCPostOp()
254 # Where aiter's fused seam kernel runs (gfx950), it folds the following
255 # attn_norm / ffn_norm into its collapse, so the separate norms are
256 # skipped for the seams it takes.
257 self.fuse_seam_norm = HAS_AITER_MHC_FUSED_POST_PRE_DELAYED_RMS_NORM
258
259 @staticmethod
260 def _hc_collapse(x: torch.Tensor, pre_mix: torch.Tensor) -> torch.Tensor:
261 """Hyper-connection collapse used by DSpark on ROCm."""
262 return torch.ops.vllm.hc_collapse_triton(x, pre_mix)
263
264 def forward(
265 self,
266 x: torch.Tensor,
267 positions: torch.Tensor,
268 input_ids: torch.Tensor | None,
269 pre_mix: torch.Tensor | None = None,
270 post_mix: torch.Tensor | None = None,
271 res_mix: torch.Tensor | None = None,
272 residual: torch.Tensor | None = None,
273 engram_hashes: torch.Tensor | None = None,
274 engram_mask: torch.Tensor | None = None,
275 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
276 # Layer 0's attention seam projects the 2-D embedding with the folded
277 # hc_attn_fn_broadcast instead of the 4-stream residual with hc_attn_fn.
278 # The fused kernel only takes the latter, so that seam keeps the
279 # separate attn_norm.
280 fuse_attn_norm = self.fuse_seam_norm and not (residual is None and x.dim() == 2)
281 # The reference collapses each sublayer's input with the *previous*
282 # sublayer's pre-mix: attention uses the pre-mix carried in (identity
283 # for the first layer), the FFN uses this layer's attention pre-mix.
284 if residual is None:
285 if x.dim() == 2:
286 # First layer: the stream is the embedding broadcast to hc
287 # copies and the identity pre-mix selects copy 0.
288 assert self.hc_attn_fn_broadcast is not None
289 residual = x.unsqueeze(1).expand(-1, self.hc_mult, -1).contiguous()
290 residual, post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
291 residual,
292 self.hc_attn_fn_broadcast,
293 self.hc_attn_scale,
294 self.hc_attn_base,
295 self.rms_norm_eps,
296 self.hc_eps,
297 self.hc_eps,
298 self.hc_post_alpha,
299 self.hc_sinkhorn_iters,
300 x=x,
301 )
302 else:
303 residual = x
304 residual, post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
305 residual,
306 self.hc_attn_fn,
307 self.hc_attn_scale,
308 self.hc_attn_base,
309 self.rms_norm_eps,
310 self.hc_eps,
311 self.hc_eps,
312 self.hc_post_alpha,
313 self.hc_sinkhorn_iters,
314 pre_mix=pre_mix,
315 norm_weight=self.attn_norm.weight if self.fuse_seam_norm else None,
316 norm_eps=self.attn_norm.variance_epsilon,
317 )
318 else:
319 pre_args = (
320 self.hc_attn_fn,
321 self.hc_attn_scale,
322 self.hc_attn_base,
323 self.rms_norm_eps,
324 self.hc_eps,
325 self.hc_eps,
326 self.hc_post_alpha,
327 self.hc_sinkhorn_iters,
328 )
329 if self.engram is not None and engram_hashes is not None:
330 # Engram injection happens between the previous sublayer's
331 # post and this block's pre, on the full hc stream, so the
332 # mix coefficients see the injected stream. That read of the
333 # residual is what stops the post from fusing into the pre
334 # here, unlike the attention seam below.
335 residual = self.mhc_post(x, residual, post_mix, res_mix)
336 residual = self.engram(
337 residual,
338 engram_hashes[:, self.engram.layer_hash_index],
339 engram_mask,
340 )
341 residual, post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
342 residual,
343 *pre_args,
344 pre_mix=pre_mix,
345 norm_weight=self.attn_norm.weight if self.fuse_seam_norm else None,
346 norm_eps=self.attn_norm.variance_epsilon,
347 )
348 else:
349 (
350 residual,
351 post_mix,
352 res_mix,
353 x,
354 attn_pre,
355 ) = self.mhc_pre_delayed(
356 residual,
357 *pre_args,
358 pre_mix=pre_mix,
359 sublayer_out=x,
360 post_layer_mix=post_mix,
361 comb_res_mix=res_mix,
362 norm_weight=self.attn_norm.weight if self.fuse_seam_norm else None,
363 norm_eps=self.attn_norm.variance_epsilon,
364 )
365 if not fuse_attn_norm:
366 x = self.attn_norm(x)
367
368 if self.use_sequence_parallel:
369 x = sp_all_gather(x)[: positions.shape[0]]
370
371 x = self.attn(positions, x, None)
372 if self.use_sequence_parallel:
373 x = sp_reduce_scatter(x)
374
375 residual, post_mix, res_mix, x, ffn_pre = self.mhc_pre_delayed(
376 residual,
377 self.hc_ffn_fn,
378 self.hc_ffn_scale,
379 self.hc_ffn_base,
380 self.rms_norm_eps,
381 self.hc_eps,
382 self.hc_eps,
383 self.hc_post_alpha,
384 self.hc_sinkhorn_iters,
385 pre_mix=attn_pre,
386 sublayer_out=x,
387 post_layer_mix=post_mix,
388 comb_res_mix=res_mix,
389 norm_weight=self.ffn_norm.weight if self.fuse_seam_norm else None,
390 norm_eps=self.ffn_norm.variance_epsilon,
391 )
392 if not self.fuse_seam_norm:
393 x = self.ffn_norm(x)
394 x = self.ffn(x, input_ids)
395 return x, residual, post_mix, res_mix, ffn_pre
396
397
398 class DeepseekV4Model(nn.Module, EagleModelMixin):
399 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
400 super().__init__()
401
402 config = vllm_config.model_config.hf_config
403 quant_config = vllm_config.quant_config
404 self.config = config
405 self.quant_config = quant_config
406 self.parallel_config = vllm_config.parallel_config
407 self.use_mega_moe = False
408 self.use_sequence_parallel = _use_sequence_parallel(vllm_config)
409 if self.use_mega_moe and not vllm_config.parallel_config.enable_expert_parallel:
410 raise NotImplementedError(
411 "DeepSeek V4 MegaMoE currently requires expert parallel. "
412 "Enable it with --enable-expert-parallel, or pick a different "
413 "moe backend."
414 )
415 self.vocab_size = config.vocab_size
416 self.hc_eps = config.hc_eps
417 self.hc_mult = config.hc_mult
418 self.hc_dim = self.hc_mult * config.hidden_size
419 self.rms_norm_eps = config.rms_norm_eps
420
421 # Three aux streams: one per non-default input GEMM in
422 # DeepseekV4Attention._run_parallel_input_projections
423 # (compressor kv_score, indexer.weights_proj). fused_wqa_wkv stays on
424 # the default stream.
425 aux_stream_list = [torch.cuda.Stream() for _ in range(3)]
426
427 # Reserved topk indices buffer for all Indexer layers to reuse.
428 self.topk_indices_buffer = torch.empty(
429 vllm_config.scheduler_config.max_num_batched_tokens,
430 config.index_topk,
431 dtype=torch.int32,
432 )
433
434 # Two-level candidate filtering: the indexer at
435 # candidate_source_layer_id publishes the top candidate blocks of
436 # compressed positions here; later ratio-1 indexers (24/28/32/36)
437 # mask their scores with it.
438 candidate_source_layer = getattr(config, "candidate_source_layer_id", -1)
439 candidate_topk_blocks = getattr(config, "candidate_topk_blocks", 0)
440 if candidate_source_layer >= 0 and candidate_topk_blocks > 0:
441 self.candidate_block_buffer = torch.empty(
442 vllm_config.scheduler_config.max_num_batched_tokens,
443 candidate_topk_blocks,
444 dtype=torch.int32,
445 )
446 else:
447 self.candidate_block_buffer = None
448
449 if get_pp_group().is_first_rank:
450 self.embed_tokens = VocabParallelEmbedding(
451 config.vocab_size,
452 config.hidden_size,
453 quant_config=quant_config,
454 prefix=f"{prefix}.embed_tokens",
455 )
456 else:
457 self.embed_tokens = PPMissingLayer()
458
459 self.engram_layout = EngramLayout.from_config(config)
460
461 self.start_layer, self.end_layer, self.layers = make_layers(
462 config.num_hidden_layers,
463 lambda prefix: DeepseekV4DecoderLayer(
464 vllm_config,
465 prefix=prefix,
466 topk_indices_buffer=self.topk_indices_buffer,
467 aux_stream_list=aux_stream_list,
468 candidate_block_buffer=self.candidate_block_buffer,
469 engram_layout=self.engram_layout,
470 ),
471 prefix=f"{prefix}.layers",
472 )
473
474 # The n-gram hash needs a slot-keyed rolling store of compressed ids
475 # (chunked prefill / decode lookback); key it off the first local
476 # layer's sliding-window KV cache. Only PP ranks owning an engram
477 # layer need it.
478 self.engram_hash: NgramHashState | None = None
479 self.engram_swa_prefix: str | None = None
480 if self.engram_layout is not None:
481 local_engram = any(
482 isinstance(layer, DeepseekV4DecoderLayer) and layer.engram is not None
483 for layer in islice(self.layers, self.start_layer, self.end_layer)
484 )
485 if local_engram:
486 first_layer = next(
487 iter(islice(self.layers, self.start_layer, self.end_layer))
488 )
489 swa_cache_module = first_layer.attn.swa_cache_layer
490 self.engram_hash = NgramHashState(
491 vllm_config, self.engram_layout, swa_cache_module
492 )
493 self.engram_swa_prefix = swa_cache_module.prefix
494
495 if get_pp_group().is_last_rank:
496 self.norm = RMSNorm(config.hidden_size, self.rms_norm_eps)
497 else:
498 self.norm = PPMissingLayer()
499
500 self.mhc_post = MHCPostOp()
501
502 spec_config = vllm_config.speculative_config
503 needs_mtp_hidden_states = spec_config is not None and (
504 spec_config.use_eagle() or spec_config.uses_draft_model()
505 )
506 if get_pp_group().is_last_rank and needs_mtp_hidden_states:
507 self._mtp_hidden_buffer = torch.empty(
508 vllm_config.scheduler_config.max_num_batched_tokens,
509 self.hc_dim,
510 dtype=vllm_config.model_config.dtype,
511 )
512 else:
513 self._mtp_hidden_buffer = None
514
515 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
516 return self.embed_tokens(input_ids)
517
518 def make_empty_intermediate_tensors(
519 self,
520 batch_size: int,
521 dtype: torch.dtype,
522 device: torch.device,
523 ) -> IntermediateTensors:
524 # PP intermediate tensors carry the multi-stream hidden_states
525 # of shape (num_tokens, hc_mult, hidden_size) — V4 expands the
526 # token embedding to hc_mult streams before the first decoder
527 # layer and keeps that shape until the final hc collapse — plus the
528 # (num_tokens, hc_mult) pre-mix the next rank's first layer needs
529 # for its attention collapse.
530 return IntermediateTensors(
531 {
532 "hidden_states": torch.zeros(
533 (batch_size, self.hc_mult, self.config.hidden_size),
534 dtype=dtype,
535 device=device,
536 ),
537 "pre_mix": torch.zeros(
538 (batch_size, self.hc_mult),
539 dtype=torch.float32,
540 device=device,
541 ),
542 }
543 )
544
545 def forward(
546 self,
547 input_ids: torch.Tensor,
548 positions: torch.Tensor,
549 intermediate_tensors: IntermediateTensors | None,
550 inputs_embeds: torch.Tensor | None = None,
551 lookback_token_ids: torch.Tensor | None = None,
552 ) -> torch.Tensor | IntermediateTensors:
553 if get_pp_group().is_first_rank:
554 if inputs_embeds is not None:
555 hidden_states = inputs_embeds
556 else:
557 hidden_states = self.embed_input_ids(input_ids)
558 else:
559 assert intermediate_tensors is not None
560 hidden_states = intermediate_tensors["hidden_states"]
561
562 if self.use_mega_moe:
563 input_ids = input_ids.to(torch.int64)
564
565 # Engram n-gram hashes for the whole (flattened) batch, computed once
566 # on the full token stream — before any sequence-parallel sharding —
567 # and consumed by the engram layers (1 and 14) below. Skipped on
568 # profile runs (KV cache unbound -> no slot space to key the rolling
569 # compressed-id cache into).
570 engram_hashes: torch.Tensor | None = None
571 engram_mask: torch.Tensor | None = None
572 if (
573 self.engram_hash is not None
574 and input_ids is not None
575 and is_forward_context_available()
576 ):
577 attn_metadata = get_forward_context().attn_metadata
578 if isinstance(attn_metadata, list):
579 attn_metadata = attn_metadata[dbo_current_ubatch_id()]
580 if isinstance(attn_metadata, dict) and self.engram_hash.ensure_cache():
581 assert self.engram_swa_prefix is not None
582 swa_metadata = typing.cast(
583 "DeepseekSparseSWAMetadata", attn_metadata[self.engram_swa_prefix]
584 )
585 # Image-span tokens are dead: they break n-grams (hash op
586 # takes True=dead) and their gate is zeroed (Engram.forward
587 # takes True=keep).
588 image_mask = image_sentinel_mask(input_ids)
589 engram_mask = ~image_mask
590 if lookback_token_ids is None:
591 if not self.engram_hash.use_slot_cache:
592 raise NotImplementedError(
593 "engram needs `lookback_token_ids` from the model "
594 "runner (the DBO/ubatch wrapper drops model kwargs)"
595 )
596 num_reqs = swa_metadata.num_decodes + swa_metadata.num_prefills
597 lookback_token_ids = input_ids.new_full(
598 (num_reqs, self.engram_hash.lookback_depth), -1
599 )
600 engram_hashes = self.engram_hash(
601 input_ids,
602 positions,
603 swa_metadata.query_start_loc,
604 image_mask,
605 lookback_token_ids,
606 image_sentinel_mask(lookback_token_ids),
607 swa_metadata.slot_mapping,
608 swa_metadata.block_table,
609 )
610 # Gather all Engram rows before entering the decoder layers.
611 for layer in islice(self.layers, self.start_layer, self.end_layer):
612 engram = getattr(layer, "engram", None)
613 if engram is not None:
614 engram.prepare_embeddings(
615 engram_hashes[:, engram.layer_hash_index]
616 )
617
618 full_num_tokens = positions.shape[0]
619 if self.use_sequence_parallel:
620 if envs.VLLM_MOE_SKIP_PADDING and is_forward_context_available():
621 forward_context = get_forward_context()
622 forward_context.is_padding = sp_padding_mask(
623 forward_context.is_padding, hidden_states
624 )
625 hidden_states = sp_shard(hidden_states)
626 input_ids = sp_shard(input_ids)
627
628 residual, post_mix, res_mix = None, None, None
629 pre_mix: torch.Tensor | None = None
630 if not get_pp_group().is_first_rank:
631 assert intermediate_tensors is not None
632 pre_mix = intermediate_tensors["pre_mix"]
633 aux_hidden_states: list[torch.Tensor] = []
634 final_aux_recon: torch.Tensor | None = None # avoid duplicate mhc_post call
635 for idx, layer in enumerate(
636 islice(self.layers, self.start_layer, self.end_layer),
637 start=self.start_layer,
638 ):
639 hidden_states, residual, post_mix, res_mix, pre_mix = layer(
640 hidden_states,
641 positions,
642 input_ids,
643 pre_mix,
644 post_mix,
645 res_mix,
646 residual,
647 engram_hashes,
648 engram_mask,
649 )
650 if idx + 1 in self.aux_hidden_state_layers:
651 # Reconstruct the aux hidden state for draft models
652 aux_recon = self.mhc_post(hidden_states, residual, post_mix, res_mix)
653 aux_hidden_state = aux_recon.mean(dim=1)
654 if self.use_sequence_parallel:
655 aux_hidden_state = sp_all_gather(aux_hidden_state)[:full_num_tokens]
656 aux_hidden_states.append(aux_hidden_state)
657 final_aux_recon = aux_recon
658 if layer is not None:
659 # Reuse if the last layer was captured as an aux hidden state
660 if self.end_layer in self.aux_hidden_state_layers:
661 hidden_states = final_aux_recon
662 else:
663 hidden_states = self.mhc_post(
664 hidden_states, residual, post_mix, res_mix
665 )
666
667 if not get_pp_group().is_last_rank:
668 return IntermediateTensors(
669 {"hidden_states": hidden_states, "pre_mix": pre_mix}
670 )
671
672 if self.use_sequence_parallel:
673 hidden_states = sp_all_gather(hidden_states)[:full_num_tokens]
674 pre_mix = sp_all_gather(pre_mix)[:full_num_tokens]
675
676 if self._mtp_hidden_buffer is not None:
677 num_tokens = hidden_states.shape[0]
678 self._mtp_hidden_buffer[:num_tokens].copy_(hidden_states.flatten(1))
679
680 # Collapse the hc copies with the pre-mix from the last layer's FFN
681 # mixes — the mix the reference applies via
682 # ``last_layer.hc_pre(h, pre_mix)`` (v4.1 has no learned hc_head).
683 assert pre_mix is not None
684 hidden_states = torch.ops.vllm.hc_collapse_triton(hidden_states, pre_mix)
685 hidden_states = self.norm(hidden_states)
686 if len(aux_hidden_states) > 0:
687 return hidden_states, aux_hidden_states
688 return hidden_states
689
690 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
691 stacked_params_mapping = [
692 # (param_name, shard_name, shard_id)
693 ("gate_up_proj", "w1", 0),
694 ("gate_up_proj", "w3", 1),
695 ("attn.fused_wqa_wkv", "attn.wq_a", 0),
696 ("attn.fused_wqa_wkv", "attn.wkv", 1),
697 ("compressor.fused_wkv_wgate", "compressor.wkv", 0),
698 ("compressor.fused_wkv_wgate", "compressor.wgate", 1),
699 ]
700 params_dict = dict(self.named_parameters())
701 loaded_params: set[str] = set()
702
703 def _resolve_param_name(name: str) -> str:
704 """Resolve scale naming across ROCm FP8 and MXFP8 methods."""
705 if name.endswith("_inv"):
706 direct_name = name.removesuffix("_inv")
707 if direct_name in params_dict:
708 return direct_name
709 inv_name = f"{name}_inv"
710 if name not in params_dict and inv_name in params_dict:
711 return inv_name
712 return name
713
714 # TP for attention
715 tp_size = get_tensor_model_parallel_world_size()
716 tp_rank = get_tensor_model_parallel_rank()
717 n_head = self.config.num_attention_heads
718 n_local_head = n_head // tp_size
719 head_rank_start = n_local_head * tp_rank
720 head_rank_end = n_local_head * (tp_rank + 1)
721
722 # Pre-compute expert mapping ONCE.
723 expert_mapping = self.get_expert_mapping()
724
725 # Block-FP8 shared experts: pad the intermediate up to the TP-uniform
726 # block count so the standard loaders below slice it evenly (trailing
727 # ranks land on the zero pad). SP / unquantized ones need no padding.
728 pad_shared_expert = (
729 getattr(self.quant_config, "weight_block_size", None) is not None
730 and not self.use_sequence_parallel
731 )
732
733 for name, loaded_weight in weights:
734 if name.startswith(("vision.", "aligner.", "image_")):
735 # Vision weights are loaded by the outer multimodal wrapper.
736 logger.warning_once("Skipping non-text weight: %s", name)
737 continue
738 if pad_shared_expert and ".shared_experts." in name:
739 loaded_weight = self._pad_shared_expert_weight(
740 self.quant_config, name, loaded_weight
741 )
742 for param_name, weight_name, shard_id in stacked_params_mapping:
743 # Skip non-stacked layers and experts (experts handled below).
744 if ".experts." in name:
745 continue
746 if weight_name not in name:
747 continue
748 name = name.replace(weight_name, param_name)
749
750 if is_pp_missing_parameter(name, self):
751 break
752 if name not in params_dict:
753 head, _, leaf = name.rpartition(".")
754 suffixed = f"{head}.base_layer.{leaf}"
755 if suffixed in params_dict:
756 name = suffixed
757 name = _resolve_param_name(name)
758 param = params_dict[name]
759 weight_loader = param.weight_loader
760 weight_loader(param, loaded_weight, shard_id)
761 loaded_params.add(name)
762 break
763 else:
764 if ".experts." in name:
765 # E8M0 scales are stored as float8_e8m0fnu in
766 # checkpoints but the MoE param is uint8. copy_()
767 # would do a numeric conversion (e.g. 2^-7 → 0),
768 # destroying the raw exponent bytes.
769 if (
770 "weight_scale" in name
771 and loaded_weight.dtype == torch.float8_e8m0fnu
772 ):
773 loaded_weight = loaded_weight.view(torch.uint8)
774 for mapping in expert_mapping:
775 param_name, weight_name, expert_id, expert_shard_id = mapping
776 if weight_name not in name:
777 continue
778 name_mapped = name.replace(weight_name, param_name)
779 if is_pp_missing_parameter(name_mapped, self):
780 continue
781 name_mapped = _resolve_param_name(name_mapped)
782 param = params_dict[name_mapped]
783 # We should ask the weight loader to return success or not
784 # here since otherwise we may skip experts with other
785 # available replicas.
786 weight_loader = typing.cast(
787 Callable[..., bool], param.weight_loader
788 )
789 success = weight_loader(
790 param,
791 loaded_weight,
792 name_mapped,
793 shard_id=expert_shard_id,
794 expert_id=expert_id,
795 return_success=True,
796 )
797 if success:
798 name = name_mapped
799 break
800 loaded_params.add(name_mapped)
801 continue
802 elif "attn_sink" in name:
803 if is_pp_missing_parameter(name, self):
804 continue
805 narrow_weight = loaded_weight[head_rank_start:head_rank_end]
806 n = narrow_weight.shape[0]
807 params_dict[name][:n].copy_(narrow_weight)
808 loaded_params.add(name)
809 continue
810 else:
811 if is_pp_missing_parameter(name, self):
812 continue
813 # Non-LoRA params on a LoRA-wrapped module live at
814 # ``<head>.base_layer.<leaf>``; the checkpoint is plain.
815 if name not in params_dict:
816 head, _, leaf = name.rpartition(".")
817 suffixed = f"{head}.base_layer.{leaf}"
818 if suffixed in params_dict:
819 name = suffixed
820 name = _resolve_param_name(name)
821 param = params_dict[name]
822 weight_loader = getattr(
823 param, "weight_loader", default_weight_loader
824 )
825 weight_loader(param, loaded_weight)
826 loaded_params.add(name)
827 continue
828
829 return loaded_params
830
831 @staticmethod
832 def _pad_shared_expert_weight(
833 quant_config: QuantizationConfig | None,
834 name: str,
835 loaded_weight: torch.Tensor,
836 ) -> torch.Tensor:
837 """Zero-pad a block-FP8 shared-expert weight/scale on its intermediate
838 axis so the standard TP loaders split it into even, block-aligned shards
839 (trailing ranks get the zero pad). gate (w1)/up (w3) [I, H] pad dim 0;
840 down (w2 -> down_proj) [H, I] pads dim 1.
841 """
842 block_size = getattr(quant_config, "weight_block_size", None)
843 assert block_size is not None
844 # Round the intermediate axis up to a whole number of TP shards. The axis
845 # is in elements for weights (step = block) and in blocks for scales.
846 step = (
847 1 if name.endswith(("weight_scale_inv", "weight_scale")) else block_size[0]
848 )
849 dim = 1 if ".down_proj." in name else 0
850 mult = get_tensor_model_parallel_world_size() * step
851 pad = cdiv(loaded_weight.shape[dim], mult) * mult - loaded_weight.shape[dim]
852 if pad == 0:
853 return loaded_weight
854 pad_shape = list(loaded_weight.shape)
855 pad_shape[dim] = pad
856 return torch.cat([loaded_weight, loaded_weight.new_zeros(pad_shape)], dim=dim)
857
858 def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
859 # Params for weights, fp8 weight scales, fp8 activation scales
860 # (param_name, weight_name, expert_id, shard_id)
861 return fused_moe_make_expert_params_mapping(
862 self,
863 ckpt_gate_proj_name="w1",
864 ckpt_down_proj_name="w2",
865 ckpt_up_proj_name="w3",
866 num_experts=self.config.n_routed_experts,
867 )
868
869 def finalize_mega_moe_weights(self) -> None:
870 return
871
872 def finalize_mhc_broadcast_weights(self) -> None:
873 if not get_pp_group().is_first_rank or self.start_layer >= self.end_layer:
874 return
875 layer = self.layers[self.start_layer]
876 if isinstance(layer, DeepseekV4DecoderLayer):
877 broadcast = (
878 layer.hc_attn_fn.detach()
879 .view(-1, layer.hc_mult, layer.hidden_size)
880 .sum(dim=1)
881 )
882 if layer.hc_attn_fn_broadcast is None:
883 layer.hc_attn_fn_broadcast = broadcast
884 else:
885 layer.hc_attn_fn_broadcast.copy_(broadcast)
886
887
888 def _linear_scale_param_name(vllm_config: VllmConfig, expert_dtype: str) -> str:
889 """Parameter name the linear quant method registers for weight scales.
890
891 Native MXFP8 checkpoints ([32, 32] blocks with MXFP4 experts) route linear
892 layers through ModelOptLinearMethod, which registers ``weight_scale``;
893 block-FP8 linear layers register ``weight_scale_inv``.
894 """
895 use_mxfp8 = (
896 getattr(vllm_config.quant_config, "weight_block_size", None) == [32, 32]
897 and expert_dtype == "fp4"
898 )
899 return "weight_scale" if use_mxfp8 else "weight_scale_inv"
900
901
902 def _make_deepseek_v4_weights_mapper(
903 expert_dtype: str, linear_scale_name: str = "weight_scale_inv"
904 ) -> WeightsMapper:
905 if expert_dtype == "fp4":
906 # MXFP4 experts use Mxfp4MoEMethod, which registers scales as
907 # ``w{1,2,3}_weight_scale`` (no _inv suffix). Linear scales use the
908 # parameter name registered by their quantization method.
909 scale_regex = {
910 # ``.base_layer.``-namespace variant (LoRA-wrapped experts).
911 re.compile(
912 r"(\.experts\.\d+\.w[123]\.base_layer)\.scale$"
913 ): r"\1.weight_scale",
914 re.compile(r"(\.experts\.\d+\.w[123])\.scale$"): r"\1.weight_scale",
915 # The ``embed.weight`` -> ``embed_tokens.weight`` suffix rule
916 # renames the engram fp8 table but not its scale; route the
917 # scale explicitly to the same module.
918 re.compile(r"(engram\.embed)\.scale$"): r"\1_tokens.weight_scale_inv",
919 # Quark exports spell the same tensor ``embed.weight_scale``,
920 # which the ``\.scale$`` rules never match.
921 re.compile(
922 r"(engram\.embed)\.weight_scale$"
923 ): r"\1_tokens.weight_scale_inv",
924 re.compile(r"\.scale$"): f".{linear_scale_name}",
925 }
926 else:
927 # FP8 experts use Fp8MoEMethod (block_quant=True), which registers
928 # scales as ``w{13,2}_weight_scale_inv``. Map all ``.scale`` keys
929 # there.
930 scale_regex = {
931 # ``.base_layer.``-namespace variant of the above.
932 re.compile(
933 r"(\.experts\.\d+\.w[123]\.base_layer)\.scale$"
934 ): r"\1.weight_scale_inv",
935 # Same engram reroute as the fp4 branch above.
936 re.compile(r"(engram\.embed)\.scale$"): r"\1_tokens.weight_scale_inv",
937 # Quark exports spell the same tensor ``embed.weight_scale``,
938 # which the ``\.scale$`` rules never match.
939 re.compile(
940 r"(engram\.embed)\.weight_scale$"
941 ): r"\1_tokens.weight_scale_inv",
942 re.compile(r"\.scale$"): f".{linear_scale_name}",
943 }
944 return WeightsMapper(
945 orig_to_new_prefix={
946 "layers.": "model.layers.",
947 "embed.": "model.embed.",
948 "norm.": "model.norm.",
949 "mtp.": "model.mtp.",
950 },
951 orig_to_new_regex=scale_regex,
952 orig_to_new_suffix={
953 "head.weight": "lm_head.weight",
954 "embed.weight": "embed_tokens.weight",
955 ".ffn.gate.bias": ".ffn.gate.e_score_correction_bias",
956 },
957 orig_to_new_substr={
958 ".shared_experts.w2": ".shared_experts.down_proj",
959 "mtp.": None,
960 # The v4.1 checkpoint declares the VL arch even for text-only
961 # use; the text backbone drops the vision-tower weights.
962 "vision.": None,
963 "aligner.": None,
964 "image_": None,
965 },
966 )
967
968
969 class DeepseekV4MixtureOfExperts(MixtureOfExperts):
970 moe_mlp_layers: list["DeepseekV4MoE"]
971
972 def extract_moe_parameters(self, example_moe: "DeepseekV4MoE | None") -> None:
973 if example_moe is None:
974 self.num_moe_layers = 0
975 self.num_expert_groups = 0
976 self.num_logical_experts = 0
977 self.num_physical_experts = 0
978 self.num_local_physical_experts = 0
979 self.num_routed_experts = 0
980 self.num_shared_experts = 0
981 self.num_redundant_experts = 0
982 return
983 self.num_logical_experts = example_moe.n_logical_experts
984 self.num_physical_experts = example_moe.n_physical_experts
985 self.num_local_physical_experts = example_moe.n_local_physical_experts
986 self.num_routed_experts = example_moe.n_routed_experts
987 self.num_shared_experts = example_moe.n_shared_experts
988 self.num_redundant_experts = example_moe.n_redundant_experts
989
990 def update_physical_experts_metadata(
991 self,
992 num_physical_experts: int,
993 num_local_physical_experts: int,
994 ) -> None:
995 assert self.num_local_physical_experts == num_local_physical_experts
996 self.num_physical_experts = num_physical_experts
997 self.num_local_physical_experts = num_local_physical_experts
998 self.num_redundant_experts = num_physical_experts - self.num_logical_experts
999 for moe in self.moe_mlp_layers:
1000 moe.n_local_physical_experts = num_local_physical_experts
1001 moe.n_physical_experts = num_physical_experts
1002 moe.n_redundant_experts = self.num_redundant_experts
1003 moe.experts.update_expert_map()
1004
1005
1006 class DeepseekV41LLMForCausalLM(
1007 nn.Module,
1008 SupportsPP,
1009 SupportsEagle3,
1010 SupportsLoRA,
1011 DeepseekV4MixtureOfExperts,
1012 ):
1013 model_cls = DeepseekV4Model
1014
1015 # Default mapper assumes the original FP4-expert checkpoint layout.
1016 # Overridden per-instance in __init__ when expert_dtype != "fp4".
1017 hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper("fp4")
1018
1019 packed_modules_mapping = {
1020 "gate_up_proj": ["w1", "w3"],
1021 "fused_wqa_wkv": ["wq_a", "wkv"],
1022 "fused_wkv_wgate": ["wkv", "wgate"],
1023 }
1024
1025 # The MTP draft head is not LoRA-adapted.
1026 lora_skip_prefixes = ["mtp."]
1027
1028 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1029 super().__init__()
1030
1031 config = vllm_config.model_config.hf_config
1032 self.config = config
1033 expert_dtype = getattr(config, "expert_dtype", "fp4")
1034 self.hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper(
1035 expert_dtype, _linear_scale_param_name(vllm_config, expert_dtype)
1036 )
1037
1038 self.model = self.model_cls(
1039 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
1040 )
1041 if get_pp_group().is_last_rank:
1042 self.lm_head = ParallelLMHead(
1043 config.vocab_size,
1044 config.hidden_size,
1045 prefix=maybe_prefix(prefix, "lm_head"),
1046 )
1047 else:
1048 self.lm_head = PPMissingLayer()
1049 self.logits_processor = LogitsProcessor(config.vocab_size)
1050 self.make_empty_intermediate_tensors = ( # type: ignore[method-assign]
1051 self.model.make_empty_intermediate_tensors
1052 )
1053
1054 self.set_moe_parameters()
1055
1056 def set_moe_parameters(self) -> None:
1057 self.num_expert_groups = getattr(self.config, "n_group", 1)
1058 self.num_moe_layers = self.config.num_hidden_layers
1059 self.moe_layers: list[nn.Module] = []
1060 self.moe_mlp_layers: list[DeepseekV4MoE] = []
1061 example_moe: DeepseekV4MoE | None = None
1062 for layer in self.model.layers:
1063 if isinstance(layer, PPMissingLayer):
1064 continue
1065 if not isinstance(layer, DeepseekV4DecoderLayer):
1066 continue
1067 if isinstance(layer.ffn, DeepseekV4MoE):
1068 example_moe = layer.ffn
1069 self.moe_mlp_layers.append(layer.ffn)
1070 self.moe_layers.append(layer.ffn.experts)
1071
1072 self.num_moe_layers = len(self.moe_layers)
1073 self.extract_moe_parameters(example_moe)
1074
1075 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
1076 return self.model.embed_input_ids(input_ids)
1077
1078 def compute_logits(
1079 self,
1080 hidden_states: torch.Tensor,
1081 ) -> torch.Tensor | None:
1082 logits = self.logits_processor(self.lm_head, hidden_states)
1083 return logits
1084
1085 def compute_logits_local(
1086 self,
1087 hidden_states: torch.Tensor,
1088 ) -> torch.Tensor:
1089 return self.logits_processor(self.lm_head, hidden_states, skip_gather=True)
1090
1091 @staticmethod
1092 def get_model_state_cls():
1093 from ..nvidia.model_state import DeepseekV41ModelState
1094
1095 return DeepseekV41ModelState
1096
1097 @property
1098 def token_lookback_depth(self) -> int:
1099 """Tokens before a chunk start the engram hash needs; the model runner
1100 passes them as `lookback_token_ids`."""
1101 engram_hash = self.model.engram_hash
1102 return engram_hash.lookback_depth if engram_hash is not None else 0
1103
1104 def forward(
1105 self,
1106 input_ids: torch.Tensor,
1107 positions: torch.Tensor,
1108 intermediate_tensors: IntermediateTensors | None = None,
1109 inputs_embeds: torch.Tensor | None = None,
1110 lookback_token_ids: torch.Tensor | None = None,
1111 ) -> torch.Tensor | IntermediateTensors:
1112 hidden_states = self.model(
1113 input_ids,
1114 positions,
1115 intermediate_tensors,
1116 inputs_embeds,
1117 lookback_token_ids=lookback_token_ids,
1118 )
1119 return hidden_states
1120
1121 def get_mtp_target_hidden_states(self) -> torch.Tensor | None:
1122 """Pre-collapse residual stream buffer (max_num_batched_tokens,
1123 hc_mult * hidden_size) for the MTP draft model. Populated by
1124 forward(); valid after each target step."""
1125 return getattr(self.model, "_mtp_hidden_buffer", None)
1126
1127 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1128 loader = AutoWeightsLoader(self)
1129 loaded_params = loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1130 self.process_weights_after_loading()
1131 return loaded_params
1132
1133 def process_weights_after_loading(self) -> None:
1134 self.model.finalize_mega_moe_weights()
1135 self.model.finalize_mhc_broadcast_weights()
1136
1137 def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
1138 return self.model.get_expert_mapping()