第 10 讲:推理 —— 为什么生成文本又慢又贵,怎么加速?

Language Modeling from Scratch · Lecture 10。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应:Stanford CS336 Spring 2026 Lecture 10(Inference,Percy Liang 的可执行讲义 lecture_10.py)。Spring 2025 的第 10 讲内容相近,有公开录像。 配套代码:code/lecture10/ 预计阅读时间:3–4 小时 硬件说明:普通电脑即可。配套代码在第 2、3 讲的小模型上实现 KV 缓存、量化和推测解码;第一次运行会花约 45 秒训练这两个小模型。


0. 本讲导航

0.1 学完本讲,你应该能够

  1. 说出推理在哪些地方出现、为什么推理效率比训练效率更重要,以及三个指标:首 token 延迟(TTFT)、延迟、吞吐量。
  2. 解释 KV 缓存为什么能把生成的计算量从 O(T3)O(T^3) 降下来,并实现带 KV 缓存的生成。
  3. 用逐项统计 FLOPs 与读写字节的方法,推出”预填充计算受限、解码内存受限”,以及”批处理能帮 MLP、帮不了注意力”。
  4. 用”每步读一遍参数和 KV 缓存”的模型估算延迟、吞吐量,解释批次大小带来的权衡。
  5. 说出减小 KV 缓存的几种方法(GQA、MQA、MLA、跨层共享、局部注意力、混合架构),并能计算 KV 缓存的大小。
  6. 说出量化的基本操作、粒度的作用、QAT 与 PTQ、GPTQ 与 AWQ 的思路。
  7. 描述剪枝 + 蒸馏的流程。
  8. 推导推测采样为什么精确地保持目标模型的分布,并估算它的加速比。
  9. 解释连续批处理和 PagedAttention 怎样应对真实服务中”请求随时到来、长度各不相同、前缀常常相同”的负载。

0.2 先修知识

知识点用在哪里最低要求
第 3 讲全文Transformer 的结构、GQA、RoPE、采样(温度、top-p)
第 5 讲第 3 节roofline、算术强度、H100 的临界点约 295
第 2 讲第 5 节浮点格式(FP32、BF16、FP8)
第 4 讲第 5.1 节MLA、滑动窗口、线性注意力 / 状态空间模型
概率第 6 节条件概率、拒绝采样的直觉

0.3 本讲地图

第1节 推理无处不在:为什么要关心推理效率
第2节 从朴素生成到 KV 缓存;预填充与解码
第3节 算术强度:为什么解码是内存受限的
第4节 延迟与吞吐量的权衡
第5节 抄近路(有损):减小 KV 缓存、量化、剪枝与蒸馏
第6节 抄近路但仔细检查(无损):推测解码
第7节 动态负载:连续批处理与 PagedAttention

CS336 课上的组织方式:先理解推理的工作负载(第 2–4 节),再看两类加速方法——有损的捷径(改模型,第 5 节)和无损的捷径(不改输出分布,第 6 节),最后看真实服务的动态负载(第 7 节)。


1. 推理无处不在

1.1 推理出现在哪里

  • 实际使用:聊天机器人、代码补全、智能体(agent)、批量数据处理;
  • 模型评估:例如评估指令遵循能力,要让模型生成大量回答(第 12 讲);
  • 强化学习:采样很多个回答,再打分(第 16 讲的 RLVR 中,采样往往是最花时间的部分)。

1.2 为什么效率这么重要

**训练是一次性的成本,推理要重复无数次。**CS336 课上引用的数字:OpenAI 每天处理约 8.6 万亿 token;作为对比,DeepSeek V4 的整个预训练用了约 32 万亿 token——也就是说,一个大型服务几天的推理量就相当于一次前沿模型的预训练。

而且:

  • 聊天机器人生成的 token 大多是给人看的,人读得慢,还算”有上限”;
  • 智能体不一样:一个请求会引出长长的内部推理轨迹和工具调用,生成的 token 数可以无限增长;
  • 生成的 token 数 = 花掉的算力。

做推理的公司:提供闭源模型服务的(OpenAI、Anthropic、Google 等),提供开放权重模型服务的(Together、Fireworks、Baseten、DeepInfra、Groq、Cerebras 等)。常用的开源推理引擎:

  • vLLM(伯克利):首创 PagedAttention,流行、默认的好选择;
  • SGLang(伯克利):首创 RadixAttention,适合智能体类负载;
  • TensorRT-LLM(NVIDIA):针对 NVIDIA GPU 高度优化;
  • llama.cpp:纯 C++,支持 CPU,适合在本地运行。

1.3 “快”是什么意思

指标含义对谁重要
首 token 延迟(TTFT)用户发出请求后,要等多久才看到第一个字交互式应用
延迟(秒 / token)对一个请求,token 出现得有多快交互式应用
吞吐量(token / 秒)对很多请求,总共每秒生成多少 token批处理、服务成本

1.4 推理为什么比训练难优化

  • 训练(有监督):所有 token 都已知,可以沿序列并行,Transformer 中都是大矩阵乘法,容易喂饱 GPU;
  • 推理:必须一个一个地生成,第 t+1t+1 个 token 要等第 tt 个生成出来,沿生成方向无法并行,GPU 很难被用满。

