← 返回博客

The KV Cache 是怎么解决计算冗余问题?

KV 缓存的工作原理:

它对应的 K 和 V 在不同生成步骤中是完全相同的。

这是由两个条件决定的:

  • 模型权重在推理阶段固定;

  • Causal Mask 保证当前位置只能看到自身以及之前的位置。

因此,KV Cache 的基本思想非常直接:

K 和 V 只计算一次,然后保存起来。之后的生成步骤直接读取缓存,而不是重新计算。

概念本身并不复杂,但真正重要的是:

  1. KV Cache 到底保存了什么?

  2. Forward Pass 如何发生变化?

  3. 为什么只保存 K/V,而不保存 Q?

  4. 它节省了多少计算?

  5. 又带来了多少显存开销?

What Exactly Gets Stored?

KV Cache 究竟存储什么?

对于模型中的每一个 Attention Layer,我们都保存两个矩阵:

纯文本
K_cache: positions 0, 1, 2, ..., n-1 的所有 Key vectors
V_cache: positions 0, 1, 2, ..., n-1 的所有 Value vectors

也就是说:

纯文本
Layer 0:
    K_cache
    V_cache

Layer 1:
    K_cache
    V_cache

Layer 2:
    K_cache
    V_cache

...

Layer L:
    K_cache
    V_cache

需要特别注意:

我们保存 K 和 V,但不保存 Q。

后面会具体解释原因。

The Shape of the Cache

KV Cache 的形状

对于:

  • 单个 Transformer Layer

  • 单个 Attention Head

有:

纯文本
K_cache shape: [current_sequence_length, head_dim]
V_cache shape: [current_sequence_length, head_dim]

例如:

纯文本
sequence_length = 1000
head_dim = 128

那么:

纯文本
K_cache = [1000, 128]
V_cache = [1000, 128]

这里每一行对应一个 token 的 K 或 V。

对于完整模型,可以抽象成:

纯文本
Full KV cache shape:

[num_layers, 2, current_sequence_length, num_heads, head_dim]

 ↑          ↑            ↑                 ↑          ↑
 │          │            │                 │          └─ 每个 head 的维度
 │          │            │                 └─ Attention heads
 │          │            └─ 随生成长度不断增加
 │          └─ K 和 V
 └─ 每个 Transformer Layer 都有自己的 Cache

也就是:

KVCache∈RL×2×N×H×dheadKVCache \in \mathbb{R}^{L\times2\times N\times H\times d_{head}}

其中:

  • LL:Transformer Layer 数

  • NN:当前 Sequence Length

  • HH:Attention Head 数量

  • dheadd_{head}:每个 Head 的维度

Concrete Example: LLaMA-7B

一个具体例子:LLaMA-7B

博客以 LLaMA-7B 为例:

纯文本
Layers       = 32
Heads        = 32
head_dim     = 128
dtype        = float16
每个数值      = 2 bytes

对于长度为 seq_len 的序列:

CacheSize=num_layers×2×seq_len×num_heads×head_dim×bytesCacheSize = num\_layers \times 2 \times seq\_len \times num\_heads \times head\_dim \times bytes

代入:

32×2×seq_len×32×128×232\times2\times seq\_len\times32\times128\times2

得到:

524,288×seq_len bytes524,288\times seq\_len\ bytes

也就是大约:

0.5MB×seq_len0.5MB\times seq\_len

因此,对于一个:

纯文本
2048 tokens

的 Sequence:

0.5MB×2048≈1GB0.5MB\times2048 \approx 1GB

也就是说:

单个请求,仅 KV Cache 就可能占用约 1GB。

这也是为什么长上下文推理中,KV Cache 会成为非常重要的显存消耗来源。

How the Forward Pass Changes

KV Cache 如何改变 Forward Pass?

现在分别看:

  1. 不使用 KV Cache;

  2. 使用 KV Cache。

区别会非常明显。

Without KV Cache

不使用 KV Cache

假设当前已经有:

纯文本
[t_0, t_1, ..., t_{n-1}]

总共 nn 个 token。

最朴素的实现类似:

纯文本
def forward_naive(all_tokens):
    hidden_states = embed(all_tokens)
    # Shape: [n, hidden_dim]

    for layer in transformer_layers:

        # 为所有 token 重新计算 Q、K、V
        Q = hidden_states @ W_Q
        # Shape: [n, head_dim]

        K = hidden_states @ W_K
        # Shape: [n, head_dim]

        V = hidden_states @ W_V
        # Shape: [n, head_dim]

        # 完整 Attention
        attention_scores = Q @ K.T / sqrt(d)
        # Shape: [n, n]

        attention_scores = apply_causal_mask(
            attention_scores
        )

        attention_weights = softmax(
            attention_scores
        )

        attention_output = attention_weights @ V
        # Shape: [n, head_dim]

        hidden_states = layer.ffn(
            attention_output
        )

    # 生成下一个 token 实际只需要最后一个位置
    return hidden_states[-1] @ W_output

问题在于:

假设上一轮已经计算过:

纯文本
K₀ K₁ K₂ ... Kₙ₋₂
V₀ V₁ V₂ ... Vₙ₋₂

这一轮生成新 token 后,又重新计算:

纯文本
K₀ K₁ K₂ ... Kₙ₋₂ Kₙ₋₁
V₀ V₁ V₂ ... Vₙ₋₂ Vₙ₋₁

但是:

纯文本
K₀ ... Kₙ₋₂
V₀ ... Vₙ₋₂

和上一轮完全一样。

这些计算全部是重复计算。

With KV Cache

使用 KV Cache

使用 Cache 后,Decode 阶段每次只传入一个新 token:

