Efficient Attention: MHA → MQA → GQA → MLA
Prerequisites: modules 04, 08. You will learn: why the KV cache dominates inference memory, and the three mechanisms — MQA, GQA, MLA — invented to shrink it, with the arithmetic.
9.1 The KV cache, and why it changes everything
Module 08 ended on a waste: at each decode step the model recomputes K and V for every previous token, identically. The fix is obvious once seen — cache them.
# naive: recompute everything, every step
for step in range(max_new):
K = W_k(all_tokens_so_far) # recomputes positions 0..t-1 AGAIN
V = W_v(all_tokens_so_far)
# cached: compute only the new token, append
for step in range(max_new):
k_new = W_k(newest_token) # ONE token
v_new = W_v(newest_token)
K = torch.cat([K_cache, k_new], dim=-2)
V = torch.cat([V_cache, v_new], dim=-2)Cost per step drops from O(T) to O(1) in projection work. Every serving stack
does this.
But it converts a compute problem into a memory problem, and the memory problem is worse.
The cache-size formula
| Term | Meaning |
|---|---|
2 |
one tensor for K, one for V |
B |
batch size (concurrent requests) |
T |
sequence length |
L |
number of layers |
H_kv |
number of key/value heads ← the lever |
d_head |
per-head dimension |
| bytes | 2 for bf16/fp16 |
Holding other model dimensions and the workload fixed, H_kv is the design
variable studied here. Depth, head width and precision are also design choices.
How bad is it?
Take a Llama-3-70B-shaped model — L = 80, H = 64 query heads, d_head = 128,
bf16 — serving a single 32,768-token request:
| Variant | H_kv |
Per token | Cache at 32k | vs MHA |
|---|---|---|---|---|
| MHA (hypothetical) | 64 | 2560 KiB | 80.00 GiB | 1× |
| GQA (what it ships with) | 8 | 320 KiB | 10.00 GiB | 8× smaller |
| MQA | 1 | 40 KiB | 1.25 GiB | 64× smaller |
80 GiB for one user's hypothetical MHA context, before roughly 140 decimal GB of BF16 model weights. This does not fit on one 80-GB-class accelerator; serving requires sharding, offload, reduced precision or a smaller memory requirement.
(Llama 3 70B actually uses GQA with 8 KV heads — the MHA row shows what it would cost without it.)
Ctrl/Cmd + wheel to zoom · drag to pan · double-click to fit · ⛶ full size
There is a second cost: small-batch decoding is often memory-bandwidth-bound. Full attention reads the relevant cache, though locality and implementation affect where those bytes are served from. Smaller cache means fewer bytes read means faster tokens/sec — the saving is speed as well as capacity.
9.2 MHA — the baseline
Every query head owns its own K and V heads. H_kv = H.
Head 1: Q1 K1 V1
Head 2: Q2 K2 V2
Head 3: Q3 K3 V3
Head 4: Q4 K4 V4Maximum expressiveness, maximum cache. Still used by OLMo 2 and Olmo 3 7B — Raschka notes OLMo 2 "still uses traditional Multi-Head Attention (MHA) instead of MLA or GQA," though the later 32B variant switched to GQA.
9.3 MQA — Multi-Query Attention
(Shazeer, 2019)
All query heads share a single K and a single V head. H_kv = 1.
Head 1: Q1 ─┐
Head 2: Q2 ─┼─ K_shared V_shared
Head 3: Q3 ─┤
Head 4: Q4 ─┘The cache shrinks by a factor of H — 64× for a 64-head model. Enormous.
The problem is equally large: every head now attends using the same key space. Module 04's entire argument was that different heads should capture different relational views; MQA forces them to share the lens they look through. Quality degrades measurably, and training can become unstable.
MQA is rarely used alone today, but it matters as the endpoint of a spectrum — and, as §9.5 notes, MLA effectively reaches it at inference.
9.4 GQA — Grouped-Query Attention
(Ainslie et al., 2023)
The interpolation, and the reason it won: query heads are partitioned into groups, and each group shares one K/V pair.
Raschka's description: "if there are 2 key-value groups and 4 attention heads, then heads 1 and 2 might share one set of keys and values, while heads 3 and 4 share another."
Group 1: Q1, Q2 ──► K1 V1
Group 2: Q3, Q4 ──► K2 V2Ctrl/Cmd + wheel to zoom · drag to pan · double-click to fit · ⛶ full size
H_kv is now a dial: H_kv = H is MHA, H_kv = 1 is MQA, anything between is
GQA. Typical production values are 4 or 8.
The quality claim: ablations in the original GQA paper and the Llama 2 paper show GQA "performs comparably to standard MHA in terms of LLM modeling performance" (Raschka). An 8× cache reduction for approximately no quality loss is why GQA became the default.
Implementation
The mechanism is: project fewer K/V heads, then repeat them to match the query heads.
import torch, torch.nn as nn, torch.nn.functional as F
class GroupedQueryAttention(nn.Module):
def __init__(self, d_model, n_heads, n_kv_heads, d_head=None):
super().__init__()
assert n_heads % n_kv_heads == 0
self.H, self.H_kv = n_heads, n_kv_heads
self.group_size = n_heads // n_kv_heads
self.d_head = d_head or d_model // n_heads
self.W_q = nn.Linear(d_model, self.H * self.d_head, bias=False)
self.W_k = nn.Linear(d_model, self.H_kv * self.d_head, bias=False) # SMALLER
self.W_v = nn.Linear(d_model, self.H_kv * self.d_head, bias=False) # SMALLER
self.W_O = nn.Linear(self.H * self.d_head, d_model, bias=False)
def forward(self, x, cos, sin, kv_cache=None):
B, T, _ = x.shape
q = self.W_q(x).view(B, T, self.H, self.d_head).transpose(1, 2)
k = self.W_k(x).view(B, T, self.H_kv, self.d_head).transpose(1, 2)
v = self.W_v(x).view(B, T, self.H_kv, self.d_head).transpose(1, 2)
q = apply_rope(q, cos, sin)
k = apply_rope(k, cos, sin)
if kv_cache is not None: # cache the SMALL tensors
k, v = kv_cache.update(k, v)
# Explicit teaching expansion allocates copies; native GQA may avoid it.
k = k.repeat_interleave(self.group_size, dim=1) # (B, H, T, d_head)
v = v.repeat_interleave(self.group_size, dim=1)
past = k.shape[2] - T
allowed = (torch.arange(k.shape[2], device=x.device)[None, :]
<= past + torch.arange(T, device=x.device)[:, None])
out = F.scaled_dot_product_attention(q, k, v, attn_mask=allowed)
out = out.transpose(1, 2).reshape(B, T, -1)
return self.W_O(out)The saving is real because only the pre-expansion tensors are cached. The
explicit repeat_interleave above allocates temporary expanded tensors each
step. Native grouped kernels can avoid this expansion; this Python reference
does not. The offset mask lets query i see keys through past+i.
Who uses GQA
| Model | H |
H_kv |
group size |
|---|---|---|---|
| Llama 3 8B | 32 | 8 | 4 |
| Llama 3 70B | 64 | 8 | 8 |
| Llama 4 Maverick | — | — | GQA |
| Qwen3 (all sizes) | 16–64 | 4–8 | 4–8 |
| Gemma 3 / Gemma 4 | 16 | 8 | 2 |
| Mistral Small 3.1 | 32 | 8 | 4 |
| gpt-oss | 64 | 8 | 8 |
| Olmo 3 32B | 40 | 8 | 5 |
9.5 MLA — Multi-Head Latent Attention
(DeepSeek-V2, 2024)
DeepSeek took a different route: instead of sharing K and V, compress them.
Raschka: "Instead of sharing key and value heads like GQA, MLA compresses the key and value tensors into a lower-dimensional space before storing them in the KV cache. At inference time, these compressed tensors are projected back to their original size before being used... This adds an extra matrix multiplication but reduces memory usage."
The mechanism
standard: x ──W_k──► K (H · d_head) cache K and V directly
MLA: x ──W_dkv──► c (d_c ≈ 512) cache ONLY c
│
├──W_uk──► K reconstruct at use time
└──W_uv──► Vc is a low-rank latent: a single compressed vector per token per layer
standing in for all heads' K and V.
Ctrl/Cmd + wheel to zoom · drag to pan · double-click to fit · ⛶ full size
The RoPE complication
One can decompress keys and then apply RoPE correctly. The difficulty is efficient weight absorption: position-dependent rotations generally cannot be absorbed into one fixed query/up-projection product across all key positions. Decoupling the positional branch avoids rebuilding the large content keys.
DeepSeek's solution: split each head's dimensions in two.
- a compressed part, reconstructed from the latent, carrying no RoPE
- a small RoPE-carrying part (64 dims), cached separately in full
So the cache holds the latent c_KV (512) plus a decoupled RoPE key (64) — 576
values per token per layer, instead of 2 × 128 heads × 128 dims = 32,768.
Queries may also use a low-rank projection. Whether its factors are explicitly evaluated or algebraically fused is an implementation choice; query compression is not inherently a training-only operation.
The numbers
DeepSeek V3 — L = 61, 128 heads, d_head = 128, bf16, 32k context:
| Per token | Cache at 32k | |
|---|---|---|
| Hypothetical equal-width MHA | 3904 KiB | 122.00 GiB |
MLA (d_c = 512 + 64 RoPE) |
68.625 KiB | 2.145 GiB |
~57× smaller against this chosen equal-width MHA baseline. DeepSeek's content and positional key widths differ; this is not an exact count of every possible expanded-key implementation or a universal MLA compression ratio.
The surprising part: it is also better
Compression usually costs quality. Raschka reports the opposite, from the DeepSeek-V2 ablations: "GQA appears to perform worse than MHA, whereas MLA offers better modeling performance than MHA, which is likely why the DeepSeek team chose MLA over GQA."
He adds a fair caveat — the paper does not show a "KV Cache per Token" comparison between MLA and GQA directly — and elsewhere notes that "according to the ablation studies in the DeepSeek-V2 paper, MLA is on par with regular full attention when the hyperparameters are carefully chosen." So: MLA is somewhere between "on par with" and "slightly better than" MHA, while using a fraction of the cache.
Why might compression help? The low-rank bottleneck is a regulariser. Forcing K/V through 512 dimensions may discard noise that full-rank projections preserve.
The cost is complexity. MLA is substantially harder to implement, and kernel support lagged for a long time.
Adoption
MLA has spread beyond DeepSeek:
| Model | Notes |
|---|---|
| DeepSeek V2 / V3 / R1 / V3.2 | originator |
| Kimi K2 (1T params) | DeepSeek V3 architecture, "fewer heads in the MLA module" |
| Kimi Linear | MLA in the global-attention layers, plus a gate |
| Mistral 3 Large (675B) | "exactly the same architecture as DeepSeek V3 and V3.1" |
| GLM-5 (744B, Feb 2026) | adopted MLA + DeepSeek Sparse Attention |
Raschka's note on Mistral 3 Large is a good summary of where the field landed: "why change what ain't broke? A lot of the secret sauce these days is in the training pipeline as well as the inference scaling strategies."
9.6 Side by side
Ctrl/Cmd + wheel to zoom · drag to pan · double-click to fit · ⛶ full size
| MHA | MQA | GQA | MLA | |
|---|---|---|---|---|
| Strategy | none | share all K/V | share in groups | compress to latent |
| KV heads | H |
1 | 4–8 | n/a (latent) |
| Cache vs MHA | 1× | 1/H |
1/g |
~1/57 (V3) |
| Quality | baseline | degrades | ≈ baseline | ≈ or > baseline |
| Complexity | trivial | trivial | easy | hard |
| Used by | OLMo 2, Olmo 3 7B | rare alone | Llama, Qwen, Gemma, Mistral, gpt-oss | DeepSeek, Kimi, Mistral 3 Large, GLM-5 |
Choosing: GQA unless you have a reason not to. It captures most of the benefit at a fraction of the implementation cost, which is why it is the 2026 default. MLA is worth it at frontier scale where cache dominates the serving budget and you can afford the engineering.
9.7 What this does not fix
All three shrink the cache by a constant factor. None change that it grows
linearly with T, or that attention compute is still O(T²).
Halving the constant buys you 2× context. It does not buy you 100×.
For that you need to change the attention pattern (module 10) or the kernel (module 11). Those are orthogonal to this module — Gemma 3 combines GQA with sliding windows; DeepSeek V3.2 combines MLA with sparse attention; everything runs on FlashAttention.
Reconciling the sources
Coverage. The playlist does not cover MQA, GQA, or MLA at all — they postdate it, and it never discusses the KV cache. Everything here comes from Raschka plus the primary papers. What the playlist does give you is why MHA has multiple heads in the first place (module 04), which is exactly what you need to understand what MQA sacrifices.
GQA vs MLA quality. Raschka reports the DeepSeek-V2 ablation showing GQA worse than MHA and MLA better. This is one team's ablation on their own architecture, and it conflicts with the GQA paper and the Llama 2 paper, which both find GQA ≈ MHA. Raschka reports the tension without resolving it, and so do we. The honest reading: GQA's quality cost is small and setup-dependent; MLA's advantage is real but measured by its authors.
Worked latent absorption and memory accounting
Use row-vector notation with cached C of shape (T,r). For one head let
U_K:(r,d_k), U_V:(r,d_v), query q:(1,d_k), and output slice
O_h:(d_v,d_model). Expanded content scores are q (C U_K).T;
absorbed scores are (q U_K.T) C.T. If a is the resulting attention row,
the head output (a C U_V) O_h equals (a C)(U_V O_h).
The latter aggregates in latent space. Head-specific softmax remains necessary;
absorption does not collapse different heads into one attention distribution.
import torch
torch.manual_seed(9)
dtype = torch.float64
c, uk, uv = torch.randn(5, 3, dtype=dtype), torch.randn(3, 4, dtype=dtype), torch.randn(3, 2, dtype=dtype)
q, out_proj = torch.randn(1, 4, dtype=dtype), torch.randn(2, 6, dtype=dtype)
expanded_scores = q @ (c @ uk).T
absorbed_scores = (q @ uk.T) @ c.T
torch.testing.assert_close(expanded_scores, absorbed_scores)
a = (expanded_scores / 4**0.5).softmax(-1)
torch.testing.assert_close((a @ (c @ uv)) @ out_proj, (a @ c) @ (uv @ out_proj))
past, new = 3, 2
allowed = torch.arange(past + new)[None, :] <= past + torch.arange(new)[:, None]
assert allowed.tolist() == [[True, True, True, True, False], [True] * 5]
print("Expanded and absorbed content branches agree; cached masks include offsets.")RoPE adds a positional score branch. Rotating expanded content keys is valid but obstructs the single position-independent absorption above. Decoupled positional keys retain that efficient content path. The latent algebra here is a shape-level derivation, not the entire DeepSeek projection stack.
For GQA, persistent cache bytes are B*T*L*H_kv*(d_k+d_v)*s. The familiar
factor two assumes equal key/value widths. A latent-plus-positional cache uses
B*T*L*(r+d_rope)*s, before metadata/alignment. Divide by 2**30 for GiB,
10**9 for GB. Sharding requires per-rank accounting: replicated KV heads
or latent states do not automatically divide by tensor-parallel degree.
Failure to fit on one GPU does not imply failure to serve across devices.
Key takeaways
- The KV cache turns decoding from
O(T)recompute intoO(1), but its size becomes the binding constraint:2·B·T·L·H_kv·d_head·bytes. - At 32k context the hypothetical MHA-shaped 70B example needs 80 GiB of cache per request, motivating multi-device serving or a reduced cache requirement.
- Holding other dimensions fixed,
H_kvis the dial here. MHA (H_kv=H), MQA (H_kv=1), and GQA (in between) are points on that one dial. - MQA shrinks the cache
H× but forces every head to share one key space, costing quality. - GQA groups query heads to share K/V. 4–8× smaller cache at approximately no quality cost. The 2026 default.
- MLA compresses K/V into a ~512-dim latent, caching that instead. ~57× smaller for DeepSeek V3, and DeepSeek's ablations show quality at or above MHA. Much harder to implement; needs a decoupled RoPE dimension.
- Smaller caches can improve bandwidth-bound decoding as well as capacity; extra projections, dispatch and kernel efficiency affect measured latency.
- All three are constant-factor wins. Cache still grows linearly in
Tand attention is stillO(T²).
Self-check
- Compute the KV cache for a model with
L = 32,H = 32,d_head = 128, bf16, atT = 8192,B = 1, forH_kv ∈ {32, 8, 1}. Which is servable on a 24 GB consumer GPU alongside a 14 GB model? - Module 04 argued multiple heads exist to capture multiple perspectives. Given that, explain precisely what MQA gives up — and why GQA gives up much less.
- Derive the content-score and output weight absorptions for MLA. What extra computation or storage is needed for positional information, and why can the resulting cache still be smaller than expanded K/V?