Prhub

#52209 Add routed expert loading for gpt-oss

原始 PR 作者 wyettzeng 合并时间 2026-08-23 17:59 文件变更 4 提交数 5 评论 38 代码增减 +370 / -3

执行摘要

GPT-OSS 支持按专家流式加载,供 RL 引擎逐个同步权重

PR body 明确说明动机:让 verl 等 RL 引擎 "sync experts one by one instead of requiring all-gathering experts during actor-rollout sync"。GPT-OSS 这类大 MoE 模型在 RL 训练中 actor 与 rollout 实例需频繁同步权重,全量 all-gather 专家权重的通信与显存开销很大;按专家粒度同步可显著降低成本。作者在评论中补充:实际 RL 训练使用 unsloth 的 BF16 版本,mxfp4/nvfp4 量化路径是按 reviewer 要求补上,并做了"逐专家回放 + 推理输出逐 token 一致"的验证。

值得精读。重点关注 GptOssRoutedExperts.weight_loader 的 return_success 契约设计(让调用方知道专家是否真正属于本 rank)、与 LoRA 包装层及 MXFP4 后处理的交互;meta.py 的恢复逻辑修复对所有 layerwise reload 模型都有参考价值。若团队在做 RL 权重同步,可与 #43375 的 P2P RDT 权重同步结合阅读。

讨论亮点

量化方法支持范围:aoshen02 追问 "Will it support other mxfp4 and quack?",并质疑 "Isn't this model is mxfp4? Does the other two load function have the same problem?"。作者最初回答只支持 BF16(RL 训练用 unsloth BF16 版),被追问后补齐量化路径,并给出"逐专家回放 + 推理 token 一致"的验证结果。

LoRA 包装层兼容性(Codex P1):指出 LoRA 启用时 mlp.experts 被替换为 FusedMoEWithLoRA,不暴露 _map_global_expert_id_to_local_expert_id,参数名也变为 experts.base_layer.routed_experts.*,直接调用会抛 AttributeError;作者回复 "followed existing convention where we add the name to the parameter so lora is dispatched correctly",并在 _get_streamed_expert_info 中兼容 base_layer. 前缀。

meta.py 修改原因:wyettzeng 解释 "native MXFP4 post-processing replaces registered expert parameters with unregistered Triton tensor attributes. During layerwise reload, the old code only removed registered parameters buffers, so restoring checkpoint-format parameters failed with KeyError: attribute already exists"。

风格与范围收束:aoshen02 建议简化 _narrow_for_rank 并消除 _map_global_expert_id_to_local_expert_id 的重复调用;Isotr0py 要求简化 _GPT_OSS_STREAMED_EXPERT_SUFFIXES 常量表、撤销不必要的 hf_to_vllm 映射删除、并把测试移到 tests/model_executor/model_loader 目录,均已按意见修改。

实现拆解

  1. 新增流式专家加载器 GptOssRoutedExperts(vllm/model_executor/models/gpt_oss.py,+197):继承 RoutedExperts,复用其 _map_global_expert_id_to_local_expert_id 把全局专家 ID 映射到本地 slot,非本 rank 专家返回 False 并配合 return_success 语义;weight_loader 只接管 gpt_oss_w13/gpt_oss_w2 两个 shard,其余走基类,并按权重类型分派到 _load_expert_bias(w2 bias 仅 tp_rank 0 持有,其余 rank 填零)、_load_unquantized_expert(转置后按列切分)、_load_packed_expert(量化打包布局:reshape 后按块切分)。
  2. 接入模型加载主流程:GptOssModel 新增 _get_streamed_expert_info 解析 checkpoint 键 ...experts.<expert_id>.<fused_param>,映射到 params_dict 中 routed_experts.* 融合参数,并兼容 LoRA 包装层的 base_layer. 前缀;_try_load_streamed_expert 调用参数上的 weight_loader,成功后把参数名记入 loaded_params;在 _load_weights_mxfp4 与 _load_weights_other 的权重循环中插入钩子,命中即加载并跳过后续分支,Quark 路径显式标注不支持;MLPBlock 通过 routed_experts_cls=GptOssRoutedExperts 注入新加载器。
  3. 修复 layerwise reload 恢复逻辑(vllm/model_executor/model_loader/reload/meta.py,+6/-3):restore_layer_on_meta 原来只删除 get_layer_tensors 返回的已注册参数/缓冲区;MXFP4 后处理会把注册专家参数替换为未注册的 Triton 属性,导致恢复 checkpoint 参数时 KeyError: attribute already exists。修复为删除「当前张量 ∪ 恢复元数据」的并集,并用 hasattr 保护。
  4. 测试配套:新增 tests/model_executor/model_loader/test_gpt_oss_weight_loading.py(152 行),用 _TestExperts 桩覆盖 wrapped parameter 解析(含 base_layer. 前缀)、非本地专家跳过、非量化专家 TP 切分(w13/w2)、w2 bias 归属 tp_rank 0、量化打包布局(mxfp4/nvfp4)切分;test_reload.py 新增 test_restore_layer_replaces_postprocessed_tensor_attribute 模拟属性被替换后 restore_layer_on_meta 能正确重建 meta 参数。
