第 19 讲:高效推理方法

Large Language Models: Methods and Applications · Lecture 19。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

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

本篇目录

对应原课:CMU 11-667 Fall 2025 · Lecture 19 · Tue 11-Nov-2025 · Efficient Inference Methods 原课 slides:Efficient Inference 阅读材料:Leviathan et al. (2023), Fast Inference from Transformers via Speculative Decoding;Dao et al. (2022), FlashAttention;Kwon et al. (2023), Efficient Memory Management for Large Language Model Serving with PagedAttention(vLLM) 配套代码:code/lecture19/(1 个模块 + 5 个演示脚本) 本讲的写法:先弄清”生成一个 token 为什么慢”(prefill 与 decode、内存带宽),再看四类方法:

  • KV 缓存与 FlashAttention:不重复计算、少读写显存;
  • PagedAttention:少浪费显存;
  • 量化:少读字节;
  • 推测解码:一次检查多个 token。

每个实验都检查”加速有没有改变输出”:哪些是精确的,哪些是近似的。GPU kernel 和服务系统的实现细节见本仓库的 CMU11-868。所有数字都来自配套代码的实际输出(计时数字取决于机器)。


目录


0. 本讲导读

0.1 这一讲要回答的问题

训练一个模型是一次性的开销;推理是每天、每个用户、每个请求都要付的开销。一个被广泛使用的模型,推理的总算力很快就会超过训练(第 12 讲练习 6、第 15 讲练习 8)。所以推理效率直接决定了成本、延迟和能耗。这一讲要回答:

  • 生成一个 token 的瓶颈在哪里?为什么 GPU 的算力在 decode 时大部分闲着?
  • 哪些加速是精确的(输出完全不变),哪些是近似的(有质量损失)?
  • 推测解码怎样做到”更快,而且输出分布严格不变”?

0.2 学完这一讲你应该能

  • 区分 prefill 和 decode,用算术强度解释为什么 decode 受内存带宽限制、批处理为什么有用;
  • 计算 KV 缓存的大小,说出减小它的办法;
  • 推导在线 softmax,解释 FlashAttention 为什么精确、省在哪里;
  • 解释连续批处理和分页 KV 缓存解决的问题;
  • 比较权重量化和激活量化,解释离群通道和 LLM.int8() / SmoothQuant;
  • 证明推测解码的输出分布等于目标模型的分布,计算期望的加速。

0.3 实验平台

infer_world.py:

  • 目标模型:d = 128、4 层,806,144 个参数,测试 NLL 1.348。
  • 草稿模型:d = 32、1 层,15,968 个参数。训练 6 遍的”好草稿” NLL 1.363,训练 1 遍的”差草稿” NLL 1.799。
  • 从零实现:带 KV 缓存的解码器、分块注意力、量化、推测解码。

1. 生成一个 token 为什么慢

1.1 自回归生成的两个阶段

用户发来一个提示(例如 500 个 token),模型生成一个回答(例如 300 个 token):

  1. Prefill(预填充):把整段提示一次送进模型,算出每一层的 key / value(存进 KV 缓存,§2)和第一个输出 token 的分布。所有提示 token 并行处理:矩阵 × 矩阵。
  2. Decode(解码):每一步只处理一个新 token,生成下一个。300 个 token 就要 300 次前向,一次接一次,不能并行:矩阵 × 向量。

用户感受到的两个延迟指标:

  • TTFT(time to first token):主要由 prefill 决定;
  • TPOT(time per output token):由 decode 决定。

服务方还关心吞吐量(每秒总共生成多少 token)。

1.2 算术强度与”内存墙”

一个 N 参数的模型,每处理一个 token 大约要 2N 次浮点运算(每个参数一次乘法、一次加法)。decode 的一步:

  • 计算:2N·B 次运算(B 是批里的序列数);
  • 读内存:每个权重都要从显存读一遍,2N 字节(fp16),和 B 无关。

算术强度 = 运算次数 / 读写字节数 ≈ B。

硬件的”平衡点”:A100 的算力约 312 TFLOPS,显存带宽约 2 TB/s,比值约 156 FLOP/字节。算术强度低于这个值时,算力在等数据——受内存带宽限制(memory-bound)。

  • batch = 1 的 decode,强度约 1,GPU 的算力 99% 以上闲着(练习 1)。
  • prefill 一次处理几百个 token,强度高,受算力限制(compute-bound)。

1.3 实验:prefill vs decode,以及批处理

代码 01 §2(CPU 上的计时,数字取决于机器):

  • 处理同样的 256 个 token:prefill 一次前向 3.5 ms(约 73,000 token/s);decode 逐个处理 95.2 ms(约 2,700 token/s),慢了约 27 倍。
  • decode 的批处理(每个序列的缓存长度 64):
批大小 B每步用时吞吐量(token/s)
10.34 ms2,909
40.47 ms8,558
161.12 ms14,315
642.38 ms26,873
25615.07 ms16,993
  • 批不太大时,每步的时间增长得远比 B 慢:读一遍权重,可以给 B 个序列都用上。推理服务因此要把很多用户的请求拼成一批。
  • 批大到一定程度,计算本身(以及注意力读 KV 缓存)成为瓶颈,吞吐量不再增长、甚至下降。这台 CPU 上 B = 256 反而比 B = 64 慢;GPU 上的拐点通常在更大的批。

