一次请求如何穿过 SGLang#

这章解决什么问题#

这一章解决的是“请求主链路在哪里开始,又在哪里发生关键 handoff”。如果只从零散模块出发阅读源码,你会看到 http_server.pygrpc_server.pyscheduler.pytokenizer_manager.py 这些名字,却不知道它们是按什么顺序被接上的。生命周期章节的任务,就是先把这条主线画出来。

第一版在这里刻意控制范围。它会讲清入口、模式分支、进入运行时的 handoff,以及后续要去哪几类模块找实现;但不会在这一章里深入 sampling、speculative decoding 或 KV cache 的局部机制。那些问题会留给后面的执行模型和调度与内存章节。

请求先从哪一个入口进来#

python/sglang/launch_server.py 看,服务化入口首先会调用 prepare_server_args(...) 解析参数,然后交给 run_server(server_args) 做模式选择。这里的分支不是装饰性的:encoder_onlygrpc_modeuse_ray 和默认 HTTP 模式分别导向不同实现,默认路径才会导入 sglang.srt.entrypoints.http_server.launch_server

这一步最关键的结论是:launch_server.py 负责“选入口”,不负责“跑生成”。你可以把它理解成总调度台。它决定请求应该进入 HTTP、gRPC、Ray,还是 encoder disaggregation path;但一旦分支确定,真正接收请求、维护请求状态、驱动 batch 的逻辑就进入 srt 内部了。

如果只靠段落描述,这条主链路仍然有点抽象。下面这张时序图解决的障碍是:把“入口选路”“请求进入 runtime”“调度器推进 batch”“detokenizer 回包”放到同一时间轴上,让你能一眼区分谁在接请求、谁在编排、谁在收尾。

sequenceDiagram
    participant Client as Client / SDK
    participant Entry as launch_server.py
    participant HTTP as http_server.py
    participant TM as TokenizerManager
    participant SCH as Scheduler
    participant DET as DetokenizerManager

    Client->>Entry: CLI / HTTP / OpenAI-compatible request
    Entry->>HTTP: select HTTP / gRPC / Ray path
    HTTP->>TM: generate_request(...)
    TM->>TM: normalize, tokenize, build request state
    TM->>SCH: send_to_scheduler.send_pyobj(...)
    SCH->>SCH: build / update batch
    SCH->>DET: send_to_detokenizer.send_output(...)
    DET->>DET: detokenize / assemble text
    DET->>TM: send_to_tokenizer.send_pyobj(...)
    TM->>HTTP: streaming / final response
    HTTP->>Client: chunks or completed result

这张图多解释了一件纯文字不容易稳定表达的事:TokenizerManager 在链路里既是请求进入 runtime 的第一站,也是结果回到调用方之前的收敛点。后面你在架构章节里看到 TokenizerManagerSchedulerDetokenizerManager 被并列成 engine 组件时,就更容易理解它们为什么不是随意拆成三个进程。

HTTP 层接住请求之后把它交给谁#

python/sglang/srt/entrypoints/http_server.pylaunch_server(...) 文档字符串已经把主链路说得很清楚:SRT server 由一个 HTTP server 和一个 engine 组成,而 engine 又由三个部件构成:TokenizerManagerSchedulerDetokenizerManager。同一段文档还明确写出进程边界:HTTP server、EngineTokenizerManager 在主进程里,SchedulerDetokenizerManager 作为子进程运行,进程间通过 ZMQ IPC 通信。

继续顺着文件往下看,/generate 路由的处理函数 generate_request(...) 会直接把 GenerateReqInput 交给 _global_state.tokenizer_manager.generate_request(obj, request)/v1/completions/v1/chat/completions 等兼容接口虽然在外层做了协议适配,但最后也都会回到同一条 runtime path。换句话说,HTTP 层的职责是把协议表面折叠到统一的请求入口,而不是自己保存调度状态。

TokenizerManager 怎样把请求推进到调度器#

python/sglang/srt/managers/tokenizer_manager.py 里的 TokenizerManager.generate_request(...) 是请求真正进入运行时编排的第一站。这个方法先做几件很“入口层”的工作:规范化请求、设置 priority、校验 routed_dp_rank、记录请求统计、解析 LoRA,再决定是单请求还是 batch 请求路径。

然后,链路会进入两个更实在的动作。第一,_tokenize_one_request(...) 或 batch 版本把文本、input_ids、多模态输入处理成调度器能消费的对象。第二,_send_one_request(...) 或 batch 发送逻辑把 tokenized request 通过 self.send_to_scheduler.send_pyobj(...) 推给 Scheduler。从这里开始,请求就不再停留在 HTTP 语义里,而是变成 runtime 内部的调度单元。

和入口相对称的是回包路径。TokenizerManager 在初始化 IPC 时会同时建立 recv_from_detokenizersend_to_scheduler 两条通道;generate_request(...) 在发出请求后,会进入 _wait_one_response(...)。这意味着 TokenizerManager 不是一次性把请求“丢给后端”就结束,而是持续维护 rid -> ReqState,在响应回来时负责把 streaming / non-streaming 行为重新拼回用户可见的输出。

SchedulerDetokenizerManager 怎样继续接力#

