AI Knowledge Tree

Multi-head latent attention (MLA)

Compress the KV cache by projecting keys and values through a shared low-rank latent, derive DeepSeek-V2's MLA, and contrast its memory and quality with GQA.

⏱ 30 minPrerequisites:GQA, MQA, and Mixture of Experts

Autoregressive decoding caches the key and value vectors of every past token so each new token attends without recomputing them. That cache is the dominant memory cost at long context. Grouped-query attention shrinks it by sharing keys and values across groups of heads. Multi-head latent attention (MLA), introduced in DeepSeek-V2, shrinks it a different way: it caches a single low-rank latent per token and reconstructs the per-head keys and values on the fly[1].

1. The cost being optimized

Let dd be the model width, nhn_h the number of heads, and dhd_h the per-head dimension (so standard multi-head attention has nhdh=dn_h d_h = d). For a sequence of length LL and nℓn_\ell layers, plain multi-head attention caches both KK and VV:

cacheMHA=2 nℓ L nh dhnumbers.\text{cache}_{\text{MHA}} = 2\, n_\ell\, L\, n_h\, d_h \quad\text{numbers}.

The factor nhdh=dn_h d_h = d per token per layer (times two for K and V) is what MLA targets.

2. Down-projection to a latent

MLA introduces a learned down-projection WDKV∈Rdc×dW^{DKV} \in \mathbb{R}^{d_c \times d} with a latent dimension dc≪nhdhd_c \ll n_h d_h. For the token representation ht∈Rdh_t \in \mathbb{R}^d it computes

ct=WDKVht∈Rdc.c_t = W^{DKV} h_t \in \mathbb{R}^{d_c}.

This vector ctc_t is the only thing cached. At attention time, two up-projections WUK,WUV∈Rnhdh×dcW^{UK}, W^{UV} \in \mathbb{R}^{n_h d_h \times d_c} reconstruct the per-head keys and values:

kt=WUKct,vt=WUVct.k_t = W^{UK} c_t, \qquad v_t = W^{UV} c_t.

Because ktk_t and vtv_t are linear in ctc_t, the up-projections can be folded into the query and output projections during inference, so the reconstruction is not paid as a separate matmul. The cache per token per layer drops from 2nhdh2 n_h d_h to dcd_c:

cacheMLA=nℓ L dc.\text{cache}_{\text{MLA}} = n_\ell\, L\, d_c.

With DeepSeek-V2's choice dcd_c around 4dh4 d_h, this is several times smaller than even a GQA cache, while staying closer to full multi-head quality because every head still gets its own reconstructed kt,vtk_t, v_t rather than a shared one.

The magnitudes make the saving concrete. At the V2-scale config below (d=5120d = 5120, nh=128n_h = 128 heads, dh=40d_h = 40, GQA with 8 KV groups, dc=512d_c = 512, dhR=64d^R_h = 64), the per-token-per-layer cache in raw numbers is:

  • MHA caches 2nhdh=2⋅128⋅40=102402 n_h d_h = 2 \cdot 128 \cdot 40 = 10240 numbers, the full keys and values for all 128 heads.
  • GQA shares them down to 8 KV groups: 2⋅8⋅40=6402 \cdot 8 \cdot 40 = 640 numbers, a 16x cut.
  • MLA caches the latent plus the decoupled key: dc+dhR=512+64=576d_c + d^R_h = 512 + 64 = 576 numbers, 17.8x below MHA and a hair under GQA's 640.

So MLA reaches GQA-level memory (576 vs 640) while every head still reconstructs its own distinct kt,vtk_t, v_t, where GQA forces eight heads to share one. Same footprint, more expressive cache.

3. The decoupled RoPE key

There is one obstruction. Rotary position embeddings rotate keys by a position-dependent matrix RtR_t, so the effective key is RtktR_t k_t. That rotation does not commute with the latent up-projection, which would block folding WUKW^{UK} away. MLA resolves it by decoupling position: it carries a small separate key ktR∈RdhRk^R_t \in \mathbb{R}^{d^R_h} that holds the rotary part, computed from its own projection and rotated directly. The score combines a content term from the latent and a positional term from the decoupled key:

score(qt,ks)=qtC⊤ksC⏟from cs+qtR⊤ Rt−s ksR⏟decoupled RoPE.\text{score}(q_t, k_s) = \underbrace{q^{C\top}_t k^C_s}_{\text{from } c_s} + \underbrace{q^{R\top}_t\, R_{t-s}\, k^R_s}_{\text{decoupled RoPE}}.

The cache then holds csc_s plus the small ksRk^R_s, still far below the multi-head baseline.

4. Memory comparison

The block below computes the per-token cache for plain MHA, GQA, and MLA at a typical configuration, showing the ratios.

python · numpy
import numpy as np

d_model = 5120
n_heads = 128
d_head = d_model // n_heads     # 40
n_kv_groups = 8                 # GQA: 8 KV heads shared across 128 query heads
d_c = 512                       # MLA latent
d_rope = 64                     # decoupled rotary key

