Prhub

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

原始 PR 作者 klshuster 合并时间 2026-08-30 10:03 文件变更 12 提交数 6 评论 10 代码增减 +602 / -93

执行摘要

为自定义 MoE runner 后端增加注册与标准 dispatch 扩展点

PR body 明确说明动机:"Allow out-of-tree MoE backends to use SGLang's standard routing and MoeRunner abstraction without adding private backend names to the public enum." 此前第三方后端必须把自己的名字硬编码进 MoeRunnerBackend 公共枚举并侵入 MoeRunner.__init__ 的 if-elif 分支链,fork 维护成本高、上游合并冲突频繁。本 PR 提供注册表与工厂机制,使扩展方能以非侵入方式接入标准 dispatch 流程,同时顺带清理 LoRA 路径上对枚举身份和 hasattr 鸭子类型的隐式依赖。

值得精读。设计亮点有三:一是谓词 mixin + 注册解析器的组合,让扩展后端与内置后端在类型系统上对齐,调用方无需分支处理;二是 DispatchMoeRunnerCore 抽象把"直接消费 dispatch 输出"的 runner 正式契约化,取代脆弱的 hasattr 检查;三是 get_moe_quant_info 为 LoRA 提供后端无关的量化信息通道。建议结合提交序列阅读——合并前的 5 个修复提交本身就是"真实环境校验"的典型案例(CI 收集失败、AST 基类解析、非 Triton 输入崩溃)。适合需要为 SGLang 接入自研 MoE kernel 或私有量化后端的团队参考。

讨论亮点

核心交锋集中在 _maybe_build_lora_hooks 的输入类型断言上。YAMY1234 在 python/sglang/srt/layers/moe/moe_runner/runner.py:207 指出:"This assertion is reached before the lora_enabled guard, so non-LoRA built-in paths using TritonKernelsRunnerInput or DeepGemmRunnerInput deterministically fail... Could we early-return when LoRA is disabled and preserve the previous generic hidden_states/topk_ids handling?" alexnails 回复 "yeah I can fix and cleanup",并在最终提交 5edece2 中修复:_maybe_build_lora_hooks 开头增加 if not self.lora_enabled or lora_info is None: return None 提前返回,断言仅保留在 LoRA 专属路径上。合并前的提交历史还显示多轮 CI 加固(CPU 收集失败、_load_moe_backend_enum AST 基类解析、context seeding),说明该 PR 在合并窗口内经历了真实运行环境校验。

实现拆解

  1. 后端标识的谓词提取与注册解析python/sglang/srt/layers/moe/utils.py):将 MoeRunnerBackend 枚举上的全部 is_*() 谓词抽到 _MoeRunnerBackendPredicates mixin,谓词判断从枚举身份比较改为按 value 字符串比较;新增 frozen dataclass RegisteredMoeRunnerBackend(同样继承 mixin)、MoeRunnerBackendLike 联合类型、全局 _REGISTERED_MOE_RUNNER_BACKEND_NAMES 集合、register_moe_runner_backend_name()(拒绝空名与内置名冲突)与 resolve_moe_runner_backend()(统一解析内置或注册后端)。initialize_moe_config()get_moe_runner_backend() / get_speculative_moe_runner_backend() 的返回类型同步切换为 MoeRunnerBackendLike,扩展后端自此获得与内置后端一致的 is_*() 判断能力。
  2. runner-core 工厂注册与 dispatch 原生抽象moe_runner/runner.pymoe_runner/base.py):runner.py 新增模块级 _CUSTOM_RUNNER_CORE_FACTORIES 注册表与 register_moe_runner_core(backend_name, factory),注册时若名字非内置会自动调用 register_moe_runner_backend_name() 登记;MoeRunner.__init__ 在既有内置分支之前优先查询自定义工厂,因此扩展既能新增后端,也可显式覆盖内置后端实现。base.py 新增 DispatchMoeRunnerCore 抽象基类,定义 runner_backend 属性与 run_from_dispatch() 接口;MoeRunner.run() 对 dispatch 原生 runner 的判定从 hasattr(runner_core, "run_from_dispatch") 收紧为 isinstance(..., DispatchMoeRunnerCore),契约由鸭子类型升级为显式类型。
  3. 显式量化方法与生命周期收口fused_moe_triton/layer.pybase_config.pymxfp4_flashinfer_trtllm_moe.py):FusedMoE.__init__ 新增 quant_method 显式入参,允许调用方绕过 quant_config 直接注入量化方法;runner 读取从"判断有无 runner 属性"改为无条件 self.runner = self.quant_method.runner,为此 FusedMoEMethodBase 增加 runner: MoeRunner | None = None 类属性兜底,非 FusedMoEMethodBase 子类的 MXFP4FlashInferTRTLLMMethod 也手工补 self.runner = Noneset_overlap_args() / clear_overlap_args()hasattr 检查改为 is not None
  4. LoRA 路径接入统一契约lora/layers.pylora/lora_moe_runner_marlin.py):FusedMoEWithLoRA.__init__ 的后端选择顺序改为优先取 base_layer.runner.runner_backend(保证 base 与 LoRA forward 的 per-format 后端解析一致),未量化 Marlin 回退 Triton 的逻辑保留;非 Marlin/非 Triton 后端不再直接抛 NotImplementedError,而是调用新增的 FusedMoEMethodBase.get_moe_quant_info(layer, runner_backend)(默认仅实现 Triton 分支,其余由量化方法扩展)。MarlinLoraRunnerCore 改为继承 DispatchMoeRunnerCore 并补 runner_backend 属性,正式进入标准 dispatch 通道。
  5. 测试与 CI 适配:新增 test/registered/unit/layers/moe/test_moe_runner_extensions.py(378 行),覆盖扩展后端走标准 dispatch、覆盖内置后端、非法名字校验、显式 quant method 全生命周期、注册后端的 LoRA 契约、非 Triton 输入跳过 LoRA hooks 等场景;test_mem_pool_ep_unit.py_load_moe_backend_enum 改为 AST 加载时同步 exec mixin 基类,test_lora_moe_inplace_unit.py 做小适配;另补 __main__ 入口修复 CPU CI 全套件收集失败。
