Prhub

#33889 moe: the shared-experts-fusion decision is a per-runner value the loader installs

原始 PR 作者 ch-wan 合并时间 2026-08-08 13:42 文件变更 40 提交数 1 评论 18 代码增减 +1492 / -416

执行摘要

共享专家融合决策改为按 runner 安装,修复 draft 污染

PR body 明确指出:draft(MTP/nextn)本身就是 DeepSeek/GLM/Qwen3.5/MiniMax 模型,旧实现里 declare_load_time_override 会把融合决策写到 target 的 bags,draft 与 target checkpoint 量化不同时会破坏 target 记录;这是祖先行为(pre-refactor 直接写共享 ServerArgs 对象)。决策应当是 per-runner 的,且应在 loader 单一模型实例化点、任何 layer 存在之前做出。同时 wrapper 模型(Kimi-K2.5、Pixtral 等)必须以构造函数下发的 config/quantization 回答 gate,否则会为错误的 checkpoint 作答,导致共享专家权重被静默错误重映射。

值得精读。这是一个典型的“把隐式跨模块副作用收敛为显式生命周期”的重构,设计决策包括:loader 单一安装点、per-runner 双子叶(ACTIVE + speculative)、类方法门控 + wrapper 委托、forward 期拒绝读取、以及用注册表遍历 + AST 扫描做防回归。建议重点阅读 python/sglang/srt/layers/moe/utils.py 与两个新测试文件,理解如何防止“加了新 wrapper 就悄悄丢掉禁用条件”这类问题。

讨论亮点

核心讨论围绕“决策迁移后哪些入口类没跟上”展开,Codex 共提出 7 个 finding,作者逐条修复并三次扩围排查:

  • GLM 非 lite NextN 未迁移(P1)Glm4MoeForCausalLMNextN 顶层仍从 config bag 读决策,decoder 已读 ACTIVE,target 自动禁用后会 fused/unfused 布局不一致,权重重映射静默跳过共享专家。修复为在构建 decoder 前运行家族门控。
  • post-build scope 覆盖 draft 叶子(P2)speculative_moe_backend_context 在 finally 无条件把 ACTIVE 写回 speculative 叶子,init_attention_backends 等后置作用域会污染 draft 决策。修复为 in_speculative_scope 标记 + 仅 gate 运行时双写。
  • wrapper 嵌套 DeepSeek 无 gate(P1)KimiK25ForConditionalGeneration 直接构造 DeepseekV3ForCausalLM,loader 只问 wrapper,standard compressed-tensors checkpoint 会走错误融合路径。修复为按构造函数下发的 text_config/quant_config 委托,并把 coverage 测试改为 hasattr 动态解析继承 gate,进而发现 PixtralForConditionalGeneration 等更多案例。
  • Qwen3.5 MTP 与 qwen3_5_text 同名类漏注册:MTP 入口丢失 ROCm 多流路径;文本-only checkpoint 解析到 qwen3_5_text 的同名类,注册循环贴错了类。分别用 _mtp_quant_config 共享归一化和 body_cls 属性委托修复。
  • RoutedExpertsCapturer 读到 draft 决策(P2):改为读取 target 模型烘焙属性 num_fused_shared_experts,不再在构造外查询进程级标志。
    复审结论:原 4 个 finding 全部修复,bugs 0 / suggestions 0 / nits 0,LGTM。

实现拆解

  1. 决策收敛到 loader:在 python/sglang/srt/layers/moe/utils.py 新增 install_shared_experts_fusion_decision(model_class, hf_config, quant_config),由 loader 唯一实例化点 _initialize_model() 调用;新增构建期只读访问器 is_shared_experts_fusion_disabled(),forward 期读取直接抛 AssertionError
  2. 门控从实例方法变为类方法:DeepSeek/GLM/MiniMax/Qwen3.5 各自把既有 determine_num_fused_shared_experts 的禁用条件迁移为 shared_experts_fusion_disable_reason(hf_config, quant_config) 类方法;determine_num_fused_shared_experts 退化为只读安装值;融合架构从方法参数变成类属性 fused_shared_experts_architecture(NextN draft、GLM DSA 变体覆盖它)。
  3. wrapper 委托KimiK25ForConditionalGenerationKimiVLForConditionalGenerationDotsVLMForCausalLMDeepseekVL2ForCausalLMDeepseekOCRForCausalLMPixtralForConditionalGenerationMiniCPMVQwen3_5ForCausalLMMTP 等 wrapper 按构造函数下发的 config/quantization 委托给内部族的 gate;qwen3_5_mtp.py 抽出共享的 _mtp_quant_config,构造函数与 gate 同源。qwen3_5_text.py 的同名类通过 body_cls 属性委托到同一 gate。
  4. draft 隔离initialize_moe_config 同时把用户意图播种到 runner 与 speculative 两个叶子;draft_model_build_scope 在 draft 构造期间把决策路由到 speculative 叶子并恢复 target 值;draft 权重更新不再改写进程的 model_path/load_format 记录;declare_load_time_override 删除。
  5. 测试配套:新增 test_shared_experts_fusion_gates.py(固定每个族的门控分支表和 wrapper 委托内容)、test_fusion_gate_coverage.py(AST + 注册表遍历,确保任何入口类都能回答 gate)、test_draft_construction_isolation.py(隔离语义);修改 test_deepseek_v4_shared_expert_fusion.py
