KV cache 那笔账:为什么 Qwen2.5-7B 在 2048 长度下大约是 117MB

从人话直觉、小数字张量形状和 Qwen2.5-7B 的真实参数出发,讲清 KV cache 公式、GQA 为什么省 7 倍,以及 100 并发为什么会变成 11.7GB。

KV cache 那笔账:为什么 Qwen2.5-7B 在 2048 长度下大约是 117MB

今天只解决一个问题:

KV cache = layers × 2(K+V) × batch × KV_heads × seq × head_dim × bytes

这行公式到底是什么意思?为什么会算成:

28 × 2 × 1 × 4 × 2048 × 128 × 2
= 117,440,512 bytes
≈ 117.4 MB

如果能把这笔账讲清楚,后面看 vLLM PagedAttention、continuous batching、长上下文显存瓶颈,都会顺很多。因为 KV cache 不是一个抽象概念,它就是推理服务里会真实膨胀的一块内存。

1. 人话直觉:KV cache 是模型给历史 token 做的索引卡片

在 attention 里,Q/K/V 可以先这样理解:

Q: 当前 token 想找什么
K: 历史 token 能被什么问题匹配
V: 历史 token 被选中后贡献什么内容

模型生成文本时分两段:

prefill: 一次性读完整个 prompt
decode: 之后每次只生成 1 个新 token

假设前面已经有 2048 个 token 的历史。继续生成时,当前 token 的 Query 要去匹配这些历史位置的 Key,然后用注意力权重去加权这些历史位置的 Value。

如果没有 KV cache,每生成一个新 token,都要把前面所有历史 token 重新跑过模型前缀,重新得到每一层的历史 K/V。这里先只看 K/V 这笔账:

第 2049 步:重算 2048 个历史 token 的 K/V
第 2050 步:重算 2049 个历史 token 的 K/V
第 2051 步:重算 2050 个历史 token 的 K/V
...

这会把 decode 阶段拖死。

KV cache 的做法很直接:

历史 token 的 K/V 算过一次,就存起来。
下一步 decode 只给新 token 算新的 K/V,再追加到 cache 末尾。

所以 KV cache 可以理解成:每一层 attention 都给历史 token 存了一套已经算好的 Key 和 Value。当前 token 来的时候,直接拿它们用,不重算历史。

2. 最小数学地基:一个张量形状就是一组格子

先不急着看 28 层、2048 长度。用小数字。

假设一个玩具模型:

layers = 2
batch = 1
KV_heads = 2
seq = 3
head_dim = 4
每个数用 fp16 = 2 bytes

只看 K cache,它的逻辑形状可以写成:

[layers, batch, KV_heads, seq, head_dim]
= [2, 1, 2, 3, 4]

这不是公式玄学,而是在数格子:

2 层
每层 1 个请求
每个请求 2 个 KV head
每个 head 存 3 个历史位置
每个位置是 4 个数

所以 K cache 里有:

2 × 1 × 2 × 3 × 4 = 48 个数

每个数是 fp16,占 2 bytes:

K cache = 48 × 2 = 96 bytes

但 attention 不只需要 K,还需要 V。K 和 V 形状一样,所以总共乘 2:

KV cache = 2(K+V) × 96 = 192 bytes

这就是大公式的核心。所有复杂版本,本质都只是在数这块多维表里有多少个数。

3. 逐项拆公式:每一项都在数什么

完整公式:

KV cache bytes
= layers × 2 × batch × KV_heads × seq × head_dim × bytes_per_value

逐项解释:

layers

每一层 Transformer 都有自己的 K/V。第 0 层的 hidden state、K/V 投影权重和第 1 层不同,所以不能共用。Qwen2.5-7B 有 28 层,因此这里是 28。

2

这个 2 不是 batch,也不是 fp16。它表示 K 和 V 两份缓存。K 一份,V 一份。

batch

一次同时处理多少条独立序列。单请求就是 1。并发 100 个请求时,粗略上就是乘 100,因为每个请求的历史上下文不同,KV cache 不能互相复用。

KV_heads

存的是 Key/Value 的 head 数,不是 Query 的 head 数。Qwen2.5-7B 用 GQA,Query 有 28 个头,但 K/V 只有 4 个头,所以这里填 4,不填 28。

seq

缓存里已经存了多少个位置。上下文越长,KV cache 线性增长。2048 token 就是 2048 个历史位置。

head_dim

每个 head 的向量维度。Qwen2.5-7B 的 hidden size 是 3584,Query heads 是 28:

3584 / 28 = 128

所以每个 head 是 128 维。

bytes_per_value

每个缓存数值占多少字节。fp16/bf16 都是 2 bytes,fp32 是 4 bytes,int8 是 1 byte。这里按 llama-bench 里的 type_k=f16type_v=f16 算,所以是 2。

4. 回到截图:Qwen2.5-7B 的 117MB 怎么来

Qwen2.5-7B 这组数字是:

layers = 28
batch = 1
KV_heads = 4
seq = 2048
head_dim = 128
type_k/type_v = fp16 = 2 bytes

代入公式:

KV cache
= layers × 2(K+V) × batch × KV_heads × seq × head_dim × 2 bytes
= 28 × 2 × 1 × 4 × 2048 × 128 × 2
= 117,440,512 bytes

