[ROCm][CI][AITER Coverage] Harden MoE sorting-backend/dispatch env-var test matrix (#58393)

Signed-off-by: Divakar Verma <divakar.verma@amd.com>

Divakar Verma committed Sep 24, 2026 at 22:14 UTC 65c5e2468949410a0328f61fc941bbb8ee80fd04
4 files changed +355 -12
tests/kernels/moe/test_modular_kernel_combinations.py
+273 -7
@@ -2,20 +2,24 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import copy
5 +import inspect
6 import textwrap
7 import traceback
8 from itertools import product
9 from typing import Any
10 +from unittest import mock
11
12 import pytest
13 import torch
14
15 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
16 +from vllm._aiter_ops import rocm_aiter_ops
17 from vllm.config import VllmConfig, set_current_vllm_config
18 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
19 +from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import AiterExperts
20 from vllm.platforms import current_platform
21 from vllm.utils.flashinfer import has_flashinfer_cutlass_fused_moe
18 -from vllm.utils.import_utils import has_deep_ep, has_deep_gemm
22 +from vllm.utils.import_utils import has_aiter, has_deep_ep, has_deep_gemm
23 from vllm.utils.torch_utils import set_random_seed
24 from vllm.v1.worker.workspace import init_workspace_manager
25
@@ -72,7 +76,7 @@ def assert_aiter_quant_scheme_case(config: Config) -> None:
76 indirectly through the general quant-config sweep.
77 See https://github.com/vllm-project/vllm/issues/54966."""
78 fe_cls = config.fused_experts_type
75 - if getattr(fe_cls, "__name__", "") != "AiterExperts":
79 + if fe_cls is not AiterExperts:
80 return
81
82 if config.quant_config is None:
@@ -93,7 +97,7 @@ def assert_aiter_activation_case(config: Config) -> None:
97 AiterExperts being reached only indirectly through the general
98 activation sweep. See https://github.com/vllm-project/vllm/issues/54966."""
99 fe_cls = config.fused_experts_type
96 - if getattr(fe_cls, "__name__", "") != "AiterExperts":
100 + if fe_cls is not AiterExperts:
101 return
102
103 assert fe_cls._supports_activation(config.activation), (
@@ -148,7 +152,7 @@ def rank_worker(
152 if (
153 topk == 1
154 and config.supports_apply_weight_on_input()
151 - and getattr(config.fused_experts_type, "__name__", "") == "AiterExperts"
155 + and config.fused_experts_type is AiterExperts
156 and config.quant_block_shape is not None
157 ):
158 print(
@@ -164,7 +168,7 @@ def rank_worker(
168 # https://github.com/vllm-project/vllm/issues/57029
169 if (
170 config.world_size > 1
167 - and getattr(config.fused_experts_type, "__name__", "") == "AiterExperts"
171 + and config.fused_experts_type is AiterExperts
172 and getattr(config.prepare_finalize_type, "__name__", "")
173 in ("DeepEPHTPrepareAndFinalize", "MoriPrepareAndFinalize")
174 ):
@@ -202,7 +206,7 @@ def rank_worker(
206
207 is_aiter_fp8 = (
208 _cp.is_rocm()
205 - and getattr(config.fused_experts_type, "__name__", "") == "AiterExperts"
209 + and config.fused_experts_type is AiterExperts
210 and config.quant_config is not None
211 )
212 if is_aiter_fp8:
@@ -262,7 +266,7 @@ def is_nyi_config(config: Config) -> bool:
266 return True
267
268 if config.activation != MoEActivation.SILU:
265 - if getattr(config.fused_experts_type, "__name__", "") != "AiterExperts":
269 + if config.fused_experts_type is not AiterExperts:
270 return True # AITER-only for this axis, for now
271 if config.quant_dtype is not None:
272 return True # unquantized-only for this axis, for now
@@ -425,6 +429,268 @@ def test_modular_kernel_combinations_singlegpu(
429 run(config, verbosity > 0)
430
431
432 +# AITER sorting-backend dispatch env-var matrix (issue #54966 step 3) -------
433 +#
434 +# AITER_USE_CK_MOE_SORTING / AITER_USE_FLYDSL_MOE_SORTING are aiter globals
435 +# read once at import time, so each combo needs a fresh child process (see
436 +# parallel_launch_with_config) rather than in-process monkeypatching.
437 +#
438 +# AITER_MOE_SORT_BACKEND is not covered: it only matters when output_aux=True,
439 +# which vLLM only sets on the MXFP4 path -- not yet wired into AiterExperts.
440 +
441 +
442 +def _aiter_sort_backend_hooks_available() -> bool:
443 + """Whether the installed aiter build exposes the sort-backend hooks that
444 + AITER_USE_CK_MOE_SORTING / AITER_USE_FLYDSL_MOE_SORTING select between.
445 + """
446 + try:
447 + import aiter.fused_moe as aiter_fused_moe
448 + except ImportError:
449 + return False
450 +
451 + required = (
452 + "_moe_sorting_impl",
453 + "_flydsl_moe_sorting",
454 + "_USE_CK_MOE_SORTING",
455 + "_USE_FLYDSL_MOE_SORTING",
456 + )
457 + return all(hasattr(aiter_fused_moe, name) for name in required)
458 +
459 +
460 +# Deliberately does NOT call _aiter_sort_backend_hooks_available() here: this
461 +# marker is evaluated at module-import time, which also happens inside each
462 +# spawned child (to unpickle its worker) -- before that case's env vars are
463 +# applied, permanently freezing aiter's sort-backend globals. The hooks check
464 +# instead runs inside the test body below (parent process only).
465 +require_aiter_moe = pytest.mark.skipif(
466 + not (
467 + current_platform.is_rocm()
468 + and has_aiter()
469 + and rocm_aiter_ops.is_fused_moe_enabled()
470 + ),
471 + reason=(
472 + "AITER MoE sorting-backend dispatch needs ROCm + AITER, with "
473 + "VLLM_ROCM_USE_AITER=1 and VLLM_ROCM_USE_AITER_MOE=1 set before "
474 + "the test process starts."
475 + ),
476 +)
477 +
478 +# (AITER_USE_CK_MOE_SORTING, AITER_USE_FLYDSL_MOE_SORTING, expected_backend)
479 +AITER_SORTING_BACKEND_ENV_CASES = [
480 + (1, 0, "ck"),
481 + (1, 1, "ck"), # CK wins over FlyDSL even when both are requested.
482 + (0, 1, "flydsl"),
483 + (0, 0, "opus"), # default when neither flag is set.
484 +]
485 +
486 +
487 +def _aiter_sorting_backend_worker(
488 + pgi: ProcessGroupInfo,
489 + vllm_config: VllmConfig,
490 + cpu_group,
491 + config: Config,
492 + weights: WeightTensors,
493 + verbose: bool,
494 + ck: int,
495 + flydsl: int,
496 + expected_backend: str,
497 +):
498 + # Imported lazily in the spawned child, after env vars are applied, so
499 + # aiter's import-time globals pick up this case's env.
500 + import aiter.fused_moe as aiter_fused_moe
501 +
502 + # Assert the env vars actually stuck in aiter's import-time globals,
503 + # rather than inferring it indirectly from which sorting function fires.
504 + assert bool(ck) == aiter_fused_moe._USE_CK_MOE_SORTING, (
505 + f"aiter.fused_moe._USE_CK_MOE_SORTING={aiter_fused_moe._USE_CK_MOE_SORTING} "
506 + f"but AITER_USE_CK_MOE_SORTING={ck} was set before this process started -- "
507 + "the env var never reached aiter's import-time globals."
508 + )
509 + assert bool(flydsl) == aiter_fused_moe._USE_FLYDSL_MOE_SORTING, (
510 + "aiter.fused_moe._USE_FLYDSL_MOE_SORTING="
511 + f"{aiter_fused_moe._USE_FLYDSL_MOE_SORTING} but AITER_USE_FLYDSL_MOE_SORTING="
512 + f"{flydsl} was set before this process started -- the env var never "
513 + "reached aiter's import-time globals."
514 + )
515 +
516 + # Captured before patching, since the patched attribute would resolve to
517 + # the mock's own (*args, **kwargs) signature instead of the real one.
518 + original_moe_sorting_impl = aiter_fused_moe._moe_sorting_impl
519 +
520 + with (
521 + mock.patch.object(
522 + aiter_fused_moe,
523 + "_moe_sorting_impl",
524 + wraps=original_moe_sorting_impl,
525 + ) as sorting_impl_mock,
526 + mock.patch.object(
527 + aiter_fused_moe,
528 + "_flydsl_moe_sorting",
529 + wraps=aiter_fused_moe._flydsl_moe_sorting,
530 + ) as flydsl_mock,
531 + ):
532 + # Reuses the real correctness/accuracy checking rank_worker already
533 + # does for every other AiterExperts combo, instead of duplicating it.
534 + rank_worker(pgi, vllm_config, cpu_group, config, weights, verbose)
535 +
536 + if expected_backend == "flydsl":
537 + assert flydsl_mock.call_count > 0, "Expected FlyDSL sorting to fire."
538 + assert sorting_impl_mock.call_count == 0, (
539 + "FlyDSL was requested and eligible, but the opus/CK sorting "
540 + "path fired instead -- a silent fallback."
541 + )
542 + else:
543 + assert flydsl_mock.call_count == 0, (
544 + f"Expected the '{expected_backend}' sorting path, but FlyDSL "
545 + "fired instead."
546 + )
547 + assert sorting_impl_mock.call_count > 0, (
548 + f"Expected the '{expected_backend}' sorting path to fire."
549 + )
550 + # Bind by signature so this works regardless of whether use_opus
551 + # is passed positionally or by keyword.
552 + call = sorting_impl_mock.call_args
553 + bound = inspect.signature(original_moe_sorting_impl).bind(
554 + *call.args, **call.kwargs
555 + )
556 + use_opus = bound.arguments["use_opus"]
557 + assert use_opus == (expected_backend == "opus"), (
558 + f"Expected use_opus={expected_backend == 'opus'} for the "
559 + f"'{expected_backend}' sorting path, got use_opus={use_opus}."
560 + )
561 +
562 +
563 +@require_aiter_moe
564 +@pytest.mark.parametrize("ck,flydsl,expected_backend", AITER_SORTING_BACKEND_ENV_CASES)
565 +def test_aiter_moe_sorting_backend_dispatch_env_matrix(
566 + ck: int, flydsl: int, expected_backend: str
567 +):
568 + """See https://github.com/vllm-project/vllm/issues/54966 ("Test AITER
569 + backend and dispatch settings")."""
570 + if not _aiter_sort_backend_hooks_available():
571 + pytest.skip(
572 + "Installed aiter build predates the sort-backend hooks "
573 + "(_moe_sorting_impl, _flydsl_moe_sorting, _USE_CK_MOE_SORTING, "
574 + "_USE_FLYDSL_MOE_SORTING)."
575 + )
576 +
577 + from vllm.model_executor.layers.fused_moe.prepare_finalize import (
578 + MoEPrepareAndFinalizeNoDPEPModular,
579 + )
580 +
581 + config = Config(
582 + Ms=[32],
583 + K=2048,
584 + N=1024,
585 + E=32,
586 + topks=[4],
587 + dtype=torch.bfloat16,
588 + quant_config=None,
589 + prepare_finalize_type=MoEPrepareAndFinalizeNoDPEPModular,
590 + fused_experts_type=AiterExperts,
591 + world_size=1,
592 + )
593 + assert config.is_valid()[0]
594 +
595 + weights = WeightTensors.make(config)
596 + vllm_config, env_dict = config.make_env_data()
597 + env_dict = {
598 + **env_dict,
599 + "AITER_USE_CK_MOE_SORTING": str(ck),
600 + "AITER_USE_FLYDSL_MOE_SORTING": str(flydsl),
601 + }
602 +
603 + parallel_launch_with_config(
604 + config.world_size,
605 + _aiter_sorting_backend_worker,
606 + vllm_config,
607 + env_dict,
608 + config,
609 + weights,
610 + False,
611 + ck,
612 + flydsl,
613 + expected_backend,
614 + )
615 +
616 +
617 +def _aiter_dispatch_policy_worker(
618 + pgi: ProcessGroupInfo,
619 + vllm_config: VllmConfig,
620 + cpu_group,
621 + config: Config,
622 + weights: WeightTensors,
623 + verbose: bool,
624 + dispatch_policy: int,
625 +):
626 + # Drives the real AiterExperts.apply() -> rocm_aiter_fused_experts() call
627 + # through the modular kernel, so it also catches a regression in apply()'s
628 + # own forwarding (unlike test_rocm_aiter_moe.py's version, which supplies
629 + # the policy value directly to rocm_aiter_fused_experts()).
630 + with mock.patch.object(
631 + rocm_aiter_ops, "fused_moe", wraps=rocm_aiter_ops.fused_moe
632 + ) as fused_moe_mock:
633 + rank_worker(pgi, vllm_config, cpu_group, config, weights, verbose)
634 +
635 + assert fused_moe_mock.call_count > 0, (
636 + "Expected AiterExperts.apply() to call rocm_aiter_ops.fused_moe."
637 + )
638 + for call in fused_moe_mock.call_args_list:
639 + forwarded = call.kwargs["moe_sorting_dispatch_policy"]
640 + assert forwarded == dispatch_policy, (
641 + f"AiterExperts.apply() forwarded moe_sorting_dispatch_policy="
642 + f"{forwarded}, but VLLM_ROCM_AITER_MOE_DISPATCH_POLICY="
643 + f"{dispatch_policy} was set before this process started."
644 + )
645 +
646 +
647 +@require_aiter_moe
648 +@pytest.mark.parametrize("dispatch_policy", [0, 1, 2])
649 +def test_aiter_moe_dispatch_policy_forwarded_through_apply(dispatch_policy: int):
650 + """See https://github.com/vllm-project/vllm/issues/54966 ("Test AITER
651 + backend and dispatch settings").
652 +
653 + Complements test_aiter_moe_dispatch_policy_forwarded_to_fused_moe in
654 + test_rocm_aiter_moe.py by driving the real AiterExperts.apply() call
655 + instead of supplying moe_sorting_dispatch_policy directly.
656 + """
657 + from vllm.model_executor.layers.fused_moe.prepare_finalize import (
658 + MoEPrepareAndFinalizeNoDPEPModular,
659 + )
660 +
661 + config = Config(
662 + Ms=[32],
663 + K=2048,
664 + N=1024,
665 + E=32,
666 + topks=[4],
667 + dtype=torch.bfloat16,
668 + quant_config=None,
669 + prepare_finalize_type=MoEPrepareAndFinalizeNoDPEPModular,
670 + fused_experts_type=AiterExperts,
671 + world_size=1,
672 + )
673 + assert config.is_valid()[0]
674 +
675 + weights = WeightTensors.make(config)
676 + vllm_config, env_dict = config.make_env_data()
677 + env_dict = {
678 + **env_dict,
679 + "VLLM_ROCM_AITER_MOE_DISPATCH_POLICY": str(dispatch_policy),
680 + }
681 +
682 + parallel_launch_with_config(
683 + config.world_size,
684 + _aiter_dispatch_policy_worker,
685 + vllm_config,
686 + env_dict,
687 + config,
688 + weights,
689 + False,
690 + dispatch_policy,
691 + )
692 +
693 +
694 if __name__ == "__main__":
695 # Ability to test individual PrepareAndFinalize and FusedExperts combination
696 from .modular_kernel_tools.cli_args import make_config, make_config_arg_parser
tests/kernels/moe/test_rocm_aiter_moe.py
+71
@@ -22,6 +22,7 @@ import importlib
22 import math
23 import warnings
24 from typing import Any, NamedTuple
25 +from unittest import mock
26
27 import pytest
28 import torch
@@ -645,6 +646,76 @@ def test_aiter_moe_padding_env_var(
646 assert envs.VLLM_ROCM_MOE_PADDING is moe_padding
647
648
649 +# Dispatch-policy forwarding test ------------------------------------------
650 +
651 +
652 +@pytest.mark.parametrize("dispatch_policy", [0, 1, 2])
653 +def test_aiter_moe_dispatch_policy_forwarded_to_fused_moe(
654 + dispatch_policy: int,
655 + monkeypatch: pytest.MonkeyPatch,
656 +):
657 + """VLLM_ROCM_AITER_MOE_DISPATCH_POLICY should reach rocm_aiter_ops.fused_moe
658 + unchanged, the same forwarding AiterExperts.apply does via
659 + rocm_aiter_fused_experts, for every documented policy value (0=auto,
660 + 1=always single-pass, 2=always multi-pass). See vllm-project/vllm#54966.
661 + """
662 + from tests.kernels.moe.utils import make_dummy_moe_config
663 + from vllm._aiter_ops import rocm_aiter_ops
664 + from vllm.model_executor.layers.fused_moe.activation import MoEActivation
665 + from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import (
666 + rocm_aiter_fused_experts,
667 + )
668 +
669 + _assert_aiter_supported()
670 + case = _make_moe_case(
671 + num_tokens=8,
672 + hidden_dim=256,
673 + intermediate_dim=512,
674 + num_experts=4,
675 + topk=2,
676 + seed=5,
677 + )
678 + w1_shuffled, w2_shuffled = _shuffle_moe_weights(case["w1"], case["w2"])
679 + moe_config = make_dummy_moe_config(
680 + num_experts=4,
681 + experts_per_token=2,
682 + hidden_dim=256,
683 + intermediate_size=512,
684 + in_dtype=torch.bfloat16,
685 + )
686 +
687 + with monkeypatch.context() as mp:
688 + mp.setenv("VLLM_ROCM_AITER_MOE_DISPATCH_POLICY", str(dispatch_policy))
689 + _reload_envs()
690 + rocm_aiter_ops.refresh_env_variables()
691 +
692 + assert rocm_aiter_ops.get_moe_dispatch_policy() == dispatch_policy, (
693 + "rocm_aiter_ops cached a stale dispatch policy after the env var changed."
694 + )
695 +
696 + with mock.patch.object(
697 + rocm_aiter_ops, "fused_moe", wraps=rocm_aiter_ops.fused_moe
698 + ) as fused_moe_mock:
699 + rocm_aiter_fused_experts(
700 + hidden_states=case["hidden_states"],
701 + w1=w1_shuffled,
702 + w2=w2_shuffled,
703 + topk_weights=case["topk_weights"],
704 + topk_ids=case["topk_ids"],
705 + moe_config=moe_config,
706 + activation=MoEActivation.SILU,
707 + moe_sorting_dispatch_policy=rocm_aiter_ops.get_moe_dispatch_policy(),
708 + )
709 +
710 + assert (
711 + fused_moe_mock.call_args.kwargs["moe_sorting_dispatch_policy"]
712 + == dispatch_policy
713 + ), (
714 + "VLLM_ROCM_AITER_MOE_DISPATCH_POLICY was not forwarded to "
715 + "rocm_aiter_ops.fused_moe."
716 + )
717 +
718 +
719 # Enum tests --------------------------------------------------------------
720
721
vllm/_aiter_ops.py
+1
@@ -2019,6 +2019,7 @@ class rocm_aiter_ops:
2019 cls._MOE_SITUV2 = envs.VLLM_ROCM_USE_AITER_MOE_SITUV2
2020 _sync_aiter_situv2_moe_env()
2021 cls._TRITON_UNQUANT_GEMM = envs.VLLM_ROCM_USE_AITER_TRITON_GEMM
2022 + cls._MOE_DISPATCH_POLICY = envs.VLLM_ROCM_AITER_MOE_DISPATCH_POLICY
2023
2024 @staticmethod
2025 def get_aiter_activation_type(activation_str: str) -> "ActivationType | None":
vllm/model_executor/layers/fused_moe/fused_flydsl_moe.py
+10 -5
@@ -7,11 +7,6 @@ import json
7 import os
8
9 import torch
10 -from aiter.fused_moe import moe_sorting as aiter_moe_sorting
11 -from aiter.ops.flydsl.kernels.moe_2stage_a16wmix import (
12 - flydsl_a16w4_gemm1,
13 - flydsl_a16w4_gemm2,
14 -)
10
11 from vllm.logger import init_logger
12 from vllm.utils.platform_utils import get_device_name_as_file_name
@@ -81,6 +76,10 @@ def moe_sorting(
76 model_dim: int,
77 block_m: int,
78 ):
79 + # Imported lazily so aiter's import-time sort-backend env vars are read
80 + # when this is called, not whenever this module happens to be imported.
81 + from aiter.fused_moe import moe_sorting as aiter_moe_sorting
82 +
83 topk_ids_i32 = topk_ids.to(torch.int32)
84 topk_w_f32 = topk_weights.to(torch.float32)
85 sorted_ids, sorted_w, sorted_expert_ids, num_valid_ids, _moe_buf = (
@@ -185,6 +184,12 @@ def fused_flydsl_moe_impl(
184 tile_n2: int | None = None,
185 tile_k2: int | None = None,
186 ) -> torch.Tensor:
187 + # Imported lazily -- see the comment in moe_sorting() above.
188 + from aiter.ops.flydsl.kernels.moe_2stage_a16wmix import (
189 + flydsl_a16w4_gemm1,
190 + flydsl_a16w4_gemm2,
191 + )
192 +
193 device = hidden_states.device
194 tokens = hidden_states.shape[0]
195 model_dim = hidden_states.shape[1]