文件 模块 状态 重要度
python/sglang/srt/layers/moe/utils.py 融合决策 modified 8.08
python/sglang/srt/models/deepseek_v2.py DeepSeek 模型 modified 8.02
test/registered/unit/models/test_shared_experts_fusion_gates.py 门控测试 added 7.97
test/registered/unit/models/test_fusion_gate_coverage.py 覆盖测试 added 7.87
test/registered/unit/spec/test_draft_construction_isolation.py 构建隔离 added 7.7
python/sglang/srt/models/qwen3_5.py Qwen3.5 模型 modified 7.66
python/sglang/srt/models/qwen3_5_mtp.py MTP 模块 modified 7.67
python/sglang/srt/models/minimax_m3.py MiniMax 模型 modified 7.64

关键符号

install_shared_experts_fusion_decision is_shared_experts_fusion_disabled draft_model_build_scope initialize_moe_config shared_experts_fusion_disable_reason determine_num_fused_shared_experts _mtp_quant_config _qwen3_5_shared_experts_fusion_disable_reason test_every_entry_class_reaching_a_gated_family_has_a_gate gated_class_names

关键源码片段

python/sglang/srt/layers/moe/utils.py core-logic

核心改造点:新增 install_shared_experts_fusion_decision / is_shared_experts_fusion_disabled / draft_model_build_scope,并在 initialize_moe_config 中播种双子叶,是 per-runner 融合决策的安装与访问入口。

# python/sglang/srt/layers/moe/utils.py(重构后的核心)
import logging
from contextlib import contextmanager
from sglang.srt.runtime_context import get_exec, get_flags
from sglang.srt.utils.common import log_info_on_rank0logger = logging.getLogger(__name__)
​
​
def initialize_moe_config(server_args: ServerArgs):
    '''从 ServerArgs 播种 MoE 运行时标志。    共享专家融合决策改为 per-runner 值:这里把用户的原始意图
    同时写到目标叶子 disable_shared_experts_fusion 与推测叶子
    speculative_disable_shared_experts_fusion;真正细化后的决策由
    install_shared_experts_fusion_decision 在模型构建前安装。
    '''
    moe = get_flags().moe
    # ... 既有 a2a / runner backend 等配置 ...
    moe.disable_shared_experts_fusion = server_args.disable_shared_experts_fusion
    moe.speculative_disable_shared_experts_fusion = (
        server_args.disable_shared_experts_fusion
    )
​
​
def is_shared_experts_fusion_disabled() -> bool:
    '''构建期读取当前 ACTIVE 的融合决策。    仅允许在构造期调用:forward 读取会与 draft 构建窗口竞争
    (draft 构建期间 ACTIVE 叶子装的是 draft 自己的值),所以这里
    显式抛 AssertionError,要求 forward 读层上烘焙的
    num_fused_shared_experts。
    '''
    from sglang.srt.model_executor.forward_context import has_forward_context
​
    if has_forward_context():
        raise AssertionError(
            'is_shared_experts_fusion_disabled() called inside a forward: the '
            'fusion decision is construction-time state (it can hold the '
            "draft's value while a draft builds). Read the value your build "
            "baked in, e.g. the layer's num_fused_shared_experts."
        )
    moe = get_flags().moe
    if moe.disable_shared_experts_fusion is None:
        return get_exec().moe.disable_shared_experts_fusion
    return moe.disable_shared_experts_fusion
​
​
@contextmanager
def draft_model_build_scope():
    '''包住一个 draft 模型的构造过程。    构造期间运行的 gate 会把决策同时写到 speculative 叶子;
    退出时恢复 target 的 ACTIVE 值。刻意不碰 runner_backend ——
    那是 speculative_moe_backend_context 的职责,且必须包住
    draft 的完整生命周期(构建 + capture + forward)。
    '''
    moe = get_flags().moe
    original_fusion = moe.disable_shared_experts_fusion
    original_scope = moe.in_speculative_scope
    try:
        moe.in_speculative_scope = True
        yield
    finally:
        moe.in_speculative_scope = original_scope
        moe.disable_shared_experts_fusion = original_fusion
