阶段 6 · 让模型跑起来

KV Cache:把算过的东西
存下来别再算一遍

上一章《混合精度与显存账本》算的是训练账单:16 倍参数量,大头在优化器状态。 模型训好开始服务,显存瓶颈换了个来源——不是权重和优化器,是每生成一个词都要重算的 K、V。
第一刀砍向这个浪费:生成第 100 个词的时候,前 99 个词被重新算了一遍。

1

一次前向只吐一个字

语言模型是自回归的——它每预测一个词,都要把这一个词再拼回输入末尾,再跑一次完整的前向(把输入从头到尾过一遍模型,算出下一个词的概率)。

互动 · 生成一句话到底跑了几次前向

已生成词数
0
已跑前向次数
0

训练时你一次把整句话喂进去,一次前向就能算出所有位置的下一个词,这是并行的; 推理时不行——第 100 个词必须等第 99 个词算出来才能开始。 生成 n 个词就要串行跑 n 次前向,每一次都比上一次长一点。

2

99% 的计算是重复的

把每一次前向拆开看。注意力层要算三样东西:Q(查询)、K(键)、V(值)。 关键观察是——

第 t 步算出的 K1…Kt−1 和 第 t−1 步算出的 K1…Kt−1
完全一样。因为第 1 个位置的输入从来没变过,它的 K、V 也永远不会变。

第 1 个词的 K 和 V,只取决于第 1 个词的嵌入(这个词被转成的一串数)——而那串数从第一步开始就再也没变过。 所以你每生成一个词,都在用完全相同的方式,把前面所有词的 K、V 重新算一遍。

互动 · 「完全一样」到底是什么意思:每个位置的 K 算出来就不再变

只存 K、V,不存 Q:Q 只在这一步「发问」,用完就丢;K、V 是资料,后面每一步都要「查」。

💡 KV Cache 就一句话

把每一步算出来的 K、V 存进显存,下一步直接拿来用,只算新来的那一个词的 K、V。
代价是显存。收益是把生成 n 个词的注意力计算量从 O(n³) 降到 O(n²)——序列越长,省得越多。

🎯 类比

像写一份不断加长的会议纪要。笨办法是:每来一个新发言,就把前面所有人的发言重新速记一遍, 再写新内容。
聪明办法是:以前的记录留在本子上不动,新发言直接往后加。
KV Cache 就是那本"不重抄的笔记本"。
这个类比管到"不重抄"为止:缓存里的 K、V 是逐位精确的原件,会议纪要是浓缩后的摘要。

3

省下的倍数随长度往上涨

下面的曲线按公式逐项累加而来。拖序列长度看差距怎么拉开。

互动 · 无 Cache vs 有 Cache 的累计注意力计算量

无 Cache 累计(FLOPs)
—
有 Cache 累计(FLOPs)
—
省下的倍数
—

没有 cache:第 t 步要重算长度 t 的完整注意力,其中 QKT 是 t×t 个点积, 所以第 t 步是 O(t²),累加起来 Σ t² ≈ n³/3。 有了 cache:第 t 步只有 1 个新 query,去和 t 个已有的 key 做点积,是 O(t), 累加起来 Σ t ≈ n²/2。 两者相除就是上面那个 2n/3:序列越长,省得越多。

M

数学 · 每步只多一对 K、V

上一节那两条曲线差了三百多倍(拉到最长时超过一千倍)。省下来的到底是什么? 把每一步摊开看,答案只有一句话:每一步真正「新算」的,只有新来那一个 token 的 K、V。 下面这张图把它画了出来——点亮的格子,就是真正要动手算的部分。

互动 · 每一步到底要算几对 K、V(亮着的格子 = 要算)

互动 · 每个符号管图上的哪一块

🎬 自己验一遍

把上面的开关切到「无缓存」,再拖序列长度—— 整片格子会越来越亮,总数按 1+2+…+n 涨上去。
切回「有缓存」,只剩一条对角线,总数永远是 n。 这个互动 n=14 时两片差 7.5 倍——它随 n 线性涨;§3 量的是总计算量,n=512 时是 341.7 倍。

4

存下来要付的显存账单

K、V 不是白存的。每存一个 token 的 K 和 V,就要占一份显存, 而且这份开销随序列长度线性增长——生成长文本时它会一直涨。 存几份由注意力头数决定:32 份是标准 MHA,8 份共享是 GQA,1 份是 MQA。

人话版 KV Cache 显存 = 2 × 层数 × KV 头数 × 头维度 × 序列长度 × batch × 精度字节

2 = K 和 V 各一份

拿 7B 模型(32 层、32 个 KV 头、头维 128)代进去:每个 token 的 K、V 合起来是 2 × 32 × 32 × 128 = 262144 个数,FP16 下就是 512 KiB——一个 token 半兆。

