工作台课程

CS336 · 从零构建语言模型

Lecture 10 · 推理

Lecture 10 · 推理

CS336: Language Modeling from Scratch · Stanford · Spring 2025
📅 May 1 · 💻 可执行讲义 · 讲者 Percy Liang


承上启下

上一讲(Lecture 9 · 缩放定律 I:基础:讨论的是训练前的预算分配问题——给定计算预算 $C$,如何在参数量 $N$ 和数据量 $D$ 之间分配才能最小化训练 loss。训练侧的核心指标是 loss、FLOPs、batch size、学习率与 scaling curve。

本讲(推理,inference):视角从「训练一个模型花多少钱」切换到「用一个模型花多少钱」。训练是一次性成本,推理是随每次调用反复发生的成本——产品一旦跑起来,推理账单往往远超训练账单。更关键的是,推理 workload 的性质与训练完全不同:训练可以在整个序列维度上并行,把计算组织成大矩阵乘法;而自回归生成必须一个 token 一个 token 地串行进行,瓶颈从「训练算力」转为「生成阶段的内存带宽(memory bandwidth)+ 动态调度」。分析工具正是 Lecture 5 · GPU 的 roofline 模型与 Lecture 2 · PyTorch 与资源核算 的 FLOPs 核算方法。

与后续的关联Lecture 12 · 评估 中跑 benchmark、Lecture 16 · 对齐 II:RLVR 中生成 RL rollout,本质上都是大规模推理 workload,推理效率直接决定评估与后训练的成本。下一讲 Lecture 11 · 缩放定律 II:细节 则回到训练侧,看工业界如何在实践中拟合与使用 scaling laws。

摘要

推理分为 compute-bound 的 prefill 和 memory-bound 的 generation 两个阶段。KV cache 让生成的计算量从 $O(T^3)$ 降到 $O(T^2)$,但也把整个服务系统变成「KV cache 显存管理问题」;GQA/MLA/CLA/局部注意力、量化/剪枝/蒸馏、投机解码、continuous batching 与 PagedAttention,全都是围绕这个瓶颈做文章。


1. 推理 workload:训练之后的长期账单

推理无处不在,而且形态各异:

  • 聊天机器人:对延迟极其敏感,用户盯着屏幕等第一个字出现。
  • 代码补全:要求比聊天更低的延迟——补全必须在程序员打下一个字符之前出来。
  • 批量数据处理:如用 LLM 清洗/标注海量文档,不在乎单条延迟,只在乎总吞吐和总成本。
  • 模型评估:跑一遍 Lecture 12 · 评估 里的 benchmark 就是一次大规模批量推理。
  • Test-time compute:o1/R1 式「先思考再回答」用更长的生成换更好的答案,直接把推理成本乘了一个数量级。
  • RL 训练中的 rollout 生成Lecture 16 · 对齐 II:RLVR 中策略模型要不断采样输出算 reward,推理慢会直接卡住训练循环。

这些场景对「快」的定义并不相同,因此需要先把指标拆清楚。

1.1 三个指标

指标 含义 关注场景
TTFT(time-to-first-token) 用户等到第一个 token 的时间,由 prefill 决定 交互体验
Latency(每 token 延迟) 生成阶段每个 token 的间隔,由 decode 决定 聊天/代码补全
Throughput(吞吐) 系统整体每秒产出的 tokens/s 批处理、平台服务

三者经常互相冲突:

  • 小 batch:单请求延迟低,但 GPU 利用率差,每 token 成本高;
  • 大 batch:吞吐高、成本低,但请求要排队,TTFT 和每 token 延迟都变差;
  • prefill 与 decode 两个阶段的最优 batch 策略也不同(见第 4 节)。

提示

推理优化没有「唯一的快」。聊天产品优化的是 TTFT 和每 token 延迟(人的阅读速度约 10 tokens/s,比这快就够用),批处理平台优化的是每百万 token 的成本。同一套硬件、同一个模型,为这两类目标设计出的 serving 系统完全不同——所以先问「为谁优化」,再谈技术。


2. 推理的算术强度

2.1 训练 vs 生成

回顾 Lecture 5 · GPU算术强度(arithmetic intensity)定义为每从显存搬运 1 字节数据所做的浮点运算数:

$$\text{intensity} = \frac{\text{FLOPs}}{\text{bytes transferred}}$$

硬件也有一个对应的比值。以 H100 为例:bf16 算力峰值约 $989\ \text{TFLOP/s}$,HBM 带宽约 $3.35\ \text{TB/s}$,两者之比约

$$\frac{989 \times 10^{12}}{3.35 \times 10^{12}} \approx 295\ \text{FLOPs/byte}$$

这就是 roofline 的「屋脊点」:算术强度高于 295 时是 compute-bound(算力受限),低于 295 时是 memory-bound(带宽受限),此时算力再强也只能空转等数据。

训练:输入完整序列,可以在序列维并行,前向/反向都是大矩阵乘法,算术强度轻松超过屋脊点,是 compute-bound。

生成:每步只产出一个新 token,且必须等上一个 token 出来才能算下一个。大量操作退化为 matrix-vector 乘法,读权重和 KV cache 的搬运成本主导一切,是 memory-bound。

2.2 Matrix-vector 的问题

考察 MLP 层的一次矩阵乘 $Y = XW$,其中 $X \in \mathbb{R}^{B \times D}$($B$ 为 batch 中的 token 数,$D$ 为隐藏维度),$W \in \mathbb{R}^{D \times F}$($F$ 为 MLP 中间维度):

  • FLOPs:$2BDF$(每个输出元素一次乘加);
  • 数据搬运(bf16,每元素 2 字节):$2(BD + DF + BF)$ 字节。

当 $D, F \gg B$ 时,分母由权重 $DF$ 主导,于是

$$\text{intensity} = \frac{2BDF}{2(BD + DF + BF)} \approx B$$

算术强度约等于 batch 中的 token 数。生成时若 $B = 1$:为了产出一个 token 的计算,要把整个 $W$ 从 HBM 读一遍,算术强度约为 1 FLOP/byte,离屋脊点 295 差了两个数量级——GPU 的算力利用率不到 1%。

提示

把权重从 HBM 搬到片上的成本是固定的「过路费」。batch 里的 token 越多,这笔过路费被摊得越薄;$B=1$ 时一个 token 独自付全款,自然亏到极点。这就是为什么「换更快的 GPU」不一定让单请求生成按比例变快——瓶颈不是算力,而是把参数和 KV cache 从 HBM 搬出来的带宽。

2.3 Attention 的算术强度:batch 帮不上忙

MLP 的强度随 $B$ 增长,但 attention 不同。设一次前向处理 $T$ 个 query token、历史共 $S$ 个 KV token,单头维度 $H$:

  • FLOPs:$QK^\top$ 与 $AV$ 两次矩阵乘,约 $4STH$;
  • 搬运:读 $K, V$(各 $S \times H$)与 $Q$($T \times H$)、写输出,约 $4H(S+T)$ 字节量级。

$$\text{intensity}_{\text{attn}} \approx \frac{4STH}{4H(S+T)} = \frac{ST}{S+T}$$

代入两个阶段:

  • Prefill($T = S$,整段 prompt 一起算):强度 $\approx S/2$。$S = 1024$ 时约 512,超过屋脊点,compute-bound;
  • Decode($T = 1$,一次一个 token):强度 $\approx S/(S+1) < 1$,深度 memory-bound。

注意这个公式里没有 $B$:每条序列都有自己私有的 KV cache,batch 内不同请求之间无法共享读取。增大 batch 能摊薄 MLP 读权重的成本,却对 attention 读 KV cache 毫无帮助。

注意

「加大 batch 就能解决 memory-bound」只对 MLP/权重部分成立。长上下文场景下 KV cache 的读取量随 $B \times S$ 线性增长,batch 越大总搬运量越大——attention 部分的算术强度始终小于 1。这也是长上下文推理格外昂贵的根本原因。


3. KV Cache:复用前缀计算

3.1 朴素自回归推理

生成第 $t$ 个 token 时,如果把前面所有 token 都重新喂进 Transformer 完整算一遍前向:第 $t$ 步 attention 计算量为 $O(t^2)$,累加 $T$ 步:

$$\sum_{t=1}^{T} O(t^2) = O(T^3)$$

生成 $T$ 个 token 要 $O(T^3)$ FLOPs——大量计算在重复:前缀 token 的 K/V 表示每一步都被原样重算了一遍。

3.2 KV Cache

KV cache 的做法:对每层、每个 KV 头、每个历史 token,把算过的 key/value 向量缓存下来:

$$\text{KV cache 形状} \approx [\,B,\ S,\ L,\ K,\ H\,]$$

其中 $B$ 为 batch size,$S$ 为序列长度,$L$ 为层数,$K$ 为 KV 头数,$H$ 为单头维度。生成新 token 时:

  • 新 token 只计算自己的 Q/K/V,并把 K/V 追加进 cache;
  • 旧 token 的 K/V 直接从 cache 读,不再重算;
  • attention 用新 Q attend 到全部历史 K/V。

于是每步 attention 降为 $O(t)$,总量降为 $O(T^2)$;MLP 每步只处理 1 个 token,总量 $O(T)$。代价是显存里多了一块随序列增长的状态。

由此推理天然分成两个阶段:

阶段 输入 特征 瓶颈
Prefill prompt 全部 token 序列内并行,一次前向填满 cache compute-bound
Generation / Decode 每次 1 个 token 自回归串行 memory-bound

提示

KV cache 是「用空间换时间」的教科书案例:把 $O(T^3)$ 的重复计算换成 $O(T^2)$ 计算 + $O(T)$ 的显存状态。但这笔交易把问题的性质变了——推理系统的核心资源从「FLOPs」变成「装 KV cache 的显存 + 读 KV cache 的带宽」,后面所有优化(GQA、MLA、PagedAttention)都是在管理这块状态。

3.3 KV Cache 的内存成本

每个请求的 KV cache 随上下文长度线性增长:

$$\text{bytes} = \underbrace{B}_{\text{batch}} \times \underbrace{S}_{\text{序列长}} \times \underbrace{L}_{\text{层数}} \times \underbrace{K}_{\text{KV 头数}} \times \underbrace{H}_{\text{头维度}} \times \underbrace{2}_{K\text{ 与 }V} \times \underbrace{2}_{\text{bf16 字节数}}$$

例子

配置:$L=40$ 层,$K=40$ 个 KV 头(标准 MHA),$H=128$,$D=5120$,$F=13824$,词表 32000。

  • 每 token 的 KV cache:$40 \times 40 \times 128 \times 2 \times 2 = 819{,}200$ 字节 $\approx 0.8\ \text{MB}$;
  • $S=1024$ 时每条序列约 $0.8\ \text{GB}$;
  • $B=64$ 时 KV cache 共约 $54\ \text{GB}$,加上 bf16 权重 $26\ \text{GB}$,恰好逼近 H100 的 80 GB 显存上限;
  • $B=256$ 直接 OOM。限制 batch size(也就限制了吞吐)的不是算力,而是 KV cache 的显存占用。

4. Throughput 与 Latency 的权衡

batch size 增大时:

  • MLP 权重被 batch 内所有请求共享读取,每 token 的搬运成本被摊薄,throughput 提升;
  • 但单个请求要与更多请求共享算力和带宽,且可能排队,latency 和 TTFT 变差;
  • KV cache 总量随 batch 线性增长,显存上限反过来限制 batch 的天花板(见 3.3 的算例)。

常见策略:

  • prefill 与 decode 分离调度:prefill 是 compute-bound 的大矩阵乘,decode 是 memory-bound 的小步快跑,两者混在一个 batch 里会互相拖累(一个长 prompt 的 prefill 会卡住所有人的 decode)。分开调度甚至分开部署(disaggregated serving)能让各自都跑在合适的 regime;
  • decode 用尽量大的 batch:提高吞吐,直到 KV cache 吃满显存;
  • 多副本部署:复制 $M$ 份模型实例,latency 不变,throughput 乘 $M$——用钱换吞吐,简单有效;
  • 模型/KV cache 分片:结合 Lecture 7 · 并行 I:基础 的张量并行等手段把模型切到多卡,更复杂,但能服务单卡装不下的大模型,也顺带扩了 KV cache 的可用显存。

提示

Throughput 和 latency 的冲突本质上是「公交车 vs 出租车」:公交车(大 batch)人均成本低但要等发车,出租车(小 batch)即叫即走但贵。serving 系统的工作就是根据 SLA 在两者之间找运营方案,而 KV cache 显存决定了「公交车最多能坐多少人」。


5. 降低 KV Cache:架构捷径

以下都是有损(lossy)捷径:改动模型架构来缩小 KV cache,可能付出一点准确率代价,换取更大的 batch 与更快的生成。

5.1 GQA / MQA

标准 MHA(multi-head attention)中 KV 头数等于 query 头数($K = N$);MQA(multi-query attention)走到另一个极端,所有 query 头共享 1 组 KV($K = 1$);GQA(grouped-query attention)取中间值,让每组 query 头共享一组 KV($1 < K < N$)。

KV cache 的缩减比例正是头数之比:

$$\text{缩减倍数} = \frac{N}{K}$$

例子

Llama 2 13B 若把 $K = 40$ 改为 $K = 8$(每 5 个 query 头共享一组 KV),KV cache 缩小 5 倍:$B=64$ 时从约 54 GB 降到约 11 GB,于是 batch 可以开到 256,吞吐大幅提升。Llama 3 系列已默认采用 GQA(见 Lecture 3 · 架构与超参数)。

GQA 是当前主流折中:实验表明适度分组几乎不掉点,比 MQA 表达力好,比 MHA 省数倍 cache。

5.2 MLA(Multi-head Latent Attention)

DeepSeek-V2/V3 使用的思路:不直接缓存完整的 K/V,而是训练一个低秩压缩——把每个 token 的隐状态投影到一个低维 latent 向量 $c_t$ 缓存起来,需要时再用上投影矩阵恢复出各头的 K/V。

例子

128 头 × 128 维的完整 K/V 每 token 每层本需 $2 \times 16384$ 维;MLA 只缓存 $c_t \in \mathbb{R}^{512}$ 外加 64 维解耦的 RoPE key,共 576 维——缓存量降到约 1/57,与只分 2~3 组的 GQA 相当,但论文报告效果反而优于 MHA。

优点:

  • KV cache 极大降低,generation 阶段要读的字节数同步降低;
  • 因为「读 cache」正是 decode 的瓶颈,这直接转化为更大的 batch 和更高的吞吐,对 MoE 大模型(参见 Lecture 4 · 混合专家模型 MoE)的推理尤其重要。

难点:

  • 与 RoPE 的兼容需要特殊处理:MLA 的效率依赖「把 K 的上投影矩阵吸收进 Q 的投影」这一代数技巧,而 RoPE 给 K 乘上了随位置变化的旋转矩阵,夹在中间导致无法吸收。DeepSeek 的解法是解耦(decoupled)RoPE:额外留出 64 维专门携带位置信息,其余维度不做旋转;
  • 架构复杂度明显上升,且必须从预训练阶段就采用,无法像量化那样对现成模型事后加装。

5.3 Cross-Layer Attention / Local Attention

CLA(cross-layer attention):相邻的多层共享同一份 K/V——GQA 是「跨头共享」,CLA 是「跨层共享」,两者正交、可叠加。实验显示它能改善「准确率 vs KV cache 大小」的帕累托前沿。

Local attention(滑动窗口注意力):每个 token 只 attend 最近 $W$ 个 token,KV cache 只需保留窗口内的条目,与总上下文长度无关。代价是长程信息会丢失、准确率受损,因此实践中与少数全局 attention 层交错使用——例如 Character.AI 的部署是每 6 层局部才配 1 层全局。

提示

5.1–5.3 的所有花样共享同一个目标函数:在尽量不掉准确率的前提下,减少生成每个 token 时必须从 HBM 读出的 KV 字节数。方向有三个正交的轴——跨头共享(GQA/MQA)、跨层共享(CLA)、压缩表示(MLA)、截断历史(local attention)——工业模型往往组合使用。


6. Transformer 之外:为推理重设架构

如果把「KV cache 随 $T$ 线性增长」看作 Transformer 的原罪,那么更激进的路线是换掉架构本身。

6.1 State Space Models

SSM(state space model)的核心思路:用一个固定大小的状态向量递归地总结全部历史,生成时只更新这个状态,而不是维护 $O(T)$ 的 KV cache——推理成本与上下文长度解耦。

代表工作:

  • S4:结构化状态空间模型的开山之作,线性时不变系统,可用卷积并行训练;
  • Mamba:引入输入依赖(selective)的状态更新参数,弥补了 SSM 在内容选择上的短板,在 1B 规模上与 Transformer 打平;
  • Jamba:AI21 的混合架构,Transformer 层与 Mamba 层按 1:7 交错,再叠加 MoE——用少量 attention 层保住检索能力,用大量 Mamba 层省 cache;
  • BASED / MiniMax-01 等线性注意力变体:线性注意力 + 局部注意力,再保留少量完整 attention 层。

优势是 $O(1)$ 的状态大小,长上下文推理内存和带宽都极省。问题在于固定大小的状态是有损压缩:associative recall(如「前文提过的某个电话号码是多少」这类精确检索)任务上 Transformer 仍显著更强。因此纯替代不容易,混合架构是当前的务实答案。

6.2 Diffusion LM

扩散式文本生成不做自回归:从一个全是噪声/掩码的序列出发,并行地对整个序列做多步迭代精化(refine)。理论上每步都能同时更新所有位置的 token,摆脱「一步一个 token」的串行瓶颈——Inception Labs 的 Mercury 声称在代码生成上比同档自回归模型快数倍。

挑战:离散文本上的扩散在数学上比连续图像更别扭,生成质量、可控性与训练配方都还在探索期,尚未撼动自回归的主流地位。

提示

SSM 与 diffusion 分别攻击自回归 Transformer 的两个软肋:SSM 砍掉 $O(T)$ 的 KV cache(状态太大),diffusion 砍掉逐 token 的串行依赖(步数太多)。它们都是「推理成本倒逼架构设计」的例证——架构选择越来越不由训练效果单独决定。


7. 量化、剪枝与蒸馏

另一类有损捷径:不改架构,直接压缩模型本身。生成是 memory-bound 的,权重字节数减半 ≈ 读权重的时间减半,所以压缩直接换来速度。

7.1 Quantization

格式 字节 用途
FP32 4 训练中的 optimizer state
BF16 / FP16 2 常规训练与推理默认
FP8 1 高效推理;训练「艺高人胆大」也可用
INT8 1 高效推理
INT4 0.5 极限压缩

PTQ(post-training quantization,训练后量化):用少量校准数据估计每个通道/分组的 scale 与 zero-point,把权重映射到低比特整数网格,不需要重新训练。QAT(quantization-aware training,量化感知训练):训练时就模拟量化误差,效果更好但要动训练流程,大规模场景不总现实。

LLM.int8() 的关键发现:模型规模超过约 6.7B 后,激活中会系统性出现少数离群(outlier)特征通道,其数值大到让普通 int8 量化的网格完全失真。解法是混合精度分解——把约 0.1% 的 outlier 通道单独用 fp16 计算,其余走 int8。代价是推理反而比 fp16 慢 15~23%(分解本身有开销),它换来的是显存减半而非速度。

AWQ(activation-aware weight quantization):观察到权重的重要性由激活分布决定,保护约 0.1%~1% 的关键权重(通过按通道缩放而非真的保留 fp16),其余压到 int3/int4。fp16 → int3 可得约 4 倍显存缩减与 3.2 倍实测加速。

注意

量化的评估不能只看 perplexity——PPL 对少数关键能力的退化不敏感(这与 Lecture 12 · 评估 讲的「PPL 不等于下游能力」是同一件事)。低比特量化后应在下游任务上复测,尤其是长推理链任务,误差会沿生成步数累积。

7.2 Pruning + Distillation

剪枝(pruning):按重要性评分删除整个层、attention 头或隐藏维度,直接得到一个更小、更快的模型(结构化剪枝,硬件友好)。剪完的模型通常明显掉点。

蒸馏(distillation):用原模型(teacher)的输出分布做监督信号,训练/修复剪枝后的小模型(student),把丢掉的能力找补回来。

两种得到小模型的 recipe:

路线 步骤
From scratch 设计快架构 → 从头预训练
Prune + Distill 取大模型 → 重要性评分并剪枝 → 用 teacher 蒸馏修复

NVIDIA 的 Minitron 系列与 Llama 3.2 的 1B/3B 都走第二条路线:其总成本可比从头训练低一个数量级,因为大模型已经付过的「学费」通过蒸馏被继承了下来。


8. Speculative Decoding:用小模型猜,大模型验

这是无损(lossless)加速:输出分布与目标模型逐 token 精确一致。

出发点是 decode 阶段的一个不对称性:生成慢,验证快。生成 $K$ 个 token 要 $K$ 次串行前向;但给定 $K$ 个候选 token,用一次并行前向就能算出它们每个位置的概率(像 prefill 一样并行)。又因为 decode 是 memory-bound,一次前向处理 1 个还是 $K$ 个 token,读权重的时间几乎一样——验证近乎免费

投机采样(speculative sampling)流程(讲义记号:draft 模型为 $p$,target 模型为 $q$):

  1. 便宜的 draft 模型自回归猜 $K$ 个 token(如 $K=4$);
  2. target 模型一次并行前向算出这些位置上的概率;
  3. 对每个候选 token $x \sim p$,以概率

$$\alpha(x) = \min\!\left(1,\ \frac{q(x)}{p(x)}\right)$$

接受;一旦拒绝,从残差分布

$$r(x) = \frac{\max\bigl(0,\ q(x) - p(x)\bigr)}{\sum_{x'} \max\bigl(0,\ q(x') - p(x')\bigr)}$$

中重新采样一个 token 并停止本轮;
4. 这个修正的 rejection sampling 保证最终每个 token 都精确服从 $q$。

例子

设词表只有 $\{A, B\}$,且 draft 高估了 $A$:$p(A) > q(A)$(于是 $p(B) < q(B)$)。

  • 输出 $A$ 的概率 $= p(A) \cdot \frac{q(A)}{p(A)} = q(A)$ ✓
  • 输出 $B$ 的概率 $= \underbrace{p(B) \cdot 1}_{\text{直接接受}} + \underbrace{p(A)\bigl(1 - \tfrac{q(A)}{p(A)}\bigr)}_{\text{拒绝 }A\text{ 后重采样,残差全在 }B} = p(B) + p(A) - q(A) = 1 - q(A) = q(B)$ ✓
    无论 draft 猜得多差,输出分布都严格等于 target——draft 质量只影响速度(接受率),不影响正确性
sequenceDiagram
    participant Draft as "Draft 模型 p"
    participant Target as "Target 模型 q"
    Draft->>Draft: 串行猜 token1..token4
    Draft->>Target: 提交候选序列
    Target->>Target: 一次前向并行算各位置概率
    Target-->>Draft: 按 min(1, q/p) 接受前 k 个,拒绝处从残差重采样

关键性质与实践:

  • exact sampling:与直接从 target 采样不可区分,无需任何质量评估;
  • draft 越接近 target,接受率越高、加速越明显。典型搭配是同家族小模型:70B target + 8B draft、8B target + 1B draft;
  • draft 与 target 必须共用同一个 tokenizer(参见 Lecture 1 · 概览与分词),否则「逐 token 验证」无从谈起。

扩展:

  • Medusa:不用独立 draft 模型,在 target 模型上加多个解码头一次并行猜多个后续 token,省掉 draft 的前向;
  • EAGLE:用 target 模型最后一层的 hidden features(而非 token)做自回归 draft,猜测质量更高、接受率更好。

提示

投机解码把 memory-bound 阶段「算力大量闲置」这个坏消息变成好消息:反正读一遍权重的时间是固定的,不如让这次前向多验证几个 token,把闲置的 FLOPs 用起来。它在概念上与 rejection sampling 完全同源——用一个便宜的 proposal 分布逼近昂贵的目标分布,错了就修正。


9. 动态请求:Continuous Batching 与 PagedAttention

前面假设 batch 是静态给定的;线上真实流量是 ragged workload(参差不齐的负载):

  • 请求到达时间不同;
  • prompt 长度不同;
  • 生成长度不同且事先未知(有人生成 10 个 token 就停,有人生成 1000 个);
  • 大量请求共享同一个 system prompt 前缀;
  • 同一 prompt 可能要采样多个候选(best-of-n)。

静态 batching 下,整个 batch 必须等最慢的请求生成完才能换人,GPU 大量空转。

9.1 Continuous Batching

Orca 提出两个核心机制:

  • Iteration-level scheduling(迭代级调度):调度的粒度不是「请求」而是「一步 decode」。每个 decode step 结束后重新组 batch——完成的请求立即腾位,新请求随时插入,不必等整个 batch 收工;
  • Selective batching(选择性批处理):不同请求序列长度不同,无法拼成规整张量。解法是把所有序列的 token 拼接(flatten)成一个大 batch 跑非 attention 算子(MLP、LayerNorm 等对 token 独立的运算),只在 attention 处按序列拆开分别计算——因为只有 attention 关心序列边界。

9.2 PagedAttention

传统 KV cache 分配按「最坏情况」给每个请求预留最大长度的连续显存,造成严重浪费:内部碎片(预留了但没用到)加外部碎片(空闲块不连续没法用),实测有效利用率可能只有 20%~40%。

PagedAttention(vLLM 的核心)把操作系统的虚拟内存分页思想搬进 KV cache 管理:

  • 把每个请求的 KV cache 切成固定大小的 block(如每块 16 个 token),按需分配;
  • 逻辑上连续、物理上不连续,靠 block table 做「页表」翻译;
  • 天然支持 prefix sharing:共享同一 system prompt 的请求映射到同一批物理 block,只存一份;
  • 分叉采样(一个 prompt 出多个候选)用 copy-on-write:共享的 block 被写入时才复制。
graph LR
    REQ["请求的逻辑 KV cache"] --> B1["block 1"]
    REQ --> B2["block 2"]
    REQ --> B3["block 3"]
    B1 -.物理映射.-> HBM1["HBM page A"]
    B2 -.物理映射.-> HBM2["HBM page Q"]
    B3 -.物理映射.-> HBM3["HBM page C"]

vLLM 还叠加了一系列系统优化:把 block 读取融合进 attention kernel(fused kernel)、FlashAttention / FlashDecoding(参见 Lecture 6 · Kernel 与 Triton)、CUDA graphs 消减 kernel launch 开销。综合下来相对此前系统有 2~4 倍吞吐提升。

提示

KV cache 管理面对的问题——变长、动态到达、共享前缀、碎片化——和操作系统管理进程内存的问题一模一样,所以答案也一样:分页 + 页表 + 写时复制。Serving 的进步很多不是 ML 创新,而是把 OS/数据库社区五十年的老智慧搬进 GPU 显存。


总结

mindmap
  root((推理))
    Metrics
      TTFT
      latency
      throughput
    Workload
      prefill compute-bound
      generation memory-bound
      KV cache
    Architecture
      GQA
      MLA
      CLA
      local attention
      SSM
    Compression
      quantization
      pruning
      distillation
    Speedups
      speculative decoding
      Medusa
      EAGLE
    Serving
      continuous batching
      PagedAttention
      prefix sharing

关键要点

  1. 推理和训练是两种 workload
    - 训练可在序列维并行、compute-bound;生成串行、算术强度约等于 batch size,深度 memory-bound。H100 屋脊点约 295 FLOPs/byte,$B=1$ 的生成只有约 1。

  2. KV cache 是推理系统的核心状态
    - 它把 $O(T^3)$ 计算降到 $O(T^2)$,但吃显存和带宽;限制 batch(吞吐)的往往是 KV cache 而非算力。

  3. Prefill 和 generation 要分开优化
    - prefill 像训练(大矩阵乘),generation 像反复搬运权重和 cache;两阶段混合调度会互相拖累。

  4. GQA/MLA/CLA/local attention 都是在减 KV cache
    - 跨头共享、压缩表示、跨层共享、截断历史四条正交的轴;架构设计越来越受推理成本驱动,SSM 与 diffusion 是更激进的重设。

  5. 量化/剪枝/蒸馏用少量精度换字节数
    - memory-bound 下权重减半近似读取时间减半;outlier 是量化的头号敌人(LLM.int8()、AWQ 都在处理它)。

  6. Speculative decoding 是 lossless shortcut
    - 小模型猜、大模型并行验,修正的 rejection sampling 保证输出分布精确等于 target;draft 质量只影响速度不影响正确性。

  7. Serving 是系统问题
    - continuous batching(迭代级调度 + 选择性批处理)和 PagedAttention(分页 + 前缀共享 + 写时复制)是把 OS 思想搬进 LLM serving。

下一讲预告(Lecture 11 · 缩放定律 II:细节

推理说明了模型训练后的成本;下一讲回到训练前的模型设计,深入看 CerebrasGPT、MiniCPM、DeepSeek 等模型如何在实践中拟合 scaling laws、选择 batch size 和学习率,并讨论 muP 是否真能让超参数跨规模迁移。


复习自测

题目

MLP 层矩阵乘的算术强度约等于 batch 中的 token 数,$B=1$ 时约为 1 FLOP/byte;而 H100 的屋脊点是 $989\ \text{TFLOP/s} \div 3.35\ \text{TB/s} \approx 295$ FLOPs/byte。1 远小于 295,说明时间花在从 HBM 搬权重上,算力利用率不足 1%——瓶颈是带宽而非算力。

题目

每 token:$40 \times 40 \times 128 \times 2 \times 2 \approx 0.8$ MB;每序列(1024 token)约 0.8 GB;$B=64$ 时约 54 GB。加上 26 GB 权重正好逼近 H100 的 80 GB。说明限制 batch size(进而限制吞吐)的是 KV cache 显存,这正是 GQA/MLA 等「减 cache」架构的动机。

题目

MLP 的权重 $W$ 是所有请求共享的,batch 越大,读一遍 $W$ 的固定成本被摊得越薄(强度 $\approx B$)。而每条序列的 KV cache 是私有的,batch 内无法共享读取——attention 的算术强度 $\approx ST/(S+T)$,公式里根本没有 $B$;decode 时($T=1$)恒小于 1,始终 memory-bound。

题目

无损性来自修正的 rejection sampling:对 draft 提议的 $x \sim p$ 以 $\min(1, q(x)/p(x))$ 接受,拒绝时从残差分布 $r(x) \propto \max(0, q(x)-p(x))$ 重采样,可证输出边际分布精确等于 $q$。验证便宜有两个原因:K 个候选位置的概率可以在一次前向中并行算出(类似 prefill);且 decode 是 memory-bound,一次前向的耗时由读权重主导,处理 1 个还是 $K$ 个 token 时间几乎相同。

题目

MLA 的推理效率依赖把 K 的上投影矩阵「吸收」进 Q 的投影,从而只需缓存低维 latent。但 RoPE 在 K 上乘了随位置变化的旋转矩阵,卡在两个投影之间使吸收不再成立。DeepSeek 的解法是解耦 RoPE:额外留出 64 维专门做旋转、携带位置信息,其余 512 维 latent 不做旋转,两部分拼接后参与 attention。


参考资料