Batching Input Data for Language Models
CausalAttention extends self-attention with a causal mask and a dropout layer, and it accepts batched inputs of shape (batch, tokens, embedding).
Why Language Models Process Batches
Up to this point, attention has been applied to a single input text. A language model rarely works that way. Training pipelines group many texts together so the hardware can process them in one pass, and the attention code has to accept that grouped input without modification. The class in this article is built with that requirement in mind. It extends the self-attention class from the earlier section by adding two things: a causal mask that hides future tokens, and a dropout layer applied to the attention weights.
What do you think happens?
The batch holds two inputs of six tokens each, every token is a three-dimensional vector, and the layer is built with an output dimension of two. What shape does the result of the attention call have?
Reveal answer
(2, 6, 2)
The output keeps the batch and token dimensions and replaces the embedding dimension with the output dimension of two. You can confirm this by tracing every tensor through the forward method, which the next sections do step by step.
From One Text to a Batch of Two
To test batch handling without a real data loader, the source duplicates one tokenized input text and stacks the two copies along a new first dimension. The result is a three-dimensional tensor: two inputs, six tokens each, and every token is a three-dimensional embedding vector. Those three numbers are the key to every shape question later in this article, so it helps to know what each one indexes.
torch.Size([2, 6, 3])- inputs is one tokenized text with shape (6, 3): six tokens, each a three-dimensional vector
- stack with dim=0 creates a new first dimension holding the two copies
- the first number indexes the sequence in the batch, not a token
Read the shape from left to right. The leading 2 says how many sequences travel together. The 6 says how many token positions each sequence has. The trailing 3 is the embedding size, meaning the number of values in each token's vector. The attention code must keep the first two dimensions meaningful throughout, because the scores are computed for every pair of tokens within each sequence.
Anatomy of the CausalAttention Class
Read the class before running anything. Most lines look familiar from the self-attention version: three linear projections produce queries, keys, and values, and the forward method computes scores, normalizes them, and mixes the values. The two additions are the dropout layer created in the constructor and the mask registered with register_buffer. Each addition appears in a different place in the forward method, so the code itself is the best map of where things happen.
- The constructor creates the three projections, the dropout layer, and the mask buffer
- The mask is built from torch.triu applied to a square matrix of ones
keys.transpose(1, 2)swaps the token and embedding dimensions while the batch stays in position 0- masked_fill_ writes -inf into the blocked score positions in place
- Dropout is applied to the normalized attention weights, not to the raw scores
The transpose deserves a closer look. Keys are transposed across dimensions 1 and 2 while the batch dimension stays in position 0. That single arrangement lets one matrix multiplication compute scores for every input in the batch at once, rather than looping over the inputs one at a time.
Tracing Shapes Through the Forward Pass
The shape prediction from the opening question works because every tensor can be followed through the forward method. Start with the input of shape 2 by 6 by 3. The three linear projections each map the embedding dimension from 3 to 2, so queries, keys, and values all have shape 2 by 6 by 2. Multiplying queries by the transposed keys produces a score for every pair of tokens within each sequence, giving a 2 by 6 by 6 matrix. The mask, the scaling, and the softmax all operate on that square matrix without changing its shape. Finally, multiplying the weights by the values produces context vectors of shape 2 by 6 by 2.
Tracing the shape of a batch of two
Trace the shape of a batch of shape (2, 6, 3) through CausalAttention with d_in = 3 and d_out = 2.
- 1Input
x has shape (2, 6, 3): two sequences, six tokens, three values per token.
- 2Projections
W_query, W_key, and W_value each map 3 to 2, so queries, keys, and values have shape (2, 6, 2).
- 3Transposed keys
keys.transpose(1, 2)has shape (2, 2, 6). The batch stays first and the token and embedding dimensions swap. - 4Scores
queries @ keys transposed gives (2, 6, 6): one score for every pair of tokens within each sequence.
- 5Mask, scale, softmax
Blocked positions are set to -inf, the scores are scaled, and softmax runs over the last dimension. The shape stays (2, 6, 6).
- 6Dropout
Dropout zeroes some entries of the weights and keeps the shape at (2, 6, 6).
- 7Context vectors
weights @ values gives (2, 6, 2). The embedding dimension is now the output dimension.
The output shape is (2, 6, 2).
Tracing shapes is also the fastest way to locate a bug. If the printed shape differs from the one you traced, the error is in a step where the last dimension or the token dimension changed unexpectedly. A wrong output dimension usually points to the projection layers, while a wrong square score matrix points to the transpose or to a mismatch between the query and key shapes.
The Causal Mask and Future Tokens
A causal mask enforces a simple rule: a token may attend to itself and to earlier tokens, but never to tokens that come later in the sequence. This restriction prevents data leakage in autoregressive models, which generate text one token at a time and must not peek at the words they have not yet produced.
The class builds the mask with torch.triu applied to a square matrix of ones. Passing diagonal=1 zeroes the main diagonal and everything below it, so the surviving ones sit strictly above the diagonal and mark exactly the future positions. Read the mask row for query token 2 above: keys 0, 1, and 2 are earlier or current tokens and remain visible, while keys 3, 4, and 5 lie in the future and are blocked.
Before softmax, blocked positions are filled with -inf. Because the exponential of negative infinity is zero, those positions receive zero weight after normalization, and the remaining weights in each row still sum to one. Masking happens on the scores, so the mask does not change the shape of the score matrix.
tensor([[0., 1., 1., 1., 1., 1.],
[0., 0., 1., 1., 1., 1.],
[0., 0., 0., 1., 1., 1.],
[0., 0., 0., 0., 1., 1.],
[0., 0., 0., 0., 0., 1.],
[0., 0., 0., 0., 0., 0.]])- A 1 marks a future position that will be blocked
- Row i shows which keys query token i must ignore
Why the Mask Is a Registered Buffer
The mask is a fixed tensor. It holds no learnable weights, so it is not a model parameter. It still has to live on the same device as the model's parameters, because the masked fill operates on scores that are computed on that device. The register_buffer call exists for this case. A buffer is stored with the model but is not treated as a trainable parameter, and the source notes that buffers move automatically to the appropriate device, CPU or GPU, together with the model.
| Property | Mask stored as a buffer | Learnable weight (parameter) |
|---|---|---|
| Holds learnable weights | No | Yes |
| Treated as a trainable parameter | No | Yes |
| Moves with the model to CPU or GPU | Yes, automatically | Moved together with the model |
Where the mask differs from a trainable weight, based on the source's description of register_buffer
Dropout After the Softmax
Dropout is a regularization technique that randomly sets a fraction of tensor elements to zero, which helps prevent overfitting. In this class it is created as nn.Dropout(dropout) in the constructor and applied to the attention weights after the softmax. The source applies dropout to the normalized weights rather than to the raw scores or the input embeddings, so each entry in the weight matrix can be zeroed independently during training.
The order of operations is the important part. Masking comes first, so blocked future positions are already -inf and get zero weight from the softmax. Softmax then normalizes each row. Dropout comes last and only perturbs the finished weights. The context vectors are produced from those dropped-out weights and the values, and the shape is still (2, 6, 2) at the end.
Mistakes When Reading Batched Attention Code
Common mistake Treating the first dimension as a token index
What it looks likeReading shape (2, 6, 3) as six sequences of three tokens
Why it failsThe leading dimension indexes the batch. The 6 is the number of tokens in each sequence.
Do this insteadName the dimensions as (batch, tokens, embedding) and keep that order in every shape check.
Common mistake Expecting the mask to change the shape of the scores
What it looks likeAssuming masking turns the (2, 6, 6) scores into a smaller matrix
Why it failsMasking only writes -inf into blocked positions. The mask, scaling, and softmax all keep the square score matrix at the same shape.
Do this insteadTrack the shape across masking and check that only the values, not the dimensions, have changed.
Common mistake Moving the mask to the device by hand
Why it failsManual moves are where device mismatch errors tend to appear once the class is part of a larger model.
Do this insteadRegister the mask with register_buffer so it follows the model to CPU or GPU automatically.
Common mistake Applying dropout to the raw scores
Why it failsThe source applies dropout to the normalized weights after the softmax, so each weight can be zeroed independently.
Do this insteadKeep dropout after the softmax, directly on attn_weights.
Common mistake Slicing the mask with the wrong length, which misaligns the blocked positions with the actual tokens
Predict and Verify the Output Shape
torch.Size([2, 6, 2])- The class is built with
d_in= 3 andd_out= 2 - The batch from earlier is passed through the layer in one call
- The printed shape should match the trace you wrote before running the code
Before running the code, write down the shape you expect from ca(batch) when the batch is (2, 6, 3) and d_out is 2. Then change the batch to three inputs and predict the new output shape. Compare your answers with the printed shapes, and if they differ, trace the forward pass again to find the step where a dimension changed unexpectedly.
Hints
- Only the embedding dimension changes from 3 to
d_out - The batch and token dimensions pass through the layer unchanged
- With three inputs the expected output is (3, 6, 2)
Key Takeaways
- CausalAttention extends self-attention with a causal mask and a dropout layer, and it accepts batched inputs of shape (batch, tokens, embedding).
- A causal mask built with torch.triu and diagonal=1 marks future positions, and masked_fill_ writes -inf into them before the softmax so that tokens cannot attend to later tokens.
- The mask is stored with
register_bufferso it moves to the correct device automatically with the model, and it is not a trainable parameter. - Dropout is applied to the normalized attention weights after the softmax, and tracing tensor shapes through the forward pass is the most reliable way to locate a bug.