推理系统 · LLM Serving

SGLang 深度解析:高效 LLM 推理引擎

从 RadixAttention 到 Continuous Batching,彻底搞懂 SGLang 为什么比 vLLM 更快,以及如何正确部署。

来源
UC Berkeley / LMSYS
核心论文
Zheng et al., 2024
核心优化
RadixAttention
定位
生产级推理框架
🚀
§0 SGLang 是什么

SGLang 是由 UC Berkeley LMSYS 团队(就是开发 Vicuna、FastChat 的那个组)于 2024 年推出的高性能 LLM 推理框架。名字来自 "Structured Generation Language"。

它不是从零搭建的推理系统,而是在 Python + Triton GPU kernel 基础上,做了两件核心事情:

① 系统层:RadixAttention

自动 KV Cache 前缀复用。同一个 system prompt 被多个请求复用时,KV Cache 只算一次,自动共享。不需要用户感知。

② 语言层:SGLang 原语

提供 Python DSL,把多轮对话、并行采样、约束生成(JSON/Regex)封装成声明式接口,运行时自动调度并行执行。

LLM 推理框架生态定位 用户应用层:Agent、RAG Pipeline、Chatbot、代码生成工具 SGLang 语言层:gen() / select() / fork() / 约束生成(JSON / Regex) SGLang Runtime:RadixAttention + Continuous Batching + Scheduler (核心系统层,本笔记重点) GPU Kernel:Triton / FlashAttention / cuBLAS 模型权重:HuggingFace / SafeTensors
图 0-1:SGLang 在 LLM 推理栈中的位置。它同时提供系统层优化(RadixAttention)和语言层 API,用户应用直接对接 SGLang。

与主流框架对比

框架核心机制KV 前缀复用编程模型适合场景
vLLMPagedAttention❌ 无自动复用OpenAI 兼容 API通用 serving,最广泛
SGLangRadixAttention✅ 自动前缀共享Python DSL + REST有公共前缀的场景(RAG/Agent)
TensorRT-LLM静态编译优化✅(手动)C++ / PythonNVIDIA 硬件极致优化
LMDeployPagedAttention + 量化Python API轻量部署,国产模型
Ollamallama.cpp简单 REST本地单机推理
一句话记住 SGLang: 当你的推理请求有大量共同前缀(System Prompt / RAG 检索文档 / Few-shot 示例),SGLang 的 RadixAttention 能把这部分的 KV Cache 自动共享,速度比 vLLM 快 2~6×。
💾
§1 KV Cache 基础:为什么缓存这么重要

要理解 SGLang 的核心优化,必须先搞清楚 LLM 推理时 KV Cache 是什么、占多大内存、为什么贵。

1.1 Attention 计算与 KV Cache

Transformer 每个 Attention 层在计算第 $t$ 个 token 时,需要访问前面所有 token 的 Key 和 Value 向量。为了避免重复计算,推理时会把它们缓存起来——这就是 KV Cache:

KV Cache 内存结构(单请求,32 层 Transformer) Token Key Cache(所有层) Value Cache(所有层) 说明 token 1(prompt) K[1] × 32 层 × 头数 V[1] × 32 层 × 头数 已计算,缓存 token 2~N(prompt) K[2~N] × 32 层 V[2~N] × 32 层 Prefill 阶段一次算完 ↑ Prompt(预填充) ↓ Generation(逐 token 生成) token N+1(生成) K[N+1] 追加 1 行 V[N+1] 追加 1 行 每步追加一行 token N+2, N+3 … 不断追加… 不断追加… KV Cache 内存计算(LLaMA-7B 为例) 32 层 × 32 头 × 128 dim × 2(K+V) × 2 字节(FP16) = 每 token 512KB batch=32,seq=2048:32 × 2048 × 512KB = 32 GB — 轻松占满一张 A100
图 1-1:KV Cache 内存结构。Prompt 部分 Prefill 阶段一次算完并缓存;Generation 阶段每步追加一行 K/V,随序列增长线性增大。

1.2 前缀重用问题:传统 PagedAttention 的盲点

