有段时间没有更新文章了,并不是因为我在憋什么大招。只不过是因为这段时间,随着 AI,尤其是 Vide Coding 的爆发,我现在对大数据 Infra 是提不起一点兴趣,不管啥框架,挂个 Claude,一分钟就能给你学完。

步入正题,今天来学习下大模型推理框架 - SGLang

第一部分:大语言模型基础

在理解 SGLang 之前,我们需要先搞清楚大语言模型(LLM)到底在做什么。

1.0 大语言模型的本质——下一个词的预测器

一句话:大模型的本质就是一个"下一个词预测器"——给定前面的所有文字,预测下一个最可能出现的词。

我们可以把 LLM 想象成一个读过几乎整个互联网的"超级填空选手"。当你给它一句话"今天天气真____",它会计算词表中每一个候选词出现在这个位置的概率:

1
2
3
4
5
6
7
"好"   → 35.2%
"不错" → 28.7%
"棒"   → 12.1%
"热"   →  8.4%
"差"   →  3.2%
"香蕉" →  0.0001%
...

然后通过采样策略(greedy、top-k、top-p 等)选择一个 Token 输出。这就是 LLM 做的全部事情——预测下一个 token 的分布,然后从中采样。

为什么输出看起来"有智能"?因为训练语料库的规模达到了数万亿 Token(书籍、网页、代码、论文),模型参数量达到数百亿甚至万亿级别。在这个规模下,模型学到的条件概率分布已经精确到足以捕获语言中绝大多数的语义关联和逻辑模式。当它看到"中国的首都是",训练数据中这个前缀后面几乎 100% 跟的是"北京",因此模型输出基本不会出现毫无关联的词。

理解了这个本质后,后面的技术细节就有了根基:Transformer 是实现这个概率模型的基本架构,自回归生成是使用它的方式,而 KV Cache 和 SGLang 的各种优化,都是为了让这个"预测下一个 Token"的过程在高并发场景下跑得更快。

1.1 Transformer 与注意力机制

Transformer 是当前几乎所有大语言模型的底层架构(GPT、LLaMA、DeepSeek 等都基于它)。它的结构是多层堆叠的 Block,每个 Block 主要由两部分组成:Self-Attention 层 和 FFN(前馈网络)层。其中 Self-Attention 是 Transformer 区别于早期 RNN/LSTM 的核心机制——它让模型能够直接建模序列中任意两个位置之间的依赖关系,而不需要像 RNN 那样逐步传递。

在 Transformer 之前,主流的序列模型是 RNN(循环神经网络)。RNN 按时间步顺序处理序列:第 1 个词的输出作为隐状态传给第 2 个词,第 2 个词的隐状态再传给第 3 个词,以此类推。这意味着第 100 个词要"感知"第 1 个词的信息,必须经过 99 步传递,信息在传递过程中不断衰减(长距离依赖问题),而且由于步骤之间有严格的先后依赖,无法并行计算。

Self-Attention 彻底改变了这一点:每个 Token 直接与序列中的所有 Token 计算关联度,不需要逐步传递。第 100 个词可以一步直接“看到”第 1 个词。这既解决了长距离依赖问题,又使得整个序列可以并行计算,充分利用 GPU 的并行能力。

Self-Attention 的计算过程

每个 Token 的 Embedding 向量经过三个不同的线性变换(即乘以三个权重矩阵 W_Q、W_K、W_V)得到三个向量,W_Q、W_K、W_V 是模型的可学习参数,在训练阶段从数据中学到——模型在海量文本上训练时,自动学会了如何将同一个 Token 投影到不同的子空间,使得 Q 擅长表达"需要什么信息"、K 擅长表达"能提供什么信息"、V 擅长携带语义内容。训练完成后这三个矩阵就固定下来,推理时直接使用:

向量含义说明
Q(Query)查询向量代表当前 Token “想要寻找什么信息”。当模型处理到某个 Token 时,Q 用来向序列中所有其他 Token 发起查询
K(Key)键向量代表当前 Token “能提供什么信息”。K 是每个 Token 暴露给其他 Token 用于匹配的标识
V(Value)值向量代表当前 Token “实际携带的语义内容”。由 K 匹配完成后,真正被聚合的信息来自 V

