9 月 13 日凌晨,0xSean 在 X 上引了 @dongxi_nlp 一句话,问"那 MLA 把 KV 压缩之后,cache 的逻辑是不是又得重新理解一遍?"
短答,要。而且改动比"压缩"两个字深得多。MLA 改的不只是 cache 的大小,是 cache 存什么、怎么寻址、按什么粒度共享、误差由谁承担、kernel 怎么写。下面把这层窗户纸捅破,从标准 KV cache 的账算起,推到 MLA 的吸收恒等式,再回头回答这个问题。
自回归生成时,每步都要拿当前 query 去和全部历史 token 的 key、value 做点积,所以历史 K、V 必须留在显存里,这就是 KV cache。标准多头注意力(MHA)下,每个 token 每层要存
每 token 每层缓存量 = 2 × n_heads × d_head 个元素
2 是 K 和 V 两份。拿 DeepSeek-V2 这个量级的配置比划,128 个头、每头 128 维,每 token 每层要存 2×128×128 = 32768 个元素,bf16 下 64 KB。60 层模型就是每 token 约 3.8 MB。128K 上下文下来,光 KV cache 就要吃掉近 480 GB 显存。长上下文推理时,显存的大头常常就是这份 cache。
GQA 和 MQA 是第一代解法,让多个 query 头共享同一份 K、V。缓存按共享组数缩小,代价是表达能力跟着缩,头数砍得越狠质量掉得越多。这是一条用质量换显存的路。
MLA(Multi-head Latent Attention,DeepSeek-V2 首发)换了条思路。它不共享头,而是给 K、V 找一个公共的低秩"母体",只存母体。
输入 x 先被压进一个 512 维的潜向量,K 和 V 再分别从潜向量升维长出来
c_kv(t) = W_DKV · x(t) # 下投影,512 维,这是要缓存的东西 k(t) = W_UK · c_kv(t) # 上投影回 128 头 × 128 维 v(t) = W_UV · c_kv(t)
512 维的 c_kv 携带着重建全部 32768 维 K、V 所需的信息,因为 W_UK、W_UV 是训练学出来的固定映射。缓存的对象从"每个头各自的 K、V"换成"每个 token 一段 512 维摘要",外加一条后面要讲的位置便签。
关键一步。W_UK 是常量矩阵,注意力分数可以改写
score(i,j) = q(i)ᵀ · k(j)
= q(i)ᵀ · W_UK · c_kv(j)
= (W_UKᵀ · q(i))ᵀ · c_kv(j) # 把 W_UK 预乘进 query
于是打分直接在潜向量 c_kv 上做,k 从头到尾不用真的算出来。输出同理
o(i) = Σ_j a(i,j) · v(j)
= Σ_j a(i,j) · W_UV · c_kv(j)
= W_UV · ( Σ_j a(i,j) · c_kv(j) ) # 潜空间加权和,最后一次性升维
数字对一下账。每 token 每层缓存 512(潜向量)+ 64(位置便签)= 576 个元素,对照 MHA 的 32768,正好就是 DeepSeek-V2 论文摘要里那个 93.3% 的压缩率,同一篇摘要还给了 5.76 倍的最大生成吞吐和 42.5% 的训练成本节省。
吸收有个天敌,位置编码。RoPE 给 q 和 k 各乘一个随位置旋转的矩阵
score(i,j) = (R_i · q̃(i))ᵀ · (R_j · k̃(j)) = q̃(i)ᵀ · R_(j-i) · k̃(j)
如果把旋转矩阵塞进低秩路径,分数变成 (W_UKᵀ·q)ᵀ · R_j · c_kv(j)。R_j 站在缓存向量旁边且每个位置都不同,意味着每次算注意力都得把缓存的 c_kv 重新旋转一遍,吸收就废了,缓存的潜向量没法固定下来。
DeepSeek 的解法是给位置信息单开一条窄路。主路径彻底不带 RoPE,干干净净地被吸收;另造一个 64 维的共享 key
k_R(t) = RoPE( W_KR · x(t) ) # 64 维,所有头共用
score = (q_C)ᵀ · k_C + (q_R)ᵀ · k_R
位置信号全由这条 64 维小通道扛着,它就是 MLA 每个 token 必须交的"位置税",也是缓存里除潜向量外唯一多出来的东西。
回到推文的问题,逐条对。
MLA 没有消灭 KV cache,自回归就甩不掉历史。它把 cache 从"每个头各自保存的回忆"换成"每个 token 一段压缩摘要加一小条位置便签"。cache 的哲学没变,cache 的对象、代数和工程实现的确实得重新理解一遍。0xSean 这一问,问到点子上了。