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
+import torch.nn.functional as F
11
+
12
+from vllm.compilation.decorators import support_torch_compile
13
+from vllm.config import VllmConfig
14
+from vllm.distributed import (
15
+ get_ep_group,
16
+ get_tensor_model_parallel_rank,
17
+ get_tensor_model_parallel_world_size,
18
+)
19
+from vllm.forward_context import get_forward_context
20
+from vllm.model_executor.layers.deepseek_v4_attention import (
21
+ DeepseekV4Indexer,
22
+ DeepseekV4MLAModules,
23
+ DeepseekV4MultiHeadLatentAttentionWrapper,
24
+)
25
+from vllm.model_executor.layers.fused_moe import FusedMoE, GateLinear
26
+from vllm.model_executor.layers.fused_moe.layer import UnquantizedFusedMoEMethod
27
+from vllm.model_executor.layers.fused_moe.router.fused_topk_bias_router import (
28
+ fused_topk_bias,
29
+)
30
+from vllm.model_executor.layers.layernorm import RMSNorm
31
+from vllm.model_executor.layers.linear import (
32
+ ColumnParallelLinear,
33
+ MergedColumnParallelLinear,
34
+ RowParallelLinear,
35
+)
36
+from vllm.model_executor.layers.logits_processor import LogitsProcessor
37
+from vllm.model_executor.layers.quantization import QuantizationMethods
38
+from vllm.model_executor.layers.quantization.fp8 import Fp8Config
39
+from vllm.model_executor.layers.quantization.mxfp4 import Mxfp4MoEMethod
40
+from vllm.model_executor.layers.quantization.utils.quant_utils import (
41
+ is_layer_skipped,
42
+)
43
+from vllm.model_executor.layers.rotary_embedding import get_rope
44
+from vllm.model_executor.layers.vocab_parallel_embedding import (
45
+ ParallelLMHead,
46
+ VocabParallelEmbedding,
47
+)
48
+from vllm.model_executor.model_loader.weight_utils import default_weight_loader
49
+from vllm.model_executor.models.deepseek_v2 import DeepseekV2MLP
50
+from vllm.model_executor.utils import set_weight_attrs
51
+from vllm.platforms import current_platform
52
+from vllm.sequence import IntermediateTensors
53
+from vllm.triton_utils import tl, triton
54
+from vllm.utils.multi_stream_utils import AuxStreamType
55
+from vllm.utils.torch_utils import direct_register_custom_op
56
+
57
+from .utils import (
58
+ AutoWeightsLoader,
59
+ WeightsMapper,
60
+ extract_layer_index,
61
+ make_layers,
62
+ maybe_prefix,
63
+)
64
+
65
+
66
+class DeepseekV4FP8Config(Fp8Config):
67
+ """FP8 config that routes MoE layers to MXFP4 quantization.
68
+
69
+ DeepSeek V4 checkpoints use FP8 for linear/attention layers but
70
+ MXFP4 for MoE expert weights. This config inherits standard FP8
71
+ behavior and overrides only the MoE dispatch.
72
+ """
73
+
74
+ def __init__(self, *args, **kwargs):
75
+ super().__init__(*args, **kwargs)
76
+ self.is_scale_e8m0: bool = True
77
+
78
+ @classmethod
79
+ def get_name(cls) -> QuantizationMethods:
80
+ return "deepseek_v4_fp8"
81
+
82
+ @classmethod
83
+ def override_quantization_method(
84
+ cls, hf_quant_cfg, user_quant, hf_config=None
85
+ ) -> QuantizationMethods | None:
86
+ if not (
87
+ isinstance(hf_quant_cfg, dict)
88
+ and hf_quant_cfg.get("quant_method") in ("fp8", "deepseek_v4_fp8")
89
+ ):
90
+ return None
91
+ model_type = getattr(hf_config, "model_type", None)
92
+ if model_type == "deepseek_v4" or user_quant == "deepseek_v4_fp8":
93
+ return "deepseek_v4_fp8"
94
+ return None
95
+
96
+ def get_quant_method(self, layer, prefix):
97
+ if isinstance(layer, FusedMoE):
98
+ if is_layer_skipped(
99
+ prefix=prefix,
100
+ ignored_layers=self.ignored_layers,
101
+ fused_mapping=self.packed_modules_mapping,
102
+ ):
103
+ return UnquantizedFusedMoEMethod(layer.moe_config)
104
+ return Mxfp4MoEMethod(layer.moe_config)
105
+ return super().get_quant_method(layer, prefix)
106
+
107
+ def is_mxfp4_quant(self, prefix, layer):
108
+ return isinstance(layer, FusedMoE)
109
+
110
+
111
+@triton.jit
112
+def _deepseek_v4_stage_mega_moe_inputs_kernel(
113
+ hidden_states,
114
+ x_fp8,
115
+ x_sf,
116
+ topk_ids,
117
+ topk_weights,
118
+ topk_idx_out,
119
+ topk_weights_out,
120
+ hidden_stride_m: tl.constexpr,
121
+ hidden_stride_k: tl.constexpr,
122
+ x_stride_m: tl.constexpr,
123
+ x_stride_k: tl.constexpr,
124
+ x_sf_stride_m: tl.constexpr,
125
+ x_sf_stride_k: tl.constexpr,
126
+ topk_ids_stride_m: tl.constexpr,
127
+ topk_ids_stride_k: tl.constexpr,
128
+ topk_weights_stride_m: tl.constexpr,
129
+ topk_weights_stride_k: tl.constexpr,
130
+ topk_idx_stride_m: tl.constexpr,
131
+ topk_idx_stride_k: tl.constexpr,
132
+ topk_weights_out_stride_m: tl.constexpr,
133
+ topk_weights_out_stride_k: tl.constexpr,
134
+ hidden_size: tl.constexpr,
135
+ top_k: tl.constexpr,
136
+ BLOCK_K: tl.constexpr,
137
+ GROUP_K: tl.constexpr,
138
+ BLOCK_TOPK: tl.constexpr,
139
+) -> None:
140
+ token_id = tl.program_id(0)
141
+ k_block_id = tl.program_id(1)
142
+
143
+ k_offsets = k_block_id * BLOCK_K + tl.arange(0, BLOCK_K)
144
+ k_mask = k_offsets < hidden_size
145
+ hidden = tl.load(
146
+ hidden_states + token_id * hidden_stride_m + k_offsets * hidden_stride_k,
147
+ mask=k_mask,
148
+ other=0.0,
149
+ ).to(tl.float32)
150
+
151
+ num_groups: tl.constexpr = BLOCK_K // GROUP_K
152
+ hidden_groups = tl.reshape(tl.abs(hidden), [num_groups, GROUP_K])
153
+ amax = tl.max(hidden_groups, axis=1)
154
+ amax = tl.maximum(amax, 1.0e-4)
155
+
156
+ scale = amax / 448.0
157
+ scale_bits = scale.to(tl.uint32, bitcast=True)
158
+ scale_exp = ((scale_bits >> 23) & 0xFF) + ((scale_bits & 0x7FFFFF) != 0).to(
159
+ tl.uint32
160
+ )
161
+ scale_exp = tl.minimum(tl.maximum(scale_exp, 1), 254)
162
+ rounded_scale = (scale_exp << 23).to(tl.float32, bitcast=True)
163
+
164
+ hidden_groups = tl.reshape(hidden, [num_groups, GROUP_K])
165
+ scaled = hidden_groups * (1.0 / rounded_scale)[:, None]
166
+ scaled = tl.reshape(scaled, [BLOCK_K])
167
+ fp8 = scaled.to(tl.float8e4nv)
168
+ tl.store(
169
+ x_fp8 + token_id * x_stride_m + k_offsets * x_stride_k,
170
+ fp8,
171
+ mask=k_mask,
172
+ )
173
+
174
+ scale_offsets = tl.arange(0, num_groups)
175
+ packed_scale = tl.sum(scale_exp << (scale_offsets * 8), axis=0).to(tl.int32)
176
+ tl.store(
177
+ x_sf + token_id * x_sf_stride_m + k_block_id * x_sf_stride_k,
178
+ packed_scale,
179
+ )
180
+
181
+ if k_block_id == 0:
182
+ topk_offsets = tl.arange(0, BLOCK_TOPK)
183
+ topk_mask = topk_offsets < top_k
184
+
185
+ ids = tl.load(
186
+ topk_ids + token_id * topk_ids_stride_m + topk_offsets * topk_ids_stride_k,
187
+ mask=topk_mask,
188
+ other=0,
189
+ ).to(tl.int64)
190
+ tl.store(
191
+ topk_idx_out
192
+ + token_id * topk_idx_stride_m
193
+ + topk_offsets * topk_idx_stride_k,
194
+ ids,
195
+ mask=topk_mask,
196
+ )
197
+
198
+ weights = tl.load(
199
+ topk_weights
200
+ + token_id * topk_weights_stride_m
201
+ + topk_offsets * topk_weights_stride_k,
202
+ mask=topk_mask,
203
+ other=0.0,
204
+ )
205
+ tl.store(
206
+ topk_weights_out
207
+ + token_id * topk_weights_out_stride_m
208
+ + topk_offsets * topk_weights_out_stride_k,
209
+ weights,
210
+ mask=topk_mask,
211
+ )
212
+
213
+
214
+def _stage_deepseek_v4_mega_moe_inputs(
215
+ hidden_states: torch.Tensor,
216
+ topk_weights: torch.Tensor,
217
+ topk_ids: torch.Tensor,
218
+ x_fp8: torch.Tensor,
219
+ x_sf: torch.Tensor,
220
+ topk_idx_out: torch.Tensor,
221
+ topk_weights_out: torch.Tensor,
222
+) -> None:
223
+ num_tokens, hidden_size = hidden_states.shape
224
+ if num_tokens == 0:
225
+ return
226
+ if hidden_size % 128 != 0:
227
+ raise ValueError(
228
+ "DeepSeek V4 MegaMoE input staging requires hidden_size to be "
229
+ "a multiple of 128."
230
+ )
231
+ top_k = topk_ids.shape[1]
232
+ if topk_weights.shape != topk_ids.shape:
233
+ raise ValueError(
234
+ "DeepSeek V4 MegaMoE input staging requires topk_weights and "
235
+ "topk_ids to have the same shape."
236
+ )
237
+
238
+ block_k = 128
239
+ grid = (num_tokens, triton.cdiv(hidden_size, block_k))
240
+ block_topk = triton.next_power_of_2(top_k)
241
+ _deepseek_v4_stage_mega_moe_inputs_kernel[grid](
242
+ hidden_states,
243
+ x_fp8,
244
+ x_sf,
245
+ topk_ids,
246
+ topk_weights,
247
+ topk_idx_out,
248
+ topk_weights_out,
249
+ hidden_states.stride(0),
250
+ hidden_states.stride(1),
251
+ x_fp8.stride(0),
252
+ x_fp8.stride(1),
253
+ x_sf.stride(0),
254
+ x_sf.stride(1),
255
+ topk_ids.stride(0),
256
+ topk_ids.stride(1),
257
+ topk_weights.stride(0),
258
+ topk_weights.stride(1),
259
+ topk_idx_out.stride(0),
260
+ topk_idx_out.stride(1),
261
+ topk_weights_out.stride(0),
262
+ topk_weights_out.stride(1),
263
+ hidden_size,
264
+ top_k,
265
+ BLOCK_K=block_k,
266
+ GROUP_K=32,
267
+ BLOCK_TOPK=block_topk,
268
+ num_warps=4,
269
+ )
270
+
271
+
272
+def make_deepseek_v4_expert_params_mapping(
273
+ num_experts: int,
274
+) -> list[tuple[str, str, int, str]]:
275
+ return [
276
+ (
277
+ "experts.w13_" if shard_id in ("w1", "w3") else "experts.w2_",
278
+ f"experts.{expert_id}.{weight_name}.",
279
+ expert_id,
280
+ shard_id,
281
+ )
282
+ for expert_id in range(num_experts)
283
+ for shard_id, weight_name in [
284
+ ("w1", "w1"),
285
+ ("w2", "w2"),
286
+ ("w3", "w3"),
287
+ ]
288
+ ]
289
+
290
+
291
+class DeepseekV4MegaMoEExperts(nn.Module):
292
+ _symm_buffer_cache: dict[tuple[int, int, int, int, int, int, int], object] = {}
293
+
294
+ def __init__(
295
+ self,
296
+ vllm_config: VllmConfig,
297
+ *,
298
+ num_experts: int,
299
+ num_local_experts: int,
300
+ experts_start_idx: int,
301
+ top_k: int,
302
+ hidden_size: int,
303
+ intermediate_size: int,
304
+ prefix: str = "",
305
+ ):
306
+ super().__init__()
307
+ self.prefix = prefix
308
+ self.num_experts = num_experts
309
+ self.num_local_experts = num_local_experts
310
+ self.experts_start_idx = experts_start_idx
311
+ self.experts_end_idx = experts_start_idx + num_local_experts
312
+ self.top_k = top_k
313
+ self.hidden_size = hidden_size
314
+ self.intermediate_size = intermediate_size
315
+ self.max_num_tokens = vllm_config.scheduler_config.max_num_batched_tokens
316
+
317
+ weight_attrs = {"weight_loader": self.weight_loader}
318
+ self.w13_weight = nn.Parameter(
319
+ torch.zeros(
320
+ num_local_experts,
321
+ 2 * intermediate_size,
322
+ hidden_size // 2,
323
+ dtype=torch.uint8,
324
+ ),
325
+ requires_grad=False,
326
+ )
327
+ set_weight_attrs(self.w13_weight, weight_attrs)
328
+
329
+ self.w13_weight_scale = nn.Parameter(
330
+ torch.zeros(
331
+ num_local_experts,
332
+ 2 * intermediate_size,
333
+ hidden_size // 32,
334
+ dtype=torch.uint8,
335
+ ),
336
+ requires_grad=False,
337
+ )
338
+ set_weight_attrs(self.w13_weight_scale, weight_attrs)
339
+ self.w13_weight_scale.quant_method = "block"
340
+
341
+ self.w2_weight = nn.Parameter(
342
+ torch.zeros(
343
+ num_local_experts,
344
+ hidden_size,
345
+ intermediate_size // 2,
346
+ dtype=torch.uint8,
347
+ ),
348
+ requires_grad=False,
349
+ )
350
+ set_weight_attrs(self.w2_weight, weight_attrs)
351
+
352
+ self.w2_weight_scale = nn.Parameter(
353
+ torch.zeros(
354
+ num_local_experts,
355
+ hidden_size,
356
+ intermediate_size // 32,
357
+ dtype=torch.uint8,
358
+ ),
359
+ requires_grad=False,
360
+ )
361
+ set_weight_attrs(self.w2_weight_scale, weight_attrs)
362
+ self.w2_weight_scale.quant_method = "block"
363
+
364
+ self._transformed_l1_weights: tuple[torch.Tensor, torch.Tensor] | None = None
365
+ self._transformed_l2_weights: tuple[torch.Tensor, torch.Tensor] | None = None
366
+
367
+ # Register in the static forward context so the custom-op wrapper
368
+ # can look up this module by name from within a torch.compile graph.
369
+ compilation_config = vllm_config.compilation_config
370
+ if prefix in compilation_config.static_forward_context:
371
+ raise ValueError(f"Duplicate layer name: {prefix}")
372
+ compilation_config.static_forward_context[prefix] = self
373
+
374
+ def _map_global_expert_id(self, expert_id: int) -> int:
375
+ if expert_id < self.experts_start_idx or expert_id >= self.experts_end_idx:
376
+ return -1
377
+ return expert_id - self.experts_start_idx
378
+
379
+ def weight_loader(
380
+ self,
381
+ param: nn.Parameter,
382
+ loaded_weight: torch.Tensor,
383
+ weight_name: str,
384
+ shard_id: str,
385
+ expert_id: int,
386
+ return_success: bool = False,
387
+ ) -> bool | None:
388
+ local_expert_id = self._map_global_expert_id(expert_id)
389
+ if local_expert_id == -1:
390
+ return False if return_success else None
391
+
392
+ expert_data = param.data[local_expert_id]
393
+ if shard_id in ("w1", "w3"):
394
+ if "w13_" not in weight_name:
395
+ return False if return_success else None
396
+ shard_offset = 0 if shard_id == "w1" else self.intermediate_size
397
+ expert_data = expert_data.narrow(0, shard_offset, self.intermediate_size)
398
+ elif shard_id == "w2":
399
+ if "w2_" not in weight_name:
400
+ return False if return_success else None
401
+ else:
402
+ raise ValueError(f"Unsupported expert shard id: {shard_id}")
403
+
404
+ if expert_data.shape != loaded_weight.shape:
405
+ raise ValueError(
406
+ f"DeepSeek V4 MegaMoE expert weight shape mismatch for "
407
+ f"{weight_name}: parameter shard {tuple(expert_data.shape)} "
408
+ f"vs checkpoint {tuple(loaded_weight.shape)}"
409
+ )
410
+ expert_data.copy_(loaded_weight)
411
+ return True if return_success else None
412
+
413
+ @staticmethod
414
+ def _ue8m0_uint8_to_float(sf: torch.Tensor) -> torch.Tensor:
415
+ return (sf.to(torch.int32) << 23).view(torch.float32)
416
+
417
+ def _check_runtime_supported(self) -> None:
418
+ if not torch.cuda.is_available():
419
+ raise NotImplementedError("DeepSeek V4 MegaMoE requires CUDA.")
420
+ device = self.w13_weight.device
421
+ if device.type != "cuda":
422
+ raise NotImplementedError(
423
+ "DeepSeek V4 MegaMoE expert weights must be loaded on CUDA."
424
+ )
425
+ if torch.cuda.get_device_capability(device)[0] != 10:
426
+ raise NotImplementedError("DeepGEMM MegaMoE requires SM100 GPUs.")
427
+ if self.hidden_size % 128 != 0 or self.intermediate_size % 128 != 0:
428
+ raise ValueError(
429
+ "DeepGEMM MegaMoE requires hidden and intermediate sizes "
430
+ "to be multiples of 128."
431
+ )
432
+
433
+ def finalize_weights(self) -> None:
434
+ if self._transformed_l1_weights is not None:
435
+ return
436
+
437
+ self._check_runtime_supported()
438
+ import vllm.third_party.deep_gemm as deep_gemm
439
+
440
+ w13_scale = deep_gemm.transform_sf_into_required_layout(
441
+ self._ue8m0_uint8_to_float(self.w13_weight_scale.data).contiguous(),
442
+ 2 * self.intermediate_size,
443
+ self.hidden_size,
444
+ (1, 32),
445
+ self.num_local_experts,
446
+ )
447
+ w2_scale = deep_gemm.transform_sf_into_required_layout(
448
+ self._ue8m0_uint8_to_float(self.w2_weight_scale.data).contiguous(),
449
+ self.hidden_size,
450
+ self.intermediate_size,
451
+ (1, 32),
452
+ self.num_local_experts,
453
+ )
454
+ self._transformed_l1_weights, self._transformed_l2_weights = (
455
+ deep_gemm.transform_weights_for_mega_moe(
456
+ (self.w13_weight.data.view(torch.int8).contiguous(), w13_scale),
457
+ (self.w2_weight.data.view(torch.int8).contiguous(), w2_scale),
458
+ )
459
+ )
460
+ # Drop the original loader-side parameters: the MegaMoE kernels only
461
+ # consume the transformed views above. transform_weights_for_mega_moe
462
+ # allocates a fresh tensor for the L1 weight (see _interleave_l1_weights)
463
+ # and fresh SF tensors for L1/L2; the L2 weight is the only tensor that
464
+ # aliases the original storage, and _transformed_l2_weights still holds
465
+ # it, so the storage stays live after we drop the Parameter.
466
+ self.w13_weight = None
467
+ self.w13_weight_scale = None
468
+ self.w2_weight = None
469
+ self.w2_weight_scale = None
470
+
471
+ def get_symm_buffer(self):
472
+ import vllm.third_party.deep_gemm as deep_gemm
473
+
474
+ group = get_ep_group().device_group
475
+ device = torch.accelerator.current_device_index()
476
+ key = (
477
+ id(group),
478
+ device,
479
+ self.num_experts,
480
+ self.max_num_tokens,
481
+ self.top_k,
482
+ self.hidden_size,
483
+ self.intermediate_size,
484
+ )
485
+ symm_buffer = self._symm_buffer_cache.get(key)
486
+ if symm_buffer is None:
487
+ symm_buffer = deep_gemm.get_symm_buffer_for_mega_moe(
488
+ group,
489
+ self.num_experts,
490
+ self.max_num_tokens,
491
+ self.top_k,
492
+ self.hidden_size,
493
+ self.intermediate_size,
494
+ )
495
+ self._symm_buffer_cache[key] = symm_buffer
496
+ return symm_buffer
497
+
498
+ def forward(
499
+ self,
500
+ hidden_states: torch.Tensor,
501
+ topk_weights: torch.Tensor,
502
+ topk_ids: torch.Tensor,
503
+ *,
504
+ activation_clamp: float | None,
505
+ fast_math: bool = True,
506
+ ) -> torch.Tensor:
507
+ if hidden_states.shape[0] > self.max_num_tokens:
508
+ raise ValueError(
509
+ f"DeepSeek V4 MegaMoE got {hidden_states.shape[0]} tokens, "
510
+ f"but the symmetric buffer was sized for {self.max_num_tokens}."
511
+ )
512
+ y = torch.empty_like(hidden_states, dtype=torch.bfloat16)
513
+ torch.ops.vllm.deepseek_v4_mega_moe_experts(
514
+ hidden_states,
515
+ topk_weights,
516
+ topk_ids,
517
+ y,
518
+ self.prefix,
519
+ activation_clamp,
520
+ fast_math,
521
+ )
522
+ return y
523
+
524
+ def _run_mega_moe(
525
+ self,
526
+ hidden_states: torch.Tensor,
527
+ topk_weights: torch.Tensor,
528
+ topk_ids: torch.Tensor,
529
+ y: torch.Tensor,
530
+ activation_clamp: float | None,
531
+ fast_math: bool,
532
+ ) -> None:
533
+ import vllm.third_party.deep_gemm as deep_gemm
534
+
535
+ symm_buffer = self.get_symm_buffer()
536
+ num_tokens = hidden_states.shape[0]
537
+ _stage_deepseek_v4_mega_moe_inputs(
538
+ hidden_states,
539
+ topk_weights,
540
+ topk_ids,
541
+ symm_buffer.x[:num_tokens],
542
+ symm_buffer.x_sf[:num_tokens],
543
+ symm_buffer.topk_idx[:num_tokens],
544
+ symm_buffer.topk_weights[:num_tokens],
545
+ )
546
+
547
+ # This method must have been already called during the weight loading phase.
548
+ # We call it again here to cover the dummy weight loading case.
549
+ self.finalize_weights()
550
+
551
+ assert self._transformed_l1_weights is not None
552
+ assert self._transformed_l2_weights is not None
553
+ deep_gemm.fp8_fp4_mega_moe(
554
+ y,
555
+ self._transformed_l1_weights,
556
+ self._transformed_l2_weights,
557
+ symm_buffer,
558
+ activation_clamp=activation_clamp,
559
+ fast_math=fast_math,
560
+ )
561
+
562
+
563
+DeepseekV4MegaMoEExperts.weight_loader.supports_moe_loading = True # type: ignore[attr-defined]
564
+
565
+
566
+def _deepseek_v4_mega_moe_experts_op(
567
+ hidden_states: torch.Tensor,
568
+ topk_weights: torch.Tensor,
569
+ topk_ids: torch.Tensor,
570
+ out: torch.Tensor,
571
+ layer_name: str,
572
+ activation_clamp: float | None,
573
+ fast_math: bool,
574
+) -> None:
575
+ self = get_forward_context().no_compile_layers[layer_name]
576
+ self._run_mega_moe(
577
+ hidden_states,
578
+ topk_weights,
579
+ topk_ids,
580
+ out,
581
+ activation_clamp,
582
+ fast_math,
583
+ )
584
+
585
+
586
+def _deepseek_v4_mega_moe_experts_op_fake(
587
+ hidden_states: torch.Tensor,
588
+ topk_weights: torch.Tensor,
589
+ topk_ids: torch.Tensor,
590
+ out: torch.Tensor,
591
+ layer_name: str,
592
+ activation_clamp: float | None,
593
+ fast_math: bool,
594
+) -> None:
595
+ return None
596
+
597
+
598
+direct_register_custom_op(
599
+ op_name="deepseek_v4_mega_moe_experts",
600
+ op_func=_deepseek_v4_mega_moe_experts_op,
601
+ mutates_args=["out"],
602
+ fake_impl=_deepseek_v4_mega_moe_experts_op_fake,
603
+)
604
+
605
+
606
+class DeepseekV4MoE(nn.Module):
607
+ def __init__(
608
+ self,
609
+ vllm_config: VllmConfig,
610
+ prefix: str = "",
611
+ ):
612
+ super().__init__()
613
+
614
+ self.tp_size = get_tensor_model_parallel_world_size()
615
+ config = vllm_config.model_config.hf_config
616
+ quant_config = vllm_config.quant_config
617
+ self.prefix = prefix
618
+ if vllm_config.parallel_config.enable_expert_parallel:
619
+ self.use_mega_moe = (
620
+ vllm_config.kernel_config.moe_backend == "deep_gemm_mega_moe"
621
+ )
622
+ else:
623
+ self.use_mega_moe = False
624
+
625
+ self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
626
+ self.hidden_size = config.hidden_size
627
+
628
+ self.n_routed_experts = config.n_routed_experts
629
+ self.n_activated_experts = config.num_experts_per_tok
630
+ self.moe_intermediate_size = config.moe_intermediate_size
631
+ self.swiglu_limit = config.swiglu_limit
632
+ self.renormalize = config.norm_topk_prob
633
+ self.scoring_func = getattr(config, "scoring_func", "sqrtsoftplus")
634
+ if self.use_mega_moe and self.scoring_func != "sqrtsoftplus":
635
+ raise NotImplementedError(
636
+ "DeepSeek V4 MegaMoE currently supports sqrtsoftplus routing only."
637
+ )
638
+
639
+ self.gate = GateLinear(
640
+ config.hidden_size,
641
+ config.n_routed_experts,
642
+ out_dtype=torch.float32,
643
+ bias=False,
644
+ prefix=f"{prefix}.gate",
645
+ )
646
+ self.gate.e_score_correction_bias = None
647
+ self.gate.tid2eid = None
648
+ is_hash_moe = extract_layer_index(prefix) < config.num_hash_layers
649
+ self.hash_indices_dtype = torch.int64 if self.use_mega_moe else torch.int32
650
+
651
+ if is_hash_moe:
652
+ # hash MoE doesn't use e_score_correction_bias
653
+ # Use randint instead of empty to avoid garbage values causing
654
+ # invalid memory access in dummy mode (--load-format="dummy")
655
+ self.gate.tid2eid = nn.Parameter(
656
+ torch.randint(
657
+ 0,
658
+ config.n_routed_experts,
659
+ (config.vocab_size, config.num_experts_per_tok),
660
+ dtype=self.hash_indices_dtype,
661
+ ),
662
+ requires_grad=False,
663
+ )
664
+ elif getattr(config, "topk_method", None) == "noaux_tc":
665
+ self.gate.e_score_correction_bias = nn.Parameter(
666
+ torch.empty(config.n_routed_experts, dtype=torch.float32),
667
+ requires_grad=False,
668
+ )
669
+
670
+ if config.n_shared_experts is None:
671
+ self.shared_experts = None
672
+ else:
673
+ intermediate_size = config.moe_intermediate_size * config.n_shared_experts
674
+
675
+ self.shared_experts = DeepseekV2MLP(
676
+ hidden_size=config.hidden_size,
677
+ intermediate_size=intermediate_size,
678
+ hidden_act=config.hidden_act,
679
+ quant_config=quant_config,
680
+ reduce_results=self.use_mega_moe,
681
+ prefix=f"{prefix}.shared_experts",
682
+ )
683
+
684
+ if self.use_mega_moe:
685
+ self._init_mega_moe_experts(vllm_config, config, prefix)
686
+ else:
687
+ self._init_fused_moe_experts(config, quant_config, prefix)
688
+
689
+ def _init_mega_moe_experts(
690
+ self,
691
+ vllm_config: VllmConfig,
692
+ config,
693
+ prefix: str,
694
+ ) -> None:
695
+ self.ep_group = get_ep_group()
696
+ self.ep_size = self.ep_group.world_size
697
+ self.ep_rank = self.ep_group.rank_in_group
698
+ assert config.n_routed_experts % self.ep_size == 0
699
+
700
+ self.n_local_experts = config.n_routed_experts // self.ep_size
701
+ self.experts_start_idx = self.ep_rank * self.n_local_experts
702
+ self.experts_end_idx = self.experts_start_idx + self.n_local_experts
703
+
704
+ self.experts = DeepseekV4MegaMoEExperts(
705
+ vllm_config,
706
+ num_experts=config.n_routed_experts,
707
+ num_local_experts=self.n_local_experts,
708
+ experts_start_idx=self.experts_start_idx,
709
+ top_k=config.num_experts_per_tok,
710
+ hidden_size=config.hidden_size,
711
+ intermediate_size=config.moe_intermediate_size,
712
+ prefix=f"{prefix}.experts",
713
+ )
714
+
715
+ def _init_fused_moe_experts(
716
+ self,
717
+ config,
718
+ quant_config,
719
+ prefix: str,
720
+ ) -> None:
721
+ self.tp_rank = get_tensor_model_parallel_rank()
722
+ assert config.n_routed_experts % self.tp_size == 0
723
+
724
+ self.n_local_experts = config.n_routed_experts // self.tp_size
725
+ self.experts_start_idx = self.tp_rank * self.n_local_experts
726
+ self.experts_end_idx = self.experts_start_idx + self.n_local_experts
727
+
728
+ self.experts = FusedMoE(
729
+ shared_experts=self.shared_experts,
730
+ gate=self.gate,
731
+ num_experts=config.n_routed_experts,
732
+ top_k=config.num_experts_per_tok,
733
+ hidden_size=config.hidden_size,
734
+ intermediate_size=config.moe_intermediate_size,
735
+ renormalize=config.norm_topk_prob,
736
+ quant_config=quant_config,
737
+ prefix=f"{prefix}.experts",
738
+ scoring_func=self.scoring_func,
739
+ routed_scaling_factor=self.routed_scaling_factor,
740
+ e_score_correction_bias=self.gate.e_score_correction_bias,
741
+ hash_indices_table=self.gate.tid2eid,
742
+ swiglu_limit=self.swiglu_limit,
743
+ router_logits_dtype=torch.float32,
744
+ )
745
+
746
+ def forward(
747
+ self, hidden_states: torch.Tensor, input_ids: torch.Tensor | None = None
748
+ ) -> torch.Tensor:
749
+ if self.gate.tid2eid is not None:
750
+ if input_ids is None:
751
+ raise ValueError("DeepSeek V4 hash MoE routing requires input_ids.")
752
+ input_ids = input_ids.to(dtype=self.hash_indices_dtype)
753
+ if not self.use_mega_moe:
754
+ return self._forward_fused_moe(hidden_states, input_ids)
755
+
756
+ org_shape = hidden_states.shape
757
+ router_logits, _ = self.gate(hidden_states)
758
+ topk_weights, topk_ids = fused_topk_bias(
759
+ hidden_states=hidden_states,
760
+ gating_output=router_logits,
761
+ scoring_func=self.scoring_func,
762
+ e_score_correction_bias=self.gate.e_score_correction_bias.data
763
+ if self.gate.e_score_correction_bias is not None
764
+ else None,
765
+ topk=self.n_activated_experts,
766
+ renormalize=self.renormalize,
767
+ indices_type=self.hash_indices_dtype,
768
+ input_tokens=input_ids,
769
+ hash_indices_table=self.gate.tid2eid,
770
+ routed_scaling_factor=self.routed_scaling_factor,
771
+ )
772
+ activation_clamp = (
773
+ float(self.swiglu_limit) if self.swiglu_limit is not None else None
774
+ )
775
+ final_hidden_states = self.experts(
776
+ hidden_states,
777
+ topk_weights,
778
+ topk_ids,
779
+ activation_clamp=activation_clamp,
780
+ )
781
+
782
+ if self.shared_experts is not None:
783
+ shared_output = self.shared_experts(hidden_states)
784
+ final_hidden_states += shared_output
785
+
786
+ return final_hidden_states.view(org_shape)
787
+
788
+ def _forward_fused_moe(
789
+ self, hidden_states: torch.Tensor, input_ids: torch.Tensor | None = None
790
+ ) -> torch.Tensor:
791
+ org_shape = hidden_states.shape
792
+ if self.experts.is_internal_router:
793
+ # In this case, the gate/router runs inside the FusedMoE class
794
+ final_hidden_states = self.experts(
795
+ hidden_states=hidden_states,
796
+ router_logits=hidden_states,
797
+ input_ids=input_ids,
798
+ )
799
+ else:
800
+ router_logits, _ = self.gate(hidden_states)
801
+ final_hidden_states = self.experts(
802
+ hidden_states=hidden_states,
803
+ router_logits=router_logits,
804
+ input_ids=input_ids,
805
+ )
806
+
807
+ return final_hidden_states.view(org_shape)
808
+
809
+ def finalize_mega_moe_weights(self) -> None:
810
+ if self.use_mega_moe:
811
+ self.experts.finalize_weights()
812
+
813
+
814
+class DeepseekV4Attention(nn.Module):
815
+ def __init__(
816
+ self,
817
+ vllm_config: VllmConfig,
818
+ prefix: str,
819
+ topk_indices_buffer: torch.Tensor | None = None,
820
+ aux_stream: torch.cuda.Stream | None = None,
821
+ ):
822
+ super().__init__()
823
+ config = vllm_config.model_config.hf_config
824
+ quant_config = vllm_config.quant_config
825
+ layer_id = extract_layer_index(prefix)
826
+
827
+ self.layer_id = layer_id
828
+ self.hidden_size = config.hidden_size
829
+ self.n_heads = config.num_attention_heads
830
+ tp_size = get_tensor_model_parallel_world_size()
831
+ assert self.n_heads % tp_size == 0
832
+
833
+ self.n_local_heads = self.n_heads // tp_size
834
+ self.q_lora_rank = config.q_lora_rank
835
+ self.o_lora_rank = config.o_lora_rank
836
+ self.head_dim = config.head_dim
837
+ self.rope_head_dim = config.qk_rope_head_dim
838
+ self.nope_head_dim = self.head_dim - self.rope_head_dim
839
+ self.n_groups = config.o_groups
840
+ self.n_local_groups = self.n_groups // tp_size
841
+ self.window_size = config.sliding_window
842
+ # NOTE(zyongye) Compress ratio can't be 0
843
+ # we do this for because MTP layer is not included
844
+ # in the compress ratio list
845
+ if layer_id < config.num_hidden_layers:
846
+ self.compress_ratio = max(1, config.compress_ratios[layer_id])
847
+ else:
848
+ self.compress_ratio = 1
849
+ self.eps = config.rms_norm_eps
850
+ self.max_position_embeddings = config.max_position_embeddings
851
+
852
+ # Padded to min 64 heads for FlashMLA, initialized to -inf
853
+ # (no sink effect). Weight loading fills the first n_local_heads slots.
854
+ padded_heads = max(self.n_local_heads, 64)
855
+ self.attn_sink = nn.Parameter(
856
+ torch.full((padded_heads,), -float("inf"), dtype=torch.float32),
857
+ requires_grad=False,
858
+ )
859
+
860
+ self.fused_wqa_wkv = MergedColumnParallelLinear(
861
+ self.hidden_size,
862
+ [self.q_lora_rank, self.head_dim],
863
+ bias=False,
864
+ quant_config=quant_config,
865
+ prefix=f"{prefix}.fused_wqa_wkv",
866
+ disable_tp=True, # fused ReplicatedLinear
867
+ )
868
+ self.q_norm = RMSNorm(self.q_lora_rank, self.eps)
869
+ self.wq_b = ColumnParallelLinear(
870
+ self.q_lora_rank,
871
+ self.n_heads * self.head_dim,
872
+ bias=False,
873
+ quant_config=quant_config,
874
+ return_bias=False,
875
+ prefix=f"{prefix}.wq_b",
876
+ )
877
+
878
+ self.kv_norm = RMSNorm(self.head_dim, self.eps)
879
+ self.wo_a = ColumnParallelLinear(
880
+ self.n_heads * self.head_dim // self.n_groups,
881
+ self.n_groups * self.o_lora_rank,
882
+ bias=False,
883
+ quant_config=quant_config,
884
+ return_bias=False,
885
+ prefix=f"{prefix}.wo_a",
886
+ )
887
+ self.wo_a.is_bmm = True
888
+ self.wo_a.bmm_batch_size = self.n_local_groups
889
+ self.wo_b = RowParallelLinear(
890
+ self.n_groups * self.o_lora_rank,
891
+ self.hidden_size,
892
+ bias=False,
893
+ quant_config=quant_config,
894
+ return_bias=False,
895
+ prefix=f"{prefix}.wo_b",
896
+ )
897
+ self.softmax_scale = self.head_dim**-0.5
898
+ self.scale_fmt = config.quantization_config["scale_fmt"]
899
+
900
+ self.rope_parameters = config.rope_scaling
901
+
902
+ # Initialize rotary embedding BEFORE DeepseekV4MLAModules (which needs it)
903
+ rope_parameters = config.rope_parameters
904
+ rope_parameters["rope_theta"] = (
905
+ config.compress_rope_theta if self.compress_ratio > 1 else config.rope_theta
906
+ )
907
+ if config.rope_parameters["rope_type"] != "default":
908
+ config.rope_parameters["rope_type"] = (
909
+ "deepseek_yarn"
910
+ if config.rope_parameters.get("apply_yarn_scaling", True)
911
+ else "deepseek_llama_scaling"
912
+ )
913
+ rope_parameters["mscale"] = 0 # Disable mscale
914
+ rope_parameters["mscale_all_dim"] = 0 # Disable mscale
915
+ rope_parameters["is_deepseek_v4"] = True
916
+ rope_parameters["rope_dim"] = self.rope_head_dim
917
+ self.rotary_emb = get_rope(
918
+ self.head_dim,
919
+ max_position=self.max_position_embeddings,
920
+ rope_parameters=rope_parameters,
921
+ is_neox_style=False,
922
+ dtype=config.torch_dtype,
923
+ )
924
+
925
+ self.indexer = None
926
+ if self.compress_ratio == 4:
927
+ # Only C4A uses sparse attention and hence has indexer.
928
+ self.indexer = DeepseekV4Indexer(
929
+ vllm_config,
930
+ config=config,
931
+ hidden_size=self.hidden_size,
932
+ q_lora_rank=self.q_lora_rank,
933
+ quant_config=quant_config,
934
+ cache_config=vllm_config.cache_config,
935
+ topk_indices_buffer=topk_indices_buffer,
936
+ compress_ratio=self.compress_ratio,
937
+ prefix=f"{prefix}.indexer",
938
+ )
939
+
940
+ mla_modules = DeepseekV4MLAModules(
941
+ vllm_config=vllm_config,
942
+ fused_wqa_wkv=self.fused_wqa_wkv,
943
+ q_norm=self.q_norm,
944
+ wq_b=self.wq_b,
945
+ kv_norm=self.kv_norm,
946
+ wo_a=self.wo_a,
947
+ wo_b=self.wo_b,
948
+ attn_sink=self.attn_sink,
949
+ rotary_emb=self.rotary_emb,
950
+ indexer=self.indexer,
951
+ indexer_rotary_emb=self.rotary_emb,
952
+ topk_indices_buffer=topk_indices_buffer,
953
+ aux_stream=aux_stream,
954
+ )
955
+ self.mla_attn = DeepseekV4MultiHeadLatentAttentionWrapper(
956
+ hidden_size=self.hidden_size,
957
+ num_heads=self.n_local_heads,
958
+ head_dim=self.head_dim,
959
+ scale=self.softmax_scale,
960
+ qk_nope_head_dim=self.nope_head_dim,
961
+ qk_rope_head_dim=self.rope_head_dim,
962
+ v_head_dim=self.head_dim,
963
+ q_lora_rank=self.q_lora_rank,
964
+ kv_lora_rank=self.head_dim,
965
+ o_lora_rank=self.o_lora_rank,
966
+ mla_modules=mla_modules,
967
+ window_size=self.window_size,
968
+ compress_ratio=self.compress_ratio,
969
+ cache_config=vllm_config.cache_config,
970
+ quant_config=quant_config,
971
+ prefix=prefix,
972
+ )
973
+
974
+ def forward(
975
+ self,
976
+ positions: torch.Tensor,
977
+ hidden_states: torch.Tensor,
978
+ llama_4_scaling: torch.Tensor | None,
979
+ ):
980
+ return self.mla_attn(positions, hidden_states, llama_4_scaling)
981
+
982
+
983
+class DeepseekV4DecoderLayer(nn.Module):
984
+ def __init__(
985
+ self,
986
+ vllm_config,
987
+ prefix,
988
+ topk_indices_buffer: torch.Tensor | None = None,
989
+ aux_stream_dict: dict[AuxStreamType, torch.cuda.Stream] | None = None,
990
+ ):
991
+ super().__init__()
992
+ config = vllm_config.model_config.hf_config
993
+ self.hidden_size = config.hidden_size
994
+
995
+ self.rms_norm_eps = config.rms_norm_eps
996
+ self.attn = DeepseekV4Attention(
997
+ vllm_config,
998
+ prefix=f"{prefix}.attn",
999
+ topk_indices_buffer=topk_indices_buffer,
1000
+ aux_stream=aux_stream_dict.get(AuxStreamType.Attention)
1001
+ if aux_stream_dict is not None
1002
+ else None,
1003
+ )
1004
+ self.ffn = DeepseekV4MoE(vllm_config, prefix=f"{prefix}.ffn")
1005
+
1006
+ self.attn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
1007
+ self.ffn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
1008
+ self.hc_mult = config.hc_mult
1009
+ self.hc_sinkhorn_iters = config.hc_sinkhorn_iters
1010
+ self.hc_eps = config.hc_eps
1011
+ self.hc_post_alpha = 2.0
1012
+ mix_hc = (2 + self.hc_mult) * self.hc_mult
1013
+ hc_dim = self.hc_mult * self.hidden_size
1014
+ self.hc_attn_fn = nn.Parameter(
1015
+ torch.empty(
1016
+ (mix_hc, hc_dim),
1017
+ dtype=torch.float32,
1018
+ ),
1019
+ requires_grad=False,
1020
+ )
1021
+ self.hc_ffn_fn = nn.Parameter(
1022
+ torch.empty(
1023
+ (mix_hc, hc_dim),
1024
+ dtype=torch.float32,
1025
+ ),
1026
+ requires_grad=False,
1027
+ )
1028
+ self.hc_attn_base = nn.Parameter(
1029
+ torch.empty(
1030
+ mix_hc,
1031
+ dtype=torch.float32,
1032
+ ),
1033
+ requires_grad=False,
1034
+ )
1035
+ self.hc_ffn_base = nn.Parameter(
1036
+ torch.empty(
1037
+ mix_hc,
1038
+ dtype=torch.float32,
1039
+ ),
1040
+ requires_grad=False,
1041
+ )
1042
+ self.hc_attn_scale = nn.Parameter(
1043
+ torch.empty(
1044
+ 3,
1045
+ dtype=torch.float32,
1046
+ ),
1047
+ requires_grad=False,
1048
+ )
1049
+ self.hc_ffn_scale = nn.Parameter(
1050
+ torch.empty(
1051
+ 3,
1052
+ dtype=torch.float32,
1053
+ ),
1054
+ requires_grad=False,
1055
+ )
1056
+
1057
+ def hc_pre(
1058
+ self,
1059
+ x: torch.Tensor,
1060
+ hc_fn: torch.Tensor,
1061
+ hc_scale: torch.Tensor,
1062
+ hc_base: torch.Tensor,
1063
+ ):
1064
+ # Lazy import to avoid top-level tilelang dependency.
1065
+ # Registers both torch.ops.vllm.mhc_pre and mhc_post,
1066
+ # so hc_post() doesn't need its own import.
1067
+ import vllm.model_executor.layers.mhc # noqa: F401
1068
+
1069
+ post_mix, res_mix, layer_input = torch.ops.vllm.mhc_pre(
1070
+ residual=x,
1071
+ fn=hc_fn,
1072
+ hc_scale=hc_scale,
1073
+ hc_base=hc_base,
1074
+ rms_eps=self.rms_norm_eps,
1075
+ hc_pre_eps=self.hc_eps,
1076
+ hc_sinkhorn_eps=self.hc_eps,
1077
+ hc_post_mult_value=self.hc_post_alpha,
1078
+ sinkhorn_repeat=self.hc_sinkhorn_iters,
1079
+ )
1080
+ return layer_input, post_mix, res_mix
1081
+
1082
+ def hc_post(
1083
+ self,
1084
+ x: torch.Tensor,
1085
+ residual: torch.Tensor,
1086
+ post: torch.Tensor,
1087
+ comb: torch.Tensor,
1088
+ ):
1089
+ return torch.ops.vllm.mhc_post(x, residual, post, comb)
1090
+
1091
+ def forward(
1092
+ self,
1093
+ x: torch.Tensor,
1094
+ positions: torch.Tensor,
1095
+ input_ids: torch.Tensor | None,
1096
+ ) -> torch.Tensor:
1097
+ residual = x
1098
+ x, post, comb = self.hc_pre(
1099
+ x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base
1100
+ )
1101
+ x = self.attn_norm(x)
1102
+ x = self.attn(positions, x, None)
1103
+ x = self.hc_post(x, residual, post, comb)
1104
+
1105
+ residual = x
1106
+ x, post, comb = self.hc_pre(
1107
+ x, self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base
1108
+ )
1109
+ x = self.ffn_norm(x)
1110
+ x = self.ffn(x, input_ids)
1111
+ x = self.hc_post(x, residual, post, comb)
1112
+ return x
1113
+
1114
+
1115
+@support_torch_compile
1116
+class DeepseekV4Model(nn.Module):
1117
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1118
+ super().__init__()
1119
+
1120
+ config = vllm_config.model_config.hf_config
1121
+ quant_config = vllm_config.quant_config
1122
+ self.config = config
1123
+
1124
+ self.vocab_size = config.vocab_size
1125
+ self.hc_eps = config.hc_eps
1126
+ self.hc_mult = config.hc_mult
1127
+ self.hc_dim = self.hc_mult * config.hidden_size
1128
+ self.rms_norm_eps = config.rms_norm_eps
1129
+
1130
+ aux_stream_list = [torch.cuda.Stream() for _ in range(1)]
1131
+ self.aux_stream_dict = {
1132
+ AuxStreamType.Attention: aux_stream_list[0],
1133
+ }
1134
+
1135
+ self.device = current_platform.device_type
1136
+ # Reserved topk indices buffer for all Indexer layers to reuse.
1137
+ self.topk_indices_buffer = torch.empty(
1138
+ vllm_config.scheduler_config.max_num_batched_tokens,
1139
+ config.index_topk,
1140
+ dtype=torch.int32,
1141
+ device=self.device,
1142
+ )
1143
+
1144
+ self.embed_tokens = VocabParallelEmbedding(
1145
+ config.vocab_size,
1146
+ config.hidden_size,
1147
+ quant_config=quant_config,
1148
+ prefix=f"{prefix}.embed_tokens",
1149
+ )
1150
+
1151
+ self.start_layer, self.end_layer, self.layers = make_layers(
1152
+ config.num_hidden_layers,
1153
+ lambda prefix: DeepseekV4DecoderLayer(
1154
+ vllm_config,
1155
+ prefix=prefix,
1156
+ topk_indices_buffer=self.topk_indices_buffer,
1157
+ aux_stream_dict=self.aux_stream_dict,
1158
+ ),
1159
+ prefix=f"{prefix}.layers",
1160
+ )
1161
+
1162
+ self.norm = RMSNorm(config.hidden_size, self.rms_norm_eps)
1163
+
1164
+ self.hc_head_fn = nn.Parameter(
1165
+ torch.empty(
1166
+ self.hc_mult,
1167
+ self.hc_dim,
1168
+ dtype=torch.float32,
1169
+ ),
1170
+ requires_grad=False,
1171
+ )
1172
+ self.hc_head_base = nn.Parameter(
1173
+ torch.empty(
1174
+ self.hc_mult,
1175
+ dtype=torch.float32,
1176
+ ),
1177
+ requires_grad=False,
1178
+ )
1179
+ self.hc_head_scale = nn.Parameter(
1180
+ torch.empty(1, dtype=torch.float32),
1181
+ requires_grad=False,
1182
+ )
1183
+
1184
+ # Pre-hc_head residual stream buffer for the MTP draft. Stable
1185
+ # address (outside the cudagraph pool) so the copy_ in forward()
1186
+ # refreshes it correctly across captured shapes.
1187
+ self._mtp_hidden_buffer = torch.empty(
1188
+ vllm_config.scheduler_config.max_num_batched_tokens,
1189
+ self.hc_dim,
1190
+ dtype=vllm_config.model_config.dtype,
1191
+ device=self.device,
1192
+ )
1193
+
1194
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
1195
+ return self.embed_tokens(input_ids)
1196
+
1197
+ def forward(
1198
+ self,
1199
+ input_ids: torch.Tensor,
1200
+ positions: torch.Tensor,
1201
+ intermediate_tensors: IntermediateTensors | None,
1202
+ inputs_embeds: torch.Tensor | None = None,
1203
+ ) -> torch.Tensor | IntermediateTensors:
1204
+ hidden_states = self.embed_input_ids(input_ids)
1205
+ hidden_states = hidden_states.unsqueeze(-2).repeat(1, self.hc_mult, 1)
1206
+
1207
+ for layer in islice(self.layers, self.start_layer, self.end_layer):
1208
+ hidden_states = layer(
1209
+ hidden_states,
1210
+ positions,
1211
+ input_ids,
1212
+ )
1213
+
1214
+ # Stash pre-hc_head residual for the MTP draft (captured copy_).
1215
+ num_tokens = hidden_states.shape[0]
1216
+ self._mtp_hidden_buffer[:num_tokens].copy_(hidden_states.flatten(1))
1217
+
1218
+ hidden_states = hc_head(
1219
+ hidden_states,
1220
+ self.hc_head_fn,
1221
+ self.hc_head_scale,
1222
+ self.hc_head_base,
1223
+ self.rms_norm_eps,
1224
+ self.hc_eps,
1225
+ )
1226
+ hidden_states = self.norm(hidden_states)
1227
+ return hidden_states
1228
+
1229
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1230
+ stacked_params_mapping = [
1231
+ # (param_name, shard_name, shard_id)
1232
+ ("gate_up_proj", "w1", 0),
1233
+ ("gate_up_proj", "w3", 1),
1234
+ ("attn.fused_wqa_wkv", "attn.wq_a", 0),
1235
+ ("attn.fused_wqa_wkv", "attn.wkv", 1),
1236
+ ("compressor.fused_wkv_wgate", "compressor.wkv", 0),
1237
+ ("compressor.fused_wkv_wgate", "compressor.wgate", 1),
1238
+ ]
1239
+ params_dict = dict(self.named_parameters())
1240
+ loaded_params: set[str] = set()
1241
+
1242
+ # TP for attention
1243
+ tp_size = get_tensor_model_parallel_world_size()
1244
+ tp_rank = get_tensor_model_parallel_rank()
1245
+ n_head = self.config.num_attention_heads
1246
+ n_local_head = n_head // tp_size
1247
+ head_rank_start = n_local_head * tp_rank
1248
+ head_rank_end = n_local_head * (tp_rank + 1)
1249
+
1250
+ # Pre-compute expert mapping ONCE.
1251
+ expert_mapping = self.get_expert_mapping()
1252
+
1253
+ for name, loaded_weight in weights:
1254
+ for param_name, weight_name, shard_id in stacked_params_mapping:
1255
+ # Skip non-stacked layers and experts (experts handled below).
1256
+ if ".experts." in name:
1257
+ continue
1258
+ if weight_name not in name:
1259
+ continue
1260
+ name = name.replace(weight_name, param_name)
1261
+
1262
+ param = params_dict[name]
1263
+ weight_loader = param.weight_loader
1264
+ weight_loader(param, loaded_weight, shard_id)
1265
+ loaded_params.add(name)
1266
+ break
1267
+ else:
1268
+ if ".experts." in name:
1269
+ # E8M0 scales are stored as float8_e8m0fnu in
1270
+ # checkpoints but the MoE param is uint8. copy_()
1271
+ # would do a numeric conversion (e.g. 2^-7 → 0),
1272
+ # destroying the raw exponent bytes.
1273
+ if (
1274
+ "weight_scale" in name
1275
+ and loaded_weight.dtype == torch.float8_e8m0fnu
1276
+ ):
1277
+ loaded_weight = loaded_weight.view(torch.uint8)
1278
+ for mapping in expert_mapping:
1279
+ param_name, weight_name, expert_id, shard_id = mapping
1280
+ if weight_name not in name:
1281
+ continue
1282
+ name_mapped = name.replace(weight_name, param_name)
1283
+ param = params_dict[name_mapped]
1284
+ # We should ask the weight loader to return success or not
1285
+ # here since otherwise we may skip experts with other
1286
+ # available replicas.
1287
+ weight_loader = typing.cast(
1288
+ Callable[..., bool], param.weight_loader
1289
+ )
1290
+ success = weight_loader(
1291
+ param,
1292
+ loaded_weight,
1293
+ name_mapped,
1294
+ shard_id=shard_id,
1295
+ expert_id=expert_id,
1296
+ return_success=True,
1297
+ )
1298
+ if success:
1299
+ name = name_mapped
1300
+ break
1301
+ loaded_params.add(name_mapped)
1302
+ continue
1303
+ elif "attn_sink" in name:
1304
+ narrow_weight = loaded_weight[head_rank_start:head_rank_end]
1305
+ n = narrow_weight.shape[0]
1306
+ params_dict[name][:n].copy_(narrow_weight)
1307
+ loaded_params.add(name)
1308
+ continue
1309
+ else:
1310
+ param = params_dict[name]
1311
+ weight_loader = getattr(
1312
+ param, "weight_loader", default_weight_loader
1313
+ )
1314
+ weight_loader(param, loaded_weight)
1315
+ loaded_params.add(name)
1316
+ continue
1317
+
1318
+ return loaded_params
1319
+
1320
+ def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
1321
+ first_layer = next(iter(islice(self.layers, self.start_layer, self.end_layer)))
1322
+ if first_layer.ffn.use_mega_moe:
1323
+ return make_deepseek_v4_expert_params_mapping(self.config.n_routed_experts)
1324
+ # Params for weights, fp8 weight scales, fp8 activation scales
1325
+ # (param_name, weight_name, expert_id, shard_id)
1326
+ return FusedMoE.make_expert_params_mapping(
1327
+ self,
1328
+ ckpt_gate_proj_name="w1",
1329
+ ckpt_down_proj_name="w2",
1330
+ ckpt_up_proj_name="w3",
1331
+ num_experts=self.config.n_routed_experts,
1332
+ )
1333
+
1334
+ def finalize_mega_moe_weights(self) -> None:
1335
+ for layer in islice(self.layers, self.start_layer, self.end_layer):
1336
+ layer.ffn.finalize_mega_moe_weights()
1337
+
1338
+
1339
+@torch.compile(backend=current_platform.simple_compile_backend)
1340
+def hc_head(
1341
+ hidden_states: torch.Tensor,
1342
+ hc_fn: torch.Tensor,
1343
+ hc_scale: torch.Tensor,
1344
+ hc_base: torch.Tensor,
1345
+ rms_norm_eps: float,
1346
+ hc_eps: float,
1347
+) -> torch.Tensor:
1348
+ x = hidden_states
1349
+ shape, dtype = x.size(), x.dtype
1350
+ x = x.flatten(1).float()
1351
+ rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + rms_norm_eps)
1352
+ mixes = F.linear(x, hc_fn) * rsqrt
1353
+ pre = torch.sigmoid(mixes * hc_scale + hc_base) + hc_eps
1354
+ y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=1)
1355
+ return y.to(dtype)
1356
+
1357
+
1358
+class DeepseekV4ForCausalLM(nn.Module):
1359
+ model_cls = DeepseekV4Model
1360
+
1361
+ hf_to_vllm_mapper = WeightsMapper(
1362
+ orig_to_new_prefix={
1363
+ "layers.": "model.layers.",
1364
+ "embed.": "model.embed.",
1365
+ "norm.": "model.norm.",
1366
+ "hc_head": "model.hc_head",
1367
+ "mtp.": "model.mtp.",
1368
+ },
1369
+ orig_to_new_regex={
1370
+ # Routed MoE expert scales: experts.N.wX.scale -> .weight_scale
1371
+ re.compile(r"(\.experts\.\d+\.w[123])\.scale$"): r"\1.weight_scale",
1372
+ # Everything else (FP8 linear + shared experts): .scale -> .weight_scale_inv
1373
+ re.compile(r"\.scale$"): ".weight_scale_inv",
1374
+ },
1375
+ orig_to_new_suffix={
1376
+ "head.weight": "lm_head.weight",
1377
+ "embed.weight": "embed_tokens.weight",
1378
+ ".ffn.gate.bias": ".ffn.gate.e_score_correction_bias",
1379
+ },
1380
+ orig_to_new_substr={
1381
+ ".attn.compressor.": ".attn.mla_attn.compressor.",
1382
+ ".shared_experts.w2": ".shared_experts.down_proj",
1383
+ },
1384
+ )
1385
+
1386
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1387
+ super().__init__()
1388
+
1389
+ config = vllm_config.model_config.hf_config
1390
+ self.config = config
1391
+
1392
+ self.model = self.model_cls(
1393
+ vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
1394
+ )
1395
+ self.lm_head = ParallelLMHead(
1396
+ config.vocab_size,
1397
+ config.hidden_size,
1398
+ prefix=maybe_prefix(prefix, "lm_head"),
1399
+ )
1400
+ self.logits_processor = LogitsProcessor(config.vocab_size)
1401
+
1402
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
1403
+ return self.model.embed_input_ids(input_ids)
1404
+
1405
+ def compute_logits(
1406
+ self,
1407
+ hidden_states: torch.Tensor,
1408
+ ) -> torch.Tensor | None:
1409
+ logits = self.logits_processor(self.lm_head, hidden_states)
1410
+ return logits
1411
+
1412
+ def forward(
1413
+ self,
1414
+ input_ids: torch.Tensor,
1415
+ positions: torch.Tensor,
1416
+ intermediate_tensors: IntermediateTensors | None = None,
1417
+ inputs_embeds: torch.Tensor | None = None,
1418
+ ) -> torch.Tensor | IntermediateTensors:
1419
+ hidden_states = self.model(
1420
+ input_ids, positions, intermediate_tensors, inputs_embeds
1421
+ )
1422
+ return hidden_states
1423
+
1424
+ def get_mtp_target_hidden_states(self) -> torch.Tensor | None:
1425
+ """Pre-hc_head residual stream buffer (max_num_batched_tokens,
1426
+ hc_mult * hidden_size) for the MTP draft model. Populated by
1427
+ forward(); valid after each target step."""
1428
+ return getattr(self.model, "_mtp_hidden_buffer", None)
1429
+
1430
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1431
+ loader = AutoWeightsLoader(self, skip_substrs=["mtp."])
1432
+ loaded_params = loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1433
+ self.model.finalize_mega_moe_weights()
1434
+ return loaded_params
1435
+
1436
+ def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
1437
+ return self.model.get_expert_mapping()