Q 和 K 用于计算"相关性",V 用于提供"内容"。具体来说:当前 Token 的 Q 与序列中每个 Token 的 K 做点积,得到一组分数(score),表示当前 Token 对每个位置的关注程度;这组分数经过 softmax 归一化为权重后,对所有 Token 的 V 做加权求和,得到当前 Token 的输出。

Attention 计算公式:Attention(Q,K,V) = softmax(QK^T / √d_k) · V,其中 √d_k 是缩放因子,防止点积值过大导致 softmax 梯度消失。

举例:对于句子 “小猫坐在垫子上”,当模型处理 “坐” 这个 Token 时,它的 Q 会与所有 Token 的 K 匹配,发现与 “小猫”(主语)和 “垫子”(位置)的 K 匹配度高,于是在加权求和时,“小猫” 和 “垫子” 的 V 会贡献更多信息到 “坐” 的输出表示中。

关键点:注意力的计算量随序列长度的平方增长——每个 Token 的 Q 都要与序列中所有 Token 的 K 计算一次点积,100 个 Token 意味着 100 个 Q 各自与 100 个 K 配对,共 100 × 100 = 10,000 次点积运算;1,000 个 Token 就是 1,000 × 1,000 = 1,000,000 次。序列长度增长 10 倍,计算量增长 100 倍。这是后面所有优化的根源之一。

1.2 自回归生成

LLM 每次推理只生成一个 Token,然后将其拼回输入序列,迭代生成下一个。

ChatGPT 回答问题时文字逐个出现,这不是 UI 效果,而是模型工作原理决定的——自回归生成(Autoregressive Generation):

1
2
3
第 1 步:输入 "今天天气"  → 模型输出 "真"
第 2 步:输入 "今天天气真"  → 模型输出 "不"
第 3 步:输入 "今天天气真不" → 模型输出 "错"

每一步,模型都需要:

  1. 接收目前为止的所有Token(用户输入 + 已生成的)
  2. 通过 Attention 计算它们之间的关系
  3. 输出一个新 Token

问题来了:回顾 1.1 节的 Attention 计算——要生成下一个 Token,模型需要用新 Token 的 Q 与序列中所有 Token 的 K 做点积、对所有 Token 的 V 做加权求和。这意味着序列中每个 Token 都必须有对应的 K 和 V 向量。如果不做任何缓存,每次推理都是一次独立的完整计算:模型拿到整个序列,为每个 Token 重新通过 W_K、W_V 矩阵计算 K 和 V。

所以第 3 步处理 “今天天气真不” 时,模型会为这 6 个 Token 全部重新计算 K 和 V,但其中 “今天天气” 的 K、V 在第 1 步就算过了,“真” 的 K、V 在第 2 步也算过了。每一步都在重复计算已有 Token 的 K/V,这是巨大的浪费。

1.3 KV Cache

缓存已计算的 K、V 向量,每步只为新 Token 计算增量,避免重复。

既然之前 Token 的 K、V 向量每一步都在重复计算,自然的优化就是把它们缓存在 GPU 显存中:

1
2
3
4
第 1 步算完后缓存:  [K₁,V₁] [K₂,V₂] [K₃,V₃] [K₄,V₄]                       ← "今天天气"
第 2 步只需算新的:  [K₁,V₁] [K₂,V₂] [K₃,V₃] [K₄,V₄] [K₅,V₅]              ← 新增 "真"
第 3 步只需算新的:  [K₁,V₁] [K₂,V₂] [K₃,V₃] [K₄,V₄] [K₅,V₅] [K₆,V₆]     ← 新增 "不"
                     \_______________ 缓存复用 _______________/  \_ 新计算 _/

有了 KV Cache,每步只需计算一个新 Token 的 Q、K、V,然后用新的 Q 和缓存中所有的 K 做匹配。每步的计算量从 O(n^2) 降到 O(n)。

但 KV Cache 有显著的内存代价:

  • 一个 70B 参数的模型,每个 Token 的 KV Cache 约占 1.25MB
  • 一条 4K 上下文长度的请求,KV Cache 约 5GB 显存
  • 一张 80GB 的 A100 GPU,去掉模型权重后,能同时服务的请求数非常有限

KV Cache 是 LLM 推理的核心矛盾——它是加速生成的必需品,同时也是显存的最大消耗者。如何管理 KV Cache,决定了推理系统的性能上限。