vLLM 的 PagedAttention 把 KV Cache 分成固定大小的 Page(block),解决了显存碎片问题。但它有一个盲点:多个请求共用同一前缀时,KV Cache 仍然各自计算、各自存储,形成大量冗余

100 个并发请求,相同 System Prompt(1000 tokens) ❌ vLLM PagedAttention:每个请求独立存储 请求 1 — System Prompt KV(500MB) 用户问题 请求 2 — System Prompt KV(重复!) 用户问题 请求 3 — System Prompt KV(再次重复) 用户问题 …… 共 100 个请求,System Prompt 存了 100 份 显存浪费: 1000 tokens × 512KB = 500MB / 请求 100 请求 × 500MB = 50 GB 显存 且每次都要重新 Prefill(浪费算力) ✅ SGLang RadixAttention:共同前缀只存一份 System Prompt KV 只存 1 份 500 MB 请求 1 独有 KV 请求 2 独有 KV 请求 3 独有 KV …… 100 个 节省: System Prompt KV 只需 500MB(1 份) 节省 49.5 GB 显存,Prefill 只算一次
图 1-2:vLLM vs SGLang 前缀处理。100 请求相同 System Prompt,vLLM 存 100 份(50GB),SGLang 只存 1 份(500MB)。
什么时候有公共前缀? 几乎所有实际业务都有:
  • RAG:检索到的相同文档片段 + 格式 prompt
  • Agent:固定的系统角色定义和工具描述
  • Few-shot:每个请求开头的固定示例
  • 多轮对话:前几轮的历史对话
  • 代码补全:相同的文件上下文
🌳
§2 RadixAttention:KV Cache 的前缀树

RadixAttention 是 SGLang 的核心创新。它把所有请求的 KV Cache 组织成一棵 Radix Tree(压缩前缀树),自动发现并复用公共前缀,完全对用户透明。

2.1 Radix Tree 数据结构

RadixAttention KV Cache 树结构示例 Root(空节点) System Prompt A "你是一个专业助手…" KV tokens: 500,引用计数: 3,已缓存 System Prompt B "You are a code expert…" KV tokens: 300,引用计数: 2,已缓存 "帮我写个报告" KV tokens: 20,引用: 1 "分析这篇文章" KV tokens: 25,引用: 2 "写个快排算法" KV tokens: 18,引用: 1 "解释这段代码" KV tokens: 30,引用: 1 生成 token 追加… 共享前缀节点(引用计数>0 不淘汰) 请求独有节点 生成中追加节点 引用计数=0 时可按 LRU 淘汰
图 2-1:RadixAttention KV Cache 树。绿色节点为多请求共享的前缀;蓝色节点为各请求独有部分;生成的 token 继续追加到叶节点。
关键: 每个节点存一段 token 序列的 KV Cache 及引用计数。新请求到来时,系统沿树根做最长前缀匹配,命中直接复用;不命中的部分才重新计算并插入树。整个过程用户无感知。

2.2 淘汰策略:引用计数 + LRU

KV Cache 节点淘汰策略 保留(不淘汰) ✓ 引用计数 > 0(有请求正在使用) ✓ 最近刚被访问(LRU 队列靠前) ✓ 是活跃叶节点的祖先(父节点受保护) ✓ 显存充足 ✓ Root 节点(永不淘汰) 淘汰(可回收) ✗ 引用计数 = 0(无活跃请求) ✗ LRU 队列末尾(最久未访问) ✗ 显存压力大时优先淘汰叶节点 ✗ 短节点优先(tokens 少,收益低) ✗ 从叶往根方向淘汰(保留上层共享前缀)
图 2-2:KV Cache 淘汰策略。引用计数 = 0 且最久未访问的叶节点优先被淘汰;System Prompt 等共享父节点受子节点保护。

2.3 性能收益