​
​
def install_shared_experts_fusion_decision(model_class, hf_config, quant_config) -> None:
    '''在 loader 唯一实例化点上为当前 runner 决策并安装融合状态。    用户显式意图优先(已禁用就不再问 gate);否则询问模型族的
    shared_experts_fusion_disable_reason(hf_config, quant_config),
    返回非空理由即记录日志并禁用。draft 构造期间
    (in_speculative_scope 为真)同步写 speculative 叶子。
    '''
    disabled = get_exec().moe.disable_shared_experts_fusion
    if not disabled:
        gate = getattr(model_class, 'shared_experts_fusion_disable_reason', None)
        reason = gate(hf_config, quant_config) if gate is not None else None
        if reason:
            log_info_on_rank0(
                logger, f'{reason} Shared experts fusion optimization is disabled.'
            )
            disabled = True
    moe = get_flags().moe
    moe.disable_shared_experts_fusion = disabled
    if moe.in_speculative_scope:
        moe.speculative_disable_shared_experts_fusion = disabled
python/sglang/srt/models/deepseek_v2.py data-contract

DeepSeek 家族门控从实例方法重构为类方法 shared_experts_fusion_disable_reason,determine_num_fused_shared_experts 退化为只读;NextN 通过 fused_shared_experts_architecture 自报架构。

# python/sglang/srt/models/deepseek_v2.py(重构后的门控)
class DeepseekV2MoE(nn.Module):
    def __init__(self, config, layer_id, quant_config=None, prefix='', ...):
        # ... 既有初始化 ...
        n_shared_experts = (
            0 if config.n_shared_experts is None else int(config.n_shared_experts)
        )
        # 直接读取 loader 安装的 ACTIVE 决策;draft 构建时这里是
        # draft 自己的值,不会污染 target 的状态记录。
        _fusion_disabled = is_shared_experts_fusion_disabled()
        # num_fused_shared_experts 驱动权重重映射:
        # > 0 时把 mlp.shared_experts 重映射进 mlp.experts.256 槽位。
        self.num_fused_shared_experts = 0 if _fusion_disabled else n_shared_experts
        # ... 后续 MoE 布局计算 ...
​
​
class DeepseekV2ForCausalLM(nn.Module):
    # 该 class 代表哪个架构的融合;NextN draft、GLM DSA 变体通过覆盖
    # 这个类属性声明自己的架构名。
    fused_shared_experts_architecture = 'DeepseekV3ForCausalLM'
​
    @classmethod
    def shared_experts_fusion_disable_reason(cls, hf_config, quant_config):
        '''回答该 checkpoint 为何不能融合共享专家;None 表示可以。        由 loader 在每 runner 构建前调用(无实例、无层),所以只能
        从传入的 hf_config / quant_config 与当前进程状态作答。
        '''
        if get_exec().moe.enforce_shared_experts_fusion:
            return None
        if is_sbo_enabled() or is_tbo_enabled():
            return 'SBO/TBO enabled: incompatible with fusing shared expert into MoE kernel.'
        if is_deepep_class_backend():
            return 'DeepEP: fusion off by default (use --enforce-shared-experts-fusion to enable).'
        if (
            hf_config.architectures[0] != cls.fused_shared_experts_architecture
            or hf_config.n_routed_experts not in (256, 384)
            or hf_config.n_shared_experts != 1
            or (
                hf_config.n_routed_experts == 384
                and (quant_config is None or quant_config.get_name() != 'quark')
            )
        ):
            # 384 专家布局仅在 Quark MXFP4 checkpoint 下是预融合的;
            # 标准 Kimi-K2.5(compressed-tensors)会把共享专家松散存储,
            # 走融合路径会静默错载。
            return 'Config does not support fused shared expert(s).'
        # 设备算力 / EP 拓扑 / W4AFP8 等分支语义相同,略去。
        return None
​
    def determine_num_fused_shared_experts(self):
        # 决策由 loader 安装,这里只读不写;不再调用
        # declare_load_time_override 改写进程配置记录。
        self.num_fused_shared_experts = (
            0 if is_shared_experts_fusion_disabled() else self.config.n_shared_experts
        )

评论区精华

GLM 非 lite NextN 未随重构迁移 正确性

Codex P1 指出 Glm4MoeForCausalLMNextN 顶层仍从 config bag 读取 disable_shared_experts_fusion,而 decoder 已改读 ACTIVE 访问器;target 自动禁用后(EP/ 量化 / 能力不足),draft 会出现 fused/unfused 布局不一致,权重重映射会静默跳过共享专家。

