为什么 GPU 生成每个 token 时利用率不到 5%?——Prefill 与 Decode 深度拆解
想象一座图书馆。馆里有一套厚重的规则书和参考知识库(模型权重)——「如何写关键词」的 W_K、「如何写摘要」的 W_V、「如何理解读者问题」的 W_Q、以及一套处理信息的百科全书(FFN 权重)。这套规则书是管理员吃饭的本事,无论编目还是回答,每次操作都必须把它从书架搬到桌面(HBM → 计算单元),搬一次就是十几 GB。
每次有人来借书(一个请求),图书馆要做两件事。第一,把所有相关藏书的索引卡片建好——书名、关键词、摘要,每本书都要对着规则书登记一遍。这是重体力活,但做一次就够了。第二,根据读者的问题,从卡片目录中找到最相关的段落,逐句回答——这个回答过程本身很轻松,但每说一句话之前,既要重新把厚重的规则书搬出来,也要翻一遍越摞越厚的卡片堆。
LLM 推理的两阶段——Prefill 和 Decode——和这座图书馆的运作逻辑一模一样。Prefill 是建卡片目录:把所有 prompt token 一次性推进模型,对着规则书为每个 token 生成 Key(检索标签)和 Value(内容摘要),存入 KV Cache。Decode 是查目录回答问题:每生成一个新 token,只计算这一个 token 的 Q(查询意图),但规则书必须重新搬出来(读模型权重),卡片目录也必须从头翻(读 KV Cache),然后才能回答。卡片越摞越厚,翻卡片的时间最终远超搬规则书和思考答案的时间。
这两个阶段在同一个模型、同一组权重上运行,但因为输入形状不同,计算模式截然不同——一个是把 GPU 算力打满的矩阵-矩阵乘法(编目),一个是让 GPU 空转等数据的矩阵-向量乘法(翻卡片)。这个差异直接解释了 KV Cache 为什么必须存在,以及围绕它的所有优化方向——GQA、量化、PagedAttention、Offloading、PD 分离——到底在优化什么。
本文用一个贯穿始终的具体例子——「4 个 token 的 prompt、d_model=4096、32 个注意力头」——把两个阶段中每一步的矩阵形状和计算量都标清楚。阅读前提:理解自注意力的 Q/K/V 投影和 softmax 注意力公式(参考 Transformer 架构详解)。
配套资源:
- 分步交互可视化 — 5 步推演 Prefill → Decode → Cache 膨胀,键盘 ← → 翻页。
- Python 校验脚本 — 微型模型验证 KV Cache 数值等价性 + FLOPs 对比,
-v打印每步形状。
一、设定:一个具体的推理场景
假设以下参数,后面每一步都会标注矩阵形状:
模型参数:
d_model = 4096 (隐藏维度)
num_heads = 32 (注意力头数)
head_dim = 128 (每头维度,4096 / 32)
num_layers = 32 (Decoder 层数)
输入:
Prompt: "法国的首都是" → 4 个 token
目标:自回归生成 "巴黎"
单层权重矩阵(每个 attention 层独立):
W_Q: (4096, 4096) ← 「如何写查询关键词」的规则书
W_K: (4096, 4096) ← 「如何写检索标签」的规则书
W_V: (4096, 4096) ← 「如何写内容摘要」的规则书
W_O: (4096, 4096) ← 「如何融合多套卡片」的规则书
(完整的 32 层模型还有 FFN 权重及 Embedding/LM Head,7B 模型总重约 14GB FP16。
这些规则书每次前向传播都必须从 HBM 全部读入计算单元,是 memory-bound 的固定开销。)
所有参数和计算均假设 FP16 精度。
二、Prefill 阶段:一次性建好卡片目录
回到图书馆的类比。Prefill 就是那个「把所有藏书的卡片一次性建好」的过程。4 本新书入库(4 个 prompt token),管理员不偷懒——全部摊开,同时翻、同时写摘要。这是最累的阶段,但做完之后,卡片目录就绪,后续的每一次查询都只需翻卡片,不用重读原书。
2.1 输入 → Q/K/V 投影
4 个 token 经过 Embedding 层后,得到输入矩阵 X:
X: (4, 4096) ← 4 个 token,每个 4096 维
Q = X × W_Q → (4, 4096) × (4096, 4096) = (4, 4096)
K = X × W_K → (4, 4096) × (4096, 4096) = (4, 4096)
V = X × W_V → (4, 4096) × (4096, 4096) = (4, 4096)
每次乘法:4 × 4096 × 4096 × 2 FLOPs ≈ 134M FLOPs。Q、K、V 三次投影共约 402M FLOPs。
类比:W_K 是「如何写关键词」的规则书,W_V 是「如何写摘要」的规则书。管理员按照规则书,一次性为 4 本书产出了 4 组卡片。这是 矩阵-矩阵乘法(GEMM)——GPU 的 Tensor Core 在 FP16 下能达到 70%+ 的利用率,就像一个经验丰富的编目员火力全开。
2.2 拆分为多头
将 Q、K、V 从 (4, 4096) reshape 为 (4, 32, 128),然后 transpose 为 (32, 4, 128)——32 个头,每个头独立计算。
Q: (32, 4, 128) ← 32 个头,每个头 4 个 token,128 维
K: (32, 4, 128)
V: (32, 4, 128)
类比:32 个头相当于 32 位管理员,每人独立为 4 本书写一套自己的卡片。一组人按题材分类,一组人按作者分类,一组人按年代分类——同一个书库,多套索引。
2.3 注意力计算(单头)
对于每个头 h:
S = Q_h × K_h^T
Q_h: (4, 128)
K_h^T: (128, 4)
S: (4, 4) ← 4×4 的注意力得分方阵
计算量:4 × 128 × 4 × 2 = 4,096 FLOPs(单头,微不足道)
S 矩阵的含义:位置 (i, j) 的值表示「token i 觉得 token j 有多相关」的原始分数。当 seq_len 只有 4 时,这是一个很小的 4×4 方阵——此时 QK 点积的计算量远小于 Q/K/V 投影的 GEMM。
S_scaled = S / √128 = S / 11.3
A = softmax(S_scaled, dim=-1) → (4, 4)
每行是"当前 token 对 4 个 token 的注意力权重",和为 1
O_h = A × V_h
A: (4, 4)
V_h: (4, 128)
O_h: (4, 128) ← 加权求和后的上下文表示
类比:每本书的关键词(Q)和其他书的关键词(K)做匹配——「法国」和「首都」在关键词空间里相关度很高——然后加重相关书的摘要(V),得到这本书在「考虑了所有其他书之后的」更好摘要。
2.4 多头拼接 + 输出投影
O_concat = concat(O_0, O_1, ..., O_31) → (4, 4096)
O = O_concat × W_O → (4, 4096) × (4096, 4096) = (4, 4096)
W_O 把 32 位管理员的独立视角融合成一套统一的输出。
以上只是 1 层 Attention。 完整的 Decoder Layer 还在 Attention 之后串接了 FFN(SwiGLU)、残差相加和 RMSNorm(详见 Transformer 架构详解 第七节)。经过全部 32 层后,最后一层的输出是一个 (4, 4096) 的隐藏状态矩阵。
取最后一个位置(第 4 个 token「是」)的隐藏状态向量 (1, 4096),乘以 LM Head 权重 (4096, vocab_size) 得到词汇表中每个 token 的 logits,再 softmax → argmax 采样,就得到了第一个输出 token——比如「巴」。
最后一层输出: (4, 4096)
取最后一个位置: (1, 4096) ← 「是」的位置
× LM Head (4096, V): (1, V) ← V = 词表大小(如 128K)
softmax + 采样: → token_id = 「巴」
这一步之后,Prefill 结束,Decode 开始。KV Cache 已经在每一层就绪。
2.5 存入 KV Cache——「卡片目录建好了」
这一步是整个推理优化的起点。 把 Prefill 计算出的所有 token 的 K 和 V 存入缓存:
KV Cache 写入(每层):
K_cache: (32, 4, 128) ← 32 个 head × 4 个 token × 128 维
V_cache: (32, 4, 128) (即 2.2 节 reshape 后的形状,直接存入,无需额外操作)
每层写入量:2 × 32 × 4 × 128 × 2 bytes (FP16) = 65,536 bytes ≈ 64 KB
32 层总计:32 × 64 KB ≈ 2 MB
2MB 对于一个 prompt 来说微乎其微。但这座图书馆有 32 层楼(32 个 Decoder Layer),每层都要建一套卡片——总共 32 套。不过 2MB 的总投入仍然是值得的,因为这样就不用在后续每次回答时重读原书了。
2.6 Prefill 阶段总结
┌──────────────────────────────────────────────────────┐
│ Prefill = 建卡片目录 │
│ │
│ 操作类型:矩阵 × 矩阵 (GEMM) ← 管理员火力全开 │
│ 瓶颈:算力 (compute-bound) ← 翻书和写摘要本身是最累的 │
│ GPU 利用率:高(Tensor Core 饱满) │
│ Q/K/V 投影占计算量的大头,注意力计算占比极小 │
│ │
│ K/V 操作:初次计算 → 写入 KV Cache(一次性投入) │
│ Q 操作:计算后即丢弃(编目时不需要保留查询意图) │
│ │
│ 关键指标:TTFT (Time To First Token) │
│ 优化方向:增大 batch → 更多书同时编目 → 提高利用率 │
└──────────────────────────────────────────────────────┘
三、Decode 阶段:查目录,逐句回答
卡片目录建好了。现在读者来问第一个问题(生成第一个 token),管理员不需要回去翻原书——卡片上已经有每本书的关键词(K)和摘要(V)。管理员只需要把问题的查询关键词(Q)和所有卡片的关键词(K)做匹配,找出最相关的摘要(V),然后回答。
这就是 Decode 阶段。关键变化:输入不是整批 prompt,而是上一个生成的 token——只有 1 个。 这意味着每次只写一张新卡片,但要翻遍所有旧卡片才能回答。翻卡片的开销远大于写新卡片。
3.1 输入 → Q/K/V 投影(注意形状变化)
x_new: (1, 4096) ← 只有一个 token!
Q = x_new × W_Q → (1, 4096) × (4096, 4096) = (1, 4096)
K = x_new × W_K → (1, 4096) × (4096, 4096) = (1, 4096)
V = x_new × W_V → (1, 4096) × (4096, 4096) = (1, 4096)
每次乘法:1 × 4096 × 4096 × 2 ≈ 33.5M FLOPs。比 Prefill 的 134M FLOPs 小了 4 倍——但这只是写一张新卡片的成本。真正的开销在后面。
关键退化:seq_len=1 让 GEMM(矩阵-矩阵乘法)退化为 GEMV(矩阵-向量乘法)。GPU 的 Tensor Core 是为大矩阵乘法设计的——用 GEMV 跑 Tensor Core 就像用卡车运一个信封,引擎很大但用不满。
更致命的是,数据搬运量几乎没变。注意 Decode 投影中读 W_Q 仍是 32 MB——这是把一整本厚重的规则书从书架搬到桌面,就为了查一个词条。
Prefill Q 投影(4 个 token):
计算量:134M FLOPs
搬规则书(读 W_Q):4096² × 2 = 32 MB
算术强度:134M / 32MB ≈ 4.2 FLOPs/byte ← 管理员忙着干活,搬一次书值得
Decode Q 投影(1 个 token):
计算量:33.5M FLOPs
搬规则书(读 W_Q):4096² × 2 = 32 MB ← 同一本厚重的规则书,但只查一个词条
算术强度:33.5M / 32MB ≈ 1.05 FLOPs/byte ← 管理员在等规则书和卡片送过来
这还只是 W_Q 一本规则书。每一次 Decode 前向传播,管理员必须搬出全套规则书——32 层的 W_Q、W_K、W_V、W_O 再加 FFN 权重,7B 模型总计约 14 GB,全部从 HBM 读到计算单元。
类比:算术强度就是路上的「车密度」。Prefill 是高峰期——每公里 200 辆车,4 吨货物分摊在 200 辆车上,堵不堵取决于引擎马力(算力)。Decode 是深夜——每公里只有 1 辆车,却要拖 14 吨的货(规则书)外加几千张卡片(KV Cache),能开多快完全取决于路的宽度(HBM 带宽)。H100 的 HBM3 带宽约 3.35 TB/s,按 1.05 FLOPs/byte 的算术强度,GPU 绝大多数时间在等数据——它不计算,它空转。
3.2 KV Cache 的追加——「新书写了一张新卡片」
新 token 的 K 和 V 追加到缓存中:
追加前:
K_cache: (32, 4, 128) ← Prefill 的 4 个 token
V_cache: (32, 4, 128)
追加后(生成第一个新 token 后):
K_cache: (32, 5, 128) ← 在 seq_len 维度上拼接(dim=1)
V_cache: (32, 5, 128)
每次 Decode 步骤追加的量很小——单 token × 32 heads × 128 dim × FP16 × 2 (K+V) = 16 KB。写新卡片的成本几乎可以忽略。读旧卡片才是瓶颈。
3.3 注意力计算——「翻遍所有旧卡片」
Q_new: (32, 1, 128) ← 新问题的查询意图
K_cache: (32, 5, 128) ← 所有旧卡片的关键词(4 旧 + 1 新)
V_cache: (32, 5, 128) ← 所有旧卡片的摘要
S = Q_new × K_cache^T
Q_new: (32, 1, 128)
K_cache^T: (32, 128, 5)
S: (32, 1, 5) ← 得分向量(不是方阵!和 Prefill 完全不同)
计算量:32 × (1 × 128 × 5) × 2 = 40,960 FLOPs(极小)
A = softmax(S / √128) → (32, 1, 5)
O = A × V_cache
A: (32, 1, 5)
V_cache: (32, 5, 128)
O: (32, 1, 128)
计算量:32 × (1 × 5 × 128) × 2 = 40,960 FLOPs(也极小)
计算量极小——但要把 (32, 5, 128) 的 K_cache 和 V_cache 从 HBM 读到计算单元。当只有 5 个 token 时这个成本不显眼,但当 seq_len 增长到 4096、8192 或更长时,读取所有旧卡片的时间远超思考答案的时间。
这就是 memory-bound 的本质:管理员每回答一句话(40,960 FLOPs 的计算),必须先把数千张卡片全部翻一遍(数 MB 的 HBM 读取)。算得快没用——卡在搬运卡片上。
(和 Prefill 一样,以上只是 1 层 Attention。完整的 32 层每层都重复这套流程——读规则书 → 写一张新卡片 → 翻全部旧卡片 → FFN 变换 → 残差连接 → 传给下一层。32 层跑完后,最后一层的隐藏状态 (1, 4096) 同样经过 LM Head → softmax → 采样,得到下一个 token。然后这个新 token 又作为下一轮 Decode 的输入——循环往复,直到生成 EOS。)
四、为什么旧卡片的内容永远不会变?——KV Cache 的数学依据
KV Cache 之所以有效,是因为一个被很多人忽略的事实:已生成 token 的 K 和 V 永远不变。 这就好比一本已经入库编目的书——它的关键词和摘要已经写在卡片上了,不会因为新书入库而改变。
4.1 因果掩码的保证
Decoder 的因果掩码确保:生成第 t 个 token 时,模型只看位置 [1, t-1]。位置 [1, t-1] 的输入没有变化——它们还是原来的 token。既然输入没变,W_K 和 W_V 也没变,那么 K_i = x_i × W_K 和 V_i = x_i × W_V 自然也完全不变。
唯一的「新东西」是位置 t 的 Q_t——它需要查询所有历史 token。所以:
在 Decode 第 t 步时:
需要重新计算的:
Q_t ← 新问题的查询意图(每个问题都不一样)
可以复用的(从 KV Cache 读取):
K_1, K_2, ..., K_{t-1} ← 旧书的关键词,已经编好了
V_1, V_2, ..., V_{t-1} ← 旧书的摘要,已经写好了
需要计算并追加的:
K_t, V_t ← 新书的卡片,算一次后加入目录
4.2 不缓存 Q 的理由
如果旧书的关键词不变,旧书的查询意图(Q)也不变——为什么不一起缓存 Q?因为进入 Decode 阶段后,只有最新 token 的 Q 参与计算。旧的 Q 已经完成了它们的使命——在各自的生成步骤中「查」过当时的所有 K。后续步骤不再需要旧 Q。
旧卡片会被之后每一个新问题反复查询。不存旧 Q 是省空间——存旧 K 和 V 是避免 O(n²) 重复计算。
五、Prefill 与 Decode 的根本差异
Prefill(建目录) Decode(查目录回答问题)
───────────────────── ─────────────────────────────── ──────────────────────────────
类比 一次入库全部藏书,并行编目 每回答一句,重新搬出规则书 + 翻全部卡片
输入形状 (batch, prompt_len, d_model) (batch, 1, d_model)
Q/K/V 投影 矩阵 × 矩阵 (GEMM) 矩阵 × 向量 (GEMV)
注意力得分矩阵 (seq, seq) 方阵 (1, seq) 向量
瓶颈 算力 (compute-bound) 显存带宽 (memory-bound)
管理员翻书写摘要本身太累 搬规则书 + 翻卡片占了 90% 时间
模型权重 读一次,摊在桌上供 4 本书共用 每回答一句重搬一次(固定 ~14GB)
读一次摊在桌上供所有 prompt token 共用 每次都搬全套,但每次只查一个词条
GPU 利用率 高(Tensor Core 饱满) 低(等待 HBM 数据)
算术强度 较高(~4 FLOPs/byte 以上) 极低(~1 FLOPs/byte)
KV Cache 操作 写入(初始化目录) 读取 + 追加(查目录 + 新书写卡)
关键指标 TTFT TPOT (Time Per Output Token)
优化策略 增大 batch、算子融合 减少卡片数量 / 压缩规则书 / 加快搬运速度
这就是 KV Cache 优化的总纲。 Decode 的 memory-bound 有两层:第一层是每次必须重搬的规则书(模型权重,固定 ~14GB),第二层是越摞越厚的卡片目录(KV Cache,随 seq_len 线性增长)。规则书的重量是固定的——优化它的方向是量化(见 6.2);卡片目录的重量是增长的——这才是 KV Cache 优化的主战场。所有的优化方向,本质上都是在回答一个问题:如何在 memory-bound 的 Decode 阶段,减少每次回答前需要搬运的数据量——无论是压缩规则书,还是减少翻卡片。
六、从计算分析到 KV Cache 优化的必然逻辑
回到图书馆。我们有一个问题:管理员每回答一句话之前,要把几千张卡片全翻一遍,翻卡片的时间占据了 90% 的响应延迟。 怎么办?
6.1 减少卡片数量:GQA 和 MQA
标准 MHA(多头注意力)下,32 位管理员每人有一套独立的卡片目录——32 套 × 每套数千张卡片 = 海量翻找。
GQA(Grouped-Query Attention)的做法:让 4 位管理员共享一套卡片目录。卡片总数缩减为原来的 1/8,翻卡片的时间也缩减为原来的 1/8。
MHA: K 读取量 = 32 × seq_len × 128 × 2 bytes
GQA (8 组):K 读取量 = 8 × seq_len × 128 × 2 bytes
seq_len=4096 时:
MHA: 32 × 4096 × 128 × 2 = 32 MB
GQA: 8 × 4096 × 128 × 2 = 8 MB
每次 Decode 步骤少搬 24 MB。LLaMA 2 70B 和 Qwen 2 都使用了 GQA,原因就在这个数字里。
6.2 压缩规则书和卡片:量化
Decode 的搬运清单上有两项:规则书(模型权重)和卡片目录(KV Cache)。FP16 的规则书每页 2 字节,FP16 的卡片也是每张 2 字节。换成 INT8 或 FP8(1 字节),搬运量直接减半。换成 INT4(0.5 字节),再减半。
规则书的量化和 KV Cache 的量化的收益不同。规则书是固定大小——7B 模型从 14GB 压到 3.5GB,每次 Decode 少搬 10.5GB,收益立竿见影——这就是为什么 INT4 量化(如 AWQ、GPTQ)主要被宣传为「降低显存」和「加速 Decode」。KV Cache 的量化的收益随序列长度放大——seq_len 从 4K 到 128K 时,压缩 KV Cache 的收益比压缩权重更大。
两者的共同代价是精度损失——就像用速记符号代替完整文字,偶尔会漏掉细节。当前工程实践的共识是「权重量化激进(INT4 可行),KV Cache 量化保守(FP8/INT8 为主,INT4 仅在特定场景验证通过)」,因为 KV Cache 的误差会在自回归生成中逐步累积。
6.3 标准化归档:PagedAttention
卡片如果不按标准格式归档——大小不一、到处乱堆——找起来更慢,且书架空间浪费严重(碎片化)。
PagedAttention(vLLM 引入)把所有卡片统一切分为固定大小的「标准卡片盒」(Page),按需申请、用完回收。同一张虚拟卡片可以映射到物理书架上的任意位置(通过 block table),就像 OS 的虚拟内存。它不减少翻卡片的总时间,但通过消除碎片让书架能多放几套目录——同样的显存容纳更多并发请求。
6.4 冷热分离:KV Cache Offloading
不常被问到的领域(比如古罗马史),对应的卡片不放在手边的书架上,而是放到地下室(CPU 内存)甚至仓库(NVMe)。偶尔用到时多花点时间取,但释放出来的桌面空间让常问领域(比如法国地理)的卡片可以摊得更开。
LMCache 和 HiCache 的分层存储架构(L1 GPU → L2 CPU → L3 NVMe)就是这个思路的工程实现——Decode 是 memory-bound 的,管理员本来就有一半时间在等数据。把一部分等待时间用来从地下室取卡也是可以接受的。
6.5 分拆编目组和查询组:Prefill-Decode 分离
编目(Prefill)是体力活,需要大力士(高算力 GPU)。查目录(Decode)需要快腿(高带宽 GPU)和面积够大的桌子(大容量 HBM)。让同一个人同时做两件事,两件事都做不好——搬重书的时候没法跑腿,占着桌子的时候没法清出空间放新卡片。
PD 分离架构把这俩人分到不同的房间:
分离前(同一张 GPU):
编目员搬书时占满了所有力气 → 查询员没法同时翻卡片
查询员的卡片摊了满桌 → 编目员没地方放新卡片
分离后:
编目 GPU:大力士 + 小桌子(高 TFLOPS,适中的 HBM —— 只存权重)
查询 GPU:快腿 + 大桌子(高带宽,大容量 HBM —— 存 KV Cache + 权重)
编目完的卡片通过 NVLink/InfiniBand 快递给查询端
到此,回到最初的类比:五种优化——GQA、量化、PagedAttention、Offloading、PD 分离——对应图书馆面对卡片目录膨胀时的五条解决路径:共享目录、速记法、标准化归档、地下室仓储、编目查询分拆人手。它们的共同起点是同一个事实:查目录比写卡片轻得多,但目录越厚,翻目录就成了瓶颈。
七、相关资源
- KV Cache 原理简介 — 本文的姊妹篇,侧重 KV Cache 的工作机制、显存公式和工程实践。
- Transformer 架构详解 — Q/K/V 投影和自注意力的完整数学推导。
- GQA: Training Generalized Multi-Query Transformer Models — GQA 原始论文。
- vLLM: PagedAttention — 分页 KV Cache 管理的原始论文。
- BentoML LLM Inference Handbook — Prefill/Decode 的交互式可视化。