Transformer, explained · Part 3 of 7 · Covers §3.2, Figure 2, Equation 1, footnote 4, §3.2.2, §3.2.3

Attention: Queries, Keys and Values

How one attention step works, why the scores are divided by the square root of the key size, and why the model runs eight heads side by side. We write attention and multi-head attention from scratch in Python, match them with the PyTorch library and with a released translation model, and show three common bugs.

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

Attention is the one step in the Transformer where a word gets information from other words. Each piece below shows the exact lines of the paper, then explains them, and then checks them on a real translation model.

What an attention function is

Figure 2: the two pictures of attention

Scaled dot-product attention

Equation 1, step by step

The equation does four things in order: score every pair, shrink the scores, turn them into weights, mix the values.

Attention(Q,K,V)=softmax ⁣(QK⊤dk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left( \frac{Q K^\top}{\sqrt{d_k}} \right) V

Read it from left to right:

  • QQ is a table with one row for each query token. Each row has dkd_k numbers.
  • KK is a table with one row for each key token, also dkd_k numbers per row. K⊤K^\top is the same table turned on its side.
  • QK⊤Q K^\top is the table of all dot products. Row ii, column jj is the score of query ii against key jj.
  • dk\sqrt{d_k} divides every score by the same number. The next section explains why.
  • softmax\text{softmax} works on each row on its own. Each row becomes weights that add up to 1.
  • VV is the table of values. The weights multiply VV, so each output row is a weighted average of the value rows.

A tiny example with three tokens and dk=2d_k = 2. The three queries are (1,0)(1, 0), (0,1)(0, 1) and (1,1)(1, 1). The three keys are (2,0)(2, 0), (0,2)(0, 2) and (1,1)(1, 1). The three values are (10,0)(10, 0), (0,10)(0, 10) and (5,5)(5, 5).

Take the first query, (1,0)(1, 0). Its dot products with the three keys are 2, 0 and 1. Divide by 2=1.414\sqrt{2} = 1.414, and the scores are 1.414, 0 and 0.707. The softmax gives:

e1.414e1.414+e0+e0.707=4.1137.141=0.576,17.141=0.140,2.0287.141=0.284\frac{e^{1.414}}{e^{1.414} + e^{0} + e^{0.707}} = \frac{4.113}{7.141} = 0.576, \qquad \frac{1}{7.141} = 0.140, \qquad \frac{2.028}{7.141} = 0.284

The output is 0.576×(10,0)+0.140×(0,10)+0.284×(5,5)=(7.18,2.82)0.576 \times (10, 0) + 0.140 \times (0, 10) + 0.284 \times (5, 5) = (7.18, 2.82). The query points along the first axis, like the first key, so the output is mostly the first value. Our script gives the same numbers:

plain text
== 1. Eq. 1 on three tokens, d_k = 2
scores QK^T/sqrt(2): [[1.414, 0.0, 0.707], [0.0, 1.414, 0.707], [1.414, 1.414, 1.414]]
weights (softmax, row 1): [0.576, 0.14, 0.284]
output row 1: [7.18, 2.82]

Look at the third row of scores: 1.414 three times. The query (1,1)(1, 1) matches all three keys equally, so it gets one third of each value. Attention can share its focus when nothing stands out.

Method 1: Equation 1 from scratch, every matrix printed

Here is the same computation as code, so you can follow each table. NumPy is a Python library for tables of numbers. In NumPy, @ is the matrix product and .T turns a table on its side.

python
Q = np.array([[1., 0.], [0., 1.], [1., 1.]])     # 3 queries, one row per token
K = np.array([[2., 0.], [0., 2.], [1., 1.]])     # 3 keys
V = np.array([[10., 0.], [0., 10.], [5., 5.]])   # 3 values
d_k = Q.shape[1]                                  # 2 numbers per query and key

scores = Q @ K.T                                  # (3, 3): row i, column j = query i . key j
scaled = scores / np.sqrt(d_k)                    # divide every score by sqrt(2) = 1.414
e = np.exp(scaled - scaled.max(axis=1, keepdims=True))  # subtract the row maximum first: same softmax, no overflow
weights = e / e.sum(axis=1, keepdims=True)        # each row now adds up to 1
out = weights @ V                                 # (3, 2): each output row is a weighted average of the value rows

The script prints every table on the way:

plain text
Q K^T  shape (3, 3)
     2.000   0.000   1.000
     0.000   2.000   1.000
     2.000   2.000   2.000
Q K^T / sqrt(d_k)  shape (3, 3)
     1.414   0.000   0.707
     0.000   1.414   0.707
     1.414   1.414   1.414
weights = softmax(rows)  shape (3, 3)
     0.576   0.140   0.284
     0.140   0.576   0.284
     0.333   0.333   0.333
row sums: [1.0, 1.0, 1.0]
output = weights V  shape (3, 2)
      7.18    2.82
      2.82    7.18
      5.00    5.00

Read the tables in order. Q K^T holds the raw dot products: row 1 is 2, 0 and 1, as we computed by hand. The next table is the same divided by 1.414. The softmax turns each row into weights, and the row sums show that each row adds up to 1. The last table is the output, and its row 1 is (7.18, 2.82), as above.

One line needs a word. Before the exponential, the code subtracts the largest score of each row. This does not change the softmax, because the same factor e−me^{-m} appears above and below the fraction. It only keeps ese^{s} from growing too large for the computer when a score is big.

Method 2: the library function

PyTorch is the library that most Transformer code uses today. Below is Equation 1 as a PyTorch function, which the rest of this page uses again and again. It also takes an optional mask, which the section on the decoder mask explains. The lines of the body follow the boxes of Figure 2: MatMul and Scale, then Mask, SoftMax and the last MatMul.

python
def attention(Q, K, V, mask=None):
    """Equation 1: softmax(Q K^T / sqrt(d_k)) V.
    Q: (..., n_q, d_k)   K: (..., n_k, d_k)   V: (..., n_k, d_v)
    mask: True where a query may look at a key, False where it may not (None = no mask)."""
    d_k = Q.shape[-1]
    scores = Q @ K.transpose(-2, -1) / math.sqrt(d_k)       # (..., n_q, n_k): every query against every key
    if mask is not None:
        scores = scores.masked_fill(~mask, float('-inf'))    # forbidden pairs get minus infinity, BEFORE the softmax
    weights = torch.softmax(scores, dim=-1)                  # (..., n_q, n_k): each row adds up to 1
    return weights @ V, weights                              # (..., n_q, d_v), (..., n_q, n_k)

PyTorch also has its own version, torch.nn.functional.scaled_dot_product_attention. We call both on the three tokens, and then on random tensors with the shape of a real attention step:

python
Qt, Kt, Vt = (torch.tensor(a, dtype=torch.float32) for a in (Q, K, V))
ours, _ = attention(Qt, Kt, Vt)                       # Method 1: our function
lib = F.scaled_dot_product_attention(Qt, Kt, Vt)       # Method 2: the library
d1 = (ours - lib).abs().max().item()                   # the largest difference over all outputs
q, k, v = torch.randn(2, 8, 10, 64), torch.randn(2, 8, 10, 64), torch.randn(2, 8, 10, 64)   # (batch, heads, n, d_k)
ours, _ = attention(q, k, v)
lib = F.scaled_dot_product_attention(q, k, v)
d2 = (ours - lib).abs().max().item()
plain text
three tokens, ours   : [[7.1797, 2.8203], [2.8203, 7.1797], [5.0, 5.0]]
three tokens, library: [[7.1797, 2.8203], [2.8203, 7.1797], [5.0, 5.0]]
max abs difference: 4.8e-07
random (batch 2, heads 8, n 10, d_k 64): output (2, 8, 10, 64), max abs difference 6.0e-07

The two methods agree to within 6×10−76 \times 10^{-7}. That is not an error in either one. A computer stores each number of this kind with about 7 correct digits, and the library can add the products in a different order. The second test has 2 sentences, 8 heads, 10 tokens and 64 numbers per head. The @ product works on the last two axes and repeats for every sentence and head, so one function handles both cases.

Dot product or additive attention

Why divide by the square root of d_k

The equation behind the footnote, with its assumption:

Var⁡(q⋅k)=∑i=1dkVar⁡(qiki)=dk⇒Var⁡ ⁣(q⋅kdk)=dkdk=1\operatorname{Var}(q \cdot k) = \sum_{i=1}^{d_k} \operatorname{Var}(q_i k_i) = d_k \quad\Rightarrow\quad \operatorname{Var}\!\left( \frac{q \cdot k}{\sqrt{d_k}} \right) = \frac{d_k}{d_k} = 1

Read it: the variance of the dot product is the sum of the variances of its dkd_k terms. This holds when the terms are independent. Each term has variance 1 when qiq_i and kik_i have mean 0 and variance 1. Dividing a random number by cc divides its variance by c2c^2, so dividing by dk\sqrt{d_k} divides the variance by dkd_k.

A tiny check by hand with dk=4d_k = 4: each term is about ±1\pm 1, so a sum of four terms often lands near ±2\pm 2, which is 4\sqrt{4}. Our script drew 100,000 random pairs for four sizes:

plain text
== 2. dot products of random vectors (mean 0, variance 1), 100,000 pairs each
d_k=   4  mean +0.003  variance     3.99  std   2.00  scaled std 0.999
d_k=  16  mean +0.012  variance    15.92  std   3.99  scaled std 0.997
d_k=  64  mean -0.034  variance    64.01  std   8.00  scaled std 1.000
d_k= 512  mean -0.077  variance   514.16  std  22.68  scaled std 1.002

The variance follows dkd_k, as the footnote says, and the scaled std stays at 1.00. Next, what this does to the softmax. We took one random query and ten random keys, and computed the weights with and without the scale. We also computed the largest entry of the softmax gradient (its Jacobian matrix). For a softmax, no entry can be larger than 0.25.

plain text
== softmax over 10 keys, one query, d_k = 64 (same random draw, scaled and not scaled)
unscaled64  scores range [-15.8, +9.5]  top weight 0.8407  largest gradient entry 0.1339
scaled64    scores range [-2.0, +1.2]  top weight 0.2484  largest gradient entry 0.1867
== softmax over 10 keys, one query, d_k = 512 (same random draw, scaled and not scaled)
unscaled512 scores range [-60.1, +46.0]  top weight 1.0000  largest gradient entry 0.0000
scaled512   scores range [-2.7, +2.0]  top weight 0.5176  largest gradient entry 0.2497

With dk=512d_k = 512 and no scale, the scores spread from −60-60 to +46+46. The softmax puts a weight of 1.0000 on one key, and every gradient entry rounds to 0.0000. That attention head cannot learn. With the scale, the same vectors give a top weight of 0.52 and a gradient entry of 0.25, the largest a softmax allows. This is a toy with random vectors, not a trained model, but it shows the effect the paper describes.

Multi-head attention

The multi-head equation

MultiHead(Q,K,V)=Concat(head1,…,headh) WO,headi=Attention(QWiQ,  KWiK,  VWiV)\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \dots, \text{head}_h)\, W^O, \qquad \text{head}_i = \text{Attention}(Q W_i^Q,\; K W_i^K,\; V W_i^V)

