Skip to content

Multi-Head Attention and Representation Subspaces

Split d_model into h smaller subspaces so each head can run its own attention pattern in parallel.

So you've got single-head attention working. One set of WQW_Q, WKW_K, WVW_V matrices, one attention pattern per position, one weighted sum of values. It works. But there's a problem worth tracing through.

Consider the sentence: "The cat sat on the mat because it was tired."

The word "it" needs to figure out what to attend to, and the answer is kind of everything, for different reasons. It needs to attend to "cat" for coreference (that's what "it" refers to). It needs to attend to "was" for syntax ("it" is the subject, "was" is its verb). And it probably wants some sense of "tired" for semantic coherence.

A single attention head produces one softmax distribution per query position. So "it" has to smear its attention across "cat" and "was" and "tired" and hope that the weighted average of their value vectors captures all three relationships simultaneously. That's a lot to ask of a single soft lookup.

You're basically forcing the model to compute one compromise attention pattern when what it really needs is several independent ones running in parallel. One for syntax, one for coreference, one for whatever else the model discovers is useful.

That's exactly what multi-head attention does, and it's the form Vaswani et al. introduced in the original transformer paper.


Prerequisites

This post assumes you've read Self-Attention: Q, K, V from First Principles (Post 4.1). You should be comfortable with scaled dot-product attention, softmax over score vectors, and the idea that WQW_Q, WKW_K, WVW_V project each token into query, key, and value vectors. We'll also use basic matrix shapes and parameter counting, but nothing beyond what showed up in that post.


Splitting the representation

The idea is almost disappointingly simple. Instead of computing one attention function over the full dmodeld_{model}-dimensional space, you split the representation into hh smaller subspaces and compute attention independently in each one.

If dmodel=512d_{model} = 512 and h=8h = 8, each head operates on vectors of dimension dk=dmodel/h=64d_k = d_{model} / h = 64. Each head gets its own projection matrices: WQiW_Q^i, WKiW_K^i, WViW_V^i, each of shape (dmodel,dk)(d_{model}, d_k). So head ii computes:

Qi=XWQi,Ki=XWKi,Vi=XWViQ_i = X W_Q^i, \quad K_i = X W_K^i, \quad V_i = X W_V^i

Then each head runs the same scaled dot-product attention from Post 4.1:

headi=softmax ⁣(QiKiTdk)Vi\text{head}_i = \text{softmax}\!\left(\frac{Q_i K_i^T}{\sqrt{d_k}}\right) V_i

Each head's output has shape (n,dk)(n, d_k), a sequence of nn tokens, each represented in that head's dkd_k-dimensional subspace. We concatenate all hh heads back together and project through one final matrix WOW_O of shape (hdk,  dmodel)(h \cdot d_k, \; d_{model}):

MultiHead(X)=Concat(head1,,headh)  WO\text{MultiHead}(X) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) \; W_O

Each head gets to learn its own attention pattern. One head might specialize in positional relationships, another in syntactic dependencies, another in something the model finds useful that we might not even have a name for. And then WOW_O mixes everything back together.

Xd_model = 8d_k=4d_k=4Head 0 (positional)Head 1 (semantic)ThecatsatdownconcatW_Ooutputd_model = 8
heads
d_k = 8/2 = 4

Toggle between h=2 and h=4 to see how splitting d_model changes the subspace dimension d_k, and how each head's attention pattern over the same four tokens looks different.


The parameter count

When I first saw multi-head attention, I assumed it must add parameters. More heads, more matrices, more stuff. It doesn't. For a fixed dmodeld_{model}, the total parameter count is the same whether you use 1 head or 16.

Here's the accounting. With hh heads, each head has three projection matrices of shape (dmodel,dk)(d_{model}, d_k) where dk=dmodel/hd_k = d_{model}/h. That's 3×dmodel×dk3 \times d_{model} \times d_k parameters per head, times hh heads:

h×3×dmodel×dmodelh=3×dmodel2h \times 3 \times d_{model} \times \frac{d_{model}}{h} = 3 \times d_{model}^2

