Serving 层与 SRT 分层#
这章解决什么问题#
这一章要解决的问题不是“请求怎样一步步流动”,而是“为什么 SGLang 要把入口、编排、执行和观测拆成不同层”。如果没有这层理解,读者看到 http_server.py、scheduler.py、model_executor、observability 这些区域时,会误以为它们只是按开发习惯分目录,而不是在划定真正的职责边界。
第一版在这里坚持一个保守原则:目录名和公开入口是事实,基于目录职责推断“分层意图”时,会明确说明这是工作性划分,而不是把目录结构直接等同于架构真相。这样可以避免过度解读源码布局。
事实锚点:进程边界本来就写在入口里#
从 python/sglang/launch_server.py 与 python/sglang/srt/entrypoints/* 看,SGLang 至少存在一层很明确的入口面:它负责接参数、选运行模式、接住 HTTP 或 gRPC 请求,并把后续工作交给 runtime path。这个层次更接近 serving surface,而不是底层生成机制本身。
更关键的是,python/sglang/srt/entrypoints/http_server.py 的 launch_server(...) 文档字符串已经把运行时拆分说得非常直接:HTTP server 负责路由请求;engine 由 TokenizerManager、Scheduler 和 DetokenizerManager 三个组件构成;HTTP server、Engine、TokenizerManager 在主进程里,而另外两个 manager 是子进程,并通过 ZMQ 做 IPC。python/sglang/srt/entrypoints/engine.py 的 Engine 类文档字符串重复了同样的描述,这不是推断,而是源码作者自己给出的结构说明。
如果只用文字,这里最容易混淆的点是“入口层、编排层、执行层、观测层到底怎么分”。下面这张组件图承担的职责,就是把这些层次放到一张边界图里,并明确哪些是 runtime 主线、哪些是横切能力。
flowchart TB
subgraph Serving["Serving Surface"]
A["launch_server.py"]
B["entrypoints/http_server.py"]
C["entrypoints/grpc_server.py"]
D["entrypoints/engine.py"]
end
subgraph Managers["Runtime Orchestration"]
E["TokenizerManager"]
F["Scheduler"]
G["DetokenizerManager"]
H["ScheduleBatch / ModelWorkerBatch"]
end
subgraph Executor["Execution Layer"]
I["model_executor / ModelRunner"]
J["models / tokenizer"]
end
subgraph Cache["State & Cache"]
K["mem_cache / ReqToTokenPool"]
L["TokenToKVPoolAllocator / KVCache"]
end
subgraph Obs["Observability"]
M["observability / req_time_stats.py"]
end
A --> B
A --> C
B --> D
C --> D
D --> E
E --> F
F --> H
H --> I
I --> J
F --> K
K --> L
E -. stage / request stats .-> M
F -. batch / phase stats .-> M
I -. execution metrics .-> M这张图比纯目录描述多解释了一点:observability 不是沿着主数据流串联的“下一跳”,而是覆盖多个层的横切面。理解这一点之后,后面的调试章节就不会再显得像附录,而会自然成为这张架构图的一个外侧切面。
编排层的边界:managers 不是“杂项目录”#
如果你顺着 TokenizerManager.generate_request(...)、Scheduler.get_next_batch_to_run() 和 DetokenizerManager.event_loop() 走一遍,会发现 managers 承担的是典型的 orchestration 角色:它们接住请求状态、维护队列和批次、发起 IPC、处理 streaming 输出、管理 LoRA 或会话等运行时控制逻辑。它们不是 attention kernel,也不是模型本体,但整个系统的“请求怎样被组织起来”恰恰主要发生在这里。
python/sglang/srt/managers/schedule_batch.py 还给了另一个很强的信号。它在文件头明确写出 batch 数据结构的流向是 ScheduleBatch -> ModelWorkerBatch -> ForwardBatch,并说明 ScheduleBatch 由 scheduler.py::Scheduler 管理,ForwardBatch 由 model_runner.py::ModelRunner 消费。这说明 managers 和 model_executor 的分界不只是“一个偏控制,一个偏计算”这么模糊,而是连数据结构转换点都已经被写死在注释里了。
执行层与内存层:为什么 model_executor 和 mem_cache 分开存在#
python/sglang/srt/model_executor/model_runner.py 在模型与 attention backend 初始化之后,会显式调用 init_memory_pool(pre_model_load_memory)。而 python/sglang/srt/mem_cache/memory_pool.py 的文件头进一步说明,SGLang 采用两级内存池:ReqToTokenPool 负责“请求到 token 位置”的映射,TokenToKVPoolAllocator 负责管理 KV cache 索引,真正的 KVCache 再持有物理缓存。
这组事实说明一个重要边界:model_executor 不是自己顺手保管 KV 状态;它依赖 mem_cache 提供明确的分配与映射层。也正因为这样,Scheduler 才能在 RadixCache、ChunkCache、分页分配器和不同 attention 变体之间切换,而不用把这些策略全塞进 ModelRunner 本身。对读者来说,这意味着你可以把“执行模型前向”与“为前向准备可复用缓存”分开阅读。
观测层不是附属品,而是独立切面#
python/sglang/srt/observability/req_time_stats.py 里定义了 RequestStage,其中包含 TOKENIZE、API_SERVER_DISPATCH、REQUEST_PROCESS、PREFILL_FORWARD、DECODE_FORWARD、DECODE_LOOP 等阶段名称。这说明 observability 不是在日志里随便打点,而是把 request lifecycle 的关键阶段抽成了稳定标签。
这也解释了为什么 observability 应该被看作独立层,而不是 managers 的附带目录。它服务的是“让系统外部可见”这个横切目标,和 tokenizer、scheduler、model runner 本身的职责不同。只要把这一层独立出来,你在读性能、延迟或 tracing 相关代码时才不会误以为那也是主链路逻辑的一部分。
常见误解与 tradeoff#
这一章里最常见的误解,是把“目录分层”直接等同于“架构绝对真相”。更稳的说法是:目录和入口文档提供了强信号,但最终我们仍然是在做工作性划分。比如 managers 中有的对象偏 orchestration,有的对象又同时承担一些状态汇总职责;这不影响整体边界有用,但提醒我们不要把结构图理解成 UML 式的完全静态真理。
这也对应一个 tradeoff:分层越清晰,越有利于读者和维护者建立整体心智模型;但分层越清晰,也越容易掩盖某些横切逻辑实际上分散在多个文件里。因此本章的策略不是“把一切讲绝”,而是先把稳定边界钉住,再在后续章节中逐渐补充那些跨层的实际耦合点。
为了避免把这件事写成抽象原则,可以把它理解成一个非常具体的判断:当你在一个文件里看到“它既处理 HTTP,又维护 request state,又直接管模型前向”,这通常意味着边界还不够清楚。而 SGLang 当前的 runtime 恰恰通过 entrypoints、managers、model_executor、mem_cache 和 observability 把这些职责分散到了更适合维护的位置。
如果这一层出问题,会表现成什么#
当这一层的边界被读错时,最常见的后果不是编译错误,而是排障路径走错。比如一个问题明明发生在 mem_cache 与 execution layer 的交界处,却被误当成 entrypoint 或 tokenizer 问题;又比如一个 tracing 指标明明属于 observability 切面,却被误判成 scheduler 主逻辑的一部分。
从调试角度看,本章真正提供的价值是“先决定该看哪一层,再决定看哪一个文件”。只要这个判断正确,后续无论是性能问题、并发问题还是扩展问题,都会少走很多弯路。
为什么不是“一个大 Engine 管一切”#
从表面看,把 HTTP、request state、batch 组织、model forward、cache 和 observability 都收进一个超大的 engine 似乎也能工作,甚至还省掉跨进程与跨模块沟通成本。但对 inference runtime 来说,这种设计的隐患也很明显:请求主线会和执行细节紧紧耦合,缓存策略会渗进协议层,排障时你很难快速判断问题到底发生在哪一层。
SGLang 当前的做法更接近“把最常变化的职责分开”。协议表面、编排逻辑、执行模型、缓存状态和观测切面虽然彼此配合,但不被迫写进同一个对象体系里。这种设计的代价当然是读源码时一开始会觉得分散,但换来的好处是:你可以按问题类型切换观察视角,而不用每次都在一个巨型 engine 里做全局推理。
本章对应哪些代码路径#
本章第一版的文件级锚点,至少包括 python/sglang/launch_server.py、python/sglang/srt/entrypoints/http_server.py、python/sglang/srt/entrypoints/engine.py、python/sglang/srt/managers/tokenizer_manager.py、python/sglang/srt/managers/scheduler.py、python/sglang/srt/managers/detokenizer_manager.py、python/sglang/srt/managers/schedule_batch.py、python/sglang/srt/model_executor/model_runner.py、python/sglang/srt/model_executor/model_runner_kv_cache_mixin.py 与 python/sglang/srt/observability/req_time_stats.py。这些锚点足以支撑“入口层、编排层、执行相关层、观测层”的初版叙事。
把这一章从模块级进一步下钻到文件级时,最稳的追踪路径是沿着 ScheduleBatch 如何变成 ForwardBatch、Scheduler 怎样与 ModelRunner 交互、以及 metrics / tracing 在哪些阶段落点这三条线往下走。第一版先把层次边界钉住,不把每一条内部调用都写死,目的是先读懂“谁负责什么”,再去读“谁具体调用了谁”。
这一章真正想帮你建立的,不是一个“所有目录都记住”的列表,而是一张边界图:入口负责接入,managers 负责编排,model_executor 负责真正执行,mem_cache 负责可复用状态,observability 负责把内部阶段暴露给外部系统。只要这张边界图稳住,后面每读一个子目录,你都更容易判断它到底在解决哪一层的问题。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。