1.2 推理特有的显存压力:KV Cache


文档摘要

1.2 推理特有的显存压力:KV Cache 读者读完这一节,应该能回答一句:自回归推理为什么要缓存 K、V,而它又是怎么把显存"按最大长度"白白吃掉一大块的。 训练时,一段序列长度是固定的,注意力一次性算完。但自回归推理(你现在用的每个对话模型都在做这件事)是逐个 token 往外蹦的:生成第 t 个 token,要用到前面 1..t-1 所有 token 的 K、V。如果每生成一个新 token 都把前面所有 token 重新算一遍 K、V,成本会随长度线性累加,完全没有性价比。于是工程上引入 KV Cache:把历史 K、V 直接缓存下来,新 token 只算自己的那一份,再"拼"上前人的缓存。 1.2.

1.2 推理特有的显存压力:KV Cache

读者读完这一节,应该能回答一句:自回归推理为什么要缓存 K、V,而它又是怎么把显存"按最大长度"白白吃掉一大块的。

训练时,一段序列长度是固定的,注意力一次性算完。但自回归推理(你现在用的每个对话模型都在做这件事)是逐个 token 往外蹦的:生成第 t 个 token,要用到前面 1..t-1 所有 token 的 K、V。如果每生成一个新 token 都把前面所有 token 重新算一遍 K、V,成本会随长度线性累加,完全没有性价比。于是工程上引入 KV Cache:把历史 K、V 直接缓存下来,新 token 只算自己的那一份,再"拼"上前人的缓存。

1.2.1 KV Cache 是怎么长起来的

自回归推理中 KV Cache 逐 token 累积

流程是这样的:

  1. 第 1 步(prefill):输入整段 prompt,算完所有 token 的 K、V 并缓存,同时产出第一个输出 token;
  2. 第 2 步(decode):用缓存的 K、V + 新 token 的 Q,算出第 2 个输出 token,同时把新 token 的 K、V 追加进缓存;
  3. 第 3 步:重复,缓存越来越长;
  4. 第 t 步:缓存里已经有 K₁..Kₜ、V₁..Vₜ。

注意两个关键点。第一,每生成一个新 token,注意力计算要"读取"前面所有历史 K、V。所以缓存越长,每一步要读的数据越多——这是推理"越往后越慢"的直观来源之一,也是 decode 阶段之所以慢的本质。第二,KV Cache 一旦写入就要长期驻留到该请求结束,它不像 1.1 里的中间矩阵算完可释放,而是整个生成生命周期都占着显存。

这里有一个反直觉的要点值得停下来想:KV Cache 的"好处"和"坏处"来自同一件事——它避免了重复计算。正因为不重复算,所以它必须一直存着;正因为一直存着,所以显存和带宽都长期被它占用。这是推理系统和训练系统最根本的不同:训练可以"算完就扔",推理必须"存着等下一步用"。所有推理显存优化的故事,本质上都是在和"这个必须长期存着的状态"打交道。

1.2.2 推理的两个阶段:prefill 与 decode

理解 KV Cache 的压力,必须分清推理的两个阶段,因为它们的瓶颈完全不同:

  • Prefill(预填充):一次性处理整段 prompt,本质是一次"大矩阵乘",更偏算力受限(吃 FLOPS)。这一步会顺带把整段 prompt 的 K、V 算好写进缓存。
  • Decode(解码):逐个生成 token。每一步只新增 1 个 token 的 Q,却要读取全部历史 K、V 做注意力。这一步每次只产出 1 个 token,计算量极小,但数据搬运量巨大,所以 decode 是典型带宽受限(吃 HBM 带宽)。

这就是为什么"生成阶段"常常比"首字延迟"更影响吞吐:在线服务里大量请求都在 decode,全都在反复读各自的 KV Cache。KV Cache 的内存带宽占用,直接决定了每秒能生成多少 token(tokens/s)。这也解释了为什么很多推理引擎会专门优化 decode(例如 FlashDecoding 的思路:把长序列的 K/V 分块并行处理单个 query)。理解 prefill 和 decode 的差别,是后面理解"为什么有的优化只加速生成、有的只加速首字"的前提。

1.2.3 KV Cache 到底占多少显存

它的显存占用可以拆成一个很直观的公式:

KV Cache 显存占用拆解
KV 显存 ≈ 2 × n × layers × hidden × batch × precision

逐项解释:

  • 2:K 和 V 各一套,所以是 2 倍;
  • n:序列长度,越长占得越多;
  • layers:Transformer 层数(7B 约 32 层,13B 约 40 层,70B 约 80 层);
  • hidden:隐藏维度(7B 约 4096,13B 约 5120,70B 约 8192);
  • batch:并发请求数,这是最容易被低估的一项——并发一上去,KV Cache 线性翻倍
  • precision:每元素字节数,fp16 是 2,fp8 是 1,int4 量化后更小。

代入几个真实配置感受量级(fp16,数值为量级估算,请以具体模型为准):

