跳到主要内容

数据截至 (上游 commit f775db03aaa8)

SGLang — 架构与原理

30 秒导读: SGLang 是一个高性能 LLM(及多模态模型)推理服务端。你对它发 OpenAI 兼容的请求,它把请求拆给「分词进程 → 调度进程 → 反分词进程」的流水线,在调度进程里用连续批处理喂 GPU。它成名的技术是 RadixAttention:用一棵基数树在不同请求之间共享 KV cache 的前缀——agent 多轮对话、共享 system prompt 的场景下,重复的前缀只算一次。


1. 这是什么(零基础也能懂)

一句话定义

SGLang 是一个把开源大模型包装成在线 API 服务的推理引擎:你给它一个 Hugging Face 模型,它起一个 OpenAI 兼容的 HTTP 服务,把成百上千并发请求的文本生成调度到一批 GPU 上高效执行。

它要解决谁的什么问题

设想你要把一个 70B 模型上线给全公司用:

  • 请求是一个接一个零散到达的,长短不一——不能攒够一批再算,得边来边算(连续批处理)。
  • 很多请求共享相同开头(同一个 system prompt、同一个 few-shot 模板)——每个请求都从头算一遍 prefill 是纯浪费。
  • 业务要求模型输出严格合法的 JSON——靠 prompt 求模型「请输出 JSON」不可靠,得在采样层面强制。
  • GPU 显存装不下所有并发请求的 KV cache 时,得有体面的降级策略而不是直接崩。

SGLang 就是系统性地回答这四个问题的那一层。这也是它和兄弟项目 vLLM 共同的战场,两者是当今开源 LLM serving 的两大主力。

它能做什么

能力具体支持
核心调度RadixAttention 前缀缓存、连续批处理、chunked prefill、CPU/GPU overlap 调度
结构化输出JSON schema / regex / EBNF / structural tag,在采样器强制合法(xgrammar / outlines / llguidance 后端)
并行TP / PP / DP / 专家并行(EP)、DP attention、prefill-decode 分离(PD 分离)
模型Llama / Qwen / DeepSeek / Kimi / GLM / Mistral 等主流架构,embedding 模型,多模态 VLM,diffusion LLM
硬件NVIDIA / AMD GPU、TPU、Intel CPU、Ascend NPU
周边LoRA 多适配器批处理、投机采样(EAGLE 等)、RL 训练 rollout 后端(verl、slime、Miles 都用它)

(功能清单见 README.md 的 "About" 一节;各机制的实现位置见本文第 8 节代码地图。)

用起来什么样

最常用法是一条命令起服务,然后用 OpenAI SDK 调用:

# 起服务(摘自 README 的用法形态)
python -m sglang.launch_server --model-path meta-llama/Llama-3.1-8B-Instruct
# 客户端:标准 OpenAI 调用,另可加 SGLang 扩展字段
import openai # 示意用法
client = openai.Client(base_url="http://127.0.0.1:30000/v1", api_key="EMPTY")
resp = client.chat.completions.create(
model="meta-llama/Llama-3.1-8B-Instruct",
messages=[{"role": "user", "content": "用 JSON 给出三个中国城市"}],
# SGLang 扩展:采样器级强制合法 JSON
extra_body={"response_format": {"type": "json_schema", "json_schema": {...}}},
)

服务端入口是 python/sglang/srt/entrypoints/http_server.py:2787launch_server;OpenAI 兼容层在 python/sglang/srt/entrypoints/openai/serving_chat.py:244(OpenAIServingChat)。

一句话直觉

把 SGLang 想成一家「中央厨房」: 前台(HTTP 进程)只接单出餐;切配间(TokenizerManager)把订单变成标准料单;炉灶(Scheduler + GPU 子进程)是全村唯一贵的东西,一口大锅连续翻炒、谁的菜熟了就先出锅;传菜口(DetokenizerManager)把锅里的 token 翻回人话。而 RadixAttention 是厨房的「高汤桶」——大家都用同一锅老汤打底,不重新熬。

一个容易混淆的点:一个 repo 里有两样东西

这个仓库同时装着两套独立的东西(aiRef/sources/sglang.md 卡片也专门提醒了这条):

  • srt(SGLang Runtime):python/sglang/srt/,推理服务端,本文的全部内容。
  • 前端 DSL:python/sglang/lang/(ir.pyinterpreter.py 等),一套「编程式 prompt」语言,可以独立使用、也能打到任何后端。

两者常被混为一谈。本 teardown 只讲 srt


2. 顶层全景(它大概怎么转)

2.1 进程拓扑

先看「谁是谁」。一次 launch_server 会拆出多个进程,用 ZMQ 的 IPC socket 连接(python/sglang/srt/entrypoints/http_server.py:2798 的 docstring 自己画了这个分工):

客户端 ──HTTP──► [主进程] HTTP server (FastAPI)
│ 同进程调用

[主进程] TokenizerManager(分词/多模态预处理/grammar 登记)
│ ZMQ PUSH

[子进程 ×tp_size] Scheduler → TpModelWorker → GPU forward
│ ZMQ PUSH(token ids)

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

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