延迟和吞吐量的权衡:批越大,吞吐量越高(每个 token 越便宜),但每个用户的 TPOT 变长。推理服务要在两者之间按产品需求取舍。


2. KV 缓存

2.1 不重复计算

因果注意力里,位置 t 的 key 和 value 只依赖 t 及之前的 token。生成第 t+1 个 token 时,前面所有位置的 k、v 和上一步完全一样,没有理由重算。

  • KV 缓存:每一层把算过的 k、v 存下来,新 token 只算自己的 q、k、v,再和缓存里的全部 k、v 做注意力。
  • 计算量:
    • 不用缓存时,第 t 步要重算整个前缀:生成 n 个 token 的总计算量 ∝ n²·(模型大小)。
    • 用缓存时,每步的线性层计算量和 t 无关,只有注意力读缓存的部分 ∝ t:总计算量 ∝ n·(模型大小) + n²·d。

2.2 实验:正确性与加速

代码 01 §1:

  • 正确性:同一段 50 个 token,“一次完整前向” vs “先 prefill 20 个、再逐个 step 30 个”,logits 的最大差别 4.3e-6(浮点误差)。
  • 速度:
生成 token 数不用缓存用 KV 缓存加速采样结果相同
640.04 s0.02 s1.7×✓
1280.11 s0.04 s2.6×✓
2560.40 s0.10 s4.1×✓
5122.16 s0.28 s7.7×✓

生成长度翻倍,不用缓存的时间翻四倍以上;用缓存的时间大约翻倍。KV 缓存是精确的:用同样的随机数,采样出的 token 完全相同。

2.3 KV 缓存有多大

每个 token 的 KV 缓存 = 2(k 和 v)× 层数 × KV 头数 × 每头维度 × 每个数的字节数。代码 01 §3(fp16):

模型每 token4K 上下文128K 上下文权重
Llama-2-7B(MHA,32 个 KV 头)512 KB2.0 GB64.0 GB13 GB
Llama-2-70B(GQA,8 个 KV 头)320 KB1.25 GB40.0 GB130 GB
Llama-3-8B(GQA,8 个 KV 头)128 KB0.5 GB16.0 GB15 GB

一个 128K 上下文的请求,Llama-2-7B 式的 KV 缓存要 64 GB,比权重大好几倍。批里有很多请求时,KV 缓存决定了一张卡能同时服务多少请求。减小它的办法:

  • GQA / MQA(第 4 讲 §3):多个 query 头共享一组 KV 头。Llama-3-8B 的 KV 缓存只有 Llama-2-7B 的 1/4。
  • KV 缓存量化:存成 8 比特甚至 4 比特(§5)。
  • 滑动窗口 / 稀疏注意力:只保留最近的或重要的 token(第 21 讲)。
  • 分页管理:减少浪费(§4)。

3. FlashAttention:分块与在线 softmax

3.1 注意力慢在哪里

标准的注意力实现:

  1. S=QK⊤/dS = QK^\top/\sqrt{d},写进显存;
  2. P=softmax(S)P = \mathrm{softmax}(S),读 S、写 P;
  3. O=PVO = PV,读 P。

T × T 的矩阵 S、P 要在显存里来回读写好几次。GPU 有两种内存:

  • 显存(HBM):几十 GB,约 2 TB/s;
  • 片上 SRAM:每个计算单元约 192 KB,比显存快一个数量级。

注意力的计算量不大,慢在读写显存;而且 T = 32K 时,一个头的 S 就有 10 亿个数,根本放不下。

FlashAttention(Dao et al. 2022)的想法:

  • 把 Q、K、V 分块读进 SRAM,在 SRAM 里算完一块的注意力,直接累加到输出上,从不把 S、P 写回显存。
  • 难点在于 softmax 的分母要看完整行的分数才知道。

3.2 在线 softmax

数值稳定的 softmax 要先减去最大值:softmax(x)i=exi−m/∑jexj−m\mathrm{softmax}(x)_i = e^{x_i - m}/\sum_j e^{x_j - m},m=max⁡jxjm = \max_j x_j。代码 02 §1:

  • 分数 [1000, 999, 998, 10] 直接算 exp 会溢出成 inf,得到 nan;
  • 减去最大值后得到 [0.665, 0.245, 0.090, 0.000]。

但”先求最大值、再求和”要扫两遍。在线 softmax(Milakov & Gimelshein 2018)只扫一遍,逐块维护”目前的最大值 m”和”目前的和 l”:

m′=max⁡(m,max⁡(新块)),l′=l⋅em−m′+∑x∈新块ex−m′m' = \max(m, \max(\text{新块})), \qquad l' = l \cdot e^{m - m'} + \sum_{x \in \text{新块}} e^{x - m'}