2. 从朴素生成到 KV 缓存

2.1 朴素生成的浪费

最朴素的生成方法(第 2、3 讲的 generate 函数就是这样写的):每生成一个 token,都把整个序列(提示词 + 已生成的部分)重新送进模型,只取最后一个位置的输出。一次前向是 O(T2)O(T^2)(注意力),生成 TT 个 token 就是 O(T3)O(T^3)。

2.2 KV 缓存

观察:很多工作可以在不同前缀之间共享。在因果注意力中,前面 token 的 K、V 向量永远不会变,因为它们看不到后面的 token。所以把每一层的 K、V 存在显存里(KV 缓存),每一步只需要:

  1. 为新 token 计算 Q、K、V;
  2. 把新的 K、V 追加到缓存;
  3. 新 token 的 Q 与缓存中所有的 K、V 做注意力。

KV 缓存的大小:对每个序列(BB)、每个 token(SS)、每一层(LL)、每个键值头(KK),存一个 HH 维的 K 向量和一个 HH 维的 V 向量。

于是推理分成两个阶段:

  1. 预填充(prefill):把提示词一次性编码成向量、填满缓存——所有位置并行计算,和训练一样;
  2. 解码(decode / generation):一个一个地生成新 token——每步只处理 1 个新 token。

2.3 亲手实现

03_kv_cache_generation.py 在第 3 讲训练好的 Transformer 上实现了带缓存的前向传播,直接复用它的权重和子模块:

for i, layer in enumerate(model.layers):
    h = layer.ln1(x)
    q, k, v = 投影(h)
    q, k = rope(q, pos), rope(k, pos)             # 注意:用全局位置 start…end-1,不是从 0 开始
    cache.k[i][:, :, start:end] = k               # 追加到缓存
    cache.v[i][:, :, start:end] = v
    out = attention(q, cache.k[i][:, :, :end], cache.v[i][:, :, :end], 因果掩码)
    x = x + output_proj(out)
    x = x + ffn(layer.ln2(x))

正确性:贪心和温度 0.8 采样两种设置下,带缓存与不带缓存生成的 120 个 token 完全相同,每一步 logits 的最大差异约 10−510^{-5}(只来自浮点运算的顺序)。

速度(生成 200 个 token,本机 CPU):

第 1~10 步平均第 191~200 步平均总耗时
不带缓存1.15 ms3.19 ms0.42 s
带 KV 缓存0.79 ms0.79 ms0.15 s

不带缓存时越往后越慢;带缓存时每步耗时基本不变。代价是显存:这个小模型每个 token 的 KV 缓存是 4 KB,而 Llama 2 13B(BF16)是 800 KB / token。

预填充 vs 逐个解码:同样把 200 个 token 的 K、V 算出来存进缓存,一次性预填充只要 2.9 ms,逐个解码要 149 ms,慢 51 倍。预填充把 200 个 token 拼成一个大矩阵乘法,权重只读一次;逐个解码每次只有 1 个 token,权重要读 200 次。下一节会算清楚为什么。


3. 算术强度:为什么解码是内存受限的

3.1 复习:矩阵乘法的算术强度

CS336 课上的方法:逐项统计 FLOPs 和读写的字节数(BF16,每个数 2 字节)。XX(B×DB\times D)乘 WW(D×FD\times F):

步骤读写字节FLOPs
从显存读 XX2BD2BD
从显存读 WW2DF2DF
计算 Y=XWY = XW2BDF2BDF
把 YY 写回显存2BF2BF
算术强度=2BDF2BD+2DF+2BF  ≈  B(B≪D,F)\text{算术强度} = \frac{2BDF}{2BD + 2DF + 2BF} \;\approx\; B \qquad (B \ll D, F)

H100 的临界算术强度是 989×1012/3.35×1012≈295989\times10^{12} / 3.35\times10^{12} \approx 295(第 5 讲)。所以:**B>295B > 295 时计算受限(好),否则内存受限(差)。**极端情况 B=1B = 1(矩阵乘向量):强度为 1,读了 D×FD \times F 的矩阵,却只做了 2DF2DF 次运算。这基本上就是推理时的情况。

3.2 MLP 层

输入 XX(B×T×DB \times T \times D),SwiGLU 的三个矩阵。统计后:

FLOPs=6BTDF,字节=4BTD+4BTF+6DF,强度≈BT\text{FLOPs} = 6BTDF,\qquad \text{字节} = 4BTD + 4BTF + 6DF,\qquad \text{强度} \approx BT
  • 预填充(T=ST = S):通过增大 BTBT(大批次、长序列)很容易做到计算受限;
  • 解码(T=1T = 1):强度 ≈ BB,而 BB 是并发请求的数量,在交互式应用中是不可控的。

3.3 注意力层(用 FlashAttention,不写出注意力矩阵)

SS 是已有的 token 数,TT 是这次要算的新 token 数:

FLOPs=4BSTD,字节=4BSD+4BTD,强度=STS+T\text{FLOPs} = 4BSTD,\qquad \text{字节} = 4BSD + 4BTD,\qquad \text{强度} = \frac{ST}{S+T}
  • 预填充(T=ST = S):强度 =S/2= S/2,很好;
  • 解码(T=1T = 1):强度 =S/(S+1)<1= S/(S+1) < 1,很差。

