Prhub

#1871 Rename PortCursors to PortAllocator and key it by the node's ip

原始 PR 作者 fzyzcjy 合并时间 2026-09-04 13:05 文件变更 11 提交数 1 评论 1 代码增减 +78 / -77

执行摘要

将端口分配器内部状态键从节点索引改为节点 IP,提升身份标识的准确性。

PR 标题明确指出变更动机是“key it by the node's ip”。commit message 进一步解释:原键 node_index 是一个基于 rank 布局的算术代理值,而节点 IP 是机器的真实身份,不依赖这种假设。虽然当前行为正确,但使用节点索引作为键是脆弱且粗放的,改为 IP 键提升了实现的精确性和可维护性。

这是一个清晰、内聚的重构 PR,值得精读。其核心决策——将端口分配状态的键从代理的节点索引改为真实的节点 IP——体现了对系统健壮性的主动改进。虽然逻辑简单,但它是整个 Rollout 模块规范化、健壮化进程中的必要步骤,可以作为代码重构中“改善内部表示”的范例学习。

讨论亮点

审核者 guapisolo 在批准评论中确认:“This cleanly migrates port-allocation identity from group-local indices to node IPs; #1872 completes the shared allocator mechanics before the #2176 safe-merge checkpoint, and the behavior remains present through the checked chain tip.” 这表明该变更是更大重构序列(#1837 栈)中的一环,旨在为后续更复杂的共享分配器机制(#1872)做准备,并确保行为在当前链路中保持一致。无其他评论或争议。

实现拆解

  1. 核心逻辑重命名与键类型调整:在 miles/ray/rollout/addr_allocator.py 中,将数据类 PortCursors 重命名为 PortAllocator,其内部 _values 字典的类型从 dict[int, int] 改为 dict[str, int]。函数 allocate_rollout_engine_addr_and_ports_normal 中的局部变量 node_port_cursor 同样调整为 dict[str, int]。
  2. 节点身份获取方式变更:在 allocate_rollout_engine_addr_and_ports_normal 函数中,新增一行 node_ip, _ = ray.get(engine._get_current_node_ip_and_free_port.remote()) 以显式获取节点 IP。内部函数 get_addr_and_ports 的参数名从 node_idx 改为 node_ip,并在其内部使用该 IP 作为 node_port_cursor 字典的键进行存取。函数最终返回更新后的 PortAllocator(_values=node_port_cursor)。
  3. 依赖方同步更新:在 miles/ray/rollout/server_group.py 的 ServerGroup.start_engines 和 recover 方法签名中,将参数 port_cursors 重命名为 port_allocator,并更新其内部调用。在 miles/ray/rollout/rollout_server.py 和 miles/ray/rollout/inference_controller.py 中,所有实例化 PortCursors.empty() 的地方统一替换为 PortAllocator.empty(),并更新相关变量名。
  4. 测试配套更新:tests/fast/ray/rollout/test_addr_allocator.py 中,测试类 TestPortCursors 和 TestSharedPortCursorsAcrossGroups 分别重命名为 TestPortAllocator 和 TestSharedPortAllocatorAcrossGroups,其测试用例中涉及 _values 字典的断言从使用整数索引改为使用 IP 字符串。tests/fast/ray/rollout/real_ray/ 下多个测试文件(如 test_server_group.py, test_fault_tolerance.py)同步更新了导入、实例化调用和 mock 对象中的键类型。
文件 模块 状态 重要度
miles/ray/rollout/addr_allocator.py 端口分配器 modified 7.86
tests/fast/ray/rollout/test_addr_allocator.py 分配器测试 modified 6.89
miles/ray/rollout/server_group.py 服务器组 modified 6.58
miles/ray/rollout/rollout_server.py 服务器启动 modified 6.08

关键符号

PortAllocator.empty PortAllocator.assign PortAllocator.next_base_port allocate_rollout_engine_addr_and_ports_normal ServerGroup.start_engines ServerGroup.recover RolloutServer.recover InferenceController.start_cell

关键源码片段

miles/ray/rollout/addr_allocator.py core-logic

核心变更文件。包含 `PortAllocator` 类定义和 `allocate_rollout_engine_addr_and_ports_normal` 函数中节点身份获取与键值调整的核心逻辑。

from dataclasses import dataclass
​
@dataclass
class PortAllocator:
    # 内部状态:从节点索引 (int) 改为节点 IP (str),以更准确地标识机器身份。
    _values: dict[str, int]
​
    @staticmethod
    def empty() -> "PortAllocator":
        return PortAllocator(_values={})
​
    def assign(self, other: "PortAllocator"):
        self._values = other._values.copy()
​
    def next_base_port(self) -> int:
        # 返回所有已知节点中已分配的最大端口值加一,作为新的起始端口。
        return max(self._values.values()) if self._values else 15000