Plus WOW_O which is (dmodel,dmodel)(d_{model}, d_{model}), so dmodel2d_{model}^2 more. Total: 4×dmodel24 \times d_{model}^2.

A single head with full-dimensional Q, K, V? Three matrices of (dmodel,dmodel)(d_{model}, d_{model}) plus an output projection. Also 4×dmodel24 \times d_{model}^2.

Parameter accounting for d_model = 128, h = 4, d_k = 32
total: 65.5k = 4 × d_model² = 4 × 16.4k
per-head Q/K/V matrices · shape (d_model, d_k) = (128, 32)
W_Q^i× h=4
4 × 4.1k = 16.4k
W_K^i× h=4
4 × 4.1k = 16.4k
W_V^i× h=4
4 × 4.1k = 16.4k
output projection · shape (d_model, d_model)
W_O
16.4k
grand total · same value for any h
all params
65.5k
d_model
h

Toggle h to watch the per-head matrices shrink exactly as fast as the head count grows. The W_O bar never moves, and the grand total stays pinned at 4 × d_model². You're rearranging the budget, not adding to it.

Same parameter budget. What changes is the structure. Multi-head attention trades representational capacity per head (each head only sees dkd_k dimensions) for the ability to maintain multiple independent attention patterns. You're not getting a bigger model. You're getting a differently shaped one.


The reshape trick

In the math I wrote above, each head has its own separate WQiW_Q^i matrix. You could implement it that way, with a loop over heads. Nobody does, because GPUs hate loops.

In practice, you use one big WQW_Q matrix of shape (dmodel,dmodel)(d_{model}, d_{model}) that computes all the queries at once. Then you reshape the output from (b,n,dmodel)(b, n, d_{model}) to (b,n,h,dk)(b, n, h, d_k) and transpose to (b,h,n,dk)(b, h, n, d_k). Now the hh dimension is a batch dimension, and a single batched matrix multiply handles all heads simultaneously.

This is one of those things where the math and the implementation look very different, but they're computing the exact same thing. The big WQW_Q matrix is literally the concatenation of all the per-head WQiW_Q^i matrices side by side. The reshape just splits its output into the per-head chunks.

import torch
import torch.nn as nn
import torch.nn.functional as F
 
class MultiHeadAttention(nn.Module):
    def __init__(self, d_model: int, num_heads: int):
        super().__init__()
        assert d_model % num_heads == 0
        self.d_model = d_model
        self.h = num_heads
        self.d_k = d_model // num_heads
 
        # One big projection for all heads, then reshape
        self.W_Q = nn.Linear(d_model, d_model, bias=False)
        self.W_K = nn.Linear(d_model, d_model, bias=False)
        self.W_V = nn.Linear(d_model, d_model, bias=False)
        self.W_O = nn.Linear(d_model, d_model, bias=False)
 
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        b, n, _ = x.shape
 
        # Project: (b, n, d_model) -> (b, n, d_model)
        Q = self.W_Q(x)
        K = self.W_K(x)
        V = self.W_V(x)
 
        # Reshape into heads: (b, n, d_model) -> (b, h, n, d_k)
        Q = Q.view(b, n, self.h, self.d_k).transpose(1, 2)
        K = K.view(b, n, self.h, self.d_k).transpose(1, 2)
        V = V.view(b, n, self.h, self.d_k).transpose(1, 2)
 
        # Scaled dot-product attention (all heads at once)
        # (b, h, n, d_k) @ (b, h, d_k, n) -> (b, h, n, n)
        scores = (Q @ K.transpose(-2, -1)) / (self.d_k ** 0.5)
        attn = F.softmax(scores, dim=-1)
 
        # (b, h, n, n) @ (b, h, n, d_k) -> (b, h, n, d_k)
        out = attn @ V
 
        # Concat heads: (b, h, n, d_k) -> (b, n, d_model)
        out = out.transpose(1, 2).contiguous().view(b, n, self.d_model)
 
        # Final projection
        return self.W_O(out)

