AAI Agentic Handbook
Problem 107 · Attention & Inference

Multi-Head and Grouped-Query Attention Shapes

MediumAI engineering
01Problem

Implement multi-head self-attention on lists: project `X` (T x d_model) with `Wq`, `Wk`, `Wv` (d_model x d_model for queries; d_model x (n_kv_heads * head_dim) for keys and values), split into heads, run causal attention per head, concatenate, and project with `Wo`. Support grouped-query attention, where `n_heads` query heads share `n_kv_heads` key/value heads (`n_heads % n_kv_heads == 0`). Also write `kv_cache_bytes(layers, n_kv_heads, head_dim, seq_len, batch, bytes_per_value)`.

02Key insight

Heads are a reshape, not extra parameters: d_model is split into n_heads slices of head_dim, each attending independently, and Wo mixes them back. GQA cuts the K and V projections (and the KV cache) by n_heads / n_kv_heads while keeping the query heads, which is why it is the default in modern open models: decode is memory-bandwidth bound, and the cache is what you stream every step. The cache formula (2 for K and V, times layers, kv heads, head_dim, tokens, batch, bytes) is the number you size GPUs with.

03Solution

Try it first.

TimeO(T * d_model^2 + n_heads * T^2 * head_dim)
SpaceO(T * d_model + T * n_kv_heads * head_dim)
tests.py · 7 passing
tests.pypython23 lines
import random
rng = random.Random(0)
rand = lambda r, c: [[rng.uniform(-1, 1) for _ in range(c)] for _ in range(r)]
T, d = 5, 8
X = rand(T, d)
Wq, Wk, Wv, Wo = rand(d, d), rand(d, d), rand(d, d), rand(d, d)
out = multi_head_attention(X, Wq, Wk, Wv, Wo, n_heads=4)
assert len(out) == T and all(len(r) == d for r in out)
one = multi_head_attention(X, Wq, Wk, Wv, Wo, n_heads=1)
ref = matmul(_causal_attention(matmul(X, Wq), matmul(X, Wk), matmul(X, Wv)), Wo)
assert all(abs(a - b) < 1e-9 for r1, r2 in zip(one, ref) for a, b in zip(r1, r2))
assert multi_head_attention(X, Wq, Wk, Wv, Wo, 4, 4) == out
Wk2, Wv2 = rand(d, 4), rand(d, 4)
gqa = multi_head_attention(X, Wq, Wk2, Wv2, Wo, n_heads=4, n_kv_heads=2)
assert len(gqa) == T and len(gqa[0]) == d
for bad in [dict(n_heads=3), dict(n_heads=4, n_kv_heads=3)]:
    try:
        multi_head_attention(X, Wq, Wk, Wv, Wo, **bad)
        assert False
    except ValueError:
        pass
assert kv_cache_bytes(32, 8, 128, 8192, 1, 2) == 2 ** 30
assert kv_cache_bytes(32, 32, 128, 8192, 1, 2) == 4 * 2 ** 30
04Staff-level follow-up

Why not go all the way to one KV head (MQA)? Quality drops measurably; GQA with 8 groups recovers most of it. Then discuss how tensor parallelism shards heads across GPUs (n_kv_heads must divide or be replicated across the TP degree), and how MLA-style latent compression shrinks the cache further at the cost of extra compute.