一条请求样本:从协议输入到回包证据#

这章解决什么问题#

前面的请求生命周期章节已经把入口、输入规范化、session 分叉、多 worker、batch fan-out、多模态和元数据传播分别讲开了,但它们仍然更像一组稳定专题,而不是一本书里常见的“整合样章”。读者如果只逐章吸收,很容易分别记住:

  • 请求会进入 TokenizerManager
  • ReqStateReq 不一样
  • Scheduler 会形成 batch
  • DetokenizerManager 会收尾
  • RequestStage 能给出证据

却不一定能把这些点重新压回同一条具体请求上。

这一章的任务,就是用一条最小但真实的请求样本,把前面这些分散机制重新串起来。它不再引入新的大主题,而是回答一个更像好技术书的问题:

  • 当一条具体请求来到系统时,前面讲过的对象、队列、输出和证据字段,到底会以什么顺序出现?

先给这条样本一个明确形状#

为了让案例尽量简单,又能覆盖足够多的 runtime 现实,这里选一条很典型的请求形状:

  • 外部表面:OpenAI-compatible chat request
  • 输入:一段 messages,带 max_tokenstemperature
  • 运行方式:非 batch、非多 worker、非多模态
  • 输出方式:streaming
  • 目标:观察一条请求怎样从协议表面进入 runtime,再怎样带着证据返回

这条样本故意不带 tool call、schema、session 或多 worker。不是因为这些不重要,而是因为案例章的职责是先把最小闭环讲完整,再把复杂人格留给各自专章。优秀技术书里的样章通常也遵守这个原则:用足够真实的最小样本组织复杂知识,而不是在一个案例里塞进所有分支。

一张图:把这条样本真正压成一条线#

下面这张图解决的理解障碍是:前面章节虽然分别讲清了入口、对象、回包和证据,但读者不一定知道它们到底怎样首尾相接。这里要看的不是“有多少组件”,而是“一条请求怎样连续变形”。

flowchart LR
    A["OpenAI-compatible chat request"] --> B["http_server / serving_chat"]
    B --> C["GenerateReqInput"]
    C --> D["TokenizerManager.generate_request()"]
    D --> E["ReqState + tokenized request"]
    E --> F["Scheduler / waiting queue / batch"]
    F --> G["BatchTokenIDOutput"]
    G --> H["DetokenizerManager"]
    H --> I["BatchStrOutput"]
    I --> J["_wait_one_response() / streaming chunk"]
    J --> K["response_sent_to_client_time / finished_time / request log"]

和前面那些分层图相比,这张图更像一本书里的案例图:它关心的不是某一层内部多复杂,而是这条样本怎样从“协议请求”连续变成“输出与证据”。

第一段:协议请求先被翻成内部请求对象#

这条样本进入系统时,首先表现成一个 OpenAI-compatible chat request。它不会直接进入 scheduler,而会先经过 http_server.py 的路由层和 serving_chat.py 这样的协议适配层。这里最重要的变化,不是“函数被调用了几次”,而是对象开始变形:

  • 外部看到的是 chat request
  • 运行时真正接住的,是 GenerateReqInput

也就是说,生命周期在一开始就已经完成了第一层翻译:从协议对象翻到 runtime 请求对象。前面 2.1 一次请求如何穿过 SGLang2.4 输入如何被规范化、分词并送入运行时 讲的,就是这一步的边界。

从读代码的角度,这一步最值得抓的入口仍然是:

  • python/sglang/srt/entrypoints/http_server.py
  • python/sglang/srt/entrypoints/openai/serving_chat.py
  • python/sglang/srt/managers/io_struct.py

这里的重点不是把它们全部展开,而是建立一个稳定判断:协议层负责把外部调用表面折成统一请求对象,而不是自己维护调度状态。

第二段:TokenizerManager 把协议对象变成可调度对象#

请求到了 TokenizerManager.generate_request(...) 之后,又会经历第二层关键变形。对这条样本来说,至少会发生三件事:

  1. 请求参数被归一化,例如 sampling 相关字段、priority 默认值、部分 tracing 相关元数据。
  2. TokenizerManager 为这条请求建立 ReqState,让后面的 streaming 回包有地方收敛。
  3. 文本输入被 tokenize,形成真正可以发给 scheduler 的 tokenized request。

这一段最值得技术书强调的点,不是“tokenize 发生了”,而是同一条请求在这里被一分为二:

  • 一份是入口/回包侧仍然持有的 ReqState
  • 一份是会继续送往 scheduler 的 tokenized request

这就是为什么前面反复强调 ReqStateReq 不是同一个对象。样章的价值,就在于让这种抽象差异不再停留在术语表里,而能真正挂回一条具体请求。

