Compresses K/V into a 512-dim latent per token (cached) and rebuilds 128 heads from it — ~10× smaller KV cache than MHA.
MLA attacks the KV-cache bottleneck differently from GQA: instead of sharing heads,
it compresses them. Keys and values are down-projected to one c_kv=512
latent per token; that latent (plus a 64-dim RoPE key) is all the cache
stores. At attention time, per-head K and V are re-expanded from the latent — or,
with the standard inference optimization, the up-projections are absorbed into
the query/output projections so attention runs directly in latent space.
Why the pieces exist:
- Low-rank Q (r=1536): queries are also bottlenecked — saves parameters
and, in DeepSeek's ablations, slightly helps quality.
- Decoupled RoPE (64 dims): RoPE's rotation doesn't commute with the
down-projection, so position rides on a small separate component that skips
compression (see the RoPE box).
- Output projection maps the 128 concatenated value heads back to d=7168.
class MLA(nn.Module):
def __init__(self, d_model=7168, n_heads=128,
q_rank=1536, kv_rank=512,
d_nope=128, d_rope=64, d_v=128):
super().__init__()
self.q_down = nn.Linear(d_model, q_rank, bias=False)
self.q_up = nn.Linear(q_rank, n_heads * (d_nope + d_rope), bias=False)
self.kv_down = nn.Linear(d_model, kv_rank + d_rope, bias=False)
self.kv_up = nn.Linear(kv_rank, n_heads * (d_nope + d_v), bias=False)
def forward(self, x, pos, cache):
q = self.q_up(rmsnorm(self.q_down(x))) # low-rank queries
ckv, k_rope = self.kv_down(x).split([512, 64], -1)
cache.append(rmsnorm(ckv), rope(k_rope, pos)) # ← the entire KV cache
k, v = self.kv_up(cache.ckv).split_heads() # re-expand (or absorb)
return attend(q, cache) # softmax(qk/√d)·v → o_proj