Prhub

#39611 [CI][Disaggregation] Fix EADDRINUSE flake in test_disaggregation_dwdp_gpt_oss

原始 PR 作者 ShangmingCai 合并时间 2026-09-15 23:05 文件变更 1 提交数 1 评论 2 代码增减 +25 / -63

执行摘要

拆分测试改走基类启动器,修复 NCCL 端口竞争

该注册测试在 CI 的 setUpClass 阶段反复 flake:日志显示 torch.distributed.DistNetworkError: The server socket has failed to listen on any local network address. port: 45623 ... EADDRINUSE,随后 RuntimeError: Rank 0 scheduler died during initialization (exit code: -3) 与 RuntimeError: Server process exited with code 1。PR 描述指出基类 PDDisaggregationServerBase 本就为规避该问题固定了两侧 rendezvous 端口(注释写明 Pin distinct nccl (torch.distributed rendezvous) ports below the ephemeral range; otherwise both sides derive them from get_free_port() and can race onto the same port),但该测试自建 launcher,参数列表中缺少 --nccl-port,两侧退回 PortArgs.init_new 里绑 0 号端口取回的临时端口并立即释放,背靠背启动时内核可能把同一端口分给两侧;prefill 侧是非 dp-attention 分支,没有 wait_port_available 预检,因此冲突没有被更早拦住。

值得精读(10 分钟内可读完),重点不在代码体量而在根因链路:测试 launcher 被覆写 → --nccl-port 丢失 → get_free_port() 返回临时端口后立刻释放 → 背靠背启动的两侧抢到同一端口 → init_process_group EADDRINUSE。建议关注三个设计决策:一是“声明差异、复用基类启动”的 fixture 设计模式;二是端口固定值与 dp-attention 派生端口段之间的边界核算;三是 launch_all() 的 fail-fast 看护。同时建议尽快补一个同源 PR 修复 test_disaggregation_dp_attention.py,否则同一类 flake 会在别的队列上重演。

讨论亮点

本 PR 没有人工 review 评论(review_comments_count = 0),作者自行合并,讨论集中在与 PR 关联的 issue 区:

ShangmingCai:/rerun-test registered/disaggregation/test_disaggregation_dwdp_gpt_oss.py

github-actions[bot]:4-gpu-b200(1 test):✅ View workflow run

即修复的实证来自 CI 复跑通过,而非代码评审;此外 PR 描述里的 Note 是唯一实质性的设计讨论点——test_disaggregation_dp_attention.py 有 identical latent bug,作者表示 happy to fold it in if reviewers prefer,但最终无人跟进,该隐患仍留在 main 上。

实现拆解

  1. 删除自建 launcher:移除测试内的 start_prefill/start_decode 以及 setUpClass 中手工的起服、wait_server_ready 健康检查和 launch_lb() 序列,同时删掉只服务于这条路径的 DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH、popen_launch_pd_server 导入,共减少 63 行。原因是自建启动器会重建整个参数列表,无法继承基类的端口固定、transfer 后端与 RDMA 设备拼装逻辑。
  2. 改为声明式配置:新增类属性 prefill_tp_size = NUM_PREFILL_GPUS、decode_tp_size = NUM_DECODE_GPUS、decode_base_gpu_id = NUM_PREFILL_GPUS,并用 extra_prefill_args(--dwdp-size 2、--disable-flashinfer-autotune、--mem-fraction-static 0.85)与 extra_decode_args(--dp 2、--enable-dp-attention、--disable-flashinfer-autotune、--mem-fraction-static 0.85)只描述与基类默认值的差异。这样 --trust-remote-code、--disaggregation-mode、--disaggregation-bootstrap-port、--tp、--nccl-port、transfer_backend、rdma_devices 全部由 PDDisaggregationServerBase 补齐。
  3. 收敛到 launch_all():setUpClass 精简为 super().setUpClass() → 设置 cls.model → cls.launch_all(),与 test_disaggregation_basic.py 的写法保持一致。基类会统一拉起 prefill/decode/load balancer、做就绪等待,并额外安装子进程 fail-fast 看护,使服务端中途死亡立刻暴露,而不是表现成下游超时。
  4. 端口冲突的收口验证:改动后命令行新增 prefill --nccl-port 11300、decode --nccl-port 11400,二者在临时端口范围之外,且不与 decode 侧 dp-attention 派生端口段(11433-11439)、bootstrap 端口(11500)和 HTTP 端口重叠,因此根因被真正消除而不是被掩盖。
  5. 测试/配套改动:本 PR 无源码主路径改动,也无新增测试文件,验证方式为在关联 issue 中触发 /rerun-test registered/disaggregation/test_disaggregation_dwdp_gpt_oss.py,4-gpu-b200 上 1 个测试通过。
  6. 已知未处理项:test_disaggregation_dp_attention.py 存在完全相同的潜在缺陷(同样覆写两个 launcher 且缺 --nccl-port),作者在 PR 描述中明确说明为控制范围未一并修复,留给后续 PR。