文件 模块 状态 重要度
python/sglang/srt/layers/moe/utils.py 后端注册 modified 8.15
python/sglang/srt/layers/moe/moe_runner/runner.py 运行器调度 modified 7.18
python/sglang/srt/layers/moe/moe_runner/base.py 运行器基类 modified 7.06
python/sglang/srt/layers/quantization/base_config.py 量化契约 modified 6.01
python/sglang/srt/lora/layers.py LoRA 层 modified 6.01
python/sglang/srt/layers/moe/fused_moe_triton/layer.py MoE 层 modified 5.28
python/sglang/srt/lora/lora_moe_runner_marlin.py Marlin LoRA modified 5.84
test/registered/unit/layers/moe/test_moe_runner_extensions.py 扩展测试 added 7.49
python/sglang/srt/layers/quantization/mxfp4_flashinfer_trtllm_moe.py 量化方法 modified 4.27
python/sglang/srt/layers/moe/moe_runner/__init__.py 导出入口 modified 4.43
test/registered/unit/lora/test_mem_pool_ep_unit.py 内存池测试 modified 4.17
test/registered/unit/lora/test_lora_moe_inplace_unit.py LoRA 测试 modified 2.88

关键符号

register_moe_runner_backend_name resolve_moe_runner_backend register_moe_runner_core get_moe_quant_info run_from_dispatch _maybe_build_lora_hooks get_moe_runner_backend

关键源码片段

python/sglang/srt/layers/moe/utils.py dependency-wiring

本 PR 的核心设计所在:提取 `_MoeRunnerBackendPredicates` 谓词 mixin,新增 `RegisteredMoeRunnerBackend` 与注册 / 解析函数,使扩展后端与内置后端共用同一套 `is_*()` 判断;`initialize_moe_config` 改用 `resolve_moe_runner_backend` 接收入口。

# python/sglang/srt/layers/moe/utils.pyclass _MoeRunnerBackendPredicates:
    """谓词 mixin:以字符串比较代替枚举身份比较。    内置后端(MoeRunnerBackend)与扩展后端(RegisteredMoeRunnerBackend)
    都继承本类,调用方可以用同一套 is_*() 判断任意后端。
    """
    value: str
​
    def is_triton(self) -> bool:
        return self.value == MoeRunnerBackend.TRITON.value
​
    def is_deep_gemm(self) -> bool:
        return self.value == MoeRunnerBackend.DEEP_GEMM.value
​
    def is_marlin(self) -> bool:
        # experimental_sgl_marlin 与 marlin 共享权重重排、量化方法选择与
        # 基础融合路径;发散点(LoRA MoE dispatch)优先检查
        # is_experimental_sgl_marlin(),因此这里按值归并判断。
        return self.value in (
            MoeRunnerBackend.MARLIN.value,
            MoeRunnerBackend.EXPERIMENTAL_SGL_MARLIN.value,
        )
​
    # ... 其余 is_*() 谓词同理,全部改为按 value 字符串比较 ...
​
​
class MoeRunnerBackend(_MoeRunnerBackendPredicates, Enum):
    """内置后端枚举,只存放公共后端名字。"""
​
    AUTO = "auto"
    DEEP_GEMM = "deep_gemm"
    TRITON = "triton"
    TRITON_KERNELS = "triton_kernel"
    MARLIN = "marlin"
    # ... 其余内置成员 ...
