[ROCm][CI] Stabilize MI355 FusedMoE test group (#53025)
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
Andreas Karatzas committed
Aug 21, 2026 at 15:15 UTC
88eb946cb1a0901bcbb842de463d6c1cae035fa9
2 files changed
+174
-70
.buildkite/test_areas/kernels.yaml
-53
@@ -468,56 +468,3 @@ steps:
468
commands:
469
- pytest -v -s kernels/moe/test_moe_layer.py
470
- pytest -v -s kernels/moe/test_deepep_v2_moe.py
471
-
472
-- label: Kernels FusedMoE Layer Test (2xMI355)
473
- key: kernels-fusedmoe-layer-test-2-mi355
474
- depends_on:
475
- - image-build-amd
476
- timeout_in_minutes: 180
477
- dind: false
478
- device: mi355_2
479
- soft_fail: true
480
- optional: true
481
- working_dir: "/vllm-workspace/tests"
482
- source_file_dependencies:
483
- - csrc/moe/
484
- - csrc/rocm/
485
- - tests/kernels/moe
486
- - vllm/model_executor/layers/fused_moe/
487
- - vllm/model_executor/layers/quantization/
488
- - vllm/distributed/
489
- - vllm/config/
490
- - vllm/forward_context.py
491
- - vllm/v1/worker/workspace.py
492
- - vllm/utils/import_utils.py
493
- - vllm/utils/math_utils.py
494
- - vllm/utils/torch_utils.py
495
- - vllm/platforms/
496
- - vllm/_aiter_ops.py
497
- commands:
498
- - pytest -v -s kernels/moe/test_moe_layer.py
499
-
500
-- label: Kernels FP8 MoE Test (MI355)
501
- key: kernels-fp8-moe-test-mi355
502
- depends_on:
503
- - image-build-amd
504
- timeout_in_minutes: 180
505
- dind: false
506
- device: mi355_1
507
- soft_fail: true
508
- optional: true
509
- working_dir: "/vllm-workspace/tests"
510
- source_file_dependencies:
511
- - csrc/moe/
512
- - vllm/model_executor/layers/fused_moe/
513
- - tests/kernels/moe/test_deepep_moe.py
514
- - vllm/_aiter_ops.py
515
- - vllm/platforms/rocm.py
516
- - vllm/envs.py
517
- commands:
518
- - pytest -v -s kernels/moe/test_gpt_oss_triton_kernels.py
519
- - pytest -v -s kernels/moe/test_modular_oai_triton_moe.py
520
- - pytest -v -s kernels/moe/test_moe.py
521
- - pytest -v -s kernels/moe/test_block_int8.py
522
- - pytest -v -s kernels/moe/test_triton_moe_no_act_mul.py
523
- - pytest -v -s kernels/moe/test_triton_moe_ptpc_fp8.py
tests/kernels/moe/test_moe_layer.py
+174
-17
@@ -7,12 +7,13 @@ Run `pytest tests/kernels/test_moe_layer.py`.
7
8
import functools
9
import os
10
+import sys
11
import tempfile
12
import traceback
13
import types
14
from collections.abc import Callable
15
from contextlib import suppress
15
-from dataclasses import astuple, dataclass, fields
16
+from dataclasses import astuple, dataclass, fields, replace
17
from itertools import product
18
from typing import get_args
19
@@ -28,6 +29,7 @@ from tests.kernels.moe.modular_kernel_tools.parallel_utils import (
29
from tests.kernels.moe.utils import TestMLP, make_test_weights, moe_quantize_weights
30
from vllm.config import (
31
CompilationConfig,
32
+ EPLBConfig,
33
ParallelConfig,
34
SchedulerConfig,
35
VllmConfig,
@@ -383,6 +385,56 @@ class MoETestConfig:
385
return MoETestConfig(*values)
386
387
388
+def _group_deepep_ll_configs(
389
+ test_configs: list[MoETestConfig],
390
+) -> list[list[MoETestConfig]]:
391
+ """Group configs that can share one DeepEP low-latency buffer."""
392
+ groups: dict[tuple[int, int, int], list[MoETestConfig]] = {}
393
+ for test_config in test_configs:
394
+ # max_num_tokens and the remaining buffer arguments are fixed for one
395
+ # outer pytest item. These are the arguments that can vary here.
396
+ buffer_key = (
397
+ test_config.k,
398
+ test_config.ep_size,
399
+ test_config.num_experts,
400
+ )
401
+ groups.setdefault(buffer_key, []).append(test_config)
402
+ return list(groups.values())
403
+
404
+
405
+@pytest.mark.cpu_test
406
+def test_group_deepep_ll_configs_by_buffer_requirements():
407
+ config_a = MoETestConfig(
408
+ 1,
409
+ 128,
410
+ 2048,
411
+ 8,
412
+ 2,
413
+ torch.bfloat16,
414
+ None,
415
+ False,
416
+ False,
417
+ False,
418
+ backend="deepep_low_latency",
419
+ ep_size=2,
420
+ dp_size=2,
421
+ )
422
+ config_b = replace(config_a, num_experts=64)
423
+ config_a_later = replace(config_a, m=32)
424
+ config_c = replace(config_a, k=4096)
425
+ config_d = replace(config_a, ep_size=4, dp_size=4)
426
+
427
+ assert _group_deepep_ll_configs(
428
+ [config_a, config_b, config_a_later, config_c, config_d]
429
+ ) == [
430
+ [config_a, config_a_later],
431
+ [config_b],
432
+ [config_c],
433
+ [config_d],
434
+ ]
435
+ assert _group_deepep_ll_configs([]) == []
436
+
437
+
438
def generate_valid_test_configs(
439
backend: str,
440
ep_size: int,
@@ -520,6 +572,17 @@ def is_valid_config(config: MoETestConfig) -> tuple[bool, str | None]:
572
f"{config.backend} does not support quantization={config.quantization}",
573
)
574
575
+ if (
576
+ on_gfx950()
577
+ and config.backend == "deepep_low_latency"
578
+ and config.quantization == "modelopt_fp4"
579
+ ):
580
+ return (
581
+ False,
582
+ "DeepEP low latency requires a batched NVFP4 MoE backend, "
583
+ "which is not available on gfx950.",
584
+ )
585
+
586
if config.backend in MORI_BACKENDS:
587
if os.environ.get("VLLM_TEST_ENABLE_MORI_MOE_LAYER") != "1":
588
return False, "mori MoE layer matrix is opt-in"
@@ -1719,6 +1782,7 @@ def _parallel_worker(
1782
test_configs: list[MoETestConfig],
1783
verbosity: int,
1784
failure_report_path: str | None = None,
1785
+ deep_ep_handle_keepalive: list[object] | None = None,
1786
**kwargs,
1787
) -> None:
1788
set_random_seed(7)
@@ -1776,15 +1840,30 @@ def _parallel_worker(
1840
finally:
1841
# DeepEP managers are not reliably reusable across many subtests in
1842
# a single worker process. Tear them down after each DeepEP case so
1779
- # later subtests do not inherit stale communication state.
1780
- if test_config.backend in {
1781
- "deepep_low_latency",
1782
- "deepep_high_throughput",
1783
- }:
1843
+ # later subtests do not inherit stale communication state. Skip this
1844
+ # on ROCm: rocSHMEM cannot reinitialize the allocator after a DeepEP
1845
+ # buffer is destroyed in the same process.
1846
+ if current_platform.is_cuda() and test_config.backend in DEEPEP_BACKENDS:
1847
torch.accelerator.synchronize()
1848
all2all_manager = get_ep_group().device_communicator.all2all_manager
1849
if all2all_manager is not None:
1850
all2all_manager.destroy()
1851
+ elif (
1852
+ deep_ep_handle_keepalive is not None
1853
+ and current_platform.is_rocm()
1854
+ and test_config.backend in DEEPEP_BACKENDS
1855
+ ):
1856
+ # The manager cache is weak. Keep its handle alive until the
1857
+ # launcher hard-exits this ROCm worker, otherwise each subtest
1858
+ # implicitly destroys and recreates the DeepEP buffer.
1859
+ all2all_manager = get_ep_group().device_communicator.all2all_manager
1860
+ if all2all_manager is not None:
1861
+ handle_cache = getattr(all2all_manager, "handle_cache", None)
1862
+ if handle_cache is not None and not deep_ep_handle_keepalive:
1863
+ with handle_cache._lock:
1864
+ cached_handles = list(handle_cache._cache.values())
1865
+ if cached_handles:
1866
+ deep_ep_handle_keepalive.extend(cached_handles)
1867
total = total + 1
1868
torch.distributed.barrier()
1869
@@ -1845,12 +1924,60 @@ def _parallel_worker(
1924
f"{failure_details_str}\n{report}"
1925
)
1926
if is_logging_rank and failure_report_path is not None:
1848
- with open(failure_report_path, "w", encoding="utf-8") as report_file:
1927
+ with open(failure_report_path, "a", encoding="utf-8") as report_file:
1928
report_file.write(failure_report)
1929
if is_logging_rank:
1930
raise RuntimeError(failure_report)
1931
1932
1933
+def _parallel_worker_rocm_deepep(
1934
+ pgi: ProcessGroupInfo,
1935
+ vllm_config: VllmConfig,
1936
+ cpu_group,
1937
+ test_configs: list[MoETestConfig],
1938
+ verbosity: int,
1939
+ failure_report_path: str | None = None,
1940
+ deep_ep_handle_keepalive: list[object] | None = None,
1941
+ **kwargs,
1942
+) -> None:
1943
+ """Run a ROCm DeepEP batch without unsafe Python/HIP teardown."""
1944
+ assert current_platform.is_rocm()
1945
+ assert deep_ep_handle_keepalive is not None
1946
+
1947
+ exit_code = 1
1948
+ try:
1949
+ _parallel_worker(
1950
+ pgi,
1951
+ vllm_config,
1952
+ cpu_group,
1953
+ test_configs,
1954
+ verbosity,
1955
+ failure_report_path=failure_report_path,
1956
+ deep_ep_handle_keepalive=deep_ep_handle_keepalive,
1957
+ **kwargs,
1958
+ )
1959
+ exit_code = 0
1960
+ except BaseException as ex:
1961
+ print(ex)
1962
+ traceback.print_exc()
1963
+ finally:
1964
+ try:
1965
+ torch.accelerator.synchronize()
1966
+ # Do not run vLLM cleanup: it explicitly destroys the DeepEP
1967
+ # buffer. Destroy only the default group, matching the accepted
1968
+ # ROCm workaround in tests/kernels/moe/parallel_utils.py.
1969
+ torch.distributed.destroy_process_group()
1970
+ except BaseException:
1971
+ traceback.print_exc()
1972
+ exit_code = 1
1973
+ finally:
1974
+ # Bypass the HIP atexit use-after-free fixed upstream by
1975
+ # https://github.com/ROCm/rocm-systems/pull/6942.
1976
+ sys.stdout.flush()
1977
+ sys.stderr.flush()
1978
+ os._exit(exit_code)
1979
+
1980
+
1981
# TODO: add cudagraphs/torch.compile tests
1982
@pytest.mark.parametrize("dp_size, tp_size, use_ep", PARALLEL_COMBOS)
1983
@pytest.mark.parametrize("backend", BACKENDS)
@@ -1909,6 +2036,12 @@ def test_moe_layer(
2036
# moe_backend=flashinfer_trtllm / flashinfer_cutlass / flashinfer_cutedsl
2037
# (BF16, FP8 and NVFP4 paths), and VLLM_USE_FLASHINFER_MOE_INT4=1.
2038
2039
+ # Repeated NIXL memory registration in this broad layer matrix fails on
2040
+ # gfx950. NIXL EPLB is covered by dedicated tests, so use Gloo here to
2041
+ # preserve the layer/EPLB coverage.
2042
+ eplb_config = EPLBConfig(
2043
+ communicator="torch_gloo" if enable_eplb and on_gfx950() else None
2044
+ )
2045
parallel_config = ParallelConfig(
2046
pipeline_parallel_size=1,
2047
data_parallel_size=dp_size,
@@ -1916,6 +2049,7 @@ def test_moe_layer(
2049
enable_expert_parallel=use_ep,
2050
all2all_backend=backend,
2051
enable_eplb=enable_eplb,
2052
+ eplb_config=eplb_config,
2053
)
2054
2055
compilation_config = CompilationConfig()
@@ -1955,19 +2089,42 @@ def test_moe_layer(
2089
) as failure_report_file:
2090
failure_report_path = failure_report_file.name
2091
2092
+ test_config_batches = [test_configs]
2093
+ if current_platform.is_rocm() and backend == "deepep_low_latency":
2094
+ # rocSHMEM cannot destroy one low-latency buffer and initialize a
2095
+ # differently sized one in the same process. Use one worker lifetime
2096
+ # per compatible buffer shape while preserving every matrix case.
2097
+ test_config_batches = _group_deepep_ll_configs(test_configs)
2098
+
2099
+ rocm_deepep = current_platform.is_rocm() and backend in DEEPEP_BACKENDS
2100
+ parallel_worker = _parallel_worker_rocm_deepep if rocm_deepep else _parallel_worker
2101
+ launch_failures: list[str] = []
2102
+
2103
try:
1959
- parallel_launch_with_config(
1960
- world_size,
1961
- _parallel_worker,
1962
- vllm_config,
1963
- None,
1964
- test_configs,
1965
- verbosity,
1966
- failure_report_path=failure_report_path,
1967
- )
2104
+ for test_config_batch in test_config_batches:
2105
+ report_size_before = os.path.getsize(failure_report_path)
2106
+ try:
2107
+ parallel_launch_with_config(
2108
+ world_size,
2109
+ parallel_worker,
2110
+ vllm_config,
2111
+ None,
2112
+ test_config_batch,
2113
+ verbosity,
2114
+ failure_report_path=failure_report_path,
2115
+ deep_ep_handle_keepalive=[] if rocm_deepep else None,
2116
+ )
2117
+ except Exception as ex:
2118
+ # Normal subtest failures are already in the shared report.
2119
+ # Preserve launcher/setup errors that occur before that write.
2120
+ if os.path.getsize(failure_report_path) == report_size_before:
2121
+ launch_failures.append(str(ex))
2122
+
2123
if os.path.getsize(failure_report_path) > 0:
2124
with open(failure_report_path, encoding="utf-8") as report_file:
1970
- pytest.fail(report_file.read())
2125
+ launch_failures.insert(0, report_file.read())
2126
+ if launch_failures:
2127
+ pytest.fail("\n\n".join(launch_failures))
2128
finally:
2129
with suppress(FileNotFoundError):
2130
os.remove(failure_report_path)