文件 模块 状态 重要度
test/registered/disaggregation/test_disaggregation_dwdp_gpt_oss.py 拆分测试 modified 6.42

关键符号

TestDisaggregationDWDPGptOss.setUpClass TestDisaggregationDWDPGptOss.start_prefill TestDisaggregationDWDPGptOss.start_decode PDDisaggregationServerBase.launch_all PDDisaggregationServerBase.setUpClass

关键源码片段

test/registered/disaggregation/test_disaggregation_dwdp_gpt_oss.py test-coverage

唯一改动文件:删除自建的 start_prefill/start_decode,改由 PDDisaggregationServerBase 的 launch_all() 起服,从而恢复 --nccl-port 固定,消除 EADDRINUSE flake。

class TestDisaggregationDWDPGptOss(PDDisaggregationServerBase):
    """PD 拆分:prefill 侧 DWDP(2 GPU),decode 侧 DP attention(2 GPU)。"""
​
    NUM_PREFILL_GPUS = 2
    NUM_DECODE_GPUS = 2
​
    # 复用基类 fixture 的启动器,而不是在测试里重写 start_prefill / start_decode。
    # 只有走基类路径,两侧才会带上固定好的 --nccl-port;否则各自调用 get_free_port()
    # 会拿到同一个临时端口,并在 init_process_group 阶段以 EADDRINUSE 崩掉。
    prefill_tp_size = NUM_PREFILL_GPUS
    decode_tp_size = NUM_DECODE_GPUS
    decode_base_gpu_id = NUM_PREFILL_GPUS
​
    # 只声明本用例与基类默认值的差异项:--trust-remote-code、--disaggregation-mode、
    # --disaggregation-bootstrap-port、--tp、--nccl-port、transfer_backend、
    # rdma_devices 等公共参数全部交由基类补齐,避免再次漏参数。
    extra_prefill_args = [
        "--dwdp-size",
        str(NUM_PREFILL_GPUS),
        "--disable-flashinfer-autotune",
        "--mem-fraction-static",
        "0.85",
    ]
    extra_decode_args = [
        "--dp",
        str(NUM_DECODE_GPUS),
        "--enable-dp-attention",
        "--disable-flashinfer-autotune",
        "--mem-fraction-static",
        "0.85",
    ]
​
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.model = GPT_OSS_MODEL_PATH
        # launch_all() 依次拉起 prefill / decode / load balancer 并等待就绪,
        # 同时注册子进程 fail-fast 看护:服务端中途死亡会立刻报错,
        # 而不是退化成下游的健康检查超时。
        cls.launch_all()
​
    def test_gsm8k(self):
        metrics = run_eval(
            SimpleNamespace(
                base_url=self.base_url,
                model=self.model,
                eval_name="gsm8k",
                api="chat",
                num_shots=5,
                num_examples=100,
                max_tokens=4096,
                num_threads=8,
                repeat=1,
                temperature=0.0,
                top_p=1.0,
                host="http://127.0.0.1",
                port=int(self.base_url.split(":")[-1]),
            )
        )
        # 精度门限与修复前保持一致,说明本次只是启动方式的调整。
        self.assertGreaterEqual(metrics["score"], GSM8K_BASELINE_ACCURACY)