​
# 在 allocate_rollout_engine_addr_and_ports_normal 函数内部:
# 1. 字典类型调整为 dict[str, int]
node_port_cursor: dict[str, int] = {}
​
# 2. 在遍历引擎时,显式获取节点 IP 作为后续操作的键
node_ip, _ = ray.get(engine._get_current_node_ip_and_free_port.remote())
get_addr, get_port = get_addr_and_ports(engine, node_ip) # 参数名也相应改为 node_ip
​
# 3. 内部闭包使用传入的 node_ip 作为键
start_port = node_port_cursor.get(node_ip, base_port)
# ... 在 port() 闭包中更新 ...
node_port_cursor[node_ip] = start_port
​
# 4. 返回时使用新的 PortAllocator
return addr_and_ports, PortAllocator(_values=node_port_cursor)
tests/fast/ray/rollout/test_addr_allocator.py test-coverage

核心测试文件。测试类和测试数据同步反映了 `PortAllocator` 的键类型变更,是验证重构正确性的关键。

# 测试类重命名并更新断言
class TestPortAllocator: # 原 TestPortCursors
    def test_next_base_port_returns_max_value(self):
        # 测试数据使用 IP 字符串作为键
        c = PortAllocator(_values={"10.0.0.1": 17000, "10.0.0.2": 16500, "10.0.0.3": 18000})
        assert c.next_base_port() == 18000
​
# 测试断言中的键类型变更
class TestAllocateNormal:
    def test_single_node_8_cards_tp1(self, patch_ray_get):
        # ... 前面逻辑 ...
        # 断言:键集合现在是 IP 字符串,而非节点索引
        assert isinstance(cursors, PortAllocator)
        assert set(cursors._values.keys()) == {"10.0.0.1"} # 原为 {0}
        assert cursors._values["10.0.0.1"] >= max(_all_ports(addr_and_ports)) + 1 # 原为 cursors._values[0]
​
class TestSharedPortAllocatorAcrossGroups: # 原 TestSharedPortCursorsAcrossGroups
    def test_sequential_groups_share_cursor_and_avoid_overlap(self, patch_ray_get):
        cursors = PortAllocator.empty() # 原 PortCursors.empty()
        # ... 测试逻辑 ...
miles/ray/rollout/server_group.py core-logic

主要依赖方。其 `start_engines` 和 `recover` 方法的参数和内部变量名同步更新,是连接分配器与服务器组的关键接缝。

from miles.ray.rollout.addr_allocator import (
    PortAllocator, # 从 PortCursors 重命名导入
    allocate_rollout_engine_addr_and_ports_external,
    allocate_rollout_engine_addr_and_ports_normal,
)
​
class ServerGroup:
    def start_engines(
        self, port_allocator: PortAllocator, start_cell_indices: list[int] | None = None # 参数名和类型更新
    ) -> tuple[list, list[int]]:
        """创建 Ray actors,分配端口,并启动 ``engine.init()``。
        就地修改 ``port_allocator`` 以推进到任何新分配端口之后。"""
        # ... 内部逻辑 ...
        base_port = port_allocator.next_base_port() # 变量名更新
        addr_and_ports, next_port_allocator = allocate_rollout_engine_addr_and_ports_normal(
            # ... 其他参数 ...
            base_port=base_port,
        )
        port_allocator.assign(next_port_allocator) # 方法调用和变量名更新
​
    async def recover(self, port_allocator: PortAllocator, filter_cell_indices: list[int] | None = None):
        # ... 内部逻辑 ...
        handles, new_engine_indices = self.start_engines(port_allocator, start_cell_indices=filter_cell_indices) # 变量名更新

评论区精华

PR #1871 作为 #1837 重构序列的一环被批准 设计

审核者 guapisolo 确认:"This cleanly migrates port-allocation identity from group-local indices to node IPs; #1872 completes the shared allocator mechanics before the #2176 safe-merge checkpoint, and the behavior remains present through the checked chain tip."

结论:该变更被认可为更大重构方案(#1837 栈)中的一个干净、正确的步骤,旨在改善端口分配的身份标识,并为后续 PR (#1872) 完善共享分配器机制做准备。行为在当前链路中保持一致。 · 已解决

风险与影响

  1. 逻辑正确性风险:虽然 get_addr_and_ports 函数原本也通过 engine._get_current_node_ip_and_free_port.remote() 获取地址,但新逻辑在函数外显式获取了一次 IP,并将其作为键。需确保两次获取的 IP 一致性,但考虑到这是同一个 Actor 的同一次调用,理论上应返回相同 IP。
  2. 依赖方遗漏风险:本次变更涉及 11 个文件,主要是测试文件。所有 PortCursors 的使用点均已更新为 PortAllocator,且测试数据中的键也从 int 改为 str。风险较低,但需通过 CI 验证无语法或导入错误。
  3. 端口分配行为不变:核心分配算法(如 next_base_port 取最大值逻辑)未变,仅键的标识含义改变。因此,只要所有使用方统一,端口分配结果应与之前完全一致,回归风险低。

此变更属于内部重构,对最终用户和系统外部行为无直接影响。它提升了代码的内部质量和可维护性,使端口分配器的身份标识更准确,为未来支持更灵活的节点拓扑或并行恢复奠定了更坚实的基础。影响范围限于 Miles 的 Ray Rollout 模块内部,特别是端口分配和服务器组管理逻辑。

键类型变更 依赖方更新

关联 Issue

#1837 Tracking issue for refactoring and enhancements

完整报告

参与讨论