← All posts

Foundations

Post 10 of 15

Attention: looking back at everything at once

Posts in this series · 15

In the recurrent networks post we read a sequence one piece at a time, carrying a hidden state forward like a note passed down a line of people. LSTM and GRU made that note much better at keeping what matters. But it is still one note, of a fixed size, passed one hop at a time.

This post is about a different idea: instead of remembering everything, look it up. Keep every position of the input around, and whenever you need something, compare what you want with what each position offers, and take a blend of the best matches. That is attention. It is the heart of the Transformer, which is the next post, and of almost every modern language and speech model, including Pocket TTS.

We need three things from earlier posts. The dot product from the embeddings post: multiply matching entries of two vectors and add, so arrows pointing the same way give a big number. Softmax from the loss post: make every score positive with eze^{z}, then divide by the total so they add to 1. And embeddings: each token is a short list of numbers.

The bottleneck: a whole sentence in one state

In 2014 Sutskever, Vinyals and Le showed that two recurrent networks could translate sentences. The first, the encoder, reads the English sentence word by word. Its final hidden state is handed to the second, the decoder, which writes the French sentence word by word. This design is called sequence-to-sequence (seq2seq). Their networks were deep LSTMs, and it worked remarkably well.

But look at what passes between the two halves: one vector. Every word, every name, every bit of grammar in the source sentence has to fit into that single fixed-size list of numbers, whether the sentence has 5 words or 50. A short sentence fits easily. A long one gets squashed, and the early words, which went through the most steps, are the most faded.

Everyday picture: someone reads you a long recipe once, then you must cook it from memory. You will do fine with “boil an egg”. With a twenty-step curry you will forget the third spice. What you want is to keep the recipe on the counter and glance back at the line you need.

Bahdanau, Cho and Bengio (2015) measured exactly this: the quality of a fixed-vector encoder-decoder dropped as sentences got longer, and adding attention largely removed the drop. Their fix was to keep all the encoder’s hidden states, one per source word, and let the decoder look back at them at every step.

An encoder funnelling a sentence into one vector, or a decoder looking back at every word
Design
Figure 1. One squeezed state versus looking back. Step through the top row to see every word funnelled into one vector; switch to attention to see the decoder reach every source state directly.

Attention as a soft lookup

In one sentence: compare what you want with what everything offers, turn the comparisons into weights that add up to 1, and take the weighted average of the contents.

The library picture

You walk into a library with a question: “books about cats”. That is your query. Every book has a label on its spine: “cats”, “dogs”, “cat care”. Those are the keys. Inside each book is its content: the values.

A normal lookup (like a Python dictionary) is hard: find the one key that matches exactly and return its value. If nothing matches exactly, you get nothing. Attention is a soft lookup: every key is scored by how well it matches, and you get back a blend of every value, mixed in proportion to how good the match was. “Cats” and “cat care” both match well, so you get mostly those; “dogs” matches a little, so you get a little of that too.

A search engine works the same way: it scores every page against your words and ranks them. Attention goes one step further and blends the top results instead of listing them.

The soft blend matters for one big reason: it is smooth. Change the query a little and the weights change a little, so the gradient from backpropagation can flow through it. A hard “pick the best” has no useful gradient.

The four steps

For one query q\mathbf{q} and nn key-value pairs (k1,v1),…,(kn,vn)(\mathbf{k}_1, \mathbf{v}_1), \dots, (\mathbf{k}_n, \mathbf{v}_n):

  1. Score each key: sj=q⋅kjs_j = \mathbf{q} \cdot \mathbf{k}_j.
  2. Softmax the scores into weights: αj=esj/∑mesm\alpha_j = e^{s_j} / \sum_m e^{s_m}.
  3. Blend: o=∑jαjvj\mathbf{o} = \sum_j \alpha_j \mathbf{v}_j.

That is three steps; the fourth is simply “repeat for every query”. Written in one line:

o=∑j=1nαj vj,αj=eq⋅kj∑m=1neq⋅km\mathbf{o} = \sum_{j=1}^{n} \alpha_j \, \mathbf{v}_j, \qquad \alpha_j = \frac{e^{\mathbf{q} \cdot \mathbf{k}_j}}{\sum_{m=1}^{n} e^{\mathbf{q} \cdot \mathbf{k}_m}}