​
​
@dataclass(frozen=True)
class RegisteredMoeRunnerBackend(_MoeRunnerBackendPredicates):
    """由 out-of-tree 扩展注册的后端标识,行为与内置枚举一致。"""
​
    value: str
​
​
MoeRunnerBackendLike = MoeRunnerBackend | RegisteredMoeRunnerBackend
_REGISTERED_MOE_RUNNER_BACKEND_NAMES: set[str] = set()
​
​
def register_moe_runner_backend_name(name: str) -> None:
    """注册扩展后端名:拒绝空名,拒绝与内置名冲突。"""
    if not name:
        raise ValueError("MoE runner backend name must not be empty")
    try:
        MoeRunnerBackend(name)
    except ValueError:
        _REGISTERED_MOE_RUNNER_BACKEND_NAMES.add(name)
    else:
        raise ValueError(f"MoE runner backend {name!r} is already built in")
​
​
def resolve_moe_runner_backend(
    backend: str | MoeRunnerBackendLike,
) -> MoeRunnerBackendLike:
    """把字符串或后端对象统一解析为内置或注册后端标识。"""
    if isinstance(backend, (MoeRunnerBackend, RegisteredMoeRunnerBackend)):
        return backend
    try:
        return MoeRunnerBackend(backend)
    except ValueError:
        if backend in _REGISTERED_MOE_RUNNER_BACKEND_NAMES:
            return RegisteredMoeRunnerBackend(backend)
        raise ValueError(
            f"MoE runner backend {backend!r} is neither built in nor registered"
        ) from None
python/sglang/srt/layers/moe/moe_runner/runner.py dependency-wiring

引入 `_CUSTOM_RUNNER_CORE_FACTORIES` 注册表与 `register_moe_runner_core` 工厂注册 API;`MoeRunner.__init__` 让自定义工厂优先于内置分支;`_maybe_build_lora_hooks` 重写为 LoRA 禁用时提前返回,修复非 Triton 后端崩溃(review 核心议题)。

# python/sglang/srt/layers/moe/moe_runner/runner.py_CUSTOM_RUNNER_CORE_FACTORIES: dict[
    str, Callable[[MoeRunnerConfig], DispatchMoeRunnerCore]
] = {}
​
​
def register_moe_runner_core(
    backend_name: str,
    factory: Callable[[MoeRunnerConfig], DispatchMoeRunnerCore],
) -> None:
    """为新增或内置后端名注册 runner-core 工厂。    若名字不是内置后端,则同步登记到后端名注册表,
    保证 resolve_moe_runner_backend 之后能解析它。
    """
    if backend_name in _CUSTOM_RUNNER_CORE_FACTORIES:
        raise ValueError(f"Runner core for {backend_name!r} is already registered")
    try:
        resolve_moe_runner_backend(backend_name)
    except ValueError:
        register_moe_runner_backend_name(backend_name)
    _CUSTOM_RUNNER_CORE_FACTORIES[backend_name] = factory
​
​
class MoeRunner:
    def __init__(self, runner_backend, config, lora_enabled=False):
        # 自定义工厂优先于内置分支:扩展可以新增后端,也可以整体
        # 替换某个内置后端的实现。
        if custom_factory := _CUSTOM_RUNNER_CORE_FACTORIES.get(runner_backend.value):
            self.runner_core = custom_factory(config)
        elif runner_backend.is_triton():
            self.runner_core = TritonRunnerCore(config)
        # ... 其余内置分支不变 ...
​
    def run(self, dispatch_output, quant_info, lora_info=None):
        def _maybe_build_lora_hooks(
            _runner_input: DispatchOutput | TritonRunnerInput,
        ) -> Optional[LoRAHooks]:
            # 提前返回:LoRA 只接在 Triton runner 上,其余后端(deep_gemm、
            # triton_kernels、aiter、ascend 等)的 runner 输入没有 topk_ids
            # 字段,LoRA 禁用时读它会直接崩溃。
            if not self.lora_enabled or lora_info is None:
                return None
​
            from sglang.srt.lora.lora_moe_runners import build_lora_hooks
​
            if isinstance(_runner_input, DispatchOutput):
                hidden_states, topk_ids = (
                    _runner_input.hidden_states,
                    _runner_input.topk_output.topk_ids,
                )
            else:
                # 能走到这里必定是 Triton 路径,LoRA 解析依赖 topk_ids
                assert isinstance(_runner_input, TritonRunnerInput), type(_runner_input)
                hidden_states = _runner_input.hidden_states
                topk_ids = _runner_input.topk_ids
            return build_lora_hooks(hidden_states, lora_info, topk_ids)
python/sglang/srt/layers/quantization/base_config.py dependency-wiring

为 `FusedMoEMethodBase` 增加 `runner` 类属性兜底与后端无关的 `get_moe_quant_info` 契约,是 LoRA runner 获取自定义后端量化信息的统一入口。

