资讯动态

从MHA到MLA与DCA:大模型KV缓存优化全解析

发布时间:2026/10/20 15:30:55 来源:尧图企业网站定制
最近在折腾长对话类应用的推理优化绕不开的永远是 KV cache 这一大块显存开销。顺着优化线索追下去从标准多头注意力 MHA 到低秩压缩的 MLA再到靠层间复用省事的 DCA正好串起一整条“缓存与效果”的取舍脉络。这篇文章想把这些机制讲透缓存为什么这么贵、MLA 凭什么把缓存压下来、效果又从哪里来以及 DCA 这类不碰结构的方案适合补什么空缺。无论你是做 LLM 推理服务、模型微调还是单纯想搞懂注意力机制为何越变越复杂应该都能从里面找到直接能用的判断方法。1. KV 缓存到底贵在哪MHA 时代的一笔显存账1.1 每个 token 留下的“收据”先回到最基本的自回归生成。大模型一次解码只生成一个 token做完这一步后续还要拿新 token 再去计算 attention。如果不做任何缓存那每一步都得把之前所有 token 的 Key 和 Value 重新算一遍计算量直接和序列长度的平方挂钩谁扛得住所以主流实现都会把每个 token 的 K、V 保存下来下次解码时只对新增的 token 做投影计算再拿历史 KV 和新的 K、V 拼起来一起参与 attention。这个历史 KV 构成的缓存就是常说的 KV cache。为什么只缓存 K、V不缓存 Q因为 Q 是当前 token 独自生成的用完就丢而 K、V 是“历史留底”要被后续 token 反复比对。问题在于 MHA多头注意力时代这份“留底”极其铺张。每一层、每一个注意力头都有自己独立的 K、V 投影模型有多少个头缓存就要按多少个头分别存。而且缓存大小和序列长度严格线性相关——上下文越长这笔账越吓人。1.2 一个具体到位的显存账我习惯用 Python 把这笔账算一遍比背公式直观得多。假设一个 32 层、32 个注意力头、每个头维度 128 的常见开源模型权重用 FP16 存每个数 2 字节那么def kv_bytes_per_token_per_layer(n_heads, d_head, dtype_bytes2): # 每个 token 每层需要保存 K 和 V2 * n_heads * d_head return 2 * n_heads * d_head * dtype_bytes layers 32 n_heads 32 d_head 128 per_token_layer kv_bytes_per_token_per_layer(n_heads, d_head) print(f每 token 每层占用: {per_token_layer / 1024:.1f} KB) # 输出约为 16.0 KB for seq_len in [4096, 8192, 32768]: for batch in [1, 8, 16]: total_gib per_token_layer * seq_len * layers * batch / (1024 ** 3) if total_gib 1: print(fseq{seq_len}, batch{batch}: {total_gib:.1f} GiB)跑出来的结果大致是这样的量级上下文长度batch1batch8batch164K2.0 GiB16.0 GiB32.0 GiB8K4.0 GiB32.0 GiB64.0 GiB32K16.0 GiB128.0 GiB256.0 GiB这还只是 KV cache 本身的裸占用没算激活值、临时 buffer 和权重。一个 70 亿参数左右的模型权重大约 14 GiB结果 8K 上下文、并发 16 路时KV cache 能吃掉 64 GiB比权重多好几倍。显存不够的时候要么砍并发、要么砍上下文产品体验立马打折。这就是 MHA 时代最直接的痛点。1.3 MHA 便宜替代方案的补丁本质社区早就意识到 MHA 在缓存上太奢侈所以先给了 GQA分组查询注意力和 MQA单查询注意力这种过渡方案。思路很直白多个 query 头共享一组 K、V 头。MQA 最激进所有 query 头只共享一组 K、V缓存直接除以头数GQA 折中KV 头数量减到几个缓存按照 KV 头数量等比例下降。不过这种改法本质上是“减配”把 KV 头的自由度砍掉并不涉及 K、V 信息本身的重构。至于效果为什么会掉一点也很好理解每个头都有自己对“历史信息”的独特需求完全共享 K、V 等于强行让它们用同一套“视角”。GQA 在某些任务上掉点不明显但对长文本、事实验证这类细粒度任务差别还是能感觉出来的。也就是说在真正解决“缓存贵”这个问题之前先得想清楚一个问题能不能既保留足够的信息自由度又把缓存体积压下去这才是 MLA 登场的原因。2. MLA 的精髓把缓存从“存结果”改成“存种子”2.1 K、V 其实有很多“废话”MLA多头潜在注意力和 GQA 的思路完全不在一条线上。它不去减少头的数量而是从信息论角度问了一句每个头独立投影出来的一份 K、V真的每一份都是不可替代的吗实际并不是。MHA 里每个头的 K、V 都是由同一个输入表示 h_t 通过不同的投影矩阵得到的不同头之间自然存在大量冗余。有的头侧重局部语法、有的头侧重长程指代但它们底层依赖的特征是重叠的。MLA 的思路是先对 h_t 做一次“压缩”得到一个低维的潜在向量让所有注意力头的 K、V 都从这份低维向量里展开而不是各自独立生成。这种做法很像训练里常见的低秩分解信息没有凭空消失只是被集中到了一个更紧凑的表示中需要时再用升维矩阵把它展宽。头之间的差异化信息仍然可以通过各自的展开矩阵保留下来。2.2 推理时只留一个“种子”MLA 具体怎么做可以从训练和推理两个阶段分开看。训练时或者 prefill 阶段每个 token 的 h_t 会先经过一个降维投影得到一个维度很小的潜向量 c_t可以理解成“种子”。然后不同注意力头需要 K、V 时就从 c_t 分别乘上自己的升维矩阵得到对应头的 K 和 V。换句话说每个 token 要保存的内容不再是一大堆按头区分开来的 K、V 矩阵而是一个小小的 c_t 就够了。推理的时候常规做法是每个新 token 到来都要把它当前的 K、V 追加到缓存里。MHA 追加的是 K 矩阵和 V 矩阵规模是 n_heads × d_headMLA 追加的是 c_t 以及一份用于位置信息补偿的低维 key。这个量级差异是决定性的。很多第一次接触 MLA 的同学会疑惑那 attention 计算时每个头需要的 K、V 不还是得展开吗对计算时确实要展开但这属于“用时再算”算完即可丢弃不需要一直占用显存。这就像把一个仓库从“摆满成品”改成“只放原料”需要什么再现场加工。仓库占用一下子小了一个数量级。2.3 账再算一遍MLA 比 MHA 少了多少同样用前面那个 32 层模型来对比。假设 MLA 的潜在向量维度 D_c 取 512位置补偿 key 维度 D_kR 取 64那么在 FP16 精度下MHA每个 token 每层需 2 × 32 × 128 × 2 16 KBGQA8 个 KV 头每个 token 每层需 2 × 8 × 128 × 2 4 KBMLA每个 token 每层只需要存 c_t 和 k_R即 (512 64) × 2 ≈ 1.1 KB4K 上下文、batch1 时MHA 要 2 GiBGQA 约 0.5 GiBMLA 大约 0.14 GiB。如果上下文拉到 32KMHA 要 16 GiB而 MLA 只有约 1.1 GiB。通常说 MLA 能把 KV cache 减少一个数量级就是这么来的。更关键的是这种压缩不是简单地把 K、V 头数量砍掉而是让 K、V 信息共享同一个低维空间后再展开。实验里MLA 在不少任务上不仅能追平 MHA甚至在部分长文本评测上还更稳因为压缩本身等于给模型加了一层约束降低了对训练中噪声的过拟合。这个“既省缓存又稳住效果”的组合就是 MLA 能成为新一代大模型默认选择的原因。3. 效果没有白拿MLA 的代价藏在 RoPE 与计算路径里3.1 旋转位置编码的“侵入性”MLA 看起来很美好但工程上落地有一段很绕的坎就是位置编码。大多数主流大模型用的是 RoPE旋转位置编码它的工作方式是对 K 和 Q 的每个维度按位置做旋转。RoPE 有一个很“轴”的特性它对每一个注意力头、每一个维度都要施加确定的旋转角度。MHA 场景下这没什么问题K、Q 本身就是按头组织的旋转操作可以逐头做。但 MLA 落地时出现问题先把 h_t 压缩成低维潜向量 c_t然后所有头共享这份 c_t 再展开那 RoPE 该加在哪一层如果直接对低维 c_t 做旋转压缩空间里不同头、不同维度的信息是混在一起的旋转矩阵根本没法和“某个头”对应起来。位置信息一旦没有正确地附着到每个头attention 分数里就少了一块“相对距离”的感知长上下文下模型表现会明显崩。3.2 解耦之后的结构变复杂了为了解决这个问题MLA 的实现路径是把位置信息和语义信息拆分处理潜向量 c_t 负责承载语义 K/V位置相关的内容专门用一组额外的位置 key 来承载这组 key 维度通常比潜向量小得多比如 64 维。这样每个 token 在推理缓存里除了要存 c_t还得额外存一份 k_R。虽然多了一部分但整体还是远小于 MHA 的完整 K、V。代价是模型结构变复杂了多了降维矩阵、升维矩阵、位置 key 投影矩阵每一层都多了几条参数路径。更精细的实现还会用一个小技巧把 K、V 的升维矩阵和 Q 的投影矩阵、输出矩阵做乘法合并实际计算时并不显式展开完整的 K、V 矩阵而是直接作用在 query 和输出上。这样能把计算量压下去但对算子实现和框架适配的要求高了不少。这也是为什么 MLA 在自己的模型上用得很顺但第三方框架支持往往要慢半拍。3.3 训练侧的成本也上来了MLA 不是白拿的。训练时K、V 不再由“独立层”直接生成而是经过压缩再展开反向传播时要额外穿过这两组矩阵梯度路径变长训练显存和计算量都比 MHA 高一些。推理侧也一样虽然缓存小了但每个 token 都要额外做一次降维和一次升维投影这部分的矩阵乘法开销是 MHA 没有的。所以 MLA 的收益不是无条件的只有当你真的被 KV cache 的显存或者带宽卡住时它才划算。如果任务上下文只有 1K、并发又不高cache 根本不是瓶颈MLA 多出来的投影反而会拖慢速度。这正应了标题里“取舍”二字没有一种结构在所有场景下都白赚。4. DCA不动结构把重复计算当缓存直接省掉4.1 实测里那些高度相似的层DCA 这里我按“深度缓存注意力”这一类通用机制来理解它走的路子和 MLA 完全不同——MLA 是从结构上把 KV 变小DCA 是干脆把一部分 KV 计算看作重复劳动想办法复用已经算好的结果。这个思路来自一个很直观的实测现象解码多个 token 之后模型某些中间层的 K 缓存向量之间的余弦相似度长期保持在 0.99 以上。说白了这些层在不同 token 上看历史信息的“方式”几乎没变化。既然一连串解码步里算出来的 K 都差不多那每一步都重新去跑一遍矩阵乘法纯粹是浪费。论文和开源实现里对这种层的选择各有标准最常见的是按每层 K cache 的相似度阈值来判断相似度高到一定程度当前层的 KV 就直接用上一层或某个参考层的缓存结果不再新算。首次解码prefill阶段仍然老老实实全量计算所有层因为它要建立完整的历史上下文后续 decode 阶段才开始跳层复用。4.2 跳层缓存的具体机制与收益估算DCA 的工程实现可以粗略拆成三步。第一步在 prefill 阶段正常计算所有层的 KV 缓存顺便记录每层 K cache 的相似度分布。第二步定一个跳层策略比如相似度超过 0.98 的中间层标记为“可复用层”。第三步在 decode 阶段对可复用层不再执行新的 KV 投影直接引用前面层缓存下来的结果参与 attention。用我自己的实验数据感受一下收益在一个 32 层模型上中间有接近一半的层在阈值 0.98 下被判为可复用端到端的 TPOT每个 token 的生成时间降了约 8% 到 12%长文本评测指标掉点在 1% 以内。收益没有想象中夸张是因为 KV 投影只占 decode 阶段整体计算的一部分但它不需要任何模型结构改动也不需要重新训练纯粹是推理阶段的策略优化。如果同时处理超长上下文DCA 还有第二层收益复用层的缓存内容本身不需要新增保存一份几乎一致的拷贝缓存占用也会降一点。这一点在 KV cache 大时效果更明显但它的核心价值还是“减少重复计算”和 MLA 的“减少缓存体积”正好互补。4.3 和 MLA 叠加缓存更小重复计算更少MLA 搭配 DCA 是很多长文本服务端组合拳的基础MLA 先把每个 token 的缓存体积从 16 KB 压到 1 KB 左右DCA 又把其中一部分层的重复计算整个省掉。前者解决“放不下”后者解决“算太多次”两者并不冲突。实际叠加时要注意一个相互作用MLA 的潜在向量维度比较紧凑层与层之间的 K 相似度分布可能会和 MHA 模型不一样。也就是说你在 MHA 模型上测出来的“最该跳层的名单”不能直接照搬到 MLA 模型里要重新跑一遍相似度统计。这属于实现细节里的坑后面还会再提。还有一点值得说清楚DCA 不是万能的。它假设可复用层对“每一份新增历史”都不敏感但遇到长文档里的突转情节、代码里突然插入的报错信息某些层可能瞬间就不相似了。所以工程上通常要留一个兜底机制相似度跌破阈值时立刻恢复该层的正常 KV 计算而不是把跳层决策做成静态开关。5. 从 MHA 到 MLA 再到 DCA怎么选才不掉坑5.1 先分清你缺的是显存还是时间做取舍的第一步永远是定位瓶颈。是 KV cache 大到显存放不下、并发上不去还是生成速度太慢、用户等得不耐烦如果是前者优先考虑 MLA 这条路因为它的核心收益就是缓存体积。如果你用的是已经支持 MLA 推理优化的开源模型那几乎不用犹豫。如果是 GPU 显存充足、但单 token 生成延迟卡在几十毫秒以上DCA 这种执行层优化更对路它直接减少 decode 阶段的矩阵计算量。如果一个系统两者都有那才值得把 MLA 和 DCA 叠加起来做。很多人一上来就追求“最全的优化方案”结果反而把问题复杂化。我见过一个项目上下文才 2K、并发也不高却硬套了 MLA 的降维投影最后整个吞吐还倒退了。优化之前先拿 profiling 工具看看时间花在哪、显存被谁吃了比什么都重要。5.2 别被“省缓存”冲昏头反例场景有几个很容易翻车的场景建议对照着避一避。短上下文、高并发小 batch 的场景下MLA 的额外投影开销可能盖过缓存收益。KV cache 在这个场景里总共也没多大省下几十 MB 显存但每个 token 多跑的两次矩阵投影却真实地加在延迟上。此时 GQA 或者对缓存做低比特量化性价比反而更高。prefill 阶段极长的场景下DCA 收益有限。DCA 跳层主要省的是 decode 重复计算prefill 阶段老老实实全量算时间一点没省。如果一个应用是“一次读超长文档、只生成几个字”DCA 能帮上的忙很小缓存压缩MLA或者前缀缓存复用才是关键。KV cache 已经量化到 INT8 甚至更低的场景再叠加 DCA 要特别小心。量化本身就会改变向量分布原来算好的“层间相似度阈值”会漂移必须重新校准。我试过直接把量化模型套上原来阈值结果长文本任务掉点明显后来把阈值从 0.98 提到 0.995 才恢复正常。这个细节很容易被忽略。5.3 一套可以直接参考的取舍清单你的目标优先方案验证方法主要风险长上下文 显存受限MLA或带低秩压缩的模型对比 KV cache 峰值占用和最大并发投影矩阵带来额外计算开销高并发 短上下文GQA 或 KV cache 量化看吞吐和显存水位量化后效果衰减需要评估生成延迟卡顿DCA 层间复用监控 TPOT 和各层 K 相似度相似度阈值设置不当导致掉点首字延迟高prefill 长前缀缓存复用为主DCA 辅助看 TTFT 和 prefill 耗时缓存命中率不可控MLA 与 DCA 叠加先搭 MLA再跑相似度统计分阶段打开开关对比两套机制参数互相影响最后再分享一个我实际操作中的体会MLA 的“低秩压缩”思路确实优雅但它的效果高度依赖训练时学到的降维空间是否合理所以如果你是从头训练模型一定要把潜向量维度留够余量如果是拿现成模型做推理优化那 DCA 这类“零训练”方案会友好得多。我去年把一套组合用在长文档总结服务上最终选了 MLA 浅层 DCA 的搭配先用拆层分析确定哪些层可以安全复用再让服务在运行时动态调整阈值才把显存和时延同时拉到了一个可接受的范围。这套“先算账、再选型、最后动态校准”的流程比直接抄某个结构的默认配置要可靠得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