为什么 GPU 生成每个 token 时利用率不到 5%?——Prefill 与 Decode 深度拆解

想象一座图书馆。馆里有一套厚重的规则书和参考知识库(模型权重)——「如何写关键词」的 W_K、「如何写摘要」的 W_V、「如何理解读者问题」的 W_Q、以及一套处理信息的百科全书(FFN 权重)。这套规则书是管理员吃饭的本事,无论编目还是回答,每次操作都必须把它从书架搬到桌面(HBM → 计算单元),搬一次就是十几 GB。

每次有人来借书(一个请求),图书馆要做两件事。第一,把所有相关藏书的索引卡片建好——书名、关键词、摘要,每本书都要对着规则书登记一遍。这是重体力活,但做一次就够了。第二,根据读者的问题,从卡片目录中找到最相关的段落,逐句回答——这个回答过程本身很轻松,但每说一句话之前,既要重新把厚重的规则书搬出来,也要翻一遍越摞越厚的卡片堆。

LLM 推理的两阶段——PrefillDecode——和这座图书馆的运作逻辑一模一样。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 分离——对应图书馆面对卡片目录膨胀时的五条解决路径:共享目录、速记法、标准化归档、地下室仓储、编目查询分拆人手。它们的共同起点是同一个事实:查目录比写卡片轻得多,但目录越厚,翻目录就成了瓶颈。


七、相关资源