👆 先悬停,再拖那个滑块

把鼠标停在下面公式里的符号上——下面「序列长度」「batch」「KV 头数」三个滑块和「KV 精度」按钮会分别亮起来。 公式里能悬停的那几项,下面都有对应的控件。

互动 · 显存计算器(7B 模型,真实公式)

KV Cache
—
模型权重
—
合计
—

互动 · KV Cache 随序列长度长,离 80GB 还有多远

当前 KV Cache
—
模型权重
—
KV = 权重的位置
—

⚠️ 把序列长度拉到 26K 左右看看

在 FP16、batch=1 的设置下,7B 模型的 KV Cache 就和 13 GiB 的模型权重一样大了。 拉到 128K 则是权重的约 5 倍;如果 batch=8,那就是 500 多 GiB——一张 80GB 的卡根本放不下。
这就是"长上下文"在工程上最硬的墙:不是算不动,是显存装不下。

5

同一张卡,两种负载

同一张卡,两个阶段的瓶颈完全不同

互动 · 为什么 Decode 永远是带宽受限:算术强度够不到那颗「算力/带宽」线

对比项Prefill(预填充)Decode(解码)
干什么把用户输入的 prompt 一次性全部读进去 一个 token 一个 token 地往外吐
并行度高——prompt 里所有位置同时算 极低——一次只有一个新 token
瓶颈算力受限(compute-bound) 显存带宽受限(memory-bound)
为什么 矩阵乘法的规模足够大,GPU 的算力被吃满了 为了算 1 个 token,要把全部权重读一遍。 读十几 GiB 的权重,却只做了极少的乘加——算力在空转
用户体验决定了"多久看到第一个字"(首字延迟 TTFT) 决定了"字吐得多快"(每秒多少 token)
💡 "算力在空转"这句话是所有推理优化的起点

为什么每生成一个 token 都要把权重读一遍?因为权重不在缓存里,而每个 token 的输出都要乘一遍全部权重——矩阵乘法不吃掉权重就出不来结果,所以每次都得从显存重新搬。
读十几 GiB 的权重,只用其中每个数算一次,GPU 的算力(TFLOPS)根本没被用上,卡住它的是显存带宽。
这一条直接催生了后面讲的投机解码:既然一次前向读一遍权重很浪费, 那就想办法一次前向多产出几个 token。

6

省 K、V 的几条路线

方法怎么省省多少代价
MHA
标准多头
baseline:每个头都有一份独立的 K、V 1×—
MQA
Multi-Query
所有头共享唯一一份 K、V 1/32 质量下降明显,训练也更不稳
GQA
Grouped-Query
折中:把头分成若干组,每组共享一份 K、V 1/8 ~ 1/4 质量几乎不掉。今天开源模型的默认选择
MLA
DeepSeek-V2
把 K、V 压缩成一个低秩的潜向量再缓存,用的时候再解压 约 1/15(减少 93.3%,比 GQA 还省) 实现复杂;增加了解压的计算量
PagedAttention
vLLM
不改 K、V 本身,而是像操作系统的虚拟内存一样分页管理, 消除显存碎片 浪费从 ~60% 降到 ~4% 几乎无代价。现在几乎所有推理框架都在用

互动 · 各条路线能把 KV Cache 压到多小

MQA / GQA / MLA 都是压缩 K、V 本身——少存点东西。 PagedAttention 完全不碰 K、V,它解决的是另一个问题:显存碎片—— 把缓存切成固定大小的块、按需分配。 不改模型,只改内存管理,吞吐就提升 2~4 倍。

7

KV Cache 带来的四个新问题

问题怎么回事
① 显存随长度线性涨 这是长上下文部署的头号瓶颈。不是算不动,是装不下。 128K 上下文的 KV Cache 可能比模型权重还大
② batch 被显存卡死,吞吐上不去 并发请求越多,KV Cache 占用越高。显存满了就只能排队—— 而 Decode 阶段又特别需要大 batch 才能把算力喂饱。 这是一个死循环:省显存是为了加 batch,加 batch 又要更多显存
③ 成了"有状态"服务 普通服务是无状态的:任何一台机器都能接任何请求。带 KV Cache 的推理做不到—— 这个用户的多轮对话必须路由到持有他缓存的那张卡,换一台就得重算。 这让负载均衡、故障转移、弹性扩缩容全部变复杂
④ 多轮对话怎么处理 用户改了前面的一句话,后面的缓存全部失效。 另一类情况是很多请求共享同一段开头(比如系统提示词),这段可以只缓存一份,叫 prefix caching(前缀缓存)

互动 · PagedAttention 到底省掉了什么:连续分配 vs 分页

