Prhub

#34997 Fix world-size-one aliasing in MLP batch sync

原始 PR 作者 merrymercy 合并时间 2026-08-17 15:04 文件变更 1 提交数 1 评论 1 代码增减 +8 / -4

执行摘要

修复 world size 1 下 MLP 批同步缓冲区别名问题

PR body 明确指出:"> At world size one, expand().contiguous() can return storage that still aliases the fallback tensor, so later masked writes can corrupt the fallback values they read." 即当 dp_size == 1tp_size * cp_size == 1 时,expand 后的视图已经连续,contiguous() 成为 no-op,导致 global_info_tensor 与原 fallback_tensor 共享同一块存储。后续 use_all_reduce 路径下的 zero_()、按 rank 写入以及 flat_info[missing] = fallback_tensor 等操作会同时读写同一块内存,破坏 fallback 的原始值,进而污染同步信息。

该 PR 值得精读。虽然改动仅一行核心变更,但它揭示了一个常见的 PyTorch 陷阱:expand() 返回的视图在已连续时,contiguous() 不会拷贝存储。对于编写分布式同步逻辑的工程师,这是一个很有教育意义的案例。建议阅读 all_gather 的整体流程,理解为何 repeat 是正确选择,并留意未来在类似场景中避免使用 expand().contiguous() 构造共享存储。

讨论亮点

该 PR 无实质 review 讨论。作者 merrymercy 在 PR body 中详细解释了根因和修复思路,并在评论中 approve 了自己的 PR(状态为 COMMENTED,内容为 "approve")。唯一的附加评论是触发 CI 重跑的指令 /tag-and-rerun-ci。没有其他开发者提出异议或补充意见,也没有未解决的疑虑。

实现拆解

以下按步骤拆解实现过程:

  1. 定位问题:在 python/sglang/srt/managers/scheduler_components/dp_attn.pyall_gather 方法中,用于聚合全局同步信息的 global_info_tensor 原本通过 fallback_tensor.expand(self.dp_size, self.tp_size * self.cp_size, info_width).contiguous() 构造。当 dp_size == 1tp_size * cp_size == 1 时,expand 产生的视图已连续,contiguous() 不会触发拷贝,因此 global_info_tensorfallback_tensor 共享底层存储。

  2. 修改方案:将缓冲区构造改为 fallback_tensor.repeat(self.dp_size, self.tp_size * self.cp_size, 1)repeat 总是分配新的存储并复制数据,确保 global_info_tensor 完全独立于 fallback_tensor。这样在 use_all_reduce 分支中对 global_info_tensorzero_() 以及后续 flat_info[missing] = fallback_tensor 等写操作不会反向污染 fallback_tensor 的原始值。

  3. 验证方式:作者通过 py_compile 确认语法正确,并用一个 torch 断言脚本验证了 repeat 后的张量拥有独立存储(gathered.zero_() 不影响 fallback)。该脚本模拟了修复前后的行为差异,确认修复有效。

  4. 未做改动:未新增自动化测试用例,也未改动其他逻辑;性能测试未运行,因为该变更仅影响一个小的同步元数据缓冲区的构造开销,可忽略。

文件 模块 状态 重要度
python/sglang/srt/managers/scheduler_components/dp_attn.py 调度器 modified 6.01

关键符号

all_gather

关键源码片段

python/sglang/srt/managers/scheduler_components/dp_attn.py core-logic

核心变更文件,修复 `all_gather` 中缓冲区与 fallback 张量的存储别名问题,直接影响 DP attention 批同步的正确性。

