Skip to content

Multi-Head Attention and Representation Subspaces

Four constructed patterns for the same query, “sat.” Each head projects the full input. Concatenation and Wₒ combine their outputs.

Project each token into h query, key, and value spaces, then compute a separate attention pattern in each head.

A single attention head uses one set of WQW_Q, WKW_K, WVW_V matrices to produce one attention pattern per position. What if a token needs different patterns for different relationships?

For an unmasked, bidirectional example, 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, combining these relationships into one weighted average of value vectors. Multiple heads give the model separate distributions to learn, without assigning each head a linguistic role in advance.


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. The examples also use matrix shapes and parameter counting.


Splitting the representation

Each head projects the full input vector into its own query, key, and value spaces. Attention then runs independently in those projected spaces. The split is in the projection outputs, not a partition that prevents a head from reading some input coordinates.

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 (h⋅dk,  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
Q/K/Vd_model = 8d_k=4d_k=4Head 0Head 1ThecatsatdownconcatW_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

More heads need not add projection parameters. In standard multi-head attention, keeping hdk=dmodelh d_k=d_{model} fixes the total width while changing how many independent attention patterns the block computes.

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.

d_head = 32

Every track uses the same scale: full width = 65,536 parameters. Biases are excluded; Q, K, and V each have h heads.

W_Q: 4 × 4,096 = 16,384 parameters

W_K: 4 × 4,096 = 16,384 parameters

W_V: 4 × 4,096 = 16,384 parameters

W_O: 16,384 = 16,384 parameters

Total = 4d_model² = 65,536, independent of 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².

Within the same parameter budget, multi-head attention trades representational capacity per head (each head produces dkd_k projected features from the full input) for multiple independent attention patterns.


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. A fused projection and batched matmul usually use the GPU more efficiently than a Python loop over heads.

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.

The big WQW_Q matrix is 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. 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

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 heatmaps use hand-constructed weights to illustrate these two patterns.

KeysQueriesThecatsatdownThecatsatdown.60.50.45.35.55dist.
Tap or hover a cell to inspect its weight. Darker cells have larger weights on a fixed 0–1 scale.
Tap or hover cells to inspect weights. Select a cell or row to see the weighted sum.

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

KeysQueriesThecatsatdownThecatsatdown.50.55.60.60dist.
Tap or hover a cell to inspect its weight. Darker cells have larger weights on a fixed 0–1 scale.
Tap or hover cells to inspect weights. Select a cell or row to see the weighted sum.

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


What heads actually learn

Voita et al. (2019) studied what individual heads specialize in and found a few recurring 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 putting more attention weight 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: This constructed head gives the query 'sat' a large weight on its subject 'cat'. The pattern resembles a dependency edge; it is not a measured head.

Constructed examples of patterns studied in trained transformers. Tap or click a head to inspect its weights. A diffuse attention map alone does not establish that a head can be pruned.

Michel et al. (2019) pruned heads from translation and fine-tuned BERT models and found substantial redundancy in some layers and tasks. Cross-attention in their translation model was more sensitive. A recognizable attention pattern alone does not tell us whether a head is necessary; that requires an intervention and evaluation.

Pruning heads tests whether an already-trained model needs them for a particular task. Sharing K and V projections is a separate architectural choice that reduces cache storage during generation.

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.

Choose any stage to read its vector below the diagram. The updates are specified example values; only their additions are shown.

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, before any block adds an update.

Select successive stages to inspect the attention and FFN updates to the same token vector.


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. At fixed head width and cache precision, this cuts the KV cache by a factor of hh. The paper reports faster decoding with a small quality tradeoff in its experiments; the latency benefit depends on hardware and batch shape.

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 heads→GQA: g KV heads→MQA: 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 headsQ0Q1Q2Q3Q4Q5Q6Q7KV0KV1KV2KV3KV4KV5KV6KV7

KV cache: 8 × (K + V) per layer per token, at fixed head width and dtype.

MHA (Multi-Head): Each query head has its own K and V projections. 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.

At fixed head width and precision, the cache grows with both sequence length and KV-head count. GQA reduces the latter; Post 5.4 derives the resulting memory budget.


Misconceptions

"More heads means more parameters." With standard multi-head attention and hdk=dmodelh d_k=d_{model}, the Q/K/V/O projection budget stays at 4dmodel24d_{model}^2 (ignoring biases). If head width stays fixed while head count grows, the parameter count grows too.

"Head width must equal dmodel/hd_{model}/h." That's a common choice, not a mathematical requirement. A projection can produce hdkh d_k features, reshape them into hh heads, and map the concatenated outputs back through WOW_O. The fixed parameter count above assumes hdk=dmodelh d_k=d_{model}.

"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.

"One wide head can reproduce several heads just by changing its weight matrices." In general it cannot. Each head computes its own softmax distribution over positions. One head has only one such distribution per query; block-diagonal projections do not supply the missing independent softmax operations.

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)