深度学习进阶(二十八)现代 LLM 的核心架构设计其三:Decoder-Only 下的 KV Cache 河内机器人
作者:admin | 分类:河内机器人 | 浏览:21 | 日期:2026年06月08日如果你用过 ChatGPT,一定好奇为什么大模型生成第一个字要等半天,之后每个字都出的飞快?如果你自己部署过本地大模型,一定见过「KV Cache 占用」这个参数,也一定好奇它到底占了那么多内存是用来干嘛的?其实这个看起来不起眼的 KV Cache,就是 Decoder-Only 架构大模型能够流畅生成文本的核心优化,没有它,大模型生成速度至少慢十倍,根本没法用。
这一篇我们就从 Attention 基础开始,拆解 Decoder-Only 下的 KV Cache 到底是什么,为什么需要它,工程上又有哪些优化思路。
一、先回到原生自注意力:为什么原生推理这么慢
我们先回忆一下 Decoder-Only 架构的自注意力计算:对于输入序列 X = [x₁, x₂, ..., x_t],我们首先把每个token投影成三个向量:Query q_i、Key k_i、Value v_i。
然后第 i 个位置的注意力输出 o_i 计算公式是:
𝑜
𝑖
=
∑
𝑗
=
1
𝑖
exp
(
𝑞
𝑖
𝑇
𝑘
𝑗
)
∑
𝑙
=
1
𝑖
exp
(
𝑞
𝑖
𝑇
𝑘
𝑙
)
𝑣
𝑗
o
i
=
j=1
∑
i
∑
l=1
i
exp(q
i
T
k
l
)
exp(q
i
T
k
j
)
v
j
原生自推理的时候,每生成一个新token,我们就要重新计算一整遍所有位置的 Attention:
比如你输入了100个token,生成第101个token的时候,要重新算101个位置的Q、K、V,再算一遍101×101的注意力矩阵;
生成第102个token的时候,又要重新算102个位置的Q、K、V,再算一遍102×102的注意力矩阵……
这不就是纯纯的浪费吗?因为之前100个token的K和V早就算过了,生成下一个token的时候,前面的K和V根本不会变啊!为什么要重新算一遍?
KV Cache 就是把这些已经算好的 K 和 V 存起来,下次生成新token的时候直接复用,不用再重新计算,一下子就省下了海量的重复计算。
二、KV Cache 到底存了什么?核心逻辑拆解
我们用生成流程一步步看,KV Cache 到底是怎么工作的:
第一步:Prefill 阶段(处理用户输入 prompt)
用户输入了长度为 n 的 prompt,我们第一次计算,要把所有 n 个token的Q、K、V全部算出来:
只需要生成下一个新token,所以只需要最后一个位置的输出
把所有 n 个token的 k₁...kₙ 和 v₁...vₙ 保存到缓存里,这就是初始的 KV Cache
第二步:Autoregressive 解码生成阶段
现在我们要生成第 n+1 个token:
只需要对第 n+1 个token计算新的 q_{n+1}, k_{n+1}, v_{n+1}
直接把 k_{n+1}, v_{n+1} 拼到我们之前存好的 KV Cache 后面,现在 Cache 里就有 n+1 组 K、V
只用 q_{n+1} 和整个 Cache 里的所有 K、V 计算注意力,输出结果,得到第 n+1 个token
接下来生成第 n+2 个token的时候,重复上面的步骤:只算新的K、V,拼到Cache后面,直接用,不用碰之前算好的内容。
整个过程下来,原来每一步都要算O(t²)的复杂度,变成了:Prefill 阶段算一次O(n²),之后每一步只算O(t),复杂度直接降了一个量级!这就是为什么第一个字慢,后面越来越快——第一个字要算整个prompt的Prefill,后面每个字只需要算一步,当然快。
三、Decoder 掩码为什么适配 KV Cache?
Decoder-Only 天生自带因果掩码(Causal Mask):每个位置只能看到自己和前面的token,看不到后面的。刚好 KV Cache 是按生成顺序存K、V,新生成的token只会把自己的K、V拼在最后,不会影响前面的因果关系,天然适配掩码规则:
新的Query只需要和所有已经生成的K、V做注意力,刚好符合因果约束
不需要对已经存好的K、V做任何修改,直接拼接就可以用,完美适配自回归生成
如果是 Encoder-Decoder 架构,KV Cache 一般会给 Encoder 的输出存一份,Decoder 的 KV 也存一份,逻辑和 Decoder-Only 类似,都是复用已经算好的结果。
四、工程上的那些坑:KV Cache 的内存和精度问题
KV Cache 好处这么多,但是也带来了新问题:它非常占内存,我们来算一笔账:
比如一个7B模型,每个参数用FP16存储,单个token的 K+V 维度是 2 * 层数 * 头维度 * 头数,简单算就是:
2 × 32层 × 128 × 32 = 262144 个元素,每个元素FP16占2字节,单个token就占了512KB;
如果输入序列长度是4096,总KV Cache占用就是 4096 × 512KB = 2GB;如果序列长度开到8192,直接就是4GB,占了快一半的显存量,难怪大家部署大模型的时候,KV Cache占用总是拖后腿。
工业界针对这个问题也做了非常多优化,我们整理几个常见的方向:
1. 量化压缩 KV Cache
最直接的思路就是把KV从FP16量化到INT8甚至INT4,精度损失几乎可以忽略,内存直接砍半甚至砍到四分之一。现在很多本地部署框架比如llama.cpp、ollama默认都开了KV量化,就是为了省内存。
有研究证明,KV的分布比模型参数更平滑,量化误差对最终输出的影响比模型参数量化更小,所以压缩KV Cache是性价比非常高的优化。
2. 滑动窗口 KV Cache
这个思路就是:太远的token对当前生成几乎没用,所以只存最近N个token的KV,超过窗口就扔掉,这样不管生成多长的文本,KV Cache占用永远不会超过N个token的大小,内存占用固定,适合生成长文本。
比如GPT-16k其实就用了滑动窗口的思路,Local Attention本质就是滑动窗口KV Cache,对多数场景下的生成质量几乎没有影响,但是内存占用降了非常多。
3. 重用KV复用与页式管理
这就是vLLM里面提到的PagedAttention思路,解决的是动态分配KV的内存碎片问题:
传统KV Cache每次分配连续的内存块,不同请求的KV不一样长,时间长了就会产生很多内存碎片,实际能用的内存比总内存小很多。
PagedAttention把KV分成固定大小的页,逻辑上连续的页物理上可以不连续,通过页表映射,不仅减少了碎片,还能让多个请求共享相同前缀的KV Cache(比如多个用户问同一个问题的开头,前面的KV可以直接共享,不用重复计算),内存利用率提升了好几倍,同时能支持更大的batch,吞吐量直接翻倍。
4. 持久化KV Cache
对于对话场景,用户每次发新消息,之前几轮对话的KV其实大部分都不变,只有最后加了新的用户消息,所以可以把之前轮次的KV Cache持久化存在内存里,不用每次用户发消息都重新算整个历史对话的KV,Prefill时间直接省了一大块,响应速度快很多。现在很多AI对话框架都用了这个优化。
五、对推理吞吐量的影响
我们最后总结一下KV Cache对大模型推理的影响:
没有KV Cache:每个token的计算量随序列长度线性增长,总复杂度O(n²),长文本下根本没法用,生成速度慢到无法忍受
有了KV Cache:总复杂度是O(n),Prefill只算一次,后面每个token只算一步,吞吐量提升一个量级,才有了我们现在能用的流畅对话体验
代价就是占用内存,但是通过量化、分页、滑动窗口这些优化,现在已经能很好的平衡内存占用和速度
六、小结
KV Cache说穿了就是一个非常简单的空间换时间优化:把已经算好不会变的结果存起来,避免重复计算,但是就是这个小小的优化,直接让Decoder-Only大模型的自回归生成从理论变成了可用的产品,是现代大模型推理不可或缺的核心设计。
现在工业界还在不停优化KV Cache,从PagedAttention到量化压缩,再到共享缓存,本质都是在平衡速度、内存和生成质量,这些优化也是让大模型能成本越来越低,落地越来越广的核心动力。