不同场景下 RadixAttention 的吞吐量提升(论文实测) 场景 KV Cache 命中率 吞吐量提升 多轮对话(Multi-turn Chat) ~80% 3.8× RAG(检索文档前缀) ~90% 5.2× Agent(固定 System Prompt) ~70% 2.9× 无公共前缀(独立单问) ~5%(几乎无复用) ≈ 1.0×(与 vLLM 持平)
图 2-3:RadixAttention 在不同场景的收益。公共前缀越多,命中率越高,加速越显著。无公共前缀时与 vLLM 基本持平。
💡 举例:构建 RAG 问答服务
  • 每次请求都包含:System Prompt(200 tokens)+ 检索到的文档块(1000 tokens)+ 用户问题(50 tokens)
  • 如果 1000 个用户问的是同一篇文档,前 1200 tokens KV Cache 只算一次
  • 第 2 个请求开始,TTFT(首 token 延迟)从 ~500ms 降到 ~20ms(跳过 Prefill)
  • 整体吞吐量提升 5~6×
§3 调度与 Batching:如何让 GPU 永不空闲

KV Cache 管理只是一方面。要榨干 GPU 的吞吐量,还需要精巧的调度策略。SGLang 在这里借鉴并改进了 Continuous Batching,并引入了 Chunked Prefill。

3.1 Continuous Batching:请求粒度的动态调度

传统推理框架(TGI 早期版本等)使用静态 Batching:等一批请求都到齐了再一起推理,某个请求先生成完也要等其他人。这会造成严重的 GPU 空转。

Static Batching vs Continuous Batching(时间轴对比) ❌ Static Batching(按最长请求等待) Req A(短) 生成 10 tokens 等待(空转) Req B(中) 生成 20 tokens Req C(长) 生成 40 tokens(决定整批等待时间) 批次结束 新请求只能在 下一批开始 GPU 利用率低:短请求完成后 GPU 在空等长请求 ✅ Continuous Batching(token 粒度动态插入) GPU 时间槽 A B C D* B D E* C D E * 号 = 新请求在前一个请求完成后立即插入 batch,无需等待整批结束 GPU 利用率:几乎 100% 每个 step 都把 batch 中空出来的位置立刻用新请求填满;新请求 P99 延迟大幅降低
图 3-1:Static Batching vs Continuous Batching。Continuous Batching 在 token 粒度动态调度,GPU 时间槽永不空闲。

3.2 Chunked Prefill:平衡 Prefill 与 Decode 的延迟

Continuous Batching 遇到一个新问题:Prefill 阶段(算整个 prompt)计算量大,会抢占 Decode 请求的 GPU 时间,导致在线请求卡顿(TTFT 增大)

Chunked Prefill:把长 Prompt 切片,与 Decode 混合调度 ❌ 不分 Chunk:Prefill 阻塞 Decode Prefill 2048 tokens(耗时 200ms) D1 D2 其他请求的 Decode 必须等 Prefill 结束才能继续 → TPOT 增大,用户感觉卡顿 ✅ Chunked Prefill(chunk_size=512):分片与 Decode 交替 Pfill[0:512] D1 D2 Pfill[512:1024] D1 D2 Pfill[1024:1536] D1 Pfill[1536:2048] → 完成 每个 chunk 后立即穿插 Decode step,在线请求 TPOT 稳定,用户无卡顿感 SGLang 配置:--chunked-prefill-size 512 chunk_size 越小 → Decode 延迟越稳定,但 Prefill 总耗时略长;通常设 512~1024
图 3-2:Chunked Prefill 把长 prompt 切成 512-token 的片段,与 Decode 请求交替执行,避免 Prefill 抢占导致的卡顿。

3.3 SGLang 调度器设计

Prefill-first 策略

新请求优先 Prefill(否则 TTFT 无限延长),Prefill 完成后加入 Decode batch。Chunked Prefill 控制单步 Prefill 计算量不超过预算。

内存感知调度

调度器实时追踪 KV Cache 占用。如显存不足,会先触发 RadixAttention 淘汰,再必要时 preempt(换出)低优先级请求(swap 到 CPU 或直接 recompute)。

TTFT

Time To First Token
首 token 延迟。受 Prefill 速度影响,RadixAttention 命中时极低(毫秒级)

TPOT

Time Per Output Token
每个生成 token 的延迟。受 batch size 和 KV Cache 访问速度影响

Throughput

output tokens / second
系统整体吞吐量。Continuous Batching + RadixAttention 双重加速

🧩
§4 编程接口:Runtime API 与 SGLang 语言

SGLang 提供两套接口:底层 HTTP/Python Runtime API(兼容 OpenAI,用于接入现有工具链),以及SGLang 语言原语(高级 DSL,用于复杂多步推理编程)。

