跳到主要内容

数据截至 (上游 commit f775db03aaa8)

01 · 服务端进程架构

本章讲什么: 一个 python -m sglang.launch_server 到底拉起了哪些进程、它们之间怎么通信、为什么这样拆。这是后面所有章节的地基。

1.1 它要解决的小问题

LLM 推理服务里,只有 GPU 上的矩阵乘是真正贵的资源。但一条请求要完整走完,还绕不开一堆 CPU 活:分词、套 chat template、解析采样参数、把 token 转回文本、HTTP 收发。

如果这些 CPU 活和 GPU forward 挤在同一个进程里串行执行,GPU 每算完一步都要停下来等 CPU——卡越贵,浪费越大。

1.2 思路:按资源类型拆进程,用管道连起来

SGLang 的拆法很直接:谁吃什么资源,谁就进哪个进程,进程之间用 ZMQ 消息管道单向传递:

  • 吃网络/CPU 的(HTTP、分词)留在主进程,用 asyncio。
  • 吃 GPU 的(调度 + forward + 采样)进子进程,每张(组)卡一个。
  • 只吃 CPU 的收尾活(detokenize)再单独一个子进程。

这样 GPU 进程的主循环可以短到只有「收请求 → 组批 → forward → 发结果」四件事。

1.3 进程拓扑图

客户端 ──HTTP──► [主进程] HTTP server (FastAPI,openai/serving_chat.py)
│ 同进程调用

[主进程] TokenizerManager(asyncio:分词、mm 预处理、grammar 登记)
│ ZMQ PUSH

[子进程 ×tp_size] Scheduler 事件循环
│ 同进程

TpModelWorker → ModelRunner → GPU forward
│ ZMQ PUSH(token ids)

[子进程] DetokenizerManager(增量 detok、stop 裁剪)
│ ZMQ PUSH(文本增量)

[主进程] TokenizerManager ──SSE──► 客户端

怎么读这张图: 箭头是 ZMQ socket 的方向。注意回环:scheduler 不直接回客户端,产出绕一圈「scheduler → detokenizer → tokenizer → HTTP」才回去——每一站只做自己最擅长的转换。

1.4 真实实现

入口与分工声明

launch_server(python/sglang/srt/entrypoints/http_server.py:2787)是总入口。它的 docstring 把分工写得明明白白——这是读这份代码的第一个锚点:

The engine consists of three components: 1. TokenizerManager … 2. Scheduler (subprocess) … 3. DetokenizerManager (subprocess) … Inter-process communication is done through IPC via the ZMQ library.

真正拉进程的是 Engine._launch_subprocesses(python/sglang/srt/entrypoints/engine.py:1024):

  1. PortArgs.init_new(server_args) 分配一组 IPC 端口(engine.py:1073)。
  2. _launch_scheduler_processes 按 TP/PP/DP 拓扑拉起 N 个 scheduler 子进程,每个跑 run_scheduler_process(engine.py:867target=run_scheduler_process_func)。
  3. 再拉 DetokenizerManager 子进程(run_detokenizer_process)。
  4. 主进程自己实例化 TokenizerManager(python/sglang/srt/managers/tokenizer_manager.py:394)。

每个 scheduler 子进程的起点是 run_scheduler_process(python/sglang/srt/managers/scheduler.py:5378):配好 rank、构造 Scheduler、把初始化信息从管道回传父进程,然后 scheduler.run_event_loop() 进入死循环。

通信:ZMQ 单向管道

scheduler 侧的四个 socket 在 SchedulerIpcChannels.create(python/sglang/srt/managers/scheduler_components/ipc_channels.py:26)里一次建好:

socket模式方向
recv_from_tokenizerPULL收新请求
send_to_detokenizerPUSH发 token 输出
send_to_tokenizerPUSH发控制类消息(abort、健康检查)
recv_from_rpcDEALER收 RPC(权重更新等)

只有 TP rank 0 建这些 socket(ipc_channels.py:36is_rank_zero 分支);其余 rank 与外界隔绝,只靠 NCCL/gloo 和 rank 0 对齐。所以对外看,N 个 scheduler 进程是「一个逻辑调度器」。

TokenizerManager 侧对称:send_to_scheduler / recv_from_detokenizertokenizer_manager.py:555-565 建立;回包在 asyncio 的 handle_loop(tokenizer_manager.py:2172)里收,按 rid 找到对应请求的 ReqState,经 SSE 推给客户端。DetokenizerManager 的主循环极简——收 token、转文本、发回(detokenizer_manager.py:177,event_loop),其中 trim_matched_stop 负责把 stop 串从输出里裁掉。

主循环的两种形态

Scheduler 有两个事件循环,由 disable_overlap_schedule(python/sglang/srt/server_args.py:880,默认 False)选择:

  • event_loop_normal(scheduler.py:1759)——朴素的串行版:收请求 → get_next_batch_to_runrun_batchprocess_batch_result,一轮一轮走。
  • event_loop_overlap(scheduler.py:1794)——默认的流水线版,见下一节。

