Transformer, explained · Part 1 of 7 · Covers Title, Abstract, §1, §2
The Big Idea: Attention Is All You Need
Why the best translation models of 2016 were slow to train, and the claim that attention alone can replace them. We read the title, the abstract and the first two sections, and we run a released Transformer that translates English into German.
Attention Is All You Need. Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Łukasz Kaiser, Illia Polosukhin. NeurIPS 2017, 2017. arXiv:1706.03762
In June 2017, a team from Google put a short paper on arXiv. It described a translation model that reads a whole sentence in one step. The best models of the time read it one word after the other. The model is the Transformer, and its design now runs almost every large language model.
Each piece below shows the exact lines of the paper, then explains them.
The title
The abstract
A released Transformer, translating one sentence
The paper's own weights are not on the public model hubs. So we use a released model with the same shapes as the paper's base model: Helsinki-NLP/opus-mt-en-de, a Transformer (MarianMT) that translates English into German. Our script loads it and translates one sentence:
== 1. A released Transformer: Helsinki-NLP/opus-mt-en-de
encoder layers 6, decoder layers 6, d_model 512, heads 8, d_ff 2048, vocabulary 58,101
parameters: 74,410,496
English: The dog sat on the mat because it was tired.
source tokens (12): ['▁The', '▁dog', '▁sat', '▁on', '▁the', '▁mat', '▁because', '▁it', '▁was', '▁tired', '.', '</s>']
German : Der Hund saß auf der Matte, weil er müde war.
output tokens (15): ['<pad>', '▁Der', '▁Hund', '▁saß', '▁auf', '▁der', '▁Matt', 'e', ',', '▁weil', '▁er', '▁müde', '▁war', '.', '</s>']The shapes match the base model of the paper. It has 6 encoder layers and 6 decoder layers, with vectors of 512 numbers. It has 8 attention heads and 2,048 numbers inside each feed-forward layer. Part 2 explains each of these numbers. The English sentence becomes 12 tokens. The mark "▁" means "a space comes before this token", and </s> marks the end of the sentence.
The translation is correct, and one detail shows real understanding. In German, "Hund" (dog) is masculine and "Matte" (mat) is feminine. So "it" becomes "er" for the dog and "sie" for the mat. The model wrote "er": it linked "it" with "dog", six words earlier, and not with the closer "mat". This kind of link between distant words is what the rest of this page is about.
This model is not the paper's model. Marian was trained later, on different data, with its own vocabulary. It shows the design and the shapes, not the paper's BLEU scores.
The problem with recurrence
The paper writes the recurrence in words. In symbols, it is this:
Read it from left to right. is the hidden state after word , a vector. is the hidden state after the word before. is the vector of word . is a function with learned weights, the same function at every step. The arrow of time is inside the formula: you cannot compute before you have .
The most basic recurrent network uses this :
Here and are learned matrices, and squeezes each number into the range from -1 to 1. To see the problem with long sentences, shrink everything to one number. Let , and . The first word has the value 1, and every later word has the value 0. Our script follows the trace of the first word:
== 2. The recurrence h_t = tanh(w * h_(t-1) + u * x_t), with w = 0.5, u = 1.0, h_0 = 0
t=1: x=1 h = tanh(0.5 x 0.0000 + 1.0 x 1) = tanh(1.0000) = 0.7616
t=2: x=0 h = tanh(0.5 x 0.7616 + 1.0 x 0) = tanh(0.3808) = 0.3634
t=3: x=0 h = tanh(0.5 x 0.3634 + 1.0 x 0) = tanh(0.1817) = 0.1797
t=4: x=0 h = tanh(0.5 x 0.1797 + 1.0 x 0) = tanh(0.0899) = 0.0896
t=5: x=0 h = tanh(0.5 x 0.0896 + 1.0 x 0) = tanh(0.0448) = 0.0448
t=6: x=0 h = tanh(0.5 x 0.0448 + 1.0 x 0) = tanh(0.0224) = 0.0224
the trace of word 1 shrinks by about half at each step: it must pass through every later stateCheck the first line by hand: , and . At each later step, the old value is multiplied by 0.5, so the trace of word 1 halves. After five more words, only 0.0224 is left. Real networks have learned weights and gates, and the LSTM was designed to slow this fading. But two facts stay: word 1 reaches word 6 only through all the states between them, and the six steps must run in order.
The loop and the matrix product, in code
The figure is the whole argument of §1, and it fits in two short functions. The first is a recurrent layer, our Method 1. Read the for line: step t uses h, and h is the result of step t - 1. There is no way to start step 5 before step 4 is done.
def rnn_layer(X, W, U, b):
"""Method 1: a recurrent layer. X is (batch, n, d). Step t cannot start before step t - 1 ends."""
B, n, d = X.shape
h = torch.zeros(B, d) # h_0
states = []
for t in range(n): # n steps, one after the other
h = torch.tanh(X[:, t] @ W.T + h @ U.T + b) # (B, d): needs the h of the step before
states.append(h)
return torch.stack(states, 1) # (B, n, d)The second is self-attention in its most basic form, with no learned weights: each word is its own query, key and value. Part 3 adds the learned matrices and the heads. Here we only need the shape of the computation. There is no loop over words. One matrix product compares every word with every word, a softmax turns each row into weights, and a second product mixes the words.
def attention_layer(X):
"""Method 1: self-attention with no learned weights, Q = K = V = X. All n rows in one go."""
d = X.shape[-1]
scores = X @ X.transpose(1, 2) / math.sqrt(d) # (B, n, n): every word against every word
weights = torch.softmax(scores, dim=-1) # each row adds up to 1
return weights @ X # (B, n, d): a weighted mix for every word at onceFollow the shapes for a batch of 32 sentences of 12 words, each word a vector of 512 numbers. X has the shape (32, 12, 512). X.transpose(1, 2) swaps the last two axes, so it has the shape (32, 512, 12). Their product has the shape (32, 12, 12): for each sentence, a table of 12 × 12 scores. The division by keeps the scores in a useful range; Part 3 explains it.
Our Method 2 is the library: nn.RNN for the loop, with the same weights, and F.scaled_dot_product_attention, the attention function that PyTorch ships. We also run two small tests. First, we compute row 7 of the attention output alone. Second, we change only word 3 and measure how much each output moves.
# Each attention row needs only X, not the other outputs: compute row 7 alone.
with torch.no_grad():
row7 = torch.softmax(X[:, 7:8] @ X.transpose(1, 2) / math.sqrt(d), -1) @ X # (B, 1, d)
log(f' attention row 7 computed alone, from X only: max |row 7 alone - row 7 of the full product| = {diff(row7[:, 0], A_ours[:, 7]):.1e}')
# Change word 3 only, and see which outputs move. Small word vectors keep the attention weights spread out.
with torch.no_grad():
Xs = 0.05 * torch.randn(B, n, d) # (B, 12, 512)
Xs2 = Xs.clone(); Xs2[:, 3] += 0.05 # the same sentence, word 3 changed
move_rnn = (rnn_layer(Xs2, W, U, b) - rnn_layer(Xs, W, U, b)).abs().amax(dim=(0, 2)) # (12,): one number per output
move_att = (attention_layer(Xs2) - attention_layer(Xs)).abs().amax(dim=(0, 2))== 1. Sequential against parallel (width d = 512, batch 32)
X (32, 12, 512)
RNN loop: 12 steps in order -> (32, 12, 512); max |ours - nn.RNN| = 8.3e-07
attention: 1 product X X^T (32, 12, 12), softmax, 1 product with X -> (32, 12, 512); max |ours - F.scaled_dot_product_attention| = 1.2e-07
attention row 7 computed alone, from X only: max |row 7 alone - row 7 of the full product| = 0.0e+00
change word 3 only; largest change of each output 0..11:
RNN 0.000 0.000 0.000 0.089 0.053 0.031 0.016 0.013 0.005 0.003 0.002 0.001
attention 0.004 0.004 0.004 0.006 0.004 0.004 0.004 0.004 0.004 0.004 0.004 0.004Both of our functions agree with the library to less than one millionth. Now read the last four lines, because they are the point of §1.
Row 7 of attention, computed alone, is exactly row 7 of the full product. It needs only the inputs X, not any other output. So the 12 rows can go to 12 groups of GPU cores at the same time. The RNN has no such row: needs , which needs , and so on.
The change test shows the two kinds of path. In the RNN, outputs 0 to 2 do not move at all, because they were computed before word 3. Output 3 moves by 0.089, and then the change fades along the chain: 0.053, 0.031, and only 0.001 at output 11. That is the fading trace of the Background page, in a real layer of 512 numbers.
In attention, every output moves, even outputs 0, 1 and 2, which come before word 3. The change reaches output 11 as strongly as output 0, because each output reads word 3 directly, in one step. Output 3 moves a little more, because word 3 is also its own query.
A learner often reads "parallel" as "no dependencies at all". That is wrong. Each attention output depends on every input word, as the second row shows. What attention removes is the dependence of one output on another output. All the inputs are known at the start, so all the outputs can start at once.
How long does each take on a laptop CPU? The script times all four versions, with 16, 64 and 256 words:
def fastest(f, k=10):
f()
best = 1e9
for _ in range(k):
t0 = time.perf_counter(); f(); best = min(best, time.perf_counter() - t0)
return best * 1000 time on the CPU (4 threads), forward pass only, fastest of 10 runs, random weights
n steps in order our loop ms nn.RNN ms our attention ms SDPA ms
16 16 vs 1 2.1 1.8 0.3 0.1
64 64 vs 1 9.0 7.4 0.7 0.5
256 256 vs 1 51.9 38.2 7.9 4.3We take the fastest of 10 runs, because a slow run usually means that the computer did something else at that moment. The library's nn.RNN is faster than our loop, because its loop is written in C++. But it still takes 256 steps in order for 256 words. Our attention and the library's SDPA (scaled dot-product attention) take one step of matrix products at every length.
Keep the warnings in mind. These are untrained layers with no learned query, key and value matrices, on a CPU with 4 threads. The times change from run to run. And attention's work grows with : at 256 words, the score table has 256 × 256 entries for each sentence. The next table shows what happens on a GPU and at 1,024 words.
Real layers on a laptop GPU
How much does the order cost with real layers on a GPU? We timed one GRU layer and one self-attention layer, both with vectors of 512 numbers, on batches of 32 random sentences. The GRU is a recurrent layer with gates, a lighter cousin of the LSTM. The attention layer is PyTorch's nn.MultiheadAttention, with learned matrices and 8 heads. The script tx_part1.py also writes the GRU as the loop that it is:
def gru_loop(x):
"""The GRU of §1, written as the loop that it is: step t needs h_(t-1)."""
cell = torch.nn.GRUCell(d, d)
cell.weight_ih, cell.weight_hh, cell.bias_ih, cell.bias_hh = gru.weight_ih_l0, gru.weight_hh_l0, gru.bias_ih_l0, gru.bias_hh_l0
h = torch.zeros(x.shape[0], d)
for t in range(x.shape[1]):
h = cell(x[:, t], h)
return hThe cell gets the weights of the full GRU layer, and the for loop takes one word per step. The output:
== 3. One GRU layer (a loop over the words) against one self-attention layer (all words at once)
width 512, batch 32, device mps (Apple M5 Pro laptop GPU), forward pass only, fastest of 20 runs
words n GRU steps GRU ms attention steps attention ms GRU / attention
16 16 2.3 1 0.7 3.3x
64 64 6.8 1 2.0 3.4x
256 256 18.4 1 4.8 3.8x
1024 1024 75.5 1 39.1 1.9x
check: the library GRU equals our step-by-step loop, max difference 3.3e-07The GRU needs steps in order; the attention layer needs one. On our laptop GPU, the attention layer was 1.9 to 3.8 times faster at every length.
The last line checks that the library GRU is the loop that we wrote by hand. The two results differ by less than one part in a million.
This table needs two warnings. First, attention is not free. Each word compares itself with every other word, so the work grows with , and the gap gets smaller at 1,024 words. Part 4 counts this cost exactly.
Second, these are single layers with random weights, timed on a laptop while other jobs ran. The ratio jumps from run to run. The table shows the shape of the problem, not the paper's training speed-up.
Training is parallel, writing is not
There is a subtle point that the abstract does not say. The Transformer's decoder still writes a translation one token at a time, because each new word depends on the words before it. The parallel speed-up is in training. During training, the correct German sentence is known in advance, so the model can predict every position at the same time. This trick is teacher forcing.
== 4. Marian: score a known German sentence in one call, or write it one token per call
German tokens m = 14
teacher forcing: 1 decoder call gives logits of shape (1, 14, 58101) (one row per position), 67 ms
the 14 predicted tokens equal the generated ones: True; mean loss 0.203
generation: 14 decoder calls, one per token, 146 msWith the known German sentence of 14 tokens, one call of the decoder gives 14 rows of scores, one row for each position. Each row has 58,101 scores, one for each token in the vocabulary. The best token in each row is exactly the token that the model generated. Writing the same sentence from nothing needed 14 calls, one after the other. A recurrent decoder needs the loop in both cases; the Transformer needs it only when it writes.
Teacher forcing in code
The table above came from one line, model(**enc, labels=labels), and the library hid the work. Here we do each step ourselves with the same released model, so that nothing is hidden. The German sentence is the model's own translation of our English sentence: 14 tokens, the last one </s>.
Step 1: shift the sentence. At position , the decoder must predict token of the German sentence. So its input at position must be the token before it. We build the input by putting a start token in front and dropping the last token:
start = torch.tensor([[cfg.decoder_start_token_id]]) # Marian uses <pad> (id 58100) as the start token
dec_in = torch.cat([start, labels[:, :-1]], dim=1) # (1, m): drop the last token, put start in front
dec_lib = shift_tokens_right(labels, cfg.pad_token_id, cfg.decoder_start_token_id)labels[:, :-1] takes every token except the last one. The library has a function for this step, shift_tokens_right, and we compare with it.
Step 2: one call for all positions. We give the model the English ids and the whole shifted German input at once:
with torch.no_grad():
out = model(**src, decoder_input_ids=dec_in)
logits = out.logits # (1, m, 58101): one row of scores per positionStep 3: the loss by hand. The loss is the next-token loss of the Background page: of the right token, averaged over the positions.
Here is the number of German tokens, is the right token at position , and are the tokens before it. In code, log_softmax turns each row of scores into log-probabilities. Then we pick, in each row, the entry of the right token:
logp = torch.log_softmax(logits, dim=-1) # (1, m, 58101)
logp_right = logp[0, torch.arange(m), labels[0]] # (m,): log p of the right token at each position
loss_ours = -logp_right.mean()
with torch.no_grad():
loss_lib = model(**src, labels=labels).loss # Method 2: the library shifts and averages for usThe line with torch.arange(m) is the one that needs a second look. It picks one number from each row: row 0 at column labels[0, 0], row 1 at column labels[0, 1], and so on. The result is 14 log-probabilities, one per position. Here is the output, with a table of what the decoder reads and what it must predict at each position:
== 2. Teacher forcing with Helsinki-NLP/opus-mt-en-de
English ids (1, 12); German labels (1, 14): ['▁Der', '▁Hund', '▁saß', '▁auf', '▁der', '▁Matt', 'e', ',', '▁weil', '▁er', '▁müde', '▁war', '.', '</s>']
decoder input (1, 14): ['<pad>', '▁Der', '▁Hund', '▁saß', '▁auf', '▁der', '▁Matt', 'e', ',', '▁weil', '▁er', '▁müde', '▁war', '.']
same as the library shift_tokens_right: True
one call: logits (1, 14, 58101)
loss by hand 0.2030; library loss 0.2030; difference 1.5e-08
pos decoder reads must predict p(right) top guess
0 <pad> ▁Der 0.796 ▁Der
1 ▁Der ▁Hund 0.946 ▁Hund
2 ▁Hund ▁saß 0.723 ▁saß
3 ▁saß ▁auf 0.782 ▁auf
4 ▁auf ▁der 0.767 ▁der
5 ▁der ▁Matt 0.915 ▁Matt
6 ▁Matt e 0.914 e
7 e , 0.771 ,
8 , ▁weil 0.837 ▁weil
9 ▁weil ▁er 0.565 ▁er
10 ▁er ▁müde 0.830 ▁müde
11 ▁müde ▁war 0.881 ▁war
12 ▁war . 0.900 .
13 . </s> 0.889 </s>
top guess equals the label at 14 of 14 positionsRead the table diagonally. The token that position 1 must predict, ▁Hund, is the token that position 2 reads. Each row sees only the tokens above it. Our loss, 0.2030, equals the library's loss and the 0.203 of the earlier run.
The lowest probability is 0.565, for "er" at position 9. That is the word whose gender depends on "dog", six words back. The model is right, but less sure than anywhere else in the sentence. Hard links between distant words are the hard part of translation.
Why does this need a mask? Position 3 reads ▁saß, and it must predict ▁auf. But ▁auf is also in the input, at position 4. Without a mask, position 3 could look one place to the right and copy the answer. The loss would be almost 0, and the model would learn nothing useful. The decoder's mask hides the later positions, so each row must really predict.
Writing: one decoder call per token
When the model translates a new sentence, it does not have the German sentence. It must write it. Here is the generation loop written out, our Method 1:
def greedy(model, src, max_new=40):
"""Method 1: our own generation loop. The encoder runs once; the decoder runs once per new token."""
enc_out = model.get_encoder()(**src) # (1, 12, 512), reused at every step
ys = torch.tensor([[cfg.decoder_start_token_id]]) # (1, 1): only the start token
step_logits = []
for _ in range(max_new):
logits = model(encoder_outputs=enc_out, attention_mask=src.attention_mask, decoder_input_ids=ys).logits
last = logits[:, -1].clone() # (1, 58101): scores for the next token only
last[:, cfg.pad_token_id] = -float('inf') # never write the start/pad token (generate does the same)
nxt = last.argmax(-1, keepdim=True) # (1, 1): the most likely token
step_logits.append(logits[0, -1])
ys = torch.cat([ys, nxt], dim=1) # feed it back in: the input grows by one
if nxt.item() == cfg.eos_token_id:
break
return ys, torch.stack(step_logits)The encoder runs once, and its 12 vectors are kept. The decoder runs inside the loop. At each step, it reads all the tokens so far, and we keep only the scores of the last position. We pick the best token and append it, so ys grows from shape (1, 1) to (1, 2), (1, 3) and so on. The loop is the same chain as in a recurrent network: step 5 needs the token of step 4.
Method 2 is the library's model.generate with one beam and no sampling, which is greedy decoding. We compare the two outputs, and we also compare the scores of our loop with the teacher-forced scores of Step 2:
== 3. Writing the translation: our greedy loop against model.generate
ours : Der Hund saß auf der Matte, weil er müde war. (14 decoder calls)
generate: Der Hund saß auf der Matte, weil er müde war.
same tokens: True
max |teacher-forced logits - loop logits at the same position| = 1.0e-05Our loop and the library write the same 14 tokens. The last line is the important one. The scores that the loop computed one step at a time equal the scores of the single teacher-forced call, to 0.00001. So the one parallel call computes exactly what 14 separate calls compute. This is why the Transformer can train on all positions at once and still learn the same thing that it does when it writes.
Our loop is slow on purpose: at every step, it runs the decoder again over all the earlier tokens. Real code keeps the keys and values of earlier positions in a cache, so each step only computes the new position. That makes each step cheaper, but the steps still run in order.
The models that came closest
The second section of the paper is short. It names the models that had already tried to remove the loop, and it says why they were not enough.
How many layers does a signal climb? Take a causal convolution of width , as in ConvS2S's decoder: each layer reaches more words back. With layers, the reach is words. Now take ByteNet-style dilated layers of width 2, with gaps of 1, 2, 4, 8 words: the reach after layers is . To link two words positions apart, the number of layers is:
The brackets round up to the next whole number. A worked case: and give convolution layers and dilated layers. A recurrent network needs 10 steps. Our script prints the table for four distances:
== 5. Layers a signal crosses between two words d positions apart
ConvS2S: kernel width 3, each layer reaches 2 more words back; ByteNet: kernel width 2, dilations 1, 2, 4, ...
d recurrent ConvS2S ByteNet self-attention
2 2 1 2 1
10 10 5 4 1
50 50 25 6 1
200 200 100 8 1At 200 words apart, a recurrent network needs 200 steps and a ConvS2S-style stack needs 100 layers. The dilated stack needs 8. Self-attention needs 1 at every distance. These are idealised counts for one layout of each model; the real ConvS2S and ByteNet use other widths. Part 4 reads the paper's own table of these costs, Table 1.
Next, in Part 2: §3 and Figure 1 of the paper, the architecture. Part 2 opens the encoder and the decoder and follows each shape through the released model. It covers the residual connections, layer normalisation, the feed-forward layers, the embeddings and the position encodings.
Run it yourself
The script behind this part is code/papers/transformer/tx_part1.py. It loads the released translation model and translates one sentence. It works the recurrence by hand and times a GRU layer against an attention layer. It also compares teacher forcing with generation, and prints the path lengths. Every number is saved in results/part1.json. The second script, tx_part1_scratch.py, holds the code of the sections "The loop and the matrix product, in code", "Teacher forcing in code" and "Writing: one decoder call per token".
pip install torch transformers sentencepiece
python tx_part1.py # downloads Helsinki-NLP/opus-mt-en-de (about 300 MB)
python tx_part1_scratch.py # the loop and the matrix product, teacher forcing and the greedy loop by hand
The paper and the code
Attention Is All You Need (NeurIPS 2017) · official code (tensor2tensor) · our scripts for this part