python/sglang/srt/managers/scheduler.py 里,Scheduler 初始化时会建立 recv_from_tokenizersend_to_tokenizersend_to_detokenizer 等 IPC 通道。它接住请求之后,核心工作不是“立刻跑模型”,而是把请求放进等待队列、维护 running_batch,再由 get_next_batch_to_run() 决定当前轮次是发起新的 prefill,还是推进已有 decode。这里你第一次真正看到“请求”被改写成“batch”。

调度器得到 batch 结果之后,不会直接把 token id 回给 HTTP 层。scheduler.py 里真正负责“把输出往后送”的路径,是通过 send_to_detokenizer.send_output(...)BatchTokenIDOutput 之类的对象发给 detokenizer;而 python/sglang/srt/managers/detokenizer_manager.pyevent_loop() 再从 recv_from_scheduler.recv_pyobj() 收数据,做 detokenize,然后通过 send_to_tokenizer.send_pyobj(output) 把文本结果送回 TokenizerManager

这条回程链解释了为什么 TokenizerManager._wait_one_response(...) 里会一直等待事件、合并 streaming chunk,并在 finished 时收尾。对调用方来说,看见的是 HTTP 响应;对 runtime 来说,实际发生的是 TokenizerManager -> Scheduler -> DetokenizerManager -> TokenizerManager 的闭环。

边界条件与容易误解的地方#

第一次阅读这条链路时,一个常见误解是把 TokenizerManager 看成“只是分词器进程”。从文件职责看,这种理解太窄了。它不仅负责 tokenization,还负责接请求、维护 rid -> ReqState、等待结果、重新组装 streaming 响应,因此它更像“请求编排的首尾收敛点”。

另一个容易混淆的点是把 HTTP server 当成主逻辑中心。实际上,HTTP 层更像协议壳:它决定怎么把外部请求翻译成 runtime 可理解的请求对象,但不负责 batch 生命周期。这就是为什么本章只把它讲到 handoff 为止,后文必须继续交给 SchedulerDetokenizerManager

调试时可以从哪里下手#

如果一个请求“能进来但不出结果”,优先检查的不是 launch_server.py,而是 TokenizerManager.generate_request(...) 是否真正把请求送进了 send_to_scheduler,以及 SchedulerDetokenizerManager 的 IPC 是否还在工作。因为从这条闭环看,最容易卡住的不是入口本身,而是请求发出之后没有再回到 TokenizerManager

如果一个请求“能返回但 streaming 表现异常”,则更适合先看 TokenizerManager._wait_one_response(...) 一侧的状态维护,再看 DetokenizerManager.event_loop() 的输出拼接路径。换句话说,排障时也应该遵守本章这条主线:先确认请求有没有完成完整闭环,再决定往哪一个局部机制深挖。

一个更像源码阅读笔记的最小检查清单#

如果你准备真正跟着这章去读代码,可以按下面这个顺序做最小检查:

1. 看 launch_server.py:确认请求从哪条 server path 进入
2. 看 http_server.py:确认外部路由最终交给谁
3. 看 TokenizerManager.generate_request(...):确认请求如何被转成 runtime 对象
4. 看 Scheduler.get_next_batch_to_run():确认请求何时进入 batch
5. 看 DetokenizerManager.event_loop():确认结果如何回到文本输出

这个顺序的价值,不在于它“唯一正确”,而在于它强迫你沿着 handoff 去读,而不是在仓库里随机跳。很多时候,一条主线能不能读清楚,靠的就是这种阅读顺序是否稳定。

为什么这一章不直接讲 kernel 或 cache 细节#

如果按“知道得越多越好”的直觉来写,最容易犯的错误是把 scheduler、cache、sampling 甚至 grammar constraint 都提前塞进这一章。这样看起来信息更满,但阅读效果反而更差,因为读者还没分清“请求去哪了”,就已经要理解“请求到了以后怎样被优化”。

优秀技术书通常不会在第一条主线里同时交代所有局部机制。它们更常见的做法,是先把一个最小闭环走通,再在后续章节中逐步把这个闭环加厚。本章遵循的正是这个原则:先让你知道请求怎样被接住、推进、回包,然后再在后文讨论这个过程怎样被调度、加速、约束和观测。

本章对应哪些代码路径#

本章的第一批事实锚点至少包括 python/sglang/launch_server.pypython/sglang/srt/entrypoints/http_server.pypython/sglang/srt/entrypoints/grpc_server.py 以及 python/sglang/srt/entrypoints/engine.py。如果你只想抓最小闭环,再补上 python/sglang/srt/managers/tokenizer_manager.pypython/sglang/srt/managers/scheduler.pypython/sglang/srt/managers/detokenizer_manager.py 就够了。

更深入的执行、调度和缓存细节,本章只做指路,不做过早展开。换句话说,这一章的代码路径映射要求至少落到文件级或调用链级,但只承担“把主链路走通”的职责,不承担后续章节的机制解释任务。等你已经能清楚说出请求经过哪些 handoff 之后,再去读 ScheduleBatchModelRunnerRadixCache,理解成本会低很多。

读完这一章之后,你至少应该能回答三件事:请求先从哪一个入口进入,HTTP 层之后由谁接住并继续编排,以及结果怎样沿着 detokenize 路径回到调用方。如果这三件事已经清楚,请求生命周期这一层就算真正建立起来了;剩下的复杂度,可以放心留给调度、缓存和执行模型章节。