Concepts / Implementing Multi-Head Attention with Weight Splits

Implementing Multi-Head Attention with Weight Splits

The current MultiHeadAttentionWrapper processes its heads sequentially through a list comprehension in the forward method.

  • Programming

Why the Wrapper Needs Changing

Up to this point, a MultiHeadAttentionWrapper has been built by combining several single-head attention modules. Each of those modules is a complete attention unit that can process an input on its own. The wrapper gathers them together so that one call can produce the output of many heads. The exercise this article belongs to asks you to change what that combined output looks like, and the heading of the exercise points at the target: returning two-dimensional embedding vectors.

Before changing anything, it helps to be precise about how the current wrapper runs. The question this article answers is how the work gets organized inside the forward method, because that organization determines how easy it is to change the output later. The rest of the article traces the current flow, then the proposed parallel flow, then the weight-split method that makes the parallel flow possible.

Tracing the Sequential Forward Pass

The current forward method passes the input through each head in turn, using a list comprehension of the form head(x) for head in self.heads. Every head receives the same input x, but the heads are evaluated one after another. Read the code below and notice that the loop is written inside the comprehension itself. Each pass through the comprehension asks one head for its result before moving on to the next head.

python
Notes on the code
  • The heads are stored in self.heads, one attention module per head
  • The list comprehension evaluates head(x) once per head, in order
  • Every head receives the same input x
  • The parallel branch is shown only as a comment because the source describes the idea without giving the code
Before
outputs = [head(x) for head in self.heads]

Head 1 pass, then Head 2 pass, then Head 3 pass: one pass per head

After
# one matrix operation produces results for all heads together

One matrix multiplication yields the outputs for every head together

What do you think happens?

If the wrapper holds three heads and the forward method is the list comprehension above, how many separate calls into head modules happen for one input x?

Reveal answer
Answer:

Three calls, one per head, made one after another.

The comprehension evaluates head(x) once for each element of self.heads. Each call is a separate step in the sequence, which is the property the source wants to change.

The wrapper is correct in the sense that it produces the right outputs. The limitation is in how it reaches the result. Each head is a separate step, so the structure of the forward method is built around a loop over heads. That structure is what the rest of this article examines.

Computing Every Head in One Operation

The improvement the source proposes is to process the heads in parallel. The concrete route it describes is to compute the outputs for all attention heads simultaneously by way of matrix multiplication. Instead of asking each head for its result in a separate step, the computation is arranged so that a single matrix operation produces the results for every head together.

This change affects the shape of the forward pass, not the purpose of the wrapper. The wrapper still combines multiple single-head attention modules, and each head still plays the same role. What changes is that the forward method no longer needs a per-head loop to get its results. The head outputs come out of one shared computation.

Weight Splits as an Organizing Principle

The next step in the source moves from the idea of parallel computation to a specific method: implementing multi-head attention with weight splits. The name carries the main point. Rather than keeping a separate, unrelated set of weights for every head as independent objects, the weights are organized so that they can be divided into portions, one portion for each head. Those portions can then be handled together in the matrix multiplication described above.

Combined weightmatrixsix columnsHead 1 slicecolumns 0-1Head 2 slicecolumns 2-3Head 3 slicecolumns 4-5portion for head 1portion for head 2portion for head 3
Which columns of the combined weight matrix belong to each head? In this illustration, six columns are divided into three equal slices of two columns, one slice per head. The column ranges are an illustrative choice for teaching the organizing principle; the source describes the partitioning idea without giving exact column assignments.

The source presents the slices as an organizing principle. It names the method and describes its goal, but it does not walk through the details in this section. The key idea to hold onto is that a single set of weights is partitioned by head so that all heads can be computed in one pass. The partition is what lets one matrix operation serve every head.

Joining Head Outputs Back Together

Whether heads are computed one at a time or together, the wrapper must still deliver one combined result. The source's rule for modifying the output is that you change how the combined result is computed and keep the role of each head unchanged. In practice, this means each head's output is joined with the others to form the final output, while each head continues to do the same job it did before.

Input xHead 1 outputJoined outputsCombined outputHead 2 outputHead 3 outputsame inputsame inputsame inputjoinedjoinedjoinedfinal result
How do the separate head outputs combine into the final output? Each head reads the same input, produces its own output, and the outputs are joined into one result.

