Prhub

#33848 [diffusion] resolve IPC A2A peers from process groups

原始 PR 作者 mickqian 合并时间 2026-08-06 23:22 文件变更 3 提交数 1 评论 3 代码增减 +83 / -9

执行摘要

修复 IPC A2A peer 解析,非零 rank 复用快速路径

PR body 明确说明动机:'Non-zero ranks in a two-rank Ulysses pair could resolve the wrong IPC peer, leading to a fallback instead of the intended fast path.' 原实现 IpcA2AState.init1 - dev 推断对端设备,隐含 rank 与设备相邻的假设;在 CFG replica 等场景下 Ulysses 组可落在任意同主机 GPU 对(如 device 2 / device 3),非零 rank 会因此解析错误 peer,使本可走 CUDA-IPC 的快速路径回退到 NCCL。

值得快速精读:_peer_cuda_device 的进程组解析模式是处理 rank 与设备映射问题的干净范例,测试针对性很强。主要关注点在 peer access 初始化流程的异常处理一致性,以及同步 collective 引入的潜在 hanging 风险。

讨论亮点

本 PR 没有技术性 review 评论。唯一的交互是作者三次执行 /tag-and-rerun-ci(含一次 extra)触发远端 CI:常规 PR 测试与 Extra 测试均通过(Run #31109533245 与 Run #31109533019)。因此没有可提炼的设计交锋,但无 review 即合入且双 CI 全绿,说明修复范围明确、风险收敛。

实现拆解

  1. 新增 _peer_cuda_device(group, rank, device) 函数(python/sglang/multimodal_gen/runtime/distributed/device_communicators/ipc_a2a.py):校验 group world size 为 2,通过 dist.all_gather_object 收集每个成员的 (socket.gethostname(), device) 元组,用 group 内 rank 索引定位对端,并依次校验两端均被发现、同主机、本端设备号与传入一致、两端设备不同;任一校验失败抛 _Unsupported,由 ipc_a2a_ready 统一转 NCCL fallback。

  2. 改造 IpcA2AState.init:用 _peer_cuda_device 替换原先硬编码的 1 - dev,对 torch.cuda.can_device_access_peer 增加 RuntimeError 捕获并转为 _Unsupported(携带设备号信息),ctypes.CDLL(...).cudaDeviceEnablePeerAccess(peer_dev, 0) 也改为使用真实 peer 设备。

  3. 调整 ipc_a2a_ready:更新注释说明 initializer 会解析双方真实设备序号、CFG replica 可使用同主机不同 GPU 对;初始化失败的异常日志由 logger.exception 降为 logger.debug + logger.warning,减少生产日志噪音。

  4. 测试与文档配套:test_ipc_a2a_lifecycle.py 新增 test_peer_cuda_device_uses_the_ulysses_group_mapping(mock socket.gethostnamedist.get_world_sizedist.all_gather_object,验证 rank=1、device=3 解析出 peer=2);docs/docs/sglang-diffusion/environment_variables.mdx 补充 SGLANG_DIFFUSION_IPC_A2ASGLANG_DIFFUSION_IPC_A2A_TIMEOUT_MSSGLANG_DIFFUSION_IPC_A2A_MAX_BUFFERS 三个环境变量的说明。

文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/distributed/device_communicators/ipc_a2a.py 通信层 modified 7.19
python/sglang/multimodal_gen/test/unit/test_ipc_a2a_lifecycle.py 通信测试 modified 5.47
docs/docs/sglang-diffusion/environment_variables.mdx 环境变量 modified 3.46

关键符号

_peer_cuda_device IpcA2AState.init ipc_a2a_ready

关键源码片段

python/sglang/multimodal_gen/runtime/distributed/device_communicators/ipc_a2a.py core-logic

核心修复所在:新增 `_peer_cuda_device` 从进程组解析对端设备,替换 `1 - dev` 假设;`init` 与 `ipc_a2a_ready` 同步调整。

# python/sglang/multimodal_gen/runtime/distributed/device_communicators/ipc_a2a.pydef _peer_cuda_device(group, rank: int, device: int) -> int:
    """返回双 rank 同主机 group 中对端 rank 的本地 CUDA 序号。"""
    world_size = dist.get_world_size(group=group)
    # 该传输只支持两 rank 的 Ulysses 对,其他拓扑直接拒绝
    if world_size != 2:
        raise _Unsupported(
            f"requires a two-rank Ulysses group, got world size {world_size}"
        )
​
    # 关键点:用 all_gather_object 交换 ( 主机名 , 设备号 ),
    # 不再假设 rank 索引与 CUDA 设备一一对应
    members: list[tuple[str, int] | None] = [None] * world_size
    dist.all_gather_object(
        members,
        (socket.gethostname(), device),
        group=group,
    )
    local = members[rank]
    peer = members[1 - rank] # 对端就是组内另一个 rank
    if local is None or peer is None:
        raise _Unsupported("could not discover both Ulysses group members")
    # 同主机校验:IPC 映射只在同机 P2P 场景才有意义
    if local[0] != peer[0]:
        raise _Unsupported(
            "requires both Ulysses ranks on the same host "
            f"(got {local[0]!r} and {peer[0]!r})"
        )
    # 本端上报的设备必须与外部传入一致,防止组创建错乱
    if local[1] != device:
        raise _Unsupported(
            f"group rank {rank} reported CUDA device {local[1]}, expected {device}"
        )
    # 两端不能落在同一张卡上
    if peer[1] == device:
        raise _Unsupported(f"both Ulysses ranks are assigned CUDA device {device}")
    return peer[1]
python/sglang/multimodal_gen/test/unit/test_ipc_a2a_lifecycle.py test-coverage

新增 `test_peer_cuda_device_uses_the_ulysses_group_mapping` 覆盖非零 rank 映射,验证修复行为。

def test_peer_cuda_device_uses_the_ulysses_group_mapping():
    group = object()
    members = [("node-a", 2), ("node-a", 3)]
​
    def gather(output, value, *, group):
        # 验证 rank=1 上报的是自己本地的 (node-a, 3)
        assert value == ("node-a", 3)
        output[:] = members
​
    with (
        patch(f"{_IPC}.socket.gethostname", return_value="node-a"),
        patch(f"{_IPC}.dist.get_world_size", return_value=2),
        patch(f"{_IPC}.dist.all_gather_object", side_effect=gather),
    ):
        # rank=1 的 peer 是 rank=0,设备号为 2
        assert _peer_cuda_device(group, rank=1, device=3) == 2

评论区精华

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

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

风险与影响

进程组同步依赖:_peer_cuda_device 内部调用 all_gather_object,是同步 collective。若组内各 rank 对 transport 可用性的判断不一致(例如凭 get_tp_world_size() 或平台判断出现分歧),可能出现部分 rank 等待、部分回退的挂起风险。好在 ipc_a2a_ready 先做统一判断再进入 init,整体一致性较好。主机名不一致:socket.gethostname() 在容器环境可能因网络命名空间不同而返回不同字符串,导致合法同主机对误判为跨主机并回退 NCCL,这是保守方向,影响可控。cudaDeviceEnablePeerAccess 未捕获异常:当两个进程同时启用同一对设备时,驱动可能因重复启用抛错;当前代码没有捕获该调用,若发生会以非 _Unsupported 异常冒泡,最终由 ipc_a2a_ready 兜底降级为 NCCL,但堆栈信息会被 logger.debug 隐藏。日志级别调整:初始化失败从 logger.exception 改为 logger.debug + logger.warning,生产环境噪音减少,但排查问题时需要临时调高 debug 级别。

影响范围:仅 IPC peer 发现逻辑;NCCL fallback 路径未改动。修复后 CFG replica / 非零 rank 场景可稳定走 CUDA-IPC 快速路径,且支持任意同主机 GPU 对(不限于 device 0/1)。对用户而言,多卡 diffusion 推理的延迟不再意外回退;对系统而言,peer 解析从静态假设变为运行时发现,为后续更灵活的 GPU 拓扑铺路;对团队而言,该改动与 diffusion 通信优化系列(#33775 等)形成配套,推进全前向 CUDA Graph 捕获目标。

核心路径变更 进程组 collective 依赖 日志级别调整

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论