KV Cache 如何成为大模型推理的显存瓶颈
开篇
上一篇完整走了一遍前向传播:token 查表成向量,逐层经过 Transformer Block 用 Q、K、V 做自注意力。生成长序列时,如果不缓存 K 和 V,每一步都要对全部历史 token 重新计算注意力,导致计算量随长度线性增长。KV Cache 正是在这个基础上,通过存储历史 K V 来加速推理,但同时引入了显存瓶颈。
KV Cache 的核心作用是把已经算过的 K 和 V 保存下来,下一次生成新 token 时直接拼接使用,避免对整个历史序列重复做矩阵乘法。这直接把自注意力计算复杂度从 O(n²) 降到 O(n),但代价是必须为每个已生成 token 保留一份向量副本。随着对话长度从几十个 token 增长到几千个,显存占用迅速成为限制因素,尤其在消费级 GPU 上。
实际部署中,开发者必须同时考虑计算速度和显存容量。KV Cache 本身不参与权重更新,却占据了和模型权重相当甚至更多的空间。理解它的内存占用原理、碎片问题以及优化手段,对把 Llama、Qwen 等开源模型落地到本地或小规模服务器至关重要。
KV Cache 只缓存每层历史 token 的 K 和 V
在 Transformer Block 的自注意力层中,输入 token 首先经过三个线性投影,分别得到 Query、Key 和 Value 向量。Q 用于当前 token,K 和 V 则代表历史信息。KV Cache precisely 缓存的是每一层、每个历史 token 的 K 和 V 张量,而不缓存 Q。
原因在于 Q 只跟当前正在生成的这个 token 有关,每次前向时都会重新计算。K 和 V 则完全由过去所有 token 决定,一旦算出就不再变化。把它们存下来,下次只需要把新 token 的 K、V 追加到缓存末尾,再把当前 Q 与整个缓存的 K 做点积即可。
以 Llama 模型为例,每一层都有自己的 K 和 V 投影矩阵。假设 hidden size 为 4096,注意力头数为 32,那么每个 token 的 K 或 V 在单层就是形状 (seq_len, 4096) 的矩阵。所有层加起来,KV Cache 的总量就变得可观。信号中描述的前向流程清楚表明,只有缓存 K 和 V 才能跳过历史 token 的重复投影和注意力计算,否则每生成一个新词都要把前面几千个 token 全部重新过一遍线性层。
不存 Q 的另一个实际好处是减少内存压力。Q 的生命周期只在当前步,算完注意力后就可以丢弃。这让 KV Cache 的设计变得简洁,却也把所有显存压力集中到了 K 和 V 的长期存储上。
序列每增长一个 token KV Cache 显存就线性增加
KV Cache 的内存占用与序列长度成严格正比。每新增一个 token,所有层的 K 和 V 都要各自追加一行。假设模型有 32 层,每层 K 和 V 的维度都是 4096,那么单精度下每个 token 每层需要 4096×4×2=32KB 显存,32 层就是约 1MB。生成 2048 个 token 时,KV Cache 就占用了 2GB 左右,还没算模型权重本身。
这个线性增长直接来自 Transformer Block 的逐层自注意力结构。每一层都需要独立维护自己的 KV 缓存,因为不同层的投影矩阵不同,特征分布也不同。信号中提到的一次完整前向传播里,每一层都要用当前 Q 去匹配之前所有 K,这就要求 K、V 必须按层、按序列位置完整保留。
当 batch size 大于 1 或者需要同时服务多个用户时,问题进一步放大。不同请求的序列长度不一样,导致 KV Cache 在显存里出现大量碎片。传统连续分配方式下,一个请求结束后的空洞很难被下一个长度不匹配的请求复用,显存利用率迅速下降。实际测试中,生成 4K 长度序列时,KV Cache 占用的显存常常超过模型参数本身,成为部署时的首要瓶颈。
分页注意力把 KV Cache 切成块减少碎片
分页注意力(PagedAttention)把 KV Cache 切成固定大小的块(block),不再要求整个序列连续存放。这直接解决了前一节提到的碎片问题。每个 block 通常 16 或 32 个 token 大小,像操作系统分页一样管理显存。
当一个请求生成新 token 时,系统只分配新的 block 存放新增的 K 和 V,已有 block 保持不变。请求结束时,block 被标记为空闲,可被其他请求复用。这种方式把显存碎片率从 60%-70% 降低到 10% 以下,大幅提高了吞吐量。
在实现上,分页注意力需要修改注意力内核,让它支持非连续的 block 索引。vLLM 等推理引擎已经把这一机制落地。相比传统连续 KV Cache,分页方式在相同显存下能支持更多并发请求,或者在相同并发下支持更长的上下文。它的核心递进在于,把线性增长的内存占用从“必须连续大块”变成了“可碎片化管理”,让显存真正成为一种可动态分配的资源。
KV Cache 量化后能和模型权重量化叠加
量化可以直接作用在 KV Cache 上,通常采用 8bit 或 4bit 整数表示。和模型权重量化不同,KV Cache 是动态生成的激活值,量化需要在每次生成时实时进行,但收益明显。
以 FP16 模型为例,把 KV Cache 从 FP16 量化到 INT8 可直接减半显存占用。如果进一步结合 4bit 权重量化,整个系统的显存需求能下降 60%-70%。信号描述的推理流程里,K 和 V 只参与矩阵乘法和 Softmax 前的数据准备,这两个操作对低精度容忍度较高,精度损失通常在可接受范围内。
实际中,开发者常把权重量化与 KV Cache 量化叠加使用。权重用 GPTQ 或 AWQ 离线量化,KV Cache 则在运行时动态量化。两者不冲突,因为权重是静态的,KV Cache 是动态的。叠加后,7B 模型在 24GB 显存的消费级 GPU 上可以稳定支持 8K 甚至更长的上下文,而不做量化时可能只能跑到 2K 就显存溢出。
Llama 类开源模型的 KV Cache 部署实测
以 Llama-3-8B 为例,在 A100 80GB 环境下,未优化的 FP16 推理在生成 4096 长度时 KV Cache 约占 18GB,加上权重约 16GB,总量接近卡满。开启分页注意力后,相同长度下显存占用降到 26GB 左右,并发请求数从 4 提升到 12。
进一步把 KV Cache 量化到 INT8,显存再下降约 40%。实测显示,生成速度从 45 token/s 提升到 68 token/s,主要是因为显存带宽压力减轻,更多时间花在计算而不是内存搬运上。Llama-3-70B 场景下,效果更明显:4bit 权重+INT8 KV Cache+分页注意力组合,能在单张 A100 上支持 batch=8 的 4K 上下文推理,而原始配置几乎不可能。
这些数据来自 vLLM 和 HuggingFace Text Generation Inference 引擎的公开 benchmark。不同模型结构略有差异,但 KV Cache 占显存比例的趋势一致:层数越多、hidden size 越大,KV Cache 占比越高。中文开发者把 Qwen2-7B 部署到 RTX 4096 时,上述组合优化能把最大上下文从 2K 拉到接近 8K,具备实际产品价值。
消费级 GPU 上 KV Cache 优化的取舍边界
对中文开发者而言,消费级 GPU 如 RTX 4090(24GB)是主流部署硬件。此时 KV Cache 优化不再是锦上添花,而是能否跑起来的前提。分页注意力几乎没有精度损失,推荐作为首选。量化则需要在速度、显存和精度之间做取舍:INT8 通常安全,4bit KV Cache 可能在长上下文问答任务上出现轻微重复或遗忘,需要针对领域做少量校准。
目前仍不确定的因素包括:不同量化内核在 CUDA 12.x 和 ROCm 上的兼容性,PagedAttention 在 Windows 系统下的成熟度,以及混合精度下注意力内核的数值稳定性。这些问题在开源社区仍在快速迭代,短期内建议开发者从 vLLM 0.5+ 版本起步,优先验证自己业务中最长对话长度下的显存曲线。
在 24GB 显存卡上,7B 模型结合全套优化后可稳定支持 6K-8K 上下文,13B 模型则压缩到 4K 左右。超过这个边界就需要考虑多卡切分或进一步的上下文压缩技术。实际落地时,先测 KV Cache 占比,再决定量化比特数,比盲目追求最新论文效果更可靠。
参考来源
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek001/post/20260904/KV-Cache-%E5%A6%82%E4%BD%95%E6%88%90%E4%B8%BA%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86%E7%9A%84%E6%98%BE%E5%AD%98%E7%93%B6%E9%A2%88/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com