Transformer KV Cache 入门笔记
Transformer KV Cache 入门笔记
1. 先说结论
KV cache 是 Transformer 在生成阶段常用的一种优化手段。
它的核心思想是:
- 历史 token 对应的
K和V已经算过一次了 - 后面每生成一个新 token,不需要把整段历史重新计算一遍
- 只需要计算“新 token”的结果
- 历史
K/V直接从缓存里拿来复用
所以,KV cache 的目的不是改变模型结构,而是减少重复计算,让推理更快。
2. 它属于 Transformer 工程的哪一部分
一个完整的 Transformer 工程,大体可以分成几块:
- 模型本体
- 训练部分
- 推理部分
- 工程优化部分
- 服务化部分
其中:
attentionembeddingMLPLayerNorm
这些属于模型本体。
而 KV cache 更准确地说属于:
- 推理部分
- 同时也属于推理优化的一部分
因为它主要解决的是“生成文本时重复计算太多”的问题。
3. KV cache 不是模型权重的一部分
这是最容易混淆的地方。
要先分清两类东西:
3.1 模型权重
模型文件里保存的是参数,例如:
WqWkWv
它们是线性层的权重矩阵,是训练后学出来的固定参数。
这些参数的作用是把输入向量映射成 Q/K/V:
Q = x @ Wq
K = x @ Wk
V = x @ Wv也就是说:
- 权重文件里存的是“怎么计算 Q/K/V 的规则”
- 不是某个具体 token 的
Q/K/V结果
3.2 前向计算产生的激活值
当一个具体 token 真正进入模型后,才会得到:
- 当前 token 的
Q - 当前 token 的
K - 当前 token 的
V
这些是运行时产生的中间结果,通常叫激活值或者中间张量。
KV cache 缓存的是这些运行时的 K/V 张量,而不是权重参数。
4. 什么是前向计算
前向计算就是:
- 把输入送进模型
- 按照模型结构一层一层往前算
- 最后得到输出
对语言模型来说,可以简单理解成:
token
-> embedding
-> positional encoding / RoPE
-> 多层 Transformer block
-> 输出 logits
-> 下一个 token 的概率这整个过程就叫前向传播。
5. Transformer 的前向结构
一个标准的 GPT 类 Transformer 前向计算,大致是这样:
输入 token
-> token embedding
-> position information
-> attention
-> residual / norm
-> MLP
-> residual / norm
-> 重复多层
-> logits其中核心 block 可以理解成:
- Attention:看上下文
- MLP:对当前位置的表示再加工
所以一个 block 的感觉可以记成:
先看别人,再自己想一遍6. Attention 里到底在算什么
在 attention 里,会从输入表示里算出三样东西:
QKV
可以先用最朴素的理解:
Q:当前这个位置想问什么K:历史每个位置的索引或标签V:历史每个位置真正提供的信息
计算流程大致是:
- 用
Q和所有K比较相似度 - 得到注意力权重
- 用这些权重对
V做加权求和
于是当前位置就能从历史上下文中取到有用信息。
7. 为什么 KV cache 缓存的是 K 和 V,而不是 Q
这是理解 KV cache 的关键。
7.1 为什么缓存 K 和 V
在自回归生成里,历史 token 的表示一旦已经算过,它们对应的 K/V 对后续步骤是可以复用的。
因为后面的 token 在做 attention 时,需要访问的是:
- 历史 token 的
K - 历史 token 的
V
这些历史内容不会因为新 token 到来而重新定义。
7.2 为什么通常不缓存 Q
Q 是“当前这一步的问题”。
每走到一个新时间步,当前 token 都不一样,所以当前步的 Q 也必须重新计算。
旧 token 的 Q 只在它自己当时那一步有用,后面的新 token 并不会再拿旧 token 的 Q 来提问。
所以可以简单记成:
Q是当前问题K/V是历史资料
后面的新问题需要反复查阅历史资料,所以缓存 K/V 最划算。
8. 用一个简单例子理解
假设提示词是:
I love NLP第一次前向时,模型会对这 3 个 token 都计算出各自的:
Q1, K1, V1Q2, K2, V2Q3, K3, V3
如果没有 KV cache,下一步要生成新 token 时,模型通常要重新把整段历史再算一遍。
如果有 KV cache,就会把历史的:
K1, K2, K3V1, V2, V3
存起来。
当新 token 到来时,只需要新算:
Q4, K4, V4
然后用:
- 当前步的
Q4 - 历史缓存的
K1, K2, K3 - 当前的
K4 - 历史缓存的
V1, V2, V3 - 当前的
V4
一起完成注意力计算。
这就是“后续每一步只喂最后一个 token 也能继续生成”的原因。
9. KV cache 在生成阶段是怎么工作的
生成的第一步通常是:
- 把整段 prompt 一次性送进模型
这一步的作用是把整段上下文先建立起来,并算出最初的一批 K/V。
后面的每一步:
- 只输入最新生成出来的那个 token
- 从 cache 里取出历史
K/V - 把新的
K/V拼接上去 - 用当前的
Q去对这些历史和当前内容做注意力
所以 KV cache 的典型流程是:
- 首轮读完整 prompt
- 存历史
K/V - 后续每轮只算 1 个新 token
- 拼接旧
K/V和新K/V - 再继续生成
10. 它和 bbycroft.net/llm 的关系
bbycroft.net/llm 更像是在讲:
- 一个 GPT 类 Transformer 的标准前向流程
- embedding、attention、MLP、residual、logits 这些主干结构
它主要回答的是:
- 模型本身怎么计算
而 KV cache 主要回答的是:
- 生成时怎么避免重复计算
所以:
bbycroft展示的是主干模型结构- KV cache 是推理过程中的工程优化逻辑
因此在类似 bbycroft 这种“架构流程可视化”里,KV cache 往往不会被单独作为一个大模块画出来。
这不代表 GPT-3 一类模型没有 KV cache,而是说明:
- KV cache 不是“GPT-3 独有的新层”
- 而是 decoder-only Transformer 在推理生成时常见的实现优化
11. 在这个仓库里该看哪些部分
如果要在 labml.ai 这个项目里学习 KV cache,最重要的是下面几处。
11.1 基础背景
transformers/mha
这里帮助理解:
Q/K/V是什么- attention 的输入输出形状是什么
- 为什么后续能够只缓存一部分中间结果
11.2 真正的 KV cache 入口
neox/modelneox/samples/generateneox/utils/cache
它们分别回答:
- attention 层里如何接入 KV cache
- 生成脚本如何启用 cache
- cache 容器本身怎么设计
12. 用最简短的话再总结一次
你可以把今天关于 KV cache 的理解压缩成下面这几句:
- KV cache 不是模型权重,而是前向计算时产生的中间结果缓存。
- 缓存的通常是历史 token 的
K和V,而不是Q。 - 它主要用于自回归生成阶段,让模型不必每次都重算整段历史。
- 它不改变 Transformer 的基本结构,只是推理优化。
- 在这个项目里,学习 KV cache 最直接的入口是
NeoX相关代码,而不是最基础的GPT教学实现。
13. 论文图、bbycroft、模型结构、模型参数之间是什么关系
初学时很容易把下面几件事混在一起:
- 模型结构
- 模型参数
- 前向计算结果
其实它们不是一回事。
13.1 模型结构
模型结构是指:
- 有哪些模块
- 这些模块怎样连接
- 数据怎样在模块之间流动
例如:
- embedding
- attention
- MLP
- residual
- LayerNorm
- logits 输出层
这部分更像“电路图”或者“流水线图”。
13.2 模型参数
模型参数是这些模块内部被训练出来的可学习数值,例如:
Q weightsK weightsV weights- bias
- MLP 的线性层参数
- embedding matrix
它们保存在权重文件里。
13.3 前向计算结果
当前输入送进模型后,会动态产生一批中间结果,例如:
Q vectorsK vectorsV vectorsAttention MatrixAttn Matrix Softmax- 最终输出向量
这些不是预先保存在模型文件里的,而是运行时算出来的。
所以:
- 论文图和
bbycroft主要在展示模型结构和前向流程 - 权重参数通常藏在这些模块内部
- 绿色的中间块更接近“运行时激活值”
14. 如何理解 bbycroft 视图中的颜色
在 bbycroft.net/llm 的 nanoGPT 类视图里,可以用一个粗略但实用的方法理解颜色:
- 蓝紫色:更接近模型参数
- 绿色:更接近前向计算过程中的中间结果
但要注意:
- 蓝紫色不等于“模型结构”
- 更准确地说,蓝紫色更像“参数层”
- 而结构层是这些模块彼此的组织关系
所以更标准的说法是:
- 结构层:模块怎么连
- 参数层:模块内部有哪些可学习参数
- 激活层:运行时算出来的中间结果
15. 图中哪些部分是 KV cache 真正缓存的对象
如果参考 bbycroft 或类似可视化,KV cache 通常缓存的是:
K vectorsV vectors
而不是:
Q weights / K weights / V weightsAttention MatrixAttn Matrix SoftmaxV Output
原因是:
- 下一步生成时,需要新的
Q - 新的
Q要去查询历史 token 对应的K/V - 因此必须保留历史的
K vectors和V vectors
而像 V Output 这样的结果,已经是某一步 attention 完成后的输出,不适合作为后续步骤重复查询的基础材料。
16. 为什么 K = x @ Wk 里的 K 不是权重
这是最关键的概念关口。
表达式:
K = x @ Wk里包含三个完全不同的角色:
x:输入向量Wk:权重矩阵K:输出结果向量
其中:
x不是权重,它是当前 token 的表示Wk才是模型参数K是输入经过参数变换后得到的结果
所以“向量”不等于“权重”。
因为:
- 输入可以是向量
- 输出也可以是向量
- 权重更常见是矩阵
是否是权重,不看它是不是向量,而看它是不是“被训练保存的参数”。
17. 一句最重要的区分
你可以强行记住下面这句:
Weights/Bias是模型参数,保存在权重文件中Vectors是前向计算时根据当前输入动态算出来的激活值
因此:
- KV cache 缓存的是历史 token 的
K vectors和V vectors - 不是模型里的
K weights和V weights
18. KV cache 是怎样“侵入”模型结构实现的
KV cache 不会把 Transformer 改造成另一种结构。
它不改变“大框架”:
- 还是 attention
- 还是 MLP
- 还是 residual 和 norm
它侵入的是:
- attention 模块的
forward实现
普通 attention 大致是:
x
-> 线性投影得到 q, k, v
-> attention(q, k, v)
-> output带 KV cache 的 attention 则更像:
x
-> 线性投影得到当前 q, k, v
-> 读取历史 k/v cache
-> 拼接成完整 k/v
-> attention(q, all_k, all_v)
-> 把新的 k/v 写回 cache
-> output所以它的本质是:
- 不改 attention 的数学主干
- 但改变 attention 在推理时的执行方式
19. 开源模型的权重文件里,参数会标明属于哪个部分吗
通常会,而且一般不是一堆完全无名的数字。
很多开源模型的权重,从逻辑上可以理解成:
- 参数名 -> 张量
也就是一种“带名字的张量字典”。
例如在 PyTorch 风格里,概念上常见形式类似:
{
"transformer.wte.weight": ...,
"transformer.h.0.attn.c_attn.weight": ...,
"transformer.h.0.attn.c_attn.bias": ...,
"transformer.h.0.mlp.c_fc.weight": ...,
}这些名字通常能告诉你:
- 这是哪一层
- 属于 attention 还是 MLP
- 是 weight 还是 bias
有些实现会把多个参数合并存储,例如把 Q/K/V 合成一个更大的投影矩阵,但通常仍然会保留能识别模块归属的命名信息。
20. 为什么缓存这些 K/V 的利用率仍然很高
初看时容易怀疑:
- 模型回答本来就可能不同
- 不同用户的 prompt 也往往不同
- 那么缓存历史
K/V的利用率是不是很低
这个担心抓错了重点。
KV cache 的高利用率,主要不是来自“不同用户之间复用同一个问题”,而是来自:
- 同一轮生成过程内部,对相同历史前缀的反复复用
20.1 单轮生成内部的高复用
假设模型已经读完一段很长的 prompt,并开始一个 token 一个 token 地往后生成。
如果没有 KV cache,那么每生成一个新 token,都要把整段历史重新参与计算。
如果有 KV cache,那么:
- 历史 token 的
K/V只算一次 - 后面每一步只新增当前 token 的
K/V - 历史部分直接复用
这就是 KV cache 最主要、最稳定的收益来源。
20.2 回答可能不同,并不影响这件事
即使模型是采样生成、同一个问题可能给出不同答案,也不妨碍 KV cache 有用。
原因是:
- 在分叉发生之前,共同前缀仍然相同
- 相同前缀对应的历史
K/V依然可以复用 - 一旦生成路径分叉,只是从分叉点之后各自维护各自的 cache
所以:
- KV cache 关注的是“已有前缀能不能少重算”
- 而不是“最终答案会不会完全一致”
21. 单轮 KV cache 和多轮 prefix/session cache 不是一回事
这两个概念经常被混用,但它们解决的问题不同。
21.1 单轮 KV cache
它发生在:
- 同一次生成请求内部
例如模型正在生成一段回答,当前回答的第 2 个 token、第 3 个 token、第 4 个 token ……都会复用前面已经算好的历史 K/V。
它解决的是:
- 同一轮生成中,不要重复计算已经存在的历史前缀
21.2 多轮 prefix cache / session cache
它发生在:
- 用户发起新的对话轮次时
比如上一轮已经有一大段稳定历史,这一轮只是多问了一个小问题。
如果系统支持更高级的会话缓存,就可能复用:
- 之前轮次中不变的前缀
- 稳定的 system prompt
- 共享的工具上下文前缀
它解决的是:
- 新一轮请求时,如何避免把完全相同的长前缀又从头处理一遍
21.3 一个简单区分
可以这样记:
- KV cache:解决“一轮回答内部”的重复计算
- prefix/session cache:解决“多轮对话之间”的前缀复用
22. 为什么粗暴 agent 会出现 token 消耗爆炸
很多简单 agent 或聊天项目,会采用一种最直接的实现方式:
- 保存全部历史消息
- 每一轮新请求时,把历史原样重新拼接成新的 prompt
- 再完整发给模型
这种做法的优点是实现简单,缺点是:
- 输入 token 越聊越多
- 前向计算成本越来越高
- 延迟越来越高
- 费用越来越高
这类问题在一些早期 agent 项目里很常见。
它的问题根源不是:
- “KV cache 没有意义”
而是:
- 应用层每一轮都把全文历史重新喂给模型
- 没有做多轮前缀复用
- 没有做历史摘要或裁剪
- 会话管理过于粗暴
23. 为什么多轮对话不等于天然命中同一份 KV cache
比如你先问一个问题,再根据回答继续追问。
直觉上会觉得:
- 新问题只是旧对话的延续
- 那是不是天然命中了上一轮的 KV cache
严格来说,不一定。
原因是:
- 单轮 KV cache 通常只服务于当前这一轮生成过程
- 新的一轮请求到来时,模型往往会把它视为一个新的前向起点
这时是否还能继续复用上一轮的结果,要看系统有没有实现:
- 会话级前缀缓存
- 稳定前缀复用
- 状态外置
- 历史压缩
所以更准确的说法是:
- 单轮回答内部,一定强依赖 KV cache
- 多轮对话之间,是否复用上一轮结果,要看平台如何实现会话管理
24. 当前上下文窗口占用,不等于累计 token 消耗
这是理解产品现象时非常重要的一点。
24.1 当前上下文窗口占用
它回答的是:
- 当前这一轮真正塞进模型窗口里的上下文占了多少
这是一个“当前状态”的概念。
24.2 累计 token 消耗
它回答的是:
- 整个会话到目前为止,总共发送和生成了多少 token
这是一个“历史累计量”的概念。
所以:
- 一个会话的累计 token 消耗可以很大
- 但当前窗口占用仍然可能不高
因为系统可能已经做了:
- 摘要压缩
- 上下文裁剪
- 分层保留
- 某些状态外置
25. 如何理解 Codex 这类平台型 agent 的现象
如果你在一个平台型 agent 中连续进行了很多轮问答,但当前上下文窗口占用增长得很慢,这通常意味着:
- 它大概率不是每轮都把所有可见历史原样重放给模型
但这并不自动等于:
- 它一定实现了“跨请求持久化 KV cache”
更可能的情况是多种机制混合:
- 稳定前缀复用
- system/developer prompt 的服务端优化
- 消息结构化存储
- 状态外置
- 轻量摘要
- 接近阈值时再进行更激进的压缩
因此,比较稳妥的判断是:
- 这类平台通常已经接管了部分上下文管理
- 所以它看起来比粗暴 agent 更省窗口
- 但仅凭现象,无法精确反推出后端具体实现细节
26. 这一段讨论可以压缩成哪几句
- KV cache 的主要价值来自单轮生成内部对历史前缀的重复利用。
- 多轮对话能否进一步复用上一轮结果,取决于系统有没有实现 prefix/session cache 或其他上下文管理机制。
- 粗暴 agent 的主要问题不是模型本身,而是每轮原样重放全部历史,导致 token 成本和延迟不断上升。
- “当前上下文窗口占用” 和 “累计 token 消耗” 是两个不同指标,不能混为一谈。
- 平台型 agent 如果多轮后窗口增长仍然很慢,通常说明它不只是简单拼接历史消息。