Build a GPT forward pass
Now the pieces go together. A GPT takes a row of token IDs and returns, for every position, one score for each token in the vocabulary: its guess at what comes next. Inside, three things happen. Each ID becomes a vector that says which token it is and where it stands. Transformer blocks let each position gather information from the positions before it. A final linear layer turns each position's vector into vocabulary scores.
Learn it
Words to know
- embedding
- A list of numbers that stands for a token. The model learns these numbers during training.
- positional encoding
- Numbers added to a token's embedding to say where in the sequence the token stands.
- causal mask
- The rule that a token may attend only to itself and earlier tokens, never to later ones.
- logit
- A raw score a model gives to one choice, before the scores are turned into probabilities.
How it works
- Two embedding tables of width
d: one with a row per token, one with a row per position. Look up both and add them. Shape:[1, length, d]. - Build the causal mask: a
lengthbylengthtable that isTrueabove the diagonal.Truemeans this position may not look there. - Pass the vectors and the mask through one transformer block:
torch.nn.TransformerEncoderLayer(d, 2, dim_feedforward=2 * d, dropout=0, batch_first=True). The shape stays[1, length, d]. - A linear layer from
dtovocabulary_sizegives the scores. Final shape:[1, length, vocabulary_size]. - Check the mask: run a row of zeros, then the same row with its last token changed to 1. No earlier position can see the last token, so their scores must come out the same.
Worked example
Input vocabulary_size = 5 length = 3 d = 4 Output [[1, 3, 5], True]
| Step | Result |
|---|---|
| Token IDs | shape [1, 3] |
| Token embeddings | shape [1, 3, 4] |
| Position embeddings | shape [3, 4] |
| Their sum | shape [1, 3, 4] |
| After the block | shape [1, 3, 4] |
| After the linear layer | shape [1, 3, 5] |
| Last token changed from 0 to 1 | scores at positions 0 and 1 stay the same |
One sequence, 3 positions, and 5 scores at each position. The position embeddings have no batch dimension; PyTorch adds the same rows to every sequence in the batch.
Why it matters
tiny_gpt.py has this forward pass with two changes: its block normalizes before each step, not after (norm_first=True), and one more normalization comes before the last layer. Its self-test runs the same check for looking ahead.
Your turn
Using PyTorch, build a one-block GPT. Return the shape of its output, and whether the scores at earlier positions stay the same when only the last token changes.
vocabulary_size- the number of tokens the model knows; at least 2
length- the number of positions in the sequence
d- the width of the embeddings; positive and even
- Returns
- The list
[shape, unchanged]: the shape of the logits as a list, andTrueif a row of zeros and the same row with its last token set to 1 give equal logits at every position before the last.
This lesson needs PyTorch, which does not run in the browser. The app gives you the exercise as a file with its tests, to run on your own computer.
Example 1
Input vocabulary_size = 5 length = 3 d = 4 Output [[1, 3, 5], True]
One sequence of 3 positions with 5 scores at each. Changing the last token leaves positions 0 and 1 as they were.
Example 2
Input vocabulary_size = 6 length = 4 d = 4 Output [[1, 4, 6], True]
Four positions and a vocabulary of 6 give [1, 4, 6], and the first three positions do not see the change.
Example 3
Input vocabulary_size = 7 length = 1 d = 8 Output [[1, 1, 7], True]
One position and a vocabulary of 7 give [1, 1, 7]. There is no earlier position to compare, so the check is true. The width 8 is used inside and does not appear in the output.
Hints
Each hint gives away a little more. Open one only when you need it.
Hint 1: Where to start
You need four modules:
torch.nn.Embedding(vocabulary_size, d)for tokens,torch.nn.Embedding(length, d)for positions, aTransformerEncoderLayer, andtorch.nn.Linear(d, vocabulary_size). Create them first, then pass the data through in that order.Hint 2: The key step
The positions are
torch.arange(length), the numbers 0 tolength - 1. The mask istorch.triu(torch.ones(length, length, dtype=torch.bool), diagonal=1). Put the forward pass in a small function, because you run it twice.Hint 3: The outline
Define
forward(ids): addtoken(ids)andposition(torch.arange(length)), runblock(x, src_mask=mask), thenhead(...). Makefirst = torch.zeros((1, length), dtype=torch.long)andsecond = first.clone()withsecond[0, -1] = 1. For their outputsaandb, return[list(a.shape), torch.allclose(a[:, :-1], b[:, :-1], atol=1e-6)].
Solution
From here on the page gives the method away. If you are still solving, go back to the hints.
Step by step
- The four parts of the model.
batch_first=Truemakes the block expect[batch, time, width].dropout=0turns off the random zeroing used in training. PyTorch calls this layer an encoder; the causal mask is what makes it a GPT block. - True above the diagonal: no looking at later positions.
triuwithdiagonal=1keeps the part above the diagonal. RowiisTruein the columns afteri. - Which token it is, plus where it stands: [1, length, d].
torch.arange(length)is the list of positions. Its embeddings have shape[length, d]and are added to every sequence in the batch. - Block, then one score per vocabulary token at each position. The block keeps the shape. The linear layer changes the last size from
dtovocabulary_size. - Change only the last token: earlier scores must stay the same.
a[:, :-1]is every position but the last.allclosecompares the two with a small tolerance and returns a plainTrueorFalse.
The same numbered steps are the comments in the solution.
Python
import torch
def solve(vocabulary_size, length, d):
# 1. The four parts of the model
token = torch.nn.Embedding(vocabulary_size, d)
position = torch.nn.Embedding(length, d)
block = torch.nn.TransformerEncoderLayer(
d, 2, dim_feedforward=2 * d, dropout=0, batch_first=True
)
head = torch.nn.Linear(d, vocabulary_size)
# 2. True above the diagonal: no looking at later positions
mask = torch.triu(torch.ones(length, length, dtype=torch.bool), diagonal=1)
def forward(ids):
# 3. Which token it is, plus where it stands: [1, length, d]
x = token(ids) + position(torch.arange(length))
# 4. Block, then one score per vocabulary token at each position
return head(block(x, src_mask=mask))
# 5. Change only the last token: earlier scores must stay the same
first = torch.zeros((1, length), dtype=torch.long)
second = first.clone()
second[0, -1] = 1
a, b = forward(first), forward(second)
return [list(a.shape), torch.allclose(a[:, :-1], b[:, :-1], atol=1e-6)]Watch out
- Embedding indexes must be whole numbers:
dtype=torch.long. A float tensor of IDs raises an error. - In this mask
Truemeans blocked. Withtrilevery position sees the tokens after it, and the check comes backFalse.
Check yourself
What is the shape of a GPT's output, and what does each size mean?
[batch, time, vocabulary]: for each sequence and each position, one score for every token that could come next.
How can you test that a model does not look ahead?
Change a later token and compare the scores at earlier positions. With a correct causal mask they do not change.