Prhub

#2604 fix(rollout): raise server readiness timeout to 120s

原始 PR 作者 guapisolo 合并时间 2026-08-25 04:24 文件变更 1 提交数 1 评论 0 代码增减 +7 / -2

执行摘要

将 router/session-server 就绪超时提升至 120s,修复 CI 偶发失败

在 CI 运行 32090638792 的 stage-c-2-gpu-h200 job 中,test_glm5_744b_a40b_4layer_r3.py 因 'Server at 172.18.0.2:4077 not ready after 30s' 失败(由 start_router 的 wait_for_server_ready(timeout=30) 引起)。该 job 中 6/7 个测试通过,失败属于偶发。根因是 spawn 子进程需要重新导入重型依赖链(典型 13 秒),而 CI 瞬时卡顿可能使启动超过 30 秒预算。PR body 详细排除了端口冲突(日志中无 EADDRINUSE,且端口 4077 在其他测试中被成功复用),确认问题为启动时间超时。

该 PR 值得快速合并,修复了 CI 偶发失败且改动简单、注释清晰。对于读者,它展示了如何通过根因分析定位偶发故障并采用合理的超时策略。无需深读,但可以留意后续是否将常量提取为可配置参数。

讨论亮点

PR 的 code review 评论为 0,仅有两条 APPROVED(来自 Shi-Dong 和 yueming-yuan)以及一条 claude[bot] 的自动提示(告知仓库配置了手动评审)。因此没有实质性的技术讨论。不过 PR body 中包含细致的根因分析和逻辑推理,如端口冲突排除、进程存活检查的作用等,可作为讨论精华。

实现拆解

本 PR 的修改集中在 miles/ray/rollout/router_manager.py,主要分两步:

  1. 新增模块常量:在文件顶部引入 _SERVER_READY_TIMEOUT_SECS = 120,并注释说明原因(spawn 上下文重新导入重型 transformers/megatron 链,CI 瞬时卡顿可能超过 30 秒预算)。
  2. 替换硬编码超时:将 start_routerstart_session_server 中的 wait_for_server_ready(..., timeout=30) 改为使用新常量 _SERVER_READY_TIMEOUT_SECS

此改动不影响快速失败:wait_for_server_ready 内部的 process.is_alive() 检查会在子进程死亡时立即抛出异常,因此宽限预算只影响仍存活但启动缓慢的情形。

测试与兼容性:未新增专门测试,但作者运行了 tests/fast/ray/rollout/test_router_manager.py(11 个测试通过)。该改动改变了超时行为,但不涉及数据结构或外部接口,向后兼容。

文件 模块 状态 重要度
miles/ray/rollout/router_manager.py 路由管理 modified 5.52

关键符号

start_router start_session_server

关键源码片段

miles/ray/rollout/router_manager.py entrypoint

核心修改文件:将 router/session-server 就绪超时从硬编码 30s 提升为模块常量 120s,修复 CI 偶发启动超时导致的训练失败。

import copy
import logging
import multiprocessing
import random
import uuidfrom sglang_router.launch_router import RouterArgsfrom miles.rollout.session.server import run_session_server
from miles.router.router import run_router as run_miles_router
from miles.utils.http_utils import _wrap_ipv6, find_available_port, get_host_info, is_port_available
from miles.utils.http_utils import run_router as run_sglang_router
from miles.utils.http_utils import wait_for_server_readylogger = logging.getLogger(__name__)# 就绪预算:spawn 子进程需要重新导入重型 transformers/megatron 链(CI 中约 13 秒),
# 瞬时卡顿可能超过 30 秒,故提升到 120 秒。
_SERVER_READY_TIMEOUT_SECS = 120
​
​
def start_router(args, *, has_pd_disaggregation: bool = False, force_new: bool = False) -> tuple[str, int]:
    """启动 sgl router 或 miles router,返回 (router_ip, router_port)。"""
    if not force_new and args.sglang_router_ip is not None:
        return args.sglang_router_ip, args.sglang_router_port
​
    router_ip = _wrap_ipv6(get_host_info()[1])
    if force_new:
        router_port = find_available_port(random.randint(3000, 4000))
    else:
        router_port = args.sglang_router_port
        if router_port is None:
            router_port = find_available_port(random.randint(3000, 4000))
​
    if args.use_miles_router:
        assert not has_pd_disaggregation, "miles router does not support PD disaggregation."
        run_router = run_miles_router
        router_args = copy.copy(args)
        router_args.sglang_router_ip = router_ip
        router_args.sglang_router_port = router_port
    else:
        run_router = run_sglang_router
        router_args = RouterArgs.from_cli_args(args, use_router_prefix=True)
        router_args.host = router_ip
        router_args.port = router_port
        router_args.prometheus_port = find_available_port(random.randint(4000, 5000))
        router_args.log_level = "warn"
        router_args.request_timeout_secs = args.sglang_router_request_timeout_secs
        if args.sglang_router_policy:
            router_args.policy = args.sglang_router_policy
        if has_pd_disaggregation:
            router_args.pd_disaggregation = True
        logger.info(f"Launch router with args: {router_args}")
​
    port = router_port
    if not is_port_available(port):
        raise RuntimeError(
            f"Port {port} is already in use — a stale router process may still be running. "
            f"Run 'pkill -9 python' to kill it, then retry."
        )
​
    # 使用 spawn(而非 fork):避免子进程继承 Ray actor 的线程 / 终结器(如 wandb 服务线程)导致死锁。
    process = multiprocessing.get_context("spawn").Process(
        target=run_router,
        args=(router_args,),
    )
    process.daemon = True
    process.start()
    wait_for_server_ready(router_ip, router_port, process, timeout=_SERVER_READY_TIMEOUT_SECS)
    logger.info(f"Router launched at {router_ip}:{router_port}")
    return router_ip, router_port

评论区精华

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

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

风险与影响

主要风险是放宽超时可能使故障检测延迟,即当子进程卡死但未退出时,训练任务会等待最多 120 秒而非 30 秒。不过由于子进程由 multiprocessing 启动且设置了 daemon,若子进程僵死,父进程仍可能通过 is_alive() 检查。另外,该超时值对所有调用统一生效,如果未来子进程启动模式变化(如增加更多重型导入),可能需要重新评估。整体而言,风险较低。

影响范围仅涉及 rollout 模块的路由器与会话服务器启动逻辑。对用户而言,CI 中偶发的启动超时失败减少,提升训练任务的稳定性。对系统而言,可能增加启动等待时间(最坏情况 120 秒),但只在异常场景下发生。对团队而言,这是一个轻量级修复,无接口变更,维护成本低。

超时延长可能导致故障检测延迟 缺少直接针对超时常量的单元测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论