编程 SGLang 深度拆解:当 LLM 推理引擎学会「结构化生成」与前缀复用——从 RadixAttention、约束解码到生产级高吞吐部署的工程全貌(2026)

2026-07-18 02:45:17 +0800 CST views 6

SGLang 深度拆解:当 LLM 推理引擎学会「结构化生成」与前缀复用——从 RadixAttention、约束解码到生产级高吞吐部署的工程全貌(2026)

过去三年,大模型领域的军备竞赛集中在「训练更大的模型」。但真正决定一个 AI 产品能不能用、贵不贵、稳不稳的,是推理侧(serving)。你花几百万训出来的模型,如果线上每秒只能吐十几个 token、GPU 利用率长期在 30% 以下、多轮对话越聊越慢,那它就是一个昂贵的 Demo,而不是产品。

为什么推理这么难?根子在于 KV Cache(键值缓存)。Transformer 自回归生成时,每生成一个 token 都要依赖前面所有 token 的 K、V 矩阵。这些中间结果必须缓存复用,否则每个请求都得从零重算——但 KV Cache 会随上下文长度线性膨胀,一个 32k 上下文的请求,光 KV 就能吃掉几 GB 显存;而多轮对话、RAG、共享系统提示词这些真实场景里,大量请求的前缀是重复的,传统做法却让每个请求各算各的。

SGLang(Structured Generation Language)就是为解决这个问题而生的。它和 vLLM、TensorRT-LLM 站在同一擂台,但走了一条更「系统」的路:不止优化单请求吞吐,而是把 「前端编程模型」和「后端运行时」协同设计,用 RadixAttention 做跨请求的前缀复用,用约束解码把结构化输出(JSON / 函数调用)的速度拉高一个数量级。

本文从工程视角,把 SGLang 的设计、架构、代码实战与性能调优讲透。

一、背景:大模型服务的「最后一公里」为什么这么难

1.1 训练是科学,推理是工程

训练是一次性的、批量的;推理是常驻的、在线的、不可预测的。你不知道下一秒来的是 10 个 2k 上下文的简单问答,还是 1 个 128k 的长文档摘要。这种负载的突发性让资源调度变得极其困难——你既不想让 GPU 在大多数时间空转,也不想在峰值来临时因为批不下而超时。

1.2 KV Cache 是吞吐的命门

Transformer 的注意力是「看全序列」的。生成第 n 个 token 时,模型需要前面 0..n-1 所有 token 的 K、V。把这些 K、V 存下来就叫 KV Cache。问题有三:

  • 显存膨胀:KV Cache 大小 ≈ 2 × 层数 × 隐藏维 × 序列长 × 批大小,和上下文长度成正比,长上下文下动辄几十 GB。
  • prefill 与 decode 两阶段异构:prefill(处理整段输入)是 Compute-Bound 的大矩阵乘,decode(逐 token 生成)是 Memory-Bound 的小矩阵乘。把两者混在一个 batch 里会互相拖累。
  • 碎片:变长序列 + 动态批处理,如果像传统那样给每个请求预分配一段连续显存,会造成大量内部碎片。

1.3 真实业务里,前缀大量重复

  • 多轮对话:每一轮都要带上完整历史;
  • RAG:同一个知识库 chunk 被几百个用户检索;
  • 系统提示词:所有请求共享同一段 system prompt;
  • 工具调用:固定的 function schema 反复出现。

传统「逐请求独立缓存」= 重复计算 = 真金白银的浪费。

1.4 vLLM 的 PagedAttention 解决了「内存碎片」,但没解决「跨请求共享」

vLLM 把 KV Cache 像操作系统虚拟内存一样分页(PagedAttention),消灭了内存碎片、支持了连续批处理(continuous batching),是里程碑式的贡献。但它每个请求的 KV 仍是逻辑隔离的——两个请求就算前缀一模一样,也无法互相复用。SGLang 的 RadixAttention 正是在这一点上补了刀:在请求之间共享前缀的 KV

二、核心概念:SGLang = 前后端协同设计的「结构化生成语言」

SGLang 不是一个单纯的「推理服务器」,它由两个部分组成:

  1. 前端(Frontend):一门嵌入 Python 的 DSL,让你用极少代码描述复杂的 LLM 程序——多轮对话、分支、并行、外部工具调用、结构化输出。
  2. 后端(Backend / SRT,SGLang Runtime):一个高性能运行时,负责调度、内存管理、多 GPU 并行、内核选择。