**关键:BB 被约掉了——批处理帮不了注意力!**为什么?

  • MLP 中,每个序列都用同样的权重(WupW_{\text{up}}、WgateW_{\text{gate}}、WdownW_{\text{down}} 与 BB 无关),批次越大,读一次权重被用的次数越多;
  • 注意力中,每个序列都有自己的 KV 缓存(Q、K、V 都与 BB 有关),批次加倍,要读的数据也加倍。

3.4 小结与实测

MLP 的强度注意力的强度
预填充B⋅SB \cdot SS/2S/2
解码BB(需要大量并发请求)<1< 1(无法改善)

01_arithmetic_intensity.py 在本机(Apple M4 Pro 的 GPU,FP16)上实测解码的一步:

  • 矩阵乘法(BB 个 token 乘同一个 4096×4096 的权重):BB 从 1 增加到 64,token 数是 64 倍,耗时只变成约 2.6 倍,每个 token 的耗时下降了约 25 倍;
  • 注意力(BB 个序列,各看自己的 2048 个历史 token):BB 从 1 到 64,耗时变成约 32 倍,每个序列的耗时只下降了约 2 倍。

**结论:预填充是计算受限的,解码是内存受限的。**解码的每一步都要把全部参数和 KV 缓存从显存读一遍,读出来的每个字节只做一两次运算,GPU 的算力大部分闲着。


4. 延迟与吞吐量的权衡

4.1 一个简单而有用的模型

既然解码是内存受限的,就可以这样估算(假设计算与读取完美重叠,忽略其他开销):

显存=2×参数量⏟BF16 参数+B×每个序列的 KV 缓存,延迟=显存带宽,吞吐量=B延迟\text{显存} = \underbrace{2 \times \text{参数量}}_{\text{BF16 参数}} + B \times \text{每个序列的 KV 缓存},\qquad \text{延迟} = \frac{\text{显存}}{\text{带宽}},\qquad \text{吞吐量} = \frac{B}{\text{延迟}}

每个序列的 KV 缓存 =S×K×H×L×2 (K、V)×2 (BF16)= S \times K \times H \times L \times 2 \,(\text{K、V}) \times 2 \,(\text{BF16})。

4.2 Llama 2 13B 在 H100 上

02_latency_throughput.py 复现了 CS336 课上的例子(上下文 1024,参数 13.0B,每个序列的 KV 缓存 839 MB):

批次总显存延迟 / token吞吐量(token/s)
126.9 GB8.0 ms125
6479.7 GB23.8 ms2,689
256240.8 GB(放不下)71.9 ms3,562

增大批次:

  • 延迟变差:KV 缓存与 BB 成正比,每步要读的数据更多;
  • 吞吐量变好:参数的读取被更多序列分摊;
  • 批次再大就放不下显存了,吞吐量的增长也在递减。

这就是推理的核心权衡:小批次 = 低延迟、低吞吐;大批次 = 高吞吐、高延迟。

另外两点(CS336 课件):

  • 最简单的并行:部署 MM 份模型副本,延迟不变,吞吐量乘以 MM。更难的并行是把模型和 KV 缓存切分到多块卡上(第 7、8 讲的思路);
  • 首 token 延迟基本由预填充的时间决定。预填充时用小批次(让用户早点看到回复),解码时用大批次(提高吞吐)。

5. 抄近路(有损):改模型,让推理更便宜

既然显存读写是瓶颈,最直接的办法就是让要读的数据变少——缩小 KV 缓存、降低数值精度、减少参数。代价是可能损失一些准确率,所以每种方法都要检查精度。

5.1 减小 KV 缓存

GQA / MQA(第 3 讲):NN 个查询头只配 KK 个键值头,每个键值头被 N/KN/K 个查询头共用。MHA:K=NK = N;MQA:K=1K = 1;GQA:介于两者之间。KV 缓存缩小 N/KN/K 倍。

把 Llama 2 13B 的键值头从 40 个改成 8 个(02_latency_throughput.py 演示 2):

配置批次总显存延迟吞吐量
MHA(K = 40)6479.7 GB23.8 ms2,689
GQA(K = 8)6433.4 GB10.0 ms6,417
GQA(K = 8)25665.6 GB19.6 ms13,068

每个序列的 KV 缓存从 839 MB 降到 168 MB:同样的批次,延迟更低、吞吐量更高;而且批次 256 现在放得下了。GQA 论文(Ainslie et al. 2023)也验证了准确率几乎没有下降。

MLA(多头潜在注意力,DeepSeek-V2,第 4 讲):不存每个头的 K、V,而是存一个压缩向量 c=Wchc = W_c h,需要时再投影回 K=WKcK = W_K c、V=WVcV = W_V c。DeepSeek-V2 把 N⋅HN \cdot H = 16384 压缩到 512 维。麻烦是 MLA 与 RoPE 不兼容,要额外加 64 维给 RoPE,共 576 维。据 DeepSeek-V2 论文,MHA 比 GQA 准确(但更贵),而 MLA 比 MHA 还好一点(而且便宜得多)。

跨层注意力(CLA):GQA 在”头”之间共享 KV,CLA 在”层”之间共享 KV。实验表明它改善了”准确率 - KV 缓存大小”的帕累托前沿。

