kv cache緩存
計算自注意力時,Q(查詢)向量在每次解碼步驟中都是全新的,而 K(鍵)和 V(值)向量大部分是重復的,緩存 K 和 V 可以避免大量的重復計算。
下面通過一個具體的例子來詳細解釋為什么。
自注意力機制
在解碼(生成)過程中,對于每一個新生成的 token,都有:
-
Q (Query):來自當前新生成的 token。它負責去“詢問”歷史上下文中哪些部分是重要的。
-
K (Key):來自所有已生成的 token(包括當前新的這個)。它作為“標識”,被 Q 查詢。
-
V (Value):來自所有已生成的 token(包括當前新的這個)。它包含每個 token 的實際信息內容。
注意力分數的計算方式是:Softmax( (Q * K^T) / sqrt(d_k) ) * V
具體例子
假設我們正在生成句子 “I love you”。
第 1 步:生成 “I”
-
輸入:
[BEGIN](假設起始符) -
輸出:
"I" -
此時,我們計算出了
Q1, K1, V1(對應 token “I”)。 -
KV Cache 狀態:緩存
K1, V1。
第 2 步:生成 “love”
-
輸入:
[BEGIN], I -
輸出:
"love" -
在這個步驟中:
-
我們只需要為新 token “love” 計算
Q2_new。 -
K 和 V 從哪里來?
-
一部分來自新 token “love” 自己計算出的
K2_new, V2_new。 -
另一部分來自 KV Cache 中讀取的上一步的
K1, V1。
-
-
-
所以,計算注意力時,完整的 K 和 V 是:
-
K = [K1, K2_new](拼接操作) -
V = [V1, V2_new]
-
-
Q 是什么? 只有
Q2_new。 -
計算:
Attention = Softmax( (Q2_new * K^T) / sqrt(d_k) ) * V -
更新 KV Cache:將新的
K2_new, V2_new拼接到緩存中,現在緩存里有[K1, K2_new]和[V1, V2_new]。
第 3 步:生成 “you”
-
輸入:
[BEGIN], I, love -
輸出:
"you" -
在這個步驟中:
-
我們只需要為新 token “you” 計算
Q3_new。 -
K 和 V 從哪里來?
-
一部分來自新 token “you” 自己計算出的
K3_new, V3_new。 -
絕大部分來自 KV Cache 中讀取的上一步的
[K1, K2_new], [V1, V2_new]。
-
-
-
所以,計算注意力時,完整的 K 和 V 是:
-
K = [K1, K2_new, K3_new] -
V = [V1, V2_new, V3_new]
-
-
Q 是什么? 只有
Q3_new。 -
計算:
Attention = Softmax( (Q3_new * K^T) / sqrt(d_k) ) * V -
更新 KV Cache:將新的
K3_new, V3_new拼接到緩存中。
Q 不進行緩存
從上面的過程可以清晰地看出:
-
Q 的生命周期很短:每個解碼步驟中,當前的 Q 向量只在這個步驟中被使用一次,用于和所有已緩存的 K 向量計算注意力分數。一旦這個步驟結束,這個 Q 向量就完成了它的使命,在后續的步驟中再也用不到了。
-
K 和 V 是累積的:歷史所有步驟的 K 和 V 向量在后續的每一個解碼步驟中都會被重復使用。如果不緩存,我們在生成第 N 個 token 時,就需要為前 N-1 個 token 重新計算它們的 K 和 V,這造成了巨大的計算浪費。
| 向量 | 是否緩存 | 原因 |
|---|---|---|
| K (Key) | 是 | 歷史所有 token 的 K 向量在后續每一步都會被新的 Q 查詢。不緩存會導致大量重復計算。 |
| V (Value) | 是 | 歷史所有 token 的 V 向量在后續每一步的注意力加權求和中都會被使用。 |
| Q (Query) | 否 | 每個 token 的 Q 向量僅在當前解碼步驟中使用一次,之后便失效。緩存它只會浪費顯存,毫無益處。 |

浙公網安備 33010602011771號