Concepts / Single-Head Attention and Stacked Attention Layers

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.

  • Programming

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?

  • (b, num_tokens, d_in)

  • (b, num_tokens, d_out)

  • (b, num_heads, num_tokens, head_dim)

Reveal answer
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.

Rule

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.

col 0head 0col 1head 0col 2head 0col 3head 1col 4head 1col 5head 1
Which slice of the d_out vector belongs to each head after view, when d_out is 6 and num_heads is 2?

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.

input x(b, num_tokens, d_in)Q, K, V projections(b, num_tokens, d_out)view(b, num_tokens, num_heads,head_dim)transpose(1, 2)(b, num_heads, num_tokens,head_dim)attention scores(b, num_heads, num_tokens,num_tokens)context per head(b, num_heads, num_tokens,head_dim)transpose(1, 2)(b, num_tokens, num_heads,head_dim)contiguous and view(b, num_tokens, d_out)out_proj(b, num_tokens, d_out)context_vec(b, num_tokens, d_out)three linear layerssplit last dimheads firstqueries @ keys transposedweights times valuesmove heads backmerge last two axescombine headsreturn
What is the shape of the tensor after each step, and which dimension changes at each point?

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.

python
Notes on the code
  • 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.

python
Notes on the code
  • transpose(1, 2) restores the token axis to position 1
  • view merges num_heads and head_dim back into d_out
  • out_proj is 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.

stacked single-headlayersone projection perroleown weights per headsplit by view andtransposemany separate modulescombined by out_proj
How do the two designs differ in how many modules run, how heads are split, and where the head outputs are combined?

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.

QuestionMultiHeadAttentionWrapperMultiHeadAttention
How are heads built?By stacking complete single-head attention layersBy one multi-head projection that is split internally
Weight matricesEach head has its own weight matricesOne projection per role: W_query, W_key, W_value, each mapping d_in to d_out
Where is the split?In the separate layersIn view and transpose inside forward
Where are heads combined?Outside the individual head layersIn 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 like

    Given d_in of 8 and d_out of 16, assuming the returned tensor is (b, num_tokens, 8)

    Why it fails

    The projections map d_in to d_out, so the returned width is d_out.

    Do this instead

    Check the last axis against d_out after out_proj, not against d_in.

  • Common mistake

    Skipping the divisibility check

    What it looks like
    Setting d_out to 10 with num_heads of 4
    Why it fails

    head_dim would be 2.5, which is not a valid width, and the constructor asserts divisibility for this reason.

    Do this instead

    Choose 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 like

    Assuming that after transpose(1, 2) the token axis is still in position 1

    Why it fails

    After 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 instead

    Write 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 like

    Assuming that merging heads needs no change to the layout

    Why it fails

    The final view merges num_heads and head_dim back into d_out, and it relies on the tensor being laid out in the expected order after transpose.

    Do this instead

    Confirm the shape after the final view is (b, num_tokens, d_out) before calling out_proj.

Practice: Trace a Shape Bug

MEDIUM

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_out is divisible by num_heads, and compute head_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

  1. 1
    MultiHeadAttention uses one projection per role, W_query, W_key, and W_value, each mapping d_in to d_out.
  2. 2
    view regroups the last dimension into num_heads and head_dim, and transpose(1, 2) moves the head axis ahead of the token axis so each head is processed separately.
  3. 3
    The final view and out_proj merge the per-head results back into a single output of shape (b, num_tokens, d_out).
  4. 4
    The integrated design replaces the wrapper's stacked single-head layers with one multi-head projection that is split internally.
  5. 5
    When 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_in to d_out, and then reshapes d_out into num_heads and head_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 applying out_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.

Related concepts

More concepts starting with S