[ROCm][CI] Enable AITER FP8/unquantized cases in modular-kernel sweep + fix MoRI per-tensor FP8 dispatch (#57074)
Signed-off-by: Divakar Verma <divakar.verma@amd.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com>
Divakar Verma committed
Sep 16, 2026 at 20:53 UTC
95f4925c3a03df8cfcaa21633ccc9dd5b426c7a4
6 files changed
+107
-69
.buildkite/test-amd.yaml
+2
-1
@@ -1704,8 +1704,9 @@ steps:
1704
- vllm/_aiter_ops.py
1705
- vllm/platforms/rocm.py
1706
commands:
1707
- - pytest -v -s kernels/moe --ignore=kernels/moe/test_modular_oai_triton_moe.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT
1707
+ - pytest -v -s kernels/moe --ignore=kernels/moe/test_modular_oai_triton_moe.py --ignore=kernels/moe/test_modular_kernel_combinations.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT
1708
- pytest -v -s kernels/moe/test_modular_oai_triton_moe.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT
1709
+ - VLLM_ROCM_USE_AITER=1 VLLM_ROCM_USE_AITER_MOE=1 pytest -v -s kernels/moe/test_modular_kernel_combinations.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT
1710
1711
- label: ":amd: (MI300) Quantization Kernels" # TBD
1712
timeout_in_minutes: 55
tests/kernels/moe/modular_kernel_tools/common.py
+32
-24
@@ -15,6 +15,7 @@ from tests.kernels.quantization.nvfp4_utils import (
15
dequantize_nvfp4_to_dtype,
16
)
17
from tests.kernels.utils import torch_experts
18
+from vllm._aiter_ops import rocm_aiter_ops
19
from vllm.config import VllmConfig
20
from vllm.distributed import (
21
get_dp_group,
@@ -33,7 +34,9 @@ from vllm.model_executor.layers.fused_moe.config import (
34
FusedMoEQuantConfig,
35
RoutingMethodType,
36
)
37
+from vllm.model_executor.layers.quantization.utils.fp8_utils import is_fp8
38
from vllm.model_executor.layers.quantization.utils.quant_utils import (
39
+ QuantKey,
40
kFp8Dynamic128Sym,
41
kFp8DynamicTensorSym,
42
kFp8DynamicTokenSym,
@@ -165,31 +168,34 @@ class Config:
168
169
return vllm_config, env_dict
170
171
+ def fp8_quant_key_pair(self) -> tuple[QuantKey, QuantKey]:
172
+ """Derive the (weight_quant_key, activation_quant_key) pair an FP8
173
+ quant config of this shape corresponds to (either OCP or FNUZ FP8,
174
+ see ``current_platform.fp8_dtype()``)."""
175
+ if self.quant_block_shape is not None:
176
+ return kFp8Static128BlockSym, kFp8Dynamic128Sym
177
+ if self.is_per_out_ch_quant:
178
+ return (
179
+ kFp8StaticChannelSym,
180
+ kFp8DynamicTokenSym
181
+ if self.is_per_act_token_quant
182
+ else kFp8StaticTensorSym,
183
+ )
184
+ return (
185
+ kFp8StaticTensorSym,
186
+ kFp8DynamicTensorSym
187
+ if self.is_per_act_token_quant
188
+ else kFp8StaticTensorSym,
189
+ )
190
+
191
def fe_supports_quant_scheme(self) -> bool:
192
"""Check if the fused experts class supports this quant config.
193
See https://github.com/ROCm/aiter/issues/2419 for AITER gaps."""
194
if self.quant_config is None or self.quant_dtype is None:
195
return True
173
- if self.quant_dtype != torch.float8_e4m3fn:
196
+ if not is_fp8(self.quant_dtype):
197
return True
175
- # Derive QuantKeys from test config
176
- if self.quant_block_shape is not None:
177
- w_key = kFp8Static128BlockSym
178
- a_key = kFp8Dynamic128Sym
179
- elif self.is_per_out_ch_quant:
180
- w_key = kFp8StaticChannelSym
181
- a_key = (
182
- kFp8DynamicTokenSym
183
- if self.is_per_act_token_quant
184
- else kFp8StaticTensorSym
185
- )
186
- else:
187
- w_key = kFp8StaticTensorSym
188
- a_key = (
189
- kFp8DynamicTensorSym
190
- if self.is_per_act_token_quant
191
- else kFp8StaticTensorSym
192
- )
198
+ w_key, a_key = self.fp8_quant_key_pair()
199
fe_cls = self.fused_experts_type
200
if hasattr(fe_cls, "_supports_quant_scheme"):
201
try:
@@ -199,10 +205,7 @@ class Config:
205
return True
206
207
def is_fp8_block_quantized(self):
202
- return (
203
- self.quant_dtype == torch.float8_e4m3fn
204
- and self.quant_block_shape is not None
205
- )
208
+ return is_fp8(self.quant_dtype) and self.quant_block_shape is not None
209
210
def is_batched_prepare_finalize(self):
211
info = prepare_finalize_info(self.prepare_finalize_type)
@@ -334,6 +337,11 @@ class Config:
337
return False, "Needs Aiter, but Aiter not available."
338
if self.needs_mori() and not has_mori(): # noqa: SIM103
339
return False, "Needs MoRI, but MoRI not available."
340
+ if self.needs_mori() and not rocm_aiter_ops.is_fused_moe_enabled():
341
+ return False, (
342
+ "Mori requires AITER's fused-moe backend to be enabled "
343
+ "(VLLM_ROCM_USE_AITER=1 and VLLM_ROCM_USE_AITER_MOE=1)."
344
+ )
345
346
try:
347
if not self.fused_experts_type._supports_current_device():
@@ -370,7 +378,7 @@ class WeightTensors:
378
def is_quantized(self) -> bool:
379
# or w1_scale is not None?
380
return (
373
- self.w1.dtype == torch.float8_e4m3fn
381
+ is_fp8(self.w1.dtype)
382
or self.w1.dtype == torch.uint8
383
or self.w1.dtype == torch.int8
384
)
tests/kernels/moe/modular_kernel_tools/mk_objects.py
+10
-8
@@ -86,14 +86,14 @@ MK_FUSED_EXPERT_TYPES: list[mk.FusedMoEExpertsModular] = []
86
standard_format = mk.FusedMoEActivationFormat.Standard
87
batched_format = mk.FusedMoEActivationFormat.BatchedExperts
88
common_float_types: list[torch.dtype | str] = [
89
- torch.float8_e4m3fn,
89
+ current_platform.fp8_dtype(),
90
torch.bfloat16,
91
torch.float16,
92
torch.float32,
93
]
94
common_float_and_int_types = common_float_types + [torch.int8]
95
nvfp4_types = ["nvfp4"]
96
-fp8_types = [torch.float8_e4m3fn]
96
+fp8_types = [current_platform.fp8_dtype()]
97
98
99
def register_prepare_and_finalize(
@@ -324,7 +324,9 @@ if has_aiter():
324
register_experts(
325
AiterExperts,
326
standard_format,
327
- fp8_types,
327
+ # AiterExperts also supports the fully-unquantized (None, None)
328
+ # scheme (see SUPPORTED_W_A in rocm_aiter_moe.py), not just fp8.
329
+ common_float_types,
330
blocked_quantization_support=True,
331
needs_aiter=True,
332
)
@@ -397,35 +399,35 @@ MK_QUANT_CONFIGS: list[TestMoEQuantConfig | None] = [
399
None,
400
# per-channel / per-column weights and per-tensor activations
401
TestMoEQuantConfig(
400
- quant_dtype=torch.float8_e4m3fn,
402
+ quant_dtype=current_platform.fp8_dtype(),
403
per_out_ch_quant=True,
404
per_act_token_quant=False,
405
block_shape=None,
406
),
407
# per-channel / per-column weights and per-token activations
408
TestMoEQuantConfig(
407
- quant_dtype=torch.float8_e4m3fn,
409
+ quant_dtype=current_platform.fp8_dtype(),
410
per_out_ch_quant=True,
411
per_act_token_quant=True,
412
block_shape=None,
413
),
414
# per-tensor weights and per-tensor activations
415
TestMoEQuantConfig(
414
- quant_dtype=torch.float8_e4m3fn,
416
+ quant_dtype=current_platform.fp8_dtype(),
417
per_out_ch_quant=False,
418
per_act_token_quant=False,
419
block_shape=None,
420
),
421
# per-tensor weights and per-token activations
422
TestMoEQuantConfig(
421
- quant_dtype=torch.float8_e4m3fn,
423
+ quant_dtype=current_platform.fp8_dtype(),
424
per_out_ch_quant=False,
425
per_act_token_quant=True,
426
block_shape=None,
427
),
428
# block-quantized weights and 128 block per-token activations
429
TestMoEQuantConfig(
428
- quant_dtype=torch.float8_e4m3fn,
430
+ quant_dtype=current_platform.fp8_dtype(),
431
per_out_ch_quant=False,
432
per_act_token_quant=False,
433
block_shape=[128, 128],
tests/kernels/moe/test_modular_kernel_combinations.py
+43
-32
@@ -37,6 +37,7 @@ from .modular_kernel_tools.parallel_utils import (
37
ProcessGroupInfo,
38
parallel_launch_with_config,
39
)
40
+from .utils import check_accuracy
41
42
has_any_multi_gpu_package = (
43
has_deep_ep() or has_deep_gemm() or has_flashinfer_cutlass_fused_moe()
@@ -47,12 +48,6 @@ meets_multi_gpu_requirements = pytest.mark.skipif(
48
reason="Requires deep_ep or deep_gemm or flashinfer packages",
49
)
50
50
-if current_platform.is_fp8_fnuz():
51
- pytest.skip(
52
- "Tests in this file require float8_e4m3fn and platform does not support",
53
- allow_module_level=True,
54
- )
55
-
51
52
def format_result(verbose, msg, ex=None):
53
if ex is not None:
@@ -70,6 +65,28 @@ def format_result(verbose, msg, ex=None):
65
print(".", end="")
66
67
68
+def assert_aiter_quant_scheme_case(config: Config) -> None:
69
+ """Make the AITER (weight_quant_key, activation_quant_key) pair this
70
+ config exercises explicit, instead of AiterExperts being reached only
71
+ indirectly through the general quant-config sweep.
72
+ See https://github.com/vllm-project/vllm/issues/54966."""
73
+ fe_cls = config.fused_experts_type
74
+ if getattr(fe_cls, "__name__", "") != "AiterExperts":
75
+ return
76
+
77
+ if config.quant_config is None:
78
+ w_key, a_key = None, None
79
+ else:
80
+ w_key, a_key = config.fp8_quant_key_pair()
81
+
82
+ assert fe_cls._supports_quant_scheme(w_key, a_key), (
83
+ f"AITER case (weight_key={w_key}, activation_key={a_key}) reached "
84
+ "the modular-kernel harness, but AiterExperts._supports_quant_scheme "
85
+ "does not declare it supported."
86
+ )
87
+ print(f"[AITER case] weight_key={w_key}, activation_key={a_key}")
88
+
89
+
90
def rank_worker(
91
pgi: ProcessGroupInfo,
92
vllm_config: VllmConfig,
@@ -125,6 +142,25 @@ def rank_worker(
142
count -= 1
143
continue
144
145
+ # Skip unsupported: AITER x DeepEP-HT/Mori dispatch crashes
146
+ # with an illegal memory access at world_size>1 on gfx942.
147
+ # https://github.com/vllm-project/vllm/issues/57029
148
+ if (
149
+ config.world_size > 1
150
+ and getattr(config.fused_experts_type, "__name__", "") == "AiterExperts"
151
+ and getattr(config.prepare_finalize_type, "__name__", "")
152
+ in ("DeepEPHTPrepareAndFinalize", "MoriPrepareAndFinalize")
153
+ ):
154
+ print(
155
+ f"Skipping[{pgi.rank}]: m={m}, topk={topk}"
156
+ " (AITER x DeepEP-HT/Mori illegal memory access,"
157
+ " https://github.com/vllm-project/vllm/issues/57029)"
158
+ )
159
+ count -= 1
160
+ continue
161
+
162
+ assert_aiter_quant_scheme_case(config)
163
+
164
# modular kernel out
165
mk_out = run_modular_kernel(pgi, vllm_config, config, weights, rank_tensors)
166
@@ -152,32 +188,7 @@ def rank_worker(
188
and config.quant_config is not None
189
)
190
if is_aiter_fp8:
155
- diff = (ref_out - mk_out).abs()
156
- n_total = diff.numel()
157
- max_diff = diff.max().item()
158
- n_exceed = int((diff > atol).sum().item())
159
- pct_exceed = n_exceed / n_total * 100
160
- # FP8 hw matmul vs f32 reference: up to ~4% of
161
- # elements may exceed base tolerance, but max
162
- # error should stay within 3x base tolerance.
163
- max_pct_allowed = 5.0
164
- relaxed_atol = atol * 4
165
- print(
166
- f"[AITER FP8 precision] "
167
- f"max_diff={max_diff:.6f}, "
168
- f"exceed_atol={n_exceed}/{n_total} "
169
- f"({pct_exceed:.4f}%), "
170
- f"max_pct_allowed={max_pct_allowed}%, "
171
- f"relaxed_limit={relaxed_atol}"
172
- )
173
- assert pct_exceed <= max_pct_allowed, (
174
- f"AITER FP8: {pct_exceed:.2f}% elements exceed "
175
- f"atol={atol} (max allowed {max_pct_allowed}%)"
176
- )
177
- assert max_diff <= relaxed_atol, (
178
- f"AITER FP8: max_diff={max_diff:.6f} exceeds "
179
- f"relaxed limit {relaxed_atol}"
180
- )
191
+ check_accuracy(ref_out, mk_out, atol=atol, rtol=rtol, percent=0.9)
192
else:
193
torch.testing.assert_close(ref_out, mk_out, atol=atol, rtol=rtol)
194
format_result(verbose, config.describe())
vllm/model_executor/layers/fused_moe/all2all_utils.py
+11
-4
@@ -284,13 +284,20 @@ def maybe_make_prepare_finalize(
284
# Note: We may want to use FP8 dispatch just to reduce
285
# data movement.
286
use_fp8_dispatch = (
287
- quant_config.is_per_act_token or quant_config.is_block_quantized
287
+ quant_config.is_per_act_token
288
+ or quant_config.is_block_quantized
289
+ or quant_config.is_per_tensor
290
)
291
if use_fp8_dispatch:
290
- # For PTPC (per token per channel) quant, scale dim is 1
291
- # For 1x128 quant, scale dim is hidden_dim // 128
292
+ # For PTPC (per token per channel) or per-tensor quant,
293
+ # scale dim is 1. For 1x128 quant, scale dim is
294
+ # hidden_dim // 128
295
quant_dtype = quant_config.quant_dtype
293
- scale_dim = 1 if quant_config.is_per_act_token else moe.hidden_dim // 128
296
+ scale_dim = (
297
+ 1
298
+ if (quant_config.is_per_act_token or quant_config.is_per_tensor)
299
+ else moe.hidden_dim // 128
300
+ )
301
else:
302
# Unquantized dispatch (e.g. AITER with defer_input_quant):
303
# dispatch raw BF16/FP16 data, no scales needed.
vllm/model_executor/layers/fused_moe/prepare_finalize/mori.py
+9
@@ -82,6 +82,15 @@ class MoriPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
82
elif quant_config.is_per_act_token:
83
quant_func = get_hip_quant(QuantType.per_Token)
84
a1, scale = quant_func(a1, quant_dtype=current_platform.fp8_dtype())
85
+ elif quant_config.is_per_tensor:
86
+ quant_func = get_hip_quant(QuantType.per_Tensor)
87
+ a1, scale = quant_func(
88
+ a1,
89
+ scale=quant_config.a1_scale,
90
+ quant_dtype=current_platform.fp8_dtype(),
91
+ )
92
+ # mori expects one scale slot per token; broadcast.
93
+ scale = scale.expand(a1.shape[0], 1).contiguous()
94
95
(
96
dispatch_a1,