ELL3N HSU

Available in Chinese only.

← 文章

KV Cache 为什么会吃显存?

梳理 KV Cache 的数据结构、显存估算方式,以及长上下文为什么会放大问题。

模型权重能装进显卡,为什么请求一长、并发一多,服务仍然会显存不足?从后端容量规划的角度看,权重只是一笔固定支出,每个活跃请求还在不断积累自己的 KV Cache。

可以从一个 token 算起:它在每一层都要留下 Key 和 Value,后续生成还会读取这些数据。算出这一份有多大,再乘上缓存长度和请求数,就能估计 KV 需要多少空间。下面以标准全注意力模型为例;滑动窗口、混合架构和跨请求共享会改变计算方式,需要另算。

从每个 token 算起

一个 token 在一层里留下两份张量,每份包含 HkvH_{kv} 个 head,每个 head 有 DhD_h 个元素。再乘上每个元素的字节数、层数和 token 数,就得到容量。假设所有请求长度相同、彼此不共享 KV,一批请求的理论容量为:

KV bytes=2×B×L×S×Hkv×Dh×p\text{KV bytes} = 2 \times B \times L \times S \times H_{kv} \times D_h \times p
符号含义
22Key 与 Value 两份张量
BB并发持有缓存的请求数
LL需要 KV 的注意力层数
SS每个请求当前缓存的 token 数
HkvH_{kv}每层的 KV head 数
DhD_h每个 head 的维度
pp每个缓存元素的字节数,BF16/FP16 为 2

hidden_size 不能直接代替 HkvDhH_{kv}D_h:标准 MHA 中它们通常相等,GQA/MQA 的 KV head 数少于 Query head 数,使用模型隐藏维度会高估这部分容量。各请求长度不同时,应把 BSB S 换成各请求缓存长度之和。

一个容量例子

取 L=24L=24、S=2048S=2048、Hkv=4H_{kv}=4、Dh=128D_h=128,KV 使用 BF16。单请求占用:

2×24×2048×4×128×2=100,663,296 bytes=96 MiB2 \times 24 \times 2048 \times 4 \times 128 \times 2 = 100{,}663{,}296\ \text{bytes} = 96\ \text{MiB}
只改变一项,其余保持不变理论容量
基线:1 个请求96 MiB
请求数从 1 增至 2192 MiB
缓存长度从 2048 增至 4096192 MiB / 请求
KV head 数从 4 增至 16384 MiB / 请求

这些是计算例子,不是运行测量。引擎实际占用还包括块取整、索引元数据、分配器开销等;进程总显存还包含权重和其他计算所需的张量。分页和前缀共享会改变物理分配,不能直接用进程显存读数核验上述单项公式。

容量之外还要看读取成本

到这里算出的是“存下多少数据”。生成下一个 token 时,全注意力 Decode 还要读取历史 KV,所以更长的上下文既占空间,也增加读取量。减少缓存精度或把一部分移到其他存储层可以缓解容量压力,但会带来质量、传输或调度方面的取舍,需要分别测量。

核对容量时,先固定并发、只增加请求长度,再固定长度、只增加并发,观察已使用 KV 块是否按预期增长。若引擎预先分配了缓存池,进程显存可能几乎不变,此时应检查池内已用块,而不能据此否定容量公式。

算出理论容量后,还需要知道请求如何分配和释放这些空间:连续批处理与分页 KV继续讨论这一步。GQA 与张量 shape 的上下文见推理总览。