1.4 Prefill vs Decode——推理的两个阶段

Prefill 是计算密集型(compute-bound),Decode 是访存密集型(memory-bound),两者对硬件的需求截然不同。

LLM 处理一条请求分为两个阶段:

Prefill(预填充)Decode(解码)
做什么一次性并行处理用户的整段 prompt逐个生成输出 Token
计算特点并行处理所有输入 Token,计算量大每步只处理 1 个新 Token,计算量小
瓶颈计算密集型(GPU 算力是瓶颈)访存密集型(显存带宽是瓶颈)
GPU 利用率高(大规模矩阵运算,算术强度高)低(大部分时间在等数据搬运)

为什么 Decode 是访存密集型?因为每步只为 1 个新 Token 做计算,计算量很小,但这个计算需要的数据量却很大:模型的全部权重参数(70B 模型约 140GB)每步都要从显存加载到计算单元,同时还要读取整个序列的 KV Cache 来完成 Attention。计算量和数据搬运量的比值(算术强度)极低——GPU 的算力远未饱和,大部分时间在等数据从显存搬运过来。

相比之下,Prefill 一次性并行处理数百甚至数千个 Token,同样加载一次模型权重,但对这些权重做了大量矩阵乘法运算,算术强度高,GPU 算力被充分利用。

第二部分:大语言模型 LLM 推理服务的核心挑战

大部分人可能会想:“我直接用 Hugging Face Transformers 的 model.generate() 不就能跑模型了吗?为什么还需要 SGLang 这种推理服务?”

答案是:单条请求跑起来没问题,但要同时服务成百上千个用户就完全不够了。Hugging Face Transformers 是为研究和原型验证设计的,它一次处理一条(或手动凑一个小 batch 的)请求,没有请求队列、没有动态批处理、没有 KV Cache 的跨请求复用、没有显存的精细管理。当并发请求增多时,你会遇到:显存很快耗尽(每条请求独立分配 KV Cache)、GPU 大量空转(一条请求生成完才处理下一条)、长请求阻塞短请求(没有调度策略)。

具体来说,高并发 LLM 推理服务面临四大挑战:

挑战一:内存瓶颈。KV Cache 占用大量显存。并发请求越多,显存消耗越大。当显存耗尽时,新请求只能排队等待,即使 GPU 算力还有空余。

挑战二:计算瓶颈。Prefill 需要集中算力一次性处理长 prompt(compute-bound),Decode 需要频繁但轻量的计算(memory-bound)。两种负载特征完全不同,混在一起互相拖累。

挑战三:调度瓶颈。请求的输入长度和输出长度差异巨大。如何安排处理顺序,才能在吞吐量、延迟、公平性之间取得平衡?

挑战四:前缀重复。实际场景中,大量请求共享相同的前缀(如 System Prompt “你是一个有帮助的AI助手”)。每条请求都独立计算这部分的 KV Cache,造成大量重复计算和显存浪费。

SGLang 的核心设计就是针对这四个挑战的系统性解决方案。

第三部分:SGLang 的整体架构

3.1 整体架构

SGLang 的推理引擎在典型单节点部署下可以抽象为三个角色

推理引擎

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    用户请求(HTTP / gRPC)
┌─────────────────────┐
│  TokenizerManager   │  ← 分词 + 请求接入
│  文字 → Token ID     │
└────────┬────────────┘
         │ ZMQ (IPC)
┌─────────────────────┐
│     Scheduler       │  ← 调度 + GPU 计算(核心)
│  批次调度 + 模型推理   │
│  KV Cache 管理       │
└────────┬────────────┘
         │ ZMQ
┌─────────────────────┐
│ DetokenizerManager  │  ← 增量解码
│  Token ID → 文字     │
└────────┬────────────┘
         │ ZMQ
    返回给用户

TokenizerManager:运行在主进程。接收文字请求,分词为 Token ID 序列;处理图片等多模态输入

Scheduler:SGLang 的核心。决定哪些请求进入本轮计算、管理 KV Cache 的分配与回收、驱动 GPU 执行模型推理

DetokenizerManager:将模型输出的 Token ID 增量转回文字,支持流式返回

三个进程之间通过 ZMQ(ZeroMQ) 做 IPC 通信,轻量且解耦。

