SGLang:TokenizerManager、Scheduler、DetokenizerManager
主线位置:整条「一个请求的一生」在 SGLang 里的走法。默认路径上 HTTP、模板与分词、调度、反分词都是 Python,前向是 Python 调 CUDA kernel;KV 前缀匹配用的树核心在 Linux 上条件默认是 Rust。
SGLANG_RUST_SERVER=1时,HTTP、分词、反分词换成嵌在调度器进程里的 Rust 线程,调度、KV、前向、采样仍是 Python。
版本:SGLang v0.5.22
先说结论:
- 单卡、
sglang serve不加参数时有三个核心进程。主进程跑 uvicorn + FastAPI 和TokenizerManager,聊天模板和分词都在它的 asyncio 事件循环里同步做。Scheduler 是子进程,调度、KV 管理、前向、采样都在这里。DetokenizerManager 是另一个子进程,专门把 token id 按批解码成文本。 - 三个进程用三段 ZMQ PUSH→PULL 连成一个环:主进程 → Scheduler → DetokenizerManager → 主进程。没有请求-应答的配对,每段都是单向的。
- 序列化默认是 pickle(
SGLANG_USE_PICKLE_IPC默认True),设为假才走 msgspec 的 msgpack。 - Scheduler 默认走重叠循环:本批在 GPU 上跑的时候,CPU 处理上一批的结果并排下一批。
- 打开
SGLANG_RUST_SERVER=1后,不再起 DetokenizerManager 和 Python 的 HTTP 服务;HTTP、分词、反分词变成 rank 0 调度器进程里的 Rust 线程,和 Python 调度循环之间走进程内的 flume 通道。
进程结构
flowchart LR
client(["HTTP 客户端"])
subgraph MAIN["主进程(Python)"]
loop["asyncio 事件循环<br/>FastAPI、OpenAIServingChat<br/>TokenizerManager"]
end
subgraph SCHED["Scheduler 进程(Python + CUDA)"]
sl["调度循环(重叠)<br/>KV 管理、前缀树<br/>TpModelWorker 前向、采样"]
end
subgraph DETOK["DetokenizerManager 进程(Python)"]
dl["事件循环<br/>按批反分词"]
end
client <-->|"HTTP / SSE"| loop
loop -->|"PUSH→PULL<br/>scheduler_input_ipc"| sl
sl -->|"PUSH→PULL<br/>detokenizer_ipc"| dl
dl -->|"PUSH→PULL<br/>tokenizer_ipc"| loop
和 vLLM 比,有两处不同。第一,反分词单独一个进程,主进程收到的已经是文本。第二,通道是环,不是一对;Scheduler 发出的东西不直接回主进程,而是先经过 DetokenizerManager。
时序
一个流式的 /v1/chat/completions 请求:
sequenceDiagram
participant C as 客户端
participant M as 主进程事件循环
participant S as Scheduler
participant D as DetokenizerManager
C->>M: POST /v1/chat/completions(stream)
M->>M: 套聊天模板、分词(同步)
M->>M: TokenizerManager 登记请求状态
M->>S: ZMQ PUSH(默认 pickle)
loop 每一步,直到请求结束
S->>S: 收请求、组批、前向、采样
S->>D: ZMQ PUSH 一批 token id
D->>D: batch_decode
D->>M: ZMQ PUSH 一批文本
M->>M: 追加到请求状态,唤醒等待者
M-->>C: SSE 一块 data
end
M-->>C: data: [DONE]
一段一段走
启动:谁拉起谁
HTTP 模式的入口是 launch_server(http_server.py#L2901),它的文档字符串把结构说得很直接:
python/sglang/srt/entrypoints/http_server.py#L2915
- The engine consists of three components:
1. TokenizerManager: Tokenizes the requests and sends them to the scheduler.
2. Scheduler (subprocess): Receives requests from the Tokenizer Manager, schedules batches, forwards them, and sends the output tokens to the Detokenizer Manager.
3. DetokenizerManager (subprocess): Detokenizes the output tokens and sends the result back to the Tokenizer Manager.
真正拉进程的是 Engine._launch_subprocesses(engine.py#L1121):
- 每张卡起一个 Scheduler 进程(engine.py#L889),入口
run_scheduler_process(scheduler.py#L6076)。前向由 Scheduler 自己持有的TpModelWorker执行(scheduler.py#L1009),不另起 worker 进程。 - 起 DetokenizerManager 进程(engine.py#L1055)。
detokenizer_worker_num默认 1(fields/serving.py#L54),大于 1 时多起几个 worker 再加一个分发进程。 - 在主进程里建
TokenizerManager(engine.py#L1327)。tokenizer_worker_num默认 1(fields/serving.py#L50)。
之后主进程跑 uvicorn(http_server.py#L2952)。
张量并行大于 1 时有多个 Scheduler 进程,只有 rank 0 从主进程收请求,再广播给其他 rank(scheduler_components/request_receiver.py#L80)。这部分留给 05。
通道和序列化
三段通道都在各自的 init_ipc_channels 里建:
| 段 | 发送端 | 接收端 |
|---|---|---|
| 主进程 → Scheduler | TokenizerManager 的 PUSH(tokenizer_manager.py#L668) | Scheduler rank 0 的 PULL(ipc_channels.py#L38) |
| Scheduler → DetokenizerManager | Scheduler 的 PUSH(ipc_channels.py#L64) | DetokenizerManager 的 PULL(detokenizer_manager.py#L126) |
| DetokenizerManager → 主进程 | DetokenizerManager 的 PUSH(detokenizer_manager.py#L133) | TokenizerManager 的 PULL(tokenizer_manager.py#L664) |
收发都经过 io_struct.py 里的一组函数:
python/sglang/srt/managers/io_struct.py#L2710
def sock_send(socket: zmq.Socket, obj: Any, flags: int = 0) -> None:
if _USE_PICKLE_IPC:
socket.send_pyobj(obj, flags=flags, protocol=pickle.HIGHEST_PROTOCOL)
return
socket.send(msgpack_encode(obj), flags=flags)
_USE_PICKLE_IPC 来自 SGLANG_USE_PICKLE_IPC,默认 True(environ.py#L361)。所以默认路径传的是 pickle;msgpack 分支要求每个消息类型都是 msgspec.Struct,否则直接报错。这一点和 vLLM 不同:vLLM 的引擎边界只有 msgpack 一种。
HTTP 进来,模板与分词
路由在 http_server.py#L1803,交给 OpenAIServingChat.handle_request(serving_base.py#L73)。聊天请求在这一层就套好模板、编成 token id:
python/sglang/srt/entrypoints/openai/serving_chat.py#L1846
else:
rendered_prompt = self.tokenizer_manager.tokenizer.apply_chat_template(
messages,
tokenize=False,
# ...
)
prompt_ids = parallel_prompt_encode(
self.tokenizer_manager.tokenizer, rendered_prompt, encode_kwargs
)
这段在 _render_and_encode_chat_template(serving_chat.py#L1800)里,直接在事件循环上调用,没有丢进线程池。parallel_prompt_encode 对长 prompt 会把文本切开、交给 HF tokenizers 的 encode_batch 在它自己的线程池里并行编码,条件不满足时退回普通的 encode。模板渲染的结果有一个小缓存,命中就跳过。
拿到 token id 后,TokenizerManager.generate_request(tokenizer_manager.py#L896)不用再分词。原生 /generate 接口送来的是文本时,才在 _tokenize_texts(#L1021)里分词;能把并发请求攒成一批分词的 enable_dynamic_batch_tokenizer 默认关(fields/serving.py#L295)。
_send_one_request(#L1769)把请求包成 TokenizedGenerateReqInput,经 _dispatch_to_scheduler(#L683)调 sock_send 发出。这是同步的 send,PUSH 套接字在队列没满时立即返回。
Scheduler:重叠循环
disable_overlap_schedule 默认 False(fields/schedule.py#L187),于是 enable_overlap 为真(scheduler.py#L495),单卡、不分离时进入 event_loop_overlap(scheduler.py#L5997):
python/sglang/srt/managers/scheduler.py#L1975
def event_loop_overlap(self):
"""A scheduler loop that overlaps the CPU processing and GPU computation."""
# ...
while True:
# ...
# Receive requests
self.ingest_requests()
# ...
# Get the next batch to run
plan = self.get_next_batch_to_run(
running_batch=self.running_batch, last_batch=self.last_batch
)
# ...
# Launch the current batch
if batch:
batch_result = self.run_batch(batch)
# ...
self.result_queue.append((batch.copy(), batch_result))
# ...
# Process the last batch
if self.last_batch:
if not disable_overlap_for_batch:
pop_and_process()
每一圈:
ingest_requests(#L2098)用非阻塞方式把 PULL 上积攒的请求全收下来,放进等待队列。get_next_batch_to_run(#L3766)决定这一批是预填充还是解码、放哪些请求;前缀匹配和 KV 分配在这里发生(02、03 部分)。run_batch(#L4490)把这一批发给 GPU,不等它算完。- 趁 GPU 在算,
process_batch_result(#L4921)处理上一批的结果:追加新 token、判断结束、释放 KV,再stream_output。
源码里留了一个例外:连续两批都是预填充时,可以先处理完上一批再发新一批,换第一批更早出首 token。它由 SGLANG_DISABLE_CONSECUTIVE_PREFILL_OVERLAP 控制,默认 False(environ.py#L642),也就是默认照样重叠。
stream_output(scheduler_components/output_streamer.py#L119)把这一批里所有要输出的请求打成一个 BatchTokenIDOutput,发给 DetokenizerManager(#L219)。和 vLLM 一样,调度器进程只发 token id。
DetokenizerManager:按批解码
DetokenizerManager 的主循环只有几行:
python/sglang/srt/managers/detokenizer_manager.py#L180
def event_loop(self):
"""The event loop that handles requests"""
while True:
with self.soft_watchdog.disable():
recv_obj = sock_recv(self.recv_from_scheduler)
output = self._request_dispatcher(recv_obj)
if output is not None:
sock_send(self.send_to_tokenizer, output)
self.soft_watchdog.feed()
handle_batch_token_id_out(#L444)对整批请求一起调 tokenizer.batch_decode(#L276),产出 BatchStrOutput 发回主进程。vLLM 是每个请求一个增量反分词器;SGLang 是每批一次批量解码,放在独立进程里,不占主进程的事件循环。
回到主进程:唤醒和 SSE
TokenizerManager 的 handle_loop(tokenizer_manager.py#L2463)是事件循环上的一个后台任务,第一次收请求时创建(#L2438)。它从 PULL 收 BatchStrOutput,在 _handle_batch_output(#L2478)里按请求 id 找到状态,把输出追加进 out_list,再 event.set() 唤醒等待者(#L2777)。
等待者是这个请求的 _stream_one_response(#L1946),它把输出逐块交给 _generate_chat_stream(serving_chat.py#L1965)。后者拼 OpenAI 格式的 SSE 块,工具调用和推理内容的解析也在这里,最后发 data: [DONE](#L2304)。
每一段在哪、是什么语言
| 段 | 进程 | 源文件 | 默认 | SGLANG_RUST_SERVER=1 |
|---|---|---|---|---|
| HTTP 进来 | 主进程 | entrypoints/http_server.py | Python | Rust(调度器进程内的线程) |
| 模板与分词 | 主进程 | entrypoints/openai/serving_chat.py | Python | Rust |
| 请求状态、发给调度器 | 主进程 | managers/tokenizer_manager.py | Python,ZMQ,默认 pickle | Rust 线程 → flume 通道 → Python |
| 组批、前缀匹配、KV 分配 | Scheduler | managers/scheduler.py、mem_cache/ | Python;树核心条件默认 Rust | 不变 |
| 前向、采样 | Scheduler | managers/tp_worker.py、model_executor/ | Python 调 CUDA | 不变 |
| 发 token id | Scheduler → DetokenizerManager | scheduler_components/output_streamer.py | Python,ZMQ | Python → flume 通道 → Rust |
| 反分词 | DetokenizerManager | managers/detokenizer_manager.py | Python | Rust |
| 唤醒、SSE、解析 | 主进程 | tokenizer_manager.py、serving_chat.py | Python | Rust |
前缀树核心的 Rust 实现和它的回退条件见 树核心。
打开 Rust 服务之后
SGLANG_RUST_SERVER 默认 False(environ.py#L1902)。设为 1 后,_launch_subprocesses 只起 Scheduler,不起 DetokenizerManager,也不建 TokenizerManager:
python/sglang/srt/entrypoints/engine.py#L1292
# The embedded Rust server (started inside the rank-0 scheduler) owns
# the API server, tokenization, and detokenization. In that mode we do
# not start the Python detokenizer subprocess(es) or tokenizer manager.
# Do not use RayEngine with the Rust server, as it is not supported.
if envs.SGLANG_RUST_SERVER.get():
scheduler_init_result.wait_for_ready()
主进程也不跑 Python 的 HTTP 服务,只等调度器退出(http_server.py#L2943)。HTTP 端口由 rank 0 调度器进程里的 Rust 线程监听:调度器初始化时调 maybe_init_rust_server(scheduler.py#L2341),由 RustServer.launch(rust_server/server.py#L124)启动 PyO3 扩展里的服务线程,然后把收请求的来源换掉:
python/sglang/srt/managers/scheduler.py#L2352
rust_server = self.get_rust_server_class().launch(self)
self.rust_server = rust_server
# The rust server *is* the ingress source: SchedulerRequestReceiver
# drains its request ring (rust_server_mode) instead of a zmq socket.
self.recv_from_tokenizer = rust_server
出口也换了:stream_output 看到 rust_server 存在,就调 push_generation 而不是发给 DetokenizerManager(output_streamer.py#L216)。Rust 和 Python 之间是两条进程内的 flume 通道,传的是 msgpack 字节和命名缓冲区(rust/sglang-server/src/tokenizer_manager/channel.rs#L1)。
flowchart LR
client(["HTTP 客户端"])
mainp["主进程(Python)<br/>只等调度器退出"]
subgraph SCHED["rank 0 Scheduler 进程"]
subgraph RT["Rust 线程(PyO3 扩展)"]
rh["HTTP、模板、分词<br/>反分词、SSE"]
end
subgraph PY["Python 调度线程"]
sl["调度循环、KV、前缀树<br/>前向、采样"]
end
rh -->|"flume:请求"| sl
sl -->|"flume:token id"| rh
end
mainp -.->|"拉起"| SCHED
client <-->|"HTTP / SSE"| rh
换掉的是表里标「Rust」的段:HTTP、模板与分词、反分词、SSE,加上两条通道。不变的是调度循环以内的一切。进程从三个变成两个,而且 Rust 和 Python 调度循环共用一个进程,没有 ZMQ 也没有 pickle。Rust 服务注册了 /generate、/v1/chat/completions、/v1/completions、/v1/models、/health 等接口(如 rust/sglang-server/src/api_server/http/openai/chat.rs#L49);离线的 sgl.Engine 不支持这个模式,会直接报错(engine.py#L309)。
它和 vLLM 的独立进程方案的对比见 两种 Rust 前端。默认路径和 Rust 服务在同一模型上的吞吐和延迟对比,待实测(C3-2)。
小结
- 单卡默认三个核心进程:主进程(HTTP、模板、分词、
TokenizerManager)、Scheduler(调度、KV、前向、采样)、DetokenizerManager(反分词),全是 Python,前向调 CUDA。 - 三段 ZMQ PUSH→PULL 连成环;默认传 pickle(
SGLANG_USE_PICKLE_IPC默认True),设为假才走 msgpack。 - 聊天模板和分词在主进程的事件循环上同步做,长 prompt 交给 HF tokenizers 的线程池并行编码;反分词在独立进程里按批做。
- Scheduler 默认重叠循环:GPU 算本批时,CPU 处理上一批结果并排下一批;连续两批预填充时不重叠是可选项,默认关。
SGLANG_RUST_SERVER=1:不起 DetokenizerManager 和 Python HTTP,rank 0 调度器进程里多出 Rust 线程,进出走 flume 通道;离线Engine不支持。