局部(滑动窗口)注意力(Longformer、Sparse Transformer、Mistral 7B):只看最近的 WW 个 token。有效上下文随层数线性增长(每层往回看 WW),KV 缓存与序列长度无关。问题是可能损失准确率,解决办法是局部层与全局层交替(混合架构)。

更激进的设计:据 CS336 2026 年的课件,DeepSeek V4 支持 100 万 token 的上下文,把几种压缩结合起来:压缩稀疏注意力(每 mm 个 token 压缩成 1 个)、DeepSeek 稀疏注意力(只选最相关的 top-k 个)、以及压缩得更狠的注意力。此外还有第 4 讲的线性注意力 / 状态空间模型(Mamba-2、Gated DeltaNet,状态大小固定),甚至扩散模型等思路。

KV 缓存计算器(02_latency_throughput.py 演示 3,一个 131,072 token 的序列,BF16):

模型每 token一个 128K 序列
Llama 2 13B(MHA)800 KB107.4 GB
Llama 3 70B(GQA,8 个 KV 头)320 KB42.9 GB
Llama 3 405B(GQA,8 个 KV 头)504 KB67.6 GB
DeepSeek-V3(MLA,512 + 64 维)69 KB9.2 GB
Mistral 7B(GQA + 4096 滑动窗口)128 KB0.5 GB(只存最近 4096 个)

长上下文推理的瓶颈往往是 KV 缓存,而不是参数。

5.2 量化

**核心想法:降低数值精度。**数据少了,延迟和吞吐量都会变好(因为内存受限)。当然要担心准确率。

基本操作(CS336 课上的例子):x=5.2342x = 5.2342,缩放因子 0.1,零点 4:

q=round(x/0.1)+4=56,x^=(q−4)×0.1=5.2q = \mathrm{round}(x / 0.1) + 4 = 56,\qquad \hat x = (q - 4) \times 0.1 = 5.2

常见格式:FP32(4 字节,训练的主权重和优化器状态需要);BF16(2 字节,推理的默认);FP8(1 字节,E4M3 最大约 ±448,H100 起有硬件支持,“敢的话可以用来训练”);INT8(1 字节,[−128, 127],比 FP8 便宜但精度略差,一般只用于推理);INT4(0.5 字节,[−8, 7],更省、更不准)。

两种做法:

  • 量化感知训练(QAT):训练时在前向传播中”量化再反量化”,模拟量化误差,让权重学会适应。效果好,但需要昂贵的大规模训练;
  • 训练后量化(PTQ):训练完再做,便宜得多。用一小批样本数据确定每层 / 每个张量的缩放因子和零点。

粒度(04_quantization.py 演示 2):多少个数共用一个缩放因子?在一个带 20 个离群值的 512×512 矩阵上做 INT4:

方案INT4 相对误差每个权重的平均比特
整个张量一个缩放因子0.9474.00
每个输出通道一个0.2014.03
每 128 个数一组0.1364.12
每 32 个数一组0.1004.50

一个离群值会把它所在那一组的缩放因子撑大,组里其他数就只能用很少几个档位表示。粒度越细越准,代价是多存缩放因子。

GPTQ(Frantar et al. 2022):用二阶信息(海森矩阵)逐列量化,并更新还没量化的权重来补偿已经产生的量化误差。

AWQ(激活感知的权重量化,Lin et al. 2023):

  • 观察:有些激活通道特别大,与它们相乘的权重更重要;
  • 只需要”保护”约 0.1%~1% 的权重;
  • 做法:把这些重要的权重乘以一个放大系数 ss 再量化,同时把对应的激活除以 ss(输出不变),重要权重的相对量化误差就变小了;
  • 论文报告:FP16 → INT3,显存省 4 倍,速度快 3.2 倍。

04_quantization.py 演示 3 在合成数据上验证(1% 的激活通道放大 20 倍):INT4 的输出相对误差从 0.118(直接取整)降到 0.076(AWQ),INT3 从 0.273 降到 0.177。背景:LLM.int8() 的作者发现,模型大到约 67 亿参数以上时,激活中会系统性地出现幅度极大的离群通道——这是大模型量化比小模型难的原因之一。

量化一个真实的(小)模型(演示 4,第 3 讲的 Transformer,只量化线性层权重):

方案每权重比特验证损失(比特/字节)上升
FP32320.2310—
INT8,每个输出通道80.23100%
INT4,整个张量40.23903.5%
INT4,每个输出通道40.23562.0%
INT4,每 32 个一组4.50.23280.8%
INT3,每 32 个一组3.50.24948.0%
INT2,每 32 个一组2.52.7749模型坏了

INT8 几乎无损;INT4 分组量化只上升约 1%;INT3 开始明显变差;INT2 把模型”量坏”了。AWQ 在这个小模型上几乎没有帮助(INT4:0.2342 vs 0.2328;INT3:0.2478 vs 0.2494):它的激活没有明显的离群通道(最大通道与中位通道之比只有 1.3~5.8),没什么需要保护的。这与合成数据的结果并不矛盾:AWQ 针对的正是大模型中系统性出现的离群通道。