4.1 Runtime API:OpenAI 兼容接口

启动服务器后,SGLang 自动提供 OpenAI 兼容的 HTTP 端点,直接替换 openai.baseUrl 即可:

# 启动 SGLang 服务
python -m sglang.launch_server \
    --model-path meta-llama/Llama-3-8B-Instruct \
    --host 0.0.0.0 --port 30000 \
    --tp 1                        # tensor_parallel_size

# 使用 OpenAI 客户端
from openai import OpenAI
client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")

response = client.chat.completions.create(
    model="meta-llama/Llama-3-8B-Instruct",
    messages=[{"role": "user", "content": "你好"}],
    max_tokens=100,
)

也支持原生 Python Runtime API,可以更细粒度地控制缓存行为:

import sglang as sgl

# 初始化 Runtime
runtime = sgl.Runtime(model_path="meta-llama/Llama-3-8B-Instruct")
sgl.set_default_backend(runtime)

# 简单生成
@sgl.function
def simple_qa(s, question):
    s += sgl.system("你是一个有用的助手")
    s += sgl.user(question)
    s += sgl.assistant(sgl.gen("answer", max_new_tokens=200))

# 单个调用
state = simple_qa.run(question="什么是深度学习?")
print(state["answer"])

runtime.shutdown()

4.2 SGLang 原语:高级并行控制

SGLang 最强大的地方在于其声明式编程原语,可以轻松实现并行采样、多步推理、Fork-Join 等复杂模式:

SGLang 核心原语:fork() 实现并行采样 共享前缀 System Prompt + 问题 = KV Cache 复用 fork(3) — 并行生成 3 个答案 Branch 0 gen("answer_0", temp=0.8) 独立 KV Cache(从 fork 点起) Branch 1 gen("answer_1", temp=0.8) 共享 prefix KV,不重复计算 Branch 2 gen("answer_2", temp=0.8) 共享 prefix KV,不重复计算 join — 汇聚 3 个结果 Best-of-N 采样 / 投票 / 结果对比
图 4-1:SGLang fork-join 模式。共享前缀只计算一次,fork 后 3 个分支并行生成,各自维护独立 KV Cache,最后 join 汇聚结果。
import sglang as sgl

@sgl.function
def best_of_n_qa(s, question, n=3):
    s += sgl.system("你是一个专业的问答助手,请尽量准确回答。")
    s += sgl.user(question)
    # fork: 并行生成 n 个候选答案(共享上方 KV Cache)
    forks = s.fork(n)
    for f in forks:
        f += sgl.assistant(sgl.gen("answer", max_new_tokens=200, temperature=0.8))
    # join: 汇聚所有结果
    s.join(forks)
    # 让模型从 n 个答案中选最好的
    s += sgl.user("以上" + str(n) + "个答案,哪个最准确?请指出序号。")
    s += sgl.assistant(sgl.gen("best", max_new_tokens=50, temperature=0.0))

state = best_of_n_qa.run(question="深度学习和机器学习的区别是什么?")
# 访问结果
for i in range(3):
    print(f"答案 {i}:", state[f"answer_{i}"])
print("最优答案:", state["best"])

4.3 约束生成(Structured Output)

SGLang 内置约束解码:强制模型输出严格符合 JSON Schema 或正则表达式的文本,无需后处理。

import sglang as sgl

@sgl.function
def extract_info(s, article):
    s += sgl.user(f"从以下文章中提取信息:{article}")
    s += sgl.assistant(
        sgl.gen(
            "json_output",
            # 强制输出符合 JSON Schema
            json_schema={
                "type": "object",
                "properties": {
                    "title":    {"type": "string"},
                    "author":   {"type": "string"},
                    "keywords": {"type": "array", "items": {"type": "string"}},
                    "summary":  {"type": "string", "maxLength": 200}
                },
                "required": ["title", "author", "keywords", "summary"]
            }
        )
    )

# 或使用 regex 约束(如提取日期)
@sgl.function
def extract_date(s, text):
    s += sgl.user(f"提取文本中的日期:{text}")
    s += sgl.assistant(
        sgl.gen("date", regex=r"\d{4}-\d{2}-\d{2}")  # 强制 YYYY-MM-DD 格式
    )