纯文本
def forward_with_cache(new_token, kv_cache):
    """
    new_token:
        当前新生成的一个 token

    kv_cache:
        保存每一层历史 K/V
    """

    hidden_states = embed(new_token)
    # Shape: [1, hidden_dim]

    for layer_idx, layer in enumerate(
        transformer_layers
    ):

        # 只计算当前 token 的 Q/K/V
        Q_new = hidden_states @ W_Q
        # Shape: [1, head_dim]

        K_new = hidden_states @ W_K
        # Shape: [1, head_dim]

        V_new = hidden_states @ W_V
        # Shape: [1, head_dim]

        # 读取这一层历史 K/V
        K_cached = kv_cache[layer_idx]['K']
        # Shape: [n-1, head_dim]

        V_cached = kv_cache[layer_idx]['V']
        # Shape: [n-1, head_dim]

        # 历史 Cache + 当前 token
        K_full = concat([
            K_cached,
            K_new
        ])
        # Shape: [n, head_dim]

        V_full = concat([
            V_cached,
            V_new
        ])
        # Shape: [n, head_dim]

        # 更新 Cache
        kv_cache[layer_idx]['K'] = K_full
        kv_cache[layer_idx]['V'] = V_full

        # 当前 token 的 Q 查询所有 K
        attention_scores = (
            Q_new @ K_full.T / sqrt(d)
        )
        # Shape: [1, n]

        attention_weights = softmax(
            attention_scores
        )
        # Shape: [1, n]

        attention_output = (
            attention_weights @ V_full
        )
        # Shape: [1, head_dim]

        hidden_states = layer.ffn(
            attention_output
        )

    return hidden_states @ W_output

核心变化就是:

纯文本
没有 KV Cache:

输入 n 个 token
↓
重新计算 n 个 Q
重新计算 n 个 K
重新计算 n 个 V
↓
计算 [n × n] Attention

变成:

纯文本
使用 KV Cache:

输入 1 个新 token
↓
计算 1 个 Q_new
计算 1 个 K_new
计算 1 个 V_new
↓
读取历史 K/V
↓
计算 [1 × n] Attention

The Key Differences

两种方式的核心区别

项目Without KV CacheWith KV Cache每次处理 token 数nn1新计算的 Knn 个1 个新计算的 Vnn 个1 个新计算的 Qnn 个1 个从显存读取 K/V无n−1n-1 个历史 K/VAttention Matrix[n,n][n,n][1,n][1,n]

所以 KV Cache 本质上做了一件事:

用 Memory Storage + Memory Read,换取避免重复计算。

也就是:

Compute⟶MemoryCompute \longrightarrow Memory

这就是 KV Cache 的基本 trade-off。

Visualizing Cache Growth

KV Cache 是如何增长的?

假设 Prompt:

纯文本
["The", "cat", "sat"]

STEP 0:Prefill

输入:

纯文本
["The", "cat", "sat"]

三个 token 并行处理。

计算:

纯文本
K₀ K₁ K₂

V₀ V₁ V₂

KV Cache:

纯文本
┌──────────────────────────┐
│ K_cache: [K₀, K₁, K₂]   │
│ V_cache: [V₀, V₁, V₂]   │
└──────────────────────────┘

然后模型输出:

纯文本
"on"

STEP 1:Decode "on"

输入只有:

纯文本
["on"]

当前 token 位置:

纯文本
position = 3

新计算:

纯文本
Q₃
K₃
V₃

从 Cache 读取:

纯文本
K₀ K₁ K₂

V₀ V₁ V₂

Attention:

纯文本
Q₃
 ↓
[K₀ K₁ K₂ K₃]

即:

Q3[K0,K1,K2,K3]TQ_3 [K_0,K_1,K_2,K_3]^T

计算结束后更新 Cache:

纯文本
┌──────────────────────────────┐
│ K_cache: [K₀,K₁,K₂,K₃]      │
│ V_cache: [V₀,V₁,V₂,V₃]      │
└──────────────────────────────┘

模型生成:

纯文本
"the"

STEP 2:Decode "the"

输入:

纯文本
["the"]

只计算:

纯文本
Q₄
K₄
V₄

读取:

纯文本
K₀ K₁ K₂ K₃

V₀ V₁ V₂ V₃

然后:

纯文本
Q₄
 ↓
[K₀ K₁ K₂ K₃ K₄]

即:

Q4[K0,K1,K2,K3,K4]TQ_4 [K_0,K_1,K_2,K_3,K_4]^T

新的 Cache:

纯文本
┌─────────────────────────────────┐
│ K_cache: [K₀,K₁,K₂,K₃,K₄]      │
│ V_cache: [V₀,V₁,V₂,V₃,V₄]      │
└─────────────────────────────────┘

然后输出:

纯文本
"mat"

所以整个 Decode 的模式始终是:

纯文本
计算新 token 的 K/V
        ↓
读取全部历史 K/V
        ↓
当前 Q 与全部 K 做 Attention
        ↓
利用 Attention Weight 聚合全部 V
        ↓
把新 K/V 添加到 Cache
        ↓
生成下一个 token

每生成一个 token:

纯文本
KV Cache += 一个 token 的 K/V

因此 Cache 大小随 Sequence Length 线性增长。

Why Don't We Cache Q?

为什么不缓存 Q?

一个自然的问题是:

既然 K 和 V 都缓存了,为什么不把 Q 也缓存?

原因就在于自回归生成时 Attention 的工作方式。

Decode 阶段,我们只计算当前新 token 的 Attention。

例如当前生成位置 nn:

QnQ_n

需要和:

K0,K1,…,KnK_0,K_1,\ldots,K_n

进行计算。

但是历史 token:

纯文本
token 0
token 1
token 2
...
token n-1

不需要

— 感谢阅读 —