Prhub

#29463 [DeepSeek V3] Reland: run routed experts on main stream in dual-stream MoE

原始 PR 作者 kpham-sgl 合并时间 2026-06-30 06:05 文件变更 1 提交数 1 评论 7 代码增减 +49 / -46

执行摘要

修复 DeepSeek V3 双流 MoE 中 CUDA 图捕获顺序导致的精度崩溃

原 PR #29142 通过将路由专家放置在主流上以利用 PDL-fuse 获得约 1% 的性能增益,但被回滚因为 gsm8k 精度从 ~0.7 跌至 0.005。根因并非跨流竞争,而是 dispose_tensor(host-side 通过 set_hidden_statesdata_ptr 置零)在共享专家内核捕获之前被执行,导致 CUDA 图记录了无效地址。本 PR 保留性能收益的同时,通过调整语句顺序修复精度问题。

  • 值得精读:本 PR 揭示了 CUDA 图捕获中一个极其微妙且危险的陷阱——host-side 张量元数据修改与 CUDA 图捕获的交互问题。
  • 值得学习的设计权衡:为性能收益(~1%),作者选择了“按顺序即正确”的方案而非更健壮的修复,但记录了明确的注释和未来改进方向。
  • 测试覆盖:虽然无直接新增的单元测试,但通过 test_moe_ep_extra.py 的 gsm8k 测试(精度 > 0.60)验证了正确性。
讨论亮点

无 reviewer 讨论。作者 kpham-sgl 在 PR 描述中详细分析了根因:

  • 排除了“跨流竞争”和“torch.compile flattening”的误判。
  • 明确写入者/读者对:写入者是 pre_permute_standard_to_deep_gemm 中的 hidden_states.set_(empty(0)),读者是共享专家 GEMM 内核启动。
  • 通过二分测试确认:共享专家先执行时精度 0.66,路由先执行时精度 0.005。

实现拆解

  1. 保留流分配方案:沿用 #29142 的流分配——路由专家在主流上执行,共享专家在替代流上执行,使得路由专家成为主流的最后一个内核,可以与 MoE 后的残差相加进行 PDL-fuse。
  2. 调整执行顺序:将共享专家计算块(with torch.cuda.stream(self.alt_stream): shared_output = ...)移动到路由专家管道之前,确保共享专家 GEMM 内核在 CUDA 图捕获时读取到有效的 hidden_states.data_ptr()
  3. 路由专家保持不变:路由管道的 self.gate, self.topk, self.experts() 调用顺序不变,仍在主流上执行。
  4. 同步不变current_stream.wait_stream(self.alt_stream) 仍放在路由专家之后,合并 shared_output 之前。
  5. 关键注释:在 forward_normal_dual_stream 方法头部新增了说明注释,记录必须先启动共享专家的原因,防止未来类似问题。
文件 模块 状态 重要度
python/sglang/srt/models/deepseek_v2.py 模型路由 modified 7.16

关键符号

forward_normal_dual_stream

关键源码片段

python/sglang/srt/models/deepseek_v2.py core-logic

核心变更文件:调整了 `forward_normal_dual_stream` 中共享专家与路由专家的执行顺序,修复了 CUDA 图捕获中的 `data_ptr` 失效问题,保留了路由专家在主流上执行的性能优化。

# python/sglang/srt/models/deepseek_v2.pydef forward_normal_dual_stream(
    self,
    hidden_states: torch.Tensor,
    should_allreduce_fusion: bool = False,
    use_reduce_scatter: bool = False,
    gemm_output_zero_allocator: BumpAllocator = None,
    input_ids: Optional[torch.Tensor] = None,
    input_ids_global: Optional[torch.Tensor] = None,
    *,
    use_flashinfer_trtllm_bypass: bool = False,
) -> torch.Tensor:
    # 关键注释 : 必须在 routed call 之前启动 shared expert.
    # routed 的 deep_gemm 预置中调用 dispose_tensor,
    # 它会将 hidden_states 的 data_ptr 通过 set_() 置为空张量 (host-side 操作 ).
    # 如果 shared expert 的内核在 routed call 之后才启动,则它捕获到 data_ptr == 0,
    # 在 decode CUDA 图重放时会读空地址,导致垃圾输出 .
    current_stream = torch.cuda.current_stream()
    self.alt_stream.wait_stream(current_stream)
    server_args = get_global_server_args()
    dispatch_info = (
        ExpertLocationDispatchInfo.init_new(layer_id=self.layer_id)
        if server_args.enable_eplb
        else None
    )
​
    # 第 1 步 : 在 alt 流上执行 shared expert ( 先执行,确保捕获有效地址 )
    with torch.cuda.stream(self.alt_stream):
        shared_output = self._forward_shared_experts(
            hidden_states, gemm_output_zero_allocator
        )
​
    # 第 2 步 : 在主流上执行 routed expert (gate + topk + experts)
    # 此时 dispose_tensor 才被调用,但 shared_output 已捕获完成
    router_logits = self.gate(hidden_states, gemm_output_zero_allocator)
    # ... topk / experts 调用序列不变 ...
    # ... finalize 和 allreduce 保持不变 ...
    current_stream.wait_stream(self.alt_stream)
    # ... 后续 merge shared_output 和 allreduce ...
    return final_hidden_states

评论区精华

CUDA 图捕获中 dispose_tensor 写后读依赖 正确性

作者在 PR body 中详细分析了根因:dispose_tensor 将 hidden_states 的 data_ptr 置零(host-side),任何后续捕获 shared expert 内核都会记录无效地址。经 bisection 验证,shared first 方案得分为 0.66,routed first 方案得分为 0.005。

结论:结论:将 shared expert 启动移前即可规避,无需更复杂的修复。作者同时指出了更健壮的未来方向:让 dispose_tensor 操作 dispach/permuted 缓冲区而非原始张量。 · 已解决

风险与影响

  1. 时序脆弱性:修复依赖 Python 语句顺序,仍然脆弱。任何未来新增的 dispose_tensor 调用或类似 host-side 修改张量元数据的操作都可能再次触发相同问题。PR 中已指出更健壮的修复方案(让 dispose_tensor 在 dispach/permuted 缓冲区上操作,而非原始张量),但未实施。
  2. 回归风险:仅修改了 deepseek_v2.py 中的控制流顺序,无风险文件。
  3. 性能影响:保留了 #29142 的性能收益(路由专家作为主流最后一个内核,可 PDL-fuse),无性能回归。
  • 用户影响:所有使用 DeepSeek V3 或类似模型且启用 deep_gemm 后端的用户将受益于约 1% 的性能提升(来自 #29142),同时避免精度崩溃。
  • 系统影响:仅影响单文件 deepseek_v2.py,且仅在 forward_normal_dual_stream 路径中生效,其他路径无影响。
  • 团队影响:无额外维护负担。
核心路径变更 时序依赖脆弱 无新增测试 性能收益与精度修复

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论