Streaming、状态回收与结果组装#
这章解决什么问题#
上一章把请求怎样进入 runtime 讲清楚了,但还留了一个很重要的问题:为什么响应能够一边生成、一边回流给调用方,而且在请求完成之后状态还能被正确收尾。如果不补这一层,请求生命周期仍然只是一条“单向路径”,而不是一个完整闭环。
这一章专门回答“结果怎样回来”以及“请求完成后状态怎样被回收”。这不是小补丁,而是理解 runtime 是否真正闭环的关键。如果一个系统只能解释请求怎样进去,却说不清结果怎样回来,它更像半张流程图,而不是一本完整技术书。
为什么 TokenizerManager 既在入口也在回包路径上#
从 python/sglang/srt/managers/tokenizer_manager.py 的职责看,TokenizerManager 不只是把文本变成 token。它还维护请求状态、等待 detokenizer 回传的输出、把 streaming chunk 重组给调用方,并在完成时更新 ReqState。这说明它在 request lifecycle 里扮演的是“首尾收敛点”,而不是单向入口。
这类设计很容易被误读。很多读者第一次看到 TokenizerManager 这个名字,会自然把它归类成“前处理组件”;但真正的源码路径表明,它同时站在入口和回包两端。也正因为这样,请求生命周期章节需要单独拿一章讲它,而不是把它只写进入口图里就算完。
streaming 不是“边生成边 print”这么简单#
从调用方视角看,streaming 好像只是把最终结果拆成多个 chunk;但在 runtime 里,streaming 其实要求系统持续维护一组中间状态。请求必须先被 tokenized、送入 scheduler、产出中间 token id,再经过 detokenize,最后由 TokenizerManager._wait_one_response(...) 把这些中间结果不断组装成调用方真正能消费的输出。
换句话说,streaming 的难点不是“能不能马上发一点字出去”,而是“这条回程链每一轮都要保持状态一致”。如果这里任何一个环节丢了阶段信息,调用方看到的就不是平滑输出,而是乱序 chunk、缺失 finish signal 或状态收尾不干净。
为什么状态回收属于请求生命周期而不是调度细节#
一个常见误解是把“请求结束后的回收”全都归给 scheduler 或 cache。其实从调用体验上看,请求什么时候真正结束、何时可以认为 finished、何时该释放对应的 request state,这些都仍然属于生命周期问题。因为它们直接决定调用方何时拿到完整结果,也决定 runtime 是否会把一条已完成请求继续当成活跃状态。
调度与缓存当然也会参与回收,但这里的出发点不同。请求生命周期关注的是“这条请求是否从调用方视角真正完成”;调度与缓存关注的是“资源是否可以复用”。这两件事相关,但不完全重合,所以本章只把重点放在前者。
一个更像运行时行为的闭环图#
这张图专门解决“请求结束时到底发生了哪些收尾动作”的理解障碍。相比上一章的主时序图,它更强调回包和状态收敛,而不是入口选路。
sequenceDiagram
participant TM as TokenizerManager
participant SCH as Scheduler
participant DET as DetokenizerManager
participant Client as Client
TM->>SCH: tokenized request / batch input
SCH->>DET: BatchTokenIDOutput / token ids
DET->>TM: detokenized chunks / finish status
TM->>TM: _wait_one_response(...) / ReqState update
TM->>Client: streaming chunks or final response
TM->>TM: mark finished / release request-local state这张图比文字多解释了一点:对调用方来说,“收到最后一个 chunk”和“runtime 内部状态已经真正完成回收”并不是同一层动作。TokenizerManager 正是连接这两层动作的地方。
边界条件与排障入口#
如果现象是“能流式返回一部分内容,但最后总是不干净收尾”,优先检查的不是 launch_server.py,而是 TokenizerManager._wait_one_response(...) 一侧的 finish 判断和 DetokenizerManager.event_loop() 的回传路径。因为这类问题大多发生在回包阶段,而不是入口阶段。
如果现象是“输出一直不返回,但 scheduler 看起来在工作”,那就要确认 send_to_detokenizer.send_output(...) 到 recv_from_scheduler.recv_pyobj() 这条链是否正常,再看 ReqState 是否仍然停留在等待态。换句话说,这一章的排障入口是回程链,而不是前向链。
本章对应哪些代码路径#
本章最重要的文件级锚点包括 python/sglang/srt/managers/tokenizer_manager.py、python/sglang/srt/managers/detokenizer_manager.py、python/sglang/srt/managers/scheduler.py 以及这些文件中的 _wait_one_response(...)、event_loop()、send_to_detokenizer.send_output(...) 一类回包链路。
如果要把“请求是怎么回来的”真正读懂,推荐顺序是:先看 TokenizerManager 如何等待结果,再看 DetokenizerManager 如何处理 scheduler 输出,最后再回头看 scheduler 发送的是什么对象。这样比从任意一端倒推更稳定。
小结#
请求生命周期不应该只包含“怎么进去”,还必须包含“怎么回来、怎么结束、怎么收尾”。这一章补的正是这部分厚度。只要把这一层补稳,你对 SGLang 的 request lifecycle 才算真正建立成一个闭环,而不是一条只有入口没有出口的流程。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。