KV Cache全解析:大模型推理显存计算与长上下文优化实战 我的大模型学习记录写到第六篇了。前几篇从tokenizer一路折腾到Transformer结构又从预训练聊到指令微调自以为对“大模型是怎么工作的”已经有点感觉了。直到上周试着把某个7B开源模型在本地跑起来输入一段2K字的长文本让它做摘要才发现真正卡脖子的地方不在训练而在推理阶段——显存涨得飞快每秒只吐出几个token整张显卡都在发抖。这篇记录我准备把推理时人人都提、但很少有人把账算清楚的KV Cache彻底拆开看一遍它从哪来、要吃多少显存、长上下文时该怎么管以及我实际把上下文从2K推到32K时踩进去的那些坑。1. 为什么生成长文本反而变慢KV Cache是怎么“被迫”出现的1.1 没有缓存时每一步都像从零开始先还原一下生成过程。大模型是自回归的给定一段输入它一次只预测下一个token预测完把这个token拼到句子末尾再预测下下个。每生成一个token整个模型都要重新跑一次前向计算序列里每个位置都要做一次注意力计算。问题就出在这个“重新跑一次”上。注意力的公式是 Attention(Q, K, V) softmax(QK^T / sqrt(d_k)) V。其中Q来自当前新token而K和V来自句子里的所有历史token。如果不做任何缓存那么每生成一个新token都要把所有历史token的K和V重新算一遍。生成第一个token时要算1个位置的K/V生成第二个时要算2个第三个时要算3个……累计计算量是12...L也就是O(L^2)。这意味着序列越长生成速度会以平方的速度劣化而不是线性退化。这个场景很像写一篇论文时每写一句话就要把之前所有引用文献重新读一遍摘要。明明你上一步已经读过这些文献了下一步却全部推倒重来。模型当然不会觉得累但显存带宽和算力会。1.2 KV Cache一场典型的“用空间换时间”工程师们的解决办法很朴素既然K/V每步都会被重复使用那就把算好的结果先存起来。KV Cache就是在推理过程中把每一层、每一个历史token的K矩阵和V矩阵缓存在显存里。新token来了以后只需要计算它自己的Q然后直接从显存里读取历史K/V完成注意力计算即可。这样一来解码阶段的计算量就从O(L^2)降到了O(L)每一步只需要处理一个新token的Q历史K/V都是现成的。这个优化在工程上是决定性的没有KV Cache的Transformer解码器根本没法用于实际生产——7B模型生成2000个token会慢到让人怀疑显卡坏了。代价也很直接显存。历史token的数量越多KV Cache占用的显存就越大而且它是线性增长的。上下文翻一倍KV Cache就翻一倍。搞懂模型为什么“长文本跑不起来”其实就是在搞懂这笔显存账。2. 把KV Cache的显存账算清楚7B模型在4K上下文下的开销拆解2.1 一个公式自己就能估算显存KV Cache的大小可以写成这样KV Cache字节数 2K和V各一份 × 层数 × KV头数 × 序列长度 × 每个头的维度 × 每个数值的字节数以目前主流的7B参数量为参照我按常见的配置来算32层、隐藏维度4096、32个Q头、8个KV头用分组查询注意力GQA、每个头的维度128、半精度FP16每个数值2字节。先算每个token要占多少K和V两份 × 32层 × 8个KV头 × 128维 × 2字节 131,072字节正好128KB。这个128KB意味着什么呢看下表上下文长度KV CacheGQA8个KV头对应MHA32个KV头2K256MB1GB4K512MB2GB8K1GB4GB32K4GB16GB128K16GB64GB一个24GB显存的显卡如果用多头注意力MHA结构4K上下文就要吃掉2GB32K直接吃掉16GB再算上模型权重和中间激活基本就到了极限。这也是为什么现在新的开源模型普遍采用GQA甚至更进一步做MLA多头潜在注意力——因为KV Cache已经在实打实地决定你显卡的上限。2.2 从MHA到GQA再到MLA为什么新模型都在压缩KVMHA每个注意力头都有自己独立的K/VKV头数等于Q头数。以7B为例通常就是32个KV头每个token缓存512KB开销最大但效果最“传统”。GQA让多个Q头共享一组K/V比如32个Q头只配8个KV头。KV Cache直接降到MHA的1/4效果损失在主流任务上很小属于性价比极高的折中。MLA更进一步把K/V先压缩到一个低维隐向量再缓存有的实现每个token只需要保存几十KB量级的数据同时还能保证效果。我自己在选模型时有个习惯先看一眼它的注意力结构。如果是MHA且只有24G显卡那长上下文基本别指望如果是GQA32K还能勉强跑一跑如果是MLA128K都有操作空间。这个判断比看宣传页上的“支持多少K上下文”靠谱得多。3. 长上下文场景下的缓存管理滑动窗口、注意力锚点与显存分页3.1 预分配缓冲区的陷阱显存是按最大值花的很多推理框架为了省事会预先给KV Cache分配一个最大值。假设最大长度设成32K那不管你实际只输入100个tokenKV Cache也按32K的容量占着。在单卡自己玩的时候这问题不明显但做部署、做多并发服务时就是灾难每个人都在按最大值占空间能同时服务的用户数少得可怜。业界的解法思路和操作系统管理内存很像。显存被切成固定大小的“页”或“块”KV Cache按需分配不够了再申请新块块与块之间物理上不必连续。这种做法也叫“按块注意力”或“分页注意力”。它最大的价值不是省显存而是让显存利用率大幅提高支持更大的并发和更长的上下文。理解这一层后再看推理框架就不会觉得那些参数设置是在故弄玄虚。3.2 滑动窗口与Attention Sink为什么不能只留“最近聊天记录”另一个常见的省显存思路是滑动窗口既然上下文太长那就只保留最近N个token的K/V更早的丢掉毕竟人聊天也是只记最近几句。这个思路本身没问题但直接丢掉开头会出大事。实际实验里会发现序列第一个token的注意力权重总是异常地高所有位置都在“注视”它哪怕它只是一个句号或者一个无关紧要的助词。这种现象被称为Attention Sink注意力汇聚点。让模型失去这个汇聚点生成质量会迅速崩溃。所以后来很多长上下文方案采取的是“锚点窗口”的组合保留最前面的若干token作为全局锚点同时维护一个滑动窗口管理最近的token中间的过期KV按需淘汰。这个设计启示很实用你以为自己在管理“最近聊天记录”其实还要保住开头的“定盘星”。在我自己的部署实践中遇到长文本任务时会专门把任务描述、角色设定放在序列开头而不是放在离生成位置最近的地方。3.3 长上下文不是越长越好的“免费午餐”把上下文从4K扩到32K你能装进prompt的材料多了但KV Cache线性增长、计算量平方增长、注意力被稀释的风险也跟着涨。我在某次长文档问答测试中发现上下文拉到16K之后模型确实能引用远端的材料但对中段信息的召回率反而下降了不少。这说明“窗口够长”和“内容能记住”是两回事缓存管理做得再好也只是保证工程上跑得动不保证语义上记得牢。后续做长文本任务时我把检索增强作为标配先筛信息再把最相关的原文放到prompt靠后的位置效果好了很多。4. KV Cache量化与稀疏注意力省显存时到底在牺牲什么4.1 INT8量化KV Cache显存省一半但是有“离群值”要注意既然KV Cache是显存大头自然有人想到把它从FP16变成INT8一档直接省一半。以128KB/token为例量化后降到64KB/token32K上下文从4GB降到2GB这对24GB显卡来说是实打实的宽容度。但量化没这么简单。K/V矩阵里经常存在少数“离群值”——个别通道的数值特别大或者特别小如果按全局min-max范围做整数映射大部分正常数值会被压得很粗糙精度损失会被放大。实践中通常用按头、按通道或按token分组的量化方式让每个局部范围都有自己的缩放系数才能把误差压住。我实测下来INT8量化KV Cache在普通生成任务里几乎没有感知差异但做低困惑度的评估任务时困惑度会有少量上升如果原本就是长上下文多轮对话误差会不会累积最好用你自己的数据实测一遍不要只看宣传里的“无损”。4.2 稀疏注意力用“只关注重点”换显存和速度再往前走一步就是稀疏注意力不去保留全部历史token的KV而是只保留局部窗口、加上少数全局锚点或者根据注意力分数只保留Top-K个关键位置。好处非常明显显存和计算量可以再降一个量级。代价也很明显——如果答案恰好藏在被丢弃的那段中间文本里模型就彻底“失忆”了这不是量化那种精度损失而是信息根本不在缓存里。我的经验是在RAG类场景里目标段落往往已经被检索到并重新组织过稀疏注意力可以放心用但在开放域长文档问答中模型需要自己去长文中定位线索保守才是对的顶多只在窗口选择上做文章别把缓存丢得太大胆。5. 实测同一个7B模型从2K上下文推到32K我踩过的坑5.1 实验配置与账面数据我用的是一张24GB的单卡加载某7B开源模型权重做4bit量化模型注意力结构是GQA8个KV头分别对比2048、8192、32768三档上下文长度测了KV Cache、峰值显存和生成速度。上下文长度KV CacheFP16KV CacheINT824G卡峰值显存生成速度约2K256MB128MB7.5GB25 token/s8K1GB512MB10.8GB18 token/s32K4GB2GB22.6GB7 token/s账面数据基本符合前面的公式32K档用FP16时整张卡已经非常紧张生成速度掉到个位数。INT8量化后显存松了一些但速度并没有大幅回升——因为长上下文下访存量依然很大。5.2 三个真实症状与对应解法第一个症状是复读。把上下文推到超过模型训练长度后生成内容开始卡在某几句话反复绕圈明显是注意力分布出了问题。位置编码外推时softmax的分数会被某些位置主导模型像是“忘了下一步该说什么”。我试了把RoPE的base从10000调大又加了NTK-Aware的缩放复读情况才明显缓解困惑度也回落到正常区间。第二个症状是“感觉模型忘了我开头说了什么”。明明注意力理论上覆盖所有位置但模型对序列开头的关注是“礼貌性”的真正有效用的往往集中在最后一个位置附近。最开始的提示、背景材料放在序列中部时召回很不稳定。后来我调整了prompt的组织方式关键指令放开头关键材料放结尾中间只放辅助信息。这个改动比换任何解码参数都来得有效。第三个症状出现在INT8量化32K的超长档。模型在多文档问答里偶尔会引用错误文件里的内容单独跑FP16时没有这个问题。看起来量化误差在超长上下文叠加后确实影响了模型对远端信息的判断。我最后对文档类任务保留FP16对一般闲聊和摘要任务用INT8既保住了关键场景的质量也换来了显存余量。5.3 别迷信“32K”这个数字最后一个体会是宣传上的“32K上下文”和你实际能用的32K是两个概念。能跑通只说明位置编码和显存管理撑得住能不能答好还得看注意力分布、KV量化策略、prompt组织方式甚至你的检索方案。我现在选模型会先看注意力结构和KV Cache算法再看训练时的长度外推方法最后才看宣传页上的最大长度。这篇记录写到这里我对KV Cache的心态已经从“知道名字”变成了“能算账、能排障”。如果你也在折腾大模型推理建议先把上面那个公式代入自己的模型试算一遍再决定要不要上长上下文、要不要开量化。毕竟显存的每一G都是要花真金白银买的。