⚠️ 一个必须记住的取舍

KV Cache 用显存换计算量。没有它,长文本生成慢到不可用; 有了它,显存成了新瓶颈。
真正的问题不是"要不要开",是"怎么让它少占点"——也就是上一节那条谱系。 实在装不下时,可以截断上下文、只留最近一段缓存,或把 KV 量化成 INT8 / INT4(第 4 节「KV 精度」按钮);少存几份就走第 6 节表里 GQA、MLA。

8

小结

这一章看起来全是工程账——FLOPs、GiB、batch。 但底下只有一句话,而且这句话的形状,所有「缓存」类优化都长得一样。

它对应哪条线 ③ 规模会赢——但这一章是那条线的反面账本。
「规模会赢」这句话要成立,前提是你能把规模装下。 这一章算的正是那个前提的价钱:模型越大、上下文越长,「装得下」本身就成了一堵新的墙。
一句话 这一章的做法,本质上是在把「反正要重算」换成「存下来」—— 同一件事不做第二遍。
自回归生成的第 t 步和第 t−1 步,有 99% 的计算是同一个计算; KV Cache 只是把那 99% 的产物收进抽屉,下次直接拿出来。
它牺牲了什么 牺牲了显存,以及一个更隐蔽的东西:无状态。
KV Cache 从来没有让计算「变少」,它只是把账单从算力那一栏挪到了显存那一栏。 挪完之后,新的墙立刻出现:显存随长度线性涨、batch 被卡死、服务从「无状态」变成「有状态」—— 这三样都不是副产物,它们就是这次交易的币种。
🎬 自己验一遍:把序列长度拖到 128K

回到第 4 节那张「显存计算器(7B 模型,真实公式)」。 把最上面那根「序列长度」滑块一路拉到 128K,盯住画布上 KV Cache ÷ 模型权重 那个读数。

单条请求时,KV Cache 约 64 GiB,已经占掉一张 80GB 卡的八成,比值从不到 1× 冲到 4× 以上—— 你花在「装已经生成的上下文」上的显存,变成了模型本身的好几倍;再把 batch 加上去就真的盖过了。
再把 「KV 头数」从 32 拖到 8(GQA),同一张图,KV 立刻掉到 1/4—— 这就是第 6 节那条谱系在做的同一件事:拿一点表达能力,去换回「装得下」。

带走一个动作:现在你应该能自己估一段上下文的 KV 显存,并说出它什么时候爆。 下一章《高效注意力》接着这条带宽受限的线,从算法上把 O(n²) 的注意力压下去;更后面的《投机解码与加速》解决另一半问题——一次前向多吐几个字。

它在暗线里站在哪

暗线这一章的回答
D 跑在什么上 带宽受限(memory-bound)——最贵的动作不是「算」,是「搬」:Decode 每生成一个 token, 都要把整个 KV Cache 从 HBM(显卡上的高带宽显存)读一遍,而算力大面积闲置。
单个请求的 decode,每读 1 个 KV 字节大约只做 1 次运算(计算量约 4·n·d,要搬 2·n·d 个数), 这个比值与 n 无关;序列越长 KV 占比越大,算术强度越靠近这个下限,第 5 节那条曲线说的「越长越低」就是这件事。常数强度的运算,永远撞在带宽那面墙上, 完整的 Roofline 账在 《硬件与算力账本》;《服务化与吞吐》也按这条线算吞吐。
E 它假设了什么 它假设「已经算出来的 K、V 永远不会变」——这在因果(causal)注意力下成立: 第 3 个位置的 K、V 只由前 3 个 token 决定,后面来多少新 token 都不会改写它。
前提一破,缓存就废了:多轮对话里用户改了一句前文,它后面全部失效,只能重算 (第 7 节第四个问题就长在这里)。
F 违背了哪个直觉 「优化 = 又快又省」是错的:省下的时间是用显存买的,显存一满,能并发的请求就变少。
极端情况下单个请求变快了,整个服务的吞吐反而下降——因为 Decode 恰恰需要大 batch 才能喂饱算力。 「更快」和「更省」在这里是对立的两个方向。
一句话带走 KV Cache

自回归推理一次前向只吐一个 token,每次都在重算前面所有词的 K、V;KV Cache 把算过的 K、V 存下来、只算新来的那一个, 把生成 n 个词的注意力计算量从 O(n³) 降到 O(n²),代价是显存随长度线性涨,省它靠 MQA → GQA → MLA 和 PagedAttention。
所以呢:Decode 阶段是显存带宽受限的——下一章《高效注意力》要接着解决的就是这件事。

9

拓展阅读

上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。

📄 这一章的说法从哪来

💻 工业界怎么写

∑ 更严格的形式