[ROCm][CI] Extended Fused MoE and FP8 MoE test support (#41100)
Signed-off-by: Andreas Karatzas <akaratza@amd.com> Signed-off-by: Andreas Karatzas <Andreas.Karatzas@amd.com>
Andreas Karatzas committed
Aug 19, 2026 at 10:19 UTC
160f7f0840e1773b2418199800c7ac66f1b4f074
9 files changed
+331
-31
.buildkite/test-amd.yaml
+58
-1
@@ -3500,9 +3500,44 @@ steps:
3500
- vllm/_aiter_ops.py
3501
- vllm/platforms/rocm.py
3502
commands:
3503
- - 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
3503
+ - pytest -v -s kernels/moe
3504
+ --ignore=kernels/moe/test_modular_oai_triton_moe.py
3505
+ --ignore=kernels/moe/test_gpt_oss_triton_kernels.py
3506
+ --ignore=kernels/moe/test_moe.py
3507
+ --ignore=kernels/moe/test_block_int8.py
3508
+ --ignore=kernels/moe/test_triton_moe_no_act_mul.py
3509
+ --ignore=kernels/moe/test_triton_moe_ptpc_fp8.py
3510
+ --ignore=kernels/moe/test_deepep_moe.py
3511
+ --ignore=kernels/moe/test_moe_layer.py
3512
+ --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT
3513
- pytest -v -s kernels/moe/test_modular_oai_triton_moe.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT
3514
3515
+- label: Kernels FusedMoE Layer Test (2xB200-2xMI355) # TBD
3516
+ timeout_in_minutes: 180
3517
+ mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355]
3518
+ dind: false
3519
+ agent_pool: mi355_2
3520
+ num_gpus: 2
3521
+ optional: true
3522
+ working_dir: "/vllm-workspace/tests"
3523
+ source_file_dependencies:
3524
+ - csrc/moe/
3525
+ - csrc/rocm/
3526
+ - tests/kernels/moe
3527
+ - vllm/model_executor/layers/fused_moe/
3528
+ - vllm/model_executor/layers/quantization/
3529
+ - vllm/distributed/
3530
+ - vllm/config/
3531
+ - vllm/forward_context.py
3532
+ - vllm/v1/worker/workspace.py
3533
+ - vllm/utils/import_utils.py
3534
+ - vllm/utils/math_utils.py
3535
+ - vllm/utils/torch_utils.py
3536
+ - vllm/platforms/
3537
+ - vllm/_aiter_ops.py
3538
+ commands:
3539
+ - pytest -v -s kernels/moe/test_moe_layer.py
3540
+
3541
- label: Kernels Quantization Test %N # TBD
3542
timeout_in_minutes: 180
3543
mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355]
@@ -3526,12 +3561,34 @@ steps:
3561
commands:
3562
- pytest -v -s kernels/quantization
3563
3564
+- label: Kernels FP8 MoE Test (2xH100-1xMI355) # TBD
3565
+ timeout_in_minutes: 180
3566
+ mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355]
3567
+ dind: false
3568
+ agent_pool: mi355_1
3569
+ working_dir: "/vllm-workspace/tests"
3570
+ source_file_dependencies:
3571
+ - csrc/moe/
3572
+ - vllm/model_executor/layers/fused_moe/
3573
+ - tests/kernels/moe/test_deepep_moe.py
3574
+ - vllm/_aiter_ops.py
3575
+ - vllm/platforms/rocm.py
3576
+ - vllm/envs.py
3577
+ commands:
3578
+ - pytest -v -s kernels/moe/test_gpt_oss_triton_kernels.py
3579
+ - pytest -v -s kernels/moe/test_modular_oai_triton_moe.py
3580
+ - pytest -v -s kernels/moe/test_moe.py
3581
+ - pytest -v -s kernels/moe/test_block_int8.py
3582
+ - pytest -v -s kernels/moe/test_triton_moe_no_act_mul.py
3583
+ - pytest -v -s kernels/moe/test_triton_moe_ptpc_fp8.py
3584
+
3585
- label: Kernels FP8 MoE Test (2xH100-2xMI355) # TBD
3586
timeout_in_minutes: 180
3587
mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355]
3588
dind: false
3589
agent_pool: mi355_2
3590
num_gpus: 2
3591
+ optional: true
3592
working_dir: "/vllm-workspace/tests"
3593
source_file_dependencies:
3594
- csrc/moe/
.buildkite/test_areas/kernels.yaml
+53
@@ -451,3 +451,56 @@ steps:
451
commands:
452
- pytest -v -s kernels/moe/test_moe_layer.py
453
- pytest -v -s kernels/moe/test_deepep_v2_moe.py
454
+
455
+- label: Kernels FusedMoE Layer Test (2xMI355)
456
+ key: kernels-fusedmoe-layer-test-2-mi355
457
+ depends_on:
458
+ - image-build-amd
459
+ timeout_in_minutes: 180
460
+ dind: false
461
+ device: mi355_2
462
+ soft_fail: true
463
+ optional: true
464
+ working_dir: "/vllm-workspace/tests"
465
+ source_file_dependencies:
466
+ - csrc/moe/
467
+ - csrc/rocm/
468
+ - tests/kernels/moe
469
+ - vllm/model_executor/layers/fused_moe/
470
+ - vllm/model_executor/layers/quantization/
471
+ - vllm/distributed/
472
+ - vllm/config/
473
+ - vllm/forward_context.py
474
+ - vllm/v1/worker/workspace.py
475
+ - vllm/utils/import_utils.py
476
+ - vllm/utils/math_utils.py
477
+ - vllm/utils/torch_utils.py
478
+ - vllm/platforms/
479
+ - vllm/_aiter_ops.py
480
+ commands:
481
+ - pytest -v -s kernels/moe/test_moe_layer.py
482
+
483
+- label: Kernels FP8 MoE Test (MI355)
484
+ key: kernels-fp8-moe-test-mi355
485
+ depends_on:
486
+ - image-build-amd
487
+ timeout_in_minutes: 180
488
+ dind: false
489
+ device: mi355_1
490
+ soft_fail: true
491
+ optional: true
492
+ working_dir: "/vllm-workspace/tests"
493
+ source_file_dependencies:
494
+ - csrc/moe/
495
+ - vllm/model_executor/layers/fused_moe/
496
+ - tests/kernels/moe/test_deepep_moe.py
497
+ - vllm/_aiter_ops.py
498
+ - vllm/platforms/rocm.py
499
+ - vllm/envs.py
500
+ commands:
501
+ - pytest -v -s kernels/moe/test_gpt_oss_triton_kernels.py
502
+ - pytest -v -s kernels/moe/test_modular_oai_triton_moe.py
503
+ - pytest -v -s kernels/moe/test_moe.py
504
+ - pytest -v -s kernels/moe/test_block_int8.py
505
+ - pytest -v -s kernels/moe/test_triton_moe_no_act_mul.py
506
+ - pytest -v -s kernels/moe/test_triton_moe_ptpc_fp8.py
tests/kernels/moe/test_modular_oai_triton_moe.py
+27
-2
@@ -8,6 +8,7 @@ from __future__ import annotations
8
9
import pytest
10
import torch
11
+import torch.nn.functional as F
12
13
from tests.utils import wait_for_gpu_memory_to_clear
14
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
@@ -37,6 +38,7 @@ from vllm.model_executor.layers.fused_moe.experts.gpt_oss_triton_kernels_moe imp
38
)
39
from vllm.model_executor.layers.fused_moe.modular_kernel import FusedMoEKernel
40
from vllm.platforms import current_platform
41
+from vllm.utils.math_utils import round_up
42
from vllm.utils.torch_utils import set_random_seed
43
44
from .utils import make_dummy_moe_config, shuffle_weight
@@ -104,13 +106,30 @@ def make_weights(dtype, k, n, e):
106
w1_tri = shuffle_weight(w1_tri)
107
w1_bias_tri = shuffle_weight(w1_bias_tri)
108
109
+ if current_platform.is_rocm():
110
+ k_align, n2_align = 256, 512
111
+ else:
112
+ k_align, n2_align = 64, 128
113
+
114
+ w1_bottom_pad = round_up(w1_tri.shape[1], k_align) - w1_tri.shape[1]
115
+ w1_right_pad = round_up(w1_tri.shape[2], n2_align) - w1_tri.shape[2]
116
+ w2_bottom_pad = w1_right_pad // 2
117
+ w2_right_pad = w1_bottom_pad
118
+
119
+ w1_tri = F.pad(w1_tri, (0, w1_right_pad, 0, w1_bottom_pad, 0, 0))
120
+ w2_tri = F.pad(w2_tri, (0, w2_right_pad, 0, w2_bottom_pad, 0, 0))
121
+ w1_bias_tri = F.pad(w1_bias_tri, (0, w1_right_pad, 0, 0))
122
+ w2_bias_tri = F.pad(w2_bias_tri, (0, w2_right_pad, 0, 0))
123
+
124
# quant triton_weights
125
w1_tri, w1_scale_tri = downcast_to_mxfp(w1_tri, torch.uint8, axis=1)
126
w1 = upcast_from_mxfp(w1_tri, w1_scale_tri, dtype, axis=1)
127
+ w1 = w1[..., :k, : 2 * n]
128
w1 = unshuffle_weight(w1)
129
130
w2_tri, w2_scale_tri = downcast_to_mxfp(w2_tri, torch.uint8, axis=1)
131
w2 = upcast_from_mxfp(w2_tri, w2_scale_tri, dtype, axis=1)
132
+ w2 = w2[..., :n, :k]
133
134
num_warps = 8
135
w_layout, w_layout_opts = layout.make_default_matmul_mxfp4_w_layout(mx_axis=1)
@@ -150,6 +169,7 @@ def make_weights(dtype, k, n, e):
169
w2_bias_tri,
170
w1_precision_config,
171
w2_precision_config,
172
+ w1_bottom_pad,
173
)
174
175
@@ -237,7 +257,7 @@ def oai_triton_moe_impl(
257
258
259
@pytest.mark.skipif(
240
- not current_platform.is_cuda(), reason="This test is skipped on non-CUDA platform."
260
+ not current_platform.is_cuda_alike(), reason="Requires CUDA-alike platform."
261
)
262
@pytest.mark.parametrize("dtype", [torch.bfloat16])
263
@pytest.mark.parametrize("m,n,k", MNK)
@@ -256,6 +276,7 @@ def test_oai_triton_moe(
276
):
277
wait_for_gpu_memory_to_clear(devices=[0], threshold_ratio=0.1)
278
set_random_seed(0)
279
+
280
(
281
w1,
282
w2,
@@ -267,9 +288,11 @@ def test_oai_triton_moe(
288
w2_bias_tri,
289
w1_precision_config,
290
w2_precision_config,
291
+ x_pad,
292
) = make_weights(dtype, k, n, num_experts)
293
294
x = torch.randn((m, k), dtype=dtype, device="cuda")
295
+ x_tri = F.pad(x, (0, x_pad, 0, 0))
296
router_logits = torch.randn(m, num_experts, device="cuda", dtype=dtype)
297
topk_weights, topk_ids = torch.topk(router_logits, k=topk, dim=-1, sorted=True)
298
topk_weights = torch.nn.functional.softmax(topk_weights, dim=-1)
@@ -278,7 +301,7 @@ def test_oai_triton_moe(
301
out_ref = torch_moe_impl(x, w1, w2, w1_bias, w2_bias, topk_weights, topk_ids)
302
303
out = oai_triton_moe_impl(
281
- x,
304
+ x_tri,
305
w1_tri,
306
w2_tri,
307
w1_precision_config,
@@ -290,6 +313,7 @@ def test_oai_triton_moe(
313
topk_ids,
314
unfused,
315
)
316
+ out = out[..., :k]
317
318
assert_close(ref=out_ref, tri=out, maxtol=0.025, rmstol=0.005)
319
@@ -321,6 +345,7 @@ def test_unfused_oai_triton_experts_apply_direct_deepseek_v4_topology(workspace_
345
w2_bias_tri,
346
w1_precision_config,
347
w2_precision_config,
348
+ _x_pad,
349
) = make_weights(dtype, k, n, num_experts)
350
351
x = torch.randn((m, k), dtype=dtype, device="cuda")
tests/kernels/moe/test_moe.py
+41
@@ -16,6 +16,7 @@ from torch.nn import Parameter
16
from torch.nn import functional as F
17
18
import vllm.model_executor.layers.fused_moe # noqa
19
+import vllm.model_executor.layers.fused_moe.fused_moe as fused_moe_module
20
from tests.kernels.moe.utils import (
21
fused_moe,
22
make_dummy_moe_config,
@@ -68,6 +69,46 @@ from vllm.utils.torch_utils import set_random_seed
69
DEVICE_TYPE = current_platform.device_type
70
71
72
+def test_triton_moe_launcher_passes_scalar_scale_as_pointer(monkeypatch) -> None:
73
+ captured: dict[str, torch.Tensor] = {}
74
+
75
+ class FakeKernel:
76
+ def __getitem__(self, grid):
77
+ def launch(*args, **kwargs) -> None:
78
+ captured["a_scale"] = args[4]
79
+
80
+ return launch
81
+
82
+ monkeypatch.setattr(fused_moe_module, "fused_moe_kernel", FakeKernel())
83
+
84
+ a_scale = torch.tensor(0.5)
85
+ fused_moe_module.invoke_fused_moe_triton_kernel(
86
+ A=torch.ones((1, 1)),
87
+ B=torch.ones((1, 1, 1)),
88
+ C=torch.empty((1, 1, 1)),
89
+ A_scale=a_scale,
90
+ B_scale=torch.ones(1),
91
+ topk_weights=torch.ones((1, 1)),
92
+ sorted_token_ids=None,
93
+ expert_ids=torch.zeros(1, dtype=torch.int32),
94
+ num_tokens_post_padded=torch.ones(1, dtype=torch.int32),
95
+ mul_routed_weight=True,
96
+ top_k=1,
97
+ config={"BLOCK_SIZE_M": 1, "BLOCK_SIZE_N": 1, "BLOCK_SIZE_K": 1},
98
+ compute_type=tl.float32,
99
+ use_fp8_w8a8=True,
100
+ use_int8_w8a8=False,
101
+ use_int8_w8a16=False,
102
+ use_int4_w4a16=False,
103
+ per_channel_quant=False,
104
+ )
105
+
106
+ captured_scale = captured["a_scale"]
107
+ assert a_scale.ndim == 0
108
+ assert captured_scale.shape == (1,)
109
+ assert captured_scale.data_ptr() == a_scale.data_ptr()
110
+
111
+
112
def iterative_moe(
113
hidden_states: torch.Tensor,
114
w1: torch.Tensor,
tests/kernels/moe/test_moe_layer.py
+106
-26
@@ -7,9 +7,11 @@ Run `pytest tests/kernels/test_moe_layer.py`.
7
8
import functools
9
import os
10
+import tempfile
11
import traceback
12
import types
13
from collections.abc import Callable
14
+from contextlib import suppress
15
from dataclasses import astuple, dataclass, fields
16
from itertools import product
17
from typing import get_args
@@ -71,6 +73,15 @@ from vllm.v1.worker.workspace import (
73
is_workspace_manager_initialized,
74
)
75
76
+
77
+def on_gfx950() -> bool:
78
+ if current_platform.is_rocm():
79
+ from vllm.platforms.rocm import on_gfx950 as rocm_on_gfx950
80
+
81
+ return rocm_on_gfx950()
82
+ return False
83
+
84
+
85
fp8_dtype = torch.float8_e4m3fn # current_platform.fp8_dtype
86
87
SHAPE_COMBOS = [
@@ -114,6 +125,7 @@ if has_nixl_ep():
125
BACKENDS += ["nixl_ep"]
126
127
DEEPEP_BACKENDS = {"deepep_high_throughput", "deepep_low_latency"}
128
+MORI_BACKENDS = {"mori_high_throughput", "mori_low_latency"}
129
130
QUANT_METHODS = [
131
None,
@@ -484,12 +496,12 @@ def is_valid_config(config: MoETestConfig) -> tuple[bool, str | None]:
496
"leads to large differences.",
497
)
498
487
- # Skip modelopt_fp4 if not on B100+ (compute capability 10.0+)
488
- if (
489
- config.quantization == "modelopt_fp4"
490
- and not current_platform.has_device_capability(100)
499
+ # Skip modelopt_fp4 if not on B100+ (compute capability 10.0+) or gfx950.
500
+ if config.quantization == "modelopt_fp4" and not (
501
+ (current_platform.is_rocm() and on_gfx950())
502
+ or current_platform.has_device_capability(100)
503
):
492
- return False, "modelopt_fp4 not supported on H100+ GPUs"
504
+ return False, "modelopt_fp4 requires native NVFP4 or emulation"
505
506
# Skip flashinfer_nvlink if not on H100+ (compute capability 10.0+)
507
if (
@@ -508,6 +520,17 @@ def is_valid_config(config: MoETestConfig) -> tuple[bool, str | None]:
520
f"{config.backend} does not support quantization={config.quantization}",
521
)
522
523
+ if config.backend in MORI_BACKENDS:
524
+ if os.environ.get("VLLM_TEST_ENABLE_MORI_MOE_LAYER") != "1":
525
+ return False, "mori MoE layer matrix is opt-in"
526
+
527
+ from vllm._aiter_ops import rocm_aiter_ops
528
+
529
+ if not rocm_aiter_ops.is_fused_moe_enabled():
530
+ return False, "mori requires AITER fused MoE"
531
+ if rocm_aiter_ops.is_fusion_moe_shared_experts_enabled():
532
+ return False, "mori does not support AITER shared expert fusion"
533
+
534
if config.backend == "deepep_low_latency":
535
from vllm.model_executor.layers.fused_moe.prepare_finalize.deepep_ll import ( # noqa: E501
536
DeepEPLLPrepareAndFinalize,
@@ -1695,14 +1718,16 @@ def _parallel_worker(
1718
cpu_group,
1719
test_configs: list[MoETestConfig],
1720
verbosity: int,
1721
+ failure_report_path: str | None = None,
1722
**kwargs,
1723
) -> None:
1724
set_random_seed(7)
1725
+ is_logging_rank = pgi.rank == 0
1726
1727
total = 0
1728
passed = 0
1729
failed = 0
1705
- fail_ids = []
1730
+ failure_details = []
1731
1732
dp_rank = vllm_config.parallel_config.data_parallel_rank
1733
@@ -1717,9 +1742,11 @@ def _parallel_worker(
1742
1743
tp_rank = pgi.rank % test_config.tp_size
1744
1720
- if verbosity > 0:
1745
+ if verbosity > 0 and is_logging_rank:
1746
print(f"subtest: {test_config.id()}", end="")
1747
1748
+ local_failed = False
1749
+ local_error: str | None = None
1750
try:
1751
_run_one_config(
1752
vllm_config,
@@ -1743,19 +1770,9 @@ def _parallel_worker(
1770
use_gate=test_config.use_gate,
1771
use_routed_input_transform=test_config.use_routed_input_transform,
1772
)
1746
- if verbosity > 0:
1747
- print(" PASSED")
1748
- else:
1749
- print(".", end="")
1750
- passed = passed + 1
1751
- except Exception as ex:
1752
- fail_ids.append(test_config.id())
1753
- failed = failed + 1
1754
- if verbosity > 0:
1755
- traceback.print_exc()
1756
- print(f"\n{str(ex)}\nFAILED")
1757
- else:
1758
- print("F", end="")
1773
+ except Exception:
1774
+ local_failed = True
1775
+ local_error = traceback.format_exc()
1776
finally:
1777
# DeepEP managers are not reliably reusable across many subtests in
1778
# a single worker process. Tear them down after each DeepEP case so
@@ -1771,6 +1788,40 @@ def _parallel_worker(
1788
total = total + 1
1789
torch.distributed.barrier()
1790
1791
+ any_failed_tensor = torch.tensor(
1792
+ [int(local_failed)], device=pgi.device, dtype=torch.int32
1793
+ )
1794
+ torch.distributed.all_reduce(
1795
+ any_failed_tensor, op=torch.distributed.ReduceOp.MAX
1796
+ )
1797
+ any_failed = bool(any_failed_tensor.item())
1798
+
1799
+ if any_failed:
1800
+ failed = failed + 1
1801
+
1802
+ gathered_errors = [None] * pgi.world_size
1803
+ torch.distributed.all_gather_object(
1804
+ gathered_errors, local_error, group=cpu_group
1805
+ )
1806
+ first_error = next(
1807
+ (error for error in gathered_errors if error is not None),
1808
+ "unknown distributed failure",
1809
+ )
1810
+ assert first_error is not None
1811
+ failure_details.append(f"{test_config.id()}\n{first_error.rstrip()}")
1812
+
1813
+ if verbosity > 0 and is_logging_rank:
1814
+ print(" FAILED")
1815
+ print(first_error.rstrip())
1816
+ elif is_logging_rank:
1817
+ print("F", end="")
1818
+ else:
1819
+ passed = passed + 1
1820
+ if verbosity > 0 and is_logging_rank:
1821
+ print(" PASSED")
1822
+ elif is_logging_rank:
1823
+ print(".", end="")
1824
+
1825
skipped = total - (passed + failed)
1826
1827
fails = f"{failed} failed" if failed > 0 else ""
@@ -1780,17 +1831,24 @@ def _parallel_worker(
1831
passes = f"{sep}{passed} passed" if passed > 0 else ""
1832
1833
report = (
1783
- f"============= {fails}{skips}{passes} of {total} total tests ============="
1834
+ f"============= {fails}{skips}{passes} of {total} total subcases ============="
1835
)
1836
1786
- sep = "\n" if verbosity == 0 else ""
1787
- print(f"{sep}{report}")
1837
+ if is_logging_rank:
1838
+ sep = "\n" if verbosity == 0 else ""
1839
+ print(f"{sep}{report}")
1840
1841
if failed > 0:
1790
- fail_ids_str = "\n".join(fail_ids)
1791
- raise RuntimeError(
1792
- f"\n============= Failed subtests =============\n{fail_ids_str}\n{report}"
1842
+ failure_details_str = "\n\n".join(failure_details)
1843
+ failure_report = (
1844
+ f"\n============= Failed subcases =============\n"
1845
+ f"{failure_details_str}\n{report}"
1846
)
1847
+ if is_logging_rank and failure_report_path is not None:
1848
+ with open(failure_report_path, "w", encoding="utf-8") as report_file:
1849
+ report_file.write(failure_report)
1850
+ if is_logging_rank:
1851
+ raise RuntimeError(failure_report)
1852
1853
1854
# TODO: add cudagraphs/torch.compile tests
@@ -1836,6 +1894,17 @@ def test_moe_layer(
1894
if os.environ.get("VLLM_LOGGING_LEVEL") is None:
1895
monkeypatch.setenv("VLLM_LOGGING_LEVEL", "ERROR")
1896
1897
+ if (
1898
+ backend in MORI_BACKENDS
1899
+ and os.environ.get("VLLM_TEST_ENABLE_MORI_MOE_LAYER") == "1"
1900
+ ):
1901
+ monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1")
1902
+ monkeypatch.setenv("VLLM_ROCM_USE_AITER_MOE", "1")
1903
+ monkeypatch.setenv("VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS", "0")
1904
+ from vllm._aiter_ops import rocm_aiter_ops
1905
+
1906
+ rocm_aiter_ops.refresh_env_variables()
1907
+
1908
# TODO: cover FlashInfer MoE backends via moe_backend, e.g.
1909
# moe_backend=flashinfer_trtllm / flashinfer_cutlass / flashinfer_cutedsl
1910
# (BF16, FP8 and NVFP4 paths), and VLLM_USE_FLASHINFER_MOE_INT4=1.
@@ -1881,6 +1950,11 @@ def test_moe_layer(
1950
if len(test_configs) == 0:
1951
pytest.skip("No supported configs found for this testpoint.")
1952
1953
+ with tempfile.NamedTemporaryFile(
1954
+ prefix="moe-layer-failures-", delete=False
1955
+ ) as failure_report_file:
1956
+ failure_report_path = failure_report_file.name
1957
+
1958
try:
1959
parallel_launch_with_config(
1960
world_size,
@@ -1889,7 +1963,13 @@ def test_moe_layer(
1963
None,
1964
test_configs,
1965
verbosity,
1966
+ failure_report_path=failure_report_path,
1967
)
1968
+ if os.path.getsize(failure_report_path) > 0:
1969
+ with open(failure_report_path, encoding="utf-8") as report_file:
1970
+ pytest.fail(report_file.read())
1971
finally:
1972
+ with suppress(FileNotFoundError):
1973
+ os.remove(failure_report_path)
1974
torch.accelerator.synchronize() # TODO: Is this needed?
1975
torch.accelerator.empty_cache()
tests/kernels/moe/utils.py
+26
-1
@@ -33,6 +33,10 @@ from vllm.model_executor.layers.fused_moe.prepare_finalize.batched import (
33
)
34
from vllm.model_executor.layers.fused_moe.router.fused_topk_router import fused_topk
35
from vllm.model_executor.layers.fused_moe.utils import moe_kernel_quantize_input
36
+from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import (
37
+ ref_nvfp4_quant,
38
+)
39
+from vllm.platforms import current_platform
40
from vllm.utils.deep_gemm import per_block_cast_to_fp8
41
from vllm.utils.math_utils import round_up
42
@@ -294,13 +298,34 @@ def moe_quantize_weights_2d(
298
assert not per_token_quant
299
w_amax = torch.abs(w).max().to(torch.float32)
300
w_gs = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / w_amax
297
- w, w_s = ops.scaled_fp4_quant(w, w_gs)
301
+ if current_platform.is_rocm():
302
+ w, w_s = _scaled_fp4_quant_emulated(w, w_gs)
303
+ else:
304
+ w, w_s = ops.scaled_fp4_quant(w, w_gs)
305
else:
306
raise RuntimeError(f"Unsupported quant type {quant_dtype}")
307
308
return w, w_s, w_gs
309
310
311
+def _pack_e2m1_fp4(fp4_values: torch.Tensor) -> torch.Tensor:
312
+ assert fp4_values.shape[-1] % 2 == 0
313
+
314
+ abs_values = fp4_values.abs()
315
+ codes = torch.empty_like(abs_values, dtype=torch.uint8)
316
+ for code, value in enumerate((0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0)):
317
+ codes[abs_values == value] = code
318
+ codes = codes | ((fp4_values < 0).to(torch.uint8) << 3)
319
+ return codes[..., 0::2] | (codes[..., 1::2] << 4)
320
+
321
+
322
+def _scaled_fp4_quant_emulated(
323
+ w: torch.Tensor, w_gs: torch.Tensor
324
+) -> tuple[torch.Tensor, torch.Tensor]:
325
+ fp4_values, w_s = ref_nvfp4_quant(w, w_gs, block_size=16)
326
+ return _pack_e2m1_fp4(fp4_values), w_s.to(torch.float8_e4m3fn)
327
+
328
+
329
def moe_quantize_weights(
330
w: torch.Tensor,
331
w_s: torch.Tensor | None,
tests/quantization/test_fp8.py
+13
@@ -31,6 +31,9 @@ from vllm.model_executor.layers.quantization.kv_cache import BaseKVCacheMethod
31
from vllm.model_executor.layers.quantization.online.fp8 import (
32
Fp8PerTensorOnlineLinearMethod,
33
)
34
+from vllm.model_executor.layers.quantization.utils.fp8_utils import (
35
+ process_fp8_input_tensor_strategy_moe,
36
+)
37
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
38
from vllm.platforms import current_platform
39
@@ -47,6 +50,16 @@ MODELS = [
50
]
51
52
53
+def test_static_fp8_moe_input_scales_remain_scalar() -> None:
54
+ a1_scale, a2_scale = process_fp8_input_tensor_strategy_moe(
55
+ torch.tensor([0.25, 0.5]),
56
+ torch.tensor([0.75, 0.6]),
57
+ enable_eplb=False,
58
+ )
59
+
60
+ assert a1_scale.ndim == a2_scale.ndim == 0
61
+
62
+
63
@pytest.mark.skipif(
64
not is_quant_method_supported("fp8"),
65
reason="FP8 is not supported on this GPU type.",
vllm/model_executor/layers/fused_moe/fused_moe.py
+6
@@ -857,6 +857,12 @@ def invoke_fused_moe_triton_kernel(
857
BLOCK_SIZE_K,
858
)
859
use_td = False
860
+
861
+ # Triton treats 0-D tensor arguments as scalar values, but the kernel
862
+ # loads tensor-wise activation scales through a pointer.
863
+ if A_scale is not None and A_scale.ndim == 0:
864
+ A_scale = A_scale.reshape(1)
865
+
866
fused_moe_kernel[grid](
867
A,
868
B,
vllm/model_executor/layers/quantization/utils/nvfp4_emulation_utils.py
+1
-1
@@ -313,7 +313,7 @@ def break_fp4_bytes(a, dtype):
313
signs = (combined & 0x08).to(torch.bool) # Sign bits
314
abs_vals = (combined & 0x07).to(torch.long)
315
316
- kE2M1 = kE2M1ToFloat_handle.val
316
+ kE2M1 = kE2M1ToFloat_handle.val.to(device=a.device)
317
# Device-aware lookup and sign application
318
values = kE2M1[abs_vals] * torch.where(signs, -1.0, 1.0)
319
# Reshape to final form