**【边界】**这里是”模拟量化”(量化后立即反量化再计算),只衡量精度。真正的加速需要专门的核函数直接读取低比特的权重。这里只量化了权重(W4A16);把激活也量化(W8A8)能用上 INT8 矩阵乘法的算力,但更难;KV 缓存也可以量化。

5.3 剪枝与蒸馏

核心想法:直接把一个昂贵模型的一部分”撕掉”,让它变便宜,然后再修好。

NVIDIA 的 Minitron(Muralidharan et al. 2024)的流程:

  1. 在一个很小的校准数据集(1024 个样本)上,估计每一层、每个注意力头、每个隐藏维度的重要性;
  2. 去掉不重要的部分,得到一个更小的模型;
  3. 用原模型蒸馏(让小模型学习大模型的输出分布)修复它。

论文报告:用这种方法从 150 亿参数的模型得到 80 亿、40 亿参数的模型,每个模型需要的训练 token 比从头训练少最多约 40 倍,效果与同规模的其他开源模型相当。

CS336 课上总结了两种”让推理更便宜”的配方:

  • 从头训练:定义更快的模型架构 → 训练它;
  • 蒸馏:定义更快的模型架构 → 用原模型的权重初始化(即使架构不同)→ 蒸馏修复。

6. 抄近路但仔细检查(无损):推测解码

6.1 利用”检查比生成快”

回忆推理的两个阶段:

  • 预填充:给定一个序列,并行地编码所有 token(计算受限)。注意,这一步顺便给出了每个位置的概率;
  • 解码:一次生成一个 token(内存受限)。

换句话说:**检查比生成快。**让目标模型”检查” kk 个 token(一次前向,像预填充),耗时几乎和”生成” 1 个 token 一样。

推测采样(speculative sampling,Leviathan et al. 2023;Chen et al. 2023):

  1. 用便宜的草稿模型 pp 一口气猜 kk 个 token(例如 4 个);
  2. 用目标模型 qq 做一次前向传播,同时得到这 kk 个位置(外加 1 个)的概率;
  3. 从左到右逐个检查:草稿 token xx 以概率 min⁡(1,q(x)/p(x))\min(1, q(x)/p(x)) 被接受;一旦某个被拒绝,就从残差分布 norm(max⁡(q−p,0))\mathrm{norm}(\max(q - p, 0)) 中重新采样一个 token,丢掉后面的草稿,本轮结束;
  4. 如果 kk 个全部接受,再从目标模型的第 k+1k+1 个位置”白送”一个 token。

6.2 为什么输出分布精确等于目标分布

这是一种修改过的拒绝采样(提议分布 pp,目标分布 qq)。修改之处:被拒绝时不重新循环,而是直接从残差分布抽一个,保证每轮至少产出一个 token。

证明:输出 token =y= y 有两条途径:

  • 草稿抽到 yy 并被接受:p(y)⋅min⁡(1,q(y)/p(y))=min⁡(p(y),q(y))p(y) \cdot \min(1, q(y)/p(y)) = \min(p(y), q(y));
  • 草稿被拒绝(总概率 β=1−∑xmin⁡(p(x),q(x))\beta = 1 - \sum_x \min(p(x), q(x))),然后从残差分布抽到 yy:β⋅max⁡(q(y)−p(y),0)∑zmax⁡(q(z)−p(z),0)=max⁡(q(y)−p(y),0)\beta \cdot \frac{\max(q(y) - p(y), 0)}{\sum_z \max(q(z) - p(z), 0)} = \max(q(y) - p(y), 0)(因为分母恰好等于 β\beta)。

两者相加:min⁡(p,q)+max⁡(q−p,0)=q(y)\min(p, q) + \max(q - p, 0) = q(y)。✓

CS336 课上的两 token 例子:词表 {A,B}\{A, B\},草稿高估了 AA(p(A)>q(A)p(A) > q(A),于是 p(B)<q(B)p(B) < q(B)),残差分布是 [0,1][0, 1]:

P[输出 A]=p(A)⋅q(A)p(A)+p(B)⋅1⋅0=q(A),P[输出 B]=p(B)⋅1+p(A)(1−q(A)p(A))⋅1=q(B)P[\text{输出 } A] = p(A)\cdot\frac{q(A)}{p(A)} + p(B)\cdot 1\cdot 0 = q(A),\qquad P[\text{输出 } B] = p(B)\cdot 1 + p(A)\left(1 - \frac{q(A)}{p(A)}\right)\cdot 1 = q(B)

关键性质:它保证是来自目标模型的精确样本——草稿只影响”快不快”,不影响”对不对”。

05_speculative_decoding.py 验证:

  • 小词表(草稿故意猜得很离谱,pp 与 qq 顺序相反):公式算出的输出分布与 qq 完全相同,抽 10 万次的频率也与 qq 一致;
  • 真实模型(目标:第 3 讲的 Transformer;草稿:第 2 讲只看 8 个字节的 MLP,小 17 倍):输出分布与目标分布的最大差异 3×10−83\times10^{-8};抽 2 万次,推测采样的频率与 qq 的总变差距离是 0.005,比”直接从 qq 抽样”的纯抽样噪声(0.011)还小。

6.3 能快多少?