它的核心哲学是:把「要生成什么结构」这件事提前告诉运行时,运行时就能在调度层面做优化。比如你声明「下一步要输出一段符合某正则的 JSON」,后端就能用约束解码跳过大量非法 token,既快又不会出错。

对比一下体验差异:用裸 OpenAI 客户端,你只能拿到一段文本,格式对不对全靠运气和后处理;用 SGLang DSL,你能拿到带约束、可组合、程序化的生成结果。前者是「调接口」,后者是「写 LLM 程序」。

三、架构分析

3.1 RadixAttention:用基数树把 KV 缓存变成「共享内存」

RadixAttention 的核心数据结构是 Radix Tree(基数树)。它把「token 序列 → KV Cache 块」组织成一棵可共享的树:

  • 所有请求的 token 序列当作树的路径;
  • 新请求到来时,从根节点匹配最长公共前缀
  • 匹配到的前缀,其 KV Cache 直接复用,只计算差异部分;
  • 未被引用的叶子节点按 LRU 淘汰。

用一段教学代码表达其核心思想(非源码,帮助建立直觉):

# 教学示意:RadixAttention 的基数树节点(表达思想,非 SGLang 源码)
class RadixNode:
    def __init__(self, tokens, parent=None):
        self.tokens = tokens        # 该边上的 token 序列
        self.children = {}          # 首 token -> RadixNode
        self.kv_block = None        # 对应的 KV Cache 物理块
        self.ref_count = 0          # 引用计数(LRU 淘汰依据)
        self.last_access = 0.0      # 最近访问时间

class RadixCache:
    def match_prefix(self, seq):
        # 从根沿 seq 走最长公共前缀,返回(命中的 KV 块列表, 剩余未命中 token)
        node, hit, i = self.root, [], 0
        while i < len(seq):
            nxt = node.children.get(seq[i])
            if nxt is None or not seq[i:i+len(nxt.tokens)].startswith(nxt.tokens):
                break
            node = nxt
            hit.append(node.kv_block)
            i += len(nxt.tokens)
        return hit, seq[i:]

    def insert(self, seq, kv_blocks):
        # 把未命中的部分插入树,建立 token -> KV 块的映射
        node = self.root
        for blk in kv_blocks:
            first = blk.tokens[0]
            node = node.children.setdefault(first, RadixNode(blk.tokens, node))
            node.kv_block = blk

    def evict(self, budget):
        # 按 ref_count / 最近访问时间 LRU 淘汰叶子节点,释放显存
        ...

概念示意:

请求A: [system][history1][q1]  -> 节点路径 A
请求B: [system][history1][q2]  -> 前缀 [system][history1] 与 A 共享 KV

命中后 B 只需计算 [q2] 的 KV,省下了前两段的前缀计算。实测在多轮对话 / 共享 system prompt 场景下,缓存命中率提升 3~5 倍,延迟随之下降、吞吐上升。这正是 SGLang 在并发负载下稳压同类引擎的关键。

对比 PagedAttention:vLLM 解决「单请求内 KV 不连续导致的内存浪费」;RadixAttention 解决「多请求间前缀重复导致的重复计算」。两者正交,可叠加——你完全可以在 SGLang 的内存管理里看到分页思想的影子。

3.2 快速约束解码(Fast Constrained Decoding):让 JSON 输出快 10 倍

很多业务要求模型输出严格 JSON / 特定 schema(对接数据库、函数调用、Agent 工具)。朴素做法是「先自由生成,再用代码解析」,一旦格式错就废,还得 retry,又慢又不稳定。

SGLang 把约束下沉到解码层:给定正则或 JSON schema,后端构造一个下推自动机(pushdown automaton),每一步只从「合法 token」里采样。好处是:

  • 输出 100% 符合格式,无需后处理,不存在解析失败;
  • 合法 token 集合大幅缩小,搜索空间变小,生成更快(社区实测结构化输出可提速约一个数量级);
  • 与采样温度不冲突,仍可在合法空间内保持多样性。

配合 torch.compile 与 FlashInfer 内核,约束解码的开销被压到极低,很多时候「带约束」反而比「自由生成 + 校验重试」更快更稳。

3.3 零开销 CPU 调度器与 Two-Batch Overlap(TBO)

GPU 计算时,CPU 调度不能成为瓶颈。SGLang 的调度器把「请求排队 → 组成 batch → 下发 GPU」的流程做到零开销,并引入 Two-Batch Overlap:让「前向计算」和「前后处理(tokenize / detokenize / 调度决策)」在两张 batch 之间重叠,进一步榨干 GPU 利用率。源码里能看到 two_batch_overlap 模块对 CUDA Graph 运行时的封装,这就是高并发下延迟稳定的底气。

