Prhub

#39584 [Fix][PD] Give prefill and decode their own RDMA NICs in disaggregation tests

原始 PR 作者 hnyls2002 合并时间 2026-09-16 06:07 文件变更 12 提交数 7 评论 3 代码增减 +104 / -121

执行摘要

PD 测试按 GPU 索引分配 RDMA 网卡,消除跨 socket 超时

PR body 明确指出根因:PD 测试用 --base-gpu-id 把 decode 侧偏移到后半段 GPU,但 get_rdma_devices_args() 从 CUDA_VISIBLE_DEVICES 推导 NIC,而 PD 测试从不设置该环境变量,于是每次调用都落入盲选 fallback,两侧共享同一对 NIC(mlx5_1, mlx5_3),两者都在 NUMA 0,decode 却跑在 NUMA 1 的 GPU 4-5 上,nvidia-smi topo -m 对每条路径都报告 SYS。更隐蔽的是“只有第一个请求烧满超时,其余请求成功,测试保持绿色、成本不可见”。作者提供了量化证据:同一测试类仅设备列表不同,decode NIC 从 (mlx5_1, mlx5_3)(跨 socket)改为 mlx5_4 后,eval 延迟从 301.33s 降到 2.10s。

值得精读。虽然是纯测试代码,但它是一个“静默性能劣化”的完整诊断案例:从症状(首请求 300s 超时且测试不红)到根因(CUDA_VISIBLE_DEVICES 未参与、盲选 fallback 跨 socket)再到三组对照验证,方法论可以复用到其他 CI 超时排查。值得关注的设计决策:绝对 GPU id 映射、删掉静默回退分支、把三个 helper 合并为一个函数。建议后续跟进两件事:确认 dsv4/hisparse 等覆盖面较小的文件在新映射下无明显回归,并考虑把 inkling 那类“range 来自 COMMON_ARGS”的注释提升为 fixture 约定。

讨论亮点

本 PR 没有人工 review 评论(comments_count=3 均为 CI bot 对 /rerun-test 的响应),核心论证全部写在 PR body 里,质量很高,值得提炼:

  • 根因链条完整:CUDA_VISIBLE_DEVICES 未设置 → 盲选 fallback → 共享 (mlx5_1, mlx5_3) → 全路径 SYS 拓扑 → 首次 KV 传输 300s 超时。
  • 三组对照实验锁定变量:同一测试类只改设备列表,8 设备自动检测 2.47s、4 设备修复前 301.33s、4 设备修复后 2.10s。
  • 作者明说此类问题难以被发现:“只有第一个请求烧满超时,其余请求成功,测试保持绿色、成本不可见”——这是测试基建中典型的静默膨胀。
  • commit efca03f 是作者自审修正:inkling_mxfp8 的 tp 来自 COMMON_ARGS 而非硬编码,GPU 范围最初推断错误,已改为 range(2)/range(2,4)。

实现拆解