怎么读这张图: 请求从左进右出再回到左。注意三个进程的边界恰好是三种资源:HTTP/tokenize 吃 CPU 和网络、scheduler 进程独占 GPU、detokenize 又只吃 CPU——拆开是为了让 GPU 进程里除了 forward 什么都不干

TP > 1 时 scheduler 子进程有 tp_size 个,但只有 rank 0 直接和 tokenizer/detokenizer 收发 ZMQ,其余 rank 之间走 NCCL/gloo(python/sglang/srt/managers/scheduler_components/ipc_channels.py:26,SchedulerIpcChannels.createis_rank_zero 分支)。

2.2 部件职责

部件干什么在哪个文件
launch_server拉起全部进程的总入口python/sglang/srt/entrypoints/http_server.py:2787
TokenizerManager分词、套 chat template、多模态预处理、grammar 触发、把请求发给 scheduler、流式回客户端python/sglang/srt/managers/tokenizer_manager.py:394
Scheduler核心:维护 waiting/running 队列、组 batch、调 GPU forward、回写结果python/sglang/srt/managers/scheduler.py:398
TpModelWorkerscheduler 手里的「GPU 手」:管 ModelRunner、执行 forward + samplepython/sglang/srt/managers/tp_worker.py:312
RadixCacheKV cache 前缀树,跨请求共享前缀python/sglang/srt/mem_cache/radix_cache.py:303
SchedulePolicy / PrefillAdder准入控制:排序 waiting queue、按 token 预算挑请求python/sglang/srt/managers/schedule_policy.py:216
GrammarManager结构化输出:异步编译 grammar、推进 FSMpython/sglang/srt/constrained/grammar_manager.py:26
DetokenizerManager增量 detokenize、stop 串裁剪python/sglang/srt/managers/detokenizer_manager.py
DataParallelController多 DP rank 时的请求路由与负载均衡python/sglang/srt/managers/data_parallel_controller.py:138

2.3 主线走一遍(一条 /generate 请求的一生)

① HTTP 收到请求 → ② TokenizerManager 分词(多模态则先跑 HF processor)
│ │
│ ZMQ PUSH │(带 grammar? 先登记 GrammarManager)
▼ ▼
③ Scheduler 子进程:waiting_queue → ④ RadixCache.match_prefix(命中共享前缀,少算 prefill)


⑤ PrefillAdder 按 token 预算组 prefill batch → ⑥ TpModelWorker forward 出首 token


⑦ 进入 running_batch,逐轮 decode(连续批)——显存不够就 retract 尾部请求回 waiting


⑧ 每轮产出 token → DetokenizerManager 增量转文本 → TokenizerManager → SSE 推给客户端

这条线对应的调度主循环是 event_loop_normal / event_loop_overlap(python/sglang/srt/managers/scheduler.py:1796 / scheduler.py:1794):每轮只做三件事——收新请求、组下一批(get_next_batch_to_run,scheduler.py:3112)、跑并处理(run_batch + process_batch_result)。第 1、3 章把它拆开讲。


3. 阅读地图(建议顺序)

五章由浅入深。时间有限就读 01 → 02,这两章抓住了 SGLang 区别于其他 serving 引擎的全部要害。

顺序章节讲什么适合谁
101-server-architecture.md进程怎么拆、ZMQ 怎么连、overlap 调度所有人必读,这是全系统的骨架
202-radix-attention.mdRadixAttention 基数树的四个原语想知道前缀缓存怎么实现的人
303-scheduler-continuous-batching.md连续批调度、准入预算、retract关心吞吐/延迟与显存的人
404-structured-output.md约束解码:grammar 编译 + vocab bitmask做 JSON 模式 / 工具调用的人
505-multimodal-distributed.md多模态流水线、TP/PP/DP/EP、PD 分离上规模部署、接 VLM 的人

4. 巧妙之处(可借鉴的技术)

每条先白话点出「妙在哪」,细节和引用在各章。

  • 把「前缀共享」变成一棵可增删的树,而不是一张缓存表。 RadixAttention 的精髓:KV cache 的 key 是 token 序列,树边可分裂(_split_node,python/sglang/srt/mem_cache/radix_cache.py:704),于是「部分命中」也能精确复用;驱逐只动叶子、用 last_access_time 做 LRU(evict,:593)。见第 2 章。
  • 运行中的请求也往树里写。 不只是请求结束才缓存——chunked prefill 每跑完一块就 cache_unfinished_req 一次(python/sglang/srt/mem_cache/radix_cache.py:516),后面的请求立刻能命中前半截,共享的时效性拉满。见第 2 章。
  • CPU 调度器和 GPU forward 跑成两级流水线。 overlap 模式下,第 N 批的 GPU 计算和第 N-1 批的 CPU 后处理同时进行,用 future_map 把「还没算出来的 tensor」包装成占位符(run_batch,python/sglang/srt/managers/scheduler.py:3862)。这就是 README 里 "zero-overhead batch scheduler" 的所指。见第 1、3 章。
  • grammar 编译异步化 + 跨 rank 同步放行。 编译 regex/schema 成 FSM 是 CPU 重活,被丢进线程池(ThreadPoolExecutor,python/sglang/srt/constrained/base_grammar_backend.py:206),请求先挂进 grammar_queue 等编译完再进 waiting queue;TP 多卡时用 all_gather 对齐「大家都编好了」(get_ready_grammar_requests,grammar_manager.py:184)。见第 4 章。
  • 显存不足不是错误,是调度的一个分支。 KV 池满了就 retract:把最晚的请求逐出 running、状态写回树、塞回 waiting 队列重跑(retract_decode,python/sglang/srt/managers/schedule_batch.py:2925)。见第 3 章。
  • 调度策略本身感知缓存。 waiting queue 默认按「最长前缀匹配」(LPM)排序,让能共享前缀的请求挨着进批,提高整批命中率;队列超过 128 条时自动降级成 FCFS 控制排序开销(_determine_active_policy,schedule_policy.py:296)。见第 3 章。

