Serving 层与 SRT 分层#

这章解决什么问题#

这一章要解决的问题不是“请求怎样一步步流动”,而是“为什么 SGLang 要把入口、编排、执行和观测拆成不同层”。如果没有这层理解,读者看到 http_server.pyscheduler.pymodel_executorobservability 这些区域时,会误以为它们只是按开发习惯分目录,而不是在划定真正的职责边界。

第一版在这里坚持一个保守原则:目录名和公开入口是事实,基于目录职责推断“分层意图”时,会明确说明这是工作性划分,而不是把目录结构直接等同于架构真相。这样可以避免过度解读源码布局。

事实锚点:进程边界本来就写在入口里#

python/sglang/launch_server.pypython/sglang/srt/entrypoints/* 看,SGLang 至少存在一层很明确的入口面:它负责接参数、选运行模式、接住 HTTP 或 gRPC 请求,并把后续工作交给 runtime path。这个层次更接近 serving surface,而不是底层生成机制本身。

更关键的是,python/sglang/srt/entrypoints/http_server.pylaunch_server(...) 文档字符串已经把运行时拆分说得非常直接:HTTP server 负责路由请求;engine 由 TokenizerManagerSchedulerDetokenizerManager 三个组件构成;HTTP server、EngineTokenizerManager 在主进程里,而另外两个 manager 是子进程,并通过 ZMQ 做 IPC。python/sglang/srt/entrypoints/engine.pyEngine 类文档字符串重复了同样的描述,这不是推断,而是源码作者自己给出的结构说明。

如果只用文字,这里最容易混淆的点是“入口层、编排层、执行层、观测层到底怎么分”。下面这张组件图承担的职责,就是把这些层次放到一张边界图里,并明确哪些是 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,并说明 ScheduleBatchscheduler.py::Scheduler 管理,ForwardBatchmodel_runner.py::ModelRunner 消费。这说明 managersmodel_executor 的分界不只是“一个偏控制,一个偏计算”这么模糊,而是连数据结构转换点都已经被写死在注释里了。

执行层与内存层:为什么 model_executormem_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 才能在 RadixCacheChunkCache、分页分配器和不同 attention 变体之间切换,而不用把这些策略全塞进 ModelRunner 本身。对读者来说,这意味着你可以把“执行模型前向”与“为前向准备可复用缓存”分开阅读。

观测层不是附属品,而是独立切面#

python/sglang/srt/observability/req_time_stats.py 里定义了 RequestStage,其中包含 TOKENIZEAPI_SERVER_DISPATCHREQUEST_PROCESSPREFILL_FORWARDDECODE_FORWARDDECODE_LOOP 等阶段名称。这说明 observability 不是在日志里随便打点,而是把 request lifecycle 的关键阶段抽成了稳定标签。

这也解释了为什么 observability 应该被看作独立层,而不是 managers 的附带目录。它服务的是“让系统外部可见”这个横切目标,和 tokenizer、scheduler、model runner 本身的职责不同。只要把这一层独立出来,你在读性能、延迟或 tracing 相关代码时才不会误以为那也是主链路逻辑的一部分。

常见误解与 tradeoff#

这一章里最常见的误解,是把“目录分层”直接等同于“架构绝对真相”。更稳的说法是:目录和入口文档提供了强信号,但最终我们仍然是在做工作性划分。比如 managers 中有的对象偏 orchestration,有的对象又同时承担一些状态汇总职责;这不影响整体边界有用,但提醒我们不要把结构图理解成 UML 式的完全静态真理。

这也对应一个 tradeoff:分层越清晰,越有利于读者和维护者建立整体心智模型;但分层越清晰,也越容易掩盖某些横切逻辑实际上分散在多个文件里。因此本章的策略不是“把一切讲绝”,而是先把稳定边界钉住,再在后续章节中逐渐补充那些跨层的实际耦合点。

为了避免把这件事写成抽象原则,可以把它理解成一个非常具体的判断:当你在一个文件里看到“它既处理 HTTP,又维护 request state,又直接管模型前向”,这通常意味着边界还不够清楚。而 SGLang 当前的 runtime 恰恰通过 entrypointsmanagersmodel_executormem_cacheobservability 把这些职责分散到了更适合维护的位置。

如果这一层出问题,会表现成什么#

当这一层的边界被读错时,最常见的后果不是编译错误,而是排障路径走错。比如一个问题明明发生在 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.pypython/sglang/srt/entrypoints/http_server.pypython/sglang/srt/entrypoints/engine.pypython/sglang/srt/managers/tokenizer_manager.pypython/sglang/srt/managers/scheduler.pypython/sglang/srt/managers/detokenizer_manager.pypython/sglang/srt/managers/schedule_batch.pypython/sglang/srt/model_executor/model_runner.pypython/sglang/srt/model_executor/model_runner_kv_cache_mixin.pypython/sglang/srt/observability/req_time_stats.py。这些锚点足以支撑“入口层、编排层、执行相关层、观测层”的初版叙事。

把这一章从模块级进一步下钻到文件级时,最稳的追踪路径是沿着 ScheduleBatch 如何变成 ForwardBatchScheduler 怎样与 ModelRunner 交互、以及 metrics / tracing 在哪些阶段落点这三条线往下走。第一版先把层次边界钉住,不把每一条内部调用都写死,目的是先读懂“谁负责什么”,再去读“谁具体调用了谁”。

这一章真正想帮你建立的,不是一个“所有目录都记住”的列表,而是一张边界图:入口负责接入,managers 负责编排,model_executor 负责真正执行,mem_cache 负责可复用状态,observability 负责把内部阶段暴露给外部系统。只要这张边界图稳住,后面每读一个子目录,你都更容易判断它到底在解决哪一层的问题。