结论:作者在 ab1e187 修复:非 lite NextN 在构建 decoder 前运行家族门控,使顶层元数据、decoder 布局与加载器一致。 · 已解决

post-build scope 覆盖 draft 融合决策叶子 正确性

Codex P2:speculative_moe_backend_context 在 finally 中把 ACTIVE 值无条件写回 speculative 叶子,而 StandaloneDraftWorker 的 init_attention_backends / init_cuda_graphs 等进入该 context 后会把 target 值覆盖到 draft 叶子上。

结论:改为 in_speculative_scope 标记 + install 时双写,无 gate 运行的作用域不动叶子;新增 test_post_build_scopes_do_not_clobber_the_draft_leaf。 · 已解决

wrapper 嵌套 DeepSeek 模型未回答门控(KimiK25 误加载) 正确性

Codex P1:KimiK25ForConditionalGeneration 直接构造 DeepseekV3ForCausalLM,loader 只询问 wrapper;wrapper 无 gate 时 compressed-tensors Kimi-K2.5 会走融合路径并静默错载松散共享专家权重。

结论:wrapper 按构造函数下发的 text_config / quant_config 委托给嵌套族 gate;coverage 测试改用 hasattr 动态解析继承门控,找到 Pixtral 等更多案例。 · 已解决

Qwen3.5 MTP 入口未注册 gate 正确性

Codex P2:ROCm 上 ModelConfig 把 Qwen3.5 draft rewrite 为 Qwen3_5ForCausalLMMTP,卸载 _maybe_autodisable 后该入口无 gate,MXFP4 场景丢失多流路径(alt_stream=None)。

结论:抽出 _mtp_quant_config 让构造和 gate 同源,MTP 入口委托 Qwen3.5 gate(text config + 归一化后的量化)。 · 已解决

qwen3_5_text 同名类未注册 gate 正确性

ch-wan 自述 registry walk 漏掉 qwen3_5_text.py 中同名 Qwen3_5ForCausalLM / Qwen3_5MoeForCausalLM(注册循环贴给了多模态类),纯文本 checkpoint 会静默跳过 ROCm 自动禁用。

结论:gate 改为通过 body_cls 属性委托,与构造读取同一属性;coverage 测试继续收紧。 · 已解决

RoutedExpertsCapturer 读取了 draft 的决策 正确性

Codex P2:STANDALONE 下 StandaloneDraftWorker 未包 speculative scope,capturer 读取进程级 ACTIVE 标志时会拿到 draft 的禁用值,导致 --enable-return-routed-experts 的捕获列宽度与 target 不匹配。

结论:capturer 改为读取 target 模型烘焙属性 num_fused_shared_experts,不再在构造外查询进程级标志。 · 已解决

风险与影响

核心风险集中在加载路径的静默错误:

  • 入口类遗漏 gate:任何注册表入口类若未实现 shared_experts_fusion_disable_reason,loader 会静默回退用户意图,跳过家族的自动禁用条件;这是 PR 自己点名的失败模式,历史上已漏过 qwen3_5_text 同名类与继承 gate 的 wrapper。coverage 测试基于 AST 文本扫描,对复杂别名/条件 import 仍可能漏报。
  • wrapper 委托一致性DeepseekVL2 不传 quantization、Pixtral 的 GQA 分支传 NoneQwen3_5ForCausalLMMTP 需要归一化量化,任何“构造函数下发的值”与“gate 收到的值”漂移都会答错 checkpoint。
  • draft 作用域时序draft_model_build_scope 不换 runner_backend 的约定依赖所有 draft worker 都配合 speculative_moe_backend_context;未来若有人在 scope 内新增跑 gate 的路径,可能重新引入叶子覆盖问题。
  • 行为回归面:40 个文件、9+ 模型族,但 GPU e2e(GLM-4.7-Flash + NEXTN、Qwen3-Next-80B-FP8)为字节级一致,16 分区 CPU 回归基线 0 回归,整体回归风险可控。

对用户:修复了 Kimi-K2.5 compressed-tensors、Qwen3.5 ROCm MXFP4 等场景的潜在静默权重误加载;flags dump 现在能同时看到 target 与 draft 两个 runner 的融合决策,日志更准确。
对系统:loader 成为融合决策的唯一安装点,declare_load_time_override 机制删除,后续新模型族接入只需实现一个类方法;draft 构建状态隔离成为推测解码的显式契约。
对团队:新增约 900 行测试(含 CPU CI 套件),测试资产显著增强;coverage 测试的“注册表走查 + AST 扫描”模式可复用到其他跨模型族的一致性检查。

核心加载路径变更 跨 9+ 模型族迁移 静默权重误加载风险 draft/target 状态隔离 依赖 AST 扫描防回归

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论