# 基类 PDDisaggregationServerBase 中被本次修复“重新接回”的端口固定逻辑
# (引自 PR 描述,用于说明根因):把两侧的 torch.distributed rendezvous 端口
# 钉在临时端口范围之外,否则两侧各自 get_free_port() 会抢到同一端口,
# 在 init_process_group 处以 EADDRINUSE 失败。
# 该基类逻辑原本被测试覆写的 launcher 绕过,因此进程只能退回
# PortArgs.init_new 的 get_free_port() 路径(无 wait_port_available 预检)。
cls.prefill_nccl_port = f"{int(base_port) + 300}" # 本用例下为 11300
cls.decode_nccl_port = f"{int(base_port) + 400}" # 本用例下为 11400

评论区精华

CI 复跑验证修复效果 测试

作者在关联 issue 中触发 `/rerun-test registered/disaggregation/test_disaggregation_dwdp_gpt_oss.py`,机器人回报在 `4-gpu-b200` 上 1 个测试通过(View workflow run)。

结论:修复在目标 runner 上验证有效,EADDRINUSE 未再现;但样本仅一次复跑,长期稳定性仍依赖后续 CI 观察。 · 已解决

test_disaggregation_dp_attention.py 存在相同潜在缺陷 正确性

PR 描述的 Note 指出 `test/registered/disaggregation/test_disaggregation_dp_attention.py` 同样覆写了两个 launcher 且省略 `--nccl-port`,作者强调为保持 PR 聚焦于真正失败的测试而暂不合并修复,并表态 `happy to fold it in if reviewers prefer`。

结论:未解决:无 reviewer 回应,该隐患仍留在 main 上,同类 flake 可能在别的 CI 队列复现。 · unresolved

缺少人工代码评审,作者自合并 other

`review_comments_count = 0`、无 Review 记录,PR 由作者 ShangmingCai 自行合并;核心结论(端口不冲突、命令行只多 `--nccl-port`)均由 PR 描述自证。

结论:对测试类小改动可接受,但设计层面的“测试夹具复用 vs 重写 launcher”模式未经过第二人确认。 · 已解决

风险与影响

  1. 启动参数控制权转移:起服命令行改由 PDDisaggregationServerBase.launch_all() 拼接,若基类后续调整默认参数(新增/改默认 flag、改超时、改 transfer backend 拼装顺序),该测试会静默跟随变化,测试与“实际起服命令”之间的可读性下降,需要靠基类单测兜底。
  2. 端口段重叠需持续关注:新增的 --nccl-port 取值为 11300/11400,目前与 decode 侧 dp-attention 派生块(11433-11439)、bootstrap(11500)不冲突;一旦基类的 base_port 或派生偏移调整(例如 +300/+400 改成其他步长),这份“不冲突”的结论需要重新核对。
  3. 同类隐患未修:test/registered/disaggregation/test_disaggregation_dp_attention.py 仍覆写 launcher 并省略 --nccl-port,仍可能在 CI 上以同样的 EADDRINUSE 形式 flake,属于本次修复的残留缺口。
  4. fail-fast 看护带来的行为变化:launch_all() 会在服务端进程死亡时立刻失败。这对 CI 是改进,但会让过去靠等待超时“偶然混过”的场景变成显式失败,短期内可能看到更多红色流水线。
  5. 无生产代码风险:改动严格限定在测试文件内(+25/-63),不触及调度器、传输层或 KV 路径,回归风险仅限于该用例自身的覆盖范围。

对 CI:消除了 4-gpu-b200 extra-b 队列中一个可复现的初始化阶段 flake,减少无意义的 rerun 和排查成本;对开发者:确立了“DWDP/拆分类测试复用基类 launch_all()”的推荐模式,后续新增拆分测试可直接照抄,避免再次踩端口竞争;对产品运行时:零影响,用户侧服务、性能与精度行为均不改变;对团队:由于无人工 review、作者自合并,修复的正确性主要依赖 CI 复跑证据,建议把防御性写法的经验沉淀到基类文档或测试写作规范中。

仅测试代码改动 同类端口隐患未修 启动参数改由基类拼接 缺少人工 review

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论