3.4 数据并行注意力(DP Attention):为 DeepSeek MLA 而生

DeepSeek 系列用 MLA(Multi-head Latent Attention),KV 被压缩成潜变量。若用普通张量并行(TP),KV 会被复制到每张卡,浪费显存。SGLang 的 DP Attention 模式:在 attention 层用数据并行(每张卡持有不同请求,共享权重、各自算各自的 KV),在 MoE / FFN 层再进入 TP,既省显存又保吞吐。这也是 DeepSeek 官方推荐 SGLang 的原因之一。

3.5 量化、推测解码、torch.compile

  • 量化:原生支持 FP8(W8A8)、FP8 KV Cache、INT4、AWQ、GPTQ,显存与带宽双降,长上下文场景收益明显。
  • 推测解码(Speculative Decoding):支持 EAGLE 等 draft 模型,小模型先「草稿」多步、大模型一次验证,decode 阶段延迟显著下降。
  • torch.compile:对算子做图级优化,进一步提速。

四、代码实战

4.1 安装与启动服务

pip install -U sglang

或用官方 Docker(推荐,避免 CUDA 版本错配):

docker run --gpus all --shm-size 16g -p 30000:30000 \
  -v /data/models:/data/models \
  lmsysorg/sglang:latest \
  python -m sglang.launch_server \
    --model-path meta-llama/Llama-3.1-8B-Instruct \
    --tp 2 \
    --port 30000 \
    --mem-fraction-static 0.86 \
    --chunked-prefill-size 8192 \
    --enable-metrics

关键参数:

  • --tp:张量并行卡数;
  • --mem-fraction-static:留给 KV Cache 的显存比例(调太高易 OOM,太低吞吐差);
  • --chunked-prefill-size:把超长 prefill 切块,避免长请求阻塞短请求;
  • --enable-metrics:暴露 Prometheus 指标。

4.2 前端 DSL 编程:多轮、分支、并行、工具调用

最基础的对话:

import sglang as sgl

@sgl.function
def chat(s, user_msg: str):
    s += sgl.system("你是一个严谨、简洁的中文技术助手。")
    s += sgl.user(user_msg)
    s += sgl.assistant(sgl.gen("answer", max_tokens=512, temperature=0.7))

runtime = sgl.RuntimeEndpoint("http://localhost:30000")
state = chat.run(user_msg="用一句话解释 RadixAttention", backend=runtime)
print(state["answer"])

分支控制流(根据上一步结果决定下一步):

@sgl.function
def router(s, question: str):
    s += sgl.system("你是意图分类器,只输出类别。")
    s += sgl.user(question)
    # select 强制从候选里选一个,避免模型自由发挥
    s += sgl.select("intent", options=["coding", "math", "chat"])
    if s.get("intent") == "coding":
        s += sgl.user("请写出可运行的代码。")
        s += sgl.assistant(sgl.gen("code", max_tokens=800))
    elif s.get("intent") == "math":
        s += sgl.assistant(sgl.gen("steps", max_tokens=400))

并行展开(Tree-of-Thought):

@sgl.function
def brainstorm(s, topic: str):
    s += sgl.system("你是创意教练。")
    s += sgl.user(f"给「{topic}」想 3 个方案:")
    forks = s.fork(3)                      # 复制成 3 份状态并行生成
    for f in forks:
        f += sgl.assistant(sgl.gen("idea", max_tokens=200))
    s += sgl.user("综合以上,给出最终推荐。")
    s += sgl.assistant(sgl.gen("final", max_tokens=200))

sgl.fork 把状态复制成多份并行生成,最后汇聚——这种「程序化 LLM 调用」用裸 API 要写一堆胶水代码,SGLang 一行搞定。

4.3 结构化输出实战(约束解码)

要求模型输出严格 JSON:

@sgl.function
def extract_person(s, text: str):
    s += "从下面的文本中抽取人物信息,只输出 JSON,不要多余文字。\n" + text
    s += sgl.gen(
        "result",
        regex=r'\{\s*"name"\s*:\s*"[^"]*",\s*"age"\s*:\s*[0-9]+,\s*"skills"\s*:\s*\[[^\]]*\]\s*\}',
    )

out = extract_person.run(
    text="张伟,28岁,擅长 Go 和 Kubernetes。",
    backend=runtime,
)
import json
print(json.loads(out["result"]))
# => {"name": "张伟", "age": 28, "skills": ["Go", "Kubernetes"]}