5. 边界与局限

诚实清单,哪些是刻意的、哪些是当前实现的软肋。

  • 重叠调度的收益依赖「GPU forward 比 CPU 后处理慢」。 小模型 + 快卡上 CPU 侧反而可能成瓶颈;disable_overlap_schedule(python/sglang/srt/server_args.py:880)是留好的退路,默认 False(即默认开 overlap)。
  • LPM 排序在超长队列下自动放弃。 等待队列 >128 条时退回 FCFS(schedule_policy.py:296-300),此时前缀局部性收益消失——高突发下 RadixAttention 的命中率会掉。
  • radix 树不是万能缓存。 page_size > 1 时匹配按页对齐截断(match_prefixpage_aligned,radix_cache.py:377-435),尾巴上不足一页的前缀不被复用;ignore_eos 请求、LoRA 不同 adapter 之间由 extra_key/cache_salt 主动隔离不共享。
  • 结构化输出只约束「形式」,不保证「语义」。 bitmask 保证输出能被 grammar 接受,但 JSON 内容是否符合业务预期仍靠模型。
  • jump-forward(压缩 FSM 跳步解码)接口还在,热路径已不走它。 各 grammar backend 仍实现 try_jump_forward(python/sglang/srt/constrained/xgrammar_backend.py:164),但在本 commit 的 scheduler 解码路径里找不到调用方——每步 bitmask 是当前主路径(2024-02 博客宣传的 compressed FSM 在 outlines 后端,见 outlines_jump_forward.py 的文件头注释)。这是代码里读出来的现状,文档照实写。
  • fast-moving。 这个库迭代极快(README 新闻流几乎每月一条大特性),本文锚定 f775db03;上游更新后优先核对第 7 节代码地图里引用的符号。

6. 横向对比

同书架上有直接对位的兄弟。

维度SGLang(本文)vLLMllama.cpptransformers
定位高性能 serving 服务端同左,两大主力之一端侧/CPU 推理模型定义与参考实现
前缀缓存基数树(RadixAttention),树边可分裂paged block hash 表不做跨请求共享无 serving 层
调度连续批 + overlap + retract连续批 + preempt单请求/简单批
结构化输出xgrammar/outlines/llguidance 后端,采样器 bitmask同样集成 xgrammar 等grammar 采样
分布式TP/PP/DP/EP + PD 分离TP/PP/DP + PD 分离无(单设备为主)训练侧并行

一句话分工:transformers 回答「模型怎么定义」,vLLM / SGLang 回答「怎么把它服务化跑满 GPU」,llama.cpp 回答「怎么在没有 GPU 的地方跑起来」。verl 这类 RL 框架则把 SGLang 当 rollout 后端用——serving 引擎成了训练栈的下游。


7. 代码地图(入口级)

每章末尾有自己的细粒度地图,这里只列从零开始读源码的入口。

主题文件路径符号名
服务启动总入口python/sglang/srt/entrypoints/http_server.pylaunch_server
子进程拉起(含 ZMQ 端口分配)python/sglang/srt/entrypoints/engine.pyEngine._launch_subprocesses
调度主循环python/sglang/srt/managers/scheduler.pyScheduler.event_loop_normalevent_loop_overlapget_next_batch_to_run
前缀缓存python/sglang/srt/mem_cache/radix_cache.pyRadixCache.match_prefix / insert / evictTreeNode
准入与调度策略python/sglang/srt/managers/schedule_policy.pySchedulePolicy.calc_priorityPrefillAdder.add_one_req
约束解码python/sglang/srt/constrained/grammar_manager.pyGrammarManager.get_ready_grammar_requests
vocab bitmaskpython/sglang/srt/sampling/sampling_batch_info.pySamplingBatchInfo.update_regex_vocab_maskGrammarMask.apply
GPU workerpython/sglang/srt/managers/tp_worker.pyTpModelWorker.forward_batch_generation
模型前向 / 注意力后端python/sglang/srt/model_executor/model_runner.pyModelRunner_get_attention_backend
DP 路由python/sglang/srt/managers/data_parallel_controller.pyDataParallelController