两者共享同一套组批/执行函数,区别只在「结果什么时候处理」。

1.5 overlap 调度:CPU 和 GPU 的两级流水线

直觉: GPU forward 一旦发射(launch)就是异步的,CPU 不必等它算完。于是第 N 批的 GPU 计算可以和第 N-1 批的 CPU 后处理(检查结束条件、写 radix 树、准备发回)同时进行。

event_loop_overlap(scheduler.py:1794-1864)的关键动作:

  1. 上一批的结果先压进 result_queue,不急着处理。
  2. 当前批照常 run_batch——在 enable_overlap 路径上(run_batch,scheduler.py:3770),forward 被发射到独立的 forward_stream CUDA stream 上,立即返回一个「结果占位符」。
  3. 返回后 CPU 回头 pop_and_process() 处理上一批——此刻 GPU 正在算当前批。
  4. 采样也拆出去延后做:TpModelWorker.forward_batch_generation(python/sglang/srt/managers/tp_worker.py:593)在 overlap 下把 sample 包成 delay_sample_func 闭包,由调度器在合适的时机(launch_batch_sample_if_needed)触发。

「结果还没算出来,CPU 怎么敢先走?」靠 future_map:跨 stream 引用的 GPU tensor 被登记成 future,CPU 侧只有在真正需要数值(比如读 token id)时才 resolve——resolve 点被小心地安排在 GPU 算完之后。这就是 README 里 "zero-overhead batch scheduler" 的落地形态。

原理演示(把上面的循环画成简化代码):

# 示意,非源码:overlap 循环的骨架
while True:
recv_new_requests() # 收新请求进 waiting 队列
batch = plan_next_batch(running_batch) # CPU:组第 N 批
if batch:
result = launch_forward_async(batch) # 发射 GPU forward,立即返回占位符
result_queue.append((batch, result))
if last_batch:
prev = result_queue.popleft()
process_result(prev) # CPU:处理第 N-1 批(此刻 GPU 正忙)
maybe_run_delayed_sample(prev) # 采样也延后到这里
last_batch = batch

重点看: process_result 和 GPU 的 forward 在墙上时钟上是重叠的。

有两个例外会主动关掉 overlap(is_disable_overlap_for_batch,scheduler.py:1867):连续两个 prefill 批之间关掉以压低首 token 延迟;投机采样 + grammar 的特定组合需要 FSM 先同步。都是「宁可牺牲一点吞吐也要保证正确/延迟」的保守分支。

1.6 关键细节与坑

  • TP > 1 时请求只进 rank 0。 其余 rank 的 scheduler 也在跑同样的循环,但请求、控制消息由 rank 0 通过分布式组广播对齐——所以组批决策在所有 rank 上必须严格一致,否则 hang(第 4 章 grammar 的 all_gather 同步就是为此)。
  • skip_tokenizer_init 模式下 detokenizer 被短路。 当引擎作为库被嵌入(RL rollout 常见),不需要分词器时,scheduler 的输出直接 PUSH 回 tokenizer 进程(ipc_channels.py:50-58 的分支)。
  • 多 tokenizer worker。 分词本身也能横向扩成多进程(tokenizer_worker_num),主进程退化成 router(multi_tokenizer_mixin.py);多模态预处理缓存按 worker 数均摊内存(base_processor.pyMultimodalPreprocessCache 初始化)。
  • 进程死掉要拉着全家一起走。 scheduler 异常时会 SIGQUIT 父进程(scheduler.py:5326parent_process.send_signal),可选 SGLANG_KILLPG_ON_SCHEDULER_EXCEPTION 直接杀进程组——避免一张卡 hang 住、其他 rank 刷屏报 NCCL 错误。
  • Rust 服务端是另一条路。 SGLANG_RUST_SERVER(http_server.py:2815 附近)把 HTTP/tokenizer/detokenizer 换成 Rust 实现,主进程不再跑 Python HTTP server;本文主线讲 Python 路径。

1.7 本章代码地图

主题文件路径符号名
总入口python/sglang/srt/entrypoints/http_server.pylaunch_server
子进程拉起python/sglang/srt/entrypoints/engine.pyEngine._launch_subprocesses_launch_scheduler_processes
scheduler 进程起点python/sglang/srt/managers/scheduler.pyrun_scheduler_process
事件循环python/sglang/srt/managers/scheduler.pyevent_loop_normalevent_loop_overlap
ZMQ 通道python/sglang/srt/managers/scheduler_components/ipc_channels.pySchedulerIpcChannels.create
分词管理器python/sglang/srt/managers/tokenizer_manager.pyTokenizerManager.generate_requesthandle_loop
反分词python/sglang/srt/managers/detokenizer_manager.pyDetokenizerManager.event_looptrim_matched_stop
GPU worker forwardpython/sglang/srt/managers/tp_worker.pyTpModelWorker.forward_batch_generation