The joining step is the part you modify when the goal is a different output format. The small illustration below uses plain Python lists to show the idea. Each head contributes a short list for one token, and the combined output is their concatenation in head order.

python
Output (expected)
[1, 2, 3, 4, 5, 6]
Notes on the code
  • Each head produces a two-value output for one token
  • The plus operator joins the lists in head order
  • The combined list has all values from every head

Worked Example: Splitting Six Columns Across Three Heads

Worked example

Dividing one weight matrix among three heads

A combined weight matrix has six columns, and the wrapper has three heads. Show how the weight split assigns columns to heads, and how the head outputs are joined into one output for a single token.

  1. 1
    Start with the combined matrix

    The weights for all heads live in one matrix with six columns, instead of three separate matrices held by unrelated objects.

  2. 2
    Divide the columns into portions

    Split the six columns into three portions of two columns each. Portion one covers columns 0 and 1, portion two covers columns 2 and 3, and portion three covers columns 4 and 5. Each portion belongs to one head.

  3. 3
    Compute all heads together

    Because the portions come from one matrix, the outputs for all three heads can be produced in a single matrix operation rather than in three separate calls.

  4. 4
    Collect each head's output

    Suppose the three heads produce [1, 2], [3, 4], and [5, 6] for one token. Each head's output keeps its own role.

  5. 5
    Join in head order

    Concatenating the outputs in head order gives [1, 2, 3, 4, 5, 6]. The combined vector is six values long, made from three two-value head outputs.

Result

The six columns are divided into three per-head portions, all heads are produced by one shared computation, and the three two-value head outputs are joined into one six-value output in head order.

Mistakes When Modifying the Wrapper Output

  • Common mistake

    Changing what a head computes when you only meant to change the output format

    What it looks like

    Editing the attention calculation inside each head so it returns a different quantity, then wondering why the wrapper's results no longer match the single-head modules.

    Why it fails

    The source's rule is to change how the combined result is computed and keep the role of each head unchanged.

    Do this instead

    Leave each head's computation alone and modify only the step that joins the head outputs into the final result.

  • Common mistake

    Joining head outputs along the wrong dimension

    What it looks like

    Stacking the head outputs into a new structure when the goal was one combined vector per token.

    Why it fails

    The combined output should be one result built from the head outputs, and the join has to match the layout the wrapper is supposed to return.

    Do this instead

    Decide which dimension the heads are joined along before editing, then check that the combined output has the shape the exercise asks for.

  • Common mistake

    Assuming the parallel version still uses a per-head loop

    What it looks like

    Keeping the list comprehension in place and adding a matrix multiplication beside it, so every head is still computed twice.

    Why it fails

    The point of computing all heads with matrix multiplication is that a single operation produces the results for every head together.

    Do this instead

    Replace the per-head evaluation with one shared computation, and split the weights so that each head's portion can be read out of the result.

  • Common mistake

    Treating the weight split as separate, unrelated matrices

    What it looks like

    Creating an independent weight matrix for each head and then calling them one by one.

    Why it fails

    The weight-split method organizes the weights so they can be divided among heads and handled together. Independent matrices keep the per-head structure the method is meant to remove.

    Do this instead

    Organize the weights as one combined set divided into per-head portions.

  • Common mistake

    Forgetting that every head receives the same input x when rewriting the forward method

Practice: Tracing the Head Joins

EASY

A wrapper has three heads. For one token, head 1 produces [7, 8], head 2 produces [9, 10], and head 3 produces [11, 12]. Write the combined output for that token, and state how many separate head evaluations a sequential forward method would perform for one input.

Hints
  • Join the outputs in head order, the same way the earlier list example did.
  • A sequential forward method evaluates each head once for the input, one head after another.

Key Takeaways

  • The current MultiHeadAttentionWrapper processes its heads sequentially, using a list comprehension of the form head(x) for head in self.heads, so each head is evaluated in a separate step.
  • The proposed improvement computes the outputs for all attention heads simultaneously by way of matrix multiplication, so one operation produces results for every head together.
  • Implementing multi-head attention with weight splits organizes the weights as one set divided into portions, one per head, so that all heads can be handled together.
  • When modifying the wrapper's output, change how the combined result is computed and keep the role of each head unchanged.

Related concepts

More concepts starting with I