Prhub

#27445 Complete server warmup before scripted runtime scripts start

原始 PR 作者 fzyzcjy 合并时间 2026-06-07 17:37 文件变更 3 提交数 5 评论 14 代码增减 +70 / -6

执行摘要

在脚本化测试开始前完成服务器预热

脚本化运行时中,调度器循环在脚本命令之间冻结,导致预热线程的 /generate 请求在脚本执行期间任意时刻到达,作为外源流量污染调度器队列、批组成和重置排空,是 CI flaky 的放大器。同时 /health 在调度器冻结时无法完成,导致就绪检查超时。需要在脚本开始前完成预热并清除残留流量。

该 PR 展示了如何通过驱动调度器循环来确保测试环境清洁,值得测试基础设施和调度器相关开发者精读。_drive_engine_through_warmup 中的两轮 quiesce 逻辑是对 PP 异步特性的巧妙应对。讨论中关于 hasattr 的取舍也体现了对调度器事件循环初始化的深入理解。

讨论亮点

Gemini Code Assist 机器人建议在 is_fully_idle 中使用 hasattr 检查 running_mbs 以避免 AttributeError。作者 fzyzcjy 反驳:所有 is_fully_idle 调用点都在调度器事件循环内,且 init_pp_loop_state 已在 pp_size > 1 时初始化了这些属性,直接访问更安全且不会掩盖初始化顺序错误。该讨论已解决,未采纳建议。

实现拆解

按以下步骤实现:

  1. 在调度器钩子中驱动预热scheduler_hook.py):新增 _drive_engine_through_warmup 函数,在 _run_dispatch_loop 发送 HookReady 之前 yield from 调用。该函数驱动调度器循环,直到代理(ScriptedTokenizerRecvProxy)观察到至少一个工作请求且调度器 is_fully_idle() 持续两个 microbatch 轮转(补偿 PP 异步的瞬时空闲状态),并在 120 秒超时后报错。若设置 skip_server_warmup 则跳过。

  2. 代理计数工作请求tokenizer_recv_proxy.py):为 ScriptedTokenizerRecvProxy 添加 work_reqs_seen 属性,在 _drain_underlying 中识别并计数进入的 TokenizedGenerateReqInput 等实际工作请求类型,供预热检测使用。

  3. 修改 HTTP 就绪端点http_server.py):将 _await_http_ready 中的检查端点从 /health 改为 /model_info,因为 /health 在调度器被脚本驱动时会产生一个真正的生成请求,而该请求无法在脚本间隙完成;/model_info 仅返回元数据,证明端口已绑定且路由已注册。

