SGLang 推理框架代码结构与核心逻辑
一、仓库整体结构
sglang/ ├── python/sglang/ # 核心 Python 代码 │ ├── srt/ # SGLang Runtime(推理引擎核心) │ └── ... # 前端 DSL / API 等 ├── rust/ # Rust 实现的高性能组件(如 router) ├── sgl-model-gateway/ # 模型网关服务 ├── 3rdparty/ # 第三方依赖(如 sgl-kernel) ├── benchmark/ # 性能测试 ├── test/ # 测试用例 ├── docker/ # Docker 部署 ├── docs/ # 文档/Cookbook └── examples/ # 使用示例二、核心推理引擎 (python/sglang/srt/)
| 目录/文件 | 功能 |
|---|---|
entrypoints/ | HTTP/gRPC 入口,兼容 OpenAI API |
managers/scheduler.py | 调度器:核心中的核心,管理 prefill/decode 批调度 |
managers/tokenizer_manager.py | 接收请求、tokenize、分发给 scheduler |
model_executor/ | 模型前向推理执行器,管理 CUDA graph、forward batch |
models/ | 各模型实现(Llama, DeepSeek, Qwen 等) |
layers/ | 模型层实现,包括 attention、quantization、MoE 等 |
mem_cache/ | KV Cache 管理:RadixTree、内存池、缓存策略 |
disaggregation/ | PD 分离:Prefill-Decode 解耦架构 |
speculative/ | 投机解码 |
distributed/ | 分布式通信(TP/PP/DP/EP) |
sampling/ | 采样策略 |
三、推理逻辑主流程
用户请求 → HTTP Server → TokenizerManager → Scheduler → ModelRunner → 返回 (顾客点菜) (前台接待) (服务员翻译菜单) (厨房调度) (厨师做菜) (上菜)3.0 进程结构:SGLang 是多进程架构
SGLang 启动时不是单一进程,而是多进程 + ZMQ 通信:
┌──────────────────────┐ ZMQ/IPC ┌──────────────────────┐ ZMQ/IPC ┌──────────────────┐ │ HTTP Server 进程 │ ────────→ │ Scheduler 进程 │ ────────→ │ Detokenizer 进程 │ │ (FastAPI + │ │ (调度 + ModelRunner) │ │ (还原文本) │ │ TokenizerManager) │ ←──────── │ 跑在 GPU 上 │ ←──────── │ │ └──────────────────────┘ └──────────────────────┘ └──────────────────┘- TokenizerManager 与 HTTP Server同进程(异步协程)
- Scheduler 是独立进程,占用 GPU
- 启动代码:
entrypoints/engine.py的init_tokenizer_manager、run_scheduler_process、run_detokenizer_process
设计目的:HTTP 进程崩溃不影响 GPU 上的 Scheduler;tokenize 的 CPU 开销不阻塞 GPU。
3.1 阶段一:HTTP Server → TokenizerManager(详细链路)
以"帮我写一首关于春天的诗"为例:
POST /v1/chat/completions { "model": "Qwen2.5-7B", "messages": [{"role": "user", "content": "帮我写一首关于春天的诗"}], "stream": true }经过的每一层:
| # | 阶段 | 做什么 | 关键代码位置 |
|---|---|---|---|
| ① | Uvicorn | 收 TCP 字节流,解 HTTP 协议 | ASGI 服务器(非 SGLang 代码) |
| ② | FastAPI 路由 | 匹配到openai_v1_chat_completions | http_server.py:1702 |
| ③ | validate_json_request | 校验Content-Type: application/json | http_server.py:627 |
| ④ | CORS / 解压中间件 | 跨域头、可选的 gzip/br 解压 | http_server.py:460-473 |
| ⑤ | Pydantic 反序列化 | JSON →ChatCompletionRequest对象,字段类型校验 | entrypoints/openai/protocol.py |
| ⑥ | OpenAIServingChat.handle_request | 打时间戳、业务校验、日志、分流 stream/非 stream | serving_base.py:73 |
| ⑦ | _convert_to_internal_request | OpenAI 协议 → SGLang 内部协议 | serving_chat.py:903 |
| ⑧ | _process_messages | 应用 chat template,可能直接 tokenize | serving_chat.py:1029 |
| ⑨ | to_sampling_params | 打包 temperature/top_p/max_tokens 等 | entrypoints/openai/protocol.py |
| ⑩ | 构造GenerateReqInput | SGLang 内部统一请求对象 | managers/io_struct.py |
| ⑪ | tokenizer_manager.generate_request(...) | 正式进入 TokenizerManager | serving_chat.py:1510 / 1720 |
chat template 举例(第 ⑧ 步):
[{"role":"user","content":"帮我写一首关于春天的诗"}] ↓ apply_chat_template "<|im_start|>user\n帮我写一首关于春天的诗<|im_end|>\n<|im_start|>assistant\n" ↓ tokenize(可能在此步、也可能延后到 TokenizerManager) [24212, 2170, 5765, 671, 7941, ...]关键点:tokenize 有时在 HTTP 进程里就做完(apply_chat_template(tokenize=True)),
有时把字符串传给 TokenizerManager 再 tokenize,取决于是否多模态、chat_encoding_spec 等。
3.2 阶段二:TokenizerManager → Scheduler
TokenizerManager(
managers/tokenizer_manager.py):- 兜底 tokenize(若上一步没做)
- 分配 request id(rid)
- 通过 ZMQ 把
GenerateReqInput发送给 Scheduler 进程 - 用 asyncio Future 等待结果,支持流式返回
Scheduler(
managers/scheduler.py):- 维护等待队列和运行批次(running batch)
- 每次迭代(几毫秒一次)决策:新请求 prefill / 老请求 decode / 淘汰 KV Cache
- Continuous Batching:请求完成立即离开,新请求立即插入
- 管理 KV Cache 的分配与回收
ModelRunner(
model_executor/):- Prefill:一次性处理 prompt,生成 KV Cache
- Decode:batch 中每个请求生成 1 个 token
- CUDA Graph 捕获重放,减少 kernel launch 开销
结果返回:
- 输出 token 通过 ZMQ 发给 Detokenizer 进程
- Detokenizer 还原文本,回传给 TokenizerManager
- TokenizerManager 通过
event.set()唤醒等待协程,经 SSE 流式返回给用户
3.3 深入:TokenizerManager 如何通过 ZMQ + Event 与 Scheduler 通信
通信架构:两条独立通道
发送和接收是独立的 ZMQ 管道,不是"请求-响应"模式:
TokenizerManager 进程 Scheduler 进程 ┌────────────────────────┐ ┌──────────────────┐ │ send_to_scheduler │ ZMQ PUSH │ 接收请求 │ │ (PUSH socket) ─────────┼───────────────────→│ 执行推理 │ │ │ │ │ │ recv_from_detokenizer │ ZMQ PULL │ Detokenizer │ │ (PULL socket) ←────────┼────────────────────┼── 发回结果 │ └────────────────────────┘ └──────────────────┘请求和响应靠rid(request id)对应。
核心流程三步:发送 → 等待 → 唤醒
步骤 A:建立"信箱" —_init_req_state(tokenizer_manager.py:3414)
state=ReqState(out_list=[],# 结果队列(后台循环往里塞数据)finished=False,# 是否完成event=asyncio.Event(),# ★核心:用于唤醒等待协程★obj=sub_obj,time_stats=...,)self.rid_to_state[rid]=state# 按 rid 注册到全局字典为什么用 Event 而不是 Future?——一个请求流式产出多个 token,Future 只能 resolve 一次,
Event 可以反复set()/clear()。
步骤 B:ZMQ 发送 —_send_one_request(tokenizer_manager.py:1540)
def_send_one_request(self,tokenized_obj):tokenized_obj=wrap_shm_features(tokenized_obj)# 多模态大张量走共享内存tokenized_obj.wrap_pickle_fields()# 部分字段 pickleself._dispatch_to_scheduler(tokenized_obj)# sock_send → ZMQ PUSH(非阻塞)state.dispatched=True_dispatch_to_scheduler最终调用sock_send(self.send_to_scheduler, obj),扔进管道就返回。
步骤 C:Event 等待 —_wait_one_response(tokenizer_manager.py:1662)
asyncdef_wait_one_response(self,obj,request=None):state=self.rid_to_state[obj.rid]whileTrue:awaitasyncio.wait_for(state.event.wait(),timeout=5s)# 挂起等信号out_list=state.out_list# 取走所有已到达的结果state.out_list=[]finished=state.finished state.event.clear()# 清信号,准备等下一批iffinished:yieldout;break# 完成,结束ifis_stream:yieldout# 流式:吐中间结果,继续等event.wait()让协程挂起不占 CPU- 超时后检测客户端是否断连,断了就 abort
- 被唤醒后取走结果 → yield(流式返回)→ clear → 继续循环
接收侧:后台handle_loop如何唤醒
auto_create_handle_loop启动一个后台协程,死循环从 ZMQ 拉结果 (tokenizer_manager.py:2173):
asyncdefhandle_loop(self):whileTrue:recv_obj=awaitasync_sock_recv(self.recv_from_detokenizer)# 阻塞收awaitself._handle_batch_output(recv_obj)# 分发_handle_batch_output遍历这批结果中的每个 rid,找到ReqState,塞结果并唤醒:
fori,ridinenumerate(recv_obj.rids):state=self.rid_to_state[rid]state.finished=recv_obj.finished_reasons[i]isnotNonestate.out_list.append(out_dict)# ← 结果入队pending_notify[rid]=stateforsinpending_notify.values():s.event.set()# ← 唤醒 _wait_one_response 协程完整时序图
generate_request 协程 后台 handle_loop 协程 Scheduler 进程 │ │ │ A. _init_req_state │ │ 建 ReqState(event) │ │ │ │ │ B. _send_one_request │ │ ZMQ PUSH ───────────────────────────┼────────────────────────→│ 收到请求 │ │ │ prefill/decode C. await event.wait() ← 挂起 │ │ 生成token ┆ recv from ZMQ ←──────────────────│ Detokenizer发回 ┆ out_list.append(结果) │ ┆ event.set() ──┐ │ ┆ │ │ │ 被唤醒 ←────────────────────────────────────┘ │ 取 out_list, clear event │ │ yield out (流式返回) │ │ │ │ │ 继续 await event.wait()... │ │ ... 直到 finished=True 跳出 │设计要点
| 设计 | 原因 |
|---|---|
| ZMQ PUSH/PULL 而非请求-响应 | 跨进程解耦;Scheduler 可批量乱序处理,靠 rid 对应 |
| Event + out_list,而非 Future | 流式多次返回;Future 只能 resolve 一次 |
| 独立的 handle_loop 后台协程 | 一个 loop 服务所有并发请求的结果收取 |
| rid_to_state 字典 | 异步结果靠 rid 路由到正确的等待协程 |
| batch_notify_size 批量唤醒 | 减少 event.set() 和协程切换开销 |
| 共享内存传大特征 | 多模态张量不塞 ZMQ,避免序列化拷贝 |
| wait_for 超时 + is_disconnected | 检测客户端断连,及时 abort 释放 GPU |
3.4 深入:Scheduler 主循环
Scheduler 是独立进程,跑在 GPU 上,主循环是同步的 while 死循环(不是 asyncio),
每一轮做四件事:收请求 → 排调度 → 跑 forward → 处理结果。
主循环入口 —event_loop_normal
scheduler.py:1682:
defevent_loop_normal(self):whileTrue:ifself.gracefully_exit:break# ① 收请求recv_reqs=self.request_receiver.recv_requests()self.process_input_requests(recv_reqs)# ② 决定下一批跑什么plan=self.get_next_batch_to_run(running_batch=self.running_batch,last_batch=self.last_batch,)batch=plan.batch_to_run# ③ 跑 forwardifbatch:result=self.run_batch(batch)self.process_batch_result(batch,result)# ④ 处理结果、送回 detokenizerelse:self.on_idle()self.last_batch=batch进阶版:event_loop_overlap(默认开启)
scheduler.py:1716。核心思想:上一轮结果的 CPU 处理,和这一轮的 GPU forward,
重叠执行:
defevent_loop_overlap(self):self.result_queue=deque()whileTrue:recv_reqs=self.request_receiver.recv_requests()self.process_input_requests(recv_reqs)plan=self.get_next_batch_to_run(...)batch=plan.batch_to_run# 先启动这一批的 GPU forward(异步,不等 GPU)ifbatch:batch_result=self.run_batch(batch)self.result_queue.append((batch.copy(),batch_result))# 然后 CPU 处理"上一批"的结果# 这两步就重叠了:GPU 在算新的,CPU 在整理旧的ifself.last_batch:tmp_batch,tmp_result=self.result_queue.popleft()self.process_batch_result(tmp_batch,tmp_result)self.last_batch=batch这就是 SGLang “Overlap Scheduler” 的本质:用一步的延迟换 CPU/GPU 并行。
四步详解
① 收请求 —process_input_requests(scheduler.py:1838)
从 ZMQ PULL socket 拉出 TokenizerManager 发来的TokenizedGenerateReqInput,
用_request_dispatcher分发:
- 生成请求 → 加入
self.waiting_queue(等待队列) - Abort / 控制命令 → 立即处理
② 调度决策 —get_next_batch_to_run(scheduler.py:2925)
调度大脑,核心逻辑:
# 1. 处理超时/异常self._abort_on_waiting_timeout()self._abort_on_running_timeout(running_batch)# 2. 把上一批 prefill 完成的请求合并到 running_batchiflast_batchandlast_batch.forward_mode.is_extend():running_batch.merge_batch(last_batch)# 3. 尝试凑一批 prefill(新请求)prefill_plan=self.get_new_batch_prefill(running_batch)new_batch=prefill_plan.batch_to_run# 4. 决策:优先 prefill,否则 decodeifnew_batchisnotNone:ret=new_batch# 有新请求可以 prefillelse:ifnotrunning_batch.is_empty():running_batch=self.update_running_batch(running_batch)ret=running_batch# 没新请求,那就 decode 老的else:ret=None# 完全空闲Prefill 优先原则:新请求不 prefill 就不能开始生成,会拖累 TTFT(首 token 延迟)。
get_new_batch_prefill(scheduler.py:3067) 用PrefillAdder逐个尝试加入等待队列的请求:
- 前缀查找:从
tree_cache(RadixTree)里找可复用的 KV 前缀,只需为新增部分分配显存 - 显存预算:估算这批 prefill 需要多少 KV token,超预算就停
- Chunked Prefill:超长 prompt 切成 chunk 分几步 prefill,避免一次 forward 拖累 decode 延迟
update_running_batch(decode 前的准备):
- 淘汰已经
finished的请求(生成 EOS 或达到 max_tokens) - 必要时 retract:显存快满时把某些请求暂时踢回等待队列,释放 KV Cache
- 为每个存活请求分配下一个 token 的 KV slot
③ 跑 forward —run_batch(scheduler.py:3528)
defrun_batch(self,batch,pp_proxy_tensors=None):self.forward_ct+=1ifself.is_generation:ifself.enable_overlap:withself.forward_stream_ctx:batch_result=self.model_worker.forward_batch_generation(batch,...)else:batch_result=self.model_worker.forward_batch_generation(batch,...)returnbatch_resultmodel_worker.forward_batch_generation内部:
- 构造
ForwardBatch(把 CPU 元数据搬到 GPU) - 决定用 CUDA Graph 还是 eager 执行
- 调用模型
forward→ 得到 logits - 采样 → 得到下一个 token
④ 处理结果 —process_batch_result
- 把新生成的 token 追加到每个请求的
output_ids - 检查是否
finished(EOS / max_new_tokens / abort) - 通过 ZMQ PUSH 把结果发给Detokenizer 进程(不是直接发 TokenizerManager)
- 释放已完成请求的 KV Cache(或保留在 RadixTree 中供后续复用)
Scheduler 全景时序图
Scheduler 主循环(每几毫秒一轮) │ ▼ ┌───────────────────────┐ │ ① recv_requests │ ← ZMQ PULL 拿新请求 │ → waiting_queue │ └───────────┬───────────┘ ▼ ┌───────────────────────┐ │ ② get_next_batch │ ← 调度大脑 │ │ │ a) 合并上批 prefill │ │ 到 running_batch │ │ │ │ b) 尝试凑 prefill │ ─→ RadixTree 找前缀 │ PrefillAdder │ ─→ KV Cache 分配器 │ │ ─→ Chunked Prefill 切块 │ │ │ c) 有 prefill? 跑! │ │ 没有? 跑 decode │ ─→ 淘汰 finished │ update_running │ ─→ retract 老请求(必要时) └───────────┬───────────┘ ▼ ┌───────────────────────┐ │ ③ run_batch │ ← 提交 GPU forward │ ScheduleBatch │ │ ↓ │ │ ForwardBatch (GPU) │ │ ↓ │ │ model.forward │ ─→ CUDA Graph 或 eager │ ↓ │ │ sample → token IDs │ └───────────┬───────────┘ ▼ ┌───────────────────────┐ │ ④ process_batch_result│ │ • 追加 output_ids │ │ • 检查 finished │ │ • ZMQ PUSH → Detokenizer 进程 │ • 释放 KV Cache │ ─→ RadixTree 保留前缀 └───────────┬───────────┘ │ ▼ 回到 ①,下一轮关键概念对照
| 概念 | 说明 |
|---|---|
waiting_queue | 新请求排队,等待被 prefill |
running_batch | 正在 decode 的请求集合 |
last_batch | 上一轮跑的 batch(overlap 模式用来延迟处理) |
ScheduleBatch | CPU 侧的批次描述(reqs 列表 + 元数据) |
ForwardBatch | 从 ScheduleBatch 派生的 GPU 侧对象(张量、input_ids 等) |
| Prefill 优先 | 每轮先看能不能 prefill,不能才 decode,保 TTFT |
| Chunked Prefill | 超长 prompt 切块,避免拖累 decode 延迟(保 ITL) |
| Retract 机制 | 显存紧张时把 running 请求踢回 waiting,释放 KV |
| Overlap Scheduler | 一步延迟,让 CPU 处理结果和 GPU forward 并行 |
3.5 各模块职责一览
| 模块 | 职责 |
|---|---|
| Uvicorn / FastAPI | HTTP/TCP 协议层,路由匹配,中间件 |
ChatCompletionRequest | Pydantic 模型,协议级字段校验 |
OpenAIServingChat | OpenAI 协议 → 内部协议的翻译层 |
GenerateReqInput | SGLang 内部统一请求对象(协议无关) |
TokenizerManager | 请求生命周期管理,tokenize,与 Scheduler 通信 |
Scheduler | 调度大脑:批处理、KV Cache 管理、prefill/decode 决策 |
ModelRunner | 执行模型前向计算,CUDA Graph 管理 |
Detokenizer | token IDs → 文本,流式输出 |
3.6 为什么要这么多层?
- 协议兼容:OpenAI / Anthropic / Ollama 各协议靠不同
Serving*类适配到同一个GenerateReqInput - 关注点分离:HTTP 层不懂模型,模型层不懂 HTTP
- 多进程隔离:HTTP 进程和 Scheduler 进程独立,故障不互相影响
- CPU/GPU 解耦:tokenize 是 CPU 密集,不占用 GPU 时间片
- 可观测性:每层可独立打点、记录日志
四、PD 分离(Prefill-Decode Disaggregation)
代码位于python/sglang/srt/disaggregation/。
核心思想:将 Prefill(计算密集)和 Decode(访存密集)拆分到不同 GPU/节点上:
Prefill 节点 Decode 节点 ┌──────────────┐ KV Transfer ┌──────────────┐ │ 处理 prompt │ ─────────────────→│ 逐 token 生成│ │ 生成 KV Cache│ │ 使用 KV Cache│ └──────────────┘ └──────────────┘关键文件:
prefill.py— Prefill 节点逻辑decode.py— Decode 节点逻辑nixl/— 基于 NIXL 的高速 KV 传输(RDMA)mooncake/— Mooncake 存储后端mori/— 另一种传输后端kv_events.py— KV 传输事件管理decode_hicache_mixin.py— 结合 HiCache 的分层存储
优势:Prefill 节点可用计算型 GPU,Decode 节点可用访存型 GPU,各自独立扩缩容,提高整体吞吐。
五、KV Cache 优化
代码位于python/sglang/srt/mem_cache/,这是 SGLang 的一大亮点:
1. RadixTree 前缀缓存 (radix_cache.py)
- 用基数树(Radix Tree)组织所有请求的 KV Cache
- 相同前缀的请求共享 KV Cache,避免重复计算
- 典型场景:多轮对话中 system prompt 只需计算一次
2. 内存池 (memory_pool.py)
- 预分配 GPU 显存,以 page(block)为单位管理
- 支持动态分配/回收,避免显存碎片
3. HiCache (hiradix_cache.py,hicache_storage.py)
- 分层缓存:GPU → CPU → Disk
- 热数据留 GPU,冷数据 offload 到 CPU/SSD
- 实现大容量 KV Cache,突破显存限制
4. 分配策略 (allocation.py,evict_policy.py)
- LRU 等淘汰策略
- 按优先级管理 cache 生命周期
5. 统一缓存 (unified_cache/,unified_memory_pool.py)
- 统一管理不同类型模型(Attention + Mamba/线性注意力)的状态缓存
六、量化(Quantization)
代码位于python/sglang/srt/layers/quantization/,支持非常丰富的量化方案:
| 量化方法 | 文件 | 说明 |
|---|---|---|
| FP8 | fp8.py | W8A8 FP8 量化,主流方案 |
| INT8 | w8a8_int8.py | W8A8 INT8 |
| GPTQ | gptq/ | 经典权重量化 |
| AWQ | awq/ | Activation-aware 量化 |
| MXFP4 | mxfp4.py | 微缩浮点 4bit,新一代方案 |
| FP4 | fp4_utils.py,nvfp4_online.py | NVIDIA FP4 |
| BitsAndBytes | bitsandbytes.py | 4/8bit 量化 |
| KV Cache 量化 | kv_cache.py,fp4_kv_cache_quant_method.py | 单独量化 KV Cache 节省显存 |
| MoE 专用 | moe_wna16.py,mxfp4_*_moe.py | MoE 模型专用量化 |
KV Cache 量化特别值得关注:即使模型权重不量化,也可以单独将 KV Cache 量化到 FP8/FP4,大幅节省显存,允许更大的 batch size。
七、其他重要特性
- Continuous Batching:Scheduler 实现动态批处理,新请求随时插入
- CUDA Graph:Decode 阶段用 CUDA Graph 减少 kernel launch 开销
- 投机解码(
speculative/):用小模型预测多个 token,大模型验证 - Tensor Parallelism / Expert Parallelism:
distributed/下支持多种并行策略 - 多种 Attention 后端:FlashAttention、FlashInfer、MLA(DeepSeek)、TRT-LLM 等
八、总结
SGLang 的核心竞争力在于RadixTree 前缀缓存 + 高效调度 + PD 分离 + 丰富的量化支持,使其在高并发推理场景下具有很强的吞吐优势。