|
7 | 7 |
|
8 | 8 | import functools |
9 | 9 | import os |
| 10 | +import sys |
10 | 11 | import tempfile |
11 | 12 | import traceback |
12 | 13 | import types |
13 | 14 | from collections.abc import Callable |
14 | 15 | from contextlib import suppress |
15 | | -from dataclasses import astuple, dataclass, fields |
| 16 | +from dataclasses import astuple, dataclass, fields, replace |
16 | 17 | from itertools import product |
17 | 18 | from typing import get_args |
18 | 19 |
|
|
28 | 29 | from tests.kernels.moe.utils import TestMLP, make_test_weights, moe_quantize_weights |
29 | 30 | from vllm.config import ( |
30 | 31 | CompilationConfig, |
| 32 | + EPLBConfig, |
31 | 33 | ParallelConfig, |
32 | 34 | SchedulerConfig, |
33 | 35 | VllmConfig, |
@@ -383,6 +385,56 @@ def convert(v: str, ty): |
383 | 385 | return MoETestConfig(*values) |
384 | 386 |
|
385 | 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 | + |
386 | 438 | def generate_valid_test_configs( |
387 | 439 | backend: str, |
388 | 440 | ep_size: int, |
@@ -520,6 +572,17 @@ def is_valid_config(config: MoETestConfig) -> tuple[bool, str | None]: |
520 | 572 | f"{config.backend} does not support quantization={config.quantization}", |
521 | 573 | ) |
522 | 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 | + |
523 | 586 | if config.backend in MORI_BACKENDS: |
524 | 587 | if os.environ.get("VLLM_TEST_ENABLE_MORI_MOE_LAYER") != "1": |
525 | 588 | return False, "mori MoE layer matrix is opt-in" |
@@ -1719,6 +1782,7 @@ def _parallel_worker( |
1719 | 1782 | test_configs: list[MoETestConfig], |
1720 | 1783 | verbosity: int, |
1721 | 1784 | failure_report_path: str | None = None, |
| 1785 | + deep_ep_handle_keepalive: list[object] | None = None, |
1722 | 1786 | **kwargs, |
1723 | 1787 | ) -> None: |
1724 | 1788 | set_random_seed(7) |
@@ -1776,15 +1840,30 @@ def _parallel_worker( |
1776 | 1840 | finally: |
1777 | 1841 | # DeepEP managers are not reliably reusable across many subtests in |
1778 | 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: |
1784 | 1847 | torch.accelerator.synchronize() |
1785 | 1848 | all2all_manager = get_ep_group().device_communicator.all2all_manager |
1786 | 1849 | if all2all_manager is not None: |
1787 | 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) |
1788 | 1867 | total = total + 1 |
1789 | 1868 | torch.distributed.barrier() |
1790 | 1869 |
|
@@ -1845,12 +1924,60 @@ def _parallel_worker( |
1845 | 1924 | f"{failure_details_str}\n{report}" |
1846 | 1925 | ) |
1847 | 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: |
1849 | 1928 | report_file.write(failure_report) |
1850 | 1929 | if is_logging_rank: |
1851 | 1930 | raise RuntimeError(failure_report) |
1852 | 1931 |
|
1853 | 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 | + |
1854 | 1981 | # TODO: add cudagraphs/torch.compile tests |
1855 | 1982 | @pytest.mark.parametrize("dp_size, tp_size, use_ep", PARALLEL_COMBOS) |
1856 | 1983 | @pytest.mark.parametrize("backend", BACKENDS) |
@@ -1909,13 +2036,20 @@ def test_moe_layer( |
1909 | 2036 | # moe_backend=flashinfer_trtllm / flashinfer_cutlass / flashinfer_cutedsl |
1910 | 2037 | # (BF16, FP8 and NVFP4 paths), and VLLM_USE_FLASHINFER_MOE_INT4=1. |
1911 | 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 | + ) |
1912 | 2045 | parallel_config = ParallelConfig( |
1913 | 2046 | pipeline_parallel_size=1, |
1914 | 2047 | data_parallel_size=dp_size, |
1915 | 2048 | tensor_parallel_size=tp_size, |
1916 | 2049 | enable_expert_parallel=use_ep, |
1917 | 2050 | all2all_backend=backend, |
1918 | 2051 | enable_eplb=enable_eplb, |
| 2052 | + eplb_config=eplb_config, |
1919 | 2053 | ) |
1920 | 2054 |
|
1921 | 2055 | compilation_config = CompilationConfig() |
@@ -1955,19 +2089,42 @@ def test_moe_layer( |
1955 | 2089 | ) as failure_report_file: |
1956 | 2090 | failure_report_path = failure_report_file.name |
1957 | 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 | + |
1958 | 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 | + |
1968 | 2123 | if os.path.getsize(failure_report_path) > 0: |
1969 | 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)) |
1971 | 2128 | finally: |
1972 | 2129 | with suppress(FileNotFoundError): |
1973 | 2130 | os.remove(failure_report_path) |
|
0 commit comments