3、KV Cache复用原理:Transformer推理中的KV Cache是什么?为什么它是显存大户?

好,咱们今天聊个硬核话题——KV Cache。

说实话,我刚开始接触Transformer推理优化那会儿,也被这个KV Cache搞得有点懵。明明就是个缓存机制,怎么就成了显存里的“吞金兽”了?后来踩了不少坑,才真正搞明白它的来龙去脉。

3.1 什么是KV Cache?

先说说它的本质。

Transformer做自回归生成时,是一个词一个词往外蹦的。比如你输入“今天天气”,模型先预测“真”,再基于“今天天气真”预测“好”,以此类推。

这里有个关键问题:每次预测新词时,注意力机制都要计算当前词跟前面所有词的关系。如果不做缓存,那每次都得把前面所有词的Key和Value重新算一遍。

你想想看,这得多浪费?

KV Cache就是干这个的——把之前算好的Key和Value存起来,下次直接用。

核心定义:KV Cache是Transformer解码过程中,为每个已生成token缓存的Key矩阵和Value矩阵。它避免了重复计算,是推理加速的关键手段。

我习惯用一个比喻来理解:

  • 没有KV Cache:每次写新句子,都要把前面写过的字重新读一遍
  • 有KV Cache:写新字时,只查一下之前记在纸上的笔记就行

说白了,就是用空间换时间。

3.2 KV Cache是怎么工作的?

咱们用代码说话。假设一个简化版的注意力计算:

# 伪代码:带KV Cache的解码过程
def decode_with_kv_cache(input_ids, past_kv=None):
    if past_kv is None:
        # 首次解码,计算所有token的K和V
        k, v = compute_kv(input_ids)
        past_kv = (k, v)
        output = attention(q, k, v)
    else:
        # 后续解码,只计算新token的K和V
        new_k, new_v = compute_kv(input_ids[:, -1:])
        # 拼接到历史缓存上
        k = torch.cat([past_kv[0], new_k], dim=2)
        v = torch.cat([past_kv[1], new_v], dim=2)
        past_kv = (k, v)
        output = attention(q, k, v)
    return output, past_kv

看到没?关键就在那个 torch.cat 上。每次生成一个新词,KV Cache就长大一点。

我在项目中遇到过一个问题:有些同学以为KV Cache只在层与层之间共享,其实不是。每一层Transformer都有自己的KV Cache,互不干扰。

小提示:KV Cache是按层存储的。一个12层的模型,就有12组独立的K和V缓存。千万别搞混了。

3.3 为什么KV Cache是显存大户?

好,重点来了。

咱们算笔账。假设:

  • 模型隐藏层维度 d = 4096
  • 层数 L = 32
  • 精度为 float16(2字节)
  • 生成长度 N = 2048
  • 批量大小 B = 1

单个序列的KV Cache大小:

KV Cache大小 = 2(K和V) × L × B × N × d × 2字节
             = 2 × 32 × 1 × 2048 × 4096 × 2
             = 1,073,741,824 字节
             ≈ 1 GB

一个序列就要1GB!

如果批量大小是32呢?那就是32GB。你看,显存就这么被吃掉了。

生成长度 单序列KV Cache 批量32时
512 256 MB 8 GB
1024 512 MB 16 GB
2048 1 GB 32 GB
4096 2 GB 64 GB

我曾经在一个长文本生成任务里,序列长度到了8192。你猜怎么着?单序列KV Cache就占了4GB,批量一上去,直接OOM。那次教训挺深刻的。

注意:KV Cache的显存占用与序列长度呈线性增长。长序列推理时,KV Cache往往比模型权重本身还占显存。这是推理优化的核心瓶颈之一。

3.4 KV Cache的复用策略

既然KV Cache这么占显存,那怎么优化?

我总结了几种常见的复用策略:

  1. 连续批处理(Continuous Batching):不同请求共享同一个KV Cache池,动态分配
  2. Prefix Caching:如果多个请求有相同的前缀(比如系统提示词),只缓存一份
  3. KV Cache量化:把float16降到int8甚至int4,显存直接减半或减四分之三
  4. 内存复用:用预分配的内存池,避免频繁申请释放

我个人最常用的是Prefix Caching。举个例子,如果你的应用里所有请求都带一段固定的系统提示,那这段提示的KV Cache只需要算一次。后面所有请求都能复用。

效果有多明显?我做过测试:

  • 没有Prefix Caching:首token延迟 200ms
  • 有Prefix Caching:首token延迟降到 50ms
  • 显存占用减少约 30%

嗯,这个优化性价比很高。

3.5 一张图看懂KV Cache

说了这么多,咱们用一张SVG图来总结一下KV Cache的核心逻辑:

KV Cache 核心逻辑 无KV Cache:重复计算 第1步:计算 token1 的 K,V 第2步:计算 token1,2 的 K,V(重复计算token1) 第3步:计算 token1,2,3 的 K,V(重复计算token1,2) 有KV Cache:增量计算 第1步:计算 token1 的 K,V → 缓存 第2步:只计算 token2 的 K,V → 拼接到缓存 第3步:只计算 token3 的 K,V → 拼接到缓存 KV Cache 缓存池 [K1,V1] | [K2,V2] | [K3,V3] | ... | [Kn,Vn] 显存占用对比 无缓存:O(N²) 计算量 | 有缓存:O(N) 计算量 显存占用:O(N) → 长序列时成为瓶颈

这张图很直观地展示了:没有KV Cache时,每次都要从头算起,计算量是O(N²)。有了KV Cache,每次只算增量,计算量降到O(N)。但代价就是显存占用也是O(N)。

3.6 避坑指南

最后分享几个我踩过的坑:

  • 坑1:我曾经以为KV Cache可以跨batch共享。不行!不同序列的KV Cache是独立的,除非它们有相同的前缀。
  • 坑2:显存碎片化。频繁的torch.cat操作会导致显存碎片。我建议用预分配的内存池来管理。
  • 坑3:精度问题。KV Cache量化到int8时,有些模型会出现质量下降。需要做校准,不能无脑量化。

嗯,KV Cache这块内容不少,但核心就两点:它是什么,为什么吃显存。理解了这两点,后面的优化策略就好办了。

想要更多资料,联系可可