跳到主要内容

第19章:KV Cache

大语言模型在生成文本时,每产生一个新 token,都需要"回顾"之前所有的 token。如果每一步都从头重新计算,代价会随序列长度平方增长。KV Cache 是解决这个问题最核心的工程优化——理解它,才能真正理解推理系统的设计权衡。


19.1 原理:为什么可以缓存

朴素实现的浪费

回顾 Transformer 的自注意力(Self-Attention)机制。对于第 tt 步生成,模型需要计算:

Attention(Qt,K1:t,V1:t)=softmax(QtK1:tTdk)V1:t\text{Attention}(Q_t, K_{1:t}, V_{1:t}) = \text{softmax}\left(\frac{Q_t K_{1:t}^T}{\sqrt{d_k}}\right) V_{1:t}

其中 QtQ_t 是当前新 token 产生的 Query,K1:tK_{1:t}V1:tV_{1:t} 是所有历史 token(包含当前)产生的 Key 和 Value。

朴素实现的做法是:每生成一个新 token,就把所有历史 token 重新输入模型,重新计算整个序列的 K、V 矩阵

这非常低效。生成第 tt 个 token 时,需要计算 tt 个位置的 K 和 V;生成第 t+1t+1 个 token 时,又要重新计算前 t+1t+1 个位置的 K 和 V——其中前 tt 个位置的计算完全是重复劳动。

生成 NN 个 token 的总计算量:t=1NO(t)=O(N2)\sum_{t=1}^{N} O(t) = O(N^2)

关键洞察:K、V 与新 token 无关

Transformer 的因果掩码(Causal Mask)保证:位置 ii 的 K 和 V,只依赖位置 ii 本身及其之前的输入,与后续生成的任何新 token 完全无关。

更具体地说,对于解码器,第 ii 个位置的 Key 和 Value 是:

Ki=WKxi,Vi=WVxiK_i = W_K \cdot x_i, \quad V_i = W_V \cdot x_i

其中 xix_i 只取决于输入序列的第 ii 个 token(及其位置编码)。一旦 xix_i 确定,KiK_iViV_i 就永远不会变化。

KV Cache 的做法

既然历史位置的 K、V 不会变,那就把每一层、每个位置的 K 和 V 存进显存,下次直接读取,不再重算。

生成第 t+1t+1 个 token 时:

  1. 只计算新 token 的 Qt+1Q_{t+1}Kt+1K_{t+1}Vt+1V_{t+1}
  2. Kt+1K_{t+1}Vt+1V_{t+1} 追加到 KV Cache;
  3. Qt+1Q_{t+1} 与 Cache 中所有 K1:t+1K_{1:t+1} 做 Attention。

每一步的计算量变为 O(t)O(t)(Attention 的 QK 乘积),但总计算量不再是 O(N2)O(N^2) 的求和,而是每步的增量计算。

:::info 加速效果 对比:

实现方式生成第 tt 步的计算生成 NN 步总计算
朴素重算O(t2)O(t^2)(全序列 Attention)O(N3)O(N^3)
KV CacheO(t)O(t)(仅新 token 的 Attention)O(N2)O(N^2)

KV Cache 把每步的瓶颈从"全序列计算"变成了"单步 Attention + 内存读取",实际推理速度可提升 数十倍。 :::


19.2 显存占用计算

KV Cache 是免费的吗?当然不是——它用显存换计算。

单 token 的 KV Cache 大小

对于一个 Transformer 模型,KV Cache 的大小由以下参数决定:

Sizeper token=2×L×H×dh×B\text{Size}_{\text{per token}} = 2 \times L \times H \times d_h \times B

其中:

  • 22:K 和 V 各一份;
  • LL:Transformer 的层数(num_layers);
  • HH:KV 头数(num_kv_heads,GQA 下小于 Q 的头数);
  • dhd_h:每个头的维度(head_dim);
  • BB:每个元素的字节数(BF16 为 2 字节,INT8 为 1 字节)。

LLaMA-3 70B 实例计算

LLaMA-3 70B 的关键参数:

  • L=80L = 80 层;
  • GQA(Grouped Query Attention)下 H=8H = 8 个 KV 头;
  • dh=128d_h = 128
  • BF16 精度,B=2B = 2 字节。

代入公式:

Sizeper token=2×80×8×128×2=327,680 字节320 KB\text{Size}_{\text{per token}} = 2 \times 80 \times 8 \times 128 \times 2 = 327{,}680 \text{ 字节} \approx 320 \text{ KB}

规模化的显存压力

现在把这个数字放到实际场景中:

场景计算显存占用
单个 4096 token 对话320KB×4096320\text{KB} \times 40961.3 GB\approx 1.3\text{ GB}
100 个并发请求1.3GB×1001.3\text{GB} \times 100130 GB\approx 130\text{ GB}
LLaMA-3 70B 权重本身(BF16)140 GB\approx 140\text{ GB}

:::warning 关键结论 100 个并发请求的 KV Cache(130 GB)已经接近模型权重本身(140 GB)的大小。这意味着:KV Cache 的显存管理,往往比模型本身更难。这也是为什么推理系统(如 vLLM)需要专门设计显存分配器。 :::


19.3 多轮对话中的 Cache 管理

多轮对话的天然复用

