Concepts / Causal Attention and Language Modeling

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.

  • Programming

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.

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

Rows: input featuresRows: output featuresColumns: outputfeaturesColumns: inputfeatures
The same weight matrix, seen as the layout you would write by hand and as the layout nn.Linear holds internally. The two orientations are transposes of each other, so the input and output roles swap between rows and columns.

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 like
    sa_v1.W_query = nn.Parameter(sa_v2.W_query.weight)
    Why it fails

    nn.Linear stores its weight in the transposed orientation, so the values end up arranged the wrong way for the first class.

    Do this instead

    Apply .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 fails

    Compatible shapes do not guarantee compatible orientation, so a wrong assignment can pass every shape check.

    Do this instead

    Run the same input through both instances and confirm that the outputs match.

  • Common mistake

    Matching the wrong pair of matrices

    Why it fails

    Copying the key weights into the query slot produces tensors of the right size but computes different attention scores.

    Do this instead

    Map 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.

token 1 to token 11.0token 1 to token 2masked, 0token 1 to token 3masked, 0token 2 to token 10.375token 2 to token 20.625token 2 to token 3masked, 0token 3 to token 1visible sharetoken 3 to token 2visible sharetoken 3 to token 3visible share
Each cell is one query token attending to one key token. Entries above the diagonal are masked to 0. Within each row, the visible entries are normalized so that they sum to 1. The first row keeps only itself, so it receives the full weight of 1.0. The second row's values are illustrative, produced by the worked example below.

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?

  • 0.0

  • 0.5

  • 1.0

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

Worked example

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.

  1. 1
    Stage 1: softmax

    The softmax has already been applied, giving [0.3, 0.5, 0.2]. These values sum to 1.0 before masking.

  2. 2
    Stage 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].

  3. 3
    Check the sum

    The row now sums to 0.8, not 1. This is why a normalization stage is still needed.

  4. 4
    Stage 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.

Result

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.

Multi-headattentionone mechanismHead 1its own attentionHead 2its own attentionHead Nits own attentionsplits intosplits intosplits into
The multi-head attention mechanism is divided into several heads. Each head is a separate attention computation that learns its own aspect of the data, and all heads run at the same time over the same sequence.

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

MEDIUM

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.

More concepts starting with C