The attention mechanism is the heart of every transformer. It's the reason GPT can write poetry, Llama can code, and Gemini can understand images. But the original design β Multi-Head Attention (MHA) β has a dirty secret.
It's a memory hog.
For every token the model generates, MHA stores a key-value pair for every head in every layer. At 128K context, that's gigabytes of memory just for the cache. Not the model weights β just the conversation history.
So researchers asked: do we really need all those key-value pairs? The answer created two variants that every modern model now uses: MQA and GQA.
Quick Refresher: How Attention Works
If you need the full deep dive, revisit Episode 2. Here's the essential mechanic:
For each token, attention computes three vectors:
Query (Q): "What am I looking for?"
Key (K): "What do I contain?"
Value (V): "What information do I carry?"Attention score = how well a Query matches each Key. The output = weighted sum of Values based on those scores.
Attention(Q, K, V) = softmax(QK^T / βd) Γ VMulti-Head Attention (MHA)
Instead of one Q/K/V per token, MHA uses multiple heads β each looking at different aspects of the relationship:
Head 1: Qβ, Kβ, Vβ β focuses on syntax ("subject-verb agreement")
Head 2: Qβ, Kβ, Vβ β focuses on semantics ("related concepts")
Head 3: Qβ, Kβ, Vβ β focuses on position ("nearby words")
...
Head 32: Qββ, Kββ, Vββ β focuses on something else
Each head has its own Q, K, and V projection matrices. The outputs are concatenated and projected back.
For a model with h = 32 heads and d = 128 dimensions per head:
Each token generates:
- 32 Query vectors (32 Γ 128 = 4096 dims)
- 32 Key vectors (32 Γ 128 = 4096 dims)
- 32 Value vectors (32 Γ 128 = 4096 dims)
The KV Cache Problem
During inference, the model generates tokens one at a time. Each new token needs to attend to all previous tokens. To avoid recomputing K and V for every previous token, we cache them β the KV cache.
Token 1: compute Kβ, Vβ β store in cache
Token 2: compute Kβ, Vβ β store in cache, attend to [Kβ,Kβ], [Vβ,Vβ]
Token 3: compute Kβ, Vβ β store in cache, attend to [Kβ,Kβ,Kβ], [Vβ,Vβ,Vβ]...
Token 128000: attend to ALL 128K previous K,V pairs
How Big Is This Cache?
For Llama 3 70B (MHA, if it used it):
KV cache per token = 2 (K+V) Γ num_layers Γ num_heads Γ head_dim Γ bytes_per_param
= 2 Γ 80 Γ 64 Γ 128 Γ 2 (FP16)
= 2,621,440 bytes per token
= 2.5 MB per tokenAt 128K context:
= 2.5 MB Γ 128,000
= 320 GB just for KV cache!
That's 320 GB β more than the model weights themselves. For a single conversation. You literally can't serve it.This is the problem MQA and GQA solve.
π§
Multi-Query Attention (MQA)
Paper: "Fast Transformer Decoding" (Noam Shazeer, 2019) Used by: Falcon, PaLM, StarCoder
The idea is brutal in its simplicity: share one K and V across all query heads.
MHA (original):
Head 1: Qβ, Kβ, Vβ β each head has its own K, V
Head 2: Qβ, Kβ, Vβ
Head 3: Qβ, Kβ, Vβ
...
Head 32: Qββ, Kββ, Vββ
Total KV: 32 K's + 32 V's = 64 vectors per token
MQA:
Head 1: Qβ, K_shared, V_shared β ALL heads share ONE K, V
Head 2: Qβ, K_shared, V_shared
Head 3: Qβ, K_shared, V_shared
...
Head 32: Qββ, K_shared, V_shared
Total KV: 1 K + 1 V = 2 vectors per token
The Impact
| Metric | MHA (32 heads) | MQA (1 KV group) | Reduction |
|---|---|---|---|
| KV vectors per token | 64 | 2 | 32x smaller |
| KV cache at 128K | 320 GB | 10 GB | 32x smaller |
| Inference speed | Baseline | ~2-3x faster | Memory bandwidth freed |
| Quality | Best | Slightly worse | Small drop |
MQA gives you a 32x reduction in KV cache memory. That's the difference between "impossible to serve" and "fits on one GPU."
But there's a cost. Having only one K/V pair means all heads see the same keys and values β they lose the ability to attend to different aspects of the input. Quality drops, especially on complex reasoning tasks.
π
Grouped-Query Attention (GQA)
Paper: "GQA: Training Generalized Multi-Query Attention" (Ainslie et al., 2023, Google) Used by: Llama 3, Mistral, Gemma 2, Phi-3, Qwen 2, DeepSeek V3
GQA is the Goldilocks solution: group the heads and share K/V within each group.
MHA (32 heads, 32 KV groups):
Group 1: Qβ, Kβ, Vβ
Group 2: Qβ, Kβ, Vβ
...
Group 32: Qββ, Kββ, Vββ
MQA (32 heads, 1 KV group):
Group 1: Qβ-Qββ, K_shared, V_shared
GQA (32 heads, 8 KV groups):
Group 1: Qβ-Qβ, Kβ, Vβ β 4 query heads share 1 KV
Group 2: Qβ -Qβ, Kβ, Vβ β 4 query heads share 1 KV
Group 3: Qβ-Qββ, Kβ, Vβ
...
Group 8: Qββ-Qββ, Kβ, Vβ
Total KV: 8 K's + 8 V's = 16 vectors per token
GQA: The Numbers
| Metric | MHA (32 KV) | GQA-8 (8 KV) | MQA (1 KV) |
|---|---|---|---|
| KV vectors/token | 64 | 16 | 2 |
| KV cache at 128K | 320 GB | 80 GB | 10 GB |
| Cache reduction | 1x | 4x | 32x |
| Quality | Best | Near-MHA | Slightly worse |
| Inference speed | Slowest | Fast | Fastest |
GQA-8 (8 groups for 32 query heads) gives you 4x memory reduction with virtually no quality loss. That's the sweet spot β and it's why every major model released in 2024-2025 uses GQA.
Visual Comparison
MHA (Multi-Head Attention) β Every head has its own K,V
βββββββββββββββββββββββββββββββββββββββββββββββββ
β QββKβ,Vβ QββKβ,Vβ QββKβ,Vβ QββKβ,Vβ β
β Qβ βKβ ,Vβ QββKβ,Vβ QββKβ,Vβ QββKβ,Vβ β
β ... (32 KV pairs total) ... β
βββββββββββββββββββββββββββββββββββββββββββββββββ
GQA (Grouped-Query) β Groups of heads share K,V
βββββββββββββββββββββββββββββββββββββββββββββββββ
β Qβ Qβ Qβ Qβ β Kβ,Vβ (Group 1) β
β Qβ Qβ Qβ Qβ β Kβ,Vβ (Group 2) β
β ... (8 KV pairs total) ... β
βββββββββββββββββββββββββββββββββββββββββββββββββ
MQA (Multi-Query) β ALL heads share one K,V
βββββββββββββββββββββββββββββββββββββββββββββββββ
β Qβ Qβ Qβ Qβ Qβ Qβ ... Qββ β Kβ,Vβ β
β (1 KV pair total) β
βββββββββββββββββββββββββββββββββββββββββββββββββ
Real-World Model Configurations
Model Attention Query Heads KV Groups KV Heads Ratio
GPT-3 MHA 96 96 96 1:1
| Falcon-40B | MQA | 64 | 1 | 1 | 64:1 |
|---|---|---|---|---|---|
| Llama 2 7B | MHA | 32 | 32 | 32 | 1:1 |
| Llama 2 70B | GQA | 64 | 8 | 8 | 8:1 |
| Llama 3 8B | GQA | 32 | 8 | 8 | 4:1 |
| Llama 3 70B | GQA | 64 | 8 | 8 | 8:1 |
| Mistral 7B | GQA | 32 | 8 | 8 | 4:1 |
| Gemma 2 27B | GQA | 32 | 16 | 16 | 2:1 |
| DeepSeek V3 | MLA | 128 | β | β | Custom |
Notice: Llama 2's small model (7B) could afford MHA. The 70B model needed GQA. By Llama 3, even the 8B model uses GQA β because context lengths got longer (8K β 128K), making KV cache the bottleneck even for small models.
π¬
The Math: Why GQA Barely Hurts Quality
Intuitively, it seems like sharing K/V would lose information. But the research shows otherwise:
Many heads learn redundant patterns β when you analyze what different heads attend to, there's massive overlap
K/V contains less unique info than Q β the query is "what am I looking for" (unique per head), while K/V is "what's in this position" (more shared)
Training adapts β when you train a model from scratch with GQA, the remaining KV heads learn to be more general
The quality difference between MHA and GQA on benchmarks:
MMLU (knowledge): MHA: 79.5% GQA-8: 79.3% (β0.2%)
HumanEval (code): MHA: 67.1% GQA-8: 66.8% (β0.3%)GSM8K (math): MHA: 84.2% GQA-8: 83.9% (β0.3%)
Less than 0.5% drop for 4x memory savings. That's a no-brainer.
π‘οΈ
Try It Yourself
python
import torch
import torch.nn as nn
class GroupedQueryAttention(nn.Module):
"""GQA implementation β the attention used in modern LLMs."""def __init__(self, d_model=512, n_heads=8, n_kv_heads=2):
super().__init__()
self.n_heads = n_heads
self.n_kv_heads = n_kv_heads
self.head_dim = d_model // n_heads
self.n_rep = n_heads // n_kv_heads # How many Q heads per KV groupself.wq = nn.Linear(d_model, n_heads * self.head_dim, bias=False)
self.wk = nn.Linear(d_model, n_kv_heads * self.head_dim, bias=False)
self.wv = nn.Linear(d_model, n_kv_heads * self.head_dim, bias=False)
self.wo = nn.Linear(n_heads * self.head_dim, d_model, bias=False)def forward(self, x):
B, T, _ = x.shape# Project Q, K, V
q = self.wq(x).view(B, T, self.n_heads, self.head_dim)
k = self.wk(x).view(B, T, self.n_kv_heads, self.head_dim)
v = self.wv(x).view(B, T, self.n_kv_heads, self.head_dim)# Repeat K, V to match number of Q heads
# This is the GQA trick β expand KV to match Q heads
k = k.repeat_interleave(self.n_rep, dim=2) # (B, T, n_heads, head_dim)
v = v.repeat_interleave(self.n_rep, dim=2)# Standard attention
q, k, v = [x.transpose(1, 2) for x in (q, k, v)] # (B, n_heads, T, head_dim)
scores = torch.matmul(q, k.transpose(-2, -1)) / (self.head_dim ** 0.5)
attn = torch.softmax(scores, dim=-1)
out = torch.matmul(attn, v)out = out.transpose(1, 2).contiguous().view(B, T, -1)
return self.wo(out)Compare parameter counts
mha = GroupedQueryAttention(d_model=512, n_heads=8, n_kv_heads=8) # MHA
gqa = GroupedQueryAttention(d_model=512, n_heads=8, n_kv_heads=2) # GQA
mqa = GroupedQueryAttention(d_model=512, n_heads=8, n_kv_heads=1) # MQAfor name, model in [("MHA", mha), ("GQA", gqa), ("MQA", mqa)]:
kv_params = sum(p.numel() for n, p in model.named_parameters() if 'wk' in n or 'wv' in n)
print(f"{name}: KV parameters = {kv_params:,}")MHA: KV parameters = 524,288
GQA: KV parameters = 131,072 (4x smaller)
MQA: KV parameters = 65,536 (8x smaller)
π¦
Practical Takeaways
MHA gives every head its own KV β highest quality, highest memory, doesn't scale to long contexts
MQA shares one KV across all heads β 32x smaller cache, but measurable quality drop
GQA groups heads to share KV β the sweet spot, ~4x smaller cache, negligible quality loss
KV cache is the bottleneck at long contexts β at 128K tokens, it dwarfs the model weights
Every major 2024-2025 model uses GQA β Llama 3, Mistral, Gemma 2, Qwen 2, Phi-3
The ratio matters β 4:1 (Llama 3 8B) to 8:1 (Llama 3 70B) is the current sweet spot
π
What's Next?
Episode 75: FlashAttention β Even with GQA reducing KV cache size, the attention computation itself is a bottleneck. FlashAttention rewrites the algorithm to be IO-aware β understanding the GPU's memory hierarchy to run 2-4x faster while using 5-20x less memory. It's the single most impactful optimization in modern LLM inference.
β Previous
Ep 73: Model Formats
Next β
Ep 75: FlashAttention
Next: Episode 75 β FlashAttention
Standard attention wastes 90% of its time waiting for data to move between memory chips. Not computing. Waiting.
This is part of a 98-episode series covering AI engineering from tokens to production deployment.