7B (32层, 4096): 每 token KV ≈ 2×32×4096×2 ≈ 0.5 MB → n=4096 单请求 ≈ 2 GB,batch=32 ≈ 64 GB 13B (40层, 5120): 每 token KV ≈ 2×40×5120×2 ≈ 0.78 MB → n=4096 单请求 ≈ 3.2 GB 70B (80层, 8192): 每 token KV ≈ 2×80×8192×2 ≈ 2.6 MB → n=4096 单请求 ≈ 10.5 GB

注意:在大 batch 推理场景下,KV Cache 往往比模型权重本身还占显存(7B 权重 fp16 才约 14 GB,而 batch=32 时 KV 就到 64 GB)。这是很多人没意识到的——你以为瓶颈是"模型太大",其实卡你的是"KV 太多"。

我再强调一次这个反差:模型权重是一次性加载、所有请求共享的"固定成本";而 KV Cache 是"每个请求、每个 token、按长度线性累积"的"变动成本"。当并发和长度都上来,变动成本会轻松超过固定成本。所以做推理容量规划时,KV Cache 才是那张真正该盯的表。

1.2.4 decode 阶段的带宽预算:一算吓一跳

KV Cache 最致命的不是"占显存",而是"每生成一个 token 都要把全部 KV 读一遍带宽"。我们算一笔账(7B、n=4096、32 层、hidden=4096、fp16):

每生成一个 token,decode 要读的 KV 字节数 = 2(K+V) × n × layers × hidden × 2(字节) = 2 × 4096 × 32 × 4096 × 2 ≈ 2.1 × 10⁹ 字节 ≈ 2 GB

也就是说,每吐出 1 个 token,就要从 HBM 搬约 2 GB 的 KV 数据。按 A100 的 2.0 TB/s 带宽算,光读 KV 就至少要 2GB / 2TB/s = 1 毫秒。这还只是"读",没算计算。换句话:即便计算免费,单卡每秒最多也只能生成约 1000 个 token(单纯被 KV 带宽卡住)。在 batch 更大、n 更长时,这个上限会进一步压低。这就是为什么"优化 KV 的读取与摆放"比"优化一点点矩阵乘"对吞吐影响更大——它直接顶在 Roofline 的带宽天花板上。

作者判断:这条账是推理优化里最该刻进脑子的一行。很多团队花大力气把注意力算子快了 30%,结果整体吞吐只涨了几个点——因为真正卡吞吐的是 decode 阶段读 KV 的带宽,而那部分压根没被算子优化碰到。要动它,得靠 PagedAttention 这类"显存怎么摆、怎么读更顺"的系统层手段。

1.2.5 连续预分配:被浪费的另一半

KV Cache 真正的坑,不止"占得多",更在"占得笨"。传统实现(例如早期 HuggingFace generate 的默认行为)会为每个请求按"最大可能序列长度"预先分配一整块连续的显存槽位。问题有三:

连续预分配:预留槽位被白白占用
  1. 预留浪费:你实际只生成了 200 个 token,但它按 max_length=4096 预留了 4096 个槽位,剩下 95% 的槽位空着也占着显存;
  2. 请求间不共享:每个请求的 KV 槽位各自连续,A 用剩的槽位 B 用不了,显存无法跨请求复用;
  3. 碎片:不同请求生成长度不一,连续分配器里会出现"这里剩 100 槽、那里剩 200 槽,却都凑不出一块连续大槽"的碎片,导致明明还有总空闲显存、却因不连续而无法接纳新请求。

举个具体例子:一张 24G 卡,每个请求预留 4K 槽位约 2 GB。传统连续分配下,最多放 12 个请求就"连续空间不足"而报 OOM;但实际 12 个请求可能平均才生成 500 token,真实只用掉约 12×0.25 GB = 3 GB,剩下 21 GB 因为碎片和预留而闲置。这就是为什么"显存明明还剩一大块却 OOM"。

作者判断(真实踩坑):我见过太多团队把"OOM"简单归因为"模型太大",然后把 batch 调小一半,吞吐立刻腰斩。其实很多时候瓶颈是 KV Cache 的预留浪费和碎片——同样一张 24G 卡,用连续预分配可能只能跑 batch=4,换成 PagedAttention 的分页式管理能跑到 batch=12 以上。这部分我们在第 2 章 2.2 节、第 3 章会彻底讲透。

1.2.6 给 KV Cache 瘦身的几种思路(先铺垫)

既然 KV Cache 这么占显存又占带宽,业界自然有一堆"减肥"办法,先在这里点名,第 3 章再深入:

  • 量化:把 K、V 从 fp16 降到 fp8/int8/int4,显存与带宽需求直接按比例缩小(但要权衡精度损失);
  • Grouped Query Attention(GQA)/ Multi-Query Attention(MQA):让多个 Q 头共享同一组 K、V 头,KV 头数减少,KV Cache 成比例缩小。例如 70B 用 GQA(8 个 KV 头 vs 64 个 Q 头)后,KV 约为原来的 1/8;
  • 分页式管理(PagedAttention):不改精度、不改模型,只改"显存怎么分配",消除碎片与预留浪费,是性价比最高的一招;
  • 前缀缓存(prefix cache)/ 提示共享:多个请求共享同一段系统提示的 KV,避免重复计算与重复占用;
  • 卸载(offload):把冷 KV 换页到 CPU 内存,需要时再换回——以延迟换容量。

