[ROCm][Perf][DeepSeek V4] Fuse native FP8 shared expert with MXFP4 routed experts (#53161)
Signed-off-by: fai <fangzhouai@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Cursor Agent <agent@cursor.com>
Fangzhou Ai committed
Sep 6, 2026 at 22:42 UTC
de69e821b7c834bcf1ec96325ec8c410ac183680
4 files changed
+839
-6
tests/model_executor/layers/test_fused_shared_expert.py
+340
-1
@@ -2,8 +2,10 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
import importlib
5
+import inspect
6
+import sys
7
from copy import deepcopy
6
-from types import SimpleNamespace
8
+from types import ModuleType, SimpleNamespace
9
from typing import Any, cast
10
from unittest.mock import patch
11
@@ -414,6 +416,343 @@ def test_deepseek_v4_shared_expert_fse_uses_mtp_quantization_config_prefix(
416
assert reason is None
417
418
419
+def test_deepseek_v4_heterogeneous_fhmoe_keeps_native_intermediate_width() -> None:
420
+ from vllm.models.deepseek_v4.amd.model import _prepare_native_fp8_shared_expert
421
+
422
+ hidden_size = 7168
423
+ intermediate_size = 384
424
+ w13 = torch.empty((2 * intermediate_size, hidden_size), dtype=torch.float8_e4m3fn)
425
+ w2 = torch.empty((hidden_size, intermediate_size), dtype=torch.float8_e4m3fn)
426
+ w13_scale_bytes = (torch.arange(6 * 56).reshape(6, 56).remainder(20) + 0x60).to(
427
+ torch.uint8
428
+ )
429
+ w2_scale_bytes = (torch.arange(56 * 3).reshape(56, 3).remainder(30) + 0x50).to(
430
+ torch.uint8
431
+ )
432
+ w13_scale = w13_scale_bytes.view(torch.float8_e8m0fnu)
433
+ w2_scale = w2_scale_bytes.view(torch.float8_e8m0fnu)
434
+
435
+ prepared = _prepare_native_fp8_shared_expert(
436
+ w13, w2, w13_scale, w2_scale, intermediate_size
437
+ )
438
+
439
+ assert prepared[0].shape == (1, 768, 7168)
440
+ assert prepared[1].shape == (1, 7168, 384)
441
+ assert prepared[2].shape == (768, 224)
442
+ assert prepared[3].shape == (7168, 16)
443
+ expected_w13_scale = w13_scale_bytes.repeat_interleave(
444
+ 128, dim=0
445
+ ).repeat_interleave(4, dim=1)
446
+ expected_w2_scale = w2_scale_bytes.repeat_interleave(128, dim=0).repeat_interleave(
447
+ 4, dim=1
448
+ )
449
+ assert torch.equal(prepared[2].view(torch.uint8), expected_w13_scale)
450
+ assert torch.equal(prepared[3].view(torch.uint8)[:, :12], expected_w2_scale)
451
+ assert torch.all(prepared[3].view(torch.uint8)[:, 12:] == 0x7F)
452
+
453
+
454
+@pytest.mark.parametrize(
455
+ ("num_tokens", "supported_through", "expected"),
456
+ [
457
+ (0, 4096, False),
458
+ (1, 4096, True),
459
+ (1536, 4096, True),
460
+ (2048, 4096, True),
461
+ (2049, 4096, True),
462
+ (4096, 4096, True),
463
+ (4097, 4096, False),
464
+ (1536, 0, False),
465
+ ],
466
+)
467
+def test_deepseek_v4_heterogeneous_fhmoe_token_policy(
468
+ monkeypatch: pytest.MonkeyPatch,
469
+ num_tokens: int,
470
+ supported_through: int,
471
+ expected: bool,
472
+) -> None:
473
+ from vllm.models.deepseek_v4.amd import model as deepseek_v4_model
474
+
475
+ checked_tokens: list[int] = []
476
+
477
+ def supports(num_tokens: int) -> bool:
478
+ checked_tokens.append(num_tokens)
479
+ return num_tokens <= supported_through
480
+
481
+ monkeypatch.setattr(
482
+ deepseek_v4_model.rocm_aiter_ops,
483
+ "fused_moe_supports_heterogeneous_shared_expert",
484
+ supports,
485
+ )
486
+
487
+ assert deepseek_v4_model._use_heterogeneous_fhmoe(num_tokens) is expected
488
+ assert checked_tokens == ([] if num_tokens == 0 else [num_tokens])
489
+
490
+
491
+def _supported_fhmoe_signature(
492
+ shared_w1=None,
493
+ shared_w2=None,
494
+ shared_w1_scale=None,
495
+ shared_w2_scale=None,
496
+ shared_expert_id=-1,
497
+) -> None:
498
+ pass
499
+
500
+
501
+def _incomplete_fhmoe_signature(
502
+ shared_w1=None,
503
+ shared_w2=None,
504
+ shared_w1_scale=None,
505
+ shared_w2_scale=None,
506
+) -> None:
507
+ pass
508
+
509
+
510
+def _install_fake_aiter_fhmoe(
511
+ monkeypatch: pytest.MonkeyPatch,
512
+ supports_dsv4_i384_fhmoe: object,
513
+ fused_moe: object,
514
+) -> None:
515
+ fake_aiter = ModuleType("aiter")
516
+ fake_aiter.__path__ = []
517
+ fake_fhmoe = ModuleType("aiter.fhmoe")
518
+ if supports_dsv4_i384_fhmoe is not None:
519
+ fake_fhmoe.__dict__["supports_dsv4_i384_fhmoe"] = supports_dsv4_i384_fhmoe
520
+ fake_fused_moe = ModuleType("aiter.fused_moe")
521
+ fake_fused_moe.__dict__["fused_moe"] = fused_moe
522
+ fake_aiter.__dict__["fhmoe"] = fake_fhmoe
523
+ fake_aiter.__dict__["fused_moe"] = fake_fused_moe
524
+ monkeypatch.setitem(sys.modules, "aiter", fake_aiter)
525
+ monkeypatch.setitem(sys.modules, "aiter.fhmoe", fake_fhmoe)
526
+ monkeypatch.setitem(sys.modules, "aiter.fused_moe", fake_fused_moe)
527
+
528
+
529
+def _supports_fhmoe_through_2047(max_tokens: int) -> bool:
530
+ return max_tokens <= 2047
531
+
532
+
533
+def _supports_fhmoe_through_2048(max_tokens: int) -> bool:
534
+ return max_tokens <= 2048
535
+
536
+
537
+def _supports_fhmoe_through_4096(max_tokens: int) -> bool:
538
+ return max_tokens <= 4096
539
+
540
+
541
+def _raises_fhmoe_config_error(max_tokens: int) -> bool:
542
+ raise OSError
543
+
544
+
545
+def _returns_truthy_non_bool(max_tokens: int) -> int:
546
+ return 1
547
+
548
+
549
+@pytest.mark.parametrize(
550
+ ("num_tokens", "capability", "fused_moe", "expected"),
551
+ [
552
+ (0, _supports_fhmoe_through_2048, _supported_fhmoe_signature, False),
553
+ (True, _supports_fhmoe_through_2048, _supported_fhmoe_signature, False),
554
+ (2048, None, _supported_fhmoe_signature, False),
555
+ (2048, True, _supported_fhmoe_signature, False),
556
+ (2048, _raises_fhmoe_config_error, _supported_fhmoe_signature, False),
557
+ (2048, _returns_truthy_non_bool, _supported_fhmoe_signature, False),
558
+ (2048, _supports_fhmoe_through_2047, _supported_fhmoe_signature, False),
559
+ (2048, _supports_fhmoe_through_2048, _incomplete_fhmoe_signature, False),
560
+ (2048, _supports_fhmoe_through_2048, _supported_fhmoe_signature, True),
561
+ (2049, _supports_fhmoe_through_2048, _supported_fhmoe_signature, False),
562
+ (4096, _supports_fhmoe_through_4096, _supported_fhmoe_signature, True),
563
+ (4097, _supports_fhmoe_through_4096, _supported_fhmoe_signature, False),
564
+ ],
565
+)
566
+def test_deepseek_v4_heterogeneous_fhmoe_aiter_capability(
567
+ monkeypatch: pytest.MonkeyPatch,
568
+ num_tokens: object,
569
+ capability: object,
570
+ fused_moe: object,
571
+ expected: bool,
572
+) -> None:
573
+ from vllm._aiter_ops import rocm_aiter_ops
574
+
575
+ _install_fake_aiter_fhmoe(monkeypatch, capability, fused_moe)
576
+
577
+ assert (
578
+ rocm_aiter_ops._probe_dsv4_i384_fhmoe_capability(cast(int, num_tokens))
579
+ is expected
580
+ )
581
+
582
+
583
+def test_deepseek_v4_heterogeneous_fhmoe_aiter_capability_catches_import_error(
584
+ monkeypatch: pytest.MonkeyPatch,
585
+) -> None:
586
+ from vllm._aiter_ops import rocm_aiter_ops
587
+
588
+ fake_aiter = ModuleType("aiter")
589
+ fake_aiter.__path__ = []
590
+ monkeypatch.setitem(sys.modules, "aiter", fake_aiter)
591
+ monkeypatch.setitem(sys.modules, "aiter.fhmoe", None)
592
+
593
+ assert not rocm_aiter_ops._probe_dsv4_i384_fhmoe_capability(2048)
594
+
595
+
596
+@pytest.mark.parametrize("error", [TypeError, ValueError])
597
+def test_deepseek_v4_heterogeneous_fhmoe_aiter_capability_catches_signature_error(
598
+ monkeypatch: pytest.MonkeyPatch,
599
+ error: type[Exception],
600
+) -> None:
601
+ from vllm._aiter_ops import rocm_aiter_ops
602
+
603
+ _install_fake_aiter_fhmoe(
604
+ monkeypatch, _supports_fhmoe_through_2048, _supported_fhmoe_signature
605
+ )
606
+
607
+ def raise_signature_error(_: object) -> inspect.Signature:
608
+ raise error
609
+
610
+ monkeypatch.setattr(inspect, "signature", raise_signature_error)
611
+
612
+ assert not rocm_aiter_ops._probe_dsv4_i384_fhmoe_capability(2048)
613
+
614
+
615
+@pytest.mark.parametrize(
616
+ ("setting", "value", "expected"),
617
+ [
618
+ ("data_parallel_size", 1, True),
619
+ ("data_parallel_size", 2, False),
620
+ ("prefill_context_parallel_size", 2, False),
621
+ ("topk_method", "greedy", False),
622
+ ("fhmoe_supported", False, False),
623
+ ],
624
+)
625
+def test_deepseek_v4_heterogeneous_fhmoe_compatibility_gates(
626
+ monkeypatch: pytest.MonkeyPatch,
627
+ setting: str,
628
+ value: object,
629
+ expected: bool,
630
+) -> None:
631
+ from vllm.models.deepseek_v4.amd import model as deepseek_v4_model
632
+
633
+ hf_config = SimpleNamespace(
634
+ n_routed_experts=384,
635
+ num_experts_per_tok=6,
636
+ n_shared_experts=1,
637
+ hidden_size=7168,
638
+ moe_intermediate_size=3072,
639
+ hidden_act="silu",
640
+ expert_dtype="fp4",
641
+ topk_method="noaux_tc",
642
+ )
643
+ parallel_config = SimpleNamespace(
644
+ enable_expert_parallel=False,
645
+ enable_eplb=False,
646
+ tensor_parallel_size=8,
647
+ data_parallel_size=1,
648
+ prefill_context_parallel_size=1,
649
+ )
650
+ if setting == "topk_method":
651
+ hf_config.topk_method = value
652
+ elif setting != "fhmoe_supported":
653
+ setattr(parallel_config, setting, value)
654
+
655
+ quant_config = SimpleNamespace(
656
+ get_name=lambda: "deepseek_v4_fp8",
657
+ moe_quant_algo="",
658
+ weight_block_size=[128, 128],
659
+ is_checkpoint_fp8_serialized=True,
660
+ is_scale_e8m0=True,
661
+ ignored_layers=None,
662
+ )
663
+ vllm_config = SimpleNamespace(
664
+ model_config=SimpleNamespace(hf_config=hf_config, dtype=torch.bfloat16),
665
+ quant_config=quant_config,
666
+ parallel_config=parallel_config,
667
+ kernel_config=SimpleNamespace(moe_backend="aiter"),
668
+ offload_config=None,
669
+ )
670
+ monkeypatch.setattr(deepseek_v4_model.current_platform, "is_rocm", lambda: True)
671
+ monkeypatch.setattr(
672
+ deepseek_v4_model.envs,
673
+ "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS",
674
+ True,
675
+ )
676
+ monkeypatch.setattr(deepseek_v4_model, "on_gfx950", lambda: True)
677
+ monkeypatch.setattr(
678
+ deepseek_v4_model.rocm_aiter_ops,
679
+ "is_fusion_moe_shared_experts_enabled",
680
+ lambda: True,
681
+ )
682
+ checked_tokens: list[int] = []
683
+ fhmoe_supported = setting != "fhmoe_supported" or value is True
684
+
685
+ def supports(num_tokens: int) -> bool:
686
+ checked_tokens.append(num_tokens)
687
+ return fhmoe_supported
688
+
689
+ monkeypatch.setattr(
690
+ deepseek_v4_model.rocm_aiter_ops,
691
+ "fused_moe_supports_heterogeneous_shared_expert",
692
+ supports,
693
+ )
694
+
695
+ assert (
696
+ deepseek_v4_model._heterogeneous_shared_expert_enabled(
697
+ cast(VllmConfig, vllm_config)
698
+ )
699
+ is expected
700
+ )
701
+ assert checked_tokens == [1]
702
+
703
+
704
+@pytest.mark.parametrize(
705
+ ("weights_shape", "ids_shape", "message"),
706
+ [
707
+ ((2, 7, 1), (2, 7, 1), "equal two-dimensional shapes"),
708
+ ((2, 7), (3, 7), "equal two-dimensional shapes"),
709
+ ((2, 6), (2, 6), "exactly 7 columns"),
710
+ ((2, 8), (2, 8), "exactly 7 columns"),
711
+ ],
712
+)
713
+def test_deepseek_v4_heterogeneous_fhmoe_rejects_invalid_routes(
714
+ weights_shape: tuple[int, ...],
715
+ ids_shape: tuple[int, ...],
716
+ message: str,
717
+) -> None:
718
+ from vllm.models.deepseek_v4.amd.model import _validate_heterogeneous_routes
719
+
720
+ with pytest.raises(ValueError, match=message):
721
+ _validate_heterogeneous_routes(
722
+ torch.empty(weights_shape),
723
+ torch.empty(ids_shape, dtype=torch.int64),
724
+ experts_per_token=6,
725
+ )
726
+
727
+
728
+def test_deepseek_v4_heterogeneous_fhmoe_accepts_appended_shared_route() -> None:
729
+ from vllm.models.deepseek_v4.amd.model import _validate_heterogeneous_routes
730
+
731
+ _validate_heterogeneous_routes(
732
+ torch.empty((2, 7)),
733
+ torch.empty((2, 7), dtype=torch.int64),
734
+ experts_per_token=6,
735
+ )
736
+
737
+
738
+def test_aiter_fused_moe_validates_shared_expert_arguments() -> None:
739
+ from vllm._aiter_ops import (
740
+ _validate_rocm_aiter_fused_moe_shared_expert_args,
741
+ )
742
+
743
+ validate = _validate_rocm_aiter_fused_moe_shared_expert_args
744
+ shared_tensor = torch.empty(0)
745
+
746
+ assert not validate(None, None, None, None, -1)
747
+ with pytest.raises(ValueError, match="requires shared weights and scales"):
748
+ validate(None, None, None, None, 0)
749
+ with pytest.raises(ValueError, match="both shared weights and scales"):
750
+ validate(shared_tensor, None, None, None, 0)
751
+ with pytest.raises(ValueError, match="non-negative shared expert ID"):
752
+ validate(shared_tensor, shared_tensor, shared_tensor, shared_tensor, -1)
753
+ assert validate(shared_tensor, shared_tensor, shared_tensor, shared_tensor, 0)
754
+
755
+
756
def test_is_model_fused_shared_expert_compatible() -> None:
757
class MoE(nn.Module):
758
def __init__(self, enabled: bool) -> None:
vllm/_aiter_ops.py
+107
@@ -211,6 +211,31 @@ def if_aiter_supported(func: Callable) -> Callable:
211
return wrapper
212
213
214
+def _validate_rocm_aiter_fused_moe_shared_expert_args(
215
+ shared_w1: torch.Tensor | None,
216
+ shared_w2: torch.Tensor | None,
217
+ shared_w1_scale: torch.Tensor | None,
218
+ shared_w2_scale: torch.Tensor | None,
219
+ shared_expert_id: int,
220
+) -> bool:
221
+ shared_tensors = (shared_w1, shared_w2, shared_w1_scale, shared_w2_scale)
222
+ has_shared_expert = any(tensor is not None for tensor in shared_tensors)
223
+ if has_shared_expert:
224
+ if not all(tensor is not None for tensor in shared_tensors):
225
+ raise ValueError(
226
+ "Heterogeneous fused MoE requires both shared weights and scales."
227
+ )
228
+ if shared_expert_id < 0:
229
+ raise ValueError(
230
+ "Heterogeneous fused MoE requires a non-negative shared expert ID."
231
+ )
232
+ elif shared_expert_id >= 0:
233
+ raise ValueError(
234
+ "A non-negative shared expert ID requires shared weights and scales."
235
+ )
236
+ return has_shared_expert
237
+
238
+
239
def _rocm_aiter_fused_moe_impl(
240
hidden_states: torch.Tensor,
241
w1: torch.Tensor,
@@ -236,7 +261,20 @@ def _rocm_aiter_fused_moe_impl(
261
swiglu_limit: float = 0.0,
262
beta: float | None = None,
263
linear_beta: float | None = None,
264
+ shared_w1: torch.Tensor | None = None,
265
+ shared_w2: torch.Tensor | None = None,
266
+ shared_w1_scale: torch.Tensor | None = None,
267
+ shared_w2_scale: torch.Tensor | None = None,
268
+ shared_expert_id: int = -1,
269
) -> torch.Tensor:
270
+ has_shared_expert = _validate_rocm_aiter_fused_moe_shared_expert_args(
271
+ shared_w1,
272
+ shared_w2,
273
+ shared_w1_scale,
274
+ shared_w2_scale,
275
+ shared_expert_id,
276
+ )
277
+
278
from aiter import ActivationType, QuantType
279
from aiter.fused_moe import fused_moe
280
@@ -253,6 +291,15 @@ def _rocm_aiter_fused_moe_impl(
291
extra_kwargs["beta"] = beta
292
extra_kwargs["linear_beta"] = linear_beta
293
294
+ if has_shared_expert:
295
+ extra_kwargs.update(
296
+ shared_w1=shared_w1,
297
+ shared_w2=shared_w2,
298
+ shared_w1_scale=shared_w1_scale,
299
+ shared_w2_scale=shared_w2_scale,
300
+ shared_expert_id=shared_expert_id,
301
+ )
302
+
303
return fused_moe(
304
hidden_states,
305
w1,
@@ -304,6 +351,11 @@ def _rocm_aiter_fused_moe_fake(
351
swiglu_limit: float = 0.0,
352
beta: float | None = None,
353
linear_beta: float | None = None,
354
+ shared_w1: torch.Tensor | None = None,
355
+ shared_w2: torch.Tensor | None = None,
356
+ shared_w1_scale: torch.Tensor | None = None,
357
+ shared_w2_scale: torch.Tensor | None = None,
358
+ shared_expert_id: int = -1,
359
) -> torch.Tensor:
360
if output_dtype is not None:
361
return torch.empty_like(hidden_states, dtype=output_dtype)
@@ -1997,6 +2049,44 @@ class rocm_aiter_ops:
2049
2050
return "gate_mode" in inspect.signature(fused_moe).parameters
2051
2052
+ @staticmethod
2053
+ def _probe_dsv4_i384_fhmoe_capability(num_tokens: int) -> bool:
2054
+ """Probe AITER's CSV-backed DSV4 native-I384 FHMoE contract."""
2055
+ if type(num_tokens) is not int or num_tokens <= 0:
2056
+ return False
2057
+
2058
+ try:
2059
+ import inspect
2060
+
2061
+ import aiter.fhmoe as fhmoe
2062
+ from aiter.fused_moe import fused_moe
2063
+
2064
+ params = inspect.signature(fused_moe).parameters
2065
+ supports_dsv4_i384_fhmoe = getattr(fhmoe, "supports_dsv4_i384_fhmoe", None)
2066
+ if not callable(supports_dsv4_i384_fhmoe):
2067
+ return False
2068
+ supports_num_tokens = supports_dsv4_i384_fhmoe(num_tokens)
2069
+ except Exception:
2070
+ return False
2071
+
2072
+ return supports_num_tokens is True and all(
2073
+ name in params
2074
+ for name in (
2075
+ "shared_w1",
2076
+ "shared_w2",
2077
+ "shared_w1_scale",
2078
+ "shared_w2_scale",
2079
+ "shared_expert_id",
2080
+ )
2081
+ )
2082
+
2083
+ @classmethod
2084
+ @if_aiter_supported
2085
+ @functools.cache
2086
+ def fused_moe_supports_heterogeneous_shared_expert(cls, num_tokens: int) -> bool:
2087
+ """Whether AITER has DSV4 native-I384 configs through the given M."""
2088
+ return cls._probe_dsv4_i384_fhmoe_capability(num_tokens)
2089
+
2090
@staticmethod
2091
def register_ops_once() -> None:
2092
global _OPS_REGISTERED
@@ -2465,6 +2555,11 @@ class rocm_aiter_ops:
2555
swiglu_limit: float = 0.0,
2556
beta: float | None = None,
2557
linear_beta: float | None = None,
2558
+ shared_w1: torch.Tensor | None = None,
2559
+ shared_w2: torch.Tensor | None = None,
2560
+ shared_w1_scale: torch.Tensor | None = None,
2561
+ shared_w2_scale: torch.Tensor | None = None,
2562
+ shared_expert_id: int = -1,
2563
) -> torch.Tensor:
2564
return torch.ops.vllm.rocm_aiter_fused_moe(
2565
hidden_states,
@@ -2491,6 +2586,11 @@ class rocm_aiter_ops:
2586
swiglu_limit,
2587
beta,
2588
linear_beta,
2589
+ shared_w1,
2590
+ shared_w2,
2591
+ shared_w1_scale,
2592
+ shared_w2_scale,
2593
+ shared_expert_id,
2594
)
2595
2596
@staticmethod
@@ -3074,6 +3174,13 @@ class rocm_aiter_ops:
3174
3175
return shuffle_scale_a16w4(tensor, num_experts, gate_up)
3176
3177
+ @staticmethod
3178
+ def shuffle_scale(tensor: torch.Tensor) -> torch.Tensor:
3179
+ """Shuffle a non-gate/up E8M0 scale tensor for AITER."""
3180
+ from aiter.ops.shuffle import shuffle_scale
3181
+
3182
+ return shuffle_scale(tensor)
3183
+
3184
@staticmethod
3185
def shuffle_weights(
3186
*tensors: torch.Tensor, layout: tuple[int, int] = (16, 16)
vllm/model_executor/layers/fused_moe/experts/rocm_aiter_moe.py
+15
@@ -250,6 +250,11 @@ def rocm_aiter_fused_experts(
250
num_local_tokens: torch.Tensor | None = None,
251
output_dtype: torch.dtype | None = None,
252
moe_sorting_dispatch_policy: int = 0,
253
+ shared_w1: torch.Tensor | None = None,
254
+ shared_w2: torch.Tensor | None = None,
255
+ shared_w1_scale: torch.Tensor | None = None,
256
+ shared_w2_scale: torch.Tensor | None = None,
257
+ shared_expert_id: int = -1,
258
) -> torch.Tensor:
259
"""ROCm AITER fused MoE expert computation."""
260
if quant_config is None:
@@ -419,8 +424,18 @@ def rocm_aiter_fused_experts(
424
bias1=quant_config.w1_bias if quant_config.use_mxfp4_w4a16 else None,
425
bias2=quant_config.w2_bias if quant_config.use_mxfp4_w4a16 else None,
426
moe_sorting_dispatch_policy=moe_sorting_dispatch_policy,
427
+ swiglu_limit=(
428
+ 0.0
429
+ if moe_config.swiglu_limit is None
430
+ else float(moe_config.swiglu_limit)
431
+ ),
432
beta=moe_config.activation_situ_beta,
433
linear_beta=moe_config.activation_situ_linear_beta,
434
+ shared_w1=shared_w1,
435
+ shared_w2=shared_w2,
436
+ shared_w1_scale=shared_w1_scale,
437
+ shared_w2_scale=shared_w2_scale,
438
+ shared_expert_id=shared_expert_id,
439
)
440
441
vllm/models/deepseek_v4/amd/model.py
+377
-5
@@ -1,7 +1,9 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import typing
4
+import weakref
5
from collections.abc import Callable, Iterable
6
+from dataclasses import replace
7
from itertools import islice
8
9
import regex as re
@@ -20,9 +22,14 @@ from vllm.logger import init_logger
22
from vllm.model_executor.layers.activation import SiluAndMul, SiluAndMulWithClamp
23
from vllm.model_executor.layers.fused_moe import (
24
FusedMoEFactory,
25
+ FusedMoEQuantConfig,
26
GateLinear,
27
+ RoutedExperts,
28
fused_moe_make_expert_params_mapping,
29
)
30
+from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import (
31
+ rocm_aiter_fused_experts,
32
+)
33
from vllm.model_executor.layers.fused_moe.utils import (
34
is_model_fused_shared_expert_compatible,
35
)
@@ -41,6 +48,7 @@ from vllm.model_executor.layers.mhc import (
48
MHCPreOp,
49
)
50
from vllm.model_executor.layers.quantization import QuantizationConfig
51
+from vllm.model_executor.layers.quantization.mxfp4 import Mxfp4MoEMethod
52
from vllm.model_executor.layers.quantization.utils.config_utils import (
53
is_shared_expert_quant_fse_compatible,
54
)
@@ -65,6 +73,7 @@ from vllm.model_executor.models.utils import (
73
)
74
from vllm.models.deepseek_v4.amd.rocm import DeepseekV4ROCMAiterMLAAttention
75
from vllm.platforms import current_platform
76
+from vllm.platforms.rocm import on_gfx950
77
from vllm.sequence import IntermediateTensors
78
79
logger = init_logger(__name__)
@@ -163,6 +172,336 @@ class DeepseekV4MLP(nn.Module):
172
return x
173
174
175
+def _use_heterogeneous_fhmoe(num_tokens: int) -> bool:
176
+ return num_tokens > 0 and (
177
+ rocm_aiter_ops.fused_moe_supports_heterogeneous_shared_expert(num_tokens)
178
+ is True
179
+ )
180
+
181
+
182
+def _validate_heterogeneous_routes(
183
+ topk_weights: torch.Tensor,
184
+ topk_ids: torch.Tensor,
185
+ experts_per_token: int,
186
+) -> None:
187
+ if (
188
+ topk_weights.ndim != 2
189
+ or topk_ids.ndim != 2
190
+ or topk_weights.shape != topk_ids.shape
191
+ ):
192
+ raise ValueError(
193
+ "Heterogeneous fused MoE routes must have equal two-dimensional shapes."
194
+ )
195
+ expected_columns = experts_per_token + 1
196
+ if topk_weights.shape[1] != expected_columns:
197
+ raise ValueError(
198
+ "Heterogeneous fused MoE routes must have exactly "
199
+ f"{expected_columns} columns, got {topk_weights.shape[1]}."
200
+ )
201
+
202
+
203
+def _heterogeneous_shared_expert_enabled(vllm_config: VllmConfig) -> bool:
204
+ config = vllm_config.model_config.hf_config
205
+ quant_config = vllm_config.quant_config
206
+ parallel_config = vllm_config.parallel_config
207
+ reasons: list[str] = []
208
+
209
+ if not rocm_aiter_ops.is_fusion_moe_shared_experts_enabled():
210
+ return False
211
+ if not on_gfx950():
212
+ reasons.append("the device is not gfx950")
213
+ if parallel_config.enable_expert_parallel:
214
+ reasons.append("expert parallelism is enabled")
215
+ if parallel_config.enable_eplb:
216
+ reasons.append("EPLB is enabled")
217
+ if parallel_config.tensor_parallel_size != 8:
218
+ reasons.append("tensor parallelism is not 8")
219
+ if parallel_config.data_parallel_size != 1:
220
+ reasons.append("data parallelism is enabled")
221
+ if parallel_config.prefill_context_parallel_size != 1:
222
+ reasons.append("prefill context parallelism is enabled")
223
+ if vllm_config.kernel_config.moe_backend != "aiter":
224
+ reasons.append("the MoE backend is not AITER")
225
+ if vllm_config.model_config.dtype != torch.bfloat16:
226
+ reasons.append("the model dtype is not BF16")
227
+ if not rocm_aiter_ops.fused_moe_supports_heterogeneous_shared_expert(1):
228
+ reasons.append(
229
+ "AITER lacks CSV-backed DSV4 I384 heterogeneous shared-expert support"
230
+ )
231
+
232
+ expected_model = {
233
+ "n_routed_experts": 384,
234
+ "num_experts_per_tok": 6,
235
+ "n_shared_experts": 1,
236
+ "hidden_size": 7168,
237
+ "moe_intermediate_size": 3072,
238
+ "hidden_act": "silu",
239
+ "expert_dtype": "fp4",
240
+ "topk_method": "noaux_tc",
241
+ }
242
+ for name, expected in expected_model.items():
243
+ if getattr(config, name, None) != expected:
244
+ reasons.append(f"{name} is not {expected!r}")
245
+
246
+ if quant_config is None or quant_config.get_name() != "deepseek_v4_fp8":
247
+ reasons.append("the DeepSeek V4 FP8 quantization config is not active")
248
+ else:
249
+ if getattr(quant_config, "moe_quant_algo", "").upper() == "NVFP4":
250
+ reasons.append("routed experts use NVFP4")
251
+ if getattr(quant_config, "weight_block_size", None) != [128, 128]:
252
+ reasons.append("shared experts are not block-128 FP8")
253
+ if not getattr(quant_config, "is_checkpoint_fp8_serialized", False):
254
+ reasons.append("shared experts are not serialized as FP8")
255
+ if not getattr(quant_config, "is_scale_e8m0", False):
256
+ reasons.append("shared-expert scales are not E8M0")
257
+ if getattr(quant_config, "ignored_layers", None):
258
+ reasons.append("the quantization config has ignored layers")
259
+
260
+ offload_config = getattr(vllm_config, "offload_config", None)
261
+ if offload_config is not None and (
262
+ getattr(getattr(offload_config, "uva", None), "cpu_offload_gb", 0) > 0
263
+ or getattr(
264
+ getattr(offload_config, "prefetch", None),
265
+ "offload_group_size",
266
+ 0,
267
+ )
268
+ > 0
269
+ ):
270
+ reasons.append("weight offloading is enabled")
271
+
272
+ if reasons:
273
+ logger.debug_once(
274
+ "DeepSeek V4 heterogeneous shared-expert fusion is unavailable: %s. "
275
+ "Continuing with the existing shared-expert path.",
276
+ "; ".join(reasons),
277
+ )
278
+ return False
279
+
280
+ logger.info_once(
281
+ "Using AITER DeepSeek V4 heterogeneous MoE for CSV-covered MoE input "
282
+ "rows (M) per invocation (native intermediate width 384)."
283
+ )
284
+ return True
285
+
286
+
287
+@torch.no_grad()
288
+def _prepare_native_fp8_shared_expert(
289
+ w13: torch.Tensor,
290
+ w2: torch.Tensor,
291
+ w13_scale: torch.Tensor,
292
+ w2_scale: torch.Tensor,
293
+ intermediate_size: int,
294
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
295
+ """Expand native block-128 E8M0 scales to FHMoE's 1x32 layout."""
296
+ if w13.dtype != torch.float8_e4m3fn or w2.dtype != torch.float8_e4m3fn:
297
+ raise ValueError("Heterogeneous shared-expert weights must be FP8 E4M3.")
298
+ hidden_size = w13.shape[1]
299
+ if w13.shape != (2 * intermediate_size, hidden_size):
300
+ raise ValueError(f"Unexpected shared W13 shape {tuple(w13.shape)}.")
301
+ if w2.shape != (hidden_size, intermediate_size):
302
+ raise ValueError(f"Unexpected shared W2 shape {tuple(w2.shape)}.")
303
+
304
+ def scale_bytes(scale: torch.Tensor) -> torch.Tensor:
305
+ if scale.dtype == torch.float8_e8m0fnu:
306
+ return scale.contiguous().view(torch.uint8)
307
+ if scale.dtype == torch.uint8:
308
+ return scale.contiguous()
309
+ if scale.dtype == torch.float32:
310
+ bits = scale.contiguous().view(torch.int32)
311
+ exponent = (bits >> 23).to(torch.uint8)
312
+ if not torch.equal(bits, exponent.to(torch.int32) << 23):
313
+ raise ValueError("FP32 shared-expert scales are not exact E8M0.")
314
+ return exponent
315
+ raise ValueError(f"Unsupported shared-expert scale dtype {scale.dtype}.")
316
+
317
+ w13_scale = scale_bytes(w13_scale)
318
+ w2_scale = scale_bytes(w2_scale)
319
+ expected_w13_scale = (2 * intermediate_size // 128, hidden_size // 128)
320
+ expected_w2_scale = (hidden_size // 128, intermediate_size // 128)
321
+ if tuple(w13_scale.shape) != expected_w13_scale:
322
+ raise ValueError(
323
+ f"Expected shared W13 scale shape {expected_w13_scale}, got "
324
+ f"{tuple(w13_scale.shape)}."
325
+ )
326
+ if tuple(w2_scale.shape) != expected_w2_scale:
327
+ raise ValueError(
328
+ f"Expected shared W2 scale shape {expected_w2_scale}, got "
329
+ f"{tuple(w2_scale.shape)}."
330
+ )
331
+
332
+ w13_scale = w13_scale.repeat_interleave(128, dim=0).repeat_interleave(4, dim=1)
333
+ w2_scale = w2_scale.repeat_interleave(128, dim=0).repeat_interleave(4, dim=1)
334
+
335
+ scale_cols = ((w2_scale.shape[1] + 7) // 8) * 8
336
+ if scale_cols != w2_scale.shape[1]:
337
+ padded_w2_scale = torch.full(
338
+ (w2_scale.shape[0], scale_cols),
339
+ 0x7F,
340
+ dtype=torch.uint8,
341
+ device=w2_scale.device,
342
+ )
343
+ padded_w2_scale[:, : w2_scale.shape[1]].copy_(w2_scale)
344
+ w2_scale = padded_w2_scale
345
+
346
+ return (
347
+ w13.unsqueeze(0),
348
+ w2.unsqueeze(0),
349
+ w13_scale.view(torch.float8_e8m0fnu),
350
+ w2_scale.view(torch.float8_e8m0fnu),
351
+ )
352
+
353
+
354
+class DeepseekV4HeterogeneousMxfp4MoEMethod(Mxfp4MoEMethod):
355
+ def process_weights_after_loading(self, layer: RoutedExperts) -> None:
356
+ super().process_weights_after_loading(layer)
357
+ assert isinstance(layer, DeepseekV4HeterogeneousSharedRoutedExperts)
358
+ layer.prepare_heterogeneous_shared_expert()
359
+
360
+
361
+class DeepseekV4HeterogeneousSharedRoutedExperts(RoutedExperts):
362
+ def __init__(
363
+ self,
364
+ *args,
365
+ shared_expert: DeepseekV4MLP,
366
+ shared_expert_id: int,
367
+ **kwargs,
368
+ ):
369
+ self._shared_expert_ref = weakref.ref(shared_expert)
370
+ self.shared_expert_id = shared_expert_id
371
+ self._routed_quant_config: FusedMoEQuantConfig | None = None
372
+ super().__init__(*args, **kwargs)
373
+ if self.expert_map_manager.num_fused_shared_experts != 1:
374
+ raise ValueError("Heterogeneous fusion requires one appended expert.")
375
+ if self.shared_expert_id != self.global_num_experts:
376
+ raise ValueError("The shared expert ID must name the appended slot.")
377
+ self.register_buffer("shared_w1", None, persistent=False)
378
+ self.register_buffer("shared_w2", None, persistent=False)
379
+ self.register_buffer("shared_w1_scale", None, persistent=False)
380
+ self.register_buffer("shared_w2_scale", None, persistent=False)
381
+
382
+ def _get_quant_method(self, prefix, quant_config, moe_config):
383
+ quant_method = super()._get_quant_method(prefix, quant_config, moe_config)
384
+ if not isinstance(quant_method, Mxfp4MoEMethod):
385
+ raise ValueError("Heterogeneous fusion requires MXFP4 routed experts.")
386
+ return DeepseekV4HeterogeneousMxfp4MoEMethod(moe_config)
387
+
388
+ @torch.no_grad()
389
+ def prepare_heterogeneous_shared_expert(self) -> None:
390
+ shared_expert = self._shared_expert_ref()
391
+ if shared_expert is None:
392
+ raise RuntimeError("The native shared-expert module was released.")
393
+
394
+ shared = _prepare_native_fp8_shared_expert(
395
+ shared_expert.gate_up_proj.weight,
396
+ shared_expert.down_proj.weight,
397
+ shared_expert.gate_up_proj.weight_scale_inv,
398
+ shared_expert.down_proj.weight_scale_inv,
399
+ self.moe_config.intermediate_size_per_partition,
400
+ )
401
+ self.shared_w1 = rocm_aiter_ops.shuffle_weight_a16w4(shared[0], 16, True)
402
+ self.shared_w2 = rocm_aiter_ops.shuffle_weight_a16w4(shared[1], 16, False)
403
+ self.shared_w1_scale = rocm_aiter_ops.shuffle_scale_a16w4(shared[2], 1, True)
404
+ self.shared_w2_scale = rocm_aiter_ops.shuffle_scale(shared[3])
405
+
406
+ self._ensure_moe_quant_config_init()
407
+ quant_config = self.quant_method.moe_quant_config
408
+ if quant_config is None:
409
+ raise RuntimeError("The routed MXFP4 quantization config is unavailable.")
410
+ if quant_config.w1_scale is None or quant_config.w2_scale is None:
411
+ raise RuntimeError("The routed MXFP4 scales are unavailable.")
412
+
413
+ def routed_scales(scale: torch.Tensor) -> torch.Tensor:
414
+ scale = scale.reshape(self.local_num_experts, -1, scale.shape[-1])[
415
+ : self.shared_expert_id
416
+ ]
417
+ return scale.reshape(-1, scale.shape[-1]).contiguous()
418
+
419
+ self._routed_quant_config = replace(
420
+ quant_config,
421
+ _w1=replace(
422
+ quant_config._w1,
423
+ scale=routed_scales(quant_config.w1_scale),
424
+ ),
425
+ _w2=replace(
426
+ quant_config._w2,
427
+ scale=routed_scales(quant_config.w2_scale),
428
+ ),
429
+ )
430
+
431
+ def forward_modular(
432
+ self,
433
+ x: torch.Tensor,
434
+ topk_weights: torch.Tensor,
435
+ topk_ids: torch.Tensor,
436
+ shared_experts=None,
437
+ shared_experts_input: torch.Tensor | None = None,
438
+ ) -> torch.Tensor:
439
+ if shared_experts is not None or shared_experts_input is not None:
440
+ raise ValueError("The heterogeneous expert owns the shared path.")
441
+ _validate_heterogeneous_routes(
442
+ topk_weights,
443
+ topk_ids,
444
+ self.moe_config.experts_per_token,
445
+ )
446
+ if any(
447
+ tensor is None
448
+ for tensor in (
449
+ self.shared_w1,
450
+ self.shared_w2,
451
+ self.shared_w1_scale,
452
+ self.shared_w2_scale,
453
+ )
454
+ ):
455
+ raise RuntimeError("Heterogeneous shared weights were not prepared.")
456
+
457
+ if _use_heterogeneous_fhmoe(x.shape[0]):
458
+ self._ensure_moe_quant_config_init()
459
+ quant_config = self.quant_method.moe_quant_config
460
+ if quant_config is None:
461
+ raise RuntimeError("The routed MXFP4 config is unavailable.")
462
+ return rocm_aiter_fused_experts(
463
+ hidden_states=x,
464
+ w1=self.w13_weight,
465
+ w2=self.w2_weight,
466
+ topk_weights=topk_weights,
467
+ topk_ids=topk_ids,
468
+ moe_config=self.moe_config,
469
+ activation=self.activation,
470
+ expert_mask=self.expert_map,
471
+ quant_config=quant_config,
472
+ output_dtype=x.dtype,
473
+ moe_sorting_dispatch_policy=(rocm_aiter_ops.get_moe_dispatch_policy()),
474
+ shared_w1=self.shared_w1,
475
+ shared_w2=self.shared_w2,
476
+ shared_w1_scale=self.shared_w1_scale,
477
+ shared_w2_scale=self.shared_w2_scale,
478
+ shared_expert_id=self.shared_expert_id,
479
+ )
480
+
481
+ shared_expert = self._shared_expert_ref()
482
+ if shared_expert is None or self._routed_quant_config is None:
483
+ raise RuntimeError("The separate shared-expert fallback is unavailable.")
484
+ shared_out = shared_expert(x)
485
+ routed_w1 = self.w13_weight[: self.shared_expert_id]
486
+ routed_w2 = self.w2_weight[: self.shared_expert_id]
487
+ routed_w1.is_shuffled = True
488
+ routed_w2.is_shuffled = True
489
+ routed = rocm_aiter_fused_experts(
490
+ hidden_states=x,
491
+ w1=routed_w1,
492
+ w2=routed_w2,
493
+ topk_weights=topk_weights[:, :-1].contiguous(),
494
+ topk_ids=topk_ids[:, :-1].contiguous(),
495
+ moe_config=self.moe_config,
496
+ activation=self.activation,
497
+ expert_mask=self.expert_map,
498
+ quant_config=self._routed_quant_config,
499
+ output_dtype=x.dtype,
500
+ moe_sorting_dispatch_policy=rocm_aiter_ops.get_moe_dispatch_policy(),
501
+ )
502
+ return routed + shared_out
503
+
504
+
505
def _fuse_shared_experts_enabled(config) -> bool:
506
return bool(
507
getattr(config, "n_shared_experts", None)
@@ -176,6 +515,7 @@ class DeepseekV4MoE(nn.Module):
515
self,
516
vllm_config: VllmConfig,
517
prefix: str = "",
518
+ fuse_heterogeneous_shared_expert: bool = False,
519
):
520
super().__init__()
521
@@ -230,7 +570,12 @@ class DeepseekV4MoE(nn.Module):
570
# TODO: Historically, only `VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=1`
571
# is checked to enable FSE for DeepSeek-v4, despite AITER not being used.
572
# This should be cleaned up and use `resolve_layer_fused_shared_expert`.
233
- fse_requested = _fuse_shared_experts_enabled(config)
573
+ self.fuse_heterogeneous_shared_expert = fuse_heterogeneous_shared_expert
574
+ fse_requested = (
575
+ _fuse_shared_experts_enabled(config)
576
+ and not self.fuse_heterogeneous_shared_expert
577
+ )
578
+ fse_compatible = False
579
if fse_requested:
580
fse_compatible, fse_reason = is_shared_expert_quant_fse_compatible(
581
quant_config,
@@ -267,12 +612,15 @@ class DeepseekV4MoE(nn.Module):
612
self.experts_start_idx = self.tp_rank * self.n_local_experts
613
self.experts_end_idx = self.experts_start_idx + self.n_local_experts
614
615
+ fuse_shared_into_routed = (
616
+ self.is_fused_shared_expert_enabled or self.fuse_heterogeneous_shared_expert
617
+ )
618
self.experts = FusedMoEFactory(
271
- shared_experts=self.shared_experts,
619
+ shared_experts=(None if fuse_shared_into_routed else self.shared_experts),
620
n_shared_experts=(
273
- config.n_shared_experts if self.is_fused_shared_expert_enabled else None
621
+ config.n_shared_experts if fuse_shared_into_routed else None
622
),
275
- fuse_shared_experts=self.is_fused_shared_expert_enabled,
623
+ fuse_shared_experts=fuse_shared_into_routed,
624
gate=self.gate,
625
num_experts=config.n_routed_experts,
626
top_k=config.num_experts_per_tok,
@@ -287,6 +635,19 @@ class DeepseekV4MoE(nn.Module):
635
hash_indices_table=self.gate.tid2eid,
636
swiglu_limit=self.swiglu_limit,
637
router_logits_dtype=torch.float32,
638
+ routed_experts_cls=(
639
+ DeepseekV4HeterogeneousSharedRoutedExperts
640
+ if self.fuse_heterogeneous_shared_expert
641
+ else None
642
+ ),
643
+ routed_experts_args=(
644
+ {
645
+ "shared_expert": self.shared_experts,
646
+ "shared_expert_id": config.n_routed_experts,
647
+ }
648
+ if self.fuse_heterogeneous_shared_expert
649
+ else None
650
+ ),
651
)
652
653
def forward(
@@ -316,6 +677,7 @@ class DeepseekV4DecoderLayer(nn.Module):
677
prefix,
678
topk_indices_buffer: torch.Tensor | None = None,
679
aux_stream_list: list[torch.cuda.Stream] | None = None,
680
+ fuse_heterogeneous_shared_expert: bool = False,
681
):
682
super().__init__()
683
@@ -333,7 +695,11 @@ class DeepseekV4DecoderLayer(nn.Module):
695
topk_indices_buffer=topk_indices_buffer,
696
aux_stream_list=aux_stream_list,
697
)
336
- self.ffn = DeepseekV4MoE(vllm_config, prefix=f"{prefix}.ffn")
698
+ self.ffn = DeepseekV4MoE(
699
+ vllm_config,
700
+ prefix=f"{prefix}.ffn",
701
+ fuse_heterogeneous_shared_expert=fuse_heterogeneous_shared_expert,
702
+ )
703
704
self.attn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
705
self.ffn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
@@ -598,6 +964,9 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
964
else:
965
self.embed_tokens = PPMissingLayer()
966
967
+ self.fuse_heterogeneous_shared_expert = _heterogeneous_shared_expert_enabled(
968
+ vllm_config
969
+ )
970
self.start_layer, self.end_layer, self.layers = make_layers(
971
config.num_hidden_layers,
972
lambda prefix: DeepseekV4DecoderLayer(
@@ -605,6 +974,9 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
974
prefix=prefix,
975
topk_indices_buffer=self.topk_indices_buffer,
976
aux_stream_list=aux_stream_list,
977
+ fuse_heterogeneous_shared_expert=(
978
+ self.fuse_heterogeneous_shared_expert
979
+ ),
980
),
981
prefix=f"{prefix}.layers",
982
)