rag的kvcache问题
课题
CacheBlend
核心发现: KV 偏差(KV Deviation)
通过计算 KV 偏差(KV Deviation) :
Δkv(KVi, KVifull)[j] = |KVi[j] − KVifull[j]|
两种编码形式产生的差异性
这个公式描述的是:同一个 Token j 在第 i 层网络中,两种完全不同的“命运轨迹”下所产生的语义向量差 。
- KVifull[j]:
当我们将前文 Chunk A 和当前文 Chunk B 拼成一个整体输入模型时,Token
j 算出来的完美全局 KV 值
。在这个宇宙里,Token j
从第一层开始就和前文进行了充分的眼神交流(Cross-Attention) 。
- KVi[j]: Chunk B 在被缓存时,完全不知道前文 Chunk A 的存在 。Token j 只能孤立地在 Chunk B 内部做自注意力计算 。
KV 偏差(Δkv)就是这两个 KV 向量在多维空间中的几何距离 。它精准地量化了一件事:由于在预计算时漏掉了前文,这个 Token 的语义被扭曲(污染)得有多严重 。
核心发现
85% 的普通 Token —— Δkv ≈ 0 这些词在独立编码和全局编码下,算出来的 KV 向量几乎完全重合 。
为什么? 因为根据注意力稀疏性原理,语言中绝大多数词(如代词、虚词、局部修饰词)都具有强烈的“空间局限性”,它们只需要和身边的邻居词互动就能把意思表达完整 。没有前文,对它们没有任何影响。
15% 的关键 Token(HKVD) —— Δkv 彻底飙高 这些词的命运发生了剧烈改变,它们被论文定义为 High-KV-Deviation(高 KV 偏差)Token 。
也就是说整个计算过程中,只有这 15% 的高能 Token 需要被重新计算,而另外 85% 的普通缓存依然在发挥着作用
跨层相关性(Layer Correlation)
Insight 2 指出:在某一层表现出最高 KV 偏差的 Token,在它的下一层大概率依然是高 KV 偏差的。
也就是说,如果我们在第 1 层发现 Token 2、Token 3 和 Token 5 因为漏掉了前文的交叉注意力,导致它们的 KV 偏差(Δkv)飙得最高 ,那么到了第 2 层、第 3 层,这三个 Token 的 KV 偏差在所有 Token 中的排名依然会名列前茅 。
渐进式过滤机制(Gradual Filtering Scheme)
Layer 1:全量铺垫,初选候选池 在网络的第一层(Layer 1),模型会老老实实对所有输入 Token 做一次全量计算 。(因为第一层计算量极小且不依赖前面的缓存,开销可以忽略不计)。计算完后,直接通过两两比对,挑出第一层中偏差最大的一批 Token,设定一个稍大一点的初始筛选比例 r1% 。
Layer 2:局部重算,动态微调 进入第二层时,GPU 只针对 Layer 1 遗留下来的这 r1% 候选 Token 计算最新 KV 值 ,并计算出它们在这一层的真实偏差 。在这个已经缩小的候选池里,模型再次大浪淘沙,剔除掉那些偏差开始变小的 Token,筛选出更小、更准的 r2% 核心 Token(r2 < r1)喂给下一层 。
后续深层(Layer 3+):维持稳定,无脑滚动 从第三层开始,这个由 10%~15% 核心 Token 组成的“高能池”就基本在语义和空间上收敛稳定了 。后续的每一步,模型都只需要服侍好上一层传下来的这 15% 核心词 ,算完后立刻把它们跟显存里复用的 85% 旧缓存进行矩阵拼接(Expand),直接交付给当前层的全局自注意力机制 。
总结
CacheBlend 整个故事的精髓就在于对这个指标的合理利用 :
过去大家觉得,只要前面换了文档,后面的 KV 就全脏了,只能全量重算(Full Recompute) 。 但通过 KV 偏差(KV Deviation) 的视角,CacheBlend 告诉我们:脏的不是全部,只有 15% 的核心 Token 语义真正变脏了 。我们只需要像精准手术一样,把这 15% 的 HKVD Token 挑出来执行局部重计算,就能以超低的算力代价,完美还原 100% 的真值质量 。
前期学习
独立编码
全量联合编码(传统方式):把文档 A 和文档 B
拼成一个超长文本 [A + B],一起喂给模型做 Prefill。
独立编码(优化尝试):把文档 A 和文档 B 彻底切开。先让模型只看文档 A,算出一套 KV Cache;再让模型只看文档 B,算出另一套 KV Cache。最后在显存里把这两块 KV Cache 像拼积木一样硬拼接起来。
什么是 RoPE(Rotary Position Embedding,旋转位置编码)
RoPE 是在大语言模型(LLM)中被广泛使用的一种位置编码机制。
核心思想:用“空间旋转”表达“相对位置”
传统的绝对位置编码(如 BERT 的可学习位置编码,或 Transformer 原版的正余弦固定编码)通常是将位置向量直接加到 Token 的 Embedding 上。
而 RoPE 的核心创新在于:它通过旋转矩阵,在复数空间(或等价的二维分量平面)中对 Query (Q) 和 Key (K) 向量进行旋转。旋转的角度与该 Token 的绝对位置成正比。
既然有了注意力掩码(Attention Mask)来限制方向,为什么还需要位置编码?
1. 掩码(Mask)的真实作用:决定“可见性”
以大模型常用的因果掩码(Causal Mask / Triangular Mask)为例,它的唯一作用是:确保当前 Token 只能看到它前面的 Token,而不能偷看后面的 Token(即防止信息向未来泄露)。
假设我们输入一个句子:[A, B, C, D] 当模型处理到第四个词
D 时,因果掩码会告诉模型:你可以关注
[A, B, C],但不能关注后面的内容。
但是,致命的问题来了: 对于 D
而言,它知道 [A, B, C]
都是过去的历史,但如果没有位置编码,在 D
的眼里,[A, B, C]
只是一个无序的集合(袋子)。
- 它不知道
A、B、C谁离它更近。 - 它不知道顺序是
A -> B -> C还是C -> B -> A。 - 即使你把输入顺序篡改成
[C, B, A, D],只要语义不变,D对它们计算出的注意力权重(Attention Score)也是完全一模一样的。
2. 位置编码(Position Encoding)的真实作用:决定“顺序与距离”
位置编码是给每一项历史信息赋予一个精确的“坐标”。有了位置编码之后,D
不仅知道 [A, B, C]
是合法的历史信息(掩码的功劳),还知道了(位置编码的功劳):
C就在我左边(距离为 1)。B在更远一点的地方(距离为 2)。A是句子的开头(距离为 3)。
有了这个距离和顺序感,模型才能理解复杂的句法结构。例如在计算
RoPE(旋转位置编码)时,通过给
C、B、A
旋转不同的角度,模型就能天然地对距离更近的 C
产生更高的注意力倾向(远程衰减特性)。
位置编码产生的根本原因——并行计算
矩阵计算带来了一个致命的数学副作用——置换不变性(Permutation Invariance)。 也就是说,在 GPU 矩阵运算的眼里,这堆 Token 只是并排躺在显存里的“一堆没有编号的数据”,谁在前、谁在后,矩阵乘法本身是完全脱敏、完全不在乎的。如果不做任何处理,句子直接退化成了乱七八糟的“词袋”。
为了在“并行计算”这个大前提下“保护”并还原位置信息,研究人员才提出了位置编码。
既然我们不能在时间上让它们排队(因为那样就无法并行了),那我们就只能在空间(数值)上给它们打上烙印。
位置编码的工程本质就是:
- 并行照常进行:所有的 Token 依然做成矩阵,同时输入,同时计算。
- 人造坐标系:在输入矩阵的一瞬间,强行为第 1 个 Token 的向量加上(或通过 RoPE 旋转)一个代表“位置 1”的微小信号;为第 2 个 Token 加上一个代表“位置 2”的信号……
这样一来,GPU 依然在轰轰烈烈地进行着全并行的矩阵大乘法,但每一个 Token 的向量骨子里都已经携带了属于它自己的“数字工牌”。
transformer的并行计算
GPU是如何一步步计算Transformer中的自注意力机制的?_哔哩哔哩_bilibili
阶段一:线性映射阶段(QKV 矩阵的生成)
你的理解:每个 token 独立与权重相乘得到各自的 qkv,然后拼接形成单头的 QKV 矩阵。
在概念和数学逻辑上,你说得完全正确!每个 Token 的确是“各走各的路”,互不干扰。
但在实际的工程实现(GPU 算子)*中,顺序刚好是反过来的:我们不是先让它们单独相乘再拼接,而是*先拼接(堆叠),再整体相乘。
- 打包(Stack):在数据刚准备输入模型时,我们就已经把这一句话的 N 个 Token 的向量叠在一起,拼成了一个大矩阵 X(形状为 [N, d])。
- 一发入魂(GEMM):把这个大矩阵 X 直接整体乘以权重矩阵 W。
为什么说这依然是并行的? 因为在 GPU 内部执行大矩阵乘法 X × W 时,硬件会自动把矩阵 X 的每一行(也就是每个 Token)分发给不同的计算核心(Thread Block)。在物理层面上,各个核心是同时计算、同时输出结果的。所以,虽然代码上只写了一行矩阵乘法,但它在硬件里展现的就是全自动的、按行并行的“并行投影”。
阶段二:注意力分数计算阶段(Q × KT)
你的理解:将 Q 矩阵拆分,分别和完整的 K 矩阵相乘,最后拼接形成完整的注意力分数矩阵。
这个理解极其深刻,完全切中了矩阵乘法的物理本质! 让我们用矩阵乘法的公式来验证你的想法。假设我们要计算注意力分数矩阵 A = Q × KT。
根据矩阵乘法的规则,输出矩阵 A 的第 i 行,仅仅取决于 Q 矩阵的第 i 行和整个 KT 矩阵:
A[i, : ] = qi × KT
这意味着:
- 想要算出
“苹果”(假设是第 4 行)对所有词的分数,GPU 只需要拿着 q4 去和完整的 K 矩阵的所有列做点乘。 - 在算这一行时,完全不需要知道 q1, q2, q3 是多少,也完全不需要等待它们的计算结果。
在 GPU 的实际调度中,这被称为“行并行”。GPU 会启动 N 组线程,第一组专门算 q1 × KT,第二组专门算 q2 × KT……它们在芯片内部同时开工,算完后直接写进显存中对应的位置,连显式“拼接”的开销都省了。
transformer训练与推理并行计算的差异
一、 训练阶段:完美的“全并行”(上帝视角)
在训练大模型时,你的核心任务是“预测下一个词”。这时候,整条语料(比如一句 1024 个 Token 的小说片段)已经安安静静地躺在你的显存里了。
既然整个序列 N = 1024 已经是已知的,我们就可以利用 Transformer 的并行特性,一次性把它们全部处理完。
1. 矩阵的绝对并行
在训练时,你的输入矩阵 X 的形状是 [1024, d]。
通过我们之前聊过的行并行:
- 整个 Q、K、V 矩阵的形状都是 [1024, d],一次大矩阵乘法(GEMM)全部算完。
- 计算注意力分数 Q × KT 时,是 [1024, d] × [d, 1024],直接喷吐出一个 [1024, 1024] 的超大注意力矩阵。
2. 既然是一起算的,怎么体现“先后顺序”?
你可能会问:“全一起算,第 500 个词不就把第 800 个词的答案偷看光了吗?”
这就是因果掩码(Causal Mask)的妙处。在计算出 [1024, 1024] 的注意力矩阵后,我们用一个上三角矩阵把右上角的信息全部强行抹成 −∞(Softmax 后变成 0)。
- 重点在于:这个“抹除”操作是在矩阵算完之后统一执行的。硬件上,第 500 个词和第 800 个词对应的矩阵行是同时在不同的 Tensor Core 上被计算出来的。 * 模型在一瞬间,同时完成了“用前1个词预测第2个词”、“用前2个词预测第3个词”……直到“用前1023个词预测第1024个词”的所有训练。
训练的属性:计算密集型(Compute-Bound)。GPU 的计算核心(Tensor Cores)一刻不停地在做大规模矩阵乘法,利用率极高,这就叫全并行。
二、 推理阶段:割裂的“两段式”(从并行跌落到串行)
当你把训练好的模型部署到线上(比如你现在在和 AI 聊天),情况发生了翻天覆地的变化。因为“未来的词”在物理上还不存在,是需要大模型一字一字吐出来的。
推理过程必须被生生割裂为两个完全不同的阶段:
阶段 1:Prefill(预热/交互阶段)—— 它是并行的!
当你把一段 500 字的 Prompt 发给大模型时,这 500 个词对模型来说是已知的前文。
- 动作:模型会把这 500 个 Token 绑定成一个 [500, d] 的矩阵,直接整体送入 GPU。
- 本质:这个阶段的计算方式和“训练”一模一样! 也是利用行并行,在一瞬间把这 500 个词的两两注意力、QKV 全部算完。
- 副产品:算完之后,这 500 个词的 K 和 V 向量会被存进KV Cache里,留着备用。
阶段 2:Decode(逐字生成阶段)—— 它是串行的!
Prefill 结束后,模型吐出了第一个新词(第 501 个词)。接下来,要生成第 502 个词,模型就必须把第 501 个词当作输入再喂给自己。
这时候,并行的神话破灭了:
- 输入极其单薄:此时的新输入 X 只有一个 Token,矩阵形状变成了 [1, d]。
- Q 矩阵变成了单行:因为输入只有一行,所以算出来的 Query 矩阵 Q 的形状也是 [1, d](只有当前这个新词有 Query)。
- K 和 V 靠缓存维持:模型不需要重新计算前 500 个词,它直接从 KV Cache 里把之前存好的 [500, d] 的 K 和 V 捞出来,和当前第 501 个词新算出来的 k501, v501 拼接,组织成一个 [501, d] 的完整历史。
推理的属性:内存带宽限制型(Memory-Bound)。GPU 大部分时间没有在做高强度的计算,而是在干苦力活:不停地把显存(HBM)里巨大的 KV Cache 搬运到片上缓存(SRAM)里,去和那一行小小的 Q 做乘法。这也是大模型生成速度(Tokens/s)会遇到瓶颈的根本原因。