Read it from the right. WiQW_i^Q, WiKW_i^K and WiVW_i^V are the three matrices of head ii, each of size 512×64512 \times 64. They turn the inputs into the queries, keys and values of that head. Attention\text{Attention} is Equation 1. Concat\text{Concat} puts the hh outputs side by side. WOW^O, of size 512×512512 \times 512, mixes them.

The parameter count with the base shapes: each head has three matrices of 512×64512 \times 64, and eight heads give 3×8×512×64=786,4323 \times 8 \times 512 \times 64 = 786{,}432 numbers. That is exactly three matrices of 512×512512 \times 512. Add WOW^O and one bias vector of 512 for each of the four matrices:

4×512×512+4×512=1,050,6244 \times 512 \times 512 + 4 \times 512 = 1{,}050{,}624

Multi-head attention from scratch, line by line

The multi-head equation hides one trick of the code. The paper writes hh separate matrices WiQW_i^Q. Code keeps them side by side as one wide matrix WQW^Q of size dmodel×dmodeld_{\text{model}} \times d_{\text{model}}. It makes the queries of all heads with one product, and then cuts the result into hh pieces. The cut is two calls: view and transpose.

We follow a small example with real tensors: 2 sentences of 5 tokens, dmodel=16d_{\text{model}} = 16 and h=4h = 4 heads, so dk=4d_k = 4. The comment at the end of a line says what it does.

