kvcache
简单回顾attention流程
1 | 【输入向量 X】(来自上一层或Embedding) |
qkv矩阵的含义是怎样的
我觉得可以从他们本身的名字入手
1. Q (Query - 查询)
- 含义:当前词“试图去关注(或询问)其他词”的向量表示。
- 角色:它是主动方。在 Self-Attention 中,每个词都会轮流作为 $Q$,去询问句子里的所有词(包括它自己):“你们谁和我有关系?关系有多大?”
2. K (Key - 键)
- 含义:当前词“被其他词关注时的特征(或身份标签)”的向量表示。
- 角色:它是被动方。它存在的意义就是和 $Q$ 做点积(Dot Product),来计算两个词之间的相关性或相似度。
3. V (Value - 值)
- 含义:当前词“本身所包含的、最核心的内容信息”。
- 角色:一旦 $Q$ 和 $K$ 算出了注意力权重(即谁和谁更相关),这些权重就会乘到对应的 $V$ 上。最终把这些加权后的 $V$ 累加起来,就得到了该词融合了上下文信息后的新表示。
我的理解:
q可以理解成新词关注历史字符的向量,也就是query,k则是历史字符被关注时的向量,两者相乘可以获得关联度
v可以代表每一个词在进入这一层时,通过与权重矩阵相乘,提取出了它最核心的、未经污染的语义特征(比如“苹果”包含了:水果、甜的、红的、科技公司、iPhone 等属性)。这个 V 就是这个词的“属性本体”。
kvcache是什么
在多次对话的过程中,前面的历史字完全没变,它们算出来的 $K$(身份标签)和 $V$(词本身的属性)也就是固定不变的。那算过一次之后,直接把它们存在内存(显存)里,后面就可以直接拿来用,因此降低了计算量,提高了响应的速度
那为什么不存q矩阵呢
这个问题的关键还是要理解qkv矩阵的含义和kvcache的流程
q矩阵代表着最新字符对于历史字符关注的向量,而新字符是不对吐出来实时变化的,所以存储q矩阵没有意义
而这里要区分一下大模型生成的两个阶段:
阶段一:Prefill(预填充阶段 —— 读懂你的 Prompt)
当你把一句话(比如“请问什么是Attention”)发给模型时,模型会一次性把这几个字全部输入进去。
- 这个时候,所有词都要互相看。所以每个词的 $Q、K、V$ 全都要计算,以此来捕捉整个句子的上下文语义。
- 计算完后,模型会把这些历史词的 $K$ 和 $V$ 老老实实地存进内存里。这就是 KV Cache 的初始化。
阶段二:Decoding(解码阶段 —— 逐字生成)
这就是你刚才描述的绝妙场景。假设模型现在吐出了一个新词:“它”。
$Q$ 是实时的、一次性的:
“它”这个新词作为当前的唯一主角,会产生一个崭新的 $Q{它}$。它需要拿着这个 $Q{它}$ 去跟前面所有的历史词进行比对。
💡 为什么不用缓存 $Q$? 因为当“它”这个词处理完、吐出下一个词(比如“是”)之后,主导权就交给了“是”的 $Q$。旧的 $Q_{它}$ 在未来的生成中永远都不会再被用到了。它是一次性的消耗品。
$K$ 和 $V$ 必须死死存住:
为了算出“它”和历史词的关联度,你需要历史词的 $K$;为了融合出“它”的新向量,你需要历史词的 $V$。
- 如果我们不缓存 $K$ 和 $V$,每生成一个新词,模型就得把前面所有的历史词重新做一遍矩阵乘法,去重新算一遍它们的 $K$ 和 $V$。
- 随着生成的句子越来越长,这种重复计算会呈爆炸式增长(计算量 $O(N^2)$),模型会越写越卡。
- 有了 KV Cache:历史词的 $K$ 和 $V$ 已经在内存里躺着了。新词“它”算完自己的 $Q、K、V$ 后,拿自己的 $Q$ 去和内存里的一大推历史 $K$ 做乘法,算完后,再顺手把自己的 $K{它}$ 和 $V{它}$ 也追加到缓存的末尾,供下一个词使用。