文件 模块 状态 重要度
vllm/model_executor/models/gpt_oss.py 模型加载 modified 8.78
tests/model_executor/model_loader/test_gpt_oss_weight_loading.py 测试配套 added 7.82
vllm/model_executor/model_loader/reload/meta.py 重载机制 modified 6.09
tests/model_executor/model_loader/test_reload.py 测试配套 modified 5.41

关键符号

GptOssRoutedExperts.weight_loader GptOssRoutedExperts._narrow_for_rank GptOssRoutedExperts._copy_to_expert GptOssRoutedExperts._load_expert_bias GptOssRoutedExperts._load_unquantized_expert GptOssRoutedExperts._load_packed_expert GptOssModel._get_streamed_expert_info GptOssModel._try_load_streamed_expert restore_layer_on_meta

关键源码片段

vllm/model_executor/models/gpt_oss.py core-logic

核心实现文件:新增 GptOssRoutedExperts 流式专家加载器、_get_streamed_expert_info/_try_load_streamed_expert 解析与钩子,并在 _load_weights_mxfp4/_load_weights_other 中接入;所有 TP 切分与量化分派逻辑都在此。

# gpt_oss.py:GptOssRoutedExperts 的核心分派逻辑
class GptOssRoutedExperts(RoutedExperts):
    """Load one GPT-OSS expert at a time without assembling the global stack."""
​
    @staticmethod
    def _narrow_for_rank(loaded_weight, dim, rank, size):
        # 按 TP rank 均匀切分,尾部不足时截断到可用长度
        start = rank * size
        available = loaded_weight.shape[dim] - start
        return loaded_weight.narrow(dim, start, min(size, max(available, 0)))
​
    @staticmethod
    def _copy_to_expert(expert_data, loaded_weight):
        # 兼容单元素标量(bias)与尾维为 1 的 checkpoint 布局
        if expert_data.numel() == loaded_weight.numel() == 1:
            expert_data.copy_(loaded_weight.reshape(()))
            return
        while loaded_weight.ndim > expert_data.ndim and loaded_weight.shape[-1] == 1:
            loaded_weight = loaded_weight.squeeze(-1)
        slices = tuple(slice(0, size) for size in loaded_weight.shape)
        expert_data[slices].copy_(loaded_weight)
​
    def weight_loader(self, param, loaded_weight, weight_name, shard_id,
                      expert_id, return_success=False):
        # 仅接管 w13/w2 两类 shard,其余交给基类
        if shard_id not in ("gpt_oss_w13", "gpt_oss_w2"):
            return super().weight_loader(param, loaded_weight, weight_name,
                                         shard_id, expert_id, return_success)
​
        # 非本 rank 专家直接跳过,return_success 供调用方记录已加载参数
        expert_id = self._map_global_expert_id_to_local_expert_id(expert_id)
        if expert_id == -1:
            return False if return_success else None
​
        expert_data = param.data[expert_id]
        weight_dtype = getattr(self.quant_method, "weight_dtype", "")
        quant_method_name = self.quant_method.__class__.__name__
​
        if weight_name.endswith("_bias"):
            self._load_expert_bias(expert_data, loaded_weight, shard_id)
        elif weight_dtype in ("gpt_oss_mxfp4", "mxfp4") or quant_method_name in (
            "CompressedTensorsW4A4Nvfp4MoEMethod",
            "Nvfp4OnlineMoEMethod",
        ):
            # 量化权重走打包布局(块缩放)切分
            self._load_packed_expert(expert_data, loaded_weight, weight_name, shard_id)
        else:
            self._load_unquantized_expert(expert_data, loaded_weight, shard_id)
        return True if return_success else None
vllm/model_executor/model_loader/reload/meta.py bugfix

共享 layerwise reload 恢复逻辑的配套修复:restore_layer_on_meta 现在会清理量化后处理生成的未注册属性,解决 MXFP4 场景下恢复 checkpoint 参数的 KeyError。影响所有 layerwise reload + 量化后处理的模型。

# meta.py:修复 restore_layer_on_meta 未清理未注册属性的问题
def restore_layer_on_meta(layer, info):
    """Restore a layer to model format with tensors on the meta device."""
    if layer.__class__.__name__ in SKIP_MODULES:
        return
​
    non_persistent = set(layer._non_persistent_buffers_set)
    restore_params, restore_buffers = info.restore_metadata
    # 需要同时清理后处理(如 MXFP4)生成的未注册属性,
    # 否则重新注册 checkpoint 参数会抛 "attribute already exists"
    tensor_names = (
        get_layer_tensors(layer).keys() | restore_params.keys() | restore_buffers.keys()
    )
    for name in tensor_names:
        if name not in SKIP_TENSORS and hasattr(layer, name):
            delattr(layer, name)
