[MXFP8][ROCm] Fix MXFP8 MoE backend selection (#49747)

Signed-off-by: Felix Marty <Felix.Marty@amd.com>

fxmarty-amd committed Jul 29, 2026 at 01:57 UTC 5369f7b7b89e48d396fa98a9d33f3fba654af8ee
6 files changed +244 -61
tests/kernels/moe/test_mxfp8_aiter_backend_selection.py
+37 -7
@@ -19,9 +19,17 @@ if not current_platform.is_rocm():
19 pytest.skip("This test can only run on ROCm.", allow_module_level=True)
20
21 from tests.kernels.moe.utils import make_dummy_moe_config # noqa: E402
22 +from vllm.model_executor.layers.fused_moe.activation import ( # noqa: E402
23 + MoEActivation,
24 +)
25 from vllm.model_executor.layers.fused_moe.experts.aiter_mxfp8_moe import ( # noqa: E402
26 + _AITER_SWIGLU_ALPHA,
27 + _AITER_SWIGLU_BETA,
28 AiterMxfp8Experts,
29 )
30 +from vllm.model_executor.layers.fused_moe.experts.mxfp8_native_moe import ( # noqa: E402
31 + Mxfp8NativeTritonExperts,
32 +)
33 from vllm.model_executor.layers.fused_moe.modular_kernel import ( # noqa: E402
34 FusedMoEActivationFormat,
35 )
@@ -33,6 +41,7 @@ from vllm.model_executor.layers.fused_moe.oracle.mxfp8 import ( # noqa: E402
41 _SUPPORTED_BACKENDS,
42 _mxfp8_backend_to_kernel_cls,
43 _select_kernel_cls,
44 + select_mxfp8_moe_backend,
45 )
46 from vllm.model_executor.layers.quantization.utils.quant_utils import ( # noqa: E402
47 kMxfp8Dynamic,
@@ -43,7 +52,17 @@ _AITER_MOD = "vllm.model_executor.layers.fused_moe.experts.aiter_mxfp8_moe"
52
53
54 def _config(ep_size: int = 1):
46 - cfg = make_dummy_moe_config(num_experts=128, experts_per_token=4, hidden_dim=6144)
55 + # AiterMxfp8Experts hardcodes SwiGLU-OAI: match its required activation and
56 + # alpha/beta so is_supported_config doesn't reject the config on those grounds.
57 + cfg = make_dummy_moe_config(
58 + num_experts=128,
59 + experts_per_token=4,
60 + hidden_dim=6144,
61 + activation=MoEActivation.SWIGLUOAI_UNINTERLEAVE,
62 + )
63 + cfg = dataclasses.replace(
64 + cfg, swiglu_alpha=_AITER_SWIGLU_ALPHA, swiglu_beta=_AITER_SWIGLU_BETA
65 + )
66 if ep_size != 1:
67 cfg = dataclasses.replace(
68 cfg,
@@ -76,12 +95,6 @@ def test_aiter_mxfp8_registered():
95 ]
96
97
79 -def test_triton_selectable():
80 - assert _BACKEND_NAME_MAP["triton"] is Fp8MoeBackend.TRITON_MXFP8
81 - # Not auto-selected (only reachable explicitly), so FlyDSL still wins auto.
82 - assert Fp8MoeBackend.TRITON_MXFP8 not in _SUPPORTED_BACKENDS
83 -
84 -
98 @pytest.mark.parametrize("ep_size", [1, 2])
99 def test_ep_supported(ep_size):
100 """FlyDSL accepts both TP and EP: apply() forwards expert_map as expert_mask."""
@@ -133,3 +146,20 @@ def test_explicit_moe_backend_aiter():
146 pytest.raises(ValueError, match="flydsl package"),
147 ):
148 _select_kernel_cls(Fp8MoeBackend.AITER_MXFP8, _config(1))
149 +
150 +
151 +def test_gfx950_picks_aiter():
152 + """Auto-select on real ROCm hardware with flydsl usable -> FlyDSL wins."""
153 + with _flydsl_installed(True):
154 + backend, experts_cls = select_mxfp8_moe_backend(_config())
155 + assert backend is Fp8MoeBackend.AITER_MXFP8
156 + assert experts_cls is AiterMxfp8Experts
157 +
158 +
159 +def test_gfx942_picks_triton():
160 + """flydsl unusable (e.g. gfx942, no FlyDSL support) -> native Triton
161 + dot_scaled backend wins instead."""
162 + with _flydsl_installed(False):
163 + backend, experts_cls = select_mxfp8_moe_backend(_config())
164 + assert backend is Fp8MoeBackend.TRITON_MXFP8
165 + assert experts_cls is Mxfp8NativeTritonExperts
tests/models/quantization/test_mxfp8.py
+166
@@ -17,8 +17,10 @@ diverse prompts from ``tests/prompts/example.txt``.
17 """
18
19 import pytest
20 +import torch
21
22 from tests.quantization.utils import is_quant_method_supported
23 +from vllm.platforms import current_platform
24
25 from ..utils import check_logprobs_close
26
@@ -81,6 +83,170 @@ def test_mxfp8_logprobs(
83 )
84
85
86 +@pytest.mark.skipif(
87 + not is_quant_method_supported("mxfp8"),
88 + reason="mxfp8 is not supported on this GPU type (requires sm_100+).",
89 +)
90 +@pytest.mark.skipif(
91 + not current_platform.is_rocm(),
92 + reason="AITER MXFP8 MoE backend is ROCm-only.",
93 +)
94 +@pytest.mark.quant_model
95 +def test_mxfp8_aiter_requires_swigluoai_activation(
96 + monkeypatch: pytest.MonkeyPatch,
97 +) -> None:
98 + from vllm.model_executor.layers.fused_moe.activation import MoEActivation
99 + from vllm.model_executor.layers.fused_moe.config import (
100 + FusedMoEConfig,
101 + FusedMoEParallelConfig,
102 + RoutingMethodType,
103 + )
104 + from vllm.model_executor.layers.fused_moe.experts import aiter_mxfp8_moe
105 + from vllm.model_executor.layers.fused_moe.oracle.mxfp8 import (
106 + select_mxfp8_moe_backend,
107 + )
108 +
109 + monkeypatch.setattr(
110 + aiter_mxfp8_moe.AiterMxfp8Experts,
111 + "_supports_current_device",
112 + staticmethod(lambda: True),
113 + )
114 + monkeypatch.setattr(
115 + aiter_mxfp8_moe,
116 + "is_aiter_mxfp8_moe_available",
117 + lambda: True,
118 + )
119 +
120 + config = FusedMoEConfig(
121 + num_experts=8,
122 + experts_per_token=2,
123 + hidden_dim=256,
124 + intermediate_size=256,
125 + num_local_experts=8,
126 + num_logical_experts=8,
127 + moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
128 + activation=MoEActivation.SILU,
129 + in_dtype=torch.bfloat16,
130 + device="cuda",
131 + routing_method=RoutingMethodType.Renormalize,
132 + moe_backend="aiter",
133 + )
134 +
135 + with pytest.raises(ValueError, match="requires activation=swigluoai_uninterleave"):
136 + select_mxfp8_moe_backend(config)
137 +
138 +
139 +@pytest.mark.skipif(
140 + not is_quant_method_supported("mxfp8"),
141 + reason="mxfp8 is not supported on this GPU type (requires sm_100+).",
142 +)
143 +@pytest.mark.skipif(
144 + not current_platform.is_rocm(),
145 + reason="AITER MXFP8 MoE backend is ROCm-only.",
146 +)
147 +@pytest.mark.quant_model
148 +def test_mxfp8_aiter_requires_swigluoai_params(
149 + monkeypatch: pytest.MonkeyPatch,
150 +) -> None:
151 + from vllm.model_executor.layers.fused_moe.activation import MoEActivation
152 + from vllm.model_executor.layers.fused_moe.config import (
153 + FusedMoEConfig,
154 + FusedMoEParallelConfig,
155 + RoutingMethodType,
156 + )
157 + from vllm.model_executor.layers.fused_moe.experts import aiter_mxfp8_moe
158 + from vllm.model_executor.layers.fused_moe.oracle.mxfp8 import (
159 + select_mxfp8_moe_backend,
160 + )
161 +
162 + monkeypatch.setattr(
163 + aiter_mxfp8_moe.AiterMxfp8Experts,
164 + "_supports_current_device",
165 + staticmethod(lambda: True),
166 + )
167 + monkeypatch.setattr(
168 + aiter_mxfp8_moe,
169 + "is_aiter_mxfp8_moe_available",
170 + lambda: True,
171 + )
172 +
173 + config = FusedMoEConfig(
174 + num_experts=8,
175 + experts_per_token=2,
176 + hidden_dim=256,
177 + intermediate_size=256,
178 + num_local_experts=8,
179 + num_logical_experts=8,
180 + moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
181 + activation=MoEActivation.SWIGLUOAI_UNINTERLEAVE,
182 + in_dtype=torch.bfloat16,
183 + device="cuda",
184 + routing_method=RoutingMethodType.Renormalize,
185 + moe_backend="aiter",
186 + )
187 +
188 + with pytest.raises(ValueError, match="hardcodes SwiGLU-OAI"):
189 + select_mxfp8_moe_backend(config)
190 +
191 +
192 +@pytest.mark.skipif(
193 + not is_quant_method_supported("mxfp8"),
194 + reason="mxfp8 is not supported on this GPU type (requires sm_100+).",
195 +)
196 +@pytest.mark.skipif(
197 + not current_platform.is_rocm(),
198 + reason="AITER MXFP8 MoE backend is ROCm-only.",
199 +)
200 +@pytest.mark.quant_model
201 +def test_mxfp8_aiter_accepts_swigluoai_params(
202 + monkeypatch: pytest.MonkeyPatch,
203 +) -> None:
204 + from vllm.model_executor.layers.fused_moe.activation import MoEActivation
205 + from vllm.model_executor.layers.fused_moe.config import (
206 + FusedMoEConfig,
207 + FusedMoEParallelConfig,
208 + RoutingMethodType,
209 + )
210 + from vllm.model_executor.layers.fused_moe.experts import aiter_mxfp8_moe
211 + from vllm.model_executor.layers.fused_moe.oracle.fp8 import Fp8MoeBackend
212 + from vllm.model_executor.layers.fused_moe.oracle.mxfp8 import (
213 + select_mxfp8_moe_backend,
214 + )
215 +
216 + monkeypatch.setattr(
217 + aiter_mxfp8_moe.AiterMxfp8Experts,
218 + "_supports_current_device",
219 + staticmethod(lambda: True),
220 + )
221 + monkeypatch.setattr(
222 + aiter_mxfp8_moe,
223 + "is_aiter_mxfp8_moe_available",
224 + lambda: True,
225 + )
226 +
227 + config = FusedMoEConfig(
228 + num_experts=8,
229 + experts_per_token=2,
230 + hidden_dim=256,
231 + intermediate_size=256,
232 + num_local_experts=8,
233 + num_logical_experts=8,
234 + moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
235 + activation=MoEActivation.SWIGLUOAI_UNINTERLEAVE,
236 + in_dtype=torch.bfloat16,
237 + device="cuda",
238 + routing_method=RoutingMethodType.Renormalize,
239 + moe_backend="aiter",
240 + swiglu_alpha=aiter_mxfp8_moe._AITER_SWIGLU_ALPHA,
241 + swiglu_beta=aiter_mxfp8_moe._AITER_SWIGLU_BETA,
242 + )
243 +
244 + backend, experts_cls = select_mxfp8_moe_backend(config)
245 +
246 + assert backend == Fp8MoeBackend.AITER_MXFP8
247 + assert experts_cls is aiter_mxfp8_moe.AiterMxfp8Experts
248 +
249 +
250 @pytest.mark.skipif(
251 not is_quant_method_supported("mxfp8"),
252 reason="mxfp8 is not supported on this GPU type (requires sm_100+).",
vllm/model_executor/layers/fused_moe/experts/aiter_mxfp8_moe.py
+27
@@ -6,10 +6,13 @@
6 ``convert_to_fp8_moe_kernel_format``.
7 """
8
9 +import math
10 +
11 import torch
12
13 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
14 from vllm.logger import init_logger
15 +from vllm.model_executor.layers.fused_moe.activation import MoEActivation
16 from vllm.model_executor.layers.fused_moe.experts.mxfp8_emulation_moe import (
17 Mxfp8TritonExpertsBase,
18 )
@@ -17,6 +20,9 @@ from vllm.platforms import current_platform
20
21 logger = init_logger(__name__)
22
23 +_AITER_SWIGLU_ALPHA = 1.702
24 +_AITER_SWIGLU_BETA = 1.0
25 +
26
27 def is_aiter_mxfp8_moe_available() -> bool:
28 """True when the FlyDSL MXFP8 MoE can run here: gfx950, the ``flydsl``
@@ -93,6 +99,27 @@ class AiterMxfp8Experts(Mxfp8TritonExpertsBase):
99 return False, (
100 "kernel requires the aiter flydsl package, which is not installed"
101 )
102 + if (
103 + is_supported
104 + and moe_config.activation != MoEActivation.SWIGLUOAI_UNINTERLEAVE
105 + ):
106 + return False, (
107 + "kernel hardcodes SwiGLU-OAI activation and requires "
108 + f"activation={MoEActivation.SWIGLUOAI_UNINTERLEAVE.value}; "
109 + f"got activation={moe_config.activation.value}"
110 + )
111 + if is_supported and (
112 + moe_config.swiglu_alpha is None
113 + or not math.isclose(float(moe_config.swiglu_alpha), _AITER_SWIGLU_ALPHA)
114 + or moe_config.swiglu_beta is None
115 + or not math.isclose(float(moe_config.swiglu_beta), _AITER_SWIGLU_BETA)
116 + ):
117 + return False, (
118 + "kernel hardcodes SwiGLU-OAI with "
119 + f"alpha={_AITER_SWIGLU_ALPHA} and beta={_AITER_SWIGLU_BETA}; "
120 + f"got swiglu_alpha={moe_config.swiglu_alpha} and "
121 + f"swiglu_beta={moe_config.swiglu_beta}"
122 + )
123 return is_supported, reason
124
125 def apply(
vllm/model_executor/layers/fused_moe/experts/mxfp8_emulation_moe.py
+2 -6
@@ -107,17 +107,13 @@ class Mxfp8EmulationTritonExperts(Mxfp8TritonExpertsBase):
107 limit = self.quant_config.gemm1_clamp_limit
108 if limit is None:
109 raise ValueError("SWIGLUOAI_UNINTERLEAVE requires gemm1_clamp_limit")
110 - alpha = self.quant_config.gemm1_alpha
111 - alpha = 1.702 if alpha is None else float(alpha)
112 - beta = self.quant_config.gemm1_beta
113 - beta = 1.0 if beta is None else float(beta)
110 apply_moe_activation(
111 activation,
112 output,
113 input,
114 clamp_limit=float(limit),
119 - alpha=alpha,
120 - beta=beta,
115 + alpha=self.gemm1_alpha,
116 + beta=self.gemm1_beta,
117 )
118 return
119 super().activation(activation, output, input)
vllm/model_executor/layers/fused_moe/experts/mxfp8_native_moe.py
+3 -6
@@ -362,10 +362,7 @@ class Mxfp8NativeTritonExperts(Mxfp8TritonExpertsBase):
362 expert_tokens_meta: mk.ExpertTokensMetadata | None,
363 apply_router_weight_on_input: bool,
364 ):
365 - alpha = self.quant_config.gemm1_alpha
366 - alpha = 1.702 if alpha is None else float(alpha)
367 - beta = self.quant_config.gemm1_beta
368 - beta = 1.0 if beta is None else float(beta)
365 + # `self.gemm1_alpha` and `self.gemm1_beta`` are set by `TritonExperts.__init__`.
366 limit = self.quant_config.gemm1_clamp_limit
367 limit = None if limit is None else float(limit)
368 out = fused_moe_mxfp8_native(
@@ -376,8 +373,8 @@ class Mxfp8NativeTritonExperts(Mxfp8TritonExpertsBase):
373 self.w2_scale_val,
374 topk_weights,
375 topk_ids,
379 - alpha=alpha,
380 - beta=beta,
376 + alpha=self.gemm1_alpha,
377 + beta=self.gemm1_beta,
378 limit=limit,
379 global_num_experts=global_num_experts,
380 expert_map=expert_map,
vllm/model_executor/layers/fused_moe/oracle/mxfp8.py
+9 -42
@@ -12,10 +12,10 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
12 kMxfp8Dynamic,
13 kMxfp8Static,
14 )
15 -from vllm.platforms import current_platform
15
16 logger = init_logger(__name__)
17
18 +# Ordered by priority.
19 _SUPPORTED_BACKENDS = (
20 Fp8MoeBackend.FLASHINFER_TRTLLM,
21 Fp8MoeBackend.DEEPGEMM,
@@ -26,6 +26,8 @@ _SUPPORTED_BACKENDS = (
26 # devices / no flydsl / EP it is skipped and native is used.
27 Fp8MoeBackend.AITER_MXFP8,
28 Fp8MoeBackend.HUMMING,
29 + Fp8MoeBackend.TRITON_MXFP8,
30 + Fp8MoeBackend.EMULATION,
31 )
32
33 _BACKEND_NAME_MAP: dict[str, Fp8MoeBackend] = {
@@ -61,15 +63,12 @@ def _mxfp8_backend_to_kernel_cls(
63
64 return [AiterMxfp8Experts]
65 if backend == Fp8MoeBackend.TRITON_MXFP8:
64 - # Explicit ``--moe-backend triton``: the Triton mxfp8 path, i.e.
65 - # dot_scaled on MX-capable HW (gfx950) and BF16 emulation otherwise.
66 - # Mirrors the ROCm auto-fallback in ``_select_rocm_mxfp8_backend``.
67 - if current_platform.supports_mx():
68 - from vllm.model_executor.layers.fused_moe.experts.mxfp8_native_moe import (
69 - Mxfp8NativeTritonExperts,
70 - )
66 + from vllm.model_executor.layers.fused_moe.experts.mxfp8_native_moe import (
67 + Mxfp8NativeTritonExperts,
68 + )
69
72 - return [Mxfp8NativeTritonExperts]
70 + return [Mxfp8NativeTritonExperts]
71 + if backend == Fp8MoeBackend.EMULATION:
72 from vllm.model_executor.layers.fused_moe.experts.mxfp8_emulation_moe import (
73 Mxfp8EmulationTritonExperts,
74 )
@@ -105,35 +104,6 @@ def _select_kernel_cls(
104 )
105
106
108 -def _select_rocm_mxfp8_backend() -> tuple[Fp8MoeBackend, type[mk.FusedMoEExperts]]:
109 - """ROCm fallback when no auto-selected MXFP8 backend is available.
110 -
111 - The aiter FlyDSL backend (``AITER_MXFP8``) is auto-picked earlier by
112 - ``select_mxfp8_moe_backend`` via ``_SUPPORTED_BACKENDS`` when usable, or
113 - explicitly via ``--moe-backend aiter``; this fallback handles the rest
114 - (native dot_scaled on gfx950, else BF16 emulation).
115 - """
116 -
117 - if current_platform.supports_mx():
118 - from vllm.model_executor.layers.fused_moe.experts.mxfp8_native_moe import (
119 - Mxfp8NativeTritonExperts,
120 - )
121 -
122 - logger.info_once("Using native CDNA4 (gfx950) MXFP8 dot_scaled MoE backend.")
123 - return Fp8MoeBackend.TRITON_MXFP8, Mxfp8NativeTritonExperts
124 -
125 - from vllm.model_executor.layers.fused_moe.experts.mxfp8_emulation_moe import (
126 - Mxfp8EmulationTritonExperts,
127 - )
128 -
129 - logger.info_once(
130 - "No native MXFP8 MoE backend available on this device; "
131 - "MXFP8 weights will be dequantized to BF16 once at load time and the "
132 - "MoE will run in BF16 (no per-step dequant)."
133 - )
134 - return Fp8MoeBackend.EMULATION, Mxfp8EmulationTritonExperts
135 -
136 -
107 def select_mxfp8_moe_backend(
108 config: FusedMoEConfig,
109 ) -> tuple[Fp8MoeBackend, type[mk.FusedMoEExperts]]:
@@ -167,8 +137,5 @@ def select_mxfp8_moe_backend(
137 logger.info_once("Using '%s' MxFp8 MoE backend.", backend.value)
138 return backend, experts_cls
139
170 - # simplify the logic for rocm, refactor later when more backends are supported
171 - if current_platform.is_rocm():
172 - return _select_rocm_mxfp8_backend()
173 -
140 + # TODO: add debug log with reason.
141 raise ValueError("No MXFP8 MoE backends available.")