python
batch, n, d_model, h = 2, 5, 16, 4
d_k = d_model // h                                  # 4 numbers per head
x = torch.randn(batch, n, d_model)                  # the input: 2 sentences of 5 tokens, 16 numbers each
p = {f'W_{m}': torch.randn(d_model, d_model) / math.sqrt(d_model) for m in 'QKVO'}   # paper layout: x @ W
p.update({f'b_{m}': torch.randn(d_model) * 0.1 for m in 'QKVO'})

Qm = x @ p['W_Q'] + p['b_Q']                         # all 4 heads' queries in one product
Qs = Qm.view(batch, n, h, d_k)                       # cut the last axis into h pieces of d_k
Qh = Qs.transpose(1, 2)                              # move heads before tokens: each head is its own problem
Kh = (x @ p['W_K'] + p['b_K']).view(batch, n, h, d_k).transpose(1, 2)   # keys: the same three steps
Vh = (x @ p['W_V'] + p['b_V']).view(batch, n, h, d_k).transpose(1, 2)   # values: the same three steps
S = Qh @ Kh.transpose(-2, -1) / math.sqrt(d_k)       # one n x n score table per head
Wt = torch.softmax(S, dim=-1)                        # each row of each table adds up to 1
heads = Wt @ Vh                                      # each head mixes its own values
merged = heads.transpose(1, 2)                       # tokens first again
concat = merged.reshape(batch, n, h * d_k)           # the h pieces side by side: the paper's Concat
y_steps = concat @ p['W_O'] + p['b_O']               # W^O mixes what the heads found

The script prints the shape after every line:

plain text
x                                    (2, 5, 16)   (batch, n, d_model)
Q = x W_Q + b_Q                      (2, 5, 16)   (batch, n, d_model)
Q.view(batch, n, h, d_k)             (2, 5, 4, 4)   (batch, n, h, d_k)
.transpose(1, 2)                     (2, 4, 5, 4)   (batch, h, n, d_k)
K, V the same way                    (2, 4, 5, 4)   (batch, h, n, d_k)
Q K^T / sqrt(d_k)                    (2, 4, 5, 5)   (batch, h, n, n)
softmax over the last axis           (2, 4, 5, 5)   (batch, h, n, n)
weights V                            (2, 4, 5, 4)   (batch, h, n, d_k)
.transpose(1, 2)                     (2, 5, 4, 4)   (batch, n, h, d_k)
.reshape(batch, n, h * d_k)          (2, 5, 16)   (batch, n, d_model)
concat W_O + b_O                     (2, 5, 16)   (batch, n, d_model)
Split heads: (batch, n, d_model) -> (batch, n, h, d_k) -> (batch, h, n, d_k)Q: 5 tokens x 16 numberst1t2t3t4t5head 1head 2head 3head 4.view(batch, n, h, d_k) cuts each rowinto 4 pieces; no number moves.transpose(1, 2)head 1head 2head 3head 4Each head is now its own 5 x 4 table: its ownqueries for the same 5 tokens. Attention runs on each.
The split of multi-head attention, for 5 tokens of 16 numbers and 4 heads. View cuts each row of 16 numbers into 4 groups of 4, one colour per head. Transpose then makes 4 separate tables, one per head, each with the same 5 tokens and 4 numbers per token.

Three lines do the real work. view(batch, n, h, d_k) reads the 16 numbers of each token as 4 groups of 4. No number moves; only the way we count them changes. transpose(1, 2) swaps the token axis and the head axis. Now the tensor is a stack of 2×4=82 \times 4 = 8 small problems, one for each sentence and head.

Equation 1 then runs on all 8 problems at once, because @ repeats over the leading axes. The scores have the shape (2, 4, 5, 5): one 5 × 5 table for each sentence and head. The way back is the mirror image. transpose(1, 2) puts the tokens first again, and reshape glues the 4 pieces of each token into 16 numbers. That is the paper's Concat, and WOW^O then mixes the pieces.

The block as three functions

The same steps fit in three short functions. split_heads and merge_heads are the two reshapes. multi_head_attention is the multi-head equation. It takes two inputs: x_q makes the queries, and x_kv makes the keys and values. For self-attention we pass the same tensor twice; for cross-attention we pass two different ones.

