Implementing Multi-Head Attention with Weight Splits
The current MultiHeadAttentionWrapper processes its heads sequentially through a list comprehension in the forward method.
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.
- 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
outputs = [head(x) for head in self.heads]Head 1 pass, then Head 2 pass, then Head 3 pass: one pass per head
# one matrix operation produces results for all heads togetherOne 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
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.
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.
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.
[1, 2, 3, 4, 5, 6]- 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
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.
- 1Start 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.
- 2Divide 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.
- 3Compute 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.
- 4Collect 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.
- 5Join 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.
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 likeEditing 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 failsThe source's rule is to change how the combined result is computed and keep the role of each head unchanged.
Do this insteadLeave 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 likeStacking the head outputs into a new structure when the goal was one combined vector per token.
Why it failsThe 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 insteadDecide 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 likeKeeping the list comprehension in place and adding a matrix multiplication beside it, so every head is still computed twice.
Why it failsThe point of computing all heads with matrix multiplication is that a single operation produces the results for every head together.
Do this insteadReplace 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 likeCreating an independent weight matrix for each head and then calling them one by one.
Why it failsThe 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 insteadOrganize 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
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 inself.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.