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.
读者读完这一节,应该能回答一句:自回归推理为什么要缓存 K、V,而它又是怎么把显存"按最大长度"白白吃掉一大块的。
训练时,一段序列长度是固定的,注意力一次性算完。但自回归推理(你现在用的每个对话模型都在做这件事)是逐个 token 往外蹦的:生成第 t 个 token,要用到前面 1..t-1 所有 token 的 K、V。如果每生成一个新 token 都把前面所有 token 重新算一遍 K、V,成本会随长度线性累加,完全没有性价比。于是工程上引入 KV Cache:把历史 K、V 直接缓存下来,新 token 只算自己的那一份,再"拼"上前人的缓存。
流程是这样的:
注意两个关键点。第一,每生成一个新 token,注意力计算要"读取"前面所有历史 K、V。所以缓存越长,每一步要读的数据越多——这是推理"越往后越慢"的直观来源之一,也是 decode 阶段之所以慢的本质。第二,KV Cache 一旦写入就要长期驻留到该请求结束,它不像 1.1 里的中间矩阵算完可释放,而是整个生成生命周期都占着显存。
这里有一个反直觉的要点值得停下来想:KV Cache 的"好处"和"坏处"来自同一件事——它避免了重复计算。正因为不重复算,所以它必须一直存着;正因为一直存着,所以显存和带宽都长期被它占用。这是推理系统和训练系统最根本的不同:训练可以"算完就扔",推理必须"存着等下一步用"。所有推理显存优化的故事,本质上都是在和"这个必须长期存着的状态"打交道。
理解 KV Cache 的压力,必须分清推理的两个阶段,因为它们的瓶颈完全不同:
这就是为什么"生成阶段"常常比"首字延迟"更影响吞吐:在线服务里大量请求都在 decode,全都在反复读各自的 KV Cache。KV Cache 的内存带宽占用,直接决定了每秒能生成多少 token(tokens/s)。这也解释了为什么很多推理引擎会专门优化 decode(例如 FlashDecoding 的思路:把长序列的 K/V 分块并行处理单个 query)。理解 prefill 和 decode 的差别,是后面理解"为什么有的优化只加速生成、有的只加速首字"的前提。
它的显存占用可以拆成一个很直观的公式:
KV 显存 ≈ 2 × n × layers × hidden × batch × precision
逐项解释:
代入几个真实配置感受量级(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 才是那张真正该盯的表。
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 这类"显存怎么摆、怎么读更顺"的系统层手段。
KV Cache 真正的坑,不止"占得多",更在"占得笨"。传统实现(例如早期 HuggingFace generate 的默认行为)会为每个请求按"最大可能序列长度"预先分配一整块连续的显存槽位。问题有三:
举个具体例子:一张 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 章会彻底讲透。
既然 KV Cache 这么占显存又占带宽,业界自然有一堆"减肥"办法,先在这里点名,第 3 章再深入:
讲一个我参与过的真实案例,帮助建立"数字感"。某客服机器人,模型 13B,用户平均对话长度约 1500 token,但系统为稳妥把 max_length 设成 8192。用连续预分配时,单卡(24G)最多并发 8 路就 OOM,实测平均 KV 占用才 1500/8192 ≈ 18%。改成 PagedAttention 后,并发提到 24 路仍游刃有余,吞吐翻了近 3 倍,首字延迟基本不变。这个案例的教训很典型:问题不在模型,也不在 GPU,而在"显存分配策略"。它也为第 2 章讲 PagedAttention 埋下了最具体的动机。
同样建议你动手算一遍自己要部署的模型,别只信别人的数字。下面这个函数把 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 讲的「减肥思路」就不再是抽象概念,而是你账本上能省下来的真金白银。
给这一节一个好记的比喻:把显存想象成一栋仓库,每个推理请求来租一个「按最大可能用量预付的仓库」。传统连续分配相当于每个租户一来就按「最大面积」预付租金、且整层不能分租给别人;短租户只占了一角却付了全款,长租户还没来、空位也租不出去,最后仓库看起来满仓(显存报警)但其实大量空间闲置。PagedAttention 相当于改成「按货架(页)灵活租用、谁用谁占、退租即释放、还能合租」。这个比喻能帮你理解为什么「总空闲显存还有,却报 OOM」——那不是显存不够,是分配方式把空间锁死了。
为了不让你读完就忘,我把这一节浓缩成三句能直接拿去开会/写方案的话:第一,KV Cache 是推理的「长期仓库」,随并发和长度线性膨胀,是大 batch 下比权重还大的显存消耗源;第二,decode 阶段每生成一个 token 都要把全部 KV 读一遍带宽,它才是吞吐的真正天花板,而不是那点矩阵乘;第三,传统连续预分配的「预留 + 碎片 + 不共享」三重浪费,是线上 OOM 最常见的真凶,换分页式管理往往比换卡更管用。记住这三句,你在做推理容量规划时就不会再被「显存够不够」这种笼统问题糊弄,而是能精准说出「够不够取决于 batch、长度和分配策略」。
读后自测(1.2):合上这一节自问——① KV Cache 的显存公式里,哪个因子最容易被低估?我押「batch(并发)」,因为它不像 n 和 layers 那样写在模型卡上,而是藏在你的流量里,一上来就把显存翻好几倍。② decode 阶段每生成一个 token 大约要搬多少 KV 数据(用你自己的模型算一下)?这个数字直接决定了你的吞吐上限。③ 看到「显存还有但 OOM」,你的第一反应应该是什么?把这三个问题答顺,KV Cache 就从「抽象概念」变成你排障时的「具体抓手」。