预计阅读 11 分钟

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.md03-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:270class RadixCache)。完整算法见 02-core-concepts/01-radix-attention.md

3.2 Compressed FSM:约束解码的跳跃压缩

要求 LLM 严格输出 JSON Schema 时,朴素做法是每生成一个 token 都查一遍"哪些 token 合法",把不合法的 mask 掉。 对复杂 schema,每步光算 mask 就要几毫秒,TPOT 直接翻倍。

SGLang 把 FSM 做了两层压缩:

  1. 节点压缩:把状态机里"只有一种转移"的链式边折叠成一条 jump-ahead 路径——例如 "name": " 这串字符在 FSM 里只能一个走法,运行时可以一次性多吐几个 token,跳过中间逐步采样。
  2. 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_tokenstemperatureregexjson_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. 自检

回答前别看上文:

  1. SGLang 解决了 vLLM 没解决的什么问题?
答案 把"程序结构(同 prompt 复用、多分支推理、严格输出格式)"显式告诉运行时,让运行时按结构调度并复用 KV,不必靠 hash 盲猜。vLLM 把所有请求当成相互独立的,前缀复用靠 block hash 表碰运气;SGLang 用前端 DSL 让运行时确定性地拿到复用关系。
  1. 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 加速)两个辅助层。
  1. 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 保持一致。
  1. Cache-aware scheduling 在什么场景下相对 FCFS 收益最大?病态情况是什么?
答案 收益最大:**前缀复用率高 + 命中长度差异大**——比如多 system prompt 混跑,命中长的请求 prefill 几乎是免费的,先让它跑性价比最高。 病态:**长尾队首**(一个超长命中请求一直占算力,新短请求 starve),SGLang 用 `_abort_on_waiting_timeout` 兜底;**新业务冷启动**(前 N 个请求命中都是 0,反而被 cache-aware 排到后面);**短 prompt 全均命中**(cache-aware 退化为 FCFS,无收益但也无害)。
  1. 我做一个"接 OpenAI API 的单轮翻译服务",SGLang 比 vLLM 快多少?为什么?
答案 两者基本打平,差距大概在 ±5% 以内。单轮翻译里每个请求 prompt 独立、没有共享前缀、没有多分支、不需要严格 JSON——SGLang 上面那四件事全用不上。 底层 attention kernel(都是 FlashInfer)、CUDA Graph、量化基本一致,性能拼的是细节。 这种场景下选 SGLang 的理由只剩"团队熟"和"部署成熟",而这两点目前 vLLM 都更强。
  1. 我做一个"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. 下一步

上游源码起点:sglang/python/sglang/launch_server.pysglang/python/sglang/srt/managers/