That .view(b, n, self.h, self.d_k).transpose(1, 2) pattern is everywhere in transformer code. Once you recognize it, you can read pretty much any attention implementation. The view splits the last dimension into (heads, d_k), and the transpose moves the head dimension next to the batch dimension so the matmul broadcasts across both.


A worked example

Let me trace through a tiny case to make this concrete. We have dmodel=8d_{model} = 8, h=2h = 2 heads, so dk=4d_k = 4. Our input is four tokens: "The", "cat", "sat", "down", each represented as an 8-dimensional vector.

After projecting through WQW_Q and reshaping, each head gets its own 4-dimensional query for each token. Same for keys and values. Head 0 might end up with queries and keys that emphasize positional information, just because of how the weights happened to initialize and train. Head 1 might end up sensitive to semantic similarity.

Head 0 computes its own attention matrix, a 4×44 \times 4 grid of softmax weights. Maybe "sat" attends mostly to "cat" and "down" (nearby positions). Head 1 computes a different attention matrix. Maybe "sat" attends mostly to "cat" (the subject that did the sitting).

Each head produces a (4,4)(4, 4) output, four tokens each with a 4-dimensional value. We concatenate along the last dimension: (4,4)(4,4)=(4,8)(4, 4) \| (4, 4) = (4, 8). Back to dmodel=8d_{model} = 8. Then we multiply by WOW_O to get the final (4,8)(4, 8) output.

The key insight is that those two heads computed genuinely different attention patterns. Token "sat" is looking at different things for different reasons, and the model gets both signals instead of having to compromise.

Here's what those two heads' attention matrices might look like. Head 0 concentrates on nearby tokens (positional), while Head 1 is doing something more semantic, linking subjects to their verbs.

KeysQueriesThecatsatdownThecatsatdown.60.50.45.35.55dist.
Tap or hover cells to inspect weights. Click a row to see the weighted sum.

Head 0 (positional): each token attends mostly to itself and its immediate neighbors.

KeysQueriesThecatsatdownThecatsatdown.50.55.60.60dist.
Tap or hover cells to inspect weights. Click a row to see the weighted sum.

Head 1 (semantic): 'sat' attends strongly to 'cat' (its subject), not just nearby positions.


What heads actually learn

This is where it gets interesting. Voita et al. (2019) did a careful study of what individual heads specialize in, and found a few clear patterns.

Positional heads learn to attend to tokens at a fixed relative offset. One head might always look at the previous token. Another might attend to the token two positions back. These are doing something like n-gram modeling inside the transformer.

Syntactic heads learn to attend along dependency parse edges. A head might consistently attend from a verb to its subject, or from a pronoun to its antecedent. These are the heads that would help with our "it" / "cat" example.

Rare-token heads light up disproportionately for infrequent words, possibly routing more computation toward tokens that are harder to predict.

Six attention patterns over “the cat sat on the mat”
rows = query token, columns = key token, opacity = attention weight
subject ← verb: When the verb 'sat' is the query, this head fires hard on its subject 'cat'. A learned dependency-parse edge.

Stylized examples of the patterns researchers actually find inside trained transformers. Click any head to see what it's doing. Notice how the diffuse head doesn't have an obvious job — those are the ones that often get pruned.

Michel et al. (2019) showed you can prune a huge number of heads at test time with minimal quality loss. In some models, removing 60% or more of heads barely moved the needle on downstream tasks. The specialized heads above are doing the heavy lifting. Many of the other heads seem to be partially redundant or learning soft variants of the same pattern.

This doesn't mean the extra heads are useless during training. Having many heads gives the optimizer more paths to explore, and some of that redundancy probably helps training stability. But at inference time, the model is over-provisioned. That observation directly motivates the KV-cache optimizations we'll get to in a moment.

Remember, the concatenated multi-head output gets projected through WOW_O and then added back to the residual stream. Each layer's attention block is writing an update into the same shared vector. Click through the stages below to see how these additive updates accumulate.