Worked by hand

Take the query q=(1,1)\mathbf{q} = (1, 1) and three items:

itemkeyvalue
1(2,0)(2, 0)(10,0)(10, 0)
2(0,1)(0, 1)(0,10)(0, 10)
3(1,1)(1, 1)(5,5)(5, 5)

Scores. s1=1×2+1×0=s_1 = 1 \times 2 + 1 \times 0 = 2. s2=1×0+1×1=s_2 = 1 \times 0 + 1 \times 1 = 1. s3=1×1+1×1=s_3 = 1 \times 1 + 1 \times 1 = 2.

Softmax. e2=7.389e^{2} = 7.389, e1=2.718e^{1} = 2.718, e2=7.389e^{2} = 7.389. Total =17.496= 17.496. Weights: 7.389/17.496=7.389 / 17.496 = 0.422, 2.718/17.496=2.718 / 17.496 = 0.155, and 0.422 again.

Blend. First number: 0.422×10+0.155×0+0.422×5=4.223+0+2.112=0.422 \times 10 + 0.155 \times 0 + 0.422 \times 5 = 4.223 + 0 + 2.112 = 6.335. Second: 0.422×0+0.155×10+0.422×5=0+1.554+2.112=0.422 \times 0 + 0.155 \times 10 + 0.422 \times 5 = 0 + 1.554 + 2.112 = 3.665. The output is (6.335,3.665)(6.335, 3.665).

What changed if a number changed? Item 1 and item 3 tie, because their keys give the same dot product with this query, even though they point in different directions (the dot product also counts length). Drag the query toward (1,0)(1, 0) and item 1 wins: the scores become 2, 0, 1 and the output moves toward (10,0)(10, 0). Notice the output is always inside the triangle of the three values: a weighted average can never leave it.

A query compared with three keys, the softmax weights, and the blended value
Query presets (or drag the arrows)
Figure 2. A soft lookup, one operation at a time. Drag the query or any key; Step shows each score, each weight and the weighted sum with the arithmetic written out.

Two ways to score: additive and dot-product

The recipe above leaves one choice open: how to score a query against a key.

Bahdanau’s additive score

Bahdanau, Cho and Bengio scored with a tiny one-hidden-layer network. In their translation model the “query” is the decoder’s previous state s\mathbf{s} and the “keys” are the encoder states hj\mathbf{h}_j:

ej=w⊤tanh⁡(Ws+Uhj)e_j = \mathbf{w}^{\top} \tanh(W\mathbf{s} + U\mathbf{h}_j)

It is called additive because the two inputs are added before the squash. A one-number example with W=U=w=1W = U = w = 1: decoder state s=0.5s = 0.5, encoder states h1=1h_1 = 1 and h2=−1h_2 = -1. Scores: tanh⁡(1.5)=\tanh(1.5) = 0.905 and tanh⁡(−0.5)=\tanh(-0.5) = −0.462. Softmax: e0.905=2.472e^{0.905} = 2.472, e−0.462=0.630e^{-0.462} = 0.630, total 3.1023.102, weights 0.797 and 0.203. The decoder mostly looks at the first source word.

The dot-product score

Luong, Pham and Manning (2015) compared several scores and found the plain dot product q⋅k\mathbf{q} \cdot \mathbf{k} works well and is much cheaper: no extra network, just multiply and add. For many queries at once it becomes a single matrix multiplication, which graphics processing units (GPUs) do extremely fast. The Transformer (Vaswani et al., 2017) uses the dot product, with one small fix that we come to next.

Scaled dot-product attention

In one sentence: score every query against every key in one matrix multiplication, shrink the scores by the square root of the vector length, softmax each row, and use the rows to blend the values.

Attention⁡(Q,K,V)=softmax⁡ ⁣(QK⊤d)V\operatorname{Attention}(Q, K, V) = \operatorname{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d}}\right) V

It is the soft lookup from before, done for every query at the same time.

Worked fully by hand on three tokens

Three tokens, each with a 2-D query, key and value (d=2d = 2, so d=1.414\sqrt{d} = 1.414). Where these come from is the subject of a later section; for now take them as given.