# python/sglang/srt/layers/quantization/base_config.pyclass FusedMoEMethodBase(QuantizeMethodBase):
    # 类属性兜底:FusedMoE.__init__ 现在无条件读取 quant_method.runner,
    # 未创建 runner 的量化方法(如仅走 fused path 的 TRT-LLM)得到 None。
    runner: MoeRunner | None = None
​
    def get_triton_quant_info(self, layer: torch.nn.Module) -> TritonMoeQuantInfo:
        """返回描述 layer 上量化状态的 TritonMoeQuantInfo。"""
        raise NotImplementedError(
            f"{type(self).__name__} must implement get_triton_quant_info()"
        )
​
    def get_moe_quant_info(
        self, layer: torch.nn.Module, runner_backend: MoeRunnerBackendLike
    ) -> MoeQuantInfo:
        """后端无关的 quant-info 契约:LoRA runner 按后端类型取量化信息。        默认只认识 Triton 后端,其余后端由量化方法自行扩展;
        这使注册的扩展后端也能驱动 LoRA 路径。
        """
        if runner_backend.is_triton():
            return self.get_triton_quant_info(layer)
        raise NotImplementedError(
            f"{type(self).__name__} does not expose quant info for {runner_backend.value!r}"
        )

评论区精华

非 Triton runner 输入在 lora_enabled 守卫前触发类型断言 正确性

YAMY1234 指出 `_maybe_build_lora_hooks` 中的 `assert isinstance(_runner_input, TritonRunnerInput)` 在 `lora_enabled` 守卫之前执行,`TritonKernelsRunnerInput` 或 `DeepGemmRunnerInput` 等非 LoRA 内置路径会确定性失败;建议 LoRA 禁用时提前返回并保留原先泛化的 `hidden_states` / `topk_ids` 处理。

结论:alexnails 确认修复("yeah I can fix and cleanup"),最终提交 `5edece2` 在函数开头增加 `if not self.lora_enabled or lora_info is None: return None`,断言保留在 LoRA 专属路径上。 · 已解决

风险与影响

  1. 全局注册表时序敏感_REGISTERED_MOE_RUNNER_BACKEND_NAMES_CUSTOM_RUNNER_CORE_FACTORIES 是进程内全局可变状态,扩展后端必须在模型构建前注册;多模型或多进程场景下注册时机与重复注册语义需要文档化,否则会出现"名字已登记但工厂未注册"或反之的中间态。
  2. 谓词 mixin 疑似遗漏 is_intel_xpu:head 版本中 _MoeRunnerBackendPredicates 未包含 is_intel_xpu,该方法残留在 resolve_moe_runner_backend 函数体内成为不可达代码(base 版本中它是 MoeRunnerBackend 的类方法)。若扩展后端或既有调用方调用 .is_intel_xpu() 会触发 AttributeError,属于重构遗留风险,建议确认真实代码后补齐。
  3. FusedMoE 无条件读取 quant_method.runner:依赖 FusedMoEMethodBase.runner = None 类属性兜底;第三方量化实现若未继承该类又忘记设置 runner 属性,会在模型构建时崩溃。本 PR 已手动修复 MXFP4FlashInferTRTLLMMethod,但契约外延仍需文档约束。
  4. 自定义工厂可覆盖内置后端MoeRunner.__init__ 中工厂分支优先于内置分支,注册同名内置后端会静默改变内置行为,设计上是有意为之,但对意外覆盖缺少告警。
  5. LoRA 关键路径行为变化FusedMoEWithLoRA 的后端选择优先级从"全局参数 > quant method runner"调整为"base layer runner > 全局参数",虽然 PR 声明无模型输出变化,但该路径直接影响 LoRA 推理,回归面需关注相关 e2e 测试。

对默认用户与现有模型:无行为变化(PR 声明 No model-output behavior changes),MoeRunnerBackend 枚举成员与原有 is_*() 谓词保持兼容。对第三方后端开发者:获得正式扩展通道,不再需要 fork 修改公共枚举与 MoeRunner 分支链。对内部一致性:MarlinLoraRunnerCore 纳入统一 DispatchMoeRunnerCore 通道,消除 hasattr 鸭子类型判断;LoRA runner 的量化信息获取统一走 get_moe_quant_info 契约。对团队维护:新增契约要求量化方法实现 get_moe_quant_info 才能支持 LoRA + 自定义后端,而 PR checklist 未勾选文档更新项,契约文档存在缺口;测试矩阵新增 CPU CI 用例(base-c-test-cpu),并修复了 test_mem_pool_ep_unit.py 的 AST 解析逻辑。

全局注册表时序敏感 谓词 mixin 疑似遗漏 is_intel_xpu LoRA 关键路径后端起决定义变更 新增公开 API 契约 合并前多轮 CI 补救修复

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论