# numbers cached per token per layer
mha = 2 * n_heads * d_head
gqa = 2 * n_kv_groups * d_head
mla = d_c + d_rope

print("per-token-per-layer cache (numbers):")
print("  MHA:", mha)
print("  GQA:", gqa)
print("  MLA:", mla)
print("MLA vs MHA:", round(mla / mha, 3))
print("MLA vs GQA:", round(mla / gqa, 3))

MLA lands well below the multi-head cache and competitive with GQA, while keeping per-head keys and values distinct.

Exercises

Exercise 1. At the V2-scale config (nh=128n_h = 128 heads, dh=40d_h = 40, GQA with nkv=8n_{kv} = 8 KV groups, MLA latent dc=512d_c = 512, decoupled key dhR=64d^R_h = 64), compute the per-token-per-layer cache in raw numbers for MHA, GQA, and MLA, and the MLA-vs-MHA reduction factor.

Solution

Step 1. MHA caches keys and values for all heads: 2nhdh=2⋅128⋅40=102402 n_h d_h = 2 \cdot 128 \cdot 40 = 10240 numbers.

Step 2. GQA shares them across 8 KV groups: 2nkvdh=2⋅8⋅40=6402 n_{kv} d_h = 2 \cdot 8 \cdot 40 = 640 numbers.

Step 3. MLA caches the latent plus the decoupled RoPE key: dc+dhR=512+64=576d_c + d^R_h = 512 + 64 = 576 numbers.

Step 4. The reduction over MHA is 10240/576≈17.8×10240 / 576 \approx 17.8\times, a hair below GQA's 640 while every head still reconstructs its own distinct kt,vtk_t, v_t rather than sharing one across eight heads.

Exercise 2. Rotary position embeddings block folding the up-projection WUKW^{UK} into the query projection. State the algebraic reason, and explain how the decoupled RoPE key restores the folding.

Solution

Step 1. Folding works because kt=WUKctk_t = W^{UK} c_t is linear in the cached latent, so qt⊤kt=qt⊤WUKct=(WUK⊤qt)⊤ctq_t^\top k_t = q_t^\top W^{UK} c_t = (W^{UK\top} q_t)^\top c_t: the up-projection moves onto the query and the score reads the latent directly.

Step 2. RoPE inserts a position-dependent rotation, making the effective key Rtkt=RtWUKctR_t k_t = R_t W^{UK} c_t. Because RtR_t sits between qt⊤q_t^\top and WUKW^{UK} and depends on position, it does not commute with WUKW^{UK}, so WUKW^{UK} can no longer be absorbed into a position-independent query projection.

Step 3. MLA splits the key into a content part (from the latent, no rotation, foldable) and a small decoupled part ktR∈RdhRk^R_t \in \mathbb{R}^{d^R_h} that carries the rotation and is cached separately. The score is qtC⊤ksC+qtR⊤Rt−sksRq^{C\top}_t k^C_s + q^{R\top}_t R_{t-s} k^R_s: the first term keeps the fold, the second handles position on a tiny vector, so only csc_s plus ksRk^R_s is cached.

Exercise 3 (code). Verify the folding identity that makes MLA cheap: with ct=WDKVhtc_t = W^{DKV} h_t and kt=WUKctk_t = W^{UK} c_t, the score qt⊤ktq_t^\top k_t equals (WUK⊤qt)⊤ct(W^{UK\top} q_t)^\top c_t, so caching the latent ctc_t suffices. Confirm the two scores match and that the latent is smaller than the reconstructed keys.

Solution
python · numpy
import numpy as np

rng = np.random.default_rng(0)
d = 16                       # model width
n_h, d_h = 4, 4              # 4 heads, per-head dim 4  -> n_h*d_h = 16
d_c = 6                      # latent dimension, smaller than n_h*d_h

W_DKV = rng.standard_normal((d_c, d))          # down-projection
W_UK = rng.standard_normal((n_h * d_h, d_c))   # up-projection to per-head keys

h_t = rng.standard_normal(d)
c_t = W_DKV @ h_t                              # the only thing cached
k_t = W_UK @ c_t                               # reconstructed keys

q = rng.standard_normal(n_h * d_h)
score_direct = q @ k_t                          # score against full keys
score_folded = (W_UK.T @ q) @ c_t               # up-proj folded into the query

assert np.isclose(score_direct, score_folded)   # same score
assert c_t.size < k_t.size                       # 6 cached vs 16 reconstructed
print("cache latent:", c_t.size, "vs per-head keys:", k_t.size)

The folded score reads the cached latent directly, so the per-head keys are reconstructed only implicitly, never stored, which is where the memory saving comes from.

The next architectural axes, sparsity and recurrence, are covered by mixture of experts and the state space models in pillar D.


References

  1. [1]DeepSeek-AI. (2024). "DeepSeek-V2: a strong, economical, and efficient mixture-of-experts language model (MLA)". arXiv:2405.04434↩