3.2 请求的完整生命周期

请求完整生命周期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
1. 用户发送 "请解释量子计算"
2. TokenizerManager 分词 → [8785, 46267, 101312, 99882]
3. Scheduler 接收,加入 waiting_queue
4. Scheduler 根据调度策略选出本轮请求,组成 Batch
5. Prefill:GPU 并行计算所有输入 Token 的 KV Cache
6. Decode:GPU 逐步生成输出 Token(每步一个)
       │  ↘ 每生成一批 Token,发送给 DetokenizerManager
7. DetokenizerManager 增量解码 → "量子" "计算" "是" "一种" ...
8. 流式返回给用户(或攒齐后一次性返回)

Scheduler 内部有三层数据抽象:

1
2
ScheduleBatch → ModelWorkerBatch → ForwardBatch
(CPU 调度数据) (CPU→GPU 桥接) (GPU 张量)

这个分层让调度逻辑(CPU 侧)和模型计算(GPU 侧)清晰解耦。

第四部分:五大核心优化

4.1 RadixAttention — 自动前缀缓存

多个请求共享相同前缀时,KV Cache 只算一次,自动复用。

实际场景中,大量请求共享相同的前缀:

1
2
3
4
5
请求 A: [System Prompt] + "请解释量子计算"
请求 B: [System Prompt] + "请翻译这段话"
请求 C: [System Prompt] + "帮我写一首诗"
    共享相同的 System Prompt → 传统做法各算一遍 KV Cache(3 倍浪费)

SGLang 使用基数树(Radix Tree)管理 KV Cache。每个树节点存储一段 Token 序列对应的 KV Cache 在 GPU 显存中的位置索引:

1
2
3
4
5
6
7
                    根节点
                   /      \
          [System Prompt A]  [System Prompt B]
           /    \                   |
     "请解释"  "请翻译"          "写代码"
       |         |                  |
    "量子计算"  "这段话"         "排序算法"

新请求到来时,沿树向下做前缀匹配:

  • 完全匹配 → 直接复用已缓存的 KV Cache,跳过这部分 Prefill
  • 部分匹配→ 树节点自动分裂(_split_node),复用匹配部分,只计算新增部分
  • 无匹配 → 正常计算,完成后插入树中供后续请求复用

显存有限,需要淘汰策略。SGLang 支持 6 种:LRU(默认)、LFU、FIFO、FILO、MRU、Priority。淘汰时使用最小堆从叶子节点开始回收,叶子被回收后若父节点变为空且未被锁定,则级联向上回收。

4.2 Continuous Batching — 持续批处理

请求完成后立即移出 batch 释放资源,新请求随时加入,GPU 不空转。

传统 Static Batching 将一组请求凑齐后一起处理,等全部完成再处理下一组。如果一条请求特别长,其余已完成的请求只能陪着空等,GPU 资源大量浪费:

Static Batching:

1
2
3
4
请求1: ████████░░░░░░░░  ← 已完成,GPU 资源空置
请求2: ████████████████  ← 最长的
请求3: ████░░░░░░░░░░░░  ← 空等
时间 ──────────────────→

Continuous Batching 在每一步模型推理后检查:完成的请求立即移出并释放 KV Cache,等待中的请求立即加入填补空位:

Continuous Batching:

1
2
3
4
5
请求1: ████████
请求3: ████         请求5: ██████████
请求4: ██████       请求6: ████████
请求2: ████████████████
时间 ──────────────────→

4.3 Chunked Prefill — 分块预填充

长 prompt 的 Prefill 拆分成多个 chunk,穿插执行 Decode,避免长请求独占 GPU。

一个 100K Token 的长文档 Prefill 可能需要数秒 GPU 独占时间。期间所有 Decode 请求被阻塞,用户感受到明显卡顿。

SGLang 将长 prompt 拆分成固定大小的 chunk(如 4096 Token),每个调度步只处理一个 chunk,Decode 请求可以在 chunk 之间穿插执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
不分块:
长请求 Prefill: ████████████████████████  ← 独占 GPU
短请求 Decode:  ░░░░░░░░░░░░░░░░░░░░░░░░ ████  ← 被阻塞

分块后(chunk_size = 4096):
长请求 chunk1: ████
短请求 Decode:      ██
长请求 chunk2:        ████
短请求 Decode:              ██
长请求 chunk3:                ████

