数据截至 (上游 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):
- 先
PortArgs.init_new(server_args)分配一组 IPC 端口(engine.py:1073)。 _launch_scheduler_processes按 TP/PP/DP 拓扑拉起 N 个 scheduler 子进程,每个跑run_scheduler_process(engine.py:867的target=run_scheduler_process_func)。- 再拉 DetokenizerManager 子进程(
run_detokenizer_process)。 - 主进程自己实例化
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_tokenizer | PULL | 收新请求 |
send_to_detokenizer | PUSH | 发 token 输出 |
send_to_tokenizer | PUSH | 发控制类消息(abort、健康检查) |
recv_from_rpc | DEALER | 收 RPC(权重更新等) |
只有 TP rank 0 建这些 socket(ipc_channels.py:36 的 is_rank_zero 分支);其余 rank 与外界隔绝,只靠 NCCL/gloo 和 rank 0 对齐。所以对外看,N 个 scheduler 进程是「一个逻辑调度器」。
TokenizerManager 侧对称:send_to_scheduler / recv_from_detokenizer 在 tokenizer_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_run→run_batch→process_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)的关键动作:
- 上一批的结果先压进
result_queue,不急着处理。 - 当前批照常
run_batch——在enable_overlap路径上(run_batch,scheduler.py:3770),forward 被发射到独立的forward_streamCUDA stream 上,立即返回一个「结果占位符」。 - 返回后 CPU 回头
pop_and_process()处理上一批——此刻 GPU 正在算当前批。 - 采样也拆出去延后做:
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.py的MultimodalPreprocessCache初始化)。 - 进程死掉要拉着全家一起走。 scheduler 异常时会
SIGQUIT父进程(scheduler.py:5326的parent_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.py | launch_server |
| 子进程拉起 | python/sglang/srt/entrypoints/engine.py | Engine._launch_subprocesses、_launch_scheduler_processes |
| scheduler 进程起点 | python/sglang/srt/managers/scheduler.py | run_scheduler_process |
| 事件循环 | python/sglang/srt/managers/scheduler.py | event_loop_normal、event_loop_overlap |
| ZMQ 通道 | python/sglang/srt/managers/scheduler_components/ipc_channels.py | SchedulerIpcChannels.create |
| 分词管理器 | python/sglang/srt/managers/tokenizer_manager.py | TokenizerManager.generate_request、handle_loop |
| 反分词 | python/sglang/srt/managers/detokenizer_manager.py | DetokenizerManager.event_loop、trim_matched_stop |
| GPU worker forward | python/sglang/srt/managers/tp_worker.py | TpModelWorker.forward_batch_generation |