在多轮对话中,第一轮的历史文本在第二轮、第三轮都会出现在 context 的开头。如果把前几轮的 KV Cache 保留下来,后续轮次只需要计算新增内容的 K、V,大量计算可以省去。

第一轮:[系统提示词][用户消息1][助手回复1]
第二轮:[系统提示词][用户消息1][助手回复1][用户消息2][助手回复2]
^^^^^^^^^^^^^^^^^^^^^^^^
这部分的 KV Cache 可以直接复用

Prefix Caching(前缀缓存)

更进一步:许多请求共享相同的系统提示词(System Prompt)。例如,一个部署在生产环境的 AI 助手,每个用户请求都以同一段几千字的系统提示词开头。

Prefix Caching 的思路是:把相同前缀的 KV Cache 在多个请求之间共享

请求 A:[系统提示词(2000 tokens)][用户 A 的消息]
请求 B:[系统提示词(2000 tokens)][用户 B 的消息]
^^^^^^^^^^^^^^^^^^^^^^^^
这 2000 个 token 的 KV Cache 只需计算一次

实现上,通常用前缀的哈希值作为 Cache key,命中时直接加载对应的 K、V 张量。vLLM、SGLang 等框架都支持这一特性,对于系统提示词较长的场景,首次请求之后的延迟可以大幅降低。

:::tip 实际效果 若系统提示词占 context 的 50%,Prefix Caching 理论上可将 TTFT(Time To First Token,首 token 延迟)减少约 50%。 :::

缓存淘汰策略

显存有限,KV Cache 不可能无限保留。常见的淘汰策略:

LRU(Least Recently Used,最近最少使用):淘汰最久没被访问的 Cache 块。适合请求分布较均匀的场景。

滑动窗口(Sliding Window):只保留最近 WW 个 token 的 KV Cache,超出窗口的直接丢弃。这是 Mistral 等模型采用的策略,在牺牲超长上下文能力的前提下,把显存占用控制在固定上限。

基于 Radix Tree 的前缀共享(SGLang 的 RadixAttention):将所有历史请求的 KV Cache 组织成一棵前缀树,自动发现和共享公共前缀,同时按 LRU 淘汰叶节点。


19.4 KV Cache 量化

为什么 KV Cache 需要单独量化

模型权重量化(如 GPTQ、AWQ)是在模型加载时对静态参数做压缩,误差是固定的。而 KV Cache 是推理过程中动态生成的,每个新 token 都会往 Cache 里追加新的 K、V 值。

这个差异带来两个问题:

  1. 误差累积:量化 KV Cache 引入的误差,会在 Attention 计算中逐步传播,越长的序列误差越大;
  2. 动态范围不稳定:K、V 的数值分布因输入内容而异,离线校准统计出的量化参数不一定适合每一步的 Cache。

FP16 → INT8

将 KV Cache 从 BF16/FP16(2 字节)降为 INT8(1 字节),显存直接减半。

常见做法是逐 token 或逐 head 的动态量化

xint8=round(xmax(x)×127)x_{\text{int8}} = \text{round}\left(\frac{x}{\max(|x|)} \times 127\right)

反量化时乘回 scale 因子。这种方式对大多数任务的精度损失在可接受范围内(困惑度上升 < 1%),是生产环境中最常用的 KV Cache 压缩方案。

INT4 量化

INT4 将显存压缩至原来的 1/4(相对 FP16),但精度风险显著上升,需要额外处理:

  • 分组量化(Group Quantization):每 gg(如 64)个元素共享一个 scale,减少 outlier 的影响;
  • Key 和 Value 分开量化:实验发现 K 和 V 的数值分布差异较大,统一量化效果差;
  • 保留注意力 sink(Attention Sink):序列开头几个 token 的 KV Cache 对精度影响不成比例地大,可以保留 FP16 精度。

量化效果对比

精度显存(相对 FP16)典型精度损失适用场景
BF16 / FP16基准高精度要求
INT80.5×极小(<1% PPL 上升)生产环境首选
INT40.25×中等,需仔细调优超长上下文、极端显存受限

:::warning KV 量化与权重量化的根本区别 权重量化:一次性压缩,推理时反量化为全精度再计算,误差不累积。

KV Cache 量化:量化后的 K、V 直接参与 Attention 计算,误差随序列长度累积。这意味着 KV 量化对长文本(> 8K tokens)的影响远大于短文本。 :::


本章小结

概念核心要点
KV Cache 原理K、V 只依赖历史 token,一次计算永久复用
计算复杂度生成总计算量从 O(N3)O(N^3) 降为 O(N2)O(N^2)
显存公式2×L×H×dh×B2 \times L \times H \times d_h \times B 字节/token
LLaMA-3 70B约 320 KB/token,100 并发 4K 上下文需 130 GB
Prefix Caching共享相同前缀的 Cache,大幅降低首 token 延迟
淘汰策略LRU、滑动窗口、Radix Tree 前缀共享
KV 量化INT8 是生产首选;INT4 需防范长序列误差累积

KV Cache 解决了"逐步生成时的重复计算"问题,但随之而来的显存压力催生了一个新挑战:当显存不足以容纳所有请求的 KV Cache 时,如何高效地分配和调度?下一章将介绍 vLLM 提出的 PagedAttention,它借鉴操作系统的虚拟内存思想,把 KV Cache 的碎片化管理问题彻底解决。