Causal Attention and Language Modeling
Two self-attention implementations can be made equivalent by assigning the weights from one instance to the other, with attention to the transposed storage form used by nn.Linear.
Two Versions, One Operation
When you write a self-attention module from scratch, you often end up with more than one version of it. An early version might store its trainable projection weights in one layout, and a later, cleaner version might organize the same mathematical operation differently. Both versions can compute the same thing while looking quite different in code.
This raises a practical question. If you have a trained or carefully set-up instance of one version, how do you move its weights into an instance of the other version so that both produce the same outputs? The answer depends on matching the right weight matrices to each other and on handling one detail of how PyTorch stores linear layer weights. This article works through that transfer, then moves on to the two additions that turn basic self-attention into causal attention for language modeling: a causal mask and multiple heads.
Matching Behavior Rather Than Appearance
The goal of the transfer is not to make two classes look alike. The goal is to make them behave alike. Two classes can have different internal structure, different attribute names, and different ways of storing their parameters, and still be equivalent if they compute the same function of their input.
Behavior is what gets tested. Once the weights are assigned correctly, you feed the same input into each instance and compare the outputs. If the outputs match, the transfer worked. That shared output is the check that confirms the two instances are equivalent for that input.
Copying Weights Between Instances
The transfer runs from an instance of the second version, called SelfAttention_v2, into an instance of the first version, called SelfAttention_v1. Each class holds trainable weight matrices for its query, key, and value projections. Copying weights therefore comes down to two steps: identify which matrix in one class corresponds to which matrix in the other, and then assign values pairwise, one matrix at a time.
- Both classes are assumed to be defined with the same input and output dimensions.
- Each of the three projection matrices is copied separately, matching query to query, key to key, and value to value.
- The .T on each source weight is the transposition step explained in the next section.
Notice that the assignment is pairwise. The query matrix of one class is matched to the query matrix of the other, and the same holds for keys and values. Mixing them up would still produce tensors of compatible shape in some cases, but the instances would then compute different attention scores and disagree on their outputs.
After the three assignments, run the same input through both instances and compare the outputs. Identical results mean the parameters now define the same function, which is what makes the two implementations equivalent.
Transposed Storage in nn.Linear
The step that most often causes trouble is the storage layout. nn.Linear stores its weight matrix in a transposed form. If you write a weight matrix by hand, you would normally give it rows for input features and columns for output features. Inside nn.Linear the orientation is the other way around, with rows for output features and columns for input features.
This is why the transfer must account for the transpose. The weight of the second instance has to be transposed before it is assigned into the first instance's matrix, so that each value lands in the orientation the first class expects. The code in the previous section applies this with .T.
Transfer Mistakes to Avoid
Most failed transfers come from a small set of errors. Each one is easy to make and easy to miss until the outputs are compared.
Common mistake Assigning an nn.Linear weight directly without transposing it
What it looks likesa_v1.W_query = nn.Parameter(sa_v2.W_query.weight)Why it failsnn.Linear stores its weight in the transposed orientation, so the values end up arranged the wrong way for the first class.
Do this insteadApply .T to the source weight before assigning it, as in sa_v1.W_query = nn.Parameter(sa_v2.W_query.weight.T).
Common mistake Trusting a matching shape as proof that the transfer is correct
Why it failsCompatible shapes do not guarantee compatible orientation, so a wrong assignment can pass every shape check.
Do this insteadRun the same input through both instances and confirm that the outputs match.
Common mistake Matching the wrong pair of matrices
Why it failsCopying the key weights into the query slot produces tensors of the right size but computes different attention scores.
Do this insteadMap each matrix pairwise by its role: query to query, key to key, value to value.
Why Causal Attention Hides Future Tokens
The next enhancement adds a causal aspect to self-attention. Causal attention modifies the mechanism so that the model cannot access future information in the sequence. This matters for language modeling, where each prediction should depend only on the words that came before it.
Consider why this restriction is needed. If a model could peek at upcoming words while it predicts the current one, it would learn a shortcut that does not exist at generation time. During training it would appear to predict well because it could see the answer, and then fail once it had to produce text one token at a time.
In GPT-style models each token is processed in turn. For the current token, the tokens that come after it in the input are masked out. The row for a given token therefore only distributes its attention over itself and the tokens before it.
Building the Masked Weight Matrix
Concretely, the attention weights above the diagonal of the attention matrix are masked out, and the remaining weights in each row are normalized so that they sum to 1. Each row is a valid distribution over the tokens that row is allowed to see. The diagram below shows which entries survive for a three-token sequence.
The masked attention weight matrix can be built in three stages, in this order. First, apply the softmax function to the attention scores. Second, zero out the elements above the diagonal so that future positions contribute nothing. Third, normalize the resulting matrix row by row so that each row again sums to 1.
What do you think happens?
After the masking and normalizing stages, how much weight does row 1 give to token 1, the only position it can see?
Reveal answer
1.0
Row 1 has only one visible position, which is itself. After zeroing the entries above the diagonal, that single entry is normalized to the full sum of 1, so it receives the whole weight of 1.0.
Masking and normalizing the second row
Suppose the softmax of token 2's scores over three tokens gives the row [0.3, 0.5, 0.2]. Token 2 may see token 1 and itself, but not token 3. Build its masked, normalized row.
- 1Stage 1: softmax
The softmax has already been applied, giving [0.3, 0.5, 0.2]. These values sum to 1.0 before masking.
- 2Stage 2: zero above the diagonal
Token 2 is in position 2, so the entry for token 3 lies above the diagonal and is zeroed. The row becomes [0.3, 0.5, 0.0].
- 3Check the sum
The row now sums to 0.8, not 1. This is why a normalization stage is still needed.
- 4Stage 3: normalize the row
Divide each remaining entry by 0.8. The first entry is 0.3 divided by 0.8, which is 0.375. The second is 0.5 divided by 0.8, which is 0.625. The masked entry stays at 0.0.
The masked, normalized row is [0.375, 0.625, 0.0]. It sums to 1 and gives no weight to the future token 3.
Why Normalization Comes Last
The ordering of the three stages matters. Zeroing the upper triangle after the softmax leaves rows that no longer sum to 1. That is why the normalization stage follows the masking stage. Right after the zeroing, the masked matrix is unnormalized, and it becomes a valid set of distributions only at the end.
Several Heads Over One Embedding
The third enhancement is the multi-head component. Here the attention mechanism is split into several heads. Each head learns different aspects of the data, and the heads together let the model attend to information from different representation subspaces at different positions at the same time. The source presents this as a way to improve performance on complex tasks.
A practical GPT-like model uses both enhancements together. The mask controls which positions a token may see, while the heads let it look at those positions in several different ways. Causal masking and multi-head splitting are therefore presented as complementary parts of the same model rather than as alternatives.
Mask a Three-Token Row by Hand
For a three-token sequence, token 2's softmax row is [0.2, 0.3, 0.5]. Build token 2's masked, normalized row, where token 2 may see only token 1 and itself. Show the row after zeroing and after normalizing.
Hints
- Token 2 is in position 2, so the entry for token 3 is above the diagonal and must be zeroed.
- After zeroing, the row is [0.2, 0.3, 0.0]. Add up its entries before normalizing.
- The sum is 0.5. Divide each remaining entry by 0.5 to finish. The answer is [0.4, 0.6, 0.0].
Key Takeaways
- Two self-attention implementations can be made equivalent by assigning the trainable weights of one instance to the other, matching query to query, key to key, and value to value.
- nn.Linear stores its weight in a transposed form, so the weight must be transposed before it is assigned, or the two instances will disagree even when shapes fit.
- A causal mask hides future tokens. The masked attention matrix is built by applying softmax, zeroing the entries above the diagonal, and then normalizing each row so that it sums to 1.
- Multi-head attention splits the attention mechanism into several heads that attend to the same sequence in parallel, and a practical GPT-like model combines causal masking with multiple heads.
- Confirm any weight transfer by running the same input through both instances and comparing their outputs.