在真实模型上生成 5 个故事(各 200 个 token,kk = 4):

  • 草稿接受率 α\alpha = 0.84;
  • 每次调用目标模型平均得到 3.56 个 token(普通解码是 1 个);理论公式(假设每个位置独立地以概率 α\alpha 接受)1−αk+11−α=3.62\frac{1 - \alpha^{k+1}}{1 - \alpha} = 3.62。

被拒绝的位置在哪里?文件用方括号标出了它们:

Once upon a time, there was a sma[l]l m[o][u][s]e named [D]amira. [D]a[m]i[r]a loved to swim in the park.
One day, [D]a[m]i[r][a] [f]ound a shiny stone. Then [D]a[m]i[r][a] [m][e]t a duck named [V]inu. ...

几乎都在名字上。草稿只看最近 8 个字节,看不到几十个字节之前出现过的名字,只能瞎猜;而模板文字它猜得很准。草稿越接近目标模型,接受率越高、加速越多。

加速比估算:设草稿每猜一个 token 的成本是目标模型的 cc 倍(这里 cc ≈ 参数量之比 0.058),目标模型检查 k+1k+1 个 token ≈ 生成 1 个 token 的成本:

加速比=(1−αk+1)/(1−α)k c+1\text{加速比} = \frac{(1 - \alpha^{k+1})/(1 - \alpha)}{k\,c + 1}
kk1246812
加速比1.742.282.943.253.353.27

kk 太小,一轮得不到几个 token;kk 太大,后面的草稿大多会被拒绝,还要付草稿的成本。

实践中(CS336 课上的例子):目标 700 亿参数配 80 亿的草稿,或目标 80 亿配 10 亿的草稿;草稿通常从目标模型蒸馏得到。改进草稿的方向:

  • Medusa:在目标模型上加几个预测头,并行地猜后面几个 token;
  • EAGLE:草稿直接使用目标模型的高层特征来猜。

CS336 的总结:推测采样利用了”检查与生成”的不对称,多亏了数学,输出是目标模型的精确样本;草稿模型还有很大的创新空间(通常需要训练)。


7. 动态负载:连续批处理与 PagedAttention

7.1 真实服务的”乱”

训练时,数据是整整齐齐的一块(批次 × 序列长度)。真实服务中(CS336 课件):

  1. 请求在不同的时间到达(为了凑批而让早到的请求等待,很糟糕);
  2. 序列有共同的前缀(系统提示词、对同一个问题生成多个样本);
  3. 序列长度各不相同(按最长的填充,很浪费)。

7.2 连续批处理

问题:训练得到的是整齐的一块 token,推理中请求在不同时间到达和结束,是一个”参差不齐”的数组。

解决办法:按迭代调度(Orca,Yu et al. 2022):

  • 一步一步地解码;
  • 新请求一到就加入批次,不必等当前批次全部生成完;已经结束的序列立即离开。

另一个问题:批处理只对形状相同的序列有效?每个请求长度都不同。

选择性批处理:注意力部分,每个序列单独计算(形状 [3,H][3, H]、[9,H][9, H]、[5,H][5, H]……);非注意力部分(MLP 等逐 token 的运算),把所有序列拼接成 [3+9+5,H][3 + 9 + 5, H] 一起算。不需要填充。

06_batching_and_paging.py 模拟了 400 个请求(最大批次 32,输出长度 10~300 随机):

到达率(个/步)方式吞吐(token/步)平均延迟(步)
0.05静态7.3441
0.05连续7.5147
0.2静态14.71034
0.2连续26.2161

请求来得慢时,吞吐量受限于到达速度,两种方式差不多,但连续批处理的延迟低得多;请求来得快时,静态批处理跟不上,请求越积越多,延迟暴涨,而连续批处理的吞吐量高得多。

7.3 PagedAttention

以前的做法:请求一来,就为它的提示词和回答预留一段连续的 KV 缓存空间(按最大长度)。

问题:碎片(就像硬盘的碎片):

  • 实际生成的 token 可能远少于最大长度——预留的空间大部分是空的(内部碎片);
  • 不同大小的预留区之间还会留下用不上的空隙(外部碎片)。

解决办法:PagedAttention(Kwon et al. 2023,vLLM 的论文;回忆一下操作系统):把每个序列的 KV 缓存切成不连续的块(例如每块 16 个 token),按需分配,用一张”块表”记录每个逻辑块在哪个物理块里。

06_batching_and_paging.py 演示 2(显存能存 16384 个 token 的 KV;请求实际长度 50~600,平均 328;最大允许 1024):

方式同时容纳的请求真正用到的槽位
按最大长度连续预分配1633%
分页(每块 16 个 token)4996%

同样的显存,能同时服务约 3 倍的请求,批次更大,吞吐量更高(第 4 节)。

共享前缀:多个序列可以共享同样的 KV 缓存块:

  • 共享系统提示词;
  • 对同一个提示词采样多个回答(例如程序生成、强化学习)。

实现方法:在块的层面共享前缀,并用写时复制(copy-on-write):物理块带引用计数,要往一个被共享的块里写时,先复制一份给自己。演示 3(提示词 100 个 token,4 个回答各 60 个 token,每块 16 个 token):不共享要 40 个块,共享前缀只要 22 个块(省 45%),写时复制恰好发生 3 次——提示词的第 7 个块只装了 4 个 token,前 3 个回答往里写时各复制一次,第 4 个回答写时它已经不再被共享。

