[DSV4][Kernel] Fuse shared experts into MegaMoE (#53040)
Signed-off-by: Canlin Guo <canlinguosdu@gmail.com>
Canlin Guo committed
Aug 21, 2026 at 03:07 UTC
4f6885fffc931ce030d55a97ffb03bbeb0934d10
5 files changed
+569
-44
tests/models/test_deepseek_v4_mega_moe.py
+232
@@ -13,6 +13,7 @@ from vllm.models.deepseek_v4.nvidia.dspark import DSparkDeepseekV4ForCausalLM
13
from vllm.models.deepseek_v4.nvidia.model import (
14
DeepseekV4ForCausalLM,
15
DeepseekV4MegaMoEExperts,
16
+ DeepseekV4MoE,
17
make_deepseek_v4_expert_params_mapping,
18
)
19
from vllm.models.deepseek_v4.nvidia.mtp import DeepSeekV4MTP
@@ -168,6 +169,149 @@ def test_deepseek_v4_mega_moe_weight_loader_uses_ep_expert_ownership():
169
assert torch.count_nonzero(experts.w13_weight[1]) == 0
170
171
172
+def test_deepseek_v4_mega_moe_finalizes_native_shared_expert_weights(monkeypatch):
173
+ class FakeDeepGemm:
174
+ transformed_dims: list[tuple[int, int]] = []
175
+ scale_inputs: list[tuple[int, ...]] = []
176
+
177
+ @staticmethod
178
+ def get_symm_buffer_for_mega_moe(*args, num_shared_experts=0, **kwargs):
179
+ return None
180
+
181
+ @staticmethod
182
+ def get_block_m_for_mega_moe(*args, **kwargs):
183
+ return 128
184
+
185
+ @staticmethod
186
+ def fp8_fp4_mega_moe(
187
+ y,
188
+ l1_weights,
189
+ l2_weights,
190
+ sym_buffer,
191
+ shared_l1_weights=None,
192
+ shared_l2_weights=None,
193
+ **kwargs,
194
+ ):
195
+ return None
196
+
197
+ @classmethod
198
+ def transform_sf_into_required_layout(cls, sf, mn, k, *args, **kwargs):
199
+ cls.scale_inputs.append(tuple(sf.shape))
200
+ return torch.empty((sf.shape[0], mn, k // 128), dtype=torch.int32)
201
+
202
+ @classmethod
203
+ def transform_weights_for_mega_moe(cls, l1_weights, l2_weights):
204
+ cls.transformed_dims.append((l1_weights[0].dim(), l2_weights[0].dim()))
205
+ if l1_weights[0].dim() == 2:
206
+ return (l1_weights[0].clone(), l1_weights[1]), l2_weights
207
+ return l1_weights, l2_weights
208
+
209
+ vllm_config = SimpleNamespace(
210
+ scheduler_config=SimpleNamespace(max_num_batched_tokens=4),
211
+ compilation_config=SimpleNamespace(static_forward_context={}),
212
+ )
213
+ experts = DeepseekV4MegaMoEExperts(
214
+ vllm_config,
215
+ num_experts=2,
216
+ num_local_experts=1,
217
+ experts_start_idx=0,
218
+ top_k=1,
219
+ hidden_size=128,
220
+ intermediate_size=128,
221
+ num_shared_experts=1,
222
+ )
223
+ experts._check_runtime_supported = lambda: None
224
+
225
+ def fp8_parameter(*shape):
226
+ return torch.nn.Parameter(
227
+ torch.empty(*shape, dtype=torch.float8_e4m3fn), requires_grad=False
228
+ )
229
+
230
+ def scale_parameter(*shape, dtype=torch.int32):
231
+ return torch.nn.Parameter(torch.ones(*shape, dtype=dtype), requires_grad=False)
232
+
233
+ shared_experts = SimpleNamespace(
234
+ gate_up_proj=SimpleNamespace(
235
+ weight=fp8_parameter(256, 128),
236
+ weight_block_size=(128, 128),
237
+ weight_scale_inv=scale_parameter(2, 1, dtype=torch.float8_e8m0fnu),
238
+ ),
239
+ down_proj=SimpleNamespace(
240
+ weight=fp8_parameter(128, 128),
241
+ weight_block_size=(128, 128),
242
+ weight_scale_inv=scale_parameter(1, 1, dtype=torch.float8_e8m0fnu),
243
+ ),
244
+ )
245
+ monkeypatch.setattr("vllm.utils.deep_gemm._import_deep_gemm", lambda: FakeDeepGemm)
246
+
247
+ original_gate_up_ptr = shared_experts.gate_up_proj.weight.data_ptr()
248
+ experts.finalize_weights(shared_experts)
249
+
250
+ assert FakeDeepGemm.transformed_dims == [(3, 3), (2, 2)]
251
+ assert FakeDeepGemm.scale_inputs[-2:] == [(1, 256, 4), (1, 128, 4)]
252
+ assert experts.has_fused_shared_experts
253
+ assert shared_experts.gate_up_proj.weight.data_ptr() != original_gate_up_ptr
254
+ assert (
255
+ experts._transformed_shared_l1_weights[0].data_ptr()
256
+ == shared_experts.gate_up_proj.weight.data_ptr()
257
+ )
258
+ assert (
259
+ experts._transformed_shared_l2_weights[0].data_ptr()
260
+ == shared_experts.down_proj.weight.data_ptr()
261
+ )
262
+
263
+
264
+@pytest.mark.parametrize("fused", [False, True])
265
+def test_deepseek_v4_mega_moe_does_not_double_add_fused_shared_expert(
266
+ monkeypatch, fused
267
+):
268
+ class FakeGate(torch.nn.Module):
269
+ tid2eid = None
270
+ e_score_correction_bias = None
271
+
272
+ def forward(self, hidden_states):
273
+ return torch.empty(hidden_states.shape[0], 2), None
274
+
275
+ class FakeExperts(torch.nn.Module):
276
+ has_fused_shared_experts = fused
277
+
278
+ def forward(self, hidden_states, *args, **kwargs):
279
+ return torch.ones_like(hidden_states)
280
+
281
+ class FakeSharedExperts(torch.nn.Module):
282
+ calls = 0
283
+
284
+ def forward(self, hidden_states):
285
+ self.calls += 1
286
+ return torch.full_like(hidden_states, 2)
287
+
288
+ moe = DeepseekV4MoE.__new__(DeepseekV4MoE)
289
+ torch.nn.Module.__init__(moe)
290
+ moe.use_mega_moe = True
291
+ moe.gate = FakeGate()
292
+ moe.experts = FakeExperts()
293
+ moe.shared_experts = FakeSharedExperts()
294
+ moe.scoring_func = "sqrtsoftplus"
295
+ moe.n_activated_experts = 1
296
+ moe.renormalize = True
297
+ moe.hash_indices_dtype = torch.int64
298
+ moe.routed_scaling_factor = 1.0
299
+ moe.swiglu_limit = 10.0
300
+ monkeypatch.setattr(
301
+ "vllm.models.deepseek_v4.nvidia.model.fused_topk_bias",
302
+ lambda **kwargs: (
303
+ torch.ones(kwargs["hidden_states"].shape[0], 1),
304
+ torch.zeros(kwargs["hidden_states"].shape[0], 1, dtype=torch.int64),
305
+ ),
306
+ )
307
+
308
+ output = moe(torch.zeros(2, 128))
309
+
310
+ expected = 1 if fused else 3
311
+ assert torch.all(output == expected)
312
+ assert moe.shared_experts.calls == (0 if fused else 1)
313
+
314
+
315
@pytest.mark.skipif(
316
not torch.cuda.is_available(),
317
reason="DeepSeek V4 MegaMoE fused input staging requires CUDA.",
@@ -246,6 +390,94 @@ def test_deepseek_v4_mega_moe_fused_input_staging_is_bitwise_exact():
390
)
391
392
393
+@pytest.mark.parametrize("shared_block_m", [8, 32, 96, 128, 192])
394
+def test_deepseek_v4_mega_moe_stages_shared_scale_tma_layout(shared_block_m):
395
+ from vllm.third_party.deep_gemm.utils import per_token_cast_to_fp8
396
+
397
+ device = torch.device("cuda")
398
+ num_tokens = shared_block_m + 7
399
+ hidden_size = 256
400
+ top_k = 8
401
+ generator = torch.Generator(device=device)
402
+ generator.manual_seed(shared_block_m)
403
+ hidden_states = torch.randn(
404
+ num_tokens,
405
+ hidden_size,
406
+ device=device,
407
+ dtype=torch.bfloat16,
408
+ generator=generator,
409
+ )
410
+ topk_ids = torch.randint(
411
+ 0,
412
+ 256,
413
+ (num_tokens, top_k),
414
+ device=device,
415
+ dtype=torch.int32,
416
+ generator=generator,
417
+ )
418
+ topk_weights = torch.randn(
419
+ num_tokens,
420
+ top_k,
421
+ device=device,
422
+ dtype=torch.float32,
423
+ generator=generator,
424
+ )
425
+
426
+ ref_x, ref_x_sf = per_token_cast_to_fp8(
427
+ hidden_states,
428
+ use_ue8m0=True,
429
+ gran_k=32,
430
+ use_packed_ue8m0=True,
431
+ )
432
+ aligned_block_m = ((shared_block_m + 127) // 128) * 128
433
+ num_shared_rows = ((num_tokens + shared_block_m - 1) // shared_block_m) * (
434
+ aligned_block_m
435
+ )
436
+ ref_shared_x_sf = torch.zeros(
437
+ num_shared_rows,
438
+ hidden_size // 128,
439
+ dtype=torch.int32,
440
+ device=device,
441
+ )
442
+ for token_id in range(num_tokens):
443
+ m_in_block = token_id % shared_block_m
444
+ transposed_m = (
445
+ (m_in_block // 128) * 128 + (m_in_block % 32) * 4 + (m_in_block % 128) // 32
446
+ )
447
+ shared_row = token_id // shared_block_m * aligned_block_m + transposed_m
448
+ ref_shared_x_sf[shared_row].copy_(ref_x_sf[token_id])
449
+
450
+ fused_x = torch.empty_like(ref_x)
451
+ fused_x_sf = torch.empty_like(ref_x_sf)
452
+ fused_shared_storage = torch.full(
453
+ (hidden_size // 128, num_shared_rows),
454
+ -1,
455
+ dtype=torch.int32,
456
+ device=device,
457
+ )
458
+ fused_shared_x_sf = fused_shared_storage.t()
459
+ fused_topk_idx = torch.empty_like(topk_ids, dtype=torch.int64)
460
+ fused_topk_weights = torch.empty_like(topk_weights)
461
+
462
+ prepare_megamoe_inputs(
463
+ hidden_states,
464
+ topk_weights,
465
+ topk_ids,
466
+ fused_x,
467
+ fused_x_sf,
468
+ fused_topk_idx,
469
+ fused_topk_weights,
470
+ shared_x_sf=fused_shared_x_sf,
471
+ shared_block_m=shared_block_m,
472
+ )
473
+ torch.accelerator.synchronize()
474
+
475
+ populated = ref_shared_x_sf != 0
476
+ assert torch.equal(fused_x.view(torch.uint8), ref_x.view(torch.uint8))
477
+ assert torch.equal(fused_x_sf, ref_x_sf)
478
+ assert torch.equal(fused_shared_x_sf[populated], ref_shared_x_sf[populated])
479
+
480
+
481
def test_deepseek_v4_pwal_hook_finalizes_mega_moe_and_mhc_broadcast():
482
"""The loader invokes the model-level PWAL hook for every load format,
483
so it must finalize megamoe + mhc broadcast weights to cover dummy
vllm/envs.py
+7
@@ -288,6 +288,7 @@ if TYPE_CHECKING:
288
VLLM_GC_DEBUG: str = ""
289
VLLM_DEBUG_WORKSPACE: bool = False
290
VLLM_DISABLE_SHARED_EXPERTS_STREAM: bool = False
291
+ VLLM_DISABLE_DSV4_MEGAMOE_SHARED_EXPERT_FUSION: bool = False
292
VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD: int = 256
293
VLLM_MULTI_STREAM_GEMM_TOKEN_THRESHOLD: int = 1024
294
VLLM_COMPILE_CACHE_SAVE_FORMAT: Literal["binary", "unpacked"] = "binary"
@@ -1993,6 +1994,12 @@ environment_variables: dict[str, Callable[[], Any]] = {
1994
"VLLM_DISABLE_SHARED_EXPERTS_STREAM": lambda: bool(
1995
int(os.getenv("VLLM_DISABLE_SHARED_EXPERTS_STREAM", "0"))
1996
),
1997
+ # Emergency rollback for the DeepSeek-V4 NVIDIA MegaMoE path. By default,
1998
+ # DeepGEMM computes replicated FP8 shared experts in the same persistent
1999
+ # SM100 kernel as the routed FP4 experts.
2000
+ "VLLM_DISABLE_DSV4_MEGAMOE_SHARED_EXPERT_FUSION": lambda: bool(
2001
+ int(os.getenv("VLLM_DISABLE_DSV4_MEGAMOE_SHARED_EXPERT_FUSION", "0"))
2002
+ ),
2003
# Limits when we run shared_experts in a separate stream.
2004
# We found out that for large batch sizes, the separate stream
2005
# execution is not beneficial (most likely because of the input clone)
vllm/models/deepseek_v4/nvidia/model.py
+274
-42
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import typing
4
from collections.abc import Callable, Iterable
5
+from inspect import signature
6
from itertools import islice
7
8
import regex as re
@@ -18,6 +19,7 @@ from vllm.distributed import (
19
)
20
from vllm.distributed.eplb.eplb_state import EplbLayerState
21
from vllm.forward_context import get_forward_context, is_forward_context_available
22
+from vllm.logger import init_logger
23
from vllm.model_executor.kernels.mhc.tilelang import (
24
hc_head_fused_kernel_tilelang,
25
mhc_fused_post_pre_tilelang,
@@ -84,6 +86,8 @@ from vllm.utils.math_utils import cdiv
86
from vllm.v1.attention.backends.registry import AttentionBackendEnum
87
from vllm.v1.worker.ubatching import dbo_current_ubatch_id
88
89
+logger = init_logger(__name__)
90
+
91
92
class DeepseekV4MLP(nn.Module):
93
def __init__(
@@ -165,7 +169,7 @@ def make_deepseek_v4_expert_params_mapping(
169
170
171
class DeepseekV4MegaMoEExperts(nn.Module):
168
- _symm_buffer_cache: dict[tuple[int, int, int, int, int, int, int], object] = {}
172
+ _symm_buffer_cache: dict[tuple[int, int, int, int, int, int, int, int], object] = {}
173
174
def __init__(
175
self,
@@ -177,6 +181,7 @@ class DeepseekV4MegaMoEExperts(nn.Module):
181
top_k: int,
182
hidden_size: int,
183
intermediate_size: int,
184
+ num_shared_experts: int = 0,
185
prefix: str = "",
186
num_logical_experts: int | None = None,
187
):
@@ -190,6 +195,7 @@ class DeepseekV4MegaMoEExperts(nn.Module):
195
self.top_k = top_k
196
self.hidden_size = hidden_size
197
self.intermediate_size = intermediate_size
198
+ self.num_shared_experts = num_shared_experts
199
self.max_num_tokens = vllm_config.scheduler_config.max_num_batched_tokens
200
201
self.num_logical_experts = (
@@ -247,6 +253,12 @@ class DeepseekV4MegaMoEExperts(nn.Module):
253
254
self._transformed_l1_weights: tuple[torch.Tensor, torch.Tensor] | None = None
255
self._transformed_l2_weights: tuple[torch.Tensor, torch.Tensor] | None = None
256
+ self._transformed_shared_l1_weights: (
257
+ tuple[torch.Tensor, torch.Tensor] | None
258
+ ) = None
259
+ self._transformed_shared_l2_weights: (
260
+ tuple[torch.Tensor, torch.Tensor] | None
261
+ ) = None
262
263
# Register in the static forward context so the custom-op wrapper
264
# can look up this module by name from within a torch.compile graph.
@@ -321,45 +333,220 @@ class DeepseekV4MegaMoEExperts(nn.Module):
333
"to be multiples of 128."
334
)
335
324
- def finalize_weights(self) -> None:
325
- if self._transformed_l1_weights is not None:
326
- return
336
+ @staticmethod
337
+ def _deep_gemm_supports_shared_experts(deep_gemm) -> bool:
338
+ """Check the Python API before touching a symmetric-memory group.
339
328
- self._check_runtime_supported()
329
- from vllm.utils.deep_gemm import _import_deep_gemm
340
+ This also gives users of an older precompiled vLLM wheel a safe serial
341
+ fallback instead of failing halfway through multi-rank buffer setup.
342
+ """
343
+ try:
344
+ buffer_params = signature(deep_gemm.get_symm_buffer_for_mega_moe).parameters
345
+ kernel_params = signature(deep_gemm.fp8_fp4_mega_moe).parameters
346
+ except (TypeError, ValueError):
347
+ return False
348
+ return (
349
+ hasattr(deep_gemm, "get_block_m_for_mega_moe")
350
+ and hasattr(deep_gemm, "transform_weights_for_mega_moe")
351
+ and "num_shared_experts" in buffer_params
352
+ and "shared_l1_weights" in kernel_params
353
+ and "shared_l2_weights" in kernel_params
354
+ )
355
331
- deep_gemm = _import_deep_gemm()
356
+ def _finalize_shared_expert_weights(
357
+ self, deep_gemm, shared_experts: DeepseekV4MLP
358
+ ) -> None:
359
+ gate_up = shared_experts.gate_up_proj
360
+ down = shared_experts.down_proj
361
+ gate_up_weight = gate_up.weight.data
362
+ gate_up_scale = gate_up.weight_scale_inv.data
363
+ down_weight = down.weight.data
364
+ down_scale = down.weight_scale_inv.data
365
+
366
+ # MegaMoE's shared FP8 MMA consumes a 1x32 scale for every weight row,
367
+ # while the checkpoint uses coarser block-FP8 scales (usually
368
+ # 128x128). Build a dedicated, numerically equivalent scale view before
369
+ # the generic linear post-load hook replaces the raw checkpoint scales
370
+ # with its 128x128 DeepGEMM layout.
371
+ checkpoint_scale_dtypes = (torch.float8_e8m0fnu, torch.uint8)
372
+ if (
373
+ gate_up_scale.dtype in checkpoint_scale_dtypes
374
+ and down_scale.dtype in checkpoint_scale_dtypes
375
+ ):
376
+ gate_up_scale = self._prepare_shared_expert_scale(
377
+ deep_gemm,
378
+ gate_up,
379
+ gate_up_scale,
380
+ gate_up_weight.shape[0],
381
+ gate_up_weight.shape[1],
382
+ )
383
+ down_scale = self._prepare_shared_expert_scale(
384
+ deep_gemm,
385
+ down,
386
+ down_scale,
387
+ down_weight.shape[0],
388
+ down_weight.shape[1],
389
+ )
390
+
391
+ if gate_up_scale is None or down_scale is None:
392
+ self.num_shared_experts = 0
393
+ return
394
333
- w13_scale = deep_gemm.transform_sf_into_required_layout(
334
- self._ue8m0_uint8_to_float(self.w13_weight_scale.data).contiguous(),
335
- 2 * self.intermediate_size,
395
+ shared_intermediate_size = self.intermediate_size * self.num_shared_experts
396
+ expected_gate_up_shape = (
397
+ 2 * shared_intermediate_size,
398
self.hidden_size,
337
- (1, 32),
338
- self.num_local_experts,
399
)
340
- w2_scale = deep_gemm.transform_sf_into_required_layout(
341
- self._ue8m0_uint8_to_float(self.w2_weight_scale.data).contiguous(),
342
- self.hidden_size,
343
- self.intermediate_size,
344
- (1, 32),
345
- self.num_local_experts,
400
+ expected_down_shape = (self.hidden_size, shared_intermediate_size)
401
+ if (
402
+ gate_up_weight.dtype != torch.float8_e4m3fn
403
+ or down_weight.dtype != torch.float8_e4m3fn
404
+ or gate_up_scale.dtype != torch.int32
405
+ or down_scale.dtype != torch.int32
406
+ or tuple(gate_up_weight.shape) != expected_gate_up_shape
407
+ or tuple(down_weight.shape) != expected_down_shape
408
+ ):
409
+ logger.warning(
410
+ "Disabling native MegaMoE shared-expert fusion for %s: expected "
411
+ "replicated block-FP8 weights with gate_up=%s, down=%s, and "
412
+ "DeepGEMM int32 scales; got gate_up=%s/%s/%s and down=%s/%s/%s.",
413
+ self.prefix,
414
+ expected_gate_up_shape,
415
+ expected_down_shape,
416
+ tuple(gate_up_weight.shape),
417
+ gate_up_weight.dtype,
418
+ gate_up_scale.dtype,
419
+ tuple(down_weight.shape),
420
+ down_weight.dtype,
421
+ down_scale.dtype,
422
+ )
423
+ self.num_shared_experts = 0
424
+ return
425
+
426
+ transformed_l1, transformed_l2 = deep_gemm.transform_weights_for_mega_moe(
427
+ (gate_up_weight, gate_up_scale),
428
+ (down_weight, down_scale),
429
+ )
430
+ # L1 interleaving allocates a full copy. Re-home the loader Parameter on
431
+ # that storage so the original 2*intermediate*hidden FP8 tensor can be
432
+ # released instead of adding roughly 0.7 GiB per rank on DSV4-Flash.
433
+ # The generic linear post-load hook may still repack the serial scales,
434
+ # but this shared MLP is never called after native fusion is enabled.
435
+ gate_up.weight.data = transformed_l1[0]
436
+ self._transformed_shared_l1_weights = (
437
+ gate_up.weight.data,
438
+ transformed_l1[1],
439
)
347
- self._transformed_l1_weights, self._transformed_l2_weights = (
348
- deep_gemm.transform_weights_for_mega_moe(
349
- (self.w13_weight.data.view(torch.int8).contiguous(), w13_scale),
350
- (self.w2_weight.data.view(torch.int8).contiguous(), w2_scale),
440
+ self._transformed_shared_l2_weights = transformed_l2
441
+
442
+ def _prepare_shared_expert_scale(
443
+ self,
444
+ deep_gemm,
445
+ linear: nn.Module,
446
+ scale: torch.Tensor,
447
+ mn: int,
448
+ k: int,
449
+ ) -> torch.Tensor | None:
450
+ block_size = getattr(linear, "weight_block_size", None)
451
+ if block_size is None or len(block_size) != 2:
452
+ logger.warning(
453
+ "Disabling native MegaMoE shared-expert fusion for %s: "
454
+ "shared FP8 weight block size is unavailable.",
455
+ self.prefix,
456
+ )
457
+ return None
458
+
459
+ block_m, block_k = block_size
460
+ expected_shape = (
461
+ (mn + block_m - 1) // block_m,
462
+ (k + block_k - 1) // block_k,
463
+ )
464
+ if block_k % 32 != 0 or tuple(scale.shape) != expected_shape:
465
+ logger.warning(
466
+ "Disabling native MegaMoE shared-expert fusion for %s: "
467
+ "cannot convert shared scale shape %s with block size %s "
468
+ "to MegaMoE's 1x32 layout for weight (%d, %d).",
469
+ self.prefix,
470
+ tuple(scale.shape),
471
+ tuple(block_size),
472
+ mn,
473
+ k,
474
)
475
+ return None
476
+
477
+ scale_fp32 = self._ue8m0_uint8_to_float(scale.view(torch.uint8))
478
+ scale_1x32 = (
479
+ scale_fp32.repeat_interleave(block_m, dim=0)
480
+ .repeat_interleave(block_k // 32, dim=1)[:mn, : k // 32]
481
+ .contiguous()
482
)
353
- # Drop the original loader-side parameters: the MegaMoE kernels only
354
- # consume the transformed views above. transform_weights_for_mega_moe
355
- # allocates a fresh tensor for the L1 weight (see _interleave_l1_weights)
356
- # and fresh SF tensors for L1/L2; the L2 weight is the only tensor that
357
- # aliases the original storage, and _transformed_l2_weights still holds
358
- # it, so the storage stays live after we drop the Parameter.
359
- self.w13_weight = None
360
- self.w13_weight_scale = None
361
- self.w2_weight = None
362
- self.w2_weight_scale = None
483
+ # The grouped API is used with a singleton dimension to request the
484
+ # MN-major, TMA-aligned packed-UE8M0 strides, then squeezed back to the
485
+ # 2D layout required for a shared expert.
486
+ return deep_gemm.transform_sf_into_required_layout(
487
+ scale_1x32.unsqueeze(0),
488
+ mn,
489
+ k,
490
+ (1, 32),
491
+ 1,
492
+ ).squeeze(0)
493
+
494
+ def finalize_weights(self, shared_experts: DeepseekV4MLP | None = None) -> None:
495
+ from vllm.utils.deep_gemm import _import_deep_gemm
496
+
497
+ deep_gemm = _import_deep_gemm()
498
+
499
+ if self._transformed_l1_weights is None:
500
+ self._check_runtime_supported()
501
+ w13_scale = deep_gemm.transform_sf_into_required_layout(
502
+ self._ue8m0_uint8_to_float(self.w13_weight_scale.data).contiguous(),
503
+ 2 * self.intermediate_size,
504
+ self.hidden_size,
505
+ (1, 32),
506
+ self.num_local_experts,
507
+ )
508
+ w2_scale = deep_gemm.transform_sf_into_required_layout(
509
+ self._ue8m0_uint8_to_float(self.w2_weight_scale.data).contiguous(),
510
+ self.hidden_size,
511
+ self.intermediate_size,
512
+ (1, 32),
513
+ self.num_local_experts,
514
+ )
515
+ self._transformed_l1_weights, self._transformed_l2_weights = (
516
+ deep_gemm.transform_weights_for_mega_moe(
517
+ (self.w13_weight.data.view(torch.int8).contiguous(), w13_scale),
518
+ (self.w2_weight.data.view(torch.int8).contiguous(), w2_scale),
519
+ )
520
+ )
521
+ # Drop the original loader-side parameters: the MegaMoE kernels only
522
+ # consume the transformed views above. transform_weights_for_mega_moe
523
+ # allocates a fresh tensor for the L1 weight (see
524
+ # _interleave_l1_weights) and fresh SF tensors for L1/L2; the L2
525
+ # weight is the only tensor that aliases the original storage, and
526
+ # _transformed_l2_weights still holds it, so the storage stays live
527
+ # after we drop the Parameter.
528
+ self.w13_weight = None
529
+ self.w13_weight_scale = None
530
+ self.w2_weight = None
531
+ self.w2_weight_scale = None
532
+
533
+ if shared_experts is None or self.num_shared_experts == 0:
534
+ return
535
+ if self._transformed_shared_l1_weights is not None:
536
+ return
537
+ if not self._deep_gemm_supports_shared_experts(deep_gemm):
538
+ logger.warning_once(
539
+ "Disabling native MegaMoE shared-expert fusion because the "
540
+ "installed DeepGEMM Python API is older than the vLLM "
541
+ "source. Rebuild the vendored _deep_gemm_C extension to enable it.",
542
+ )
543
+ self.num_shared_experts = 0
544
+ return
545
+ self._finalize_shared_expert_weights(deep_gemm, shared_experts)
546
+
547
+ @property
548
+ def has_fused_shared_experts(self) -> bool:
549
+ return self._transformed_shared_l1_weights is not None
550
551
def get_symm_buffer(self):
552
from vllm.utils.deep_gemm import _import_deep_gemm
@@ -376,6 +563,7 @@ class DeepseekV4MegaMoEExperts(nn.Module):
563
self.top_k,
564
self.hidden_size,
565
self.intermediate_size,
566
+ self.num_shared_experts if self.has_fused_shared_experts else 0,
567
)
568
symm_buffer = self._symm_buffer_cache.get(key)
569
if symm_buffer is None:
@@ -386,6 +574,9 @@ class DeepseekV4MegaMoEExperts(nn.Module):
574
self.top_k,
575
self.hidden_size,
576
self.intermediate_size,
577
+ num_shared_experts=(
578
+ self.num_shared_experts if self.has_fused_shared_experts else 0
579
+ ),
580
)
581
self._symm_buffer_cache[key] = symm_buffer
582
return symm_buffer
@@ -492,6 +683,19 @@ class DeepseekV4MegaMoEExperts(nn.Module):
683
else None,
684
)
685
686
+ shared_x_sf = None
687
+ shared_block_m = None
688
+ if self.has_fused_shared_experts:
689
+ shared_x_sf = symm_buffer.shared_l1_acts_sf
690
+ shared_block_m = deep_gemm.get_block_m_for_mega_moe(
691
+ get_ep_group().world_size,
692
+ self.num_experts,
693
+ symm_buffer.num_max_tokens_per_rank,
694
+ num_tokens,
695
+ self.top_k,
696
+ "fp8xfp4",
697
+ )
698
+
699
prepare_megamoe_inputs(
700
hidden_states,
701
topk_weights,
@@ -501,18 +705,32 @@ class DeepseekV4MegaMoEExperts(nn.Module):
705
symm_buffer.topk_idx[:num_tokens],
706
symm_buffer.topk_weights[:num_tokens],
707
is_padding=is_padding,
708
+ shared_x_sf=shared_x_sf,
709
+ shared_block_m=shared_block_m,
710
)
711
712
assert self._transformed_l1_weights is not None
713
assert self._transformed_l2_weights is not None
508
- deep_gemm.fp8_fp4_mega_moe(
509
- y,
510
- self._transformed_l1_weights,
511
- self._transformed_l2_weights,
512
- symm_buffer,
513
- activation_clamp=activation_clamp,
514
- fast_math=fast_math,
515
- )
714
+ if self.has_fused_shared_experts:
715
+ deep_gemm.fp8_fp4_mega_moe(
716
+ y,
717
+ self._transformed_l1_weights,
718
+ self._transformed_l2_weights,
719
+ symm_buffer,
720
+ shared_l1_weights=self._transformed_shared_l1_weights,
721
+ shared_l2_weights=self._transformed_shared_l2_weights,
722
+ activation_clamp=activation_clamp,
723
+ fast_math=fast_math,
724
+ )
725
+ else:
726
+ deep_gemm.fp8_fp4_mega_moe(
727
+ y,
728
+ self._transformed_l1_weights,
729
+ self._transformed_l2_weights,
730
+ symm_buffer,
731
+ activation_clamp=activation_clamp,
732
+ fast_math=fast_math,
733
+ )
734
return y
735
736
@@ -645,6 +863,16 @@ class DeepseekV4MoE(nn.Module):
863
self.experts_start_idx = self.physical_expert_start
864
self.experts_end_idx = self.physical_expert_end
865
866
+ # Native DeepGEMM fusion requires each EP rank to own the complete
867
+ # shared MLP. Sequence parallel replicates those weights while sharding
868
+ # tokens. TP=1 is also naturally replicated. With PP+TP the shared MLP
869
+ # remains tensor-sharded, so retain the serial path.
870
+ fuse_shared_experts = bool(
871
+ self.shared_experts is not None
872
+ and not envs.VLLM_DISABLE_DSV4_MEGAMOE_SHARED_EXPERT_FUSION
873
+ and (self.use_sequence_parallel or self.tp_size == 1)
874
+ )
875
+
876
self.experts = DeepseekV4MegaMoEExperts(
877
vllm_config,
878
num_experts=self.n_physical_experts,
@@ -654,6 +882,7 @@ class DeepseekV4MoE(nn.Module):
882
top_k=config.num_experts_per_tok,
883
hidden_size=config.hidden_size,
884
intermediate_size=config.moe_intermediate_size,
885
+ num_shared_experts=(self.n_shared_experts if fuse_shared_experts else 0),
886
prefix=f"{prefix}.experts",
887
)
888
@@ -739,7 +968,10 @@ class DeepseekV4MoE(nn.Module):
968
activation_clamp=activation_clamp,
969
)
970
742
- if self.shared_experts is not None:
971
+ if (
972
+ self.shared_experts is not None
973
+ and not self.experts.has_fused_shared_experts
974
+ ):
975
shared_output = self.shared_experts(hidden_states)
976
final_hidden_states += shared_output
977
@@ -759,7 +991,7 @@ class DeepseekV4MoE(nn.Module):
991
992
def finalize_mega_moe_weights(self) -> None:
993
if self.use_mega_moe:
762
- self.experts.finalize_weights()
994
+ self.experts.finalize_weights(self.shared_experts)
995
996
997
def _select_dsv4_attn_cls(vllm_config: VllmConfig) -> type[DeepseekV4Attention]:
vllm/models/deepseek_v4/nvidia/ops/prepare_megamoe.py
+51
@@ -17,6 +17,7 @@ def _prepare_megamoe_inputs_kernel(
17
hidden_states,
18
x_fp8,
19
x_sf,
20
+ shared_x_sf,
21
topk_ids,
22
topk_weights,
23
is_padding,
@@ -28,6 +29,8 @@ def _prepare_megamoe_inputs_kernel(
29
x_stride_k: tl.constexpr,
30
x_sf_stride_m: tl.constexpr,
31
x_sf_stride_k: tl.constexpr,
32
+ shared_x_sf_stride_m: tl.constexpr,
33
+ shared_x_sf_stride_k: tl.constexpr,
34
topk_ids_stride_m: tl.constexpr,
35
topk_ids_stride_k: tl.constexpr,
36
topk_weights_stride_m: tl.constexpr,
@@ -42,6 +45,7 @@ def _prepare_megamoe_inputs_kernel(
45
BLOCK_K: tl.constexpr,
46
GROUP_K: tl.constexpr,
47
BLOCK_TOPK: tl.constexpr,
48
+ SHARED_BLOCK_M: tl.constexpr,
49
) -> None:
50
token_id = tl.program_id(0)
51
k_block_id = tl.program_id(1)
@@ -84,6 +88,25 @@ def _prepare_megamoe_inputs_kernel(
88
packed_scale,
89
)
90
91
+ # DeepGEMM's SM100 shared-expert TMA loads require the activation scales
92
+ # in an MN-major layout whose row permutation depends on the MegaMoE
93
+ # scheduler's runtime BLOCK_M. Write that view while the packed UE8M0 scale
94
+ # is already resident, avoiding another kernel and temporary tensor.
95
+ if shared_x_sf is not None:
96
+ m_block_id = token_id // SHARED_BLOCK_M
97
+ m_in_block = token_id % SHARED_BLOCK_M
98
+ aligned_block_m: tl.constexpr = triton.cdiv(SHARED_BLOCK_M, 128) * 128
99
+ transposed_m = (
100
+ (m_in_block // 128) * 128 + (m_in_block % 32) * 4 + (m_in_block % 128) // 32
101
+ )
102
+ shared_row = m_block_id * aligned_block_m + transposed_m
103
+ tl.store(
104
+ shared_x_sf
105
+ + shared_row * shared_x_sf_stride_m
106
+ + k_block_id * shared_x_sf_stride_k,
107
+ packed_scale,
108
+ )
109
+
110
if k_block_id == 0:
111
topk_offsets = tl.arange(0, BLOCK_TOPK)
112
topk_mask = topk_offsets < top_k
@@ -131,6 +154,8 @@ def prepare_megamoe_inputs(
154
topk_idx_out: torch.Tensor,
155
topk_weights_out: torch.Tensor,
156
is_padding: torch.Tensor | None = None,
157
+ shared_x_sf: torch.Tensor | None = None,
158
+ shared_block_m: int | None = None,
159
) -> None:
160
num_tokens, hidden_size = hidden_states.shape
161
if num_tokens == 0:
@@ -146,6 +171,28 @@ def prepare_megamoe_inputs(
171
"DeepSeek V4 MegaMoE input staging requires topk_weights and "
172
"topk_ids to have the same shape."
173
)
174
+ if (shared_x_sf is None) != (shared_block_m is None):
175
+ raise ValueError(
176
+ "DeepSeek V4 MegaMoE shared input staging requires both "
177
+ "shared_x_sf and shared_block_m."
178
+ )
179
+ if shared_x_sf is not None:
180
+ assert shared_block_m is not None
181
+ if shared_block_m <= 0:
182
+ raise ValueError("MegaMoE shared_block_m must be positive.")
183
+ expected_sf_k = hidden_size // 128
184
+ if shared_x_sf.ndim != 2 or shared_x_sf.shape[1] != expected_sf_k:
185
+ raise ValueError(
186
+ "MegaMoE shared_x_sf must have shape "
187
+ f"(*, {expected_sf_k}), got {tuple(shared_x_sf.shape)}."
188
+ )
189
+ aligned_block_m = triton.cdiv(shared_block_m, 128) * 128
190
+ required_rows = triton.cdiv(num_tokens, shared_block_m) * aligned_block_m
191
+ if shared_x_sf.shape[0] < required_rows:
192
+ raise ValueError(
193
+ "MegaMoE shared_x_sf has insufficient rows: requires "
194
+ f"{required_rows}, got {shared_x_sf.shape[0]}."
195
+ )
196
197
block_k = 128
198
grid = (num_tokens, triton.cdiv(hidden_size, block_k))
@@ -155,6 +202,7 @@ def prepare_megamoe_inputs(
202
hidden_states,
203
x_fp8,
204
x_sf,
205
+ shared_x_sf,
206
topk_ids,
207
topk_weights,
208
is_padding,
@@ -166,6 +214,8 @@ def prepare_megamoe_inputs(
214
x_fp8.stride(1),
215
x_sf.stride(0),
216
x_sf.stride(1),
217
+ shared_x_sf.stride(0) if shared_x_sf is not None else 0,
218
+ shared_x_sf.stride(1) if shared_x_sf is not None else 0,
219
topk_ids.stride(0),
220
topk_ids.stride(1),
221
topk_weights.stride(0),
@@ -180,5 +230,6 @@ def prepare_megamoe_inputs(
230
BLOCK_K=block_k,
231
GROUP_K=32,
232
BLOCK_TOPK=block_topk,
233
+ SHARED_BLOCK_M=shared_block_m or 1,
234
num_warps=4,
235
)
vllm/models/kimi_k3/nvidia/model.py
+5
-2
@@ -93,7 +93,10 @@ from vllm.models.common.ops.sequence_parallel import (
93
sp_reduce_scatter,
94
sp_shard,
95
)
96
-from vllm.models.deepseek_v4.nvidia.model import DeepseekV4MegaMoEExperts
96
+from vllm.models.deepseek_v4.nvidia.model import (
97
+ DeepseekV4MegaMoEExperts,
98
+ DeepseekV4MLP,
99
+)
100
from vllm.models.deepseek_v4.nvidia.ops.prepare_megamoe import prepare_megamoe_inputs
101
from vllm.models.kimi_k3.nvidia.kda import KimiK3DeltaAttention
102
from vllm.models.kimi_k3.nvidia.latent_moe_runner import (
@@ -354,7 +357,7 @@ class KimiK3MegaMoEExperts(DeepseekV4MegaMoEExperts):
357
torch.distributed.barrier(group=ep_group.cpu_group)
358
self._synchronized_ep_groups.add(key)
359
357
- def finalize_weights(self) -> None:
360
+ def finalize_weights(self, shared_experts: DeepseekV4MLP | None = None) -> None:
361
if self._transformed_l1_weights is not None:
362
return
363