Single-Head Attention and Stacked Attention Layers
MultiHeadAttention uses one projection per role (query, key, value) to a width of d_out, then reshapes that width into num_heads and head_dim.
Starting from the Code, Not the Theory
Multi-head attention is often introduced as a theory of several attention heads working side by side. This article takes a different route. It starts with the code of the MultiHeadAttention class, which is a PyTorch module, and follows the tensors through its forward method, where every computation happens. The goal is to understand not only what the method computes, but how the shape of the data changes at each line.
Read the forward method once, from top to bottom. As you read, notice which lines create new tensors and which lines only rearrange tensors that already exist. Most of the method is the familiar attention computation. What makes it multi-head is the reorganization that happens around that computation, and that reorganization is almost entirely a matter of shapes.
Three things in the method matter most. First, the input is projected by three linear layers into keys, queries, and values. Second, three view calls and three transpose calls reorganize those projections so that each head has its own slice. Third, the final view and the out_proj layer turn the per-head results back into a single output.
Predicting the Output Shape Before Tracing
Before following the tensors, make a prediction. The input x has shape (b, num_tokens, d_in), and the constructor was given d_out as the projection size. Decide what shape the method returns.
What do you think happens?
Given input shape (b, num_tokens, d_in) and a constructor argument d_out, what shape does MultiHeadAttention return?
Reveal answer
(b, num_tokens, d_out)
The input width is d_in, but the returned width is d_out. The method passes through several intermediate shapes, including per-head shapes with four axes, before it returns to a three-axis output.
The answer is not the same as the input shape. Keep that difference in mind. The method changes the width from d_in to d_out, and it also changes the number of axes temporarily when it splits the features into heads. Tracing those intermediate shapes is the whole job of this article.
One Wide Projection per Role
The class does not create a separate weight matrix for each head. It creates one projection per role: W_query, W_key, and W_value. Each of these maps d_in to d_out. Each projection therefore produces one wide matrix that contains the information for all heads side by side.
The head structure is not built into the weights. It is imposed afterward, by reshaping that wide matrix. This is the central design decision: a single large projection per role, split internally, rather than many small independent projections.
The constructor enforces this relationship. It asserts that d_out is divisible by num_heads, and then it sets head_dim to d_out divided by num_heads. Written as a formula, head_dim is $\frac{d_{\text{out}}}{\text{num\_heads}}$. The head count must divide the output width evenly, because an uneven split would leave some head without a well-defined slice.
Splitting d_out Into Heads
After the projection, each tensor is still one wide matrix per token. The head structure is created by two operations applied in sequence: a view call that regroups the last dimension into heads, and a transpose call that moves the head dimension ahead of the token dimension. The two operations are easiest to follow as a before-and-after pair.
Where Each Head Lives in the Vector
To make the split concrete, imagine a tiny case with d_out equal to 6 and num_heads equal to 2. Then head_dim is 3. The diagram below labels each of the six columns of the projected vector with the head it belongs to. This example is generated for teaching purposes; the same arithmetic applies to any d_out that is divisible by num_heads.
After the view, the first three columns form head 0 and the last three form head 1. The transpose then reorders the axes so that each head's three-column slice is processed as its own matrix, separate from the other head. Nothing in the weights was split; the split exists only in how the same wide vector is indexed.
Tracing the Full Shape Path
The next diagram shows the full path of data through the forward method. Each stage corresponds to one statement in the method, so it is useful for deciding which line to inspect first. Shapes are given for a batch of b sequences, each with num_tokens tokens.
Read the diagram as a sequence of one view, then one transpose, for each of the three projections. The score stage is the only one where the token axis appears twice, because each token's query is compared with every token's key. The stage labeled attention scores is an inference from the matrix product queries @ keys.transpose(2, 3), which multiplies the query matrix for each head by the transposed key matrix for that head.
- queries and keys both have shape (b,
num_heads,num_tokens,head_dim) at this point transpose(2, 3)swaps the last two axes of keys so the matrix product is defined
Merging Heads Back into One Output
After the attention weights multiply the values, each head's context has shape (b, num_heads, num_tokens, head_dim). The method first moves the head dimension back with transpose(1, 2), which returns the tensor to (b, num_tokens, num_heads, head_dim). It then calls contiguous() and view(b, num_tokens, self.d_out).
Because d_out equals num_heads times head_dim, the final view merges the last two axes back into one feature axis. The per-head results sit side by side again as a single width of d_out.
transpose(1, 2)restores the token axis to position 1- view merges
num_headsandhead_dimback intod_out out_projis a Linear layer that combines information from all heads
The source describes the final linear layer as optional. Here out_proj is a Linear layer that takes the merged output and produces the returned context_vec. Its role is to combine the information from all heads. The shape of the returned tensor is (b, num_tokens, d_out), which matches the prediction you made earlier only after every intermediate reshape has been undone.
Integrated Design Versus the Wrapper
The earlier MultiHeadAttentionWrapper built multiple heads by stacking complete single-head attention layers, each with its own weight matrices. The MultiHeadAttention class takes an integrated approach instead: it starts with one multi-head projection per role and splits it internally. The diagram below shows the query side of this difference.
The source states that the same integrated idea applies to keys and values, which it leaves out of the figure to reduce clutter. The result is that the class reaches the same conceptual outcome as the wrapper, with fewer separate weight matrices and a single large projection per role.
| Question | MultiHeadAttentionWrapper | MultiHeadAttention |
|---|---|---|
| How are heads built? | By stacking complete single-head attention layers | By one multi-head projection that is split internally |
| Weight matrices | Each head has its own weight matrices | One projection per role: W_query, W_key, W_value, each mapping d_in to d_out |
| Where is the split? | In the separate layers | In view and transpose inside forward |
| Where are heads combined? | Outside the individual head layers | In transpose, view, and out_proj at the end of forward |
Two designs for multi-head attention
Finding Where a Shape Bug Begins
When a shape error appears, the fastest route is to print the shape after each step and compare it with the expected shape from the pipeline above. The first step whose printed shape differs from the expected one is the step where the bug begins. Everything after that point inherits the wrong shape, so the error message at the end is usually a symptom rather than the cause.
Common mistake Expecting the output width to equal d_in
What it looks likeGiven d_in of 8 and d_out of 16, assuming the returned tensor is (b, num_tokens, 8)
Why it failsThe projections map
d_intod_out, so the returned width isd_out.Do this insteadCheck the last axis against d_out after out_proj, not against d_in.
Common mistake Skipping the divisibility check
What it looks likeSetting d_out to 10 with num_heads of 4Why it failshead_dimwould be 2.5, which is not a valid width, and the constructor asserts divisibility for this reason.Do this insteadChoose num_heads so that it divides d_out evenly before building the module.
Common mistake Forgetting that transpose changes the axis order, not the data
What it looks likeAssuming that after transpose(1, 2) the token axis is still in position 1
Why it failsAfter
transpose(1, 2)the head axis sits in position 1 and the token axis moves to position 2, which changes the meaning of each later operation.Do this insteadWrite down the axis order after every transpose and check which axis each matrix multiplication uses.
Common mistake Treating the view at the end as a no-op
What it looks likeAssuming that merging heads needs no change to the layout
Why it failsThe final view merges
num_headsandhead_dimback intod_out, and it relies on the tensor being laid out in the expected order after transpose.Do this insteadConfirm the shape after the final view is (b, num_tokens, d_out) before calling out_proj.
Practice: Trace a Shape Bug
Suppose a learner builds MultiHeadAttention with d_in of 12, d_out of 8, and num_heads of 2, and an error occurs at out_proj with a width mismatch. Using only the shape pipeline in this article, name the first step where the shape could have gone wrong, and explain what you would print to confirm it.
Hints
- Start by checking whether
d_outis divisible bynum_heads, and computehead_dim. - Then follow the three projections, the view, the transpose, and the merge back to
d_out. - The first printed shape that differs from the expected shape marks the bug.
Summary of the Shape Path
- 1MultiHeadAttention uses one projection per role, W_query, W_key, and W_value, each mapping
d_intod_out. - 2view regroups the last dimension into
num_headsandhead_dim, andtranspose(1, 2)moves the head axis ahead of the token axis so each head is processed separately. - 3The final view and
out_projmerge the per-head results back into a single output of shape (b,num_tokens,d_out). - 4The integrated design replaces the wrapper's stacked single-head layers with one multi-head projection that is split internally.
- 5When a shape bug appears, print the shape after each step and locate the first step whose shape differs from the expected one.
At a glance
- MultiHeadAttention uses one projection per role, W_query, W_key, and W_value, each mapping
d_intod_out, and then reshapesd_outintonum_headsandhead_dim. - view regroups the last dimension into heads, and
transpose(1, 2)moves the head dimension ahead of the token dimension so each head's matrices can be multiplied. - The forward method returns a tensor of shape (b,
num_tokens,d_out) after merging heads and applyingout_proj. - The integrated design uses one multi-head projection per role instead of the wrapper's stacked single-head layers, reaching the same conceptual outcome with fewer separate weight matrices.
- To locate a shape bug, check the shape after each step and identify the first step where the shape differs from the expected one.