显著降低短请求的 TTFT(Time To First Token)

4.4 Zero-Overhead Overlap Scheduling — 零开销重叠调度

CPU 调度与 GPU 计算流水线化,调度开销被 GPU 计算时间完全隐藏。

SGLang 的 Scheduler 有两种事件循环模式:

普通模式(event_loop_normal)——串行:

1
接收请求 → 调度 → GPU forward → 处理结果 → 接收请求 → 调度 → ...

重叠模式(event_loop_overlap)——CPU/GPU 流水线:

1
2
3
时间步 1:  GPU forward batch₁  |  CPU 空闲(首次启动)
时间步 2:  GPU forward batch₂  |  CPU 处理 batch₁ 结果 + 调度 batch₃
时间步 3:  GPU forward batch₃  |  CPU 处理 batch₂ 结果 + 调度 batch₄

效果:调度开销几乎为零。

4.5 Speculative Decoding — 投机解码

用小模型快速猜多个 Token,大模型一次性验证,将逐 Token 生成变为批量验证。

Decode 阶段每步只生成 1 个 Token,GPU 算力大量空闲(memory-bound)。投机解码的思路是:

  1. Draft 阶段:用小模型快速生成 k 个候选 Token
  2. Verify 阶段:大模型对这 k 个 Token 做一次推理来验证
  3. Accept/Reject:从第一个被拒绝的 Token 处截断,接受之前的所有 Token

效果: Decode 速度提升 2-3x,数学上保证输出分布与原始大模型一致。

为什么能保证分布一致?关键在于验证阶段的接受/拒绝策略。对于小模型提出的每个候选 Token x,大模型会计算自己在该位置的概率 p(x),同时已知小模型给出的概率 q(x)。接受概率为 min(1, p(x)/q(x))

  • 如果 p(x) >= q(x)(大模型认为该 Token 的概率不低于小模型的预测),则必定接受
  • 如果 p(x) < q(x)(大模型认为概率更低),则以 p(x)/q(x) 的概率接受,否则拒绝

当某个 Token 被拒绝时,不是简单丢弃,而是从一个修正分布 norm(max(0, p(x) - q(x))) 中重新采样一个 Token 作为替代。可以证明,这个"接受或从修正分布重采样"的过程,使得最终每个位置输出的 Token 的边际分布严格等于大模型原始的 p(x)——无论小模型的质量如何。小模型越准,接受率越高,加速比越大;但即使小模型很差,输出质量也不会下降,只是退化到与原始逐 Token 生成相同的速度。

第五部分:调度策略

Scheduler 的另一个职责是决定 waiting_queue 中哪些请求优先进入计算。SGLang 支持两类策略:

缓存感知策略(与 RadixAttention 配合):

  • LPM(Longest Prefix Match):优先调度在 Radix Tree 中能匹配到最长缓存前缀的请求,最大化 KV Cache 复用率
  • DFS-Weight:通过 DFS 遍历 Radix Tree 计算分支权重,按权重重排等待队列

缓存无关策略:

  • FCFS:先来先服务
  • LOF(Longest Output First):预期输出最长的先处理
  • Random

默认使用 LPM。当等待队列超过 128 条时自动退化为 FCFS,避免前缀匹配本身成为 CPU 瓶颈。

这种缓存感知调度是 SGLang 的特色——调度策略不只考虑公平性,还要最大化已有缓存的复用率。

第六部分:总结

SGLang 到底做了什么让 LLM 推理变快了?

LLM 推理的根本矛盾在于:KV Cache 既是必需品又是最大的显存消耗者;Prefill 和 Decode 的硬件需求截然不同却共享同一套资源。SGLang 在三个维度同时优化:

  • 内存管理:RadixAttention 用基数树自动复用共享前缀的 KV Cache,减少显存浪费
  • 计算调度:Continuous Batching 消除 GPU 空闲,Chunked Prefill 防止长请求阻塞短请求,Overlap Scheduling 将 CPU 调度与 GPU 计算流水线化
  • 缓存复用:缓存感知调度策略(LPM)优先处理能最大化复用缓存的请求

最终目标:让 GPU 的每一个时钟周期都在做有价值的计算,而不是在等待、重复或空转。