vLLM 的其他优化:把”按块表读取”和注意力融合成一个核函数(减少核函数启动开销);使用最新的核函数(FlashAttention、FlashDecoding);用 CUDA Graph 避免核函数启动开销。SGLang 的 RadixAttention 更进一步:用一棵前缀树缓存所有见过的前缀,新请求自动复用最长的公共前缀。

CS336 的总结:借用操作系统的想法(分页),让显存适应动态的负载。


8. 本讲小结

  1. 推理很重要:实际使用、评估、强化学习都要推理;训练是一次性的,推理是重复的。指标:TTFT、延迟、吞吐量。
  2. KV 缓存:前面 token 的 K、V 不会变,存下来复用。推理分预填充(并行)和解码(逐个)。
  3. 算术强度:矩阵乘法 / MLP ≈ B⋅TB \cdot T,注意力 =ST/(S+T)= ST/(S+T)。预填充计算受限,解码内存受限;批处理能帮 MLP,帮不了注意力。
  4. 延迟 = 显存 / 带宽,吞吐量 = 批次 / 延迟:大批次提高吞吐、恶化延迟,还受显存限制。
  5. 有损的捷径:缩小 KV 缓存(GQA、MLA、CLA、局部注意力、混合架构);量化(粒度、GPTQ、AWQ;INT8 几乎无损、INT4 分组量化损失很小);剪枝 + 蒸馏。
  6. 无损的捷径:推测解码利用”检查比生成快”,输出是目标模型的精确样本;加速取决于草稿的接受率和成本。
  7. 动态负载:连续批处理(按迭代调度、选择性批处理);PagedAttention(分页、前缀共享、写时复制)。
  8. 新架构潜力巨大:推理效率的很大一部分取决于模型本身(KV 缓存的大小、是否需要逐 token 的注意力),这又回到了第 3、4 讲的架构设计。

9. 与作业的关系

  • 作业 5(对齐)的强化学习部分需要对同一个问题采样很多回答,通常会用 vLLM 做生成——本讲的 KV 缓存、连续批处理、前缀共享都在它背后工作。理解这些能帮你判断:采样为什么是训练循环中最慢的一环,批次和采样数怎么设置更划算。
  • 作业 1、2 中你写的 generate 函数(没有 KV 缓存)可以用本讲 03_kv_cache_generation.py 的方法加速。
  • 资源核算类的问题常常会问:给定模型和 GPU,最大批次是多少、延迟和吞吐量是多少——就是第 4 节的模型。

10. 练习

练习 1(KV 缓存大小):Llama 3 8B:32 层,32 个查询头,8 个键值头,每头 128 维,BF16。(a) 每个 token 的 KV 缓存多大?(b) 上下文 8192、批次 32 时,KV 缓存共多大?(c) 如果改成 MHA 呢?

参考答案

(a) 32×8×128×2×232 \times 8 \times 128 \times 2 \times 2 = 131,072 字节 = 128 KB。(b) 128 KB×8192×32128\text{ KB} \times 8192 \times 32 ≈ 34.4 GB(与 16 GB 的 BF16 参数相比更大)。(c) MHA 有 32 个键值头,是 GQA 的 4 倍,约 137 GB,一块 80 GB 的卡放不下。

练习 2(算术强度):解码时,MLP 的强度约为 BB。H100 的临界点是 295。(a) 至少要多少个并发请求,MLP 才能计算受限?(b) 为什么注意力无论如何都做不到?(c) 这对”一块 GPU 同时服务多少用户”意味着什么?

参考答案

(a) 约 300 个(实际还要算上 BB 不远小于 DD、FF 时的修正)。(b) 注意力的强度 S/(S+1)<1S/(S+1) < 1,与 BB 无关,因为每个序列都要读自己的 KV 缓存。(c) 并发越多,MLP 越高效;但 KV 缓存与并发数成正比地增长,很快会占满显存,而且注意力的读取时间也随之线性增长。所以要缩小 KV 缓存(GQA、MLA、量化),才能容纳更多并发。

练习 3(延迟与吞吐):用第 4 节的模型,Llama 2 13B(GQA,K = 8)在一块 H100 上,(a) 最多能放下多大的批次(显存 80 GB)?(b) 这时延迟和吞吐量是多少?

参考答案

(a) 参数 22.7 GB,每个序列 168 MB,(80−22.7)/0.168≈341(80 - 22.7)/0.168 \approx 341。(b) 显存约 80 GB,延迟 ≈ 80 GB / 3.35 TB/s ≈ 23.9 ms,吞吐量 ≈ 341 / 0.0239 ≈ 14,300 token/s。(实际上批次这么大时计算可能成为瓶颈,而且还要留显存给激活值和碎片。)

练习 4(量化粒度):一个 4096×4096 的权重矩阵用 INT4、每 128 个数一组、缩放因子用 FP16。(a) 平均每个权重多少比特?(b) 整个矩阵占多少字节?与 BF16 相比省了多少?

参考答案

(a) 4+16/128=4.1254 + 16/128 = 4.125 比特。(b) 40962×4.125/84096^2 \times 4.125 / 8 ≈ 8.65 MB;BF16 是 33.6 MB,省了约 74%。