约束解码原理: SGLang 在每步生成时对 logits 进行 mask——不符合约束的 token 概率置为 -∞,确保每步输出都在合法集合内。基于 outlines 库实现,支持 JSON Schema / Regex / CFG 多种约束形式。
🛠️
§5 部署实践:多 GPU、量化与投机解码

5.1 多 GPU 部署(Tensor Parallel)

SGLang 通过 --tp 参数一键开启 Tensor Parallel,自动把模型权重切分到多张卡:

# 单机多卡(8 卡 A100,TP=8)
python -m sglang.launch_server \
    --model-path meta-llama/Llama-3-70B-Instruct \
    --tp 8 \
    --port 30000

# 多机部署(2 节点,每节点 8 卡,TP=16)
# 节点 0(主节点)
python -m sglang.launch_server \
    --model-path meta-llama/Llama-3-70B-Instruct \
    --tp 16 \
    --dist-url "tcp://主节点IP:29500" \
    --nnodes 2 --node-rank 0

# 节点 1
python -m sglang.launch_server \
    --model-path meta-llama/Llama-3-70B-Instruct \
    --tp 16 \
    --dist-url "tcp://主节点IP:29500" \
    --nnodes 2 --node-rank 1
多 GPU 部署方案选型(按模型大小) 模型规模 推荐方案 显存需求 典型配置 7B / 8B 单卡 TP=1 14 GB (BF16) A100-40G × 1 30B / 34B TP=2(节点内) 60 GB (BF16) A100-80G × 2 70B TP=4 或 TP=8 140 GB (BF16) A100-80G × 2/4 405B / 671B (MoE) TP=8 / 多机 TP 800 GB+ (BF16) H100-80G × 8+
图 5-1:多 GPU 部署方案按模型大小选型。SGLang 通过 --tp 参数一键配置 Tensor Parallel。

5.2 量化(INT4 / FP8)

量化在保持推理质量的同时大幅减少显存占用和提升吞吐量。SGLang 支持多种量化格式:

# AWQ 量化(INT4,推荐)
python -m sglang.launch_server \
    --model-path casperhansen/llama-3-8b-instruct-awq \
    --quantization awq \
    --port 30000

# GPTQ 量化
python -m sglang.launch_server \
    --model-path TheBloke/Llama-2-7B-Chat-GPTQ \
    --quantization gptq

# FP8 量化(H100 原生支持,精度最高)
python -m sglang.launch_server \
    --model-path meta-llama/Llama-3-8B-Instruct \
    --quantization fp8  # 需要 H100 / 4090
量化格式显存占用精度损失推理速度推荐场景
BF16(无量化)2 字节/参数baselinebaseline质量优先
FP81 字节/参数极小1.5~2×H100 生产环境首选
AWQ (INT4)0.5 字节/参数2~3×消费级 GPU / 资源受限
GPTQ (INT4)0.5 字节/参数小~中1.5~2×与 AWQ 类似,模型生态更丰富

5.3 投机解码(Speculative Decoding)

投机解码用小模型(draft model)快速生成多个候选 token,再用大模型(target model)一次性并行 verify,大幅降低 TTFT 和 TPOT:

投机解码原理(Speculative Decoding) ① Draft 阶段 小模型(如 68M Vicuna) 快速串行生成 K=4 个候选 token K=4 时只需 4 次 small-model forward ② Verify 阶段 大模型(如 Llama-3-70B) 并行验证 K 个 token(1 次 forward) 1 次 large-model forward ≈ 验证 K 个 token ③ Accept/Reject 接受符合大模型分布的 token 拒绝不符合的,从该处重采样 平均接受 2~3 个 / 轮(取决于 draft 质量) 效果:等价于大模型独立生成(不改变分布),但速度快 2~4× 原理:接受率 α 决定实际加速。当 draft 模型与 target 模型输出分布越接近,接受率越高,加速越大。 代价:需要额外显存放 draft 模型。Draft model 通常为 target 的 1/10 体积(如 70B+7B 组合)。
图 5-2:投机解码原理。小模型快速草稿(Draft),大模型并行验证(Verify),Accept/Reject 保证与纯大模型等价的输出分布。
# 启动投机解码(使用 draft model)
python -m sglang.launch_server \
    --model-path meta-llama/Llama-3-70B-Instruct \
    --speculative-draft-model-path meta-llama/Llama-3-8B-Instruct \
    --speculative-num-draft-tokens 4 \
    --tp 4 --port 30000