1.2.7 一个真实部署场景的对照

讲一个我参与过的真实案例,帮助建立"数字感"。某客服机器人,模型 13B,用户平均对话长度约 1500 token,但系统为稳妥把 max_length 设成 8192。用连续预分配时,单卡(24G)最多并发 8 路就 OOM,实测平均 KV 占用才 1500/8192 ≈ 18%。改成 PagedAttention 后,并发提到 24 路仍游刃有余,吞吐翻了近 3 倍,首字延迟基本不变。这个案例的教训很典型:问题不在模型,也不在 GPU,而在"显存分配策略"。它也为第 2 章讲 PagedAttention 埋下了最具体的动机。

1.2.8 动手验证:估算你手头模型的 KV 占用

同样建议你动手算一遍自己要部署的模型,别只信别人的数字。下面这个函数把 1.2.3 的公式封装好,填进去就能出结果(数值为量级估算,具体请以模型配置为准):

def kv_cache_gb(layers, hidden, n, batch=1, precision=2): # 2(K+V) * n * layers * hidden * batch * precision 字节,转 GB bytes_ = 2 * n * layers * hidden * batch * precision return bytes_ / 1024**3 print("7B n=4096 batch=1 :", round(kv_cache_gb(32, 4096, 4096), 2), "GB") print("7B n=4096 batch=32:", round(kv_cache_gb(32, 4096, 4096, 32), 2), "GB") print("70B n=4096 batch=1 :", round(kv_cache_gb(80, 8192, 4096), 2), "GB")

把 batch 从 1 改到 32,你会亲眼看到 KV 占用线性翻 32 倍——这就是为什么「并发」是 KV Cache 的头号杀手。再试着把 precision 从 2 改成 1(fp8)或 0.5(int4),你会看到量化能立刻把这条曲线压下去。算完这组数,1.2.5 讲的「预留浪费」和 1.2.6 讲的「减肥思路」就不再是抽象概念,而是你账本上能省下来的真金白银。

1.2.9 心智模型:KV Cache 是「按请求租下的长期仓库」

给这一节一个好记的比喻:把显存想象成一栋仓库,每个推理请求来租一个「按最大可能用量预付的仓库」。传统连续分配相当于每个租户一来就按「最大面积」预付租金、且整层不能分租给别人;短租户只占了一角却付了全款,长租户还没来、空位也租不出去,最后仓库看起来满仓(显存报警)但其实大量空间闲置。PagedAttention 相当于改成「按货架(页)灵活租用、谁用谁占、退租即释放、还能合租」。这个比喻能帮你理解为什么「总空闲显存还有,却报 OOM」——那不是显存不够,是分配方式把空间锁死了。

1.2.10 本节你该记住的三句话

为了不让你读完就忘,我把这一节浓缩成三句能直接拿去开会/写方案的话:第一,KV Cache 是推理的「长期仓库」,随并发和长度线性膨胀,是大 batch 下比权重还大的显存消耗源;第二,decode 阶段每生成一个 token 都要把全部 KV 读一遍带宽,它才是吞吐的真正天花板,而不是那点矩阵乘;第三,传统连续预分配的「预留 + 碎片 + 不共享」三重浪费,是线上 OOM 最常见的真凶,换分页式管理往往比换卡更管用。记住这三句,你在做推理容量规划时就不会再被「显存够不够」这种笼统问题糊弄,而是能精准说出「够不够取决于 batch、长度和分配策略」。

1.2.11 本节小结

  • 自回归推理必须缓存历史 K、V(KV Cache),decode 阶段每步读取全部历史,decode 是带宽受限
  • KV Cache 显存 ≈ 2·n·layers·hidden·batch·precision,大 batch 下常超过模型权重本身(7B/4096/32 约 64 GB);它是"随并发和长度线性累积的变动成本";
  • 每生成一个 token 要读约 2 GB KV(7B/4096 示例),直接顶在带宽天花板上,是吞吐的真正天花板;
  • 传统连续预分配造成"预留浪费 + 跨请求不共享 + 碎片"三重浪费,是 OOM 的常见真因;真实案例显示换分页管理可让并发翻 3 倍;
  • 减肥思路有量化、GQA/MQA、分页管理、前缀缓存、卸载——其中 PagedAttention 性价比最高,第 2、3 章见。

读后自测(1.2):合上这一节自问——① KV Cache 的显存公式里,哪个因子最容易被低估?我押「batch(并发)」,因为它不像 n 和 layers 那样写在模型卡上,而是藏在你的流量里,一上来就把显存翻好几倍。② decode 阶段每生成一个 token 大约要搬多少 KV 数据(用你自己的模型算一下)?这个数字直接决定了你的吞吐上限。③ 看到「显存还有但 OOM」,你的第一反应应该是什么?把这三个问题答顺,KV Cache 就从「抽象概念」变成你排障时的「具体抓手」。


发布者: 作者: 转发
评论区 (0)
U