第三段:scheduler 看到的已经不是原始请求,而是等待中的调度单元#

请求进入 scheduler 之后,系统看到的就不再是 chat request,也不再是 ReqState,而是 waiting queue 里的调度单元。这里对这条样本来说最重要的变化有两层:

  • 它从“单请求输入”变成了“等待加入某一轮 batch 的候选”
  • 它的推进速度开始不只由输入内容决定,还由 waiting queue、预算和当前 batch 状态决定

也就是说,请求生命周期在这里开始受到第 4 节会讲的调度现实约束。即使这条样本非常简单,它也仍然要经过:

  • waiting queue
  • batch 成形
  • ScheduleBatch

然后才会进入执行层。样章的好处,是能帮助读者真正接受一个事实:请求在系统里从来不是“一直保持原样往前走”,而是不断被改写成更适合下游层次的对象。

第四段:输出先以 token 形式回来,再被重新组装成文本#

一旦执行层产出结果,这条样本首先回来的不是用户直接看到的文本,而是更低层的输出对象,例如 BatchTokenIDOutput。这意味着“请求完成”其实有两次不同的完成边界:

  • 一次是执行层已经产出 token 结果
  • 一次是调用方真正收到了 streaming chunk 或最终文本

这中间隔着 DetokenizerManager。它负责把 token 级结果重新翻成文本级结果,并通过回包链把这些结果送回 TokenizerManager。于是 TokenizerManager._wait_one_response(...) 才能继续做两件事:

  • 持续向调用方流式发送 chunk
  • 维护 ReqState,直到真正 finished

很多读者第一次读这部分时会下意识地以为“模型已经生成完,就等于请求完成了”。这条案例故意把两次完成边界拆开,就是为了让读者在很早的地方就建立更准确的运行时感觉。

第五段:证据字段是在收尾时真正闭环的#

如果这条样本是 streaming 请求,那么它不会只留下一个“结果字符串”,还会留下至少一条最小证据链。对这条样本最值得抓的证据字段通常包括:

  • RequestStage 切片
  • queue_time
  • TTFT
  • response_sent_to_client_time
  • finished_time
  • request logger / exporter 在 finished 时落下的记录

这里最重要的不是记住字段名,而是理解字段出现的顺序。对这条请求来说,response_sent_to_client_time 只会在对外发送真正完成之后才有意义,而 finished_time 则是系统内部收尾更靠后的边界。也正因为如此,前面第 8 节才需要单独讲“这些时间字段为什么看起来会打架”。

换句话说,证据链不是附属品,而是这条样本在系统里真正结束时留下的第二份产物。

把这条样本重新翻回五个阅读锚点#

如果你想顺着这条样本回源码,最稳的不是同时开十个文件,而是只抓五个锚点:

1. http_server / serving_chat:请求怎样从协议层落成内部对象
2. TokenizerManager.generate_request(...):入口侧如何建立 ReqState 并发出请求
3. Scheduler 主循环:请求何时真正进入 batch
4. DetokenizerManager.event_loop():token 结果怎样被翻回文本
5. _wait_one_response(...):chunk 怎样回到调用方,并在这里补齐证据

这五个锚点的价值,不在于它们覆盖了所有细节,而在于它们正好对应这条案例的五次对象变化。只要这五个锚点稳住,后面再去看 batch fan-out、多模态、session 或 tool workflow,也更不容易迷路。

这条样本没有覆盖什么#

为了避免这章看起来像“万能总图”,这里也明确说清它没覆盖的东西:

  • 没覆盖多 worker 入口放大
  • 没覆盖 batch fan-out / parallel sampling
  • 没覆盖 session / abort / health 分叉
  • 没覆盖多模态输入
  • 没覆盖结构化约束与 tool workflow

这不是缺点,而是案例章应该有的边界。它的任务是把最小主链压成一本书里真正可拿来反复回看的“样章”。复杂人格应该回到它们自己的章节,而不是在这里把案例写成故障树。

小结#

请求生命周期真正像一本书,而不是几篇并列文章,往往就差这样一章样章。它让前面分别讲过的协议层、内部对象、scheduler、回包链和证据字段重新站回同一条具体请求上。

如果读完这一章之后,你能稳定说出:

  • 请求先在哪一层从协议对象变成内部对象
  • 哪一层同时持有 ReqState 和 tokenized request
  • 哪一层把请求改写成 batch 现实
  • 哪一层把 token 结果重新翻回文本
  • 哪些证据字段是在请求真正收尾时才闭环

那么第 2 节就不再只是“请求链若干专题”,而开始更像一本完整技术书里的一个成熟 part。