变更入口是 python/sglang/test/server_fixtures/disaggregation_fixture.py,随后向 11 个覆写 start_prefill/start_decode 的测试文件同步替换。分四步:

  1. 重写 get_rdma_devices_args()(fixture 内模块级函数):新增可选参数 gpu_indices,缺省时回退到 CUDA_VISIBLE_DEVICES;GPU id 先经 sorted({int(g) for g in ...}) 归一化;核心映射改为 min(gpu // gpus_per_rdma, n_rdma - 1),直接用节点级绝对 GPU id 整除映射,不再用组内相对索引换算。同时删除两个问题分支:len(gpu_indices) > 4 的静默回退(它让 8 卡环境永远失去亲和性)和仅发警告的 base_rdma_group 校验。total_gpus 探测失败时用 max(8, gpu_indices[-1] + 1) 兜底,保证映射不为 0。
  2. 新增 rdma_devices_for() 夹具方法:PDDisaggregationServerBase.rdma_devices_for(cls, gpu_indices) 在 CI 下返回 ["--disaggregation-ib-device", get_rdma_devices_args(gpu_indices)],非 CI 直接返回静态 cls.rdma_devices(行为与原先一致)。fixture 默认的 start_prefill/start_decode 分别传 range(prefill_tp_size) 与 range(decode_base_gpu_id, decode_base_gpu_id + decode_tp_size),与 --base-gpu-id 语义对齐。
  3. 批量替换 42 处覆写点:test_disaggregation_different_tp.py(8 组 TP 2/4 组合)、test_epd_disaggregation.py(encode/prefill/decode 三角色在 GPU 0/1/2 各自独立,prefill 传 range(1,2)、decode 传 range(2,3))、test_disaggregation_hybrid_attention.py、test_disaggregation_dp_attention.py(用 PREFILL_DP_SIZE/DECODE_DP_SIZE 类常量推导 range)、test_disaggregation_nixl.py(基类 NixlPDDisaggregationServerBase 也换用)、test_disaggregation_pp.py、test_disaggregation_inkling_mxfp8.py(范围由 COMMON_ARGS 中的 --tp 2 决定,prefill range(2)、decode range(2,4),并加注释说明)、test_disaggregation_aarch64.py、test_disaggregation_decode_offload.py、test_disaggregation_dsv4.py、test_disaggregation_hisparse.py。
  4. 验证配套:base-c-test-8-gpu-h20 三个 partition 全绿,different_tp 1184s → 859s,StagingRadixPrefillLargerTP 的 eval 从 301.3s 到 3.87s;另在 8-gpu-h200、4-gpu-h100、8-gpu-b300、4-gpu-b200 与 2-gpu-large 跑通,并对 hybrid_attention、hisparse、decode_radix_cache_swa、inkling_mxfp8、dwdp_gpt_oss、decode_offload、aarch64 执行 /rerun-test 全部通过。AMD 测试不受影响:其各类均自行实现 start_prefill/start_decode 并直接读 cls.rdma_devices,不经过本 PR 改动的 fixture 默认路径。
文件 模块 状态 重要度
python/sglang/test/server_fixtures/disaggregation_fixture.py 测试夹具 modified 6.71
test/registered/disaggregation/test_disaggregation_different_tp.py TP 差异 modified 4.49
test/registered/disaggregation/test_epd_disaggregation.py 编码拆分 modified 4.39
test/registered/disaggregation/test_disaggregation_hybrid_attention.py 混合注意力 modified 4.33
test/registered/disaggregation/test_disaggregation_dp_attention.py DP 注意力 modified 3.63
test/registered/disaggregation/test_disaggregation_nixl.py NIXL 后端 modified 3.63
test/registered/disaggregation/test_disaggregation_pp.py PP 拆分 modified 3.63
test/registered/disaggregation/test_disaggregation_inkling_mxfp8.py MXFP8 用例 modified 3.49
test/registered/disaggregation/test_disaggregation_decode_offload.py 解码卸载 modified 3.43
test/registered/disaggregation/test_disaggregation_aarch64.py ARM 测试 modified 3.43
test/registered/disaggregation/test_disaggregation_dsv4.py DSV4 用例 modified 3.43
test/registered/disaggregation/test_disaggregation_hisparse.py 稀疏注意力 modified 3.43

关键符号

rdma_devices_for get_rdma_devices_args

关键源码片段

python/sglang/test/server_fixtures/disaggregation_fixture.py test-coverage

核心变更文件:重写 get_rdma_devices_args() 改用绝对 GPU id 映射,新增 rdma_devices_for() 夹具方法并在默认 start_prefill/start_decode 中接入,删除盲选回退与 base_rdma_group 校验,函数从 141 行压缩到 57 行。

# python/sglang/test/server_fixtures/disaggregation_fixture.py
​
@classmethod
def rdma_devices_for(cls, gpu_indices) -> list:
    """返回某个服务进程实际占用 GPU 对应的 `--disaggregation-ib-device` 参数。
​
    非 CI 环境沿用静态配置 `cls.rdma_devices`(行为与旧代码一致);
    CI 环境按绝对 GPU id 实时推导,让 prefill 与 decode 各用各的 NIC,
    避免 decode 侧(`--base-gpu-id` 偏移)重新映射回 NUMA 0 的第一批网卡。
    """
    if not is_in_ci():
        return cls.rdma_devices
    return ["--disaggregation-ib-device", get_rdma_devices_args(gpu_indices)]
​
​
def get_rdma_devices_args(gpu_indices=None) -> str:
    """按节点级绝对 GPU 索引选择 RDMA NIC,返回逗号分隔的设备名。
​
    旧实现从 `CUDA_VISIBLE_DEVICES` 推导,且 8 卡环境会落入“首对网卡”盲选,
    导致 PD 测试两侧共用同一对 NIC、跨 socket 传输烧满 300s 超时。
    新实现由调用方传入该服务实际占用的 GPU id(与 `--base-gpu-id` 对齐)。
    """
​
    def _parse_list_env(var_name: str):
        # 解析逗号分隔的环境变量,空值统一返回 None
        val = os.getenv(var_name)
        items = [x.strip() for x in (val or "").split(",") if x.strip()]
        return items or None
​
    # 设备来源优先级:环境变量 > 自动探测 > 硬编码兜底
    rdma_all_devices = (
        _parse_list_env("SGLANG_CI_RDMA_ALL_DEVICES")
        or _get_available_ib_devices()
        or [f"mlx5_roce{i}" for i in range(8)]
    )
    n_rdma = len(rdma_all_devices)
​
    # 未显式传参时回退到 CUDA_VISIBLE_DEVICES,并统一排序去重
    if gpu_indices is None:
        gpu_indices = _parse_list_env("CUDA_VISIBLE_DEVICES") or []
    try:
        gpu_indices = sorted({int(g) for g in gpu_indices})
    except ValueError:
        warnings.warn(f"Invalid GPU indices: {gpu_indices}")
        gpu_indices = []
    if not gpu_indices:
        # 没有任何可用索引时只能取首对 NIC(原盲选 pair 兜底)
        return ",".join([rdma_all_devices[0], rdma_all_devices[n_rdma // 2]])
​
    # 用节点 GPU 总数估算每个 NIC 服务的 GPU 数;探测失败时按最大索引兜底
    try:
        import torch
​
        total_gpus = torch.cuda.device_count()
    except Exception:
        total_gpus = 0
    total_gpus = total_gpus or max(8, gpu_indices[-1] + 1)
​
    gpus_per_rdma = max(1, total_gpus // n_rdma)
    # 关键点:用绝对 GPU id 整除映射,而不是组内相对索引,
    # 这样 decode 侧(GPU 4-5)会落到后半段 NIC(如 mlx5_4),与 NUMA 拓扑对齐
    devices = [
        rdma_all_devices[min(gpu // gpus_per_rdma, n_rdma - 1)] for gpu in gpu_indices
    ]
    # 去重并保持顺序,同一侧不重复传同一 NIC
    resolved = ",".join(dict.fromkeys(devices))
    logger.warning(
        "RDMA for gpus=%s: total_gpus=%d n_rdma=%d -> %s",
        gpu_indices,
        total_gpus,
        n_rdma,
        resolved,
    )
    return resolved

评论区精华

inkling_mxfp8 的 GPU 范围推断自审修正 正确性

提交 efca03f7da832e36187d9e1d1513d78fe86760b9 显示作者发现该用例的 tp 来自 COMMON_ARGS(--tp 2)而非硬编码,因此 GPU 范围不应按单卡推断,需要与 COMMON_ARGS 对齐。

结论:采用 range(2) 与 range(2, 4),并在代码中加注释说明该依赖关系。 · 已解决

盲选 fallback 与跨 socket 超时的量化验证 设计

作者在 PR body 中用三组对照(8 设备自动检测 2.47s vs 4 设备修复前 301.33s vs 修复后 2.10s)证实根因,并指出该问题因只有首请求超时而长期不可见。

结论:确认 NIC 映射必须基于绝对 GPU id,并删除 len(gpu_indices) > 4 的静默回退分支。 · 已解决

风险与影响

  • 拓扑假设:新映射假定 NIC 均匀分摊 GPU(gpus_per_rdma = total_gpus // n_rdma),对非对称 NUMA/NIC 布局的机器仍可能选错 NIC;但相比原先必错(固定首对 NIC)已有本质改善,且映射结果会打印 warning 日志便于核对。
  • 隐式耦合:test_disaggregation_inkling_mxfp8.py 的 range 依赖 COMMON_ARGS 中的 --tp 2,若 COMMON_ARGS 调整需同步改 range,属隐蔽耦合点。
  • 覆盖面:dsv4、hisparse、decode_offload、aarch64 等文件改动最小(各 2 行),未逐一给出实测数据,主要依赖 base-c 全绿兜底。
  • 无生产代码变更:所有改动限于 python/sglang/test/server_fixtures 与 test/registered 下,不影响运行时行为;非 CI 环境路径完全不变(rdma_devices_for 直接返回 cls.rdma_devices)。
  • 对用户/系统:零生产影响,全部改动在测试侧。
  • 对 CI:8-gpu-h20 lane 的 test_disaggregation_different_tp.py 单测耗时降约 27%(1184s → 859s),消除了 PD 测试第一个请求隐藏的 300s 超时等待;同类收益预计覆盖所有走 fixture 默认路径的 Mooncake PD 测试。
  • 对团队:为后续 PD 测试建立新约定——凡覆写 start_prefill/start_decode 的类都应改用 cls.rdma_devices_for(range(...)) 并按实际 GPU 占用传参,否则会退回盲选 pair;fixture 默认实现已示范正确用法。
  • 对可观测性:删除的 base_rdma_group 警告与 >4 GPU 静默回退使配置路径更清晰,映射日志(RDMA for gpus=...)让网卡选择可审计。
测试基础设施变更 NIC 映射依赖均匀拓扑假设 INKLING 用例与 COMMON_ARGS 隐式耦合 无生产代码路径变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论