warmup、权重就绪与 readiness 门#

前面的运行时架构章节已经解释了启动入口、进程装配、并行身份和分布式 group 初始化,但如果没有一章把“什么时候才算真正 ready”讲清楚,读者很容易把三件完全不同的事混成一句“服务起来了”:

  • 进程已经拉起来
  • 端口已经可达
  • 服务真的可以接流量

对 SGLang 来说,这三者在实现上并不等价。Engine._launch_subprocesses(...) 解决的是“系统被装起来了”,而 _wait_and_warmup(...)_execute_server_warmup(...)initial_weights_loaded 这些路径解决的是“系统已经被证明真的能跑”。这条启动后门禁链,正是这章要补齐的核心。

先把 readiness 画成一条门禁链#

很多系统文档在这里都会偷懒,把 ready 简化成“HTTP 端口监听成功”。SGLang 的源码明显更严格。下面这张图的职责,就是把这种严格性明确压成一条链:

flowchart TD
    Boot["Engine._launch_subprocesses()"] --> WaitW["wait_weights_ready ?"]
    WaitW --> Warm["execute_server_warmup()"]
    Warm --> Status["TokenizerManager.server_status = Up"]
    Status --> Ready["ready to serve traffic"]
    Warm --> Unhealthy["warmup failed -> UnHealthy / kill process tree"]

图里最重要的一点是:readiness 不是一个布尔开关,而是一条时序化门禁链。只要这个区别站稳,后面再读启动日志、health check 和 watchdog,读者就不容易把“能连上”和“真 ready”混成一回事。

server_status 是这条链真正对外暴露的状态#

TokenizerManager 初始化时会把 server_status 设成 ServerStatus.Starting。只有 warmup 路径成功之后,http_server.py 才会显式把它切到 ServerStatus.Up;如果 warmup 或 health check 失败,又会切成 ServerStatus.UnHealthy

这说明 server_status 不是什么普通监控字段,而是启动语义的正式外显状态。看到它是 Starting,就不应把服务当成 ready;看到它已经 UnHealthy,也不该再把后续请求失败简单归咎于某个 API surface。换句话说,它是门禁链的结果,不是门禁链外面的装饰标签。

_wait_weights_ready() 防的不是“启动慢”,而是“框架起来了但执行体还没 ready”#

http_server.py::_wait_and_warmup(...)checkpoint_engine_wait_weights_before_ready 打开时,会先调用 _wait_weights_ready()。这条路径会轮询:

  • tokenizer_manager.initial_weights_loaded

直到超时或成功。

这很值得技术书单独点破,因为它明确承认了一种非常现实的中间状态:

  • 服务框架已经起来了
  • 但权重还没 ready

如果不把这种状态单独区分出来,系统就很容易过早暴露成“可用”,然后再在真正执行第一条请求时暴露出更难排查的问题。这里体现出来的是一个成熟 inference runtime 才会做的边界切分:可连接不等于可执行。

_execute_server_warmup() 真正验证的是“最小真实请求链”#

这也是很多人会低估的地方。warmup 在这里做的并不是抽象自检,而是构造一条最小但真实的请求去跑通 runtime:

  1. 先轮询 /model_info,确认 HTTP 表面已经起来
  2. 再根据模型人格决定 warmup 目标:
    • generation 模型走 /generate/v1/chat/completions
    • encoder-only 模型走 /encode
  3. 构造最小 warmup request
  4. 真正发请求并等待返回
  5. 成功后才把 server_status 设成 Up

这说明 warmup 的本质不是“打个 ping”,而是“至少跑通一条极小但真实的请求链”。也正因为如此,它在书里值得被单独讲:它定义了系统对“ready”的最低证明标准。

warmup 请求为什么必须按模型人格切换#

源码会根据 model_infoserver_args 判断:

  • 是否是 generation 模型
  • 是否是 VLM
  • 是否跳过 tokenizer init
  • 是否处在 disaggregation 模式

然后决定 warmup 用:

  • /generate
  • /v1/chat/completions
  • /encode

以及构造什么最小输入。这种设计特别像成熟系统才会有的认真程度:它不是用同一种 smoke test 敷衍所有人格,而是在尽量贴近当前运行时的真实执行形态。

收益当然很清楚:更容易在启动阶段尽早暴露真实问题。代价也同样真实:warmup 自己会随模型人格和部署模式变复杂。但这恰恰是值得写进系统书的 tradeoff,而不是该被抹平的实现噪音。

disaggregation warmup 更复杂,是因为它在验证的不只是本地 forward#

disaggregation_mode != "null" 时,warmup 还会带上:

  • bootstrap_host
  • bootstrap_room
  • fake transfer 所需的特殊输入

这说明分离式运行时的 readiness 证明比单机生成路径更严格。它不只要求本地模型能 forward,还要求 prefill / decode 的 transfer 前置条件能够成立。也就是说,分离式 warmup 在这里不是“附加性能优化”,而是这类拓扑能否进入正式运行状态的门槛。

skip_server_warmup 跳过的不是优化动作,而是最关键的一层证明#

如果打开 skip_server_warmup,系统会直接把 server_status 设成 Up。这行为什么危险,很值得直说:

  • 收益:启动更快
  • 代价:你失去了“真实请求已跑通”的证明

很多读者看到“可选跳过 warmup”时,会自然把它理解成“只是少跑一个预热动作”。源码实际告诉你的,是它跳过了 readiness 最关键的一层证据。把这件事点破,对上线判断特别重要。

非零 rank 的 dummy health check server 说明多节点 ready 语义并不对称#

entrypoints/engine.py 里,非零 rank 节点在某些多节点场景下不会继续起 tokenizer / detokenizer 主线,而是:

  • scheduler_init_result.wait_for_ready()
  • 启动 launch_dummy_health_check_server(...)
  • 然后等待 completion

这说明这些节点的 ready 语义和 rank 0 不一样。它们不直接对外承载完整 HTTP surface,而是以一个更轻的 health endpoint 参与整个多节点生命周期。也正因为这样,一本系统书不能把“ready”写成单一节点视角。

真正遇到启动卡住时,先要分清是哪一道门没过#

更稳的判断顺序通常是:

  1. 是进程还没装起来,还是已经装起来但还停在 Starting
  2. initial_weights_loaded 有没有变成 True
  3. /model_info 是否已经可达
  4. warmup request 究竟卡在构造、发送还是返回
  5. server_status 是一直 Starting,还是已经转成 UnHealthy

这条顺序很值钱,因为它把启动问题先拆成几道明确门禁,而不是把所有失败都模糊成“服务没起来”。

最容易出现的三种误判#

第一,误以为端口监听成功就代表 ready。
源码明确要求 warmup 成功后才会把状态切成 Up

第二,误以为 warmup 只是可有可无的性能预热。
它同时承担 readiness 证明职责。

第三,误以为多节点各 rank 的 ready 语义完全一样。
非零 rank 可能只暴露 dummy health server,而不承载完整 HTTP 主线。

小结#

这章真正补齐的,是运行时架构里最容易被略过的一条启动后门禁链:Engine._launch_subprocesses(...) 只说明系统被装起来,_wait_weights_ready() 说明执行能力是否 ready,_execute_server_warmup() 说明真实请求链是否跑通,而 server_status 把这条门禁链的结果正式暴露出来。

读懂这层之后,再看 health check、watchdog 和启动日志,你就不会再把“能连上”和“真 ready”混成一回事。