Prhub

#45960 [Bugfix] Seed RayExecutorV2 TCPStore port by DP rank to avoid collisions

原始 PR 作者 eicherseiji 合并时间 2026-06-30 22:37 文件变更 2 提交数 15 评论 1 代码增减 +64 / -1

执行摘要

按 DP rank 分配 TCPStore 端口窗口,避免 Ray 数据并行端口冲突

Under data parallel with the Ray V2 executor, each DP engine runs its own RayExecutorV2 and selects the torch.distributed TCPStore port with an independent random get_open_port() in _init_workers_ray. Co-located engines can land on the same port through the pick-release-bind window. A worker then connects to another engine's store on that port and the handshake value does not match, so distributed init fails. 该问题在 DP=8 部署中间歇性出现,引发 Ping failed, invalid value returned from server 错误。

建议阅读 _select_tcpstore_port 的设计:通过静态方法+偏移窗口解决 TOCTOU 问题,兼顾简单性和确定性;测试通过 mock 内部函数实现可测性,值得在类似竞态修复中参考。对于关注分布式可靠性的团队,此 PR 是重要改进。

讨论亮点

核心讨论来自 reviewer rguiu 对代码第 20 行的评论:

"_get_open_port is private. Would it be worth either promoting it to a public function or wrapping the call behind get_open_port with a start_port/max_attempts parameter?"
该评论未获作者直接回复,但最终合并的代码保留了使用私有函数 _get_open_port,并在测试中通过 monkeypatch.setattr(ray_executor_v2, "_get_open_port", ...) 直接模拟该函数。这是一个设计权衡:保持现有 API 不变,测试通过模块级 mock 侵入性略高但可行。

实现拆解

实现拆解如下:

  1. 提取端口选择方法:在 vllm/v1/executor/ray_executor_v2.py 中新增 @staticmethod _select_tcpstore_port(local_dp_rank, master_port),核心逻辑是:若 local_dp_rankNone 则直接随机回退(非 DP 路径);否则以 master_port + 100 + local_dp_rank * 32 为起始端口,在 32 端口窗口内扫描空闲端口,窗口全满时回退随机。
  2. 修改初始化调用:在 _init_executor 中将原来的 get_open_port() 替换为调用 _select_tcpstore_port,传入 parallel_config.data_parallel_rank_localparallel_config.data_parallel_master_port,并赋值给 distributed_init_method
  3. 新增工具函数引入:在 vllm/utils/network_utils.py 中已有 _get_open_port 私有函数(支持 start_portmax_attempts),PR 在 ray_executor_v2.py 增加该符号的导入,用于窗口内扫描。
  4. 编写单元测试:在 tests/distributed/test_ray_v2_executor.py 中新增三个测试函数:test_select_tcpstore_port_seeds_disjoint_windows(验证 4 个 rank 的窗口不重叠)、test_select_tcpstore_port_non_dp_uses_random(验证非 DP 路径)、test_select_tcpstore_port_full_window_uses_random(验证窗口满载回退)。测试使用 monkeypatch 模拟内部端口函数,无需启动 Ray 集群。
文件 模块 状态 重要度
vllm/v1/executor/ray_executor_v2.py 执行器 modified 7.21
tests/distributed/test_ray_v2_executor.py 测试 modified 6.59

关键符号

_select_tcpstore_port _init_executor

关键源码片段

vllm/v1/executor/ray_executor_v2.py core-logic

核心修复文件:新增 `_select_tcpstore_port` 静态方法实现按 DP rank 偏移的端口扫描,并修改 `_init_executor` 调用,避免 TCPStore 端口冲突。

@staticmethod
def _select_tcpstore_port(local_dp_rank: int | None, master_port: int) -> int:
    """为当前引擎选择 `torch.distributed` TCPStore 端口。    同节点上多个数据并行引擎若使用独立随机扫描可能选中同一端口。
    通过按节点局部 DP 等级分配不同的偏移窗口来避免冲突。
    非 DP 引擎和窗口满载时回退到完全随机端口。
    """
    # 非 DP 引擎:`local_dp_rank` 为 `None`,直接使用原有随机方法
    if local_dp_rank is None:
        return get_open_port()
​
    # 每个窗口大小固定为 32 个端口,从 `master_port + 100` 开始
    window = 32
    start_port = master_port + 100 + local_dp_rank * window
​
    # 尝试在窗口内找到空闲端口,若全部占用则回退到随机
    try:
        return _get_open_port(start_port=start_port, max_attempts=window)
    except RuntimeError:
        return get_open_port()

评论区精华

`_get_open_port` 私有函数是否应公开或包装 设计

Reviewer `rguiu` 指出 `_get_open_port` 是私有函数,建议提升为公有或给 `get_open_port` 增加参数。

结论:作者未回复该评论,但最终合并保留使用 `_get_open_port`,测试通过 `monkeypatch` mock 了该函数。 · 已解决

风险与影响

  1. 端口范围假设:窗口起始 master_port + 100 + rank*32 假设 master_port 合理且 master_port+100+max_rank*32 不超出可用端口范围(通常 1024-65535),若 master_port 过大(如接近 65500)可能导致尝试绑定不合法端口而始终抛异常,回退随机后仍可能冲突。
  2. 回退仍保留冲突概率:窗口满载或非 DP 时,回退到原来的随机 get_open_port(),未消除该路径下的冲突可能。
  3. 测试依赖 mock 内部函数:测试假定了 _get_open_port 的行为,若日后该私有函数签名变更或删除,测试会失败;但该风险可控。
  4. DP rank 依赖:依赖 parallel_config.data_parallel_rank_local 正确设置,若配置异常导致 None 则走非 DP 回退,与预期一致。

影响范围:仅影响使用 Ray V2 后端且配置了数据并行(DP > 1)的 vLLM 部署。影响程度:此修复消除了高压力场景(DP=8)下间歇性 TCPStore 端口冲突,提升分布式初始化稳定性。副作用:几乎为零,因为仅对特定配置的端口选择逻辑做了分窗偏移,非 DP 路径无变化。

端口范围假设 回退保留原有冲突风险 测试 mock 内部函数

关联 Issue

#37452 Fix DP coordinator ZMQ port TOCTOU

完整报告

参与讨论