配套测试:无额外测试,仅影响现有测试的运行方式。该变更是 PR 1/2,PR 2(#27446)修复 is_fully_idle 在 PP 下的遗漏问题。

文件 模块 状态 重要度
python/sglang/test/scripted_runtime/scheduler_hook.py 调度钩子 modified 6.27
python/sglang/test/scripted_runtime/tokenizer_recv_proxy.py 接收代理 modified 5.01
python/sglang/test/scripted_runtime/http_server.py HTTP 服务器 modified 4.55

关键符号

_drive_engine_through_warmup _await_http_ready ScriptedTokenizerRecvProxy._drain_underlying

关键源码片段

python/sglang/test/scripted_runtime/scheduler_hook.py test-coverage

核心变更文件,新增 `_drive_engine_through_warmup` 函数,在发送 `HookReady` 前驱动调度器处理预热请求,是解决 flaky 的关键。

# 文件 : python/sglang/test/scripted_runtime/scheduler_hook.py
# 新增函数 : _drive_engine_through_warmup
# 在 _run_dispatch_loop 中 yield from 调用,确保脚本开始前服务器已预热完毕def _drive_engine_through_warmup(ctx: ScriptedContext) -> Generator:
    # Run the engine until the server warmup request has been received and
    # fully processed, so scripts never observe foreign warmup traffic.
    scheduler = ctx.scheduler
    server_args = scheduler.server_args
    if server_args.skip_server_warmup:
        logger.info('scripted_runtime: skip_server_warmup set, not driving warmup')
        return
​
    logger.info('scripted_runtime: driving engine until server warmup completes')
    start_time = time.monotonic()
​
    # is_fully_idle() 在 PP 下可能瞬时报告空闲(microbatch 仍在传输中),
    # 因此要求空闲状态连续保持两个 microbatch 轮转周期
    quiesce_iters = 2 * (server_args.pp_size + server_args.pp_async_batch_depth)
    proxy = ctx._tokenizer_recv_proxy
    deadline = start_time + WARMUP_DRIVE_TIMEOUT_S
​
    idle_streak = 0
    while idle_streak < quiesce_iters:
        if time.monotonic() >= deadline:
            raise RuntimeError(
                'scripted_runtime: server warmup did not complete within '
                f'{WARMUP_DRIVE_TIMEOUT_S}s '
                f'(work_reqs_seen={proxy.work_reqs_seen}, '
                f'idle_streak={idle_streak})'
            )
        yield # 让调度器循环前进一次
        # 当代理观察到至少一个工作请求且调度器完全空闲时,增加 idle_streak
        if proxy.work_reqs_seen > 0 and scheduler.is_fully_idle():
            idle_streak += 1
        else:
            idle_streak = 0
​
    logger.info(
        'scripted_runtime: server warmup drained in %.1fs (work_reqs_seen=%d)',
        time.monotonic() - start_time,
        proxy.work_reqs_seen,
    )
python/sglang/test/scripted_runtime/tokenizer_recv_proxy.py test-coverage

新增工作请求计数,为预热检测提供观察点。

# 文件 : python/sglang/test/scripted_runtime/tokenizer_recv_proxy.py
# 在 _drain_underlying 中计数实际工作请求from sglang.srt.managers.io_struct import (
    BatchTokenizedEmbeddingReqInput,
    BatchTokenizedGenerateReqInput,
    TokenizedEmbeddingReqInput,
    TokenizedGenerateReqInput,
)_WORK_REQ_TYPES = (
    TokenizedGenerateReqInput,
    TokenizedEmbeddingReqInput,
    BatchTokenizedGenerateReqInput,
    BatchTokenizedEmbeddingReqInput,
)
​
​
class ScriptedTokenizerRecvProxy:
    def __init__(self, *, underlying: zmq.Socket) -> None:
        self._underlying = underlying
        self._buffer: deque = deque()
        self.work_reqs_seen: int = 0 # 新增:记录进入的工作请求总数
​
    def _drain_underlying(self) -> None:
        while True:
            try:
                req = self._underlying.recv_pyobj(zmq.NOBLOCK)
            except zmq.ZMQError:
                break
            # 仅对真正的工作请求类型进行计数,忽略控制消息
            if isinstance(req, _WORK_REQ_TYPES):
                self.work_reqs_seen += 1
            self._buffer.append(req)

评论区精华

is_fully_idle 中对 running_mbs 的防御性检查 正确性

Gemini Code Assist 建议使用 hasattr 避免 AttributeError;作者认为所有调用点都已确保初始化,直接访问更安全。

结论:未采纳 hasattr 建议,保持直接访问。 · 已解决

风险与影响

主要风险:若预热驱动逻辑未能正确检测完成,可能引入 120 秒超时并报错,导致测试失败(而非 flaky)。但超时机制比原有不可控的污染更可靠。若 is_fully_idle 在特定条件下返回错误值(例如 PR 2 修复前),可能导致过早结束预热,但 PR 2 会修复该问题。此外,改动仅限测试环境,无生产影响。

对用户无影响;对系统:测试环境中的脚本化运行时代理和 HTTP 就绪检查行为改变,预热现在在脚本开始前完成,消除了外源流量引起的 flaky。对团队:CI 中与脚本化运行时相关的测试将更稳定,减少 rerun。该变更是测试基础设施改进,影响范围仅限于启用脚本化运行时的测试(主要涉及 chunked_prefill 和单元测试)。

测试基础设施变更 依赖后续修复 超时风险

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论