tokenquerykeyvalue
1(1,0)(1, 0)(1,1)(1, 1)(2,0)(2, 0)
2(1,1)(1, 1)(0,1)(0, 1)(0,2)(0, 2)
3(2,1)(2, 1)(1,2)(1, 2)(2,2)(2, 2)

Step one: every query against every key. Nine dot products, one per cell.

QK⊤=(101213314)QK^{\top} = \begin{pmatrix} 1 & 0 & 1 \\ 2 & 1 & 3 \\ 3 & 1 & 4 \end{pmatrix}

Step two: scale. Divide every cell by 1.4141.414:

QK⊤2=(0.70700.7071.4140.7072.1212.1210.7072.828)\frac{QK^{\top}}{\sqrt{2}} = \begin{pmatrix} 0.707 & 0 & 0.707 \\ 1.414 & 0.707 & 2.121 \\ 2.121 & 0.707 & 2.828 \end{pmatrix}

Step three: softmax, row by row.

Each row adds to 1 (up to rounding). Every token pays most attention to token 3, whose key (1,2)(1, 2) is the longest and so matches everything best.

Step four: blend the values.

output=(1.6041.1981.7201.4321.8511.389)\text{output} = \begin{pmatrix} 1.604 & 1.198 \\ 1.720 & 1.432 \\ 1.851 & 1.389 \end{pmatrix}

Each token’s new vector is a mixture of all three values, with its own recipe. Notice the output has the same shape as the input: three tokens in, three tokens out. That is why attention layers can be stacked.

Scaled dot-product attention computed cell by cell: scores, scaling, softmax and output
Figure 3. Scaled dot-product attention, cell by cell. Twenty-one steps: nine dot products, one scaling, three row softmaxes, six output numbers, and two summaries. Every number is the one in the text.

Why divide by the square root of d

In one sentence: dot products of longer vectors spread out more, and big spread makes softmax pick one winner and stop learning; dividing by d\sqrt{d} cancels the spread.

The spread grows with d

Suppose the numbers in queries and keys are random, each with average 0 and spread (standard deviation) 1, which is roughly what a well-initialised network produces. A dot product of two such vectors adds up dd products. Each product has average 0 and variance 1, and the variances of independent pieces add, so the dot product has variance dd and standard deviation d\sqrt{d}. Vaswani et al. (2017) give exactly this argument in a footnote.

Checked with 10,000 random pairs (NumPy, seed 0):

ddspread of q⋅k\mathbf{q} \cdot \mathbf{k}d\sqrt{d}spread after dividing by d\sqrt{d}
41.9920.99
163.9840.99
647.9981.00
25616.06161.00

After dividing, the spread is about 1 whatever dd is.

Why big spread hurts: softmax saturates

Softmax only cares about differences between scores. With differences around 1, the weights are a gentle mix. With differences around 8, one weight is nearly 1 and the rest nearly 0: softmax has become a hard max. That is called saturation, and it is bad for training: when one weight is almost exactly 1, nudging the scores barely changes anything, so the gradient through the softmax is almost zero (the loss post showed that softmax’s slope is largest when the probabilities are spread out).

One draw of a query and three keys (NumPy, seed 42):

The scaled d=64d = 64 case looks just like the d=4d = 4 case. That is the whole point: the size of the vectors no longer decides how sharp attention is. The network can still learn to make attention sharp, by growing its queries and keys, but it starts from a place where gradients flow.

A histogram of dot products widening with vector length, and softmax weights saturating unless scaled
Divide by √d
Figure 4. Why scale by the square root of d. Raise d with scaling off and watch the histogram widen and softmax snap to a single winner; turn scaling on and both settle. The figure draws its own random vectors, so its numbers differ slightly from the NumPy draws in the text.

Where queries, keys and values come from

In one sentence: each token’s embedding is multiplied by three different learned matrices, giving three different views of the same token.

Q=XWQ,K=XWK,V=XWVQ = X W_Q, \qquad K = X W_K, \qquad V = X W_V

Why three? Because what a word is looking for is not the same as what it offers, and neither is the same as what it should pass on. The word “it” might be looking for a noun; the word “cat” advertises “I am a noun”; and what “cat” passes on is its meaning. One embedding cannot play all three roles well, so the network learns three separate projections.

