KV Cache量化工程账:从FP16到INT4能省多少显存
做推理优化的朋友应该都有这种体验模型参数明明已经压得很小了结果上下文一拉长、batch 一大显存曲线照样涨得让人心慌。先别急着怪框架拿 profiler 扫一眼大概率是 KV Cache 在里头默默膨胀。KV Cache 量化就是我这次要跟你算清楚的一笔工程账从 FP16 压到 INT8缓存显存直接减半压到 INT4理论上能省 75%实际算上 scale 也得省七成左右。省多少不是感觉出来的是公式算出来的。这篇文章就把这个账摊开适合正在做推理服务显存优化、想上长上下文场景、或者被 OOM 折腾过的团队参考。1. 先算明白KV Cache 到底在吃哪份显存1.1 显存去向不是玄学是公式KV Cache 的大小从来就不是模糊的“大概占用”它完全可以用公式精确表达。推理过程中每个 token 的 K 和 V 向量都要按层缓存下来供后续计算注意力时重复使用。所以显存占用可以拆成“每 token 每层多少”和“一共多少层、多少 token”两部分单 token 显存 2KV× 层数 × KV 头数 × 头维度 × 每元素字节数整个推理过程的 KV Cache 显存 batch 大小 × 当前序列长度 × 上面这个单 token 数值。拿一个典型的 7B 规模模型来算32 层、hidden size 4096、注意力 32 头、每头维度 128并且是标准的 MHA多头注意力结构。这里每层每个 token 的 K 和 V 加起来是 2×32×1288192 个元素FP16 下一个元素占 2 字节也就是一层 16KB。32 层堆下来单个 token 就要吃掉 512KB。这个数字看着不大乘上序列长度就不对了。输入 1024 个 token、输出 1024 个 token等到解码结束时 KV Cache 里实际缓存了 2048 个位置512KB×20481GB。你没看错一个 7B 模型光 KV Cache 就吃掉 1GB。如果把上下文撑到 32K这个数字直接变成 16GB。在 80GB 显存的卡上模型权重也许只占十来 GBKV Cache 却悄悄占了接近一半。1.2 为什么量化在这里特别划算量化本质上做的是减法FP16 是 2 字节INT8 是 1 字节INT4 是 0.5 字节。KV Cache 的显存直接跟字节数成正比所以降精度是立竿见影的线性收益不需要改任何算法也不用重新训练模型纯粹从工程层面就能把缓存压缩下来。更关键的是KV Cache 里存的 K 和 V 张量分布特性比普通激活值更适合量化。权重量化最怕遇到大量极端离群值而注意力计算里的 K/V 经过多层投影后整体数值往往集中在相对平稳的区间里尽管不同层、不同头之间会有差异但很少有那种“一个值顶一万个值”的极端情况。这也解释了为什么 KV Cache 量化能成为行业里普遍接受的优化手段——它不是靠运气而是靠分布特征。当然KV Cache 量化带来的好处不只是显存容量。解码阶段是典型的带宽瓶颈每次生成一个 token 都要读取全部 KV Cache 参与注意力计算。精度降到 INT4 后同一时间能从显存里读出来的有效数据翻倍这对吞吐量的提升甚至比显存容量的意义更直接。2. 量化方案怎么选INT8、INT4 与分组粒度把 FP16 直接切成 INT8 还是 INT4不是拍脑袋决定的。方案选型要看三件事误差能不能接受、scale 开销多少、kernel 好不好写。2.1 对称量化和非对称量化的选择对称量化是最常见的起点公式也干净先统计张量里的绝对最大值算出 scale然后每个元素都除以 scale 再四舍五入。整个过程只需要存一个 scale反量化时乘回去就行。非对称量化多了一个 zero point能表示偏移分布理论上更通用。但 KV Cache 量化里我自己的习惯是先用对称量化试水原因有两个。第一K/V 经过注意力投影后分布通常没有严重偏离零点对称量化已经能覆盖绝大多数情况第二少存一个 zero point读取和反量化的运算都更轻。真碰到个别模型 V 的分布有偏移、对称量化精度崩了再切到非对称方案不迟。有个工程细节值得注意对称量化只看绝对值最大值所以如果张量里出现一两个特别大的离群值整个量化的有效精度都会被拖垮。这种情况与其换非对称量化不如先检查分组粒度把离群值带来的影响限制在一个小分组里效果往往更好。2.2 per-token、per-channel、per-group 到底怎么定量化粒度解决的是“多少个元素共享一个 scale”的问题。粒度越粗scale 数量越少省得越多但误差越大粒度越细scale 越多误差越小但显存开销和计算开销都会涨。KV Cache 的张量形状一般是 batch × KV 头数 × 序列长度 × 头维度。per-token 是按序列位置做的实现简单但 token 之间数值差异不小per-channel 是沿头维度做一个通道一个 scale适合 head_dim 固定的场景。实际操作中我更推荐 per-group把相邻的若干元素分成一组共享同一个 scale。group_size 取多少主流实践中 32、64、128 都有人用。head_dim 是 128 时group64 算是一个甜点位置——每个头被分成两组误差控制得住scale 数量不会太离谱。如果对精度特别敏感降到 32如果显存抠得紧可以试 128但要接受潜在的长尾误差。2.3 K 和 V 要区别对待很多量化方案默认 K 和 V 采用相同精度但实际工程里我会把两个张量分开考虑。从不少团队的实践反馈来看V 对量化的敏感度往往高于 K。注意力计算中 V 直接参与加权求和误差会直接混进输出而 K 主要影响注意力分数的相对排序小扰动有时反而不明显。所以落地方案不只有“全 INT8”和“全 INT4”两个选项还有一个很实用的中间档K 做 INT4V 保持 INT8。这个混合方案算下来显存节省比例处在两者之间精度损失比全 INT4 小很多。以 7B 模型为例K 和 V 各占 KV Cache 的一半全 FP16 时是 1GBK 降到 INT4、V 降到 INT8 后总占用变成 0.375GB省了接近三分之二。混合精度的代价是实现复杂度代码里要同时维护两套 scale、两套反量化逻辑。不过现在主流的推理框架基本都对这种组合做了抽象配置好精度策略就行不用自己造轮子。3. 显存账本不同配置下到底省多少3.1 MHA 结构下的账本回到前面那个典型配置32 层、32 个 KV 头、head_dim 128、batch1、输入 1024 输出 1024KV 长度 2048。我把 FP16、INT8、INT4 的账一次算齐。FP16 下 KV Cache 是 1GB这个我们已经算过了。INT8 就是把字节数砍一半512MB。INT4 的纯数据是 256MB但别忘了 scale 也要占显存。以 group_size64 为例元素总数是 2048×32×2×32×128约 5.37 亿个除以 64 得到约 838 万个 group每个 group 存一个 FP32 的 scale占用 4 字节合计 32MB。所以 INT4 的实际总占用是 288MB而不是理想的 256MB。换成分组表格更清楚精度方案KV Cache 显存相比 FP16 节省FP161GB基线INT8512MB50%INT4group64288MB71%INT4group128272MB73%batch 从 1 涨到 4所有数字同步翻 4 倍FP16 是 4GBINT4 约 1.1GB。batch 越大的场景量化省出来的绝对值越可观这也是为什么服务端推理几乎都要开 KV Cache 量化的原因。3.2 GQA/MQA 结构下的账本现在很多新模型不再用 MHA而是用 GQA分组查询注意力甚至 MQA多查询注意力本质是多个 query 头共享少数 KV 头。这种结构本身就大幅压缩了 KV Cache。假设同样 32 层的模型KV 头数从 32 降到 4单 token 的 KV 元素从 8192 个降到 1024 个FP16 下每 token 每层 2KB32 层一共 64KB2048 长度的 KV Cache 只有 128MB。在这个基础上做量化省的比例依然在但 scale 的占比会更明显。INT4 纯数据 32MBgroup64 时需要约 104 万个 scale占 4MB总占用 36MB。所以 GQA 加量化不是简单“更省”而是 KV Cache 本身已经很小量化后可以称为“几乎可以忽略”。结构/方案KV 头数FP16INT8INT4scaleMHA321GB512MB288MBGQA4128MB64MB36MB这也是我常跟别人说的一句话换模型结构带来的显存收益往往比硬上量化还大。如果模型支持 GQA优先选 GQA在 GQA 的基础上再做量化属于锦上添花而不是雪中送炭。3.3 长上下文场景为什么是非省不可长上下文是 KV Cache 量化的主战场。同样是 7B 模型把上下文长度拉到 32KMHA 结构下 FP16 的 KV Cache 是 16GBINT8 是 8GBINT4 加 scale 约 4.5GB。在单卡 80GB 的环境下16GB 的缓存占掉五分之一如果 batch 再上去很快就把显存顶穿。所以很多长上下文产品默认就开 INT8 甚至 INT4 的 KV Cache不是出于“优化洁癖”而是不开根本跑不动。量化后的 KV Cache 能把 32K 上下文的显存占用从 16GB 拉到 4.5GB省出来的空间可以放更大的 batch 或者更长的上下文这对在线服务的吞吐量和成本控制是直接的。4. 省了显存精度账怎么还误差来源与评估方法4.1 量化误差从哪里来KV Cache 量化不是无损操作。误差主要来自两个地方一是超出量化范围的值被截断二是组内共享 scale 带来的近似误差。分布最值大但整体集中在零附近的张量对称量化表现好一旦出现离群值最大值对应的 scale 变大正常小数值的有效分辨率反而被牺牲了这就是误差的主要来源。更麻烦的是自回归场景下的累积效应。推理时 KV Cache 一旦量化后续每一步注意力计算都在拿有损的缓存做输入误差不是一次性消费完而是会随序列长度不断累积。短文本上测不出来一跑长文本可能出现事实细节丢失、检索不准确这类表现。我在实际调优时发现这种问题在“需要从长上下文里精确引用的任务”上尤其明显。另外如果在模型权重已经量化到 INT4 的基础上再做 KV Cache 量化两重误差叠加。权重误差影响每层输出KV 误差影响注意力加权它们不是简单相加而是在注意力分数经过 softmax 后相互放大。这种情况别指望哪一边能“覆盖”另一边的误差得单独验证。4.2 怎么在部署前评估“省得不亏”评估方法不能只看一两个指标。最基础的是困惑度对比同样的验证集、同样的解码种子测 FP16 基线、INT8、INT4 的困惑度差异。以一个常见的 7B 模型为例FP16 基线困惑度 9.81INT8 大概是 9.84INT4 用 group64 可能到 9.91把 group 降到 32 可以收回到 9.86 附近——这些数值具体到不同模型会有差异但趋势是一致的INT8 几乎无损INT4 需要靠分组粒度换精度。困惑度之外强烈建议加上长文本任务集验证。比如摘要式问答、文档检索这类需要模型从长上下文里捞细节的任务对 KV Cache 的量化误差非常敏感。阈值怎么定我习惯定一个“可接受退化线”任何任务指标下降不超过 1-2% 就可以放行核心任务有硬性要求时则要求无损等效。还有一个排查技巧逐层扫描敏感度。把模型某一层的 KV 量化、其他层保持 FP16跑一遍验证集重复若干次就能找出最不抗量化的层。敏感层单独用更大 group 或更高精度其余层继续低精度这样能兼顾显存和精度。这个操作看起来很繁琐但比盲目把整个模型统一压到 INT4 靠谱得多。5. 落地实操从方案选型到踩坑实录5.1 实现层面最容易踩的坑先说一个最常见的误区很多人以为量化 KV Cache 只要把存储类型从 FP16 换成 INT4显存就直接降四分之三结果上线后看显存曲线纹丝不动。问题往往出在读取路径上——如果每次注意力计算都先把 INT4 反量化回 FP16再临时存在另一个 buffer 里参与运算那省掉的存储空间又在一个临时张量上还回去了。正确的做法是让算子直接消费量化格式。K/V 从显存里读出来的是打包后的 INT4scale 同步读出在寄存器或缓存里完成反量化立刻参与矩阵运算而不是先落一块 FP16 的中间张量再计算。这一步做不好量化的意义就少了一半。另一个坑是 INT4 的打包与对齐。INT4 一个字节装两个元素内存布局通常是按 32 位甚至 64 位对齐打包的。读取时解包、跨线程边界处理都要小心。有些实现在短序列和小 batch 场景下反而比 FP16 慢就是因为解包和 scale 读取的开销没有摊薄。建议上线前先跑一个小 benchmark看看实际吞吐和延迟是否真的正向提升别只看显存数字。还要提一下 scale 的统计方式。如果每来一个新请求都动态统计 K/V 的最大值再计算 scale会有额外的读取和计算开销首次请求明显变慢。更稳的做法是用校准集离线统计数据分布得到每个 tensor 的 scale走静态或者半静态量化路线。动态量化适合对分布变化特别敏感的模型但对大部分部署场景静态校准已经足够。5.2 常见问题速查现象可能原因处理建议显存没有明显下降只量化了模型权重没有真正启用 KV Cache 量化路径用 profiler 按 tensor 维度看显存占用确认 KV Cache 的 dtype量化后首请求延迟变大动态统计 scale 带来额外开销改离线校准的静态 scaleINT4 精度明显比 INT8 差KV 分布不适合直接压到 4bit也可能是 V 更敏感换成 K 用 INT4、V 用 INT8 的混合方案或者把 group_size 从 64 降到 32group_size 调小后省显存变少scale 数量过多尤其在 GQA/MQA 下占比放大小 KV Cache 场景优先用 128 的 group_size或者放弃 INT4 直接用 INT8KV Cache 降下来了整体还是 OOM显存大头不只有 KV Cache激活值、临时缓冲也在占空间先做完整显存成分分析再决定下一步优化手段最后分享一个我自己习惯的推进顺序先不开任何量化把 KV Cache 的纯显存占用算出来确认这是当前瓶颈然后上 INT8因为这步几乎无风险INT8 跑稳之后再在评测集上试 INT4 的 group_size 64跑长文本任务看误差如果退化明显退回到 K 做 INT4、V 做 INT8 的组合。量化省出来的显存不是终点把它换成更大的 batch 或更长的上下文才是这笔账真正划算的地方。别小看 scale 那几十 MB 的开销在 KV Cache 已经被 GQA 压小之后scale 的占比会越来越碍眼算账的时候一定把它带上。