Gated MLA

MLA 和 Gated Attention 解决的是两个不同的问题:

Gated MLA 就是把后者的门控接到前者的输出上。压缩路径、KV cache 结构、MQA absorb 等 MLA 特性全部保持不变,只在注意力输出之后多一条门控支路。 所以它不是一种新的压缩方案,而是对 MLA 表达能力与训练稳定性的一次低成本补强。

公式

把 MLA 的压缩写开,并用 \(d_h\) 表示 QK 的 head 维度:

\[ c_{KV}=XW^{DKV},\qquad K=c_{KV}W^{UK},\qquad V=c_{KV}W^{UV} \]

在标准 MLA 输出之后加入门控:

\[ \tilde{\boldsymbol{O}} = \Big(\operatorname{softmax}\!\Big(\frac{\boldsymbol{Q}\boldsymbol{K}^{\mathsf T}}{\sqrt{d_h}}+M\Big)\boldsymbol{V}\Big) \odot \sigma\!\left(\boldsymbol{X}\boldsymbol{W}_g\right) \]

其中 \(\boldsymbol{W}_g\) 直接从当前层的 hidden state \(\boldsymbol{X}\in\mathbb{R}^{T\times d}\) 生成,与 Q/K/V 的四条投影支路并列,因此它的输入永远是当前的 \(X\),不依赖历史 token,也不进入 KV cache。

门控的形状与粒度

关键在 \(\boldsymbol{W}_g\) 的输出宽度:

\(\boldsymbol{W}_g\) 输出宽度 门控形状 粒度
\(H\) \([B,S,H]\) 每个 head 一个标量
\(H d_v\) \([B,S,H,d_v]\) 每个 head 的每个 value 通道一个门控
\(d\) \([B,S,d]\) 与 head 结构无关的通道级门控

现代 MLA 实现通常取 \(H d_v\),也就是让门控宽度与 o_proj 的输入宽度一致。这样做有两个直接好处:

  1. 门控可以 reshape 成 \([H,d_v]\) 后与注意力输出逐元素相乘,不需要广播,kernel 融合最简单;
  2. 粒度为 (head, value channel),比 head 级标量更细,同时保留了 head 之间的区分度。

以 Kimi K3 的 MLA 层为例,g_proj.weight 的形状是 \(12288\times 7168\),而 o_proj.weight 是 \(7168\times 12288\),两者恰好互为转置。\(12288=H d_v\) 正是全部 head 拼起来的注意力输出宽度,这说明 g_proj 产生的门控作用在 o_proj 之前、带 head 结构的 value 空间上,而不是 hidden 通道上。

门控加在哪里

同一个门控放在不同位置,语义和成本都不一样:

位置 门控结构 说明
\(W^{UV}\) 展开之后、o_proj 之前 \([B,S,H,d_v]\) 最常见,能区分 head,与 o_proj 输入对齐
o_proj 之后 \([B,S,d]\) 退化为 hidden 通道级,失去了 head 维度
直接乘在 latent \(c_{KV}\) 上 \([B,S,d_c]\) 同时改动所有 head 的 K 与 V,语义完全不同,属于另一类设计

需要特别强调的是 decode 阶段。MLA 在 decode 时走 MQA absorb 路径,先把 \(W^{UK}\) 吸收进 Q、把输出的 \(W^{UV}\) 展开推迟到最后:

\[ \Big(\operatorname{softmax}\!\Big(\frac{(c_qW^{UQ}W^{UK\,\mathsf T})c_{KV}^{\mathsf T}}{\sqrt{d_h}}\Big)c_{KV}\Big)W^{UV} \]

门控乘在这一步之后。它只依赖 \(X\),既不影响权重吸收的数学形式,也不影响 latent 的缓存方式,所以 MLA 的 decode 优化可以原样保留。

为什么 MLA 特别需要门控

除了 Gated Attention 的通用收益(缓解 softmax 凸组合带来的输出幅度约束、attention sink 与 massive activation),MLA 还有两个自身原因:

代价

门控不是“零成本的一行乘法”,它的开销与一张独立投影矩阵相当:

