Prhub

#6708 [model] fix: NPU patches for the Qwen3-MoE compatible with different transformers

原始 PR 作者 mikequan0425 合并时间 2026-06-16 16:00 文件变更 1 提交数 2 评论 1 代码增减 +51 / -18

执行摘要

修复 Qwen3-MoE NPU 补丁对不同 Transformers 版本的兼容性

关联 Issue #6707 指出,Qwen3-MoE 的 NPU 补丁 _qwen3_sparse_moe_routed_forward_npu 仅适配了 Transformers 4.57.x 的 API(self.gate 为 nn.Linear,self.experts 为模块列表),在 Transformers 5.x 中这些 API 发生变化(self.gate 返回三元组,self.experts 使用融合权重),导致运行时错误或不正确的路由行为。本 PR 旨在消除版本差异,使补丁能够自动适配两种 API 风格。

建议精读此 PR,特别是 _qwen3_sparse_moe_routed_forward_npu 中的版本适配逻辑(gate 输出的类型检测、权重提取分支),以及 _qwen3_sparse_moe_uses_legacy_block_api 的检测策略。这些设计模式可推广至其他需要跨版本兼容的场景。

讨论亮点

PR 未产生人工 review 评论,只有 Gemini Code Assist 的自动总结性评论和仓库维护者 wucong25 的批准。未发现明显的讨论或争议。

实现拆解

  1. 新增版本检测函数 _qwen3_sparse_moe_uses_legacy_block_api:通过检查 self.gate 是否为 nn.Linear 以及 self 是否拥有 top_k 和 norm_topk_prob 属性,判断当前模型实例是否使用 Transformers 4.57.x 风格的旧版 API。
  2. 重构 _qwen3_sparse_moe_routed_forward_npu 的路由逻辑:原代码直接假设 self.gate(hidden_states) 返回原始 router logits;现改为兼容检测返回类型:若返回三元组则直接解包(Transformers 5.x),否则按旧版逻辑手动计算路由权重和选定专家。同时使用 getattr 链式回退获取 top_k 和 norm_topk_prob 参数,支持其存放于 self.gate 或 self 属性中的不同位置。
  3. 适配专家权重的提取方式:原代码假设 self.experts 为专家模块列表,每个模块包含独立的 up_proj、gate_proj、down_proj;现新增分支检测 self.experts 是否包含融合权重属性 gate_up_proj 和 down_proj(Transformers 5.x),并通过 chunk 切分 gate/up 权重;否则回退到旧版列表式提取。
  4. 调整 num_experts 的获取逻辑:新增链式 getattr 回退机制,支持从 self、self.experts、self.gate 或 self.gate.weight 中获取专家数,以适应不同的属性存放位置。
  5. 修改 qwen3_moe_sparse_moe_block_forward_npu 和 qwen3_next_sparse_moe_block_forward_npu 的返回签名:对于旧版 API,返回 (final_hidden_states, router_logits);对于新版 API,仅返回 final_hidden_states,因为新版路由器已在内部处理路由逻辑,无需对外暴露 logits。
文件 模块 状态 重要度
verl/models/transformers/npu_patch.py 模型补丁 modified 7.9

关键符号

_qwen3_sparse_moe_uses_legacy_block_api qwen3_moe_sparse_moe_block_forward_npu qwen3_next_sparse_moe_block_forward_npu _qwen3_sparse_moe_routed_forward_npu

关键源码片段

verl/models/transformers/npu_patch.py data-contract

包含所有核心变更:新增版本检测函数,重构路由前向和权重提取逻辑,调整返回签名。

def _qwen3_sparse_moe_uses_legacy_block_api(self) -> bool:
    """Return True for transformers 4.57.x-style Qwen3 MoE blocks."""
    return (
        isinstance(getattr(self, "gate", None), nn.Linear) # gate 是标准 Linear 层
        and hasattr(self, "top_k")
        and hasattr(self, "norm_topk_prob")
    )
​
​
def _qwen3_sparse_moe_routed_forward_npu(self, hidden_states: torch.Tensor):
    # ... 省略输入展开和形状预处理 ...
    gate_output = self.gate(hidden_states)
    if isinstance(gate_output, tuple) and len(gate_output) == 3:
        # Transformers 5.x: gate 返回 (router_logits, routing_weights, selected_experts)
        router_logits, routing_weights, selected_experts = gate_output
    else:
        # Transformers 4.57.x: gate 直接返回 raw logits,需手动计算路由
        router_logits = gate_output
        top_k = getattr(self.gate, "top_k", getattr(self, "top_k", getattr(self, "num_experts_per_tok", 2)))
        routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float)
        routing_weights, selected_experts = torch.topk(routing_weights, top_k, dim=-1)
        norm_topk_prob = getattr(self.gate, "norm_topk_prob", getattr(self, "norm_topk_prob", True))
        if norm_topk_prob:
            routing_weights /= routing_weights.sum(dim=-1, keepdim=True)
    # 后续处理:权重汇聚、permute、grouped matmul 等 ...
    # 权重提取分支:新版使用融合权重,旧版使用独立权重列表
    if hasattr(self.experts, "gate_up_proj") and hasattr(self.experts, "down_proj"):
        # Transformers 5.x: 融合权重
        gate_proj_weight, up_proj_weight = self.experts.gate_up_proj.chunk(2, dim=1)
        w1 = up_proj_weight.transpose(1, 2).to(input_dtype)
        w2 = gate_proj_weight.transpose(1, 2).to(input_dtype)
        w3 = self.experts.down_proj.transpose(1, 2).to(input_dtype)
    else:
        # Transformers 4.57.x: 独立权重列表
        up_weight_list = [e.up_proj.weight for e in self.experts]
        gate_weight_list = [e.gate_proj.weight for e in self.experts]
        down_weight_list = [e.down_proj.weight for e in self.experts]
        w1 = torch.stack(up_weight_list).transpose(1, 2).to(input_dtype)
        w2 = torch.stack(gate_weight_list).transpose(1, 2).to(input_dtype)
        w3 = torch.stack(down_weight_list).transpose(1, 2).to(input_dtype)
    # ... 后续 grouped matmul 和 unpermute ...
    return hidden_states, routed_hidden_states, router_logits

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 回归风险:对旧版 Transformers 4.57.x 的兼容性通过 _qwen3_sparse_moe_uses_legacy_block_api 条件分支维护,但若检测条件判断错误(例如新版本中 self.gate 仍为 nn.Linear 但逻辑不同),可能导致错误的分支选择。不过当前检测逻辑较为稳健。
  2. 性能风险:getattr 链式回退增加了少量运行时开销,但仅在前向路径中执行有限次数,影响可忽略。
  3. 覆盖风险:单文件修改未附带测试用例,若未来 Transformers 进一步变更 MoE API,可能再次出现不兼容问题。建议增加针对不同 Transformers 版本的单元测试。
  1. 用户影响:使在 Ascend NPU 上使用 Qwen3-MoE 模型的用户能够平滑升级或降级 Transformers 版本,无需手动修改代码。
  2. 系统影响:单文件变更,代码量小(+51/-18),耦合低,风险可控。
  3. 团队影响:无,仅维护性提升。
缺少测试覆盖

关联 Issue

#6707 [Ascend][Qwen3-MoE]npu fused kernel for Qwen3-MoE patch failed

完整报告

参与讨论