SGLang 是什么:和 vLLM 不一样的四个地方
谁该读这一篇? 听过 SGLang,但想用 30 分钟搞清"vLLM 已经很强了,它凭什么也跑得起来"的工程师 / 架构师。 前置阅读:
00-prerequisites.md(如果你不知道什么是 prefix cache、TTFT/TPOT,先回去补)。 耗时: 30 分钟 学完能: 1. 用一句话回答"SGLang 解决了什么 vLLM 没解决的问题"; 2. 画出 SGLang 的双层架构图(前端 DSL + 后端 Runtime),说出每一层的职责; 3. 解释 RadixAttention / Compressed FSM / Cache-aware Scheduling / Frontend DSL 各自的创新点; 4. 给三类典型场景(chat agent、RAG、结构化输出),能判断 SGLang 合不合适; 5. 知道接下来该按什么顺序读这本书。
1. 起源与定位
SGLang 是 LMSYS Org(搞 Chatbot Arena、Vicuna、S-LoRA 的同一拨人)在 2024 年初开源的。 它不是另一个 vLLM 复刻——团队从一开始就赌两件事:
第一,复杂的 LLM 程序(agent、RAG、tool-use、严格 JSON 输出)在生产业务里占比越来越高,这种程序里"同一段 KV"会被反复用到,按请求做 batching 浪费的算力大得吓人。 第二,要把这种复用吃干净,引擎需要前端 DSL 把程序的复用结构暴露出来,后端再按结构调度——光靠运行时猜不行。
vLLM、TGI、TensorRT-LLM 都把自己定位成"OpenAI 兼容 API 的高性能后端",请求间能不能复用,全靠运行时盲猜。 SGLang 在这之上多塞了一层显式的前端语言,把"这段 prompt 是常量"、"这两个分支共享前缀"这种结构信息直接告诉运行时。
值得说清楚:用 SGLang 不强制写 DSL,OpenAI 兼容 API 也照样可以。只是 DSL 写法才是它跑得最快的姿势。
2. 双层架构:Frontend Language + SGLang Runtime(SRT)
flowchart TB
subgraph FRONTEND["前端层 (sglang/python/sglang/lang/)"]
DSL["SGLang DSL<br/>(@function, gen, select, fork)"]
OAI["OpenAI 兼容 API<br/>/v1/chat/completions"]
end
subgraph RUNTIME["后端层 SRT (sglang/python/sglang/srt/)"]
TM["TokenizerManager<br/>(请求入口进程)"]
SCH["Scheduler<br/>(调度 + RadixCache)"]
WK["Model Worker<br/>(GPU 前向)"]
DT["DetokenizerManager<br/>(出口进程)"]
end
KERN["sgl-kernel<br/>(CUDA + Rust)"]
ROUTER["sgl-router<br/>(Rust 路由)"]
DSL --> TM
OAI --> TM
ROUTER --> TM
TM --> SCH
SCH --> WK
WK -.读写KV.-> SCH
WK --> DT
DT --> TM
WK --> KERN
SCH --> KERN
classDef frontend fill:#eff5ff,stroke:#2563eb,color:#1a1f29;
classDef runtime fill:#f7f8fa,stroke:#5b6573,color:#1a1f29;
classDef ext fill:#fff3e0,stroke:#b35900,color:#1a1f29;
class DSL,OAI frontend;
class TM,SCH,WK,DT runtime;
class KERN,ROUTER ext;
前端层负责把"程序里的复用结构"压成请求计划,再丢给运行时;可以用 SGLang DSL 写,也可以走 OpenAI 兼容 API(这种情况下自动退化成单请求模式,没有复用结构可以利用)。 后端 SRT(SGLang Runtime)是三进程模型——Tokenizer / Scheduler / Detokenizer——加上 Model Worker,进程间用 ZMQ 通信,每一步都过一遍 RadixCache 找前缀复用。 sgl-kernel 是 C++/CUDA + Rust 写的高性能算子库;sgl-router 是 Rust 写的请求路由器,部署在多副本前面,做 cache-aware 路由。
文件入口:
sglang/python/sglang/launch_server.py:1。 详细分进程的数据流见01-overview/02-architecture.md和03-code-walkthrough/01-server-entry.md。
3. 跑得快的四件事
下面四件事是 SGLang 真正区别于 vLLM 的地方。前两件是数据结构层面的设计,后两件一个是调度策略、一个是前端表达力。
3.1 RadixAttention:用前缀树管 KV cache
请求 A: "System: You are X.\nUser: 1+1=?"
请求 B: "System: You are X.\nUser: 2+2=?"
请求 C: "System: You are Y.\nUser: hi"
朴素 hash 表:以 16 token 为单位算 hash,存 dict。
block_hash_0 → KV slot
block_hash_1 → KV slot
...
RadixAttention:所有请求的 prefix 在一棵树里
root
│
├──"System: You are X.\nUser: " ──┬── "1+1=?" (A)
│ └── "2+2=?" (B)
└──"System: You are Y.\nUser: " ──── "hi" (C)
为什么树比 hash 表好?三件事。
匹配粒度细——树以 token 或 page 为最小单位,命中可以止于任意位置;hash 表必须整块命中,最后那几个 token 凑不齐 16 个就被算两遍。
天然支持分支——fork 出去的多个子节点共享父节点的 KV,删除靠引用计数自然回收,不用做任何 hash 计算。
策略可以做得更细——树的"边"上保留了 token 序列,LFU、LRU、引用计数等淘汰策略都好实现。
代价是实现复杂得多。源码主战场:sglang/python/sglang/srt/mem_cache/radix_cache.py:270(class RadixCache)。完整算法见 02-core-concepts/01-radix-attention.md。
3.2 Compressed FSM:约束解码的跳跃压缩
要求 LLM 严格输出 JSON Schema 时,朴素做法是每生成一个 token 都查一遍"哪些 token 合法",把不合法的 mask 掉。 对复杂 schema,每步光算 mask 就要几毫秒,TPOT 直接翻倍。
SGLang 把 FSM 做了两层压缩:
- 节点压缩:把状态机里"只有一种转移"的链式边折叠成一条 jump-ahead 路径——例如
"name": "这串字符在 FSM 里只能一个走法,运行时可以一次性多吐几个 token,跳过中间逐步采样。 - mask 缓存:对状态机的每个状态预先算好 vocab mask 张量,按 LRU 缓存。
效果:复杂 JSON Schema 输出能比 outlines / xgrammar 快 1.5-3×。
源码:sglang/python/sglang/srt/constrained/。
完整算法在 02-core-concepts/04-compressed-fsm.md。
3.3 Cache-aware Scheduling:调度时把"前缀命中长度"算进去
vLLM 的 v1 调度器主要按 FCFS 走,带优先级覆盖。 SGLang 不这么干——它把"这个请求能命中多少 token"做成调度权重,命中越多越靠前:
queue: ┌────────────────────────────────┐
│ req-X (命中 10 token) │
│ req-Y (命中 1490 token) │ ← 应该先调度它
│ req-Z (命中 0 token) │
└────────────────────────────────┘
道理很简单:命中多的请求几乎不需要 prefill,可以"白嫖"已经在显存里的 KV;这一步的 prefill 算力让出来给真正需要的请求用,命中多的那个也不至于在队列里干等。
源码:sglang/python/sglang/srt/managers/schedule_policy.py。完整算法见 02-core-concepts/03-cache-aware-scheduling.md。
3.4 Frontend DSL:把程序的复用结构直接告诉运行时
import sglang as sgl
@sgl.function
def multi_branch_qa(s, question):
s += "You are a helpful assistant.\n" # ← 这一段所有请求都共享
s += "User: " + question + "\n"
forks = s.fork(3) # ← 显式开 3 个分支,共享上面的 KV
for f in forks:
f += "Assistant: "
f += sgl.gen("answer", max_tokens=128)
s += "Final: " + sgl.gen("final", max_tokens=128, choices=[
forks[0]["answer"], forks[1]["answer"], forks[2]["answer"]
])
result = multi_branch_qa.run(question="1+1=?")
注意几个关键原语:
s += "..."是追加到 prompt,运行时知道这是常量、可以缓存。s.fork(3)是显式分支,运行时知道 3 个分支共享父节点 KV,不需要复制。sgl.gen(...)是调用 LLM 生成,可以带max_tokens、temperature、regex、json_schema等约束。sgl.select(...)是受限选择(如分类),跑一次 forward 选出一个。
这些原语带来的信息,vLLM 根本拿不到——比如"这两个请求是从同一个父节点 fork 出来的", 有了这条线索,RadixAttention 让子节点直接共享父 KV,连 hash 计算都不用做。
源码:sglang/python/sglang/lang/interpreter.py。
完整解析在 02-core-concepts/02-frontend-dsl.md。
4. 与 vLLM / TGI / TensorRT-LLM 的定位差异
| 维度 | SGLang | vLLM | TGI | TensorRT-LLM |
|---|---|---|---|---|
| 主战场 | 复杂 LLM 程序 + 高吞吐 | 通用高吞吐 OpenAI 兼容 | HuggingFace 生态 | NVIDIA 极致单机性能 |
| 前缀缓存 | Radix tree(token 级) | Hash 表(block 级) | 简单 LRU | 有限 |
| 调度 | Cache-aware | FCFS + 优先级 | FCFS | 内部静态优化 |
| 结构化输出 | 压缩 FSM | xgrammar / outlines | outlines | NVIDIA fork |
| 前端 DSL | 有 | 无 | 无 | 无 |
| 多模态 | 完整支持 | 完整支持 | 部分 | 部分 |
| 易用性 | 中(DSL 有学习成本) | 高 | 高 | 低(需编译模型) |
| 部署成熟度 | 中(router 较新) | 高 | 高 | 高 |
什么时候选 SGLang:
- 业务里有大量"同 system prompt 多请求"、"多分支推理"、"严格 JSON 输出"——比例越高 SGLang 收益越大。
- 团队愿意接受 DSL 的学习曲线(或至少愿意用受约束解码 API)。
- 单机 / 小集群、追求极致 throughput。
什么时候选 vLLM:
- 完全是单轮独立请求,请求间几乎无前缀复用。
- 团队人多、需要文档生态最完整的方案。
- 已经在 vLLM 上跑得很稳,迁移收益小于风险。
详细横向对比和决策表见 03-sglang-vs-vllm.md。
5. 一个请求在 SGLang 内部的全链路(这次带源码位置)
1. HTTP 请求落到 launch_server 启的 HTTP server
→ sglang/python/sglang/launch_server.py
2. TokenizerManager 收下,tokenize prompt
→ sglang/python/sglang/srt/managers/tokenizer_manager.py
3. 通过 ZMQ 把 GenerateReqInput 发给 Scheduler
→ sglang/python/sglang/srt/managers/io_struct.py
4. Scheduler 主循环:
a. RadixCache.match_prefix() 找最长命中前缀
→ sglang/python/sglang/srt/mem_cache/radix_cache.py:361
b. 把请求按 cache-aware policy 排序入队
→ sglang/python/sglang/srt/managers/schedule_policy.py
c. get_next_batch_to_run() 决定本步喂哪些请求
→ sglang/python/sglang/srt/managers/scheduler.py:2348
d. run_batch() 触发 GPU 前向
→ sglang/python/sglang/srt/managers/scheduler.py:2907
5. Model Worker 在 GPU 上跑 forward
→ sglang/python/sglang/srt/model_executor/model_runner.py
新算的 KV 写入 RadixCache 对应 TreeNode
→ sglang/python/sglang/srt/mem_cache/radix_cache.py:421 (insert)
6. Sampler 出 token id,回流 Scheduler
→ sglang/python/sglang/srt/managers/scheduler.py:3084 (process_batch_result)
7. Scheduler 转发 token id 给 DetokenizerManager
→ sglang/python/sglang/srt/managers/detokenizer_manager.py
8. DetokenizerManager 增量解码出字符串,回写客户端
→ 通过 SSE 流式返回
9. EOS 或 max_tokens 触发,请求标记 finished,RadixCache 对应节点 ref_count--
→ 引用计数归 0 后该节点进 LRU,随时可能被淘汰
记住第 4 步:它就是 03-code-walkthrough/03-scheduler.md 整章的主线。
6. 性能数字仅供参考
| 场景 | 相比 vLLM 的相对吞吐(粗略) |
|---|---|
| 单轮独立请求,无前缀复用 | ~1.0× ~ 1.1× |
| 5% system prompt 复用 | 1.2× ~ 1.5× |
| 多轮 chat、RAG 类长共享前缀 | 2× ~ 4× |
| Agent / 多分支推理 | 3× ~ 6× |
| 严格 JSON Schema 输出 | 1.5× ~ 3×(主要靠压缩 FSM) |
这些数字会随版本变化,且对配置非常敏感。 真正的基准跑法在
07-hands-on/02-benchmark-throughput.md, 千万别直接拿别人 PPT 的数字下结论。
7. 小结
SGLang 不是 vLLM 的 fork,它是从"程序结构 + 缓存复用"这个新角度切进来的引擎。 架构上是两层:前端 DSL 把复用结构暴露出来,后端 SRT 用 RadixAttention、压缩 FSM、cache-aware scheduling 把这些结构带来的好处吃干净。 具体的差异化点就是上面讲的四件事:前缀树管 KV、压缩 FSM 加速约束解码、调度算前缀命中、前端 DSL。 业务里前缀复用越多,SGLang 相对 vLLM 越占优;如果全是单轮独立请求,两者基本一样。
8. 自检
回答前别看上文:
- SGLang 解决了 vLLM 没解决的什么问题?
答案
把"程序结构(同 prompt 复用、多分支推理、严格输出格式)"显式告诉运行时,让运行时按结构调度并复用 KV,不必靠 hash 盲猜。vLLM 把所有请求当成相互独立的,前缀复用靠 block hash 表碰运气;SGLang 用前端 DSL 让运行时确定性地拿到复用关系。- SGLang 的"双层"具体是哪两层?分别在源码的哪个目录?
答案
前端层:[`sglang/python/sglang/lang/`](../sglang/python/sglang/lang/)——SGLang DSL 解释器(`interpreter.py`)+ API(`api.py`)+ chat template。把 Python 程序编译为执行计划发给后端。 后端层(SRT, SGLang Runtime):[`sglang/python/sglang/srt/`](../sglang/python/sglang/srt/)——三进程(managers/)+ ModelRunner(model_executor/)+ RadixCache(mem_cache/)+ 各种 backend(layers/)。 还有 [`sgl-kernel/`](../sglang/sgl-kernel/)(C++/CUDA 算子)和 [`rust/`](../sglang/rust/)(router、tokenizer 加速)两个辅助层。- RadixAttention 用 radix tree 而不是 hash 表,最大的好处是什么?代价是什么?
答案
好处:**命中粒度精确到 1 token**(vLLM hash 表必须 16 token 整块命中),**天然支持分支**(fork 出的子节点共享父 KV,引用计数自动回收)。 代价:实现复杂得多——`match_prefix` 要做就地分裂([`radix_cache.py:361`](../sglang/python/sglang/srt/mem_cache/radix_cache.py)),`evict` 要处理"内部节点不能直接 evict、子全空才能"([`radix_cache.py:561`](../sglang/python/sglang/srt/mem_cache/radix_cache.py));多 TP 时 tree 还要跟着 broadcast 保持一致。- Cache-aware scheduling 在什么场景下相对 FCFS 收益最大?病态情况是什么?
答案
收益最大:**前缀复用率高 + 命中长度差异大**——比如多 system prompt 混跑,命中长的请求 prefill 几乎是免费的,先让它跑性价比最高。 病态:**长尾队首**(一个超长命中请求一直占算力,新短请求 starve),SGLang 用 `_abort_on_waiting_timeout` 兜底;**新业务冷启动**(前 N 个请求命中都是 0,反而被 cache-aware 排到后面);**短 prompt 全均命中**(cache-aware 退化为 FCFS,无收益但也无害)。- 我做一个"接 OpenAI API 的单轮翻译服务",SGLang 比 vLLM 快多少?为什么?
答案
两者基本打平,差距大概在 ±5% 以内。单轮翻译里每个请求 prompt 独立、没有共享前缀、没有多分支、不需要严格 JSON——SGLang 上面那四件事全用不上。 底层 attention kernel(都是 FlashInfer)、CUDA Graph、量化基本一致,性能拼的是细节。 这种场景下选 SGLang 的理由只剩"团队熟"和"部署成熟",而这两点目前 vLLM 都更强。- 我做一个"30 个分类标签的高 QPS 分类服务",应该用 SGLang 哪个原语?
答案
`sgl.select(name, choices=[label_1, label_2, ..., label_30])`。 原理:一次 prefill 拿到 prompt 末尾的 logits,然后对每个 choice tokenize 后算 P(choice | prompt),挑最大的——只需 1 次完整 prefill + 一次"短 forward 扫 choices"。 对比朴素 `gen` 自由生成再后处理:要 generate 几十个 token 直到模型说出标签名,TPOT × 几十;而且模型可能编出非法标签。`select` 在协议层杜绝错误。 实测 `select` 比 `gen+parse` 快 5-10×、错误率 0。9. 下一步
- 走主线:
02-architecture.md— 把三进程的数据流彻底搞清。 - 横向对比:
03-sglang-vs-vllm.md— 选型时直接抄表。 - 直接进核心算法:
02-core-concepts/01-radix-attention.md。 - 动手跑起来:
07-hands-on/01-setup.md。 - 对照 vLLM:先看
vllm-learning/01-overview/01-what-is-vllm.md再回来读 §4 那张对比表,差异会更立体。
上游源码起点:
sglang/python/sglang/launch_server.py、sglang/python/sglang/srt/managers/。