练习 5(推测采样):词表 {A,B,C}\{A, B, C\},目标 q=(0.5,0.3,0.2)q = (0.5, 0.3, 0.2),草稿 p=(0.2,0.2,0.6)p = (0.2, 0.2, 0.6)。(a) 每步接受草稿的概率是多少?(b) 残差分布是什么?(c) 验证输出分布等于 qq。

参考答案

(a) ∑min⁡(p,q)=0.2+0.2+0.2=0.6\sum \min(p, q) = 0.2 + 0.2 + 0.2 = 0.6。(b) max⁡(q−p,0)=(0.3,0.1,0)\max(q - p, 0) = (0.3, 0.1, 0),归一化为 (0.75,0.25,0)(0.75, 0.25, 0)。(c) 输出 AA:0.2+0.4×0.75=0.50.2 + 0.4 \times 0.75 = 0.5;BB:0.2+0.4×0.25=0.30.2 + 0.4 \times 0.25 = 0.3;CC:0.2+0=0.20.2 + 0 = 0.2。✓

练习 6(最佳猜测长度):接受率 α\alpha = 0.7,草稿成本 cc = 0.1。用 6.3 节的公式算出 kk = 1~6 的加速比,最佳的 kk 是多少?

参考答案

期望 token 数 (1−0.7k+1)/0.3(1 - 0.7^{k+1})/0.3:kk=1:1.70,2:2.19,3:2.53,4:2.77,5:2.94,6:3.06。每轮耗时 0.1k+10.1k + 1:1.1、1.2、1.3、1.4、1.5、1.6。加速比:1.55、1.83、1.95、1.98、1.96、1.91。最佳 kk ≈ 4,约 2 倍。接受率越低,最佳的 kk 越小。

练习 7(为什么批处理对推测解码不太友好)★:在大批次服务中,推测解码的收益通常会变小。结合第 3、4 节,说说为什么。

参考答案

推测解码的前提是”解码内存受限,检查 k+1k+1 个 token 和生成 1 个几乎一样快”。批次很大时,MLP 已经接近计算受限,检查 k+1k+1 个 token 意味着 k+1k+1 倍的计算,不再”免费”;而且被拒绝的草稿 token 浪费了真实的算力。所以推测解码在小批次、低延迟的场景下收益最大,在高吞吐的离线批处理中收益有限。

练习 8(动手): (a) 在 03_kv_cache_generation.py 中实现 GQA 式的 KV 缓存(只存 KK 个头),并计算缓存大小的变化。 (b) 在 05_speculative_decoding.py 中换一个更好的草稿(例如 1 层的小 Transformer),接受率会提高多少?被拒绝的位置还集中在名字上吗? (c) 在 06_batching_and_paging.py 的连续批处理中加入显存限制,比较连续预分配与分页的吞吐量。


11. 延伸阅读

  • CS336 Spring 2026 Lecture 10(stanford-cs336/lectures 中的 lecture_10.py)。
  • Google,How to Scale Your Model(Scaling Book)的 Transformers 与 Inference 两章:本讲的记号与分析方法来源于此。
  • Pope et al.,Efficiently Scaling Transformer Inference(2022):推理的延迟、吞吐量与切分。
  • Ainslie et al.,GQA(2023);DeepSeek-AI,DeepSeek-V2(2024,MLA);Brandon et al.,Reducing Transformer Key-Value Cache Size with Cross-Layer Attention(2024)。
  • Dettmers et al.,LLM.int8()(2022);Frantar et al.,GPTQ(2022);Lin et al.,AWQ(2023)。
  • Muralidharan et al.,Compact Language Models via Pruning and Knowledge Distillation(2024,Minitron)。
  • Leviathan et al.,Fast Inference from Transformers via Speculative Decoding(2023);Chen et al.,Accelerating Large Language Model Decoding with Speculative Sampling(2023);Cai et al.,Medusa(2024);Li et al.,EAGLE(2024)。
  • Yu et al.,Orca: A Distributed Serving System for Transformer-Based Generative Models(OSDI 2022);Kwon et al.,Efficient Memory Management for Large Language Model Serving with PagedAttention(SOSP 2023,vLLM);Zheng et al.,SGLang(2024)。

12. 下一讲预告

第 9 讲说明了怎么用小模型的实验预测”训练多大的模型、用多少数据”。但还有一个问题没有解决:超参数。在小模型上调好的学习率、批次大小、初始化,能直接用到大模型上吗?

下一讲(规模定律 II)会回答:

  • 为什么简单地把模型变宽,最优学习率就会漂移?
  • μP(最大更新参数化)怎样让”小模型上调好的超参数”直接迁移到大模型?
  • 学习率调度:为什么越来越多的模型用 WSD(预热 - 稳定 - 衰减)代替余弦调度?它和规模定律实验有什么关系?
  • 临界批量大小在实践中怎么用?Cerebras-GPT、MiniCPM、DeepSeek、Llama 3 等模型的规模实验是怎么做的?

下一讲之前,建议你:跑通 code/lecture10/ 下的 6 个脚本,读懂 forward_with_cache 和 speculative_generate,完成练习 1、2、5、6。

← 返回课程目录