Prhub

#50969 [BugFix] Bind RayExecutorV2 TCPStore before publishing its port

原始 PR 作者 aoshen02 合并时间 2026-08-29 03:27 文件变更 2 提交数 9 评论 12 代码增减 +57 / -105

执行摘要

修复 RayExecutorV2 TCPStore 绑定竞态,rank 0 先绑定再发布。

PR 描述中明确了两类正确性问题:其一,多节点 placement 时 driver 探测的是自身主机的端口占用状态,而 rank 0 可能位于另一节点,探测结果无效;其二,即使探测移到 rank 0,get_open_port() 在返回前已关闭 socket,在 init_process_group 创建 TCPStore 之前存在 TOCTOU 竞态窗口,且该窗口覆盖 Ray RPC 回 driver 与全部 worker 初始化分发,并发冷启动时碰撞概率显著上升。修复目标是让端口选择与绑定在同一原子动作内完成,并让绑定方持有端口直到发布完成。

值得精读。核心设计决策"先绑定后发布"(bind-then-publish)是消除 TOCTOU 竞态的通用模式,可复用到其他需要发布网络地址的分布式服务;PR 对 multi_tenant TCPStore 复用机制的分析展示了如何在不改变 rendezvous 协议的前提下消除竞态窗口。同时注意 review 提出的测试覆盖缺口,可结合 follow-up 观察该问题域在 vLLM 中的后续演进。

讨论亮点

合并者 AndreasKaratzas 批准后提出唯一实质讨论:他认可旧端口窗口专属测试可以删除,但 test_colocated_tcpstore_ports_differ 保护的是外部可见不变量(同节点多引擎端口互不相同),并非纯实现细节;新测试 test_dist_init_store_owns_port 只验证单 store 的所有权与复用,不再直接保护同节点引擎碰撞场景。他要求在合入后以 follow-up 方式用新的 create_dist_init_method() 路径恢复覆盖(例如创建两个 rank 0 worker/store 并断言其临时端口不同)。作者未在评论中回复,PR 已合并,follow-up 是否创建未在材料中体现。

