Prhub

#2016 Fully support --rollout-external-engine-addrs

原始 PR 作者 zhuzilin 合并时间 2026-06-04 22:05 文件变更 19 提交数 17 评论 0 代码增减 +963 / -108

执行摘要

全量支持 --rollout-external-engine-addrs 外部引擎

之前只部分支持 --rollout-external,但外部引擎的发现和注册逻辑分散且不完整。为支持 delta 权重更新等非 NCCL 同步场景,需要让 slime 能灵活对接用户自己启动的 SGLang 引擎池(例如 PD 分离集群)。PR 通过统一的 discovery+apply 模式完整实现了外部引擎接入。

建议精读 slime/backends/sglang_utils/external.py 的设计,特别是如何通过 HTTP 自发现外部引擎属性并动态注册到路由器。slime/ray/placement_group.py 中外部引擎模式的 GPU 布局调整值得关注,这是将外部资源融入 Ray 集群的典型案例。slime/backends/sglang_utils/sglang_engine.py 中的路由器注册重构也值得参考。

讨论亮点

该 PR 未产生 review 评论,无公开讨论记录。不过通过 17 次提交的演进可以看到作者多次修复了参数移除导致的问题、delta 权重同步适配以及 CI 相关问题。

实现拆解

步骤 1:新建 external.py 模块