​
    for name, param in restore_params.items():
        if name not in SKIP_TENSORS:
            param = restore_layer_refs(param, layer)
            layer.register_parameter(name, param)
​
    for name, buffer in restore_buffers.items():
        if name not in SKIP_TENSORS:
            buffer = restore_layer_refs(buffer, layer)
            layer.register_buffer(name, buffer, persistent=name not in non_persistent)

评论区精华

量化方法(mxfp4/quark)是否同样支持流式加载 question

aoshen02 追问 "Will it support other mxfp4 and quack?" 并质疑 "Isn't this model is mxfp4? Does the other two load function have the same problem?";作者最初回答只针对 BF16,被追问后补充了量化路径支持。

结论:最终支持 BF16 与 mxfp4/nvfp4 打包布局,Quark 路径显式注释不支持;作者补充了量化方法逐专家回放的推理一致性验证。 · 已解决

LoRA 包装层下流式加载路径解析(Codex P1) 正确性

Codex 指出 LoRA 启用时 mlp.experts 被替换为 FusedMoEWithLoRA,不暴露 _map_global_expert_id_to_local_expert_id,参数名变为 experts.base_layer.routed_experts.*,直接调用会抛 AttributeError。

结论:作者按现有约定将名称挂到参数上以支持 LoRA 分发,并在 _get_streamed_expert_info 中兼容 base_layer. 前缀。 · 已解决

restore_layer_on_meta 需清理后处理生成的未注册属性 正确性

wyettzeng 说明 native MXFP4 后处理把注册专家参数替换为未注册 Triton 属性,旧逻辑只删注册参数导致恢复时 KeyError: attribute already exists。

结论:改为删除「当前张量 ∪ 恢复元数据」并集并加 hasattr 保护,附带新增回归测试。 · 已解决

_map_global_expert_id_to_local_expert_id 重复调用 style

aoshen02 指出在 _load_weights_other 外层与 GptOssRoutedExperts.weight_loader 内部重复映射专家 ID。

结论:作者回复 "good catch, removed the outer one",移除外层调用。 · 已解决

_narrow_for_rank 切分代码可读性 style

aoshen02 建议简化窄切分逻辑(per_rank_size、available、min(size, available) 写法),并建议复用 bias 切分代码。

结论:作者重构为当前的 _narrow_for_rank 静态方法并统一各加载路径。 · 已解决

常量表结构与 hf_to_vllm 映射是否应保留 设计

Isotr0py 认为 _GPT_OSS_STREAMED_EXPERT_SUFFIXES 的(shard_id, hf_suffixes)嵌套结构可读性差,并指出移除原 hf_to_vllm 映射此变更不必要。

结论:作者简化常量表并还原 hf_to_vllm 映射("reverted")。 · 已解决

测试文件位置 测试

Isotr0py 要求把新测试文件移到 tests/model_executor/model_loader 目录。

结论:作者按意见移动("done")。 · 已解决

风险与影响

共享恢复路径回归风险:restore_layer_on_meta 是所有 layerwise reload 模型的公共路径,删除范围从注册张量扩大到「当前张量 ∪ 恢复元数据」并集,虽有 hasattr 保护,属性名冲突时仍可能影响其他使用量化后处理的模型,需要非 gpt-oss 模型回归验证。LoRA 兼容性依赖约定:流式加载对 LoRA 包装层的解析依赖"参数名挂载"的既有约定,若 FusedMoEWithLoRA 结构变化,_get_streamed_expert_info 的 base_layer. 探测可能失效。量化覆盖有限:Quark 路径显式不支持流式加载,_load_packed_expert 假设统一打包布局,新增量化格式需扩展分派。切分边界假设:_narrow_for_rank 依赖 TP 均匀切分假设,尾部不足靠 max(available, 0) 截断,极端形状下可能出现拷贝形状不匹配。验证范围:仓库内 CI 主要是单元测试,多卡 TP/EP + LoRA 的 e2e 回归依赖 verl 等 RL 工作流后续验证。

对 verl 等 RL 训练工作流直接受益:actor-rollout 同步可从全量 all-gather 降为按专家粒度传输;对 GPT-OSS 模型加载路径是主要变更点,不影响其他 MoE 模型;meta.py 的修复影响所有使用 layerwise reload + 量化后处理的模型;新增 152 行单元测试覆盖了 TP 切分、bias 归属、量化布局等关键分支,但缺少多卡 e2e。整体影响面为模型级 + 共享工具函数的小幅改动。

共享重载逻辑变更 LoRA 兼容性依赖约定 量化覆盖有限 模型专属路径 多卡 e2e 待验证

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论