项目 代价
参数量 \(d\times Hd_v\)。K3 中为 \(12288\times 7168\approx 88\text{M}\),BF16 约 168 MB
Prefill 计算 每 token \(d\cdot Hd_v\) 次乘加,与一次 o_proj 的 GEMM 同量级
Decode 成本 每个 step 要完整读取这张权重,约 168 MB/层,是显存带宽开销而非 cache 开销
KV cache 完全不受影响,门控只依赖当前 hidden state
精度 sigmoid 需要在较高精度下计算,否则饱和区梯度会消失

因此在显存与通信预算里,g_proj 必须和 q_proj、o_proj 一样被当作主矩阵统计,不能因为它是“门控”就按小参数处理。推理实现上,门控通常被融合进 attention kernel 的 epilogue 或 o_proj 的前置步骤,避免额外的 HBM 往返。

与 KDA 的 g_proj 的区别

K3 的 KDA 层里同样有一张 \(12288\times7168\) 的 g_proj,名字和形状都一样,但含义完全不同:

归属 门控位置 门控作用对象 数学形式
MLA 层 注意力输出之后 单次 attention 的输出 \(\boldsymbol{O}\odot\sigma(XW_g)\)
KDA 层 递归状态更新中 历史状态 \(\boldsymbol{S}\) 的遗忘与新值写入 \(\alpha_t,\beta_t\) 作用在 \(\boldsymbol{S}_t=\alpha_t\boldsymbol{S}_{t-1}+\dots\)

读权重表或做显存分析时,这两者不能互相印证,也不能用 KDA 的 gate 语义去解释 MLA 的 g_proj。

最小实现

下面省略 RoPE、KV cache 与 MQA absorb,只保留 latent 压缩与输出门控的位置:

class GatedMLA(nn.Module):
    def __init__(self, d_model, n_heads, d_qk, d_v, d_c):
        super().__init__()
        self.n_heads, self.d_qk, self.d_v = n_heads, d_qk, d_v

        # MLA 的 latent 压缩:低秩 Q 与共享的 KV latent
        self.q_a_proj = nn.Linear(d_model, d_c, bias=False)
        self.q_b_proj = nn.Linear(d_c, n_heads * d_qk, bias=False)
        self.kv_a_proj = nn.Linear(d_model, d_c, bias=False)
        self.kv_b_proj = nn.Linear(d_c, n_heads * (d_qk + d_v), bias=False)

        # 门控宽度取 H * d_v,与 o_proj 的输入宽度一致
        self.g_proj = nn.Linear(d_model, n_heads * d_v, bias=False)
        self.o_proj = nn.Linear(n_heads * d_v, d_model, bias=False)

    def forward(self, x):
        B, S, _ = x.shape

        q = self.q_b_proj(self.q_a_proj(x)).view(B, S, self.n_heads, self.d_qk)
        kv = self.kv_b_proj(self.kv_a_proj(x)).view(
            B, S, self.n_heads, self.d_qk + self.d_v
        )
        k, v = kv.split([self.d_qk, self.d_v], dim=-1)
        q, k, v = (t.transpose(1, 2) for t in (q, k, v))  # [B, H, S, *]

        scores = (q @ k.transpose(-1, -2)) * self.d_qk ** -0.5
        out = scores.softmax(dim=-1) @ v  # [B, H, S, d_v],真实实现需先加 causal mask

        # [B, S, H, d_v],每个 head 的每个 value 通道单独门控
        gate = torch.sigmoid(self.g_proj(x)).view(B, S, self.n_heads, self.d_v)
        out = out.transpose(1, 2) * gate

        return self.o_proj(out.reshape(B, S, -1))

常见面试问题

小结

Gated MLA 是在 MLA 的注意力输出上乘一个由 hidden state 动态生成的门控,门控宽度通常取 \(H d_v\),与 o_proj 的输入对齐,作用位置在 \(W^{UV}\) 展开之后、o_proj 之前。它不改变 MLA 的压缩语义、不影响 KV cache 与权重吸收,代价是一张与 o_proj 同量级的主矩阵及其 decode 带宽,换来的是更细粒度的输出控制、对低秩 value 的补偿,以及与线性注意力层更对等的门控能力。