最大值变大时,之前算的指数统一乘以 em−m′e^{m-m'} 缩小,等价于从一开始就用新的最大值。上面的例子分两块处理,得到 m = 1000、l = 1.503215,和一次性算的完全相同(练习 3)。

分块注意力:对每个 query 维护 (m, l, acc),每读入一块 K、V:

s=qK块⊤/d,m′=max⁡(m,max⁡s),acc←acc⋅em−m′+es−m′V块,l←l⋅em−m′+∑es−m′s = qK_{\text{块}}^\top/\sqrt d,\quad m' = \max(m, \max s),\quad acc \leftarrow acc\cdot e^{m-m'} + e^{s - m'}V_{\text{块}},\quad l \leftarrow l\cdot e^{m-m'} + \textstyle\sum e^{s-m'}

最后输出 acc / l。

3.3 实验:分块注意力 = 标准注意力

代码 02 §2:

  • 随机数据:Q、K、V 随机(T = 256,d = 64,因果掩码),块大小 1、16、64、256 时,和标准注意力的最大差别都在 1e-14 量级。任何时候只需要存 T × 块大小 的分数。
  • 真实模型:目标模型第 1 层、4 个头、一篇 52 个 token 的测试文档,分块注意力和标准注意力的最大差别 1.3e-15。

FlashAttention 是精确的:它改变的是计算的顺序和中间结果存在哪里,不是近似。

3.4 省在哪里

代码 02 §3 用一个简化的模型数读写显存的元素个数(一个头,d = 128,SRAM 放得下的块大小 B = 128):

  • 标准:≈ 4Td + 4T²;
  • 分块(FlashAttention-2 的顺序):每次把 B 行 Q 留在 SRAM,依次读入全部 K、V 块,≈ 2Td·(T/B) + 2Td。
T标准(百万个元素)分块(百万个元素)比值标准要存的 S、P(每个头)
10244.72.42.0×4 MB
409669.234.62.0×64 MB
163841082.1541.12.0×1 GB
6553617213.48606.72.0×16 GB

在这个简化模型里,读写量只少了一半(K、V 要被反复读 T/B 次)。更大的收益来自两方面:

  1. 不存 T × T 的中间矩阵:显存从 O(T²) 降到 O(T),长上下文才放得下。反向传播时重算注意力,而不是把它存下来——用计算换显存,在显存带宽受限时反而更快。
  2. kernel 融合:矩阵乘、掩码、softmax、再乘 V 在一个 kernel 里完成,不再多次启动、多次读写。

实测 FlashAttention 比标准实现快 2~4 倍。FlashAttention-2 改进了并行方式(按 query 块并行);FlashAttention-3 利用了 H100 的异步和 FP8 特性。decode 阶段还有专门的变体(FlashDecoding:按 key 的长度方向并行)。


4. PagedAttention 与推理服务

4.1 连续批处理

推理服务同时收到很多请求,每个请求的提示长度、回答长度都不同。

  • 静态批处理:一批请求一起开始,等最长的那个结束才开始下一批。短的请求早早结束,位置空着。
  • 连续批处理(continuous batching,Orca,Yu et al. 2022):每一步都可以让结束的请求离开、新的请求加入,批始终是满的。

4.2 KV 缓存的显存管理

每个请求最后会生成多少 token,事先不知道。传统做法是给每个请求预留”提示 + 最大生成长度”的连续显存。这带来三种浪费:

  1. 预留了没用上:最大长度 2048,实际只生成了 100 个;
  2. 内部碎片:为将来的 token 提前占着;
  3. 外部碎片:空闲的总量够,却凑不出一段足够长的连续空间。

Kwon et al. (2023) 测得已有系统只有 20%~40% 的 KV 缓存显存真正被用上。

PagedAttention(vLLM)借用操作系统的分页:

  • 把 KV 缓存切成固定大小的块(例如 16 个 token),每个请求有一张”块表”,记录它的逻辑块对应哪些物理块。
  • 块按需分配,不必连续。
  • 注意力 kernel 按块表去取 K、V。

4.3 实验:服务模拟

代码 03(纯 Python 的模拟):

  • 请求:400 个请求一次性到达,提示 16~512 个 token。回答长度服从几何分布:均值 128、中位数 94、最长 683;服务允许的上限是 1024。
  • 预算:KV 缓存能放 32,768 个 token。
  • 调度:连续批处理,每步给每个运行中的请求生成一个 token。
管理方式总步数平均批大小显存利用率预留占比浪费抢占
连续,预留 提示 + 上限270618.822.7%74.4%51.7%0
连续,预知真实长度(不可能)136137.445.1%59.1%14.0%0
分页(块大小 16)109246.856.3%57.4%1.1%158

(利用率 = 真正存着 KV 的 token 占预算的比例;浪费 = 预留占比 − 利用率。都是对全部步数的平均,包括队列排空后只剩少数长请求在跑的尾部阶段。)

  • 预留最大长度:每个请求占”提示 + 1024”个位置,实际平均只用几百个。一半的显存被”订”了却空着,同时运行的请求很少,总步数最多。
  • 即使假想能预知真实长度,连续分配也有两种损失:为还没生成的 token 提前占着的位置(14%),以及外部碎片。
  • 分页:浪费的只有每个请求最后一块里没填满的部分(平均半块),只有 1.1%。同时运行的请求最多,总步数比预留最大长度少了 60%。
    • 代价是显存满了要抢占:这里把被抢占的请求放回队首、之后从头重算,重算的步数已经算在总步数里;vLLM 也支持把它的块换出到 CPU 内存。
    • 注意力 kernel 要按块表去找 K、V。

4.4 共享前缀

分页还让多个序列共享同一段 KV 缓存:几个序列的块表指向同一批物理块,用引用计数管理;只有需要修改的块才复制(写时复制,copy-on-write)。

代码 03 §3:同一个 512 个 token 的提示,并行采样 8 个回答,每个 128 个 token(best-of-n、自洽性投票都这样做):

  • 不共享:5,120 个 token 的空间;
  • 共享提示的 32 个块:1,536 个 token 的空间,省了 70%。

同样的机制让多轮对话、共享系统提示的请求复用前缀的 KV 缓存(前缀缓存,prefix caching;SGLang 的 RadixAttention 用基数树管理共享的前缀)。

4.5 其他服务层面的优化

  • 分块 prefill(chunked prefill):把长提示切成几段,和其他请求的 decode 混在一批里,避免一个长提示让所有人卡住。
  • prefill / decode 分离(disaggregation):两个阶段的瓶颈不同(算力 vs 带宽),放到不同的 GPU 上分别优化(Splitwise、DistServe)。
  • 专门的推理引擎:vLLM、SGLang、TensorRT-LLM、llama.cpp 等。

5. 量化

5.1 为什么量化能加速

decode 受内存带宽限制:每生成一个 token,都要把全部权重读一遍。把权重从 16 比特压到 4 比特,读的字节少了 4 倍,显存也省了 4 倍,可以放更大的模型或更大的批。代价是误差——这是近似的加速。

对称(absmax)量化:q=round(w/s)q = \mathrm{round}(w/s),s=max⁡∣w∣/(2b−1−1)s = \max|w| / (2^{b-1}-1),反量化 w^=q⋅s\hat w = q \cdot s。s 由一组数里绝对值最大的那个决定,所以”一组”的范围(粒度)很重要:

  • 逐张量:整个矩阵一个 s;
  • 逐通道:每个输出通道一个 s;
  • 分组:每 g 个数一个 s(GPTQ / AWQ 常用 g = 128),要多存缩放因子。

5.2 实验:只量化权重

代码 04 §1:

  • 权重分布:一个权重矩阵(第 1 层 FFN 的 up,512 × 128)的最大绝对值 0.323、标准差 0.067。
  • 测试 NLL:未量化 1.348;对照 16K 参数的草稿模型 1.363。
比特逐张量逐通道分组 32分组时每个权重的实际比特数
81.3481.3481.3488.5
41.3501.3481.3484.5
31.3541.3511.3513.5
29.1411.5201.3832.5
  • 8 比特和 4 比特几乎无损;3 比特开始有一点损失;2 比特时逐张量量化完全崩溃,分组还能用。
  • 分组越细,一个极端值影响的数越少,代价是要多存缩放因子(16 比特 / 32 个数 = 每个数多 0.5 比特)。
  • 这个小模型对量化特别宽容。真实的 LLM 在 4 比特时常要用更聪明的方法才能接近无损:
    • GPTQ(Frantar et al. 2023)用二阶信息逐列修正量化误差(CMU11-868 第 20 讲);
    • AWQ(Lin et al. 2024)根据激活的大小保护重要的权重通道。
    • 3 比特以下损失明显。

5.3 激活量化与离群通道

只量化权重时,矩阵乘法前要把权重反量化回 16 比特,省的是读内存。W8A8 让激活也变成 8 比特,矩阵乘法直接用整数单元,省的是算力,适合 prefill 和大批量。激活每个输入都不同,缩放因子只能在推理时动态算:逐张量(整批一个),或逐 token(每个 token 一个)。

离群通道:Dettmers et al. (2022, LLM.int8()) 发现,6.7B 以上的模型里,少数几个激活维度会比其他维度大几十倍,而且几乎每个 token 都这样。我们的小模型没有,代码 04 §3 用一个不改变函数的变换造了一个:

  • 把每个 LayerNorm 第 5 维的缩放乘以 100,后面 Linear 权重的第 5 列除以 100。测试 NLL 仍是 1.3481。
  • 第 1 层 LayerNorm 输出的各通道最大绝对值:最大的 286.0,其次 4.6,中位数 2.8。
激活比特原模型,逐 token离群,逐张量离群,逐 tokenLLM.int8()SmoothQuant
81.3481.4821.3531.3481.348
61.34810.1193.4131.3481.348
41.34910.1208.5501.3491.349
  • 一个通道大了 100 倍,整个 token(或整批)的缩放因子就被它决定,其他通道的值都被挤进了很少几个格子,信息几乎全丢了。逐 token 也救不了,因为离群通道在每个 token 里都在。6 比特时模型已经崩溃。
  • LLM.int8():把离群通道留在 16 比特单独算,其余照常量化(混合精度分解),完全恢复。代价是实现复杂,离群那部分不能用整数单元。
  • SmoothQuant(Xiao et al. 2023):
    • 每个通道选一个 sj=max⁡∣Xj∣α/max⁡∣Wj∣1−αs_j = \max|X_j|^\alpha / \max|W_j|^{1-\alpha},激活除以 sjs_j、权重乘以 sjs_j,函数不变。
    • 这把激活的”难”迁移一部分给权重。权重本来很”平”(最大值只有标准差的 5 倍左右),分到一部分难度也还承受得住。
    • 之后再把权重也量化成 8 比特(逐通道),W8A8 的测试 NLL 是 1.348。

5.4 其他

  • 数值格式:除了整数,还有 FP8(E4M3、E5M2,H100 原生支持)和 FP4(Blackwell);浮点格式对分布的尾部更宽容。
  • KV 缓存量化:长上下文时 KV 缓存比权重大(§2.3),把它量化到 8 或 4 比特同样重要(KIVI 等)。
  • 量化感知训练(QAT)和极低比特:BitNet b1.58 用三值权重从头训练,需要训练时就考虑量化。

6. 推测解码

6.1 想法

decode 受内存带宽限制(§1.2):目标模型一次前向处理 1 个 token 和处理 5 个 token 的时间差不多,读权重的开销一样。推测解码(Leviathan et al. 2023;Chen et al. 2023)利用这一点:

  1. 一个小得多的草稿模型连续猜 γ 个 token(便宜);
  2. 目标模型一次前向同时算出这 γ 个位置(以及后面一个位置)的分布;
  3. 按下面的规则从前往后检查每个猜测,第一次拒绝时停下。

接受 / 拒绝规则:草稿模型在某个位置给出分布 q、猜出 x;目标模型在同一位置的分布是 p。

  • 以概率 min⁡(1,p(x)/q(x))\min(1, p(x)/q(x)) 接受 x;
  • 否则拒绝,从修正分布 norm(max⁡(0,p−q))\mathrm{norm}(\max(0, p - q)) 里重新采一个 token,丢掉后面的猜测;
  • γ 个全部接受时,再从目标模型的最后一个分布里采一个 token(白送的)。

6.2 为什么输出分布严格不变

输出 x 的总概率:

P(x)=q(x)min⁡ ⁣(1,p(x)q(x))+P(拒绝)max⁡(0,p(x)−q(x))∑x′max⁡(0,p(x′)−q(x′))P(x) = q(x)\min\!\left(1, \frac{p(x)}{q(x)}\right) + P(\text{拒绝})\frac{\max(0, p(x)-q(x))}{\sum_{x'}\max(0, p(x')-q(x'))}

第一项等于 min⁡(p(x),q(x))\min(p(x), q(x))。拒绝的概率是 1−∑x′min⁡(p,q)=∑x′max⁡(0,p−q)1 - \sum_{x'}\min(p, q) = \sum_{x'}\max(0, p - q)(两个分布的总变差距离),正好和第二项的分母约掉。所以

P(x)=min⁡(p(x),q(x))+max⁡(0,p(x)−q(x))=p(x)P(x) = \min(p(x), q(x)) + \max(0, p(x) - q(x)) = p(x)

每个 token 的分布严格等于目标模型的分布,无论草稿模型多差。草稿模型只影响速度(接受率),不影响质量。

6.3 实验:逐个位置的精确验证

代码 05 §1:在 200 篇测试文档的每一个位置上,用两个模型的真实分布直接算上面的式子。

草稿模型测试 NLL参数接受率 α = E[Σ min(p, q)]max |P_输出 − p|
差的草稿(训练 1 遍)1.79915,9680.6944.8e-7
好的草稿(训练 6 遍)1.36315,9680.9214.8e-7
目标模型自己1.348806,1441.0000

输出分布和目标模型的差别只有浮点误差。这和量化、剪枝这类”近似”加速有本质的不同。

6.4 实验:1000 篇文档

代码 05 §2:用差的草稿模型(γ = 4)做推测解码,生成 1000 篇文档,和其他方式比较。“完全合语法”用花园世界的真实语法检查整篇文档。

采样方式完全合语法平均长度前两个词的分布与目标的 TV
直接从目标模型采样95.0%17.40
再从目标模型采样一次(换种子)94.0%16.60.091
推测解码(差的草稿)94.6%16.30.089
直接从差的草稿模型采样8.4%15.10.211

草稿模型自己只能写出 8.4% 合语法的文档。用它做推测解码,输出和直接从目标模型采样没有区别:和目标的差别与”目标模型换一个种子”一样大,只是抽样噪声。

6.5 实验:能快多少

如果每个猜测独立地以概率 α 被接受,每轮(每次调用目标模型)期望得到的 token 数是

1−αγ+11−α\frac{1 - \alpha^{\gamma+1}}{1 - \alpha}

代码 05 §3(每种设置 100 篇文档):

草稿模型γ实测 token / 次理论值估计加速
差的草稿11.671.691.64×
差的草稿42.672.742.47×
差的草稿83.043.142.62×
好的草稿11.891.921.85×
好的草稿44.034.273.73×
好的草稿86.166.625.31×

(估计加速 = token / 次 ÷ (1 + γc),c = 草稿 / 目标的计算量 ≈ 参数量之比 = 0.020。)

  • 实测略低于理论值:文档在 <eos> 处结束,最后一轮的猜测常常用不完;α 在不同位置也不一样,开头难猜、尾巴好猜。
  • 好的草稿(α ≈ 0.92)在 γ = 8 时一次调用平均得到 6 个 token;差的草稿(α ≈ 0.69)γ 再大也只有 3 个左右。连续猜对 γ 个的概率是 αγ\alpha^\gamma,猜得越多,后面的越容易白猜。γ 有一个最优值(练习 7)。
  • 真实的加速还取决于硬件和实现:
    • 目标模型一次处理 γ+1 个 token 的时间要和处理 1 个差不多,这在 decode 受带宽限制时成立;
    • 草稿模型要足够便宜;
    • 批很大时 decode 已经接近算力受限,推测解码的收益会变小。
    • Leviathan et al. 报告 T5-XXL 上 2~3 倍的加速。

6.6 变体

  • 贪心解码时更简单:草稿的 token 等于目标模型的 argmax 就接受,输出和目标模型的贪心解码完全相同。
  • 不用单独的草稿模型:
    • Medusa(Cai et al. 2024)在目标模型上加几个预测”后面第 k 个 token”的小头;
    • EAGLE(Li et al. 2024)用目标模型的隐藏状态预测下一个隐藏状态;
    • 自推测(self-speculative)用目标模型跳过一些层作为草稿。
  • 树状推测:草稿给出多个候选分支,目标模型用特殊的注意力掩码一次验证整棵树(SpecInfer)。
  • 查表草稿:Lookahead decoding、prompt lookup decoding 用 n-gram 或提示里出现过的片段作为猜测,对”复述提示”类的任务(摘要、改写代码)特别有效。

7. 小结、常见误区、练习

7.1 小结

  1. 瓶颈:
    • prefill 受算力限制(矩阵 × 矩阵),decode 受内存带宽限制(矩阵 × 向量,算术强度 ≈ 批大小)。
    • 批处理提高吞吐量,但有延迟代价和上限。
  2. KV 缓存:
    • 精确地去掉重复计算(512 个 token 快 7.7 倍,结果完全相同)。
    • 它的大小 = 2 × 层数 × KV 头数 × 头维度 × 字节数,长上下文时比权重还大。
  3. FlashAttention:
    • 在线 softmax + 分块,精确(误差 1e-14 量级)。
    • 省掉 T × T 的中间矩阵(显存 O(T²) → O(T)),减少显存读写、融合 kernel。
  4. PagedAttention:
    • 按块按需分配 KV 缓存,浪费从 52% 降到 1%,同样的显存能同时服务 2.5 倍的请求。
    • 共享前缀省 70% 的显存(并行采样)。
  5. 量化(近似):
    • 权重 8 / 4 比特几乎无损,分组让 2~3 比特可用。
    • 激活的离群通道会毁掉激活量化,LLM.int8() 和 SmoothQuant 能解决。
  6. 推测解码(精确):
    • 接受 / 拒绝规则保证输出分布严格不变(误差 5e-7;差的草稿 8.4% 合语法,推测解码 94.6%)。
    • 每次调用的 token 数 ≈ (1 − α^(γ+1))/(1 − α)。

7.2 常见误区

  • “GPU 越快,生成越快”:batch 小时 decode 受显存带宽限制,算力再强也闲着(§1.2)。
  • “KV 缓存是一种近似”:它是纯粹的不重复计算,结果完全相同(§2.2)。
  • “FlashAttention 用了近似的注意力”:它是精确的,只改变了计算顺序(§3.3)。
  • “FlashAttention 减少了注意力的计算量”:计算量不变(反向传播时甚至更多);它减少的是显存读写和中间结果的显存(§3.4)。
  • “量化到 4 比特就是损失 75% 的信息”:权重的冗余很大,4 比特常常几乎无损;难的是激活的离群值(§5)。
  • “推测解码用小模型生成,所以质量会下降”:输出分布严格等于目标模型(§6.2、§6.4)。
  • “草稿模型越大越好”:草稿越大 α 越高,但每次猜测的成本也越高;要在 α 和 c 之间权衡(练习 7)。

7.3 练习

练习 1(decode 的上限):一个 7B 参数的模型(fp16,14 GB 权重)在 A100 上做 batch = 1 的 decode。

(a) 每个 token 读权重至少要多久?算力上算完要多久?每秒最多生成多少 token? (b) 批大小多大时,算力才开始成为瓶颈?

参考答案

(a) 读权重:14 GB / 2 TB/s = 7 ms。计算:2 × 7×10⁹ = 1.4×10¹⁰ 次运算 / 312×10¹² = 0.045 ms。读内存比计算慢 150 多倍,每秒最多约 1 / 7 ms ≈ 140 个 token(还没算 KV 缓存的读取)。

(b) 算术强度 ≈ B,硬件平衡点 ≈ 312 TFLOPS / 2 TB/s = 156,所以 B ≈ 150 左右才开始受算力限制。实际上 KV 缓存的读取随 B 和上下文长度增加,平衡点会更早到来,而且显存也限制了 B 能开多大(§2.3)。

练习 2(KV 缓存与并发):一张 80 GB 的 A100 跑 Llama-2-7B(权重 13 GB),剩下约 60 GB 给 KV 缓存(fp16)。

(a) 每个请求的上下文是 4K 时,最多能同时服务多少个请求? (b) 换成 Llama-3-8B(GQA,8 个 KV 头)呢? (c) 再把 KV 缓存量化到 8 比特呢?

参考答案

(a) 每 token 512 KB,4K 上下文每个请求 2 GB,60 / 2 = 30 个。

(b) 每 token 128 KB,每个请求 0.5 GB,60 / 0.5 = 120 个(权重约 15 GB,剩下约 58 GB,差不多)。

(c) KV 缓存减半,约 240 个。

这就是 GQA 和 KV 缓存量化在推理服务里如此重要的原因:并发数直接决定了每个 token 的成本。分页管理(§4)让这些理论上的容量真正被用上,而不是被预留浪费掉。

练习 3(在线 softmax 手算):分数分两块到来:先是 [2, 1],再是 [3]。按在线 softmax 的更新规则算 (m, l),并和一次性计算的结果比较。

参考答案

第一块后:m = 2,l = e⁰ + e⁻¹ = 1 + 0.368 = 1.368。

第二块:m’ = max(2, 3) = 3;l’ = 1.368 × e^(2−3) + e^(3−3) = 1.368 × 0.368 + 1 = 1.503。

一次性:Σ exp(x − 3) = e⁻¹ + e⁻² + e⁰ = 0.368 + 0.135 + 1 = 1.503。✓

softmax(3) = 1/1.503 = 0.665,和 §3.2 的例子(1000、999、998 的相对差相同)一样。

练习 4(注意力的中间矩阵):T = 32K,32 个头,fp16,batch = 1。

(a) 标准注意力在一层里要存的 S(分数矩阵)有多大? (b) FlashAttention(块大小 128)任何时候需要的分数存储呢?

参考答案

(a) 每个头 32768² × 2 字节 = 2 GB,32 个头 64 GB(P 还要再来一份)。一张 80 GB 的卡连一层都放不下。

(b) 每个头 T × 128 个分数(实际实现里是 128 × 128 的块,在 SRAM 里),和 T 成线性关系:32768 × 128 × 2 字节 = 8 MB,32 个头 256 MB,而且根本不写回显存。长上下文训练和推理离不开 FlashAttention。

练习 5(分页的块大小):§4.3 用块大小 16。

(a) 平均每个请求浪费多少个 token 的空间?块大小 256 呢? (b) 块越小越好吗?

参考答案

(a) 最后一块平均半满:块大小 16 时平均浪费约 8 个 token;块大小 256 时约 128 个——对平均只生成 128 个 token 的请求来说,浪费接近一半。

(b) 不是。块越小:

  • 块表越长,查表开销越大;
  • 注意力 kernel 每次读的连续内存越短,访存效率越低;
  • 分配 / 释放更频繁。

vLLM 默认用 16。此外,前缀共享以块为单位:块太大,能共享的部分变少。

练习 6(量化误差):§5.2 的权重矩阵最大绝对值 0.323、标准差 0.067。用逐张量的 4 比特对称量化(qmax⁡=7q_{\max} = 7)。

(a) 量化步长 s 是多少? (b) 均匀量化的均方误差约为 s²/12,信噪比(权重方差 / 误差方差)是多少? (c) 如果矩阵里有一个 3.0 的极端值呢?

参考答案

(a) s = 0.323 / 7 = 0.046。

(b) 误差方差 ≈ 0.046² / 12 = 1.8×10⁻⁴,权重方差 0.067² = 4.5×10⁻³,信噪比约 25(14 dB)。实验里 4 比特逐张量量化只让 NLL 从 1.348 变成 1.350。

(c) s = 3.0 / 7 = 0.43,比大多数权重(|w| < 0.2)大一倍以上:几乎所有权重都被量化成 0。信噪比约 0.067² / (0.43²/12) ≈ 0.3,信息基本丢光。这就是离群值的问题,分组量化、逐通道量化、或者把离群值单独存(§5.3)都是为了它。

练习 7(推测解码的最优 γ):接受率 α = 0.8,草稿模型的成本是目标模型的 c = 0.05。估计加速 = 1−αγ+1(1−α)(1+γc)\frac{1-\alpha^{\gamma+1}}{(1-\alpha)(1+\gamma c)}。

(a) 算 γ = 1、4、8、10 时的加速。 (b) 最优的 γ 是多少?为什么不是越大越好?

参考答案

(a) γ = 1:1.80 / 1.05 = 1.71×;γ = 4:3.36 / 1.20 = 2.80×;γ = 8:4.33 / 1.40 = 3.09×;γ = 10:4.57 / 1.50 = 3.05×。

(b) 最优在 γ = 8 附近(3.09×)。

  • 分子(每次得到的 token 数)随 γ 增长越来越慢,趋于 1/(1−α) = 5:连续猜对的概率 α^γ 衰减。
  • 分母(成本)随 γ 线性增长。

α 越高、c 越小,最优的 γ 越大。实际系统还会根据最近的接受率动态调整 γ。

练习 8(贪心推测解码):目标模型用贪心解码(每步取 argmax)。

(a) 推测解码的接受规则应该怎样改? (b) 证明输出和目标模型的贪心解码完全相同。

参考答案

(a) 草稿的猜测 x 等于目标模型在该位置的 argmax 就接受,否则拒绝并输出目标模型的 argmax。这是一般规则在 p 退化成 one-hot 时的特例:p(x) = 1 时 min(1, p/q) = 1,p(x) = 0 时为 0;max(0, p − q) 归一化后就是那个 one-hot。

(b) 归纳法:

  • 假设到当前位置为止,输出和贪心解码相同,那么目标模型在这个位置看到的前缀相同,argmax 相同。
  • 接受的 token 等于 argmax;拒绝时输出的也是 argmax。所以这个位置也相同。

被接受的猜测都在目标模型”一次前向”里检查过——它们的前缀就是已经确认的输出,所以检查是有效的。

练习 9(思考:设计一个聊天服务的推理栈):一个聊天产品,用户希望第一个字快(TTFT < 1 秒),之后打字速度流畅(每秒 20 个 token 以上);公司希望每个 token 尽量便宜。只讨论原理,说出你会用哪些方法,以及它们之间的权衡。

参考答案(开放)

一定要用的(精确,几乎没有代价):

  • KV 缓存;
  • FlashAttention;
  • 分页 KV 缓存 + 连续批处理:并发数决定成本;
  • 前缀缓存:系统提示和多轮对话的历史在每个请求里重复。

按需权衡的:

  • 批大小:越大越便宜,但每个用户的 TPOT 变长。要设一个上限,保证每秒 20 个 token。
  • 长提示:用分块 prefill 和其他请求的 decode 混合,避免一个长文档的 prefill 让所有人卡住;流量大时可以把 prefill 和 decode 分到不同的机器上。
  • 量化:
    • 权重 8 比特或 4 比特(GPTQ / AWQ)省显存、提速 decode;
    • KV 缓存 8 比特提高并发。
    • 需要在自己的评测集上确认质量损失可以接受(第 5 讲)。
  • 推测解码:
    • 输出不变,能降低 TPOT。在批小(深夜流量低)时收益最大;高峰期批很大、接近算力受限时收益变小,可以动态开关。
    • 草稿可以用同系列的小模型或 Medusa / EAGLE 头。
  • GQA 的模型:选型时就考虑 KV 缓存的大小。

还要考虑的:

  • 监控 TTFT、TPOT 的分布(尤其是 P99),而不只是平均值;
  • 不同长度请求的公平调度;
  • 抢占策略;
  • 模型更新时的兼容性。

7.4 延伸阅读

  • 原课阅读:
    • Leviathan et al. (2023), Fast Inference from Transformers via Speculative Decoding
    • Dao et al. (2022), FlashAttention
    • Kwon et al. (2023), Efficient Memory Management for Large Language Model Serving with PagedAttention
  • 推理的瓶颈与服务:
    • Pope et al. (2022), Efficiently Scaling Transformer Inference
    • Yu et al. (2022), Orca: A Distributed Serving System for Transformer-Based Generative Models(连续批处理)
    • Zheng et al. (2024), SGLang(RadixAttention)
    • Agrawal et al. (2024), Sarathi-Serve(分块 prefill)
    • Zhong et al. (2024), DistServe
  • 注意力:
    • Milakov & Gimelshein (2018), Online normalizer calculation for softmax
    • Dao (2023), FlashAttention-2
    • Shah et al. (2024), FlashAttention-3
    • Shazeer (2019), Fast Transformer Decoding: One Write-Head is All You Need(MQA)
    • Ainslie et al. (2023), GQA
  • 量化:
    • Dettmers et al. (2022), LLM.int8()
    • Xiao et al. (2023), SmoothQuant
    • Frantar et al. (2023), GPTQ
    • Lin et al. (2024), AWQ
    • Liu et al. (2024), KIVI(KV 缓存量化)
    • Ma et al. (2024), The Era of 1-bit LLMs(BitNet b1.58)
  • 推测解码:
    • Chen et al. (2023), Accelerating Large Language Model Decoding with Speculative Sampling
    • Cai et al. (2024), Medusa
    • Li et al. (2024), EAGLE
    • Miao et al. (2024), SpecInfer
    • Fu et al. (2024), Lookahead Decoding
  • 本仓库的相关内容:
    • 第 4 讲(GQA)
    • 第 13 讲(混合精度)
    • 第 21 讲(长上下文)
    • 第 22 讲(MoE 推理)
    • CMU11-868(第 9 讲解码、第 10 讲融合 kernel、第 19~20 讲量化与 GPTQ)

7.5 下一讲预告

到这里,我们有了一个训练好、对齐过、跑得快的模型。可是用户面对的不是”一个续写模型”,而是一个聊天助手,甚至是一个能调用工具、在网页上替你做事的智能体。第 20 讲:聊天机器人、LLM 智能体与人工评估。我们会看:

  • 对话模板和”只在助手的回答上算损失”;
  • 智能体的”观察 → 思考 → 行动”循环;
  • Chatbot Arena 怎样用众包的成对比较给模型排名(又是 Bradley–Terry);
  • 为什么人工评估本身也可能不可靠。
← 返回课程目录