[ROCm][Perf] Add opt-in a4w4 (FP4 activation) MoE for DeepSeek V4.1 on AITER (#58819)

Signed-off-by: fai <fangzhouai@gmail.com> Signed-off-by: Fangzhou Ai <31551580+Fangzhou-Ai@users.noreply.github.com> Co-authored-by: Cursor Agent (Claude Sonnet 5) <cursoragent@cursor.com>

Fangzhou Ai committed Sep 30, 2026 at 03:07 UTC 77e8b02c8d69f25867227fb9883797996fbfcd7b
8 files changed +204 -11
tests/kernels/moe/test_rocm_aiter_moe.py
+109
@@ -638,6 +638,115 @@ def test_aiter_moe_padding_env_var(
638 assert envs.VLLM_ROCM_MOE_PADDING is moe_padding
639
640
641 +# a4w4 (FP4 activation) opt-in gating test ---------------------------------
642 +
643 +
644 +@pytest.mark.parametrize(
645 + ("env_value", "model_type", "expected"),
646 + [
647 + # Unset/"0" is opt-in-only: a4w4 never turns on by itself, even on
648 + # a validated model type.
649 + (None, "deepseek_v41", False),
650 + ("0", "deepseek_v41", False),
651 + ("1", "deepseek_v41", True),
652 + ("1", "deepseek_v41_text", True),
653 + ],
654 +)
655 +def test_aiter_moe_a4w4_dsv4_is_opt_in(
656 + env_value: str | None,
657 + model_type: str,
658 + expected: bool,
659 + monkeypatch: pytest.MonkeyPatch,
660 +):
661 + """vllm-project/vllm#58819's a4w4 MoE activation path must stay a plain
662 + explicit opt-in (VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1), with no
663 + default-on behavior, but is still only permitted on the model types
664 + this flag has been validated for."""
665 + import types
666 +
667 + from tests.kernels.moe.utils import make_dummy_moe_config
668 + from vllm._aiter_ops import rocm_aiter_ops
669 +
670 + _assert_aiter_supported()
671 +
672 + fake_vllm_config = types.SimpleNamespace(
673 + model_config=types.SimpleNamespace(
674 + hf_config=types.SimpleNamespace(model_type=model_type)
675 + )
676 + )
677 +
678 + with monkeypatch.context() as mp:
679 + mp.setenv("VLLM_ROCM_USE_AITER", "1")
680 + mp.setenv("VLLM_ROCM_USE_AITER_MOE", "1")
681 + if env_value is None:
682 + mp.delenv("VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4", raising=False)
683 + else:
684 + mp.setenv("VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4", env_value)
685 + _reload_envs()
686 + rocm_aiter_ops.refresh_env_variables()
687 +
688 + with mock.patch(
689 + "vllm.config.get_current_vllm_config_or_none",
690 + return_value=fake_vllm_config,
691 + ):
692 + moe_config = make_dummy_moe_config(
693 + num_experts=4,
694 + experts_per_token=2,
695 + hidden_dim=256,
696 + intermediate_size=512,
697 + in_dtype=torch.bfloat16,
698 + )
699 +
700 + assert moe_config.use_mxfp4_w4a4_dsv4 is expected
701 +
702 +
703 +@pytest.mark.parametrize("model_type", ["deepseek_v4", "gpt_oss", None])
704 +def test_aiter_moe_a4w4_dsv4_rejects_unvalidated_model_type(
705 + model_type: str | None,
706 + monkeypatch: pytest.MonkeyPatch,
707 +):
708 + """Forcing a4w4 on a model type this flag hasn't been GSM8K/AgentX
709 + validated for must raise: it also flips the MXFP4 weight-shuffle layout
710 + (ATOM SEPARATED vs INTERLEAVE), which silently produces garbled output
711 + on the wrong model rather than just being a slower path — the same bug
712 + class vllm-project/vllm#58819's own second commit fixed for DeepSeek
713 + V4.1 itself."""
714 + import types
715 +
716 + from tests.kernels.moe.utils import make_dummy_moe_config
717 + from vllm._aiter_ops import rocm_aiter_ops
718 +
719 + _assert_aiter_supported()
720 +
721 + fake_vllm_config = types.SimpleNamespace(
722 + model_config=types.SimpleNamespace(
723 + hf_config=types.SimpleNamespace(model_type=model_type)
724 + )
725 + )
726 +
727 + with monkeypatch.context() as mp:
728 + mp.setenv("VLLM_ROCM_USE_AITER", "1")
729 + mp.setenv("VLLM_ROCM_USE_AITER_MOE", "1")
730 + mp.setenv("VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4", "1")
731 + _reload_envs()
732 + rocm_aiter_ops.refresh_env_variables()
733 +
734 + with (
735 + mock.patch(
736 + "vllm.config.get_current_vllm_config_or_none",
737 + return_value=fake_vllm_config,
738 + ),
739 + pytest.raises(ValueError, match="VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4"),
740 + ):
741 + make_dummy_moe_config(
742 + num_experts=4,
743 + experts_per_token=2,
744 + hidden_dim=256,
745 + intermediate_size=512,
746 + in_dtype=torch.bfloat16,
747 + )
748 +
749 +
750 # Dispatch-policy forwarding test ------------------------------------------
751
752
vllm/_aiter_ops.py
+25
@@ -345,6 +345,7 @@ def _rocm_aiter_fused_moe_impl(
345 shared_w1_scale: torch.Tensor | None = None,
346 shared_w2_scale: torch.Tensor | None = None,
347 shared_expert_id: int = -1,
348 + q_dtype_a: torch.dtype | None = None,
349 ) -> torch.Tensor:
350 has_shared_expert = _validate_rocm_aiter_fused_moe_shared_expert_args(
351 shared_w1,
@@ -378,6 +379,12 @@ def _rocm_aiter_fused_moe_impl(
379 shared_w2_scale=shared_w2_scale,
380 shared_expert_id=shared_expert_id,
381 )
382 + if q_dtype_a is not None:
383 + # DeepSeek V4.1 a4w4 override (use_mxfp4_w4a4_dsv4 in
384 + # rocm_aiter_moe.py). rocm_aiter_ops.fused_moe_supports_quant_dtype_a()
385 + # is checked at config time, so this is only reached on an AITER
386 + # build new enough to accept it.
387 + extra_kwargs["quant_dtype_a"] = q_dtype_a
388
389 return fused_moe(
390 hidden_states,
@@ -435,6 +442,7 @@ def _rocm_aiter_fused_moe_fake(
442 shared_w1_scale: torch.Tensor | None = None,
443 shared_w2_scale: torch.Tensor | None = None,
444 shared_expert_id: int = -1,
445 + q_dtype_a: torch.dtype | None = None,
446 ) -> torch.Tensor:
447 if output_dtype is not None:
448 return torch.empty_like(hidden_states, dtype=output_dtype)
@@ -2485,6 +2493,21 @@ class rocm_aiter_ops:
2493
2494 return "gate_mode" in inspect.signature(fused_moe).parameters
2495
2496 + @classmethod
2497 + @if_aiter_supported
2498 + @functools.cache
2499 + def fused_moe_supports_quant_dtype_a(cls) -> bool:
2500 + """Probe whether the installed aiter.fused_moe accepts `quant_dtype_a`.
2501 +
2502 + Added in https://github.com/ROCm/aiter/pull/5439 (unreleased at
2503 + merge time). Older AITER can't override the activation quant dtype.
2504 + """
2505 + import inspect
2506 +
2507 + from aiter.fused_moe import fused_moe
2508 +
2509 + return "quant_dtype_a" in inspect.signature(fused_moe).parameters
2510 +
2511 @staticmethod
2512 def _probe_dsv4_i384_fhmoe_capability(num_tokens: int) -> bool:
2513 """Probe AITER's CSV-backed DSV4 native-I384 FHMoE contract."""
@@ -3005,6 +3028,7 @@ class rocm_aiter_ops:
3028 shared_w1_scale: torch.Tensor | None = None,
3029 shared_w2_scale: torch.Tensor | None = None,
3030 shared_expert_id: int = -1,
3031 + q_dtype_a: torch.dtype | None = None,
3032 ) -> torch.Tensor:
3033 return torch.ops.vllm.rocm_aiter_fused_moe(
3034 hidden_states,
@@ -3036,6 +3060,7 @@ class rocm_aiter_ops:
3060 shared_w1_scale,
3061 shared_w2_scale,
3062 shared_expert_id,
3063 + q_dtype_a,
3064 )
3065
3066 @staticmethod
vllm/envs.py
+8
@@ -141,6 +141,7 @@ if TYPE_CHECKING:
141 VLLM_ROCM_USE_AITER_LINEAR_HIPBMM: bool = False
142 VLLM_ROCM_USE_AITER_MOE: bool = True
143 VLLM_ROCM_AITER_MOE_DISPATCH_POLICY: int = 0
144 + VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4: bool | None = None
145 VLLM_ROCM_USE_AITER_MOE_SITUV2: Literal["auto", "a4w4", "a8w4", "a16w4"] = "auto"
146 VLLM_ROCM_USE_AITER_RMSNORM: bool = True
147 VLLM_ROCM_USE_AITER_MLA: bool = True
@@ -1291,6 +1292,13 @@ environment_variables: dict[str, Callable[[], Any]] = {
1292 ["auto", "a4w4", "a8w4", "a16w4", "0", "1"],
1293 case_sensitive=False,
1294 ),
1295 + # Opt-in switch for a4w4 (FP4 activation) MoE on DeepSeek V4.1, AITER
1296 + # MXFP4 backend. Default is a8w4 (FP8); set to "1" to enable a4w4
1297 + # ("true" is not accepted -- only "0"/"1"). Raises if set for other
1298 + # models.
1299 + "VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4": lambda: maybe_convert_bool(
1300 + os.getenv("VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4")
1301 + ),
1302 # MoE sorting dispatch policy for AITER fused MoE kernels.
1303 # 0 = auto (default): single-pass for small batches, multi-pass
1304 # for large batches
vllm/model_executor/layers/fused_moe/config.py
+35
@@ -6,6 +6,7 @@ from typing import Union
6
7 import torch
8
9 +import vllm.envs as envs
10 from vllm.config import ParallelConfig, SchedulerConfig
11 from vllm.config.kernel import MoEBackend
12 from vllm.distributed import get_dp_group, get_pcp_group, get_tensor_model_parallel_rank
@@ -1247,6 +1248,12 @@ class FusedMoEParallelConfig:
1248 )
1249
1250
1251 +# Model types validated for VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1. This flag
1252 +# also changes the MXFP4 weight shuffle layout, so using it on other models
1253 +# can produce garbled output.
1254 +_AITER_MOE_A4W4_DSV4_VALIDATED_MODEL_TYPES = ("deepseek_v41", "deepseek_v41_text")
1255 +
1256 +
1257 # Adapted from pplx-kernels tests/all_to_all_utils.py
1258 @dataclass
1259 class FusedMoEConfig:
@@ -1314,6 +1321,11 @@ class FusedMoEConfig:
1321 tp_shard_with_padding: bool = False
1322 rocm_aiter_fmoe_enabled: bool = False
1323 aiter_fmoe_shared_expert_enabled: bool = False
1324 + # Whether to force MXFP4 (a4w4) MoE activations for DeepSeek V4.1 on
1325 + # ROCm/AITER. Opt-in via VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1; rejected
1326 + # for any other model type. Resolved here, not in the forward path,
1327 + # because get_current_vllm_config() isn't set there.
1328 + use_mxfp4_w4a4_dsv4: bool = False
1329
1330 def __post_init__(self):
1331 from vllm._aiter_ops import rocm_aiter_ops
@@ -1345,6 +1357,29 @@ class FusedMoEConfig:
1357 rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
1358 )
1359
1360 + if self.rocm_aiter_fmoe_enabled and envs.VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4:
1361 + from vllm.config import get_current_vllm_config_or_none
1362 +
1363 + vllm_config = get_current_vllm_config_or_none()
1364 + model_type = (
1365 + getattr(vllm_config.model_config.hf_config, "model_type", None)
1366 + if vllm_config is not None
1367 + else None
1368 + )
1369 + if model_type not in _AITER_MOE_A4W4_DSV4_VALIDATED_MODEL_TYPES:
1370 + raise ValueError(
1371 + f"VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1 only supports "
1372 + f"model_type in {_AITER_MOE_A4W4_DSV4_VALIDATED_MODEL_TYPES}, "
1373 + f"got {model_type!r}. Unset this env var for this model."
1374 + )
1375 + if not rocm_aiter_ops.fused_moe_supports_quant_dtype_a():
1376 + raise ValueError(
1377 + "VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1 needs an AITER build "
1378 + "with fused_moe(quant_dtype_a=...) support "
1379 + "(ROCm/aiter#5439+). Upgrade AITER or unset this env var."
1380 + )
1381 + self.use_mxfp4_w4a4_dsv4 = True
1382 +
1383 if self.use_mori_kernels:
1384 assert self.rocm_aiter_fmoe_enabled, (
1385 "Mori needs to be used with aiter fused_moe for now."
vllm/model_executor/layers/fused_moe/experts/rocm_aiter_moe.py
+14 -7
@@ -375,15 +375,13 @@ def rocm_aiter_fused_experts(
375 intermediate_pad // 64 * 64 * (2 if moe_config.tp_size == 1 else 1)
376 )
377
378 - # https://github.com/ROCm/aiter/pull/3123 specialized the AITER stage1 GEMMs
379 - # for interleaved vs separated gate and up weights.
380 - # For gpt-oss i.e. use_mxfp4_w4a16=True, the weights are shuffled by
381 - # `rocm_aiter_ops.shuffle_weight_a16w4` in `oracle/mxfp4.py`,
382 - # which always sets `is_guinterleave=True`.
383 - # Hence, we pass in GateMode.INTERLEAVE to match the weight shuffling.
378 + # AITER's stage1 GEMM needs gate_mode to match how weights were
379 + # shuffled at load time (oracle/mxfp4.py). gpt-oss uses INTERLEAVE;
380 + # DeepSeek V4.1 a4w4 uses SEPARATED (see below).
381 from aiter.ops.flydsl.moe_common import GateMode
382
383 gate_mode = ""
384 + q_dtype_a = None
385 if activation == MoEActivation.SITU:
386 gate_mode = (
387 GateMode.INTERLEAVE.value
@@ -391,7 +389,15 @@ def rocm_aiter_fused_experts(
389 else GateMode.SEPARATED.value
390 )
391 elif quant_config.use_mxfp4_w4a16:
394 - gate_mode = GateMode.INTERLEAVE.value
392 + if moe_config.use_mxfp4_w4a4_dsv4:
393 + # Opt-in a4w4 for DeepSeek V4.1 (VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1).
394 + # Must match the SEPARATED weight shuffle from load time.
395 + from aiter import dtypes
396 +
397 + gate_mode = GateMode.SEPARATED.value
398 + q_dtype_a = dtypes.fp4x2
399 + else:
400 + gate_mode = GateMode.INTERLEAVE.value
401 elif activation_interleave is not None:
402 gate_mode = (
403 GateMode.INTERLEAVE.value
@@ -433,6 +439,7 @@ def rocm_aiter_fused_experts(
439 shared_w1_scale=shared_w1_scale,
440 shared_w2_scale=shared_w2_scale,
441 shared_expert_id=shared_expert_id,
442 + q_dtype_a=q_dtype_a,
443 )
444
445
vllm/model_executor/layers/fused_moe/oracle/mxfp4.py
+11 -4
@@ -1382,6 +1382,7 @@ def convert_weight_to_mxfp4_moe_kernel_format(
1382 w2_bias: torch.Tensor | None = None,
1383 _cache_permute_indices: dict[torch.Size, torch.Tensor] | None = None,
1384 activation: MoEActivation | None = None,
1385 + use_separated_a4w4: bool = False,
1386 ) -> tuple[
1387 torch.Tensor,
1388 torch.Tensor,
@@ -1626,10 +1627,16 @@ def convert_weight_to_mxfp4_moe_kernel_format(
1627 from aiter.ops.shuffle import shuffle_scale as _shuf_s
1628 from aiter.ops.shuffle import shuffle_weight as _shuf_w
1629
1630 + # DeepSeek V4.1 a4w4 uses ATOM's SEPARATED gate/up layout instead of
1631 + # the default INTERLEAVE shuffle (INTERLEAVE + fp4x2 has no tuned
1632 + # kernel and produces garbage output). Must match GateMode.SEPARATED
1633 + # in rocm_aiter_moe.py.
1634 + is_guinterleave = not use_separated_a4w4
1635 +
1636 w13_weight = torch.nn.Parameter(
1637 _shuf_w(
1638 w13_weight.data.view(torch.float4_e2m1fn_x2),
1632 - is_guinterleave=True,
1639 + is_guinterleave=is_guinterleave,
1640 gate_up=True,
1641 ),
1642 requires_grad=False,
@@ -1637,14 +1644,14 @@ def convert_weight_to_mxfp4_moe_kernel_format(
1644 shuffled_w13_scale = _shuf_s(
1645 w13_weight_scale.reshape(-1, w13_weight_scale.shape[-1]),
1646 num_experts,
1640 - True,
1647 + is_guinterleave,
1648 True,
1649 )
1650
1651 w2_weight = torch.nn.Parameter(
1652 _shuf_w(
1653 w2_weight.data.view(torch.float4_e2m1fn_x2),
1647 - is_guinterleave=True,
1654 + is_guinterleave=is_guinterleave,
1655 gate_up=False,
1656 ),
1657 requires_grad=False,
@@ -1653,7 +1660,7 @@ def convert_weight_to_mxfp4_moe_kernel_format(
1660 shuffled_w2_scale = _shuf_s(
1661 w2_weight_scale.reshape(-1, w2_weight_scale.shape[-1]),
1662 num_experts,
1656 - True,
1663 + is_guinterleave,
1664 False,
1665 )
1666
vllm/model_executor/layers/quantization/mxfp4.py
+1
@@ -736,6 +736,7 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
736 w2_bias=w2_bias,
737 _cache_permute_indices=self._cache_permute_indices,
738 activation=self.moe.activation,
739 + use_separated_a4w4=self.moe.use_mxfp4_w4a4_dsv4,
740 )
741 )
742
vllm/model_executor/layers/quantization/online/mxfp4.py
+1
@@ -220,6 +220,7 @@ class Mxfp4OnlineMoEMethod(OnlineMoEMethodBase):
220 w2_weight_scale=w2_scale,
221 w13_bias=w13_bias,
222 w2_bias=w2_bias,
223 + use_separated_a4w4=self.moe.use_mxfp4_w4a4_dsv4,
224 )
225 )
226