执行摘要
- 一句话:共享专家融合决策改为按 runner 安装,修复 draft 污染
- 推荐动作:值得精读。这是一个典型的“把隐式跨模块副作用收敛为显式生命周期”的重构,设计决策包括:loader 单一安装点、per-runner 双子叶(ACTIVE + speculative)、类方法门控 + wrapper 委托、forward 期拒绝读取、以及用注册表遍历 + AST 扫描做防回归。建议重点阅读
python/sglang/srt/layers/moe/utils.py 与两个新测试文件,理解如何防止“加了新 wrapper 就悄悄丢掉禁用条件”这类问题。
功能与动机
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:在
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。
- 门控从实例方法变为类方法: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 变体覆盖它)。
- wrapper 委托:
KimiK25ForConditionalGeneration、KimiVLForConditionalGeneration、DotsVLMForCausalLM、DeepseekVL2ForCausalLM、DeepseekOCRForCausalLM、PixtralForConditionalGeneration、MiniCPMV、Qwen3_5ForCausalLMMTP 等 wrapper 按构造函数下发的 config/quantization 委托给内部族的 gate;qwen3_5_mtp.py 抽出共享的 _mtp_quant_config,构造函数与 gate 同源。qwen3_5_text.py 的同名类通过 body_cls 属性委托到同一 gate。
- draft 隔离:
initialize_moe_config 同时把用户意图播种到 runner 与 speculative 两个叶子;draft_model_build_scope 在 draft 构造期间把决策路由到 speculative 叶子并恢复 target 值;draft 权重更新不再改写进程的 model_path/load_format 记录;declare_load_time_override 删除。
- 测试配套:新增
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(模块 融合决策;类别 source;类型 core-logic;符号 install_shared_experts_fusion_decision, is_shared_experts_fusion_disabled, draft_model_build_scope, initialize_moe_config): 核心改造点:新增 install_shared_experts_fusion_decision / is_shared_experts_fusion_disabled / draft_model_build_scope,并在 initialize_moe_config 中播种双子叶,是 per-runner 融合决策的安装与访问入口。
python/sglang/srt/models/deepseek_v2.py(模块 DeepSeek 模型;类别 source;类型 data-contract;符号 shared_experts_fusion_disable_reason, determine_num_fused_shared_experts, fused_shared_experts_architecture): DeepSeek 家族门控从实例方法重构为类方法 shared_experts_fusion_disable_reason,determine_num_fused_shared_experts 退化为只读;NextN 通过 fused_shared_experts_architecture 自报架构。
test/registered/unit/models/test_shared_experts_fusion_gates.py(模块 门控测试;类别 test;类型 test-coverage;符号 _quant, _FusionGateCase, _seed, _reason): 以 loader 提问方式固定每个模型族的门控分支表,并验证 wrapper 委托的 config/quantization 与构造一致。
test/registered/unit/models/test_fusion_gate_coverage.py(模块 覆盖测试;类别 test;类型 test-coverage;符号 _gated_classes, gated_class_names, TestFusionGateCoverage, test_every_entry_class_reaching_a_gated_family_has_a_gate): 用 AST + 注册表遍历保证任何入口类到达门控家族时都持有 gate,防回归(对 qwen3_5_text 同名类等历史漏网)。
test/registered/unit/spec/test_draft_construction_isolation.py(模块 构建隔离;类别 test;类型 test-coverage;符号 _AlwaysDisables, _NoGate, _install, TestFusionDecisionFlag): 验证 flag 播种、draft 作用域路由/恢复、forward 期拒绝读取与权重更新记录隔离。
python/sglang/srt/models/qwen3_5.py(模块 Qwen3.5 模型;类别 source;类型 data-contract;符号 _qwen3_5_shared_experts_fusion_disable_reason, _disable_shared_experts_fusion, shared_experts_fusion_disable_reason): Qwen3.5 的 ROCm 自动禁用条件迁移为模块级 gate,并注册到四个入口类;qwen3_5_text 同名类通过 body_cls 委托补上。
python/sglang/srt/models/qwen3_5_mtp.py(模块 MTP 模块;类别 source;类型 data-contract;符号 _mtp_quant_config, shared_experts_fusion_disable_reason): MTP 入口委托 Qwen3.5 gate,并抽取 _mtp_quant_config 使构造与 gate 共享同一量化归一化,避免为 target 的量化作答。
python/sglang/srt/models/minimax_m3.py(模块 MiniMax 模型;类别 source;类型 data-contract;符号 shared_experts_fusion_disable_reason, determine_num_fused_shared_experts): MiniMax 家族同样把 determine_num_fused_shared_experts 重构为类方法门控并向 loader 模式靠拢。
关键符号: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
核心改造点:新增 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_rank0
logger = 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
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
)
评论区精华
核心讨论围绕“决策迁移后哪些入口类没跟上”展开,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。
-
GLM 非 lite NextN 未随重构迁移 (correctness): 作者在 ab1e187 修复:非 lite NextN 在构建 decoder 前运行家族门控,使顶层元数据、decoder 布局与加载器一致。
- post-build scope 覆盖 draft 融合决策叶子 (correctness): 改为 in_speculative_scope 标记 + install 时双写,无 gate 运行的作用域不动叶子;新增 test_post_build_scopes_do_not_clobber_the_draft_leaf。
- wrapper 嵌套 DeepSeek 模型未回答门控(KimiK25 误加载) (correctness): wrapper 按构造函数下发的 text_config / quant_config 委托给嵌套族 gate;coverage 测试改用 hasattr 动态解析继承门控,找到 Pixtral 等更多案例。
- Qwen3.5 MTP 入口未注册 gate (correctness): 抽出 _mtp_quant_config 让构造和 gate 同源,MTP 入口委托 Qwen3.5 gate(text config + 归一化后的量化)。
- qwen3_5_text 同名类未注册 gate (correctness): gate 改为通过 body_cls 属性委托,与构造读取同一属性;coverage 测试继续收紧。
- RoutedExpertsCapturer 读取了 draft 的决策 (correctness): 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 分支传 None、Qwen3_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 扫描防回归
关联脉络
- PR #33925 config: route DCP topology reads through get_parallel(): 同属配置一致化系列:消除配置种子与运行时读取的不一致,与本 PR 把融合决策收敛到 loader 单一安装点呼应。
- PR #34026 [Spec] Propagate state capture outputs in DFlash: 同为推测解码场景下 draft/target 状态生命周期收口,关注状态泄漏与构建/执行时序。
参与讨论