因为约束在解码层生效,模型不可能输出非法 JSON,彻底省掉了「解析失败重试」的灾难。

4.4 OpenAI 兼容客户端

已有代码不想改?SGLang 暴露 OpenAI 兼容端点:

from openai import OpenAI

client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")
resp = client.chat.completions.create(
    model="Llama-3.1-8B-Instruct",
    messages=[{"role": "user", "content": "Hi"}],
    temperature=0.7,
    stream=True,
)
for chunk in resp:
    print(chunk.choices[0].delta.content or "", end="")

4.5 多卡与分布式部署

张量并行(单机多卡)

python -m sglang.launch_server --model-path <model> --tp 8 --port 30000

DP Attention(DeepSeek 多卡)

python -m sglang.launch_server \
  --model-path deepseek-ai/DeepSeek-V3 \
  --tp 8 --dp 8 \
  --enable-dp-attention \
  --port 30000

流水线并行(超大模型跨机)

python -m sglang.launch_server \
  --model-path <model> \
  --tp 8 --pp 4 \
  --nnodes 2 --node-rank 0 \
  --dist-init-addr 192.168.0.1:22125 \
  --port 30000

4.6 量化与推测解码

# FP8 量化,显存与带宽双降
python -m sglang.launch_server --model-path <model> --quantization fp8 --tp 2

# EAGLE 推测解码,降低 decode 延迟
python -m sglang.launch_server \
  --model-path <target> \
  --speculative-algorithm EAGLE \
  --speculative-draft-model-path <draft> \
  --speculative-num-steps 3 \
  --tp 2

五、性能优化清单

5.1 看哪三个指标

  • TTFT(Time To First Token):首 token 延迟,受 prefill 速度影响,决定「用户觉得卡不卡」;
  • TPOT(Time Per Output Token):后续每 token 延迟,受 decode 速度影响,决定「吐字顺不顺」;
  • 吞吐(req/s 或 token/s):整体产能,决定「要多少张卡」。

SGLang 开 --enable-metrics 后,在 :30000/metrics 暴露 Prometheus 指标(运行中请求数、token 用量、缓存命中率等),配 Grafana 即可做实时看板。

5.2 调优参数速查

目标参数
提升 KV 容量 / 吞吐调大 --mem-fraction-static(0.8~0.9)
长请求不饿死短请求--chunked-prefill-size(如 4096 / 8192)
DeepSeek 类 MLA 模型省显存--enable-dp-attention
decode 延迟高开 EAGLE 推测解码
显存紧张FP8 / INT4 量化
算子再提速确保 torch.compile 生效(部分版本默认开启)

5.3 压测

python -m sglang.bench_serving \
  --backend sglang \
  --model-path <model> \
  --dataset-name random \
  --num-prompts 1000 \
  --request-rate 16

拿它和 vLLM 跑同一组负载(尤其是带共享前缀的「多轮 / RAG」负载),你会直观看到 SGLang 在前缀复用型场景下的领先幅度。

六、总结与展望

SGLang 的价值不在于「又写了一个推理引擎」,而在于它把 「程序化、结构化的 LLM 调用」和「运行时优化」真正打通了。RadixAttention 解决了跨请求前缀复用这个被长期忽视的痛点;约束解码把「格式正确性」从后处理挪到了生成过程本身;前后端协同设计让复杂 Agent 逻辑的表达成本骤降。

横向看:

  • vLLM:生态最成熟、模型支持最广,连续批处理的标杆;
  • TensorRT-LLM:NVIDIA 硬件上极致性能,但绑定强、上手重;
  • LMDeploy:TurboMind 引擎,国内生态友好;
  • SGLang:在结构化生成 + 前缀复用 + DeepSeek 系模型上目前最有竞争力。

面向 2026 年的 Agent 时代,推理引擎的下一战场是 Agent-native serving:多步工具调用、长程上下文、异构硬件(昇腾、GPU 混部)、prefill / decode 分离部署。SGLang 已经在原生支持昇腾、DP Attention、Two-Batch Overlap 上抢跑了。

如果你的业务里有多轮对话、RAG、函数调用、严格 JSON 输出中的任意一种,SGLang 值得你认真压测一轮——很多时候,换引擎比加卡更省钱。

推荐文章

Linux 网站访问日志分析脚本
2024-11-18 19:58:45 +0800 CST
百度开源压测工具 dperf
2024-11-18 16:50:58 +0800 CST
虚拟DOM渲染器的内部机制
2024-11-19 06:49:23 +0800 CST
程序员茄子在线接单