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.
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 be the model width, the number of heads, and the per-head dimension (so standard multi-head attention has ). For a sequence of length and layers, plain multi-head attention caches both and :
The factor 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 with a latent dimension . For the token representation it computes
This vector is the only thing cached. At attention time, two up-projections reconstruct the per-head keys and values:
Because and are linear in , 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 to :
With DeepSeek-V2's choice around , 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 rather than a shared one.
The magnitudes make the saving concrete. At the V2-scale config below (, heads, , GQA with 8 KV groups, , ), the per-token-per-layer cache in raw numbers is:
- MHA caches numbers, the full keys and values for all 128 heads.
- GQA shares them down to 8 KV groups: numbers, a 16x cut.
- MLA caches the latent plus the decoupled key: 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 , 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 , so the effective key is . That rotation does not commute with the latent up-projection, which would block folding away. MLA resolves it by decoupling position: it carries a small separate key 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:
The cache then holds plus the small , 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.
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 ( heads, , GQA with KV groups, MLA latent , decoupled key ), 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: numbers.
Step 2. GQA shares them across 8 KV groups: numbers.
Step 3. MLA caches the latent plus the decoupled RoPE key: numbers.
Step 4. The reduction over MHA is , a hair below GQA's 640 while every head still reconstructs its own distinct rather than sharing one across eight heads.
Exercise 2. Rotary position embeddings block folding the up-projection into the query projection. State the algebraic reason, and explain how the decoupled RoPE key restores the folding.
Solution
Step 1. Folding works because is linear in the cached latent, so : 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 . Because sits between and and depends on position, it does not commute with , so 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 that carries the rotation and is cached separately. The score is : the first term keeps the fold, the second handles position on a tiny vector, so only plus is cached.
Exercise 3 (code). Verify the folding identity that makes MLA cheap: with and , the score equals , so caching the latent suffices. Confirm the two scores match and that the latent is smaller than the reconstructed keys.
Solution
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]DeepSeek-AI. (2024). "DeepSeek-V2: a strong, economical, and efficient mixture-of-experts language model (MLA)". arXiv:2405.04434↩