slime/backends/sglang_utils/external.py 中定义了 ExternalEngineInfo 数据类,包含 url、host、port、worker_type、num_gpus、disaggregation_bootstrap_port 和 server_info 字段。实现了 normalize_external_engine_addr 地址标准化(host:port → http://host:port)。get_server_info 通过 HTTP GET 请求 /server_info/get_server_info 端点获取引擎元信息。_infer_worker_type 根据 server_info 的 disaggregation_mode 判断 worker 类型(prefill、decode、regular)。discover_external_engines 遍历地址列表,调用上述函数组装 ExternalEngineInfo 列表,并计算每个引擎的 num_gpus(优先取 server_info 中的 num_gpus,否则取 tp_size * pp_size)。apply_external_engine_info_to_args 在参数解析阶段调用 discover,并将结果写入 args.rollout_external_engine_infosargs.rollout_num_enginesargs.rollout_num_gpus,同时记录日志。

步骤 2:重构 rollout.py 引擎启动逻辑

移除 ServerGroup.start_engines 方法中的 if self.args.rollout_external 分支和 _allocate_rollout_engine_addr_and_ports_external 函数。外部引擎的地址和端口信息已通过 args.rollout_external_engine_infos 传递,不再需要特殊分配路径。添加 from slime.backends.sglang_utils.external import start_external_rollout_servers 导入,用于后续创建外部引擎的 RolloutServer 包装。调整 RolloutManager.server_get_updatable_server 的类型注解为 Any 以适应多 Server 类型。

步骤 3:调整 placement_group.py 的 GPU 布局

新增 _get_placement_group_layout 函数,根据运行模式计算需要多少 GPU。当 rollout_external 为 True 时,外部引擎不计入 placement group,因此 num_gpus 只取 actor 部分(actor_num_nodes * actor_num_gpus_per_node)。如果同时 debug_rollout_only,则返回 0 总 GPU(因为引擎在外部)。同时修改 _create_placement_group 支持 num_gpus=0 时返回 None 避免创建空 placement group。

步骤 4:重构 sglang_engine.py 中引擎初始化和路由器注册

将之前内联的 _get_actual_server_args(通过 HTTP 获取引擎参数)替换为调用 get_server_info(从 external.py)。提取 _register_to_router 方法,统一处理向 SGLang 路由器注册 worker 的逻辑,支持 worker_type 和 PD 模式的 bootstrap_port。对旧版路由器(≤0.2.1)只支持 regular 类型;新版路由器发送 JSON payload 包含 worker_type 和可选的 bootstrap_port。当 worker_type 为 prefill 但缺少 bootstrap_port 时抛出 RuntimeError。

步骤 5:更新测试

新增 tests/test_external_sglang_engines.py 单元测试,使用 monkeypatch 模拟 requests.get,验证 discover、apply 函数在 regular、PD 混合场景下的正确性,以及缺少地址时的异常。新增 tests/test_qwen3_4B_external_pd.py 端到端测试,在单机 6 GPU 环境启动 4 GPU 训练 + 2 个外部 SGLang 引擎(1 prefill + 1 decode),通过 delta weight update + disk 传输完成权重同步。新增 tests/test_placement_group.py 测试新的布局函数。

文件 模块 状态 重要度
slime/backends/sglang_utils/external.py 外部引擎 added 9.05
slime/ray/rollout.py Rollout 核心 modified 8.16
slime/backends/sglang_utils/sglang_engine.py 引擎适配 modified 7.77
slime/ray/placement_group.py 布局调度 modified 7.05
tests/test_external_sglang_engines.py 外部引擎测试 added 7.93

关键符号

discover_external_engines apply_external_engine_info_to_args get_server_info normalize_external_engine_addr _register_to_router _get_placement_group_layout start_rollout_servers _get_updatable_server get_rollout_num_engines

关键源码片段

slime/backends/sglang_utils/external.py core-logic

核心新增模块,定义外部引擎发现、元信息获取、参数注入等完整流程,是 PR 的基石。

# 文件 : slime/backends/sglang_utils/external.py# 外部引擎发现:根据地址列表批量获取引擎元信息
def discover_external_engines(addrs: list[str], timeout: float = 30.0) -> list[ExternalEngineInfo]:
    infos = []
    for addr in addrs:
        # 标准化地址(补充 http 前缀)
        url = normalize_external_engine_addr(addr)
        parsed = urlparse(url)
        assert parsed.hostname is not None and parsed.port is not None
        # 通过 HTTP 查询 /server_info 端点
        server_info = get_server_info(url, timeout=timeout)
​
        # 解析并行度参数,计算 GPU 数量
        pp_size = int(server_info.get("pp_size") or server_info.get("pipeline_parallel_size") or 1)
        tp_size = int(server_info.get("tp_size") or server_info.get("tensor_parallel_size") or 1)
        num_gpus = int(server_info.get("num_gpus") or server_info.get("num_gpus_per_engine") or tp_size * pp_size)
        bootstrap_port = server_info.get("disaggregation_bootstrap_port")
        bootstrap_port = int(bootstrap_port) if bootstrap_port is not None else None
​
        infos.append(
            ExternalEngineInfo(
                url=url,
                host=parsed.hostname,
                port=parsed.port,
                worker_type=_infer_worker_type(server_info),
                num_gpus=num_gpus,
                disaggregation_bootstrap_port=bootstrap_port,
                server_info=server_info,
            )
        )
    return infos
​
​
# 将外部引擎信息应用到参数对象,供后续 RolloutManager 使用
def apply_external_engine_info_to_args(args, logger=None) -> None:
    addrs = args.rollout_external_engine_addrs
    if not addrs:
        raise ValueError("apply_external_engine_info_to_args requires --rollout-external-engine-addrs.")
​
    infos = discover_external_engines(addrs)
    if not infos:
        raise ValueError("--rollout-external-engine-addrs did not contain any engines.")
​
    args.rollout_external_engine_infos = [info.to_dict() for info in infos]
    args.rollout_num_engines = len(infos)
    args.rollout_num_gpus = sum(info.num_gpus for info in infos)
​
    if logger is not None:
        summary = [
            {
                "url": info.url,
                "worker_type": info.worker_type,
                "num_gpus": info.num_gpus,
                "disaggregation_bootstrap_port": info.disaggregation_bootstrap_port,
            }
            for info in infos
        ]
        logger.info(f"Detected external SGLang engines: {summary}")
slime/ray/placement_group.py core-logic

新增 _get_placement_group_layout 函数,正确处理外部引擎模式下的 GPU 分配;支持零 GPU 的 placement group。

# 文件 : slime/ray/placement_group.py# 根据运行模式评估 placement group 需要的 GPU 总数和 rollout 偏移
def _get_placement_group_layout(args) -> tuple[int, int]:
    actor_num_gpus = args.actor_num_nodes * args.actor_num_gpus_per_node
​
    if args.debug_train_only:
        return actor_num_gpus, 0
​
    if args.rollout_external:
        # 外部引擎自备 GPU,不占用 trainer 节点资源
        if args.debug_rollout_only:
            return 0, 0
        return actor_num_gpus, actor_num_gpus
​
    if args.debug_rollout_only:
        return args.rollout_num_gpus, 0
​
    if args.colocate:
        return actor_num_gpus, 0
​
    # 标准模式:actor GPU + rollout GPU,offset 为 actor GPU 数
    return actor_num_gpus + args.rollout_num_gpus, actor_num_gpus

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

1) 依赖外部 SGLang 引擎的 /server_info 端点,如果 SGLang 版本接口不兼容将导致发现失败;
2) 移除 --rollout-external 参数,旧用户脚本需要更新为 --rollout-external-engine-addrs
3) 外部引擎模式下 placement group 的 GPU 计数改为只计 actor 部分,可能影响 colocate 或 debug 模式混合场景的资源配置;
4) 外部引擎无法参与 NCCL 集合通信,必须使用 delta weight update + disk 传输,如果用户配置其他传输方式(如 NCCL)会导致同步失败;
5) 端到端测试依赖 bond IP 和 IB 设备,在 CI 中可能因网络环境差异被跳过,外部模式回归覆盖不足。

用户影响:需要学习新的参数并部署外部 SGLang 引擎;旧的 --rollout-external 不再生效。系统影响:外部引擎模式不再占用 trainer GPU,placement group 资源分配更合理;路由器注册逻辑支持 PD 分离。团队影响:新增 external.py 核心模块,需维护与 SGLang server_info 接口的兼容性;测试覆盖了单元和端到端,但依赖特定硬件。

外部依赖裸奔 配置破坏 测试环境依赖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论