# python/sglang/srt/managers/scheduler_components/dp_attn.py
# 核心改动:构造全局信息缓冲时弃用 expand().contiguous(),改用 repeat()def _get_fallback_tensor(self, device, dtype=torch.int64) -> torch.Tensor:
    # 返回一个默认 fallback 张量,表示空闲 / 无效的同步信息
    return torch.tensor(
        [
            0, # num_tokens
            0, # num_tokens_for_logprob
            1, # can_run_decode_cuda_graph
            0, # is_extend_in_batch
            1, # local_can_run_tbo
            ForwardMode.IDLE.value, # local_forward_mode
            0, # can_run_prefill_cuda_graph
        ],
        device=device,
        dtype=dtype,
    )def all_gather(self, device, group, use_all_reduce=False):
    local_info_tensor = self._get_local_tensor(device=device)
    fallback_tensor = self._get_fallback_tensor(device=device)
    info_width = local_info_tensor.numel()
    # 注意:在 world size 为 1 时,expand().contiguous() 不会拷贝存储,
    # 后续对 global_info_tensor 的写操作会污染 fallback_tensor。
    # 使用 repeat() 强制分配独立存储,避免别名问题。
    global_info_tensor = fallback_tensor.repeat(
        self.dp_size, self.tp_size * self.cp_size, 1
    )
​
    if use_all_reduce:
        # Admission 可能暴露不同的 WORLD size;使用固定全局槽位。
        global_info_tensor.zero_()
        flat_info = global_info_tensor.view(-1, info_width)
        rank = torch.distributed.get_rank(group)
        if 0 <= rank < flat_info.shape[0]:
            flat_info[rank] = local_info_tensor
        torch.distributed.all_reduce(
            global_info_tensor,
            op=torch.distributed.ReduceOp.SUM,
            group=group,
        )
        missing = flat_info.abs().sum(dim=1) == 0
        flat_info[missing] = fallback_tensor
    else:
        torch.distributed.all_gather_into_tensor(
            global_info_tensor.flatten(),
            local_info_tensor,
            group=group,
        )
​
    tp_info = global_info_tensor.view(
        self.dp_size * self.tp_size * self.cp_size, info_width
    )
    # 后续基于 tp_info 判断活跃 rank 等逻辑 ...

评论区精华

PR 自审与 CI 重跑 other

作者 merrymercy 在 PR 描述中详细解释了根因和修复,并在 review 中自行 approve(状态 COMMENTED,内容为 "approve")。唯一额外评论是触发 CI 重跑的指令 `/tag-and-rerun-ci`。

结论:无独立评审意见,修复被作者确认,无未解决疑虑。 · 已解决

风险与影响

风险点如下:

  • 存储别名修复的有效性repeat() 强制拷贝,从语义上消除了别名风险,但需确认在所有调用 all_gather 的场景(如 CPU/GPU、不同分布式配置)中行为一致。变更只影响缓冲区的构造方式,不改变后续聚合逻辑,因此引入新回归的可能性很低。
  • 缺少自动化测试:PR 未添加任何单元测试或回归测试,仅靠作者手动脚本验证。未来若有人重构该段逻辑,可能重新引入类似别名问题而无法被测试捕获。
  • 性能影响repeatexpand().contiguous() 相比多一次内存拷贝,但缓冲区极小(7 个 int64 元素),且仅在每次批同步时构造,开销可忽略。
  • 核心路径变更dp_attn.py 属于调度器组件,all_gather 是批同步的关键路径,任何行为变化都可能影响多卡推理的正确性,尽管本次改动副作用极小。

影响范围集中在使用 DP attention 批同步的场景,尤其是 dp_size == 1tp_size * cp_size == 1 的部署(例如单进程推理或单数据并行组)。修复前,此类场景下同步信息可能被污染,导致调度决策错误(如 can_run_decode_cuda_graphForwardMode 等标志错乱),进而引发推理结果异常或卡顿。修复后,该边界情况得到正确处理。对多数据并行场景(dp_size > 1)来说,原有逻辑已通过 expand().contiguous() 产生独立存储,行为不变。总体影响程度为中等,因为触发了隐蔽的数据损坏 bug,且影响所有使用该路径的部署,但实际故障可能较难复现。

核心路径变更 缺少自动化测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论