Worked

Three tokens with 2-D embeddings (1,0)(1, 0), (0,1)(0, 1) and (1,1)(1, 1), and these matrices:

WQ=(1011),WK=(1101),WV=(2002)W_Q = \begin{pmatrix} 1 & 0 \\ 1 & 1 \end{pmatrix}, \quad W_K = \begin{pmatrix} 1 & 1 \\ 0 & 1 \end{pmatrix}, \quad W_V = \begin{pmatrix} 2 & 0 \\ 0 & 2 \end{pmatrix}

A row vector times a matrix: the first output number is (first input × top-left) + (second input × bottom-left), the second uses the right column.

These are exactly the queries, keys and values of the three-token example. WVW_V here just doubles everything; real ones mix the numbers. If training changed WKW_K, every key would move, every score would change, and so would who attends to whom. That is how attention patterns are learned.

Pocket TTS does exactly this with 1,024-number embeddings and three 1,024 × 1,024 matrices; its post shows the shapes.

Self-attention and cross-attention

The same formula is used in two arrangements.

Everyday picture: self-attention is a group discussion, where everyone listens to everyone in the room. Cross-attention is an interview, where one side asks and the other side answers.

The line can blur. In Pocket TTS the voice recording you want to clone is simply placed at the start of the same sequence, so the model gets cross-attention’s effect from ordinary self-attention; its voice-cloning section walks through it.

Causal masking: no peeking at the future

When a model generates text or audio one token at a time, token 3 does not exist yet while token 2 is being made. If during training token 2 could attend to token 3, it would learn to copy the answer, and fail as soon as the answer is not there. So we forbid looking forward.

In one sentence: add −∞-\infty to every score where the key comes after the query, so softmax turns those into weights of exactly 0.

softmax⁡ ⁣(QK⊤d+M)V,Mij={0if j≤i−∞if j>i\operatorname{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d}} + M\right) V, \qquad M_{ij} = \begin{cases} 0 & \text{if } j \le i \\ -\infty & \text{if } j > i \end{cases}

Why add −∞-\infty before the softmax, rather than zeroing the weights after? Because the remaining weights must still add up to 1. Masking first lets softmax share the full weight among only the allowed positions.

Worked

For three tokens the mask is

M=(0−∞−∞00−∞000)M = \begin{pmatrix} 0 & -\infty & -\infty \\ 0 & 0 & -\infty \\ 0 & 0 & 0 \end{pmatrix}

Add it to the scaled scores from before:

Compare row 2 with the unmasked version (0.284, 0.140, 0.576): the weight that was on token 3 has been shared out between tokens 1 and 2, in the same 2.028 : 1 proportion as before.

This is the mask in the Pocket TTS formula, and the reason it can stream audio: frame ii never depends on frames after it.

Scores with future positions masked out, then softmaxed row by row
Causal mask
Figure 5. The causal mask. Toggle the mask and step through the rows: masked cells become exactly zero and the remaining weights still add to 1.

Multi-head attention

One attention computes one set of weights per token: one idea of “what is relevant”. But a word may need several things at once. In “the cat chased it”, the word “it” wants to know which noun it refers to (cat) and also what just happened (chased). One softmax row cannot put most of its weight in two places without blurring both.

In one sentence: run several small attentions side by side, each with its own WQW_Q, WKW_K, WVW_V, then glue their outputs together and mix them with one more matrix.

MultiHead⁡(X)=Concat⁡(head1,…,headh) WO,headi=Attention⁡(XWQ(i),XWK(i),XWV(i))\operatorname{MultiHead}(X) = \operatorname{Concat}(\text{head}_1, \dots, \text{head}_h)\, W_O, \qquad \text{head}_i = \operatorname{Attention}(X W_Q^{(i)}, X W_K^{(i)}, X W_V^{(i)})

Split the dimensions, do not multiply them. If the model width is dmodel=512d_{\text{model}} = 512 and there are h=8h = 8 heads, each head works with 512/8=512 / 8 = 64 numbers. Eight heads of 64 cost about the same as one head of 512, so you get several points of view for free. These are the original Transformer’s numbers (Vaswani et al., 2017).

A toy with two heads

