1. 从一次显存告急说起为什么要盯上多模态KV Cache多模态大模型跑推理的时候显存占用往往比纯文本模型更让人头疼。文本模型里KV Cache 的大小基本只跟序列长度、层数、注意力头数、头维度这几个量挂钩算起来很线性。但一旦把图像、视频、音频这些模态塞进同一个上下文序列长度会瞬间膨胀——一张 448×448 的图经过视觉编码器切 patch动辄就是几百上千个 token一段几秒的视频token 数直接上万。这些视觉 token 和文本 token 一起进入解码器KV Cache 就跟着一起涨。我在一个多模态对话的模拟项目里就踩过这个坑。当时上下文里放了三张高分辨率图加一段长文本单次推理的 KV Cache 占用直接顶到了几十 GB 量级batch size 只能压到 1吞吐惨不忍睹。更麻烦的是多模态场景下 KV Cache 的“冗余”分布和纯文本完全不一样文本 token 的 KV 往往语义密度高、重复度低而视觉 token 里有大量背景 patch、相似 patch它们的 KV 高度冗余却和文本 token 混在同一个 cache 里被一视同仁地保存。这就是 UniCache 这类统一多模态 KV Cache 压缩策略要解决的核心问题。它不是一个单纯的“砍 token”技巧而是一套针对多模态混合上下文的统一压缩框架。我把它拆成三篇来写这是第二篇专门讲压缩策略本身——也就是“到底怎么压、压哪里、压多少、怎么保证不崩”。第一篇讲的是多模态 KV Cache 的构成与瓶颈分析第三篇会讲工程落地和评测。这一篇是整套方案里最硬核的部分适合已经在跑多模态推理、被显存和吞吐卡住、想动手改 cache 管理的同学。先说清楚这篇要覆盖什么统一的多模态 KV 表示、压缩的粒度选择、重要性评估、分层压缩策略、跨模态的预算分配以及压缩后如何做质量兜底。每一块我都会给出为什么这么设计、参数怎么算、实操里怎么调。看完你应该能自己搭一个可用的多模态 KV 压缩流程而不是只会调别人的开关。2. 统一多模态KV表示先想清楚“压的是什么”2.1 多模态KV Cache的构成拆解要压缩先得知道 cache 里到底存了什么。在标准的 decoder-only 多模态架构里KV Cache 是逐层、逐头保存的。对于第 $l$ 层第 $h$ 个注意力头缓存的是Key 张量形状[batch, num_heads, seq_len, head_dim]Value 张量形状同上这里的seq_len就是多模态拼接后的总序列长度。假设文本部分有 $T$ 个 token视觉部分有 $V$ 个 token音频部分有 $A$ 个 token那么 $seq_len T V A$再加上一些特殊分隔 token。显存占用近似为$$ \text{CacheSize} \approx 2 \times L \times H \times (TVA) \times D \times \text{dtype_bytes} $$其中 $L$ 是层数$H$ 是 KV 头数$D$ 是头维度系数 2 是因为 Key 和 Value 各存一份。以一个 7B 级别、$L32$、$H32$、$D128$ 的模型为例用 fp162 字节存储如果 $TVA 8192$那么单条样本的 KV Cache 就是$$ 2 \times 32 \times 32 \times 8192 \times 128 \times 2 \approx 4.3\ \text{GB} $$注意这只是单条样本。batch 一上去显存直接爆炸。而多模态场景里 $V$ 往往远大于 $T$视觉 token 成了 cache 占用的主要来源。所以压缩的第一直觉就是视觉 token 的 KV 是重点对象。2.2 为什么不能简单套用文本KV压缩方法纯文本的 KV 压缩已经有不少成熟思路比如基于注意力分数淘汰、基于滑动窗口保留近期 token、基于 token 合并merging等。这些方法直接搬到多模态上往往会出问题原因有几个。第一视觉 token 的注意力分布和文本不一样。文本里重要的往往是少数关键词注意力集中且稀疏视觉里注意力可能分散在整片区域上尤其是需要全局理解的任务比如“图里有几只猫”你把背景 patch 全砍了模型可能连物体边界都判断不准。第二视觉 token 之间存在强空间相关性。相邻 patch 的 KV 高度相似简单按注意力分数淘汰可能把一整片区域都误删导致局部信息缺失。而文本 token 之间这种空间相关性很弱。第三跨模态对齐 token 极其敏感。文本里指代图像的词比如“这个”“左边那个”它的注意力会强烈指向特定视觉区域。如果压缩时把这些对齐关系破坏掉模型就会“答非所问”。我实测过粗暴地对视觉 token 做 50% 淘汰在指代类问题上准确率能掉二十多个点。所以 UniCache 的核心思路不是“对视觉和文本用同一套规则”而是建立一个统一的表示和评估框架让不同模态的 token 在同一个重要性尺度下被比较再按模态特性分配压缩预算。2.3 统一表示的关键把异构KV映射到可比空间统一表示要解决的是“怎么让视觉 token 和文本 token 的重要性可以放在一起比”。直接比原始注意力分数是不公平的因为视觉 token 数量多注意力被稀释单 token 分数天然偏低文本 token 少分数天然偏高。一个可行的做法是做模态内归一化。对每个模态内部的注意力分数先做 z-score 或 softmax 归一化映射到 [0,1] 区间再跨模态比较。这样视觉 token 里相对重要的比如物体中心 patch就能和文本里的关键词处在同一量级。另一个维度是引入“跨模态注意力”作为额外权重。具体来说统计文本 token 对视觉 token 的注意力总量以及视觉 token 对文本 token 的注意力总量把这种跨模态交互强度作为重要性的一部分。一个视觉 patch 如果被多个文本 token 强烈关注它的重要性就应该被抬高哪怕它在视觉模态内部的注意力分数不算顶尖。我在实现时用的统一重要性分数大致是$$ s_i \alpha \cdot \tilde{a}_i \beta \cdot c_i \gamma \cdot r_i $$其中 $\tilde{a}_i$ 是模态内归一化注意力$c_i$ 是跨模态交互强度$r_i$ 是位置/新近度先验$\alpha,\beta,\gamma$ 是可调权重。这个公式不是唯一解但它把三个关键因素显式拆开了调参时心里有数。实测下来 $\alpha$ 取 0.5、$\beta$ 取 0.35、$\gamma$ 取 0.15 是个不错的起点。注意归一化一定要按模态分别做不要全局做。全局归一化会让文本 token 系统性压过视觉 token压缩后模型会“只读文本不看图”。3. 压缩粒度选择token级、头级还是层级的取舍3.1 三种压缩粒度的对比压缩粒度决定了你在哪个维度上做裁剪或合并。常见的有三种各有适用场景。粒度操作对象压缩率上限实现复杂度对多模态的友好度Token 级序列维度上的 KV 条目高可到 70%中需处理跨模态对齐头级注意力头的 KV中30%-50%低较友好头功能分化明显层级整层的 KV低10%-30%低风险高易伤深层语义Token 级压缩最直观就是决定哪些位置的 KV 保留、哪些丢弃或合并。它的压缩率最高但也是最容易出问题的因为直接改变了序列结构。头级压缩利用的是注意力头的功能分化。多模态模型里有些头专门处理视觉、有些专门处理文本、有些负责跨模态对齐。把冗余的头比如多个头学到相似模式合并或裁剪对整体影响较小。这个粒度实现简单风险可控。层级压缩最激进直接砍掉某些层的 KV。但多模态任务对深层语义依赖强砍层往往导致生成质量断崖式下跌。我一般只在极端显存受限时考虑而且只砍最浅或最深的少数层。3.2 为什么UniCache主推token级头级混合单用 token 级压缩率高但风险大单用头级安全但压缩率不够。UniCache 的做法是混合先做头级筛选把明显冗余的头合并掉降低 KV 的“宽度”再在剩下的头上做 token 级压缩降低“长度”。两个维度叠加压缩率能到 60%-75%同时质量损失可控。具体顺序很重要。我试过先 token 后头效果不如先头后 token。原因是头级合并会改变每个 token 的 KV 表示如果先做 token 淘汰淘汰依据注意力分数是基于原始头的合并后这个依据就失真了。先做头级合并再基于合并后的表示算 token 重要性评估更准。头级合并的具体做法对同一层内的 KV 头做聚类把余弦相似度高于阈值的头归为一组组内 KV 做加权平均合并成一个头。阈值一般取 0.85-0.92。太低会误合并功能不同的头太高压缩效果不明显。这个阈值需要按模型调不同模型的头冗余度差别很大。3.3 粒度选择的实操判断标准怎么决定用哪种粒度我给几个实操判断标准。如果你的瓶颈是显存、且能接受一定质量损失优先 token 级 头级混合。如果瓶颈是延迟、显存还够优先头级因为头级合并对序列结构无影响推理时 kernel 更友好。如果模型本身头数就少比如 GQA 架构KV 头只有 4-8 个头级压缩空间有限重点放 token 级。反过来如果模型是标准 MHA、KV 头几十个头级压缩收益很大。还有一个经验视觉 token 占比越高token 级压缩的收益越大但风险也越大。当视觉 token 超过总序列的 70% 时我建议 token 级压缩率不要超过 50%剩下的压缩额度靠头级补。4. 重要性评估怎么判断哪些KV该留4.1 注意力分数不是唯一信号很多人做 KV 压缩第一反应就是看注意力分数分数低的淘汰。这在纯文本上勉强能用在多模态上不够。原因前面说过视觉 token 注意力被稀释单看分数会误杀。我在模拟项目里做过对比实验只用注意力分数做评估在图像描述任务上压缩 50% 后 BLEU 掉了 8 个点加入跨模态交互和位置先验后同样压缩率只掉 2 个点。差距很明显。所以重要性评估要多信号融合。除了注意力还要看这个 token 的 KV 是否被后续层反复读取层间一致性、它的 Value 是否和邻近 token 高度相似冗余度、它是否处在跨模态对齐的关键位置。4.2 多信号融合的评分公式与参数我把评分拆成四个分量每个分量归一化后加权求和注意力分量$a_i$该 token 收到的累计注意力模态内归一化。跨模态分量$c_i$该 token 与另一模态 token 的交互强度。冗余分量$d_i$与邻近 token 的 KV 相似度越相似说明越可压缩作为负权重。新近度分量$r_i$位置越靠后权重越高因为近期 token 对生成影响大。综合分数$$ s_i w_a \cdot a_i w_c \cdot c_i - w_d \cdot d_i w_r \cdot r_i $$权重建议起点$w_a0.4$$w_c0.3$$w_d0.2$$w_r0.1$。冗余分量取负是因为越冗余越应该被压掉。这里有个细节$d_i$ 的计算要用滑动窗口只和前后各 $k$ 个 token 比相似度$k$ 取 4-8。窗口太大会把远处不相关 token 也算进来冗余度估计失真。4.3 分层评估浅层和深层用不同策略多模态模型的浅层和深层KV 的角色不一样。浅层更多是低级特征边缘、纹理、局部模式冗余度高可以激进压缩深层是高级语义和跨模态融合冗余度低要保守。我的做法是分层设置压缩率。假设总压缩目标是 60%浅层前 1/3 层压 75%中层中间 1/3压 60%深层后 1/3压 45%。这样整体达标又保护了深层语义。评估信号也要分层调权重。浅层加大冗余分量权重$w_d$ 提到 0.3因为浅层冗余确实多深层加大注意力分量权重$w_a$ 提到 0.5因为深层注意力更能反映语义重要性。实操心得分层压缩率不要设成硬阶梯用平滑曲线过渡更好。硬阶梯会在层边界处造成表示突变生成时可能出现不连贯。我一般用余弦退火式的平滑曲线从浅层的高压缩率平滑过渡到深层的低压缩率。5. 跨模态预算分配视觉和文本各压多少5.1 预算分配的核心矛盾压缩总预算是有限的视觉和文本怎么分是个核心矛盾。视觉 token 多、冗余高理应多压但视觉信息又是多模态任务的关键压狠了模型就“瞎”了。文本 token 少、冗余低压多了直接伤语义。我见过两种极端做法都不好。一种是按 token 数量比例分配压缩量视觉 token 多就多压结果视觉信息损失过大另一种是平均分配压缩率视觉文本各压 50%结果文本被过度压缩语义崩了。合理的做法是按“信息密度”分配。视觉 token 信息密度低大量冗余可以承受更高压缩率文本 token 信息密度高压缩率要低。但具体数值要靠实验定。5.2 基于信息密度的动态分配方法我用的方法分三步。第一步估计每个模态的信息密度。用一个简单代理指标模态内 KV 的平均成对相似度。相似度越高说明冗余越多信息密度越低。视觉模态这个值通常明显高于文本。第二步按信息密度反比分配压缩率。设视觉信息密度为 $\rho_v$文本为 $\rho_t$总压缩率为 $R$则$$ R_v R \cdot \frac{\rho_t}{\rho_v \rho_t} \cdot k, \quad R_t R \cdot \frac{\rho_v}{\rho_v \rho_t} \cdot k $$其中 $k$ 是归一化系数保证加权平均后等于 $R$。这个公式让信息密度低的模态承担更高压缩率。第三步设上下限保护。视觉压缩率上限 80%文本压缩率上限 40%。超过上限就截断把多余额度转给另一模态。这个保护很重要防止某一模态被压崩。实测下来视觉压 65%-75%、文本压 20%-30% 是个比较稳的区间整体压缩率能到 55%-65%。5.3 对齐token的特殊保护跨模态对齐 token 要单独保护。这些 token 包括文本里的指代词、视觉里的物体中心 patch、以及模态分隔符附近的 token。它们的压缩率要额外降低我一般给它们打 0.5 的压缩折扣也就是别人压 60%它们只压 30%。识别对齐 token 的方法看跨模态注意力。文本 token 如果对某片视觉区域注意力集中它就是对齐 token视觉 patch 如果被多个文本 token 关注也是对齐 token。用一个阈值筛比如跨模态注意力占比超过 30% 就标记为对齐 token。这部分 token 数量通常不多占总序列 5%-15%但保护它们对质量影响很大。我在指代任务上测过保护对齐 token 后压缩 60% 的准确率损失从 15 个点降到 4 个点。6. 压缩执行合并、淘汰还是量化6.1 三种压缩操作的适用场景确定保留哪些 KV 之后还要决定怎么处理被压的部分。三种操作淘汰直接丢、合并多个合成一个、量化降低精度。淘汰最简单但信息直接丢失。合并保留信息但引入混合表示可能模糊细节。量化不改变结构只降精度对 kernel 友好但压缩率有限。我的组合策略是高冗余 token 用合并低冗余但低重要性的用淘汰保留下来的 KV 统一做低比特量化。三层叠加压缩率最大化。具体来说冗余度 $d_i$ 高于 0.8 的 token 走合并把一组相似 token 的 KV 加权平均成一个$d_i$ 在 0.5-0.8 且重要性分数低于阈值的走淘汰剩下的保留KV 量化到 int8 甚至 int4。6.2 合并操作的实现细节合并不是简单平均。直接平均会让合并后的 KV 偏离原分布注意力计算时可能失配。我用的加权平均权重是该 token 的重要性分数归一化后的值。重要的 token 在合并结果里占比更大。合并还有个坑Key 和 Value 的合并策略要一致否则 K 和 V 的对应关系错乱。我一开始只合并了 K 没合并 V结果注意力分数算出来完全不对排查了半天才发现。合并后要重新归一化。因为加权平均后向量的模长会变不归一化的话后续注意力点积的尺度会漂移。用 L2 归一化把合并后的 KV 拉回原尺度。6.3 量化与压缩的叠加量化是压缩的最后一环也是性价比很高的一环。KV 从 fp16 量化到 int8显存直接减半质量损失通常很小1 个点以内。量化到 int4显存减到 1/4但质量损失开始明显需要配合更保守的 token 压缩率。量化的关键是 scale 的选取。per-token 量化比 per-tensor 量化精度高但开销大。我一般用 per-head 量化在精度和开销之间取平衡。scale 用该头内 KV 的最大绝对值除以量化范围。注意量化和 token 压缩叠加时要先做 token 压缩再量化。反过来先量化token 重要性评估会受量化噪声影响评估不准。7. 质量兜底压缩后怎么保证不崩7.1 压缩质量的自检指标压缩完不能直接上线要自检。我盯三个指标注意力分布偏移、生成困惑度、任务准确率。注意力分布偏移压缩前后同一批输入的最后几层注意力分布做 KL 散度。KL 超过 0.1 就说明压缩过头了要回调压缩率。生成困惑度在验证集上算困惑度压缩后涨幅超过 5% 就要警惕。任务准确率在指代、描述、问答三类任务上各测一批任何一类掉超过 5 个点都要回调。这三个指标里注意力分布偏移最灵敏能提前预警。我一般先用它做快速筛查再用后两个做确认。7.2 动态回退机制压缩率不该是固定的要根据输入动态调。简单输入单图短文本可以激进压缩复杂输入多图长文本、密集指代要保守。我实现了一个动态回退推理前先估计输入复杂度用视觉 token 数、文本 token 数、跨模态对齐 token 比例三个量算一个复杂度分数。分数高就自动降低压缩率分数低就提高。复杂度分数公式$$ \text{complexity} \log(V) \log(T) \lambda \cdot \frac{N_{align}}{TV} $$$\lambda$ 取 2.0 左右因为对齐 token 比例对复杂度影响大。复杂度分数映射到压缩率区间 [0.4, 0.75]分数越高压缩率越低。这个机制让平均压缩率比固定策略低 3-5 个点但质量稳定性好很多极端输入的崩坏率大幅下降。7.3 常见崩坏模式与修复压缩后常见的崩坏模式有三种我都遇到过。第一种是“视觉失明”模型回答完全忽略图像内容。原因通常是视觉 token 压太狠或者对齐 token 被误删。修复降低视觉压缩率提高对齐 token 保护折扣。第二种是“语义漂移”回答和问题相关但细节错乱。原因多是深层 KV 被过度压缩。修复降低深层压缩率或对深层禁用 token 淘汰只做量化。第三种是“重复生成”模型陷入重复循环。原因常是合并操作破坏了 KV 的多样性注意力塌缩。修复降低合并阈值减少合并比例或对合并后的 KV 加一点噪声扰动。这三种崩坏我做了个速查表排查时按表走能快速定位。崩坏模式典型表现最可能原因首选修复视觉失明忽略图像视觉压缩过度/对齐token丢失降视觉压缩率加对齐保护语义漂移细节错乱深层KV过度压缩降深层压缩率深层禁淘汰重复生成循环输出合并破坏多样性降合并阈值加噪声扰动8. 参数调优从默认配置到场景适配8.1 默认配置与调参顺序给一套我常用的默认配置适合大多数多模态推理场景起步头级合并阈值0.88分层压缩率浅 0.75 / 中 0.60 / 深 0.45重要性权重$w_a0.4, w_c0.3, w_d0.2, w_r0.1$视觉/文本压缩率0.70 / 0.25对齐 token 折扣0.5量化int8per-head调参顺序很重要。先调分层压缩率影响最大再调视觉/文本分配再调重要性权重最后调头级阈值。每调一个用自检指标验证别一次调多个否则出了问题不知道是哪个引起的。8.2 不同场景的参数偏移场景不同参数要偏。我列几个典型场景的偏移方向。长视频理解视觉 token 极多视觉压缩率可以提到 0.75-0.8但要对时间维度做特殊处理相邻帧的相似 patch 优先合并。文本压缩率保持 0.25 不变。密集文档问答文本 token 多且信息密度高文本压缩率要降到 0.15-0.2视觉压缩率可以到 0.7。对齐 token 保护要加强折扣降到 0.4。实时对话延迟敏感头级压缩优先token 级压缩保守。头级阈值可以降到 0.85 多合并一些token 压缩率整体降 5 个点。8.3 调参的常见误区第一个误区是追求极限压缩率。我见过有人把压缩率拉到 85%跑分看着还行实际用起来各种崩。压缩率不是越高越好找到质量和显存的平衡点才是目标。我的经验是多模态场景下 60%-70% 是甜点区超过 75% 风险陡增。第二个误区是忽略 batch 维度。压缩策略在小 batch 上调好大 batch 上可能不一样因为 batch 内不同样本的复杂度不同动态回退会互相影响。我建议调参时用实际部署的 batch size别用 1。第三个误区是不做回归测试。每次改参数都要在固定测试集上跑一遍记录指标。我建了个简单的回归表每次调参后填一行时间长了就能看出哪些参数敏感、哪些不敏感。9. 我在实操中踩过的几个坑第一个坑是 KV 头合并后忘了同步更新注意力 mask。合并头之后头的数量变了但 mask 还是按旧头数生成的导致注意力计算维度不匹配直接报错。这个坑排查起来很费劲因为报错信息指向的是矩阵乘法维度不直接指向 mask。后来我养成了习惯任何改变 KV 结构的操作都先检查 mask 和相关索引。第二个坑是动态回退的复杂度估计用了压缩前的 token 数但压缩后 token 数变了导致回退逻辑自相矛盾。修复方法是复杂度估计必须在压缩决策之前完成用原始输入算算完再决定压缩率。第三个坑是量化 scale 用了全局最大值结果某些头的 KV 动态范围小量化后精度损失大。改成 per-head 后好了很多。这个坑的教训是KV 不同头的数值分布差异很大别用全局统计量。第四个坑是分层压缩率的平滑曲线用了线性插值但线性插值在层数少的时候过渡不够平滑。改成余弦插值后生成连贯性有改善。层数越少插值函数的选择越重要。这些坑都不大但每个都能让你卡半天。写出来是希望你别重复踩。10. 一个可复现的最小实现骨架最后给一个最小实现骨架帮你快速搭起来。伪代码为主重点是流程和关键参数。def compress_kv_cache(kv_cache, config): # 1. 头级合并 kv_cache merge_heads(kv_cache, thresholdconfig.head_threshold) # 2. 计算重要性分数 scores compute_importance( kv_cache, weightsconfig.importance_weights, windowconfig.similarity_window ) # 3. 分层确定压缩率 layer_rates cosine_schedule( config.shallow_rate, config.deep_rate, num_layerskv_cache.num_layers ) # 4. 跨模态预算分配 modal_budget allocate_budget( kv_cache, total_rateconfig.total_rate, visual_capconfig.visual_cap, text_capconfig.text_cap ) # 5. 对齐token保护 scores apply_alignment_protection( scores, kv_cache, discountconfig.align_discount ) # 6. 执行压缩合并淘汰 kv_cache execute_compression( kv_cache, scores, layer_rates, modal_budget, merge_thresholdconfig.merge_threshold ) # 7. 量化 kv_cache quantize(kv_cache, bitsconfig.quant_bits, granularityper_head) return kv_cache这个骨架里每一步的参数都在前面章节讲过。你可以先按默认配置跑通再逐项调优。跑通之后用第 7 节的自检指标验证用第 8 节的调参顺序优化。我个人在实际操作中的体会是多模态 KV 压缩最难的从来不是某个单点技术而是各环节的配合。头级和 token 级的顺序、压缩和量化的叠加、静态策略和动态回退的衔接这些配合关系决定了最终效果。单看每个环节都不复杂但组合起来坑很多。建议你先把流程跑通再逐个环节深入别一上来就追求最优参数。先把 60% 压缩率稳定跑住再往 70% 冲这样每一步都有退路。