# Eagle-2 投机解码(SGLang 内置,更高接受率)
python -m sglang.launch_server \
    --model-path meta-llama/Llama-3-70B-Instruct \
    --speculative-algorithm EAGLE2 \
    --tp 4 --port 30000
📊
§6 性能对比 & 选型指南

SGLang vs vLLM 性能基准(官方 Benchmark)

SGLang vs vLLM 吞吐量对比(Llama-3-8B,A100-80G) 测试场景 吞吐量(output tokens/s) SGLang 提升 ShareGPT(多轮对话,有公共前缀) vLLM 2840 t/s 4280 t/s (SGLang) +51% RAG(固定文档前缀,高命中率) 1580 t/s (vLLM) 4850 t/s (SGLang) +207% Agent(System Prompt 共享,中等命中率) 1920 t/s (vLLM) 2810 t/s (SGLang) +46% 无公共前缀(独立单问) 2620 t/s (vLLM) 2710 t/s (SGLang) ≈ 持平 测试条件:Llama-3-8B,A100-80G,batch=100,SGLang v0.3,vLLM v0.5(2024年7月数据) 实际差距因模型、硬件、负载不同而有所变化。建议在自己的场景下 benchmark。
图 6-1:SGLang vs vLLM 吞吐量对比。前缀复用越多,SGLang 优势越大;无公共前缀场景基本持平。

选型决策表

场景推荐框架理由
RAG / 检索增强系统SGLang ✅文档前缀高度重复,RadixAttention 效果最佳
多轮对话 ChatbotSGLang ✅历史对话作为前缀可复用,且支持 fork 并行采样
Agent / 工具调用SGLang ✅固定 system prompt 共享 + 约束生成 JSON 输出
结构化数据提取SGLang ✅内置 JSON Schema / Regex 约束解码
通用 OpenAI 兼容 APIvLLM 或 SGLang两者都支持,SGLang 在有前缀时更优
独立单次推理(无前缀)vLLM / TRT-LLM无复用场景下 SGLang 无优势;TRT-LLM 编译优化更好
NVIDIA 硬件极致优化TensorRT-LLM静态图编译,A100/H100 性能天花板
本地单机轻量部署Ollama / LMDeploy部署简单,无需 Python 环境

常见问题 FAQ

  • Q:SGLang 和 vLLM 能混用吗?
    A:不能直接混用,但都兼容 OpenAI API 格式,可以通过负载均衡在两者之间路由。
  • Q:RadixAttention 会影响输出质量吗?
    A:不会。它只是复用已计算的 KV Cache,与模型参数无关,输出分布完全等价。
  • Q:SGLang 支持 LoRA 吗?
    A:支持,--lora-paths 参数可加载多个 LoRA adapter,运行时动态切换。
  • Q:显存不够时 SGLang 会怎么处理?
    A:RadixAttention 淘汰旧缓存 → preempt 低优先级请求(recompute 策略)→ 拒绝新请求(可配置)。
  • Q:SGLang 的 fork() 安全吗?会影响其他请求?
    A:安全。fork 后各分支的 KV Cache 是 Copy-on-Write 的,共享前缀只读,独有部分独立存储。

一句话总结

SGLang 的核心洞察是:LLM 推理中大量请求共享相同的前缀,这些重复计算是系统层面的浪费。RadixAttention 用一棵前缀树把这个浪费彻底消灭——共享的前缀只算一次,只存一份。有了这个基础,再叠加 Continuous Batching、Chunked Prefill 和高级编程原语,才有了 SGLang「有公共前缀场景比 vLLM 快 2~5×」的竞争力。

选型原则只有一句话:如果你的推理工作有大量公共前缀(RAG / Agent / 多轮),选 SGLang;如果基本没有公共前缀,vLLM 足够了