如果按十进制 MB:

117,440,512 / 1,000,000
= 117.4 MB

如果按二进制 MiB:

117,440,512 / 1024 / 1024
= 112.0 MiB

所以这里的 117.4 MB 没错,只是它用的是十进制 MB。很多系统工具显示的是 MiB/GiB,数字会略小一点。

5. 为什么公式里没有 Q

这是最容易问错的一点:

既然 attention 叫 Q/K/V,为什么 cache 只存 K/V,不存 Q?

原因是 decode 时只需要当前 token 的 Query。

生成下一个 token 时,当前 token 会拿自己的 Q 去查所有历史 K:

当前 Q × 历史 K -> attention score
attention score -> softmax -> weight
weight × 历史 V -> 当前 token 的 attention 输出

过去 token 的 Q 已经完成了它们自己的那一步输出。后面生成新 token 时,不需要再拿过去 token 的 Q 去查别人。真正会被未来反复访问的是过去 token 的 K 和 V。

所以每一步 decode 做的是:

1. 给当前 token 算 Q/K/V
2. 把当前 token 的 K/V 追加进 KV cache
3. 用当前 Q 读取所有历史 K/V
4. 产出当前 token 的 attention 结果

Q 是临时工作区,K/V 才是要长期保存的历史资料。

6. GQA 为什么能把 KV cache 从 820MB 降到 117MB

如果没有 GQA,K/V head 数通常会和 Query head 数一样。对 Qwen2.5-7B 来说,就是把 KV_heads=4 换成 KV_heads=28

重新算:

28 × 2 × 1 × 28 × 2048 × 128 × 2
= 822,083,584 bytes
≈ 822.1 MB

而使用 GQA 时:

28 × 2 × 1 × 4 × 2048 × 128 × 2
= 117,440,512 bytes
≈ 117.4 MB

两者比例正好是:

28 / 4 = 7

也就是说,GQA 在这笔 KV cache 账上省了 7 倍。

直觉上可以这样理解:

28 个 Query head 仍然保留,负责从不同角度提问;
但 Key/Value 只准备 4 组,被多组 Query head 共享。

这就是为什么前面 QKV 投影里会看到:

Q: [S, 28, 128]
K: [S,  4, 128]
V: [S,  4, 128]

Q 头多,表达能力还在;K/V 头少,cache 和带宽压力降下来。

7. 100 并发为什么会变成 11.7GB

单请求、2048 长度、fp16 KV cache 是:

117.4 MB

如果同时有 100 个请求,每个请求都有自己的上下文:

117.4 MB × 100
= 11,740 MB
≈ 11.7 GB

这还只是 2048 长度。如果上下文变成 8192,seq 直接乘 4:

117.4 MB × 4
≈ 469.8 MB / request

100 并发就是:

469.8 MB × 100
≈ 47.0 GB

注意,模型权重大小基本是固定的。比如 Q4_K_M GGUF 大约 4.7GB,加载后权重不会因为上下文变长而线性增长。但 KV cache 会随着:

上下文长度 seq
并发 batch/request 数
层数 layers
KV head 数
KV 精度

一起增长。

这就是长上下文服务里 KV cache 会变成一等公民的原因。

8. 这笔账和 PagedAttention 有什么关系

上面公式算的是理想 payload:真正有用的 K/V 数值占多少内存。实际推理框架还会遇到几个工程问题:

不同请求长度不同,cache 容易碎片化
请求会来来走走,连续大块内存不好复用
有些请求提前结束,空出来的 cache 要回收
并发调度时,新 token 要不断追加 cache

如果每个请求都按最大上下文长度预留连续 KV cache,浪费会很大。vLLM 的 PagedAttention 就是为了解这个问题:把 KV cache 切成块,像操作系统管理内存页一样管理它。

所以这篇的 117MB 不是终点,而是下一步的起点。

你先知道单条请求的 KV cache 为什么是:

layers × 2 × batch × KV_heads × seq × head_dim × bytes

才能继续追问:

当请求很多、长度不同、不断进出时,这些 cache 应该怎么分配、复用和回收?

这就是 PagedAttention 要回答的问题。

9. 最后一遍心算模板

以后看到任何模型,都按这几个问题填空:

1. 多少层?layers
2. K/V 几个头?KV_heads
3. 每个 head 多宽?head_dim
4. 当前上下文多长?seq
5. 几个请求一起算?batch 或并发数
6. KV cache 用什么精度?bytes_per_value

然后套:

KV cache bytes
= layers × 2 × batch × KV_heads × seq × head_dim × bytes_per_value

Qwen2.5-7B @ seq=2048:

28 × 2 × 1 × 4 × 2048 × 128 × 2
= 117,440,512 bytes
≈ 117.4 MB

没有 GQA:

28 × 2 × 1 × 28 × 2048 × 128 × 2
≈ 822.1 MB

100 并发:

117.4 MB × 100
≈ 11.7 GB

这三个数字就是 W1-Q5 的核心答案。

自测

不看上文,试着回答三件事:

1. KV cache 为什么乘 2?
2. Qwen2.5-7B 为什么用 KV_heads=4,而不是 heads=28?
3. 为什么 Q 不进 cache?

如果这三问能答顺,KV cache 这笔账就不是背公式了。