1. 为什么每个搞大模型的人都绕不开 KV Cache如果你部署过任何一个像样的语言模型不管是 7B 还是 70B你迟早会撞上同一个问题显存不够用吞吐上不去延迟下不来。很多人第一反应是去优化模型权重、量化、剪枝折腾一圈发现效果有限。真正卡脖子的地方往往不在权重本身而在推理过程中那个被反复读写、疯狂吃显存的东西——KV Cache。我最早接触这个概念是在做本地部署的时候。当时用一张消费级显卡跑一个 13B 的模型单条对话还行一旦并发上来显存直接爆掉服务直接挂。查了半天日志发现显存占用跟上下文长度几乎成正比增长而且增长得非常夸张。后来才明白这就是 KV Cache 在作祟。它既是让自回归生成变得高效的关键机制也是吃掉显存、限制并发和上下文长度的头号元凶。这篇文章我想把 KV Cache 这件事从头到尾讲透。不是那种只告诉你缓存 Key 和 Value 可以加速的泛泛之谈而是从它为什么存在、它到底占多少显存、怎么算、怎么优化、工程上怎么落地这几个角度把我在实际部署和调优中踩过的坑、算过的账、用过的方案都摊开来讲。适合正在做推理优化、模型部署、或者单纯想把大模型原理搞明白的读者。哪怕你只是用 Ollama 或者 vLLM 跑过模型看完也能明白那些显存数字背后到底发生了什么。2. KV Cache 的本质用空间换时间的经典交易2.1 自回归生成到底在重复算什么要理解 KV Cache得先理解 Transformer 解码器是怎么生成文本的。大模型生成是自回归的也就是一个 token 一个 token 往外蹦。每生成一个新 token模型都要把到目前为止的所有 token重新过一遍注意力计算。问题就出在这里。假设你要生成第 100 个 token按照最朴素的做法模型需要把前面 99 个 token 加上当前这个一共 100 个 token 全部送进网络重新计算一遍注意力。而生成第 101 个 token 时又要重新算这 101 个。这种重复计算是巨大的浪费因为前面那些 token 的 Key 和 Value 投影结果其实根本没有变。注意力的计算公式是Attention(Q, K, V) softmax(QK^T / sqrt(d)) V其中 Q 是当前 token 的查询向量K 和 V 是序列中所有 token 的键和值。对于已经处理过的历史 token它们的 K 和 V 是固定的不会因为后面来了新 token 而改变。既然不变为什么每次都要重算答案就是——缓存起来。2.2 KV Cache 的核心思想KV Cache 做的事情非常直白把每一层、每个历史 token 计算出来的 Key 和 Value 张量存下来生成新 token 时直接复用只计算当前这个新 token 的 Q、K、V。这样一来每生成一个 token注意力计算从对全序列重算变成了当前 token 的 Q 去和缓存的 K、V 做一次注意力。计算复杂度从每步 O(n²) 降到 O(n)整个生成过程的复杂度从 O(n³) 降到 O(n²)。这个提升是数量级的没有 KV Cache现在的大模型推理根本不可能实用。用一个生活化的类比你在写一份长报告每写一句都要回头把前面所有句子重新读一遍才能保证连贯。KV Cache 相当于你给前面每句话都做了摘要卡片写新句子时只需要翻卡片不用重读全文。卡片就是缓存翻卡片就是复用。2.3 为什么是 Key 和 Value不是 Query这里有个很多人会疑惑的点为什么只缓存 K 和 V不缓存 Q原因在于 Q 的角色。Query 是当前这个 token 想查询什么它只跟当前 token 有关每个新 token 都会产生一个新的 Q历史 token 的 Q 在后续计算中根本用不到。而 K 和 V 是历史 token 提供的信息它们会被后续所有 token 的注意力反复查询所以必须缓存。换句话说Q 是一次性的K 和 V 是长期资产。缓存长期资产丢弃一次性消耗品这就是 KV Cache 的经济学。3. 算一笔账KV Cache 到底吃掉多少显存3.1 显存占用的计算公式很多人对 KV Cache 的显存占用没有概念觉得缓存嘛能有多大。实际算一下你就知道为什么它这么可怕。KV Cache 的显存占用公式是KV Cache 显存 2 × batch_size × seq_len × num_layers × num_kv_heads × head_dim × dtype_bytes逐个解释这些参数2因为要存 Key 和 Value 两份batch_size并发处理的序列数seq_len序列长度也就是上下文长度num_layersTransformer 层数num_kv_headsKV 注意力头数量注意不是 Q 的头数后面讲 GQA 会展开head_dim每个注意力头的维度dtype_bytes数据类型字节数FP16 是 2FP8 是 1INT8 是 13.2 一个真实模型的显存账拿一个典型的 13B 模型举例假设配置是40 层hidden size 512032 个注意力头head_dim 128FP16 精度。在标准的多头注意力MHA下num_kv_heads 等于 num_heads也就是 32。那么单个 token 的 KV Cache 占用是2 × 1 × 1 × 40 × 32 × 128 × 2 655,360 字节 ≈ 0.625 MB看起来不多但这是单个 token。如果上下文长度是 4096单个序列就是0.625 MB × 4096 ≈ 2.56 GB如果并发 8 条序列就是 20.5 GB。而模型权重本身 FP16 下大约 26 GB。也就是说KV Cache 的显存占用几乎和模型权重一个量级甚至在高并发长上下文场景下会超过权重。这就是为什么很多人发现模型能加载进去但一跑长对话就 OOM。权重是固定的KV Cache 是随上下文和并发动态增长的它才是那个不可控的变量。3.3 不同配置下的显存对比我把几种常见配置的 KV Cache 占用整理成表格方便你对照自己的场景估算模型规模层数KV头数上下文并发FP16 KV Cache7B323220481约 0.5 GB7B323281928约 8 GB13B403240968约 20 GB70B808 (GQA)819216约 21 GB70B8064 (MHA)819216约 168 GB最后一行那个 168 GB 不是笔误。70B 模型如果用标准 MHAKV Cache 会大到离谱这也是为什么现在几乎所有大模型都改用 GQA 的原因。这个表你可以直接拿去估算自己的部署方案把并发和上下文代进去心里就有数了。提示估算时一定要把 batch_size 和 seq_len 都考虑进去很多人只算单条序列结果上线一并发就崩。4. 从 MHA 到 GQAKV Cache 优化的主战场4.1 多头注意力为什么这么费缓存标准的多头注意力MHAMulti-Head Attention里每个注意力头都有自己独立的 Q、K、V 投影。32 个头就是 32 组 K 和 V全部要缓存。头越多缓存越大。但研究者后来发现KV 头其实没必要和 Q 头一样多。Q 需要多个头来捕捉不同的查询视角但 K 和 V 提供的信息可以在多个 Q 头之间共享。这个洞察直接催生了 MQA 和 GQA。4.2 MQA激进但有效的共享MQAMulti-Query Attention的做法是所有 Q 头共享同一组K 和 V。也就是说 num_kv_heads 1。这样 KV Cache 直接缩小到原来的 1/num_heads。32 个头的话缓存变成原来的 1/32效果立竿见影。但代价是模型质量会下降因为 K 和 V 的表达能力被压缩得太狠注意力机制失去了部分区分度。MQA 在推理速度上很香但质量损失让很多团队不敢直接用。4.3 GQA质量和效率的平衡点GQAGrouped-Query Attention分组查询注意力是目前的主流方案。它的思路是折中把 Q 头分成若干组每组共享一组 K 和 V。比如 32 个 Q 头分成 8 组每组 4 个 Q 头共享一组 KV那么 num_kv_heads 8。KV Cache 缩小到原来的 8/32 1/4质量损失却比 MQA 小得多。这也是为什么 Llama 2 70B、Llama 3、Qwen 等主流模型都用 GQA。它把 KV Cache 这个显存大户压到了一个可接受的范围同时保住了模型效果。注意力类型KV头数相对缓存大小质量典型使用者MHA Q头数100%最好早期模型GQAQ头数/组数25% 左右接近 MHALlama 3、QwenMQA13% 左右有损失部分推理优化场景4.4 实操心得选模型时先看 KV 头数我在选型的时候有个习惯拿到一个模型先看它的 config 里 num_key_value_heads 是多少。如果这个数等于 num_attention_heads说明是标准 MHA部署时显存压力会很大并发上不去。如果是 GQA心里就踏实一半。这个细节很多人忽略但它直接决定了你的部署成本和并发能力。同样参数量的两个模型GQA 的那个可能能多扛 3 到 4 倍的并发。5. PagedAttention把操作系统内存管理搬进显存5.1 传统 KV Cache 的浪费在哪即便用了 GQAKV Cache 的显存管理还是有问题。传统做法是为每条序列预分配一块连续的显存大小按最大可能长度算。这就带来两个浪费第一预留浪费。你按 4096 长度预分配但实际对话可能只用了 500 个 token剩下 3596 个位置的显存白白占着别的请求用不了。第二碎片浪费。不同序列长度不一连续分配会导致显存碎片最后明明总量够却找不到一块连续空间。实测下来传统方案下 KV Cache 的显存利用率经常只有 20% 到 40%大部分显存都在空转。5.2 PagedAttention 的核心机制vLLM 提出的 PagedAttention 借用了操作系统的虚拟内存分页思想。它把 KV Cache 切成固定大小的块block比如每块存 16 个 token 的 KV。序列的 KV 不再要求连续存放而是通过一张块表block table把逻辑上的连续序列映射到物理上分散的块。这样做的好处按需分配用到多少分配多少不再预留消除碎片块大小固定任何空闲块都能用共享前缀多条序列如果开头相同比如相同的 system prompt可以共享同一批块省显存vLLM 的实测数据显示PagedAttention 能把 KV Cache 的显存利用率从不到 40% 提升到 90% 以上吞吐量提升 2 到 4 倍。这个提升在长上下文和高并发场景下尤其明显。5.3 前缀共享一个被低估的省钱技巧PagedAttention 带来的前缀共享能力在实际业务里价值巨大。很多应用场景下大量请求共享同一段很长的 system prompt 或者 few-shot 示例。传统方案下每个请求都要为这段前缀单独存一份 KV浪费惊人。用 PagedAttention这段前缀的块只存一份所有请求的块表都指向它。我做过一个测试在一个 system prompt 长达 2000 token 的场景下开启前缀共享后显存占用直接降了 60% 以上并发能力翻倍。注意前缀共享要求前缀完全一致包括每一个 token。差一个字符都会导致无法共享所以设计 prompt 模板时要尽量标准化。6. 工程落地KV Cache 优化的实操清单6.1 量化 KV Cache除了结构优化最直接的省显存手段是量化 KV Cache。把 KV Cache 从 FP16 降到 FP8 或 INT8显存直接减半。现在 vLLM、TensorRT-LLM 等框架都支持 KV Cache 量化。FP8 量化对质量影响很小基本可以无脑开。INT8 稍微激进一点但在大多数场景下也够用。我的一般建议是显存紧张就上 FP8还紧张再考虑 INT8同时监控输出质量有没有明显退化。6.2 合理设置 max_model_len 和并发很多人部署时把 max_model_len 设成模型支持的最大值比如 128K觉得越大越好。但实际上max_model_len 直接决定了 KV Cache 的峰值占用。设成 128K框架就要为可能的 128K 长度准备资源。我的做法是根据实际业务需求设置。如果业务里 99% 的请求都在 8K 以内那就设 8K 或 16K别为了那 1% 的极端情况牺牲整体并发。6.3 调度策略抢占和交换当显存真的不够时框架需要决定牺牲谁。常见策略有两种交换Swap把暂时不活跃序列的 KV Cache 换出到 CPU 内存需要时再换回来重计算Recompute直接丢弃某些序列的 KV Cache需要时重新算一遍交换省算力但费带宽重计算省显存但费算力。vLLM 会根据情况自动选择。理解这个机制有助于你判断为什么某些请求会突然变慢——很可能是它的 KV Cache 被换出或丢弃了。6.4 常见问题速查表现象可能原因排查方向加载成功但一对话就 OOMKV Cache 峰值超显存降 max_model_len、开量化、减并发并发上不去KV Cache 占用过高换 GQA 模型、开 PagedAttention长对话越来越慢KV Cache 换出/重计算检查显存余量、调调度参数显存利用率低未用分页或碎片严重启用 PagedAttention、开前缀共享输出质量下降KV 量化过度从 INT8 退回 FP8 或 FP167. 我在实际调优中的几点体会KV Cache 这个东西理论看着简单真到工程里全是细节。我踩过最深的坑是早期用连续分配方案显存明明显示还有 10G但新请求就是进不来最后发现是碎片问题换成 PagedAttention 后立刻顺畅。另一个体会是不要迷信参数。同样标称 32K 上下文的模型实际能稳定跑多长取决于你的显存、并发和框架实现。我一般会做压力测试从短到长逐步加找到那个再长就崩的临界点然后留 20% 余量作为生产配置。还有一点KV Cache 的优化不是孤立的。它和批处理策略、调度算法、量化方案是联动的。单独调一个参数往往效果有限要整体看。比如你开了 PagedAttention就可以更激进地提高并发因为显存利用率上去了反过来如果还用连续分配提高并发只会加速 OOM。最后分享一个小技巧监控 KV Cache 的实际占用率。vLLM 会暴露 gpu_cache_usage 这类指标盯着它看你就能知道自己的显存到底是被浪费了还是真的用满了。这个数字比总显存占用有用得多它直接告诉你还有多少优化空间。
阅读完成 · 觉得有帮助?