Sentence: “the cat chased it”. Look only at the query for “it”. Suppose training has produced two heads whose scaled scores for “it” against the four tokens are:

Give each head a one-number value: in head 1 the value is 1 for the action word and 0 otherwise; in head 2 it is 1 for “cat” and 0 otherwise. Head 1 outputs 0.870, head 2 outputs 0.870, and the concatenated vector (0.870,0.870)(0.870, 0.870) carries both facts. WOW_O then mixes them into the next layer’s input. A single head would have had to split one row of weights between “cat” and “chased”.

These head roles are invented for the toy. Real heads do sometimes learn recognisable jobs like these, but many do not have a clean description.

Parameter count

Each of WQW_Q, WKW_K, WVW_V is dmodel×dmodeld_{\text{model}} \times d_{\text{model}} when you stack all heads’ pieces together, and so is WOW_O. So one multi-head attention layer has

4×dmodel24 \times d_{\text{model}}^2

weights (plus small biases, if used). For dmodel=512d_{\text{model}} = 512: 4×512×512=4 \times 512 \times 512 = 1,048,576. For Pocket TTS’s width of 1,024: 4×10242=4 \times 1024^2 = 4,194,304 per layer. The number of heads does not change this; it only decides how the width is split.

PyTorch’s nn.MultiheadAttention uses biases, so it adds 4×512=2,0484 \times 512 = 2{,}048 to the count. Changing the number of heads changes nothing:

import torch.nn as nn

attn = nn.MultiheadAttention(embed_dim=512, num_heads=8)
print(sum(p.numel() for p in attn.parameters()))
for heads in (1, 8, 16):
    layer = nn.MultiheadAttention(512, heads)
    print(heads, sum(p.numel() for p in layer.parameters()))

It prints:

1050624
1 1050624
8 1050624
16 1050624
Two attention heads from the word "it", one landing on "chased" and one on "cat"
Heads
Figure 6. Two heads, two questions. Step through each head’s scores and softmax, then the concatenation. Sliders change each head’s peak score; with one head the weight must be shared.

The cost: every position against every position

The score table QK⊤QK^{\top} has one cell for every pair of positions. With TT tokens that is T×T=T2T \times T = T^2 cells, each a dot product of length dd. Double the length and the table gets four times bigger.

tokens TTscore cells T2T^2memory for one head’s table (2 bytes per number)
10100200 bytes
10010,00020 KB
1,0001,000,0002 MB
4,00016,000,00032 MB
10,000100,000,000200 MB
100,00010,000,000,00020 GB

That is per head, per layer, if the whole table is stored at once. Speech makes this bite quickly: a few minutes of audio at tens of frames per second is already thousands of positions. Clever implementations avoid storing the full table, but the amount of arithmetic still grows as T2T^2.

A recurrent network, in contrast, costs only in proportion to TT, but it must take TT steps one after another. Attention does more total work, but all of it at once, which is why it trains so much faster on GPUs.

The KV cache, briefly

During generation, each new token needs a query against all previous keys and values. Those earlier keys and values never change (the causal mask guarantees the past cannot see the future), so recomputing them every step would be waste. Instead the model stores them: the key-value (KV) cache. Each new step computes one new query, key and value, appends the key and value to the cache, and attends over it. The price is memory: the cache grows with every token, for every layer and head. Managing that memory is a large part of serving large models; my inference engineering series deals with moving the KV cache between GPU and CPU memory.

The attention score table growing as the square of the sequence length
Figure 7. Quadratic cost. Double the length and the square of scores quadruples. The readout shows cells, bytes for one head and a linear reference.

Attention has no sense of order

Look back at the formula. Every query is compared with every key, and the results are summed. Nothing in it says which token came first. If you shuffle the input tokens, the outputs are shuffled in the same way, but each token’s output is otherwise unchanged: “the cat chased it” and “it chased the cat” look like the same bag of words. (With the causal mask, order leaks in a little through who can see whom, but only crudely.)

Order obviously matters in language and in sound. The fix is to add information about position to each embedding before attention sees it. How to do that, with sine waves or with rotations, is one of the first topics in the Transformer post, along with the residual connections and feed-forward layers that turn attention into a full model.

What to remember

Try it yourself

Sources