1. 从显存焦虑说起多模态推理的KV Cache到底卡在哪做多模态大模型推理优化的人大概率都经历过这样一个场景单张卡跑纯文本模型时batch size 开到 16 甚至 32 都还算从容可一旦把图像 token 接进来显存占用就像被人从背后推了一把——原本能跑 8 路并发的配置瞬间掉到 2 路甚至 1 路都勉强。更让人头疼的是吞吐量下降并不是线性的而是断崖式的。这个断崖的根源很大程度上就落在KV Cache上。先把概念捋清楚。自回归生成过程中每一层注意力都需要缓存历史 token 的 Key 和 Value 张量避免重复计算。对于纯文本模型序列长度 L、层数 N、隐藏维度 d、注意力头数 hKV Cache 的显存占用大致正比于2 × N × L × d这里把 K 和 V 合在一起算。这个量级在文本场景下已经不小但至少是可预期的。多模态模型把这个可预期性打破了。原因有三层第一层视觉 token 数量爆炸。一张 448×448 的图经过 ViT 的 patch 切分动辄产生 256 到 576 个 token高分辨率场景下上千也不稀奇。而一段文本 prompt 可能才几十个 token。视觉 token 在序列里占比经常超过 80%。第二层跨模态注意力结构复杂。统一多模态模型比如把视觉编码器和语言模型做深度耦合的架构里视觉 token 和文本 token 之间的注意力模式差异极大。视觉 token 内部存在大量空间冗余——相邻 patch 的特征高度相似而文本 token 的信息密度则高得多。用同一套压缩策略一刀切要么压不干净要么把关键信息压没了。第三层推理框架的缓存管理粒度粗。很多框架默认按整条序列分配 KV Cache视觉 token 和文本 token 混在一起无法差异化处理。结果就是视觉部分占了大量显存却贡献了很低的实际信息量。UniCache 这个系列要解决的正是这个矛盾。前两篇分别处理了统一多模态模型的基础缓存结构和跨模态对齐问题而这一篇——2/3 篇——聚焦的是最核心的一环KV Cache 的压缩策略。换句话说前面把缓存怎么组织讲清楚了这一篇要回答的是缓存怎么瘦身且瘦得不让模型变傻。这篇文章适合谁看如果你正在做多模态推理部署、显存优化、或者单纯对 KV Cache 压缩的工程实现感兴趣那接下来的内容应该能给你一些可以直接抄作业的思路。我会从压缩的必要性讲起拆解几种主流策略在多模态场景下的适配问题然后给出 UniCache 采用的组合方案最后附上实测中的坑和调参经验。提示本文讨论的所有压缩策略都基于公开的学术思路和工程实践具体数值为模拟场景下的经验值实际部署时需根据你的模型结构和硬件配置重新标定。2. 为什么直接套用文本 KV Cache 压缩会翻车在动手设计压缩策略之前有必要先搞清楚一个反直觉的事实文本领域那些成熟的 KV Cache 压缩方法直接搬到多模态模型上效果往往大打折扣甚至起反作用。这不是实现细节的问题而是多模态注意力的统计特性本身就不同。2.1 视觉 token 的注意力分布长什么样先看一个关键观察。在纯文本注意力中token 之间的注意力权重分布通常呈现局部集中 少数全局锚点的模式相邻 token 之间权重高同时少数关键 token比如句首、实体词会吸引大量注意力。基于这个特性像 H2OHeavy Hitter Oracle、StreamingLLM 这类方法通过保留重击者token 和注意力汇token 就能压得很干净。但视觉 token 的注意力分布是另一回事。视觉 patch 之间存在强烈的空间局部相关性一个 patch 和它上下左右的邻居特征几乎一样。这意味着注意力权重在视觉区域内部是弥散的——没有明显的重击者或者说重击者太多导致基于阈值筛选的方法要么保留一大堆冗余 token要么误删真正携带边缘、纹理信息的关键 patch。更麻烦的是跨模态注意力。文本 token 对视觉 token 的注意力往往集中在少数几个与当前语义相关的区域比如问图里的猫是什么颜色注意力会聚焦到猫所在的 patch 区域。而视觉 token 对文本 token 的注意力则相对均匀。这种不对称性意味着压缩视觉 KV 和压缩文本 KV必须用不同的判据。2.2 三种常见压缩思路的适配性对比目前主流的 KV Cache 压缩思路大致分三类我把它们在多模态场景下的表现整理成下表压缩思路代表方法核心判据多模态适配问题基于注意力分数筛选H2O、SnapKV累计注意力权重视觉区域权重弥散阈值难定易误删基于 token 合并CaM、MiniCache特征相似度视觉 token 相似度高合并收益大但易过度平滑基于低秩投影各类 PCA/线性投影主成分保留跨模态子空间差异大统一投影损失信息基于量化KV 量化INT8/INT4数值精度视觉 token 动态范围大低位宽量化误差明显这张表不是要否定某一种方法而是说明单一策略在多模态场景下都有明显短板。这也是 UniCache 选择组合策略的根本原因。2.3 一个容易忽略的细节位置编码的干扰还有一个坑很多人在做压缩时没注意到——位置编码。多模态模型里视觉 token 的位置编码方式和文本 token 往往不同有的是 2D 位置编码有的是可学习的位置嵌入。当你对 KV Cache 做压缩、改变了 token 的序列排布后位置编码和实际语义的对应关系可能被打乱。我实测过一个案例对视觉 token 做均匀下采样压缩后模型对图中左上角的物体这类空间指代问题的回答准确率掉了将近 30%。原因就是压缩破坏了 2D 位置结构的连续性。后来改成保留位置编码的稀疏采样按网格间隔抽取而非连续截断准确率才回升到可接受范围。注意做任何 KV Cache 压缩之前先确认你的压缩操作是否会破坏位置编码的语义一致性。这一点在文本场景下影响较小但在多模态场景下是致命的。3. UniCache 的压缩组合拳分层、分模态、分阶段搞清楚问题之后UniCache 的压缩策略设计思路就清晰了不能一刀切要分层、分模态、分阶段。下面拆开讲。3.1 分层策略浅层保细节深层做激进压缩Transformer 的不同层对 KV Cache 的敏感度是不一样的。浅层靠近输入主要负责提取底层特征视觉 token 的空间细节在这里最丰富压缩太狠会直接丢失边缘、纹理信息。深层靠近输出则更多承载语义抽象冗余度高可以压得更激进。UniCache 采用的分层压缩比大致是这样的以 32 层模型为例第 1-8 层压缩比 1.0不压缩保留完整视觉细节第 9-20 层压缩比 0.5对视觉 token 做相似度合并第 21-32 层压缩比 0.25对视觉和文本 token 统一做注意力筛选这个分配不是拍脑袋来的。我做过一组消融实验把压缩集中在浅层模型在细粒度视觉问答比如计数、颜色识别上掉点严重把压缩集中在深层则对整体语义理解影响很小。背后的逻辑是浅层的视觉特征还没被抽象每个 token 都携带独立信息深层的特征已经高度语义化冗余自然更大。3.2 分模态判据视觉看相似度文本看注意力前面说过视觉和文本的注意力分布特性不同所以压缩判据也要分开。对视觉 tokenUniCache 主要用特征相似度合并。具体做法是对同一层内的视觉 KV计算相邻 token 的余弦相似度超过阈值经验值 0.92 左右的就合并成一个Value 取加权平均。因为视觉 patch 的空间冗余高这个方法能压掉大量重复信息而且合并后的特征仍然平滑不会引入突变。对文本 token则用累计注意力筛选。保留那些累计注意力权重排名靠前的 token以及序列首尾的注意力汇token。文本 token 数量本来就少压缩收益不如视觉大但胜在精准不容易误伤。这里有个实操细节两种判据的压缩是独立进行的但压缩后的 KV 要重新对齐序列位置。我一开始图省事把视觉和文本压缩后的 KV 直接拼接结果注意力计算时位置错乱输出全是乱码。正确做法是维护一个压缩后位置映射表确保每个保留的 token 都知道自己在原序列中的位置。3.3 分阶段预填充阶段重压缩解码阶段轻压缩KV Cache 的生命周期分两个阶段预填充prefill和解码decode。预填充阶段整条 prompt包括所有视觉 token一次性过模型KV Cache 一次性写满。这个阶段是压缩的主战场——因为此时你掌握全部上下文可以做全局的相似度分析和注意力统计压缩决策质量最高。解码阶段每生成一个 token 就追加一份 KV。这个阶段如果还做复杂压缩会拖慢生成速度。UniCache 在这里只做轻量级的滑动窗口淘汰保留最近 N 个 token 的 KV加上预填充阶段筛选出的全局锚点token。这个分阶段设计的收益很明显预填充阶段把显存峰值压下来解码阶段保持低延迟。实测下来相比不做任何压缩的基线显存占用降低约 60%而端到端延迟只增加了不到 8%主要是预填充阶段的相似度计算开销。4. 把压缩策略落地关键参数与实现要点理论讲完了接下来是能直接上手操作的部分。这一节我会给出 UniCache 压缩策略的核心参数配置、实现伪代码以及几个必须注意的工程细节。4.1 核心参数配置表先上参数表这些是我在多轮实验中逐步收敛出来的经验值可以作为你的起点参数含义推荐值调整方向sim_threshold视觉 token 合并相似度阈值0.90-0.94调高保留更多细节调低压缩更狠attn_keep_ratio文本 token 注意力保留比例0.6-0.75任务越复杂保留比例越高shallow_layers不压缩的浅层数总层数 × 0.25视觉细节要求高时增大deep_compress_ratio深层压缩比0.25-0.35显存紧张时调低window_size解码阶段滑动窗口大小128-256长生成任务适当增大anchor_count全局锚点 token 数32-64多图任务适当增大这些值不是绝对的。比如你跑的是文档理解任务文字密集的扫描件视觉 token 的信息密度比自然图像高得多sim_threshold就要往上调否则会把文字笔画合并糊掉。4.2 视觉 token 相似度合并的实现逻辑核心逻辑用伪代码表示大概是这样def merge_visual_kv(kv_cache, sim_threshold): # kv_cache: [num_tokens, 2, head_dim] merged [] i 0 while i len(kv_cache): j i 1 # 向后寻找可合并的连续相似 token while j len(kv_cache): sim cosine_similarity(kv_cache[i], kv_cache[j]) if sim sim_threshold: break j 1 # 合并 i 到 j-1 的 token group kv_cache[i:j] merged_kv weighted_average(group) merged.append((merged_kv, i, j)) # 记录原始位置范围 i j return merged这里有几个实现要点第一相似度计算用 K 还是 V我的经验是用 K 判断用 V 合并。因为 Key 决定注意力权重相似度高的 Key 意味着这些 token 在注意力机制里可互换而 Value 承载实际信息合并时取加权平均能保留信息量。第二合并要记录位置范围。上面代码里的(merged_kv, i, j)就是位置映射。后续计算注意力时如果某个查询关注到合并后的 token需要知道它代表原始序列的哪一段。第三别用全局相似度矩阵。一开始我图省事算了整个视觉序列的两两相似度结果 O(n²) 的复杂度直接把预填充时间拉长了一倍。后来改成只在局部窗口内计算比如前后各 8 个 token复杂度降到 O(n×w)速度快了很多压缩效果几乎没损失。4.3 文本 token 注意力筛选的坑文本侧的注意力筛选看起来简单但有个坑我踩过注意力分数要在正确的层和头上统计。不同注意力头关注的东西不一样有的头关注语法结构有的头关注语义关联。如果你把所有头的注意力分数平均后再筛选会抹平这种差异导致保留的 token 偏向通用重要而非任务相关。UniCache 的做法是按头分组统计把注意力头分成若干组比如每 4 个头一组每组独立筛选最后取并集。这样能保证不同关注模式下的关键 token 都被保留。代价是保留的 token 数量会多一些但准确率明显更稳。另一个细节是注意力分数的归一化。原始注意力分数受序列长度影响很大长序列下分数普遍偏低。筛选前要先做 softmax 归一化否则阈值完全没法统一。4.4 压缩后的缓存重建压缩完不是就完事了还要把压缩后的 KV 重新组织成推理框架能用的格式。这一步容易出问题的地方在于缓存索引的一致性。推理框架通常用past_key_values这样的结构管理缓存内部按层、按头组织。你压缩了 token 维度后每一层的 token 数量可能都不一样因为各层压缩比不同。这时候如果框架还按统一长度去索引就会越界或错位。我的做法是为每一层单独维护一个长度数组推理时按层取对应的长度。听起来麻烦但这是保证正确性的必要代价。有些框架支持变长缓存那就省事很多选框架时可以留意这个特性。5. 实测数据与踩坑记录压缩到底能省多少又会掉多少点这一节不讲理论只讲实测。我会给出几组对比数据以及我在调参过程中踩过的具体坑。5.1 显存与延迟的收益测试环境是一台单卡推理服务器模型是模拟的统一多模态架构约 7B 参数32 层输入是一张 448×448 图像加一段约 50 token 的文本 prompt输出长度固定 128 token。配置显存峰值预填充延迟解码延迟端到端延迟无压缩基线18.2 GB210 ms12 ms/token1.75 s仅视觉压缩11.5 GB245 ms12 ms/token1.78 s仅文本压缩16.8 GB225 ms12 ms/token1.76 sUniCache 组合7.3 GB268 ms13 ms/token1.93 s可以看到组合策略把显存峰值从 18.2 GB 压到 7.3 GB降幅约 60%。代价是预填充延迟增加了约 58 ms相似度计算和注意力统计的开销端到端延迟增加约 10%。这个 trade-off 在显存受限的场景下非常划算——毕竟显存不够的话根本跑不起来。提示预填充延迟的增加主要来自相似度计算。如果你的场景对首 token 延迟极度敏感可以适当降低相似度计算的窗口大小或者只在部分层做合并。5.2 精度掉点哪些任务敏感哪些不敏感压缩必然带来信息损失关键是损失落在哪里。我测了几类典型任务图像描述生成BLEU 分数下降约 2%基本无感视觉问答简单事实准确率下降约 3%视觉问答细粒度计数/空间关系准确率下降约 8-12%文档 OCR 理解准确率下降约 5%文字密集时更明显纯文本任务几乎无影响因为文本压缩比例低结论很清晰压缩对语义级任务友好对细粒度感知任务不友好。如果你的应用场景是图里有什么这类粗粒度理解放心压如果是图里有几个红球、分别在什么位置这类精细任务压缩比要保守。5.3 我踩过的三个具体坑坑一相似度阈值设太低图像糊了。一开始我把sim_threshold设成 0.85想着压得越狠越好。结果模型对图像细节的描述变得极其笼统问它猫的胡须是什么颜色它回答猫是灰色的——胡须信息被合并没了。后来调到 0.92细节才回来。坑二解码阶段窗口太小长生成断片。滑动窗口设成 64 时生成超过 200 token 后模型开始重复前面说过的话明显是上下文丢失。调到 192 后正常。经验是窗口大小至少覆盖你期望的局部连贯段落长度。坑三多图输入时锚点不够用。单图任务下anchor_count32够用但输入 4 张图时每张图分到的锚点太少跨图推理能力下降。后来改成按图数量动态调整锚点数每张图至少保证 16 个锚点。5.4 一个提效小技巧缓存压缩的预热如果你的服务是常驻的可以在启动时用几条典型输入做一次预热压缩把相似度计算的中间结果比如视觉 token 的特征聚类中心缓存下来。后续请求可以直接复用这些聚类中心做快速合并判断省掉重复的相似度计算。我实测这个技巧能把预填充阶段的压缩开销降低约 40%。当然预热的输入要覆盖你的典型场景否则聚类中心偏了反而影响效果。这个技巧适合输入分布相对稳定的线上服务不适合输入千变万化的开放场景。6. 组合策略的边界什么时候该收手写到这里压缩策略的收益和代价都摆清楚了。最后想聊聊边界问题——不是所有场景都适合激进压缩有些情况下收手比硬压更明智。6.1 三类不建议深度压缩的场景第一类超高分辨率输入。当输入图像分辨率超过 1024×1024视觉 token 数量本身就极大此时压缩带来的显存收益很可观但信息损失也成倍放大。我的建议是这种情况下优先考虑降低输入分辨率或分块处理而不是硬压 KV Cache。因为分辨率降下来token 数量自然减少且信息损失是均匀的比压缩导致的选择性丢失更可控。第二类细粒度定位任务。比如目标检测、关键点识别这类需要精确定位的任务视觉 token 的空间位置信息至关重要。压缩合并会破坏位置精度导致定位偏移。这类任务建议只做浅层不压缩、深层轻度压缩的保守配置。第三类多轮对话中的视觉上下文。多轮对话里历史轮次的图像 KV 会一直留在缓存中。如果对历史图像做激进压缩后续轮次引用历史图像细节时就会失忆。我的做法是对当前轮图像正常压缩对历史轮图像做更保守的压缩或者干脆只保留其文本摘要对应的 KV。6.2 压缩策略的自适应调整思路固定参数永远不是最优解。UniCache 的思路是让压缩策略根据输入特征自适应统计视觉 token 的平均相似度相似度高图像平滑、冗余大就压狠一点相似度低图像纹理复杂就压轻一点统计文本 prompt 的长度和复杂度长 prompt 保留更多文本 token根据当前显存水位动态调整压缩比显存紧张时自动加码这个自适应逻辑实现起来不复杂核心就是几个 if-else 判断加动态阈值。但收益很明显在混合负载场景下自适应策略比固定策略的平均精度高约 4-5 个百分点同时显存峰值控制得更稳。6.3 后续可以继续挖的方向KV Cache 压缩这个方向远没到尽头。我个人比较看好的几个延伸点一是训练时的压缩感知让模型在训练阶段就适应压缩后的 KV 分布而不是推理时硬压二是跨请求的缓存共享多个请求如果包含相同图像可以共享压缩后的视觉 KV进一步省显存三是硬件感知的压缩针对特定加速器的内存带宽和计算特性定制压缩粒度。这些方向我还在陆续尝试有新的实测结果再单独开篇聊。就目前这套组合策略而言它在显存收益和精度保持之间找到了一个我认为足够实用的平衡点——不是理论最优但工程上跑得通、调得动、稳得住。如果你也在做类似的多模态推理优化希望这篇的踩坑记录能帮你少走点弯路。