Prhub

#30387 Fix zero expert routed ids for MoE backends

原始 PR 作者 BBuf 合并时间 2026-07-08 21:23 文件变更 2 提交数 1 评论 3 代码增减 +69 / -1

执行摘要

修复零专家路由传入非法负数 id 导致 CUDA 崩溃

LongCat-Flash 中 zero experts 表示为 top-k id >= num_experts,预处理将 id 设为 -1 并清零 scale。但某些 MoE 内核仍将 id 用作 gather/sort 输入,不处理负数 id,在高并发场景下出现 CUDA illegal memory access。改为合法 id 0 配合零 scale 在数学上等价,且避免传递负数 id。

建议合入,但后续需跟进 review 中提出的命名问题和非 identity 路径保护。

讨论亮点

Review 中 gemini-code-assist[bot] 提出了两个中优先级问题:一是变量名 normal_expert_maskzero_expert_mask 含义颠倒导致可读性差;二是当 zero_expert_type 不为 "identity"zero_expert_scales 未定义可能导致 NameError。当前 PR 尚未处理这两点。

实现拆解

  1. 核心修复:在 python/sglang/srt/layers/moe/ep_moe/kernels.pyzero_experts_compute_triton 函数中,将零专家的 expert_indices-1 改为 0,同时仍将对应 expert_scales 置零,确保路由专家的贡献被消除。
  2. 新增单元测试test/registered/moe/test_zero_experts.py,测试零专家路由后 expert_indices 均非负,且输出与理论值一致(零专家部分输出 hidden_states * 剩余 scale)。
文件 模块 状态 重要度
python/sglang/srt/layers/moe/ep_moe/kernels.py MoE 层 modified 5.56
test/registered/moe/test_zero_experts.py 测试 added 6.85

关键符号

zero_experts_compute_triton

关键源码片段

python/sglang/srt/layers/moe/ep_moe/kernels.py core-logic

核心修复文件,修改了零专家 id 从 -1 变为 0,避免 MoE 内核崩溃。

def zero_experts_compute_triton(
    expert_indices, expert_scales, num_experts, zero_expert_type, hidden_states
):
    N = expert_indices.numel()
    top_k = expert_indices.size(-1)
    grid = lambda meta: (triton.cdiv(N, meta["BLOCK_SIZE"]),)
​
    if zero_expert_type == "identity":
        zero_expert_mask = expert_indices < num_experts
        zero_expert_scales = expert_scales.clone()
        zero_expert_scales[zero_expert_mask] = 0.0
​
    normal_expert_mask = expert_indices >= num_experts
    # 使用合法专家 id 0 代替 -1,避免 MoE 内核无法处理负数 id
    # 配合零 scale 仍能消除该路由专家的贡献,数学上等价
    expert_indices[normal_expert_mask] = 0
    expert_scales[normal_expert_mask] = 0.0
​
    output = torch.zeros_like(hidden_states).to(hidden_states.device)
    hidden_dim = hidden_states.size(-1)
    num_tokens = hidden_states.size(0)
​
    grid = lambda meta: (num_tokens * (hidden_dim // meta["BLOCK_SIZE"]),)
    compute_identity_kernel[grid](
        top_k,
        hidden_states,
        zero_expert_scales, # 注意:若 zero_expert_type 非 "identity",此处 NameError
        num_tokens,
        output,
        hidden_dim,
        zero_expert_scales.stride(0),
        BLOCK_SIZE=256,
    )
​
    return output
test/registered/moe/test_zero_experts.py test-coverage

新增的 CUDA 单元测试,验证零专家路由后 expert_indices 非负且输出正确。

import unittest
import torch
from sglang.srt.layers.moe.ep_moe.kernels import zero_experts_compute_triton
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCaseregister_cuda_ci(est_time=5, stage="base-b", runner_config="1-gpu-small")@unittest.skipIf(not torch.cuda.is_available(), "CUDA is required")
class TestZeroExpertsComputeTriton(CustomTestCase):
    def test_zero_expert_routes_keep_valid_ids(self):
        num_experts = 4
        # 构造 2 个 token,每个 top-3 路由,其中 id >= num_experts 视为零专家
        hidden_states = torch.arange(2 * 512, dtype=torch.float32, device="cuda").reshape(2, 512)
        expert_indices = torch.tensor(
            [[0, 4, 1], [5, 2, 6]], dtype=torch.int64, device="cuda"
        )
        expert_scales = torch.tensor(
            [[0.1, 0.3, 0.2], [0.4, 0.5, 0.6]], dtype=torch.float32, device="cuda"
        )
        original_indices = expert_indices.clone()
        original_scales = expert_scales.clone()
​
        output = zero_experts_compute_triton(
            expert_indices=expert_indices,
            expert_scales=expert_scales,
            num_experts=num_experts,
            zero_expert_type="identity",
            hidden_states=hidden_states,
        )
        torch.cuda.synchronize()
​
        # 验证所有 expert_indices 非负
        self.assertTrue(torch.all(expert_indices >= 0).item())
        # 正常路由的 id 和 scale 保持不变
        normal_mask = original_indices < num_experts
        torch.testing.assert_close(expert_indices[normal_mask], original_indices[normal_mask])
        torch.testing.assert_close(expert_scales[normal_mask], original_scales[normal_mask])
        # 零专家的 id 被改为 0,scale 被改为 0
        zero_mask = original_indices >= num_experts
        torch.testing.assert_close(expert_indices[zero_mask], torch.zeros_like(expert_indices[zero_mask]))
        torch.testing.assert_close(expert_scales[zero_mask], torch.zeros_like(expert_scales[zero_mask]))
        # 输出应为 hidden_states * 剩余 scale 的和(仅保留正常路由的 scale)
        expected_scale = (original_scales * zero_mask.to(original_scales.dtype)).sum(dim=-1, keepdim=True)
        torch.testing.assert_close(output, hidden_states * expected_scale)if __name__ == "__main__":
    unittest.main()

评论区精华

变量命名歧义:normal_expert_mask 和 zero_expert_mask 含义颠倒 设计

gemini-code-assist[bot] 指出 zero_expert_mask 实际选中正常路由专家,normal_expert_mask 实际选中零专家,命名与语义相反,降低可读性。

结论:未在 PR 中处理,建议后续重构命名。 · 待处理

zero_expert_scales 在非 identity 模式下未定义导致 NameError 正确性

gemini-code-assist[bot] 指出若 zero_expert_type 不是 "identity",zero_expert_scales 在后续使用时会触发 NameError。建议初始化或抛出 ValueError。

结论:未在 PR 中处理,当前仅支持 "identity" 模式。 · 待处理

风险与影响

低风险:修复仅更改了 id 赋值(-1 -> 0)且已通过 H200 单测和 GSM8K 端到端验证;但变量命名反转可能使未来维护者困惑,且未处理的 NameError 分支在非 identity 模式下存在潜在运行时崩溃。

直接影响使用 MoE 路由且存在零专家选择的推理场景(如 DeepSeek 系列)。修复合入后,LongCat-Flash-Lite FP8 等场景下 CUDA 非法访问崩溃消失,端到端负载完成。

潜在未定义变量分支 变量命名混淆

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论