[MoE] Add extension points for custom runner backends (#32665)

Co-authored-by: Alex Nails <alex.nails@radixark.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kurt Shuster
2026-08-29 19:03:41 -07:00
committed by GitHub
co-authored by Alex Nails Claude Opus 5
parent ca8ff035c3
commit ed39568e79
12 changed files with 613 additions and 104 deletions
@@ -0,0 +1,378 @@
import sys
from types import SimpleNamespace
import pytest
import torch
from sglang.srt.layers.moe import MoeA2ABackend, MoeRunnerBackend
from sglang.srt.layers.moe.fused_moe_triton import layer as fused_moe_layer_module
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
from sglang.srt.layers.moe.moe_runner import runner as runner_module
from sglang.srt.layers.moe.moe_runner.base import (
DispatchMoeRunnerCore,
MoeQuantInfo,
MoeRunnerConfig,
MoeRunnerCore,
PermuteMethodPool,
RunnerInput,
RunnerOutput,
)
from sglang.srt.layers.moe.token_dispatcher.standard import (
StandardCombineInput,
StandardDispatchOutput,
)
from sglang.srt.layers.moe.topk import StandardTopKOutput
from sglang.srt.layers.moe.utils import (
RegisteredMoeRunnerBackend,
register_moe_runner_backend_name,
resolve_moe_runner_backend,
)
from sglang.srt.layers.quantization.unquant import UnquantizedFusedMoEMethod
from sglang.srt.lora.layers import FusedMoEWithLoRA
from sglang.srt.runtime_context import get_context, get_flags, get_parallel
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=15, suite="base-c-test-cpu")
class _TestDispatchRunnerCore(DispatchMoeRunnerCore):
def __init__(self, config: MoeRunnerConfig, backend):
super().__init__(config)
self._backend = backend
self.calls = []
@property
def runner_backend(self):
return self._backend
def run_from_dispatch(
self,
dispatch_output,
quant_info,
runner_config,
hooks=None,
):
self.calls.append((dispatch_output, quant_info, runner_config, hooks))
return StandardCombineInput(dispatch_output.hidden_states + 1)
@pytest.fixture
def isolated_runner_registries(monkeypatch):
from sglang.srt.layers.moe import utils as moe_utils
monkeypatch.setattr(moe_utils, "_REGISTERED_MOE_RUNNER_BACKEND_NAMES", set())
monkeypatch.setattr(runner_module, "_CUSTOM_RUNNER_CORE_FACTORIES", {})
with get_flags().moe.override(a2a_backend=MoeA2ABackend.NONE):
yield
def test_registered_runner_core_uses_standard_dispatch(
isolated_runner_registries,
) -> None:
backend_name = "test_dispatch_extension"
runner_module.register_moe_runner_core(
backend_name,
lambda config: _TestDispatchRunnerCore(
config, resolve_moe_runner_backend(backend_name)
),
)
backend = resolve_moe_runner_backend(backend_name)
assert isinstance(backend, RegisteredMoeRunnerBackend)
assert backend.value == backend_name
runner = runner_module.MoeRunner(backend, MoeRunnerConfig())
dispatch_output = StandardDispatchOutput(
hidden_states=torch.zeros(1, 2),
hidden_states_scale=None,
topk_output=StandardTopKOutput(
topk_weights=torch.ones(1, 1),
topk_ids=torch.zeros(1, 1, dtype=torch.int64),
router_logits=torch.zeros(1, 1),
),
)
quant_info = MoeQuantInfo()
result = runner.run(dispatch_output, quant_info)
assert torch.equal(result.hidden_states, torch.ones(1, 2))
assert runner.runner_core.calls == [
(dispatch_output, quant_info, runner.config, None)
]
with pytest.raises(ValueError, match="already registered"):
runner_module.register_moe_runner_core(backend_name, _TestDispatchRunnerCore)
def test_runner_core_registration_can_override_builtin_backend(
isolated_runner_registries,
) -> None:
backend = MoeRunnerBackend.FLASHINFER_CUTLASS
runner_module.register_moe_runner_core(
backend.value,
lambda config: _TestDispatchRunnerCore(config, backend),
)
runner = runner_module.MoeRunner(backend, MoeRunnerConfig())
assert isinstance(runner.runner_core, _TestDispatchRunnerCore)
assert runner.runner_core.runner_backend is backend
def test_runner_backend_names_must_be_builtin_or_registered(
isolated_runner_registries,
) -> None:
assert resolve_moe_runner_backend("triton") is MoeRunnerBackend.TRITON
assert MoeRunnerBackend.EXPERIMENTAL_SGL_TRTLLM.is_flashinfer_trtllm()
assert MoeRunnerBackend.EXPERIMENTAL_SGL_MARLIN.is_marlin()
with pytest.raises(ValueError, match="neither built in nor registered"):
resolve_moe_runner_backend("unknown_backend")
with pytest.raises(ValueError, match="must not be empty"):
register_moe_runner_backend_name("")
with pytest.raises(ValueError, match="already built in"):
register_moe_runner_backend_name("triton")
def test_fused_moe_uses_explicit_quant_method_for_full_lifecycle(monkeypatch) -> None:
calls = []
method = UnquantizedFusedMoEMethod()
monkeypatch.setattr(
method, "create_weights", lambda **kwargs: calls.append("weights")
)
def create_runner(layer, config) -> None:
calls.append("runner")
method.runner = SimpleNamespace()
monkeypatch.setattr(method, "create_moe_runner", create_runner)
monkeypatch.setattr(
fused_moe_layer_module,
"create_moe_dispatcher",
lambda config: SimpleNamespace(),
)
with get_context().override_server_args(
model_path="dummy"
), get_flags().moe.override(
runner_backend=MoeRunnerBackend.AUTO,
a2a_backend=MoeA2ABackend.NONE,
), get_parallel().override(
moe_ep_size=1,
moe_ep_rank=0,
moe_tp_size=1,
moe_tp_rank=0,
tp_size=1,
tp_rank=0,
):
layer = FusedMoE(
num_experts=2,
hidden_size=4,
intermediate_size=8,
layer_id=0,
quant_method=method,
)
assert layer.quant_method is method
assert layer.runner is method.runner
assert calls == ["weights", "runner"]
def test_lora_uses_quant_method_contract_for_registered_backend(
monkeypatch, isolated_runner_registries
) -> None:
backend_name = "test_lora_extension"
register_moe_runner_backend_name(backend_name)
backend = resolve_moe_runner_backend(backend_name)
quant_info = object()
quant_calls = []
class FakeQuantMethod:
def get_moe_quant_info(self, layer, runner_backend):
quant_calls.append((layer, runner_backend))
return quant_info
base_layer = FusedMoE.__new__(FusedMoE)
torch.nn.Module.__init__(base_layer)
base_layer.quant_method = FakeQuantMethod()
base_layer.moe_runner_config = MoeRunnerConfig()
base_layer.dispatcher = object()
base_layer.num_local_experts = 2
base_layer.should_fuse_routed_scaling_factor_in_topk = False
base_layer.moe_tp_size = 1
base_layer.moe_tp_rank = 0
base_layer.intermediate_size_per_partition = 8
base_layer.runner = SimpleNamespace(runner_backend=backend)
lora_backend = SimpleNamespace(is_moe_lora=False)
created_runners = []
monkeypatch.setattr(
runner_module,
"MoeRunner",
lambda selected_backend, config, lora_enabled: created_runners.append(
(selected_backend, config, lora_enabled)
)
or object(),
)
wrapper = FusedMoEWithLoRA(base_layer, lora_backend)
assert wrapper._quant_info is quant_info
assert quant_calls == [(base_layer, backend)]
assert created_runners == [(backend, base_layer.moe_runner_config, True)]
class _NonTritonRunnerInput(RunnerInput):
"""Stands in for deep_gemm/aiter/ascend inputs: no ``topk_ids`` field."""
def __init__(self, backend, hidden_states):
self._backend = backend
self.hidden_states = hidden_states
@property
def runner_backend(self):
return self._backend
class _NonTritonRunnerOutput(RunnerOutput):
def __init__(self, backend, hidden_states):
self._backend = backend
self.hidden_states = hidden_states
@property
def runner_backend(self):
return self._backend
class _TestPermuteRunnerCore(MoeRunnerCore):
def __init__(self, config: MoeRunnerConfig, backend):
super().__init__(config)
self._backend = backend
self.hooks_seen = []
@property
def runner_backend(self):
return self._backend
def run(self, runner_input, quant_info, running_state, hooks=None):
self.hooks_seen.append(hooks)
return _NonTritonRunnerOutput(self._backend, runner_input.hidden_states + 1)
def test_non_triton_runner_input_skips_lora_hooks(
monkeypatch, isolated_runner_registries
) -> None:
"""LoRA-disabled runs must not inspect LoRA fields on the runner input.
Every non-Triton backend (deep_gemm, triton_kernels, aiter, ascend, ...)
produces a runner input without ``topk_ids``, so building hooks eagerly
would break each of them.
"""
monkeypatch.setattr(
PermuteMethodPool,
"_pre_permute_methods",
dict(PermuteMethodPool._pre_permute_methods),
)
monkeypatch.setattr(
PermuteMethodPool,
"_post_permute_methods",
dict(PermuteMethodPool._post_permute_methods),
)
backend_name = "test_permute_extension"
runner_module.register_moe_runner_core(
backend_name,
lambda config: _TestPermuteRunnerCore(
config, resolve_moe_runner_backend(backend_name)
),
)
backend = resolve_moe_runner_backend(backend_name)
PermuteMethodPool.register_pre_permute(
"standard",
backend_name,
lambda dispatch_output, quant_info, config, state: _NonTritonRunnerInput(
backend, dispatch_output.hidden_states
),
)
PermuteMethodPool.register_post_permute(
backend_name,
"standard",
lambda runner_output, quant_info, config, state: StandardCombineInput(
runner_output.hidden_states
),
)
runner = runner_module.MoeRunner(backend, MoeRunnerConfig())
dispatch_output = StandardDispatchOutput(
hidden_states=torch.zeros(1, 2),
hidden_states_scale=None,
topk_output=StandardTopKOutput(
topk_weights=torch.ones(1, 1),
topk_ids=torch.zeros(1, 1, dtype=torch.int64),
router_logits=torch.zeros(1, 1),
),
)
result = runner.run(dispatch_output, MoeQuantInfo())
assert torch.equal(result.hidden_states, torch.ones(1, 2))
assert runner.runner_core.hooks_seen == [None]
def test_trtllm_quant_method_defines_runner_after_create_moe_runner() -> None:
"""FusedMoE reads `quant_method.runner` right after `create_moe_runner`, so
a method that never builds a MoeRunner must still define the attribute."""
from sglang.srt.layers.quantization.mxfp4_flashinfer_trtllm_moe import (
Mxfp4FlashinferTrtllmMoEMethod,
)
method = Mxfp4FlashinferTrtllmMoEMethod.__new__(Mxfp4FlashinferTrtllmMoEMethod)
assert not hasattr(method, "runner")
method.create_moe_runner(
SimpleNamespace(num_local_experts=2), MoeRunnerConfig(swiglu_limit=None)
)
assert method.runner is None
def test_fused_moe_layer_runner_is_none_when_method_builds_no_runner(
monkeypatch,
) -> None:
"""The overlap-args helpers key off `runner is not None`, so a layer whose
quant method builds no MoeRunner must fall back instead of raising."""
method = UnquantizedFusedMoEMethod()
monkeypatch.setattr(method, "create_weights", lambda **kwargs: None)
monkeypatch.setattr(method, "create_moe_runner", lambda layer, config: None)
monkeypatch.setattr(
fused_moe_layer_module,
"create_moe_dispatcher",
lambda config: SimpleNamespace(),
)
with get_context().override_server_args(
model_path="dummy"
), get_flags().moe.override(
runner_backend=MoeRunnerBackend.AUTO,
a2a_backend=MoeA2ABackend.NONE,
), get_parallel().override(
moe_ep_size=1,
moe_ep_rank=0,
moe_tp_size=1,
moe_tp_rank=0,
tp_size=1,
tp_rank=0,
):
layer = FusedMoE(
num_experts=2,
hidden_size=4,
intermediate_size=8,
layer_id=0,
quant_method=method,
)
assert layer.runner is None
layer.clear_overlap_args()
assert layer.down_gemm_overlap_args is None
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -65,6 +65,7 @@ def _make_base_layer(quant_method=None) -> types.SimpleNamespace:
moe_tp_size=1,
moe_tp_rank=0,
intermediate_size_per_partition=32,
runner=None,
)
@@ -114,15 +114,18 @@ def _load_lora_weight_to_buffer(pool, **kwargs):
def _load_moe_backend_enum():
tree = ast.parse(MOE_UTILS_PATH.read_text())
backend = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "MoeRunnerBackend"
)
classes = {node.name: node for node in tree.body if isinstance(node, ast.ClassDef)}
backend = classes["MoeRunnerBackend"]
body = [
classes[base.id]
for base in backend.bases
if isinstance(base, ast.Name) and base.id in classes
]
body.append(backend)
namespace = {"Enum": Enum}
exec(
compile(
ast.fix_missing_locations(ast.Module(body=[backend], type_ignores=[])),
ast.fix_missing_locations(ast.Module(body=body, type_ignores=[])),
str(MOE_UTILS_PATH),
"exec",
),