Residual Stream — click a block to inspect
xEmbed+Attn 1+FFN 1+Attn 2+FFN 2+Attn 3+FFN 3OutputLayer 1Layer 2Layer 3
Token Embedding
Residual stream after this stage:
[
0.30
-0.10
0.50
0.20
-0.40
0.10
0.80
-0.30
]
This is the initial token embedding. Everything builds on it.

The residual stream carries information forward. Each attention block (and each FFN block) reads from the stream, computes an update, and adds it back. Multi-head attention's output is one of those additive updates.


Sharing K and V: GQA and MQA

Multi-head attention as described above means every head has its own WKW_K and WVW_V projections. At inference time, when you're caching key-value pairs to avoid recomputation (the KV cache), that means storing separate K and V tensors for every head, at every layer, for every token in the context. For a model with 32 heads and a long context, this gets expensive fast.

Shazeer (2019) proposed Multi-Query Attention (MQA): keep separate Q projections for each head, but share a single K and V projection across all heads. Every head computes its own queries but looks up the same keys and values. This cuts the KV cache by a factor of hh and speeds up inference dramatically, at the cost of some quality.

Ainslie et al. (2023) proposed a middle ground: Grouped-Query Attention (GQA). Instead of one KV pair for all heads, or one KV pair per head, you divide the heads into gg groups. Each group shares one K and V projection. If you have h=32h = 32 heads and g=8g = 8 groups, each group of 4 heads shares one KV set. The KV cache shrinks by a factor of h/g=4h/g = 4.

MHA: h KV headsGQA: g KV headsMQA: 1 KV head\text{MHA: } h \text{ KV heads} \quad \rightarrow \quad \text{GQA: } g \text{ KV heads} \quad \rightarrow \quad \text{MQA: } 1 \text{ KV head}
mode8 KV heads for 8 query heads
Q headsKV headsQ0Q1Q2Q3Q4Q5Q6Q7KV0KV1KV2KV3KV4KV5KV6KV7KV cache: 8 × (K + V) per layer per token

MHA (Multi-Head): Each query head has its own K and V projections. Full expressiveness, but the KV cache stores 8 separate key-value pairs per layer per token.

Toggle between MHA, GQA, and MQA to see how query heads map to KV heads. Color groups show which query heads share the same keys and values.

The reason this matters so much is tied to how autoregressive decoding works. During generation, you're producing one token at a time, and each new token needs to attend to all previous tokens. Without the KV cache, you'd recompute attention over the entire sequence at every step. With the cache, you store the K and V projections and only compute the new token's query. But the cache grows linearly with sequence length and with the number of KV heads. GQA directly attacks that second factor. More on this in Post 5.4.


Misconceptions

"More heads means more parameters." No. For a fixed dmodeld_{model}, changing the number of heads doesn't change the parameter count. It changes the structure: each head is smaller, but there are more of them. Same total budget. What you actually buy with more heads is more parallel relation types, not more capacity.

"Each head dimension can be anything." It can't, at least not in standard multi-head attention. The whole reshape trick only works because dk=dmodel/hd_k = d_{model} / h, so concatenating hh heads of size dkd_k gives you back something of size dmodeld_{model}. If you wanted heads with arbitrary dimension you'd have to give up the clean reshape and pay for separate matmuls. Some research variants do that. The vast majority of production attention does not.

"Each head has a clear, interpretable role." Some do. Voita et al. found clear positional and syntactic heads, and induction heads are a known phenomenon in mechanistic interpretability work. But many heads don't have a clean interpretation, and a lot of them can be pruned without meaningful degradation. The picture is messier than the clean "one head per linguistic phenomenon" story.

"Multi-head attention is fundamentally different from single-head attention." It's the same computation, restructured. You could replicate multi-head attention with a single head that has a very specific block-diagonal structure in its weight matrices. Multi-head just makes that structure explicit and easy for the optimizer to find.

What's next

The attention mechanism as described so far lets every token attend to every other token. For language generation that's a problem, since a model predicting the next word shouldn't be allowed to peek at future words during training. The next post covers causal masking, how a single upper-triangular matrix enforces that constraint.


Additional reading (and watching)