实现拆解

  1. 变更入口:vllm/v1/executor/ray_executor_v2.py 中 RayWorkerProc 的初始化路径,这是 Ray V2 后端所有 worker 的公共构造基类。
  2. 移除 driver 侧端口选择:删除 _select_tcpstore_port 静态方法以及 _get_open_port、get_open_port 导入;RayWorkerProc.init 移除 distributed_init_method 参数,改为保存 vllm_config.parallel_config 供后续创建 store 使用。
  3. rank 0 绑定并持有 server:新增 create_dist_init_method(),在 rank 0 actor 内创建 TCPStore(host_name=host, port=0, world_size=..., is_master=True, wait_for_workers=False, multi_tenant=True),由内核原子选择并绑定端口;store 保存在 self._dist_init_store 上以保持监听 socket 存活,函数返回 get_distributed_init_method(host, store.port) 形式的 tcp:// 地址。
  4. 运行时注入地址:initialize_worker 新增 distributed_init_method 形参,在 GPU 映射确定、真正构造 WorkerProc 时才把该地址写入 _init_kwargs,确保不会在端口竞态窗口内提前启动 worker。
  5. 测试配套:tests/distributed/test_ray_v2_executor.py 删除 4 个针对 _select_tcpstore_port 的单元测试(含 #53666 引入的 test_colocated_tcpstore_ports_differ),新增 test_dist_init_store_owns_port 断言三个不变量:发布端口非零、外部 socket 绑定同一端口得到 EADDRINUSE、第二个 master TCPStore 可复用 server 并交换 key。
文件 模块 状态 重要度
vllm/v1/executor/ray_executor_v2.py 执行器 modified 7.79
tests/distributed/test_ray_v2_executor.py 执行器测试 modified 6.86

关键符号

create_dist_init_method initialize_worker _select_tcpstore_port test_dist_init_store_owns_port

关键源码片段

tests/distributed/test_ray_v2_executor.py test-coverage

测试配套:删除 4 个针对 _select_tcpstore_port 的单元测试,新增 test_dist_init_store_owns_port 验证端口被 rank 0 持有、EADDRINUSE 与 multi_tenant 复用。

def test_dist_init_store_owns_port(monkeypatch):
    """rank 0 actor 持有的 TCPStore 端口在发布后仍保持绑定。"""
    # 通过 __new__ 绕过完整 WorkerProc 初始化,只构造带最小配置的实例
    worker = ray_executor_v2.RayWorkerProc.__new__(ray_executor_v2.RayWorkerProc)
    worker._parallel_config = SimpleNamespace(world_size=1)
    monkeypatch.setattr(ray.util, "get_node_ip_address", lambda: "127.0.0.1")
​
    init_method = worker.create_dist_init_method()
    port = urlsplit(init_method).port
    assert port is not None and port != 0 # 端口非零,说明已绑定真实端口
​
    # 外部进程尝试绑定同一端口必须失败,证明端口被 store 持有
    with socket.socket() as contender, pytest.raises(OSError) as exc_info:
        contender.bind(("127.0.0.1", port))
    assert exc_info.value.errno == errno.EADDRINUSE
​
    # multi_tenant 模式下第二个 master store 可复用同一 server 并交换数据
    reused_store = TCPStore(
        "127.0.0.1",
        port,
        world_size=1,
        is_master=True,
        wait_for_workers=False,
        multi_tenant=True,
    )
    worker._dist_init_store.set("key", "value")
    assert reused_store.get("key") == b"value"

评论区精华

保留同节点多引擎端口不冲突的测试覆盖 测试

合并者 AndreasKaratzas 指出:旧端口选择专属测试可以合理消失,但 test_colocated_tcpstore_ports_differ 保护的是外部可见不变量(同一节点多个引擎的 TCPStore 端口互不相同),不是实现细节;新测试 test_dist_init_store_owns_port 只验证单个 store 的所有权与复用,不再直接保护同节点引擎碰撞场景。

结论:先合入本 PR 解决 CI 失败,要求作者开 follow-up 使用新的 create_dist_init_method() 路径恢复覆盖,例如创建两个 rank 0 worker/store 并断言其持有的临时端口不同。 · 已合并,follow-up 状态未在材料中确认

风险与影响

  1. 核心路径变更:RayExecutorV2 是 vLLM 主要 Ray 后端之一,改动影响所有 Ray V2 分布式冷启动路径,位于初始化关键链路上。
  2. 测试覆盖缺口:test_colocated_tcpstore_ports_differ 被删除且未在合并前重建等价覆盖,同节点多引擎并发冷启动场景暂时缺少直接防护测试,已要求 follow-up。
  3. 依赖 PyTorch 语义:方案依赖 multi_tenant=True 的 server 复用行为;作者验证了 use_libuv 四种组合在 Linux 上均复用成功,但其他平台未验证,若 PyTorch 变更该语义可能导致初始化失败。
  4. 兼容性:legacy Ray V1 executor 有意保持不变,提交历史显示曾尝试扩展到 Ray V1 后被 revert,保证本 PR 范围收敛。
  5. 端口生命周期:rank 0 actor 持有 store 至进程结束,端口在引擎生命周期内不再释放,频繁创建/销毁引擎的场景需关注端口占用。

用户侧:多节点 Ray 部署与并发引擎冷启动时偶发的 EADDRINUSE 失败被消除,分布式初始化可靠性提升。系统侧:初始化路径减少一次 driver 到 rank 0 的端口协商往返,端口生命周期从"探测-发布-绑定"变为"绑定-发布-复用",竞态窗口彻底关闭。团队侧:Ray V2 executor 代码净删约 47 行,端口窗口分配机制(#45960/#53666 路线)被更简单的内核原子绑定取代,但需跟进维护者提出的测试补强。

核心初始化路径变更 同节点碰撞测试覆盖待补齐 依赖 PyTorch multi_tenant 语义 #53666 补丁被取代并移除

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论