python
def split_heads(t, h):
    """(batch, n, d_model) -> (batch, h, n, d_k): cut each token vector into h pieces, one per head."""
    batch, n, d_model = t.shape
    return t.view(batch, n, h, d_model // h).transpose(1, 2)

def merge_heads(t):
    """(batch, h, n, d_k) -> (batch, n, h * d_k): put the h pieces of each token side by side (Concat)."""
    batch, h, n, d_k = t.shape
    return t.transpose(1, 2).reshape(batch, n, h * d_k)

def multi_head_attention(x_q, x_kv, p, h, mask=None):
    """MultiHead(Q, K, V) = Concat(head_1, ..., head_h) W^O, with head_i = Attention(Q W_i^Q, K W_i^K, V W_i^V).
    x_q (batch, n_q, d_model) makes the queries. x_kv (batch, n_k, d_model) makes the keys and values:
    the same tensor for self-attention, the encoder output for cross-attention.
    p: W_Q, W_K, W_V, W_O of shape (d_model, d_model) and b_Q, b_K, b_V, b_O of shape (d_model,).
    The h matrices W_i^Q of the paper are the h column blocks of W_Q, each d_model x d_k."""
    Q = split_heads(x_q @ p['W_Q'] + p['b_Q'], h)     # (batch, h, n_q, d_k)
    K = split_heads(x_kv @ p['W_K'] + p['b_K'], h)    # (batch, h, n_k, d_k)
    V = split_heads(x_kv @ p['W_V'] + p['b_V'], h)    # (batch, h, n_k, d_v)
    heads, weights = attention(Q, K, V, mask)         # (batch, h, n_q, d_v), (batch, h, n_q, n_k)
    return merge_heads(heads) @ p['W_O'] + p['b_O'], weights   # (batch, n_q, d_model)
plain text
step by step vs multi_head_attention(): max abs difference 0.0e+00

The function and the line-by-line code give the same output, with a difference of exactly 0. They do the same products in the same order.

Method 2: torch.nn.MultiheadAttention

PyTorch has a ready-made block, torch.nn.MultiheadAttention. To compare it with our code, we copy our matrices into it. There is one trap. The library stores each matrix as (output size, input size) and computes xW⊤x W^\top, while the paper writes xWx W. So we copy the transpose. The library also keeps WQW^Q, WKW^K and WVW^V in one tall matrix, in_proj_weight.

python
mha = torch.nn.MultiheadAttention(d_model, h, batch_first=True)
with torch.no_grad():
    # The library stores each matrix as (out, in) and computes x @ W.T, so we copy the transpose.
    mha.in_proj_weight.copy_(torch.cat([p['W_Q'].T, p['W_K'].T, p['W_V'].T]))   # (3 d_model, d_model)
    mha.in_proj_bias.copy_(torch.cat([p['b_Q'], p['b_K'], p['b_V']]))
    mha.out_proj.weight.copy_(p['W_O'].T)
    mha.out_proj.bias.copy_(p['b_O'])
    y_lib, w_lib = mha(x, x, x, need_weights=True, average_attn_weights=False)
d_out = (y_ours - y_lib).abs().max().item()                  # Method 1 against Method 2: outputs
d_w = (w_ours - w_lib).abs().max().item()                      # and the attention weights of every head
causal = torch.tril(torch.ones(n, n, dtype=torch.bool))       # True = may look
with torch.no_grad():
    y_ours_c, _ = multi_head_attention(x, x, p, h, mask=causal)
    y_lib_c, _ = mha(x, x, x, attn_mask=~causal)                # nn.MultiheadAttention: True = may NOT look
d_causal = (y_ours_c - y_lib_c).abs().max().item()
n_par = sum(t.numel() for t in mha.parameters())
plain text
library output (2, 5, 16), weights (2, 4, 5, 5)
output : max abs difference 4.8e-07
weights: max abs difference 1.2e-07
with the causal mask (library gets ~mask): max abs difference 3.6e-07
parameters: 1088 = 4 x 16 x 16 + 4 x 16

The outputs agree to 4.8×10−74.8 \times 10^{-7}, and so do the attention weights of all 4 heads. The parameter count follows the formula above: 4 matrices of 16 × 16 and 4 bias vectors of 16 make 1,088.

Look at the mask line. For nn.MultiheadAttention, True in attn_mask means "may NOT look". That is the opposite of scaled_dot_product_attention, where True means "may look". So we pass ~causal, the same table with True and False swapped. Libraries do not agree on this point, so check the meaning of each mask you use.

Multi-head attention by hand, on a real model

The paper's own weights are not easy to run today. So we used a released model with the same shapes: Helsinki-NLP/opus-mt-en-de, an English-to-German translation Transformer. It has 6 encoder and 6 decoder layers, dmodel=512d_{\text{model}} = 512 and 8 heads. It is not the paper's model: it was trained on other data, with another tokenizer and other settings.

plain text
== 3. Helsinki-NLP/opus-mt-en-de (MarianMT)
d_model 512, heads 8, d_k = d_v = 64, encoder layers 6, decoder layers 6
  q_proj   weight (512, 512)
  k_proj   weight (512, 512)
  v_proj   weight (512, 512)
  out_proj weight (512, 512)
parameters in one multi-head attention block: 1,050,624 (4 x 512 x 512 + 4 x 512 = 1,050,624)
source tokens: ['▁The', '▁cat', '▁sat', '▁on', '▁the', '▁mat', '▁because', '▁it', '▁was', '▁tired', '.', '</s>']
translation (greedy): Die Katze saß auf der Matte, weil sie müde war.

The library stores the eight WiQW_i^Q matrices as one 512×512512 \times 512 matrix, q_proj. Columns 1 to 64 belong to head 1, columns 65 to 128 to head 2, and so on. The count of 1,050,624 matches our hand count.

Then we caught the input of the first encoder attention block, and computed Equation 1 and the multi-head equation ourselves, with the model's weights:

plain text
== 4. encoder layer 1 self-attention rebuilt by hand
x (12, 512) -> Q,K,V per head (8, 12, 64) -> weights (8, 12, 12) -> heads (8, 12, 64) -> concat (12, 512) -> W^O -> (12, 512)
largest difference from the library output: 0.0e+00

Follow the shapes. Twelve tokens of 512 numbers become, for each of 8 heads, twelve queries, keys and values of 64 numbers. Each head makes a 12×1212 \times 12 table of weights. Each head outputs twelve vectors of 64, and the concat brings back 512. Our result equals the library output exactly. The two lines of the paper are the whole computation.

That rebuild used the library's own q_proj modules, so it did the same products as the library, in the same order. Next, we ran our own multi_head_attention function on the model's weights. A small helper reads the four matrices of a block and turns them into the layout of the paper:

python
def marian_params(att):
    """Read one Marian attention block as the paper's matrices. nn.Linear stores (out, in): transpose to x @ W."""
    return dict(W_Q=att.q_proj.weight.T, b_Q=att.q_proj.bias, W_K=att.k_proj.weight.T, b_K=att.k_proj.bias,
                W_V=att.v_proj.weight.T, b_V=att.v_proj.bias, W_O=att.out_proj.weight.T, b_O=att.out_proj.bias)

With hooks on three blocks, we ran the model once on the English sentence and its German translation. Then we called our function on the input of the first encoder block:

python
s = seen['encoder self-attention']
y, w = multi_head_attention(s['x_q'], s['x_q'], marian_params(blocks['encoder self-attention']), H)
diff = (y - s['y']).abs().max().item()                # against the output of the library's own block
plain text
source 12 tokens, decoder input 14 tokens
encoder self-attention : x (1, 12, 512) -> weights (1, 8, 12, 12), max abs difference 1.5e-06

Our function matches the library to 1.5×10−61.5 \times 10^{-6}, on outputs that reach 4.3. The products are the same, but our code orders the steps a little differently, so the last digits round differently. The same code later handles the decoder and cross-attention blocks; only the inputs and the mask change.

Terminal output of tx_part3.py with every result on this page

What do the eight heads look at? Here is the top key of each head for the query "it", in the first encoder layer:

plain text
layer 1, query "it" (position 7): top key per head
  head 1: [('▁tired', 0.75), ('</s>', 0.08)]
  head 2: [('▁it', 0.2), ('▁tired', 0.19)]
  head 3: [('</s>', 0.6), ('▁the', 0.1)]
  head 4: [('▁was', 0.33), ('▁mat', 0.17)]
  head 5: [('</s>', 0.41), ('▁the', 0.18)]
  head 6: [('</s>', 0.19), ('▁tired', 0.18)]
  head 7: [('</s>', 0.26), ('.', 0.2)]
  head 8: [('▁was', 0.68), ('</s>', 0.09)]

The heads do not agree, which is the point. Head 1 puts 0.75 of its weight on "tired". Head 8 puts 0.68 on "was", the next word. Head 2 spreads its weight. Several heads park weight on the end marker </s>, a common habit of trained models when a head has nothing specific to find. This is one sentence and the first layer only, so do not read a fixed job into each head.

The three uses of attention

The same multi-head block appears three times in the model. Only the source of the queries, keys and values changes.

We ran the source sentence and its translation through the model and asked for every attention weight. The shapes show the three uses: the encoder table is English by English, the cross table is German by English.

plain text
== 5. the three uses (full forward pass on the source and the translation)
decoder input tokens: ['<pad>', '▁Die', '▁Katze', '▁saß', '▁auf', '▁der', '▁Matt', 'e', ',', '▁weil', '▁sie', '▁müde', '▁war', '.']
encoder self-attention weights per layer: (1, 8, 12, 12)  (batch, heads, n_src, n_src)
decoder self-attention weights per layer: (1, 8, 14, 14)  (batch, heads, n_tgt, n_tgt)
cross-attention weights per layer:        (1, 8, 14, 12)  (batch, heads, n_tgt, n_src)

The first decoder token, <pad>, is the start signal that this model uses. Below is the cross-attention of decoder layer 3, as the mean of its 8 heads. For each German query, it shows the English token with the most weight:

plain text
cross-attention, decoder layer 3, mean of heads: the source token each German token attends to most
  query ▁Die         -> ▁The       (0.31)
  query ▁Katze       -> ▁cat       (0.25)
  query ▁saß         -> </s>       (0.37)
  query ▁auf         -> ▁on        (0.31)
  query ▁der         -> ▁the       (0.26)
  query ▁Matt        -> ▁mat       (0.43)
  query e            -> ▁mat       (0.27)
  query ,            -> ▁because   (0.55)
  query ▁weil        -> </s>       (0.27)
  query ▁sie         -> ▁it        (0.30)
  query ▁müde        -> ▁tired     (0.33)
  query ▁war         -> </s>       (0.37)
  query .            -> .          (0.52)
startstart → The: 0.03start → cat: 0.05start → sat: 0.04start → on: 0.01start → the: 0.02start → mat: 0.02start → because: 0.07start → it: 0.02start → was: 0.02start → tired: 0.01start → .: 0.16start → end: 0.53DieDie → The: 0.31Die → cat: 0.18Die → sat: 0.06Die → on: 0.06Die → the: 0.01Die → mat: 0.01Die → because: 0.06Die → it: 0.05Die → was: 0.03Die → tired: 0.02Die → .: 0.03Die → end: 0.15KatzeKatze → The: 0.14Katze → cat: 0.25Katze → sat: 0.11Katze → on: 0.05Katze → the: 0.01Katze → mat: 0.02Katze → because: 0.04Katze → it: 0.10Katze → was: 0.03Katze → tired: 0.01Katze → .: 0.03Katze → end: 0.20saßsaß → The: 0.03saß → cat: 0.02saß → sat: 0.23saß → on: 0.14saß → the: 0.02saß → mat: 0.02saß → because: 0.05saß → it: 0.03saß → was: 0.03saß → tired: 0.01saß → .: 0.04saß → end: 0.37aufauf → The: 0.03auf → cat: 0.02auf → sat: 0.07auf → on: 0.31auf → the: 0.12auf → mat: 0.10auf → because: 0.07auf → it: 0.02auf → was: 0.01auf → tired: 0.02auf → .: 0.03auf → end: 0.20derder → The: 0.02der → cat: 0.01der → sat: 0.02der → on: 0.10der → the: 0.26der → mat: 0.26der → because: 0.05der → it: 0.01der → was: 0.00der → tired: 0.01der → .: 0.04der → end: 0.20MattMatt → The: 0.01Matt → cat: 0.02Matt → sat: 0.01Matt → on: 0.01Matt → the: 0.05Matt → mat: 0.43Matt → because: 0.04Matt → it: 0.02Matt → was: 0.00Matt → tired: 0.01Matt → .: 0.03Matt → end: 0.37ee → The: 0.01e → cat: 0.02e → sat: 0.02e → on: 0.03e → the: 0.09e → mat: 0.27e → because: 0.16e → it: 0.10e → was: 0.01e → tired: 0.02e → .: 0.05e → end: 0.22,, → The: 0.02, → cat: 0.01, → sat: 0.03, → on: 0.01, → the: 0.01, → mat: 0.01, → because: 0.55, → it: 0.03, → was: 0.04, → tired: 0.05, → .: 0.13, → end: 0.13weilweil → The: 0.01weil → cat: 0.01weil → sat: 0.03weil → on: 0.01weil → the: 0.01weil → mat: 0.01weil → because: 0.19weil → it: 0.13weil → was: 0.15weil → tired: 0.06weil → .: 0.13weil → end: 0.27siesie → The: 0.01sie → cat: 0.02sie → sat: 0.01sie → on: 0.00sie → the: 0.01sie → mat: 0.01sie → because: 0.04sie → it: 0.30sie → was: 0.14sie → tired: 0.15sie → .: 0.09sie → end: 0.23müdemüde → The: 0.00müde → cat: 0.00müde → sat: 0.01müde → on: 0.00müde → the: 0.00müde → mat: 0.01müde → because: 0.02müde → it: 0.05müde → was: 0.13müde → tired: 0.33müde → .: 0.11müde → end: 0.31warwar → The: 0.00war → cat: 0.01war → sat: 0.02war → on: 0.00war → the: 0.00war → mat: 0.01war → because: 0.03war → it: 0.05war → was: 0.16war → tired: 0.06war → .: 0.28war → end: 0.37.. → The: 0.01. → cat: 0.01. → sat: 0.01. → on: 0.00. → the: 0.01. → mat: 0.01. → because: 0.04. → it: 0.03. → was: 0.03. → tired: 0.03. → .: 0.52. → end: 0.31Thecatsatonthematbecauseitwastired.endquery
Cross-attention in decoder layer 3 of the translation model, mean of 8 heads. Rows are the German tokens (queries), columns are the English tokens (keys). A darker cell means more weight. Most rows are darkest at the English word with the same meaning.

Most German words find their English partner with no one telling the model which word matches which. "sie" finds "it", which is a nice case: German uses "sie" because "Katze" is feminine. "Matt" and "e" are two pieces of "Matte", and both look at "mat". Some rows go to the end marker instead, as in the heads above. One layer and the mean of heads hide a lot, so this table is a picture of one sentence, not a rule.

Cross-attention in code

In code, cross-attention is the same function with a different second input. The queries come from the decoder: 14 German positions. The keys and values come from the encoder output: 12 English positions. Our script caught both in decoder layer 1 of the model and called our function:

python
s = seen['cross-attention']
y, w = multi_head_attention(s['x_q'], s['x_kv'], marian_params(blocks['cross-attention']), H)
diff = (y - s['y']).abs().max().item()
plain text
cross-attention        : queries (1, 14, 512) from the decoder, keys and values (1, 12, 512) from the encoder
                         -> weights (1, 8, 14, 12) -> output (1, 14, 512), max abs difference 1.2e-05
largest output value of each block: encoder self-attention 4.3, decoder self-attention 8.0, cross-attention 26.2
cross-attention layer 1, query "Katze", mean of 8 heads, top keys: [('</s>', 0.64), ('▁cat', 0.15), ('.', 0.05)]

Look at the shapes. The weights are (1, 8, 14, 12). Each of the 8 heads has a table with a row for each German query and a column for each English key. The table is not square, because the two sentences have different lengths. The output is (1, 14, 512): one new vector for each German position, filled with information from the English sentence.

Our function matches the library to 1.2×10−51.2 \times 10^{-5}. That is larger than before, but the outputs of this block are also larger: up to 26.2. Relative to the size of the values, it is the same rounding as before. In this first decoder layer, "Katze" puts 0.64 of its weight on the end marker and 0.15 on "cat". The clear match of layer 3, above, appears only after a few layers.

The mask: no looking ahead

Why minus infinity works

The softmax uses ese^{s}, and e−∞=0e^{-\infty} = 0. So a masked score gives a weight of exactly zero, and the other weights still add up to 1.

softmax(s1,…,sn)j=esj∑mesm,sj=−∞  ⇒  esj=0\text{softmax}(s_1, \dots, s_n)_j = \frac{e^{s_j}}{\sum_{m} e^{s_m}}, \qquad s_j = -\infty \;\Rightarrow\; e^{s_j} = 0

A tiny example: the scores of word 2 against words 1, 2 and 3 are (2, 1, 5). Word 3 is in the future, so its score becomes −∞-\infty. The weights are e2/(e2+e1)=7.389/10.107=0.731e^2 / (e^2 + e^1) = 7.389 / 10.107 = 0.731, then 0.2690.269, then 00. Without the mask, the large future score 5 would have taken most of the weight.

The mask in code

The mask is a table of True and False: True where a query may look at a key. Our code builds it with two loops, so the rule is visible: query ii may look at key jj only if j≤ij \le i. The same table comes from torch.tril, which keeps the lower triangle of a matrix.

python
n = 5
allowed = torch.zeros(n, n, dtype=torch.bool)
for i in range(n):                 # query position i ...
    for j in range(n):             # ... may look at key position j only if j is not in the future
        allowed[i, j] = j <= i

Then we make random queries, keys and values for 5 tokens and follow the scores through the mask. masked_fill writes minus infinity into every cell where the mask is False. Our attention function does exactly this, before the softmax.

python
g = torch.Generator().manual_seed(3)
q, k, v = torch.randn(n, 4, generator=g), torch.randn(n, 4, generator=g), torch.randn(n, 4, generator=g)
scores = q @ k.T / math.sqrt(4)
masked = scores.masked_fill(~allowed, float('-inf'))
out_mask, w_mask = attention(q, k, v, allowed)
lib_causal = F.scaled_dot_product_attention(q, k, v, is_causal=True)
lib_bool = F.scaled_dot_product_attention(q, k, v, attn_mask=allowed)
d_causal = (out_mask - lib_causal).abs().max().item()
d_bool = (out_mask - lib_bool).abs().max().item()
plain text
same as torch.tril(ones): True
allowed (1 = may look, 0 = future)  shape (5, 5)
    1  0  0  0  0
    1  1  0  0  0
    1  1  1  0  0
    1  1  1  1  0
    1  1  1  1  1
scores before the mask  shape (5, 5)
    -0.212   0.955   0.141   0.252  -0.031
    -0.057   0.968   0.105   0.652  -1.325
     0.165   0.101  -0.190   0.419  -0.887
     0.589  -1.212  -0.498   0.380  -1.627
    -0.513   0.422   0.423   0.672  -0.942
scores after the mask  shape (5, 5)
    -0.212    -inf    -inf    -inf    -inf
    -0.057   0.968    -inf    -inf    -inf
     0.165   0.101  -0.190    -inf    -inf
     0.589  -1.212  -0.498   0.380    -inf
    -0.513   0.422   0.423   0.672  -0.942
weights  shape (5, 5)
     1.000   0.000   0.000   0.000   0.000
     0.264   0.736   0.000   0.000   0.000
     0.379   0.355   0.266   0.000   0.000
     0.432   0.071   0.146   0.351   0.000
     0.100   0.254   0.255   0.326   0.065
ours vs library is_causal=True: max abs difference 1.5e-07
ours vs library attn_mask=allowed: max abs difference 1.5e-07
1. scores Q K^T / sqrt(d_k)q1query 1, key 1: -0.212-0.21query 1, key 2: 0.9550.96query 1, key 3: 0.1410.14query 1, key 4: 0.2520.25query 1, key 5: -0.031-0.03q2query 2, key 1: -0.057-0.06query 2, key 2: 0.9680.97query 2, key 3: 0.1050.11query 2, key 4: 0.6520.65query 2, key 5: -1.325-1.32q3query 3, key 1: 0.1650.16query 3, key 2: 0.1010.10query 3, key 3: -0.190-0.19query 3, key 4: 0.4190.42query 3, key 5: -0.887-0.89q4query 4, key 1: 0.5890.59query 4, key 2: -1.212-1.21query 4, key 3: -0.498-0.50query 4, key 4: 0.3800.38query 4, key 5: -1.627-1.63q5query 5, key 1: -0.513-0.51query 5, key 2: 0.4220.42query 5, key 3: 0.4230.42query 5, key 4: 0.6720.67query 5, key 5: -0.942-0.94k1k2k3k4k52. future set to -infq1query 1, key 1: -0.212-0.21-inf-inf-inf-infq2query 2, key 1: -0.057-0.06query 2, key 2: 0.9680.97-inf-inf-infq3query 3, key 1: 0.1650.16query 3, key 2: 0.1010.10query 3, key 3: -0.190-0.19-inf-infq4query 4, key 1: 0.5890.59query 4, key 2: -1.212-1.21query 4, key 3: -0.498-0.50query 4, key 4: 0.3800.38-infq5query 5, key 1: -0.513-0.51query 5, key 2: 0.4220.42query 5, key 3: 0.4230.42query 5, key 4: 0.6720.67query 5, key 5: -0.942-0.94k1k2k3k4k53. softmax of each rowq1query 1, key 1: 1.0001.00query 1, key 2: 0.0000.00query 1, key 3: 0.0000.00query 1, key 4: 0.0000.00query 1, key 5: 0.0000.00q2query 2, key 1: 0.2640.26query 2, key 2: 0.7360.74query 2, key 3: 0.0000.00query 2, key 4: 0.0000.00query 2, key 5: 0.0000.00q3query 3, key 1: 0.3790.38query 3, key 2: 0.3550.36query 3, key 3: 0.2660.27query 3, key 4: 0.0000.00query 3, key 5: 0.0000.00q4query 4, key 1: 0.4320.43query 4, key 2: 0.0710.07query 4, key 3: 0.1460.15query 4, key 4: 0.3510.35query 4, key 5: 0.0000.00q5query 5, key 1: 0.1000.10query 5, key 2: 0.2540.25query 5, key 3: 0.2550.25query 5, key 4: 0.3260.33query 5, key 5: 0.0650.06k1k2k3k4k5Row i is query i, column j is key j. Each weight row adds up to 1, and every future weight is exactly 0.
Masked attention on 5 tokens in three steps. Left: the scores of every query against every key. Middle: every score of a future key is replaced by minus infinity. Right: the weights after the softmax. The future weights are exactly 0, and each row adds up to 1.

Follow row 2. Its scores are -0.06 and 0.97 for the two allowed keys, and three future scores. The mask turns the future into minus infinity, and the softmax gives 0.264 and 0.736 to the two allowed keys. Row 1 has only one allowed key, so it gets the full weight of 1. Row 5 is the last token: it sees every key, and its weights are the same as without a mask.

The library offers two ways to say the same thing. is_causal=True builds the triangle for you. attn_mask=allowed takes your own table, where True also means "may look". Both match our output to 1.5×10−71.5 \times 10^{-7}.

In the real model, we checked every weight above the diagonal of the decoder self-attention table:

plain text
decoder self-attention, weight above the diagonal (future tokens): max 0.0e+00
decoder self-attention, last layer, mean of heads, first 5 rows x 5 columns:
  1.00 0.00 0.00 0.00 0.00   <- query <pad>
  0.59 0.41 0.00 0.00 0.00   <- query ▁Die
  0.63 0.04 0.32 0.00 0.00   <- query ▁Katze
  0.53 0.04 0.12 0.31 0.00   <- query ▁saß
  0.52 0.06 0.06 0.12 0.25   <- query ▁auf
startstart → start: 1.00DieDie → start: 0.59Die → Die: 0.41KatzeKatze → start: 0.63Katze → Die: 0.04Katze → Katze: 0.32saßsaß → start: 0.53saß → Die: 0.04saß → Katze: 0.12saß → saß: 0.31aufauf → start: 0.52auf → Die: 0.06auf → Katze: 0.06auf → saß: 0.12auf → auf: 0.25derder → start: 0.43der → Die: 0.05der → Katze: 0.01der → saß: 0.03der → auf: 0.13der → der: 0.35MattMatt → start: 0.53Matt → Die: 0.05Matt → Katze: 0.02Matt → saß: 0.03Matt → auf: 0.02Matt → der: 0.15Matt → Matt: 0.20ee → start: 0.35e → Die: 0.01e → Katze: 0.04e → saß: 0.14e → auf: 0.04e → der: 0.06e → Matt: 0.01e → e: 0.35,, → start: 0.45, → Die: 0.04, → Katze: 0.05, → saß: 0.06, → auf: 0.02, → der: 0.03, → Matt: 0.00, → e: 0.11, → ,: 0.24weilweil → start: 0.33weil → Die: 0.16weil → Katze: 0.04weil → saß: 0.04weil → auf: 0.03weil → der: 0.09weil → Matt: 0.01weil → e: 0.04weil → ,: 0.05weil → weil: 0.21siesie → start: 0.44sie → Die: 0.03sie → Katze: 0.01sie → saß: 0.02sie → auf: 0.01sie → der: 0.03sie → Matt: 0.00sie → e: 0.01sie → ,: 0.05sie → weil: 0.14sie → sie: 0.23müdemüde → start: 0.32müde → Die: 0.01müde → Katze: 0.01müde → saß: 0.01müde → auf: 0.01müde → der: 0.01müde → Matt: 0.00müde → e: 0.01müde → ,: 0.06müde → weil: 0.13müde → sie: 0.13müde → müde: 0.29warwar → start: 0.41war → Die: 0.01war → Katze: 0.03war → saß: 0.02war → auf: 0.01war → der: 0.01war → Matt: 0.00war → e: 0.07war → ,: 0.03war → weil: 0.02war → sie: 0.03war → müde: 0.04war → war: 0.32.. → start: 0.44. → Die: 0.01. → Katze: 0.01. → saß: 0.01. → auf: 0.00. → der: 0.01. → Matt: 0.00. → e: 0.00. → ,: 0.01. → weil: 0.01. → sie: 0.01. → müde: 0.00. → war: 0.02. → .: 0.46startDieKatzesaßaufderMatte,weilsiemüdewar.query
Decoder self-attention in the last decoder layer of the translation model, mean of 8 heads. Each row is a German query, each column a German key. The empty cells above the diagonal are the masked future positions: their weight is exactly zero.

Every future weight is exactly zero. The first row has only one place to look, so it puts 1.00 on itself. Each later row spreads its weight over the past, with much of it on the start token and on itself. The table is a triangle, and that triangle is the whole idea of the decoder mask.

Our own function gives the same decoder self-attention as the library, with the mask from torch.tril:

python
s = seen['decoder self-attention']
m = s['x_q'].shape[1]
y, w = multi_head_attention(s['x_q'], s['x_q'], marian_params(blocks['decoder self-attention']), H,
                            mask=torch.tril(torch.ones(m, m, dtype=torch.bool)))
diff = (y - s['y']).abs().max().item()
plain text
decoder self-attention : x (1, 14, 512) -> weights (1, 8, 14, 14), max abs difference 3.3e-06

The mask also has a partner trick. The German input to the decoder is shifted right by one position: the start token comes first. So the query at "Katze" predicts the next word, "saß", and sees only words up to "Katze". Part 2 shows where this mask and this shift sit in the decoder stack.

Three common mistakes, in code

Attention code often fails without a sound. The shapes are right, nothing crashes, and the numbers are wrong. Here are three mistakes that we made on purpose, each with its wrong output. The tests use the real encoder block from above, or the 5-token example of the mask.

Mistake 1: forgetting the scale

The same code as before, but the scores are not divided by dk\sqrt{d_k}:

python
Qh, Kh, Vh = (split_heads(s['x_q'] @ pm[f'W_{c}'] + pm[f'b_{c}'], H) for c in 'QKV')
w_bad = torch.softmax(Qh @ Kh.transpose(-2, -1), dim=-1)          # no / math.sqrt(d_k)
y_bad = merge_heads(w_bad @ Vh) @ pm['W_O'] + pm['b_O']
plain text
mistake 1, no scale:
  query "it", head 1: top weight 0.75 with the scale, 1.00 without
  largest weight in any row, all heads: 0.866 with the scale, 1.000 without
  output: max abs difference from the library 3.36

Without the division by 64=8\sqrt{64} = 8, every score is 8 times larger. Head 1 at the query "it" had 0.75 of its weight on "tired". Without the scale, it puts 1.00 on one key, and the soft choice becomes a hard one. This is the effect of footnote 4, now in a trained model. The output moves by up to 3.36, which is a different layer, not a rounding error. A new model with this bug would also train badly, because of the small gradients.

Mistake 2: masking after the softmax

Two wrong versions. The first applies the softmax and then sets the future weights to 0. The second masks before the softmax, but with 0 in place of minus infinity:

python
w_after = torch.softmax(scores, dim=-1) * allowed                   # softmax first, then zero the future
w_zero = torch.softmax(scores.masked_fill(~allowed, 0.0), dim=-1)   # "mask" with 0 in place of minus infinity
plain text
mistake 2, mask after the softmax (5-token example of section 3):
  weights, mask after softmax  shape (5, 5)
     0.119   0.000   0.000   0.000   0.000
     0.137   0.383   0.000   0.000   0.000
     0.234   0.219   0.164   0.000   0.000
     0.413   0.068   0.139   0.335   0.000
     0.100   0.254   0.255   0.326   0.065
  row sums: [0.119, 0.52, 0.617, 0.955, 1.0]  (correct: all 1.0)
  variant: masked scores set to 0, not -inf; weight on the future per row: [0.832, 0.456, 0.391, 0.193, 0.0]

In the first version, the future weights are 0, so the mask seems to work. But the softmax already shared the weight among all 5 keys, the future ones too. Row 1 keeps only 0.119 of its weight, and the rows no longer add up to 1. The output is a shrunken average, and the amount of the shrink depends on the future scores. So the future still leaks in. Only row 5, which has no future, is right.

The second version is just as bad. A score of 0 is not "no score", because e0=1e^0 = 1. Row 1 gives 0.832 of its weight to future keys. The mask must use minus infinity, and it must come before the softmax.

Mistake 3: transposing the wrong axes

The first wrong version splits the heads with one view and no transpose. The second splits correctly, but merges with one reshape and no transpose:

python
xq = s['x_q']
b_, n_, dm = xq.shape
Qw, Kw, Vw = ((xq @ pm[f'W_{c}'] + pm[f'b_{c}']).view(b_, H, n_, dm // H) for c in 'QKV')   # wrong split
heads_w, _ = attention(Qw, Kw, Vw)
y_w1 = heads_w.reshape(b_, n_, dm) @ pm['W_O'] + pm['b_O']
Qr, Kr, Vr = (split_heads(xq @ pm[f'W_{c}'] + pm[f'b_{c}'], H) for c in 'QKV')            # right split
heads_r, _ = attention(Qr, Kr, Vr)
y_w2 = heads_r.reshape(b_, n_, dm) @ pm['W_O'] + pm['b_O']                                # wrong merge
plain text
mistake 3, wrong axes:
  split with view(batch, h, n, d_k), no transpose: shapes (1, 8, 12, 64) (look right), max abs difference 3.84
  right split, but merge with reshape and no transpose: output (1, 12, 512), max abs difference 4.57
  right split and right merge: max abs difference 1.5e-06

This one is the hardest to see. view(batch, h, n, d_k) gives the right shape, (1, 8, 12, 64), so the rest of the code runs. But view only reads the numbers again in their order in memory. The 512 numbers of token 1 become rows 1 to 8 of "head 1", and the first half of token 2 becomes rows 9 to 12. Each "head" now mixes pieces of several tokens, and attention compares the wrong things.

The right order is to cut each token first, with view(batch, n, h, d_k), and then move the heads to the front with transpose. The merge has the same trap in reverse. Both wrong versions change the output by about 4, while the right code matches the library to 1.5×10−61.5 \times 10^{-6}. A comparison with a trusted library, as on this page, catches such bugs at once.

Terminal output of tx_part3b.py, the code walkthroughs of this page

Part 7 puts these functions together with the rest of the model and builds the whole Transformer from scratch.

The paper and the code

Paper (arXiv 1706.03762) · Official code (tensor2tensor) · Our scripts for this series