Inside the Transformer

Attention, heads, and blocks, built one operation at a time

Sjoerd Vink

How does a word pick up meaning from the words around it?

This essay builds the answer, attention, from a single dot product, and then assembles it into a small Transformer. Each step is a small change to the one before.

  1. 01 Attention
  2. 02 Queries, keys, values
  3. 03 Heads
  4. 04 Transformer
thedogchasedtheball0.120.320.120.32?

01 Attention

Every token starts on its own.

A Transformer first turns each token of its input into a vector. In this sentence each of the five tokens gets four numbers, drawn as a column of shaded cells.

At this point a vector only describes its own word. The vector for “chased” is the same in every sentence, no matter who did the chasing.

To mean something here, “chased” needs information from the other tokens: who chased, and what was chased.

How can one token gather useful information from the others?

Attention gives every other token a weight, then mixes their information according to those weights. Here “chased” takes most from “dog” and “ball”.

The result is a new vector for “chased” that carries its context. The rest of this essay builds the operation that produces these weights.

Start with something simpler: three words, two numbers each, and nothing learned.

x = X[0]                    # dogscores = X @ xweights = softmax(scores)output = weights @ X
dogpuppycarnew dogdog[1.0, 0.5]1.25puppy[0.6, 0.6]0.90car[−0.3, 0.9]0.15sum7.11dog · dog = 1.0×1.0 + 0.5×0.5 = 1.25dog · dog = 1.0×1.0 + 0.5×0.5 = 1.25dog · puppy = 1.0×0.6 + 0.5×0.6 = 0.90dog · car = 1.0×(−0.3) + 0.5×0.9 = 0.15exp(1.25) = 3.49exp(0.90) = 2.46exp(0.15) = 1.163.49 / 7.11 = 0.492.46 / 7.11 = 0.351.16 / 7.11 = 0.160.49·dog + 0.35·puppy + 0.16·car= 0.49×[1.0, 0.5] + 0.35×[0.6, 0.6] + 0.16×[−0.3, 0.9]= [0.65, 0.60]

01 Attention/ Similarity

Similar vectors have large dot products.

Here are three word vectors with two numbers each. “dog” and “puppy” point in nearly the same direction. “car” points elsewhere. Stacked as rows, they form a matrix XX of shape (3, 2).

X=[1.00.50.60.6−0.30.9]X = \begin{bmatrix} 1.0 & 0.5 \\ 0.6 & 0.6 \\ -0.3 & 0.9 \end{bmatrix}

Pick “dog”. To measure how related another vector is to it, take the dot product. Multiply matching entries and add them up.

dog⋅dog=1.0×1.0+0.5×0.5=1.25\begin{aligned} \text{dog} \cdot \text{dog} &= 1.0 \times 1.0 + 0.5 \times 0.5 \\ &= 1.25 \end{aligned}

Do the same for every word. X @ x computes all three dot products at once, one per row of XX.

A dot product is large when two vectors point the same way, near zero when they are at right angles, and negative when they point apart. “puppy” scores 0.90, “car” only 0.15.

These scores are not weights yet. They could be negative, and they do not add up to anything in particular. Softmax fixes both. First it exponentiates every score, which makes it positive.

e1.25=3.49e0.90=2.46e0.15=1.16\begin{aligned} e^{1.25} &= 3.49 \\ e^{0.90} &= 2.46 \\ e^{0.15} &= 1.16 \end{aligned}

Then it divides each by their total, 7.11. The results are positive and sum to 1, and the highest score still gets the most weight.

softmax⁡(s)i=esi∑jesj\operatorname{softmax}(s)_i = \frac{e^{s_i}}{\sum_j e^{s_j}}

Now use the weights. The new vector for “dog” is the weighted sum of all three vectors. In the figure the three scaled vectors are laid end to end.

0.49 dog+0.35 puppy+0.16 car=[0.65, 0.60]\begin{aligned} &0.49\,\text{dog} + 0.35\,\text{puppy} \\ &+ 0.16\,\text{car} \\ &= [0.65,\ 0.60] \end{aligned}

The weights decide how much to take from each vector. The output is the mix.

scores = X @ X.Tweights = softmax(scores, axis=-1)output = weights @ X
dogpuppycarlooked at →asking ↓dogpuppycar1.250.900.150.900.720.360.150.360.90S(T, T)Σ 1.00Σ 1.00Σ 1.00@1.00.50.60.6−0.30.9X(T, D)=0.650.600.550.630.250.72output(T, D)dog · puppy = 1.0×0.6 + 0.5×0.6 = 0.90S[dog, car] = S[car, dog] = 0.15car: exp(0.15), exp(0.36), exp(0.90) = 1.16, 1.43, 2.46 ÷ 5.05 = 0.23, 0.28, 0.49car: 0.23×[1.0, 0.5] + 0.28×[0.6, 0.6] + 0.49×[−0.3, 0.9] = [0.25, 0.72]

01 Attention/ All tokens

Every token asks at once.

The row of scores we computed belongs to “dog”. In the full picture, each row belongs to one token, the one that asks. Each column is a token that is looked at.

Filling in every row gives the score matrix S=XX⊤S = XX^{\top}, of shape (T, T) for TT tokens. Entry (i,j)(i, j) is the dot product of token ii with token jj.

This matrix is symmetric. “dog” scores “car” exactly as “car” scores “dog”.

Softmax is applied to each row separately. That is what axis=-1 says: normalize along the last axis, across the columns of a row. Each row of AAis one token's weights and sums to 1. The columns do not.

A=softmax⁡(XX⊤)=[0.490.350.160.410.350.240.230.280.49]\begin{aligned} A &= \operatorname{softmax}(XX^{\top}) \\ &= \begin{bmatrix} 0.49 & 0.35 & 0.16 \\ 0.41 & 0.35 & 0.24 \\ 0.23 & 0.28 & 0.49 \end{bmatrix} \end{aligned}

One matrix product now does the mixing for every token. Row ii of AXAX is token ii's weighted sum of all rows of XX, so the output has one new vector per token, shape (T, D) with DD = 2 numbers per token.

output=softmax⁡(XX⊤) X\text{output} = \operatorname{softmax}(XX^{\top})\,X

Its first row is the vector for “dog” from before.

This already works, but it is rigid. Scores depend only on how similar two vectors already are, and nothing in it can be learned.

In the sentence, “chased” does not want words that resemble a verb. It wants its subject and its object.

Q = X @ W_QK = X @ W_KV = X @ W_V
thedogchasedtheball0.00.01.00.01.00.00.01.00.01.00.00.00.00.01.00.01.00.00.0−0.5X(T, D)0.01.50.50.02.00.00.00.00.00.00.01.00.00.00.00.0WQ=0.00.00.01.00.01.50.50.02.00.00.00.00.00.00.01.00.01.50.50.0Q(T, D)1.00.00.01.00.01.00.00.00.00.01.00.00.00.00.00.0WK=0.00.01.00.01.00.00.01.00.01.00.00.00.00.01.00.01.00.00.01.0K(T, D)1.00.00.00.50.01.00.00.00.00.01.00.00.00.00.01.0WV=0.00.01.00.01.00.00.01.50.01.00.00.00.00.01.00.01.00.00.00.0V(T, D)x @ W adds up rows of W, each scaled by an entry of xx_chased = [0.0, 1.0, 0.0, 0.0] → row 2 of W_Qq_chased = [2.0, 0.0, 0.0, 0.0]x_dog = [1.0, 0.0, 0.0, 1.0] → row 1 + row 4 of W_Vv_dog = [1.0, 0.0, 0.0, 0.5] + [0.0, 0.0, 0.0, 1.0] = [1.0, 0.0, 0.0, 1.5]chased: q [2.0, 0.0, 0.0, 0.0] k [0.0, 1.0, 0.0, 0.0] v [0.0, 1.0, 0.0, 0.0]

02 Queries, keys, values

One vector, three roles.

Back to the sentence. XX has one row per token and DD = 4 numbers per row, shape (T, D).

In the simple version, each token vector did three jobs at once. Attention gives each job its own vector.

Query. What am I looking for?

Key. What kind of query should match me?

Value. What do I hand over when I am matched?

Each role is a learned linear projection of XX. The queries are Q=XWQQ = XW_Q.

The row of “chased” in XX is [0.0, 1.0, 0.0, 0.0][0.0,\ 1.0,\ 0.0,\ 0.0], so its query is simply the second row of WQW_Q: [2.0, 0.0, 0.0, 0.0][2.0,\ 0.0,\ 0.0,\ 0.0].

Keys and values come from two more weight matrices. All three have shape (D, D), so QQ, KK and VV have the same shape as XX.

Q=XWQK=XWKV=XWV\begin{aligned} Q &= XW_Q \\ K &= XW_K \\ V &= XW_V \end{aligned}

These weights were picked by hand so the numbers stay readable. In a trained model they are learned, and much less tidy.

Row ii of QQ, KK and VV all belong to token ii. “chased” now has a query [2.0, 0.0, 0.0, 0.0][2.0,\ 0.0,\ 0.0,\ 0.0] that matches keys with a large first entry, which in this example are the nouns. Its key [0.0, 1.0, 0.0, 0.0][0.0,\ 1.0,\ 0.0,\ 0.0] is what other queries will find.

Because the query and key come from different matrices, what a token looks for no longer has to resemble what it is.

Q = X @ W_QK = X @ W_KV = X @ W_Vscores = Q @ K.T / np.sqrt(d_k)weights = softmax(scores, axis=-1)output = weights @ V
thedogchasedtheball0.00.00.01.00.01.50.50.02.00.00.00.00.00.00.01.00.01.50.50.0Q(T, D)0.00.01.00.01.00.00.01.00.01.00.00.00.00.01.00.01.00.00.01.0Kthedogchasedtheball0.001.000.000.001.000.500.001.500.500.000.002.000.000.002.000.001.000.000.001.000.500.001.500.500.00QKT(T, T)q·k = 2.0÷ √d_kΣ 1.00Σ 1.00Σ 1.00Σ 1.00Σ 1.00thedogchasedtheball0.00.01.00.01.00.00.01.50.01.00.00.00.00.01.00.01.00.00.00.0V0.520.160.320.390.300.320.380.220.640.120.240.480.520.160.320.390.300.320.380.22output(T, D)0.120.320.120.120.32weightq_chased = [2.0, 0.0, 0.0, 0.0] k_dog = [1.0, 0.0, 0.0, 1.0]q_chased · k_dog = 2.0×1.0 + 0.0×0.0 + 0.0×0.0 + 0.0×1.0 = 2.02.0 / √d_k = 2.0 / √4 = 2.0 / 2 = 1.00chased: exp(0, 1, 0, 0, 1) = 1.00, 2.72, 1.00, 1.00, 2.72 ÷ 8.44 = 0.12, 0.32, 0.12, 0.12, 0.32weights @ V: (T, T) @ (T, D) = (T, D)chased: 0.32·(v_dog + v_ball) + 0.12·(v_the + v_chased + v_the) = [0.64, 0.12, 0.24, 0.48]each row: softmax over its scores, then a mix of value rows

02 Queries, keys, values/ Scaled dot-product attention

Queries meet keys.

The steps are the same as before. Only the inputs change: scores now compare one token's query with every token's key.

Turning KK on its side gives K⊤K^{\top}, shape (D, T). The product QK⊤QK^{\top} holds every query–key dot product, shape (T, T). Rows are queries, columns are keys. In the figure, each score sits where its row of QQ meets its column of K⊤K^{\top}.

qchased=[2.0, 0.0, 0.0, 0.0]kdog=[1.0, 0.0, 0.0, 1.0]qchased⋅kdog=2.0×1.0+0+0+0=2.0\begin{aligned} q_{\text{chased}} &= [2.0,\ 0.0,\ 0.0,\ 0.0] \\ k_{\text{dog}} &= [1.0,\ 0.0,\ 0.0,\ 1.0] \\ q_{\text{chased}} \cdot k_{\text{dog}} &= 2.0 \times 1.0 + 0 + 0 + 0 = 2.0 \end{aligned}

Unlike XX⊤XX^{\top}, this matrix is not symmetric. “chased” scores “dog” at 2.0, but “dog” scores “chased” at 1.5.

Every score is then divided by dk\sqrt{d_k}, where dkd_k is the length of the query and key vectors. Here dk=4d_k = 4, so we divide by 2.

A dot product adds up dkd_k products. If the entries of qq and kk are independent with mean 0 and variance 1, the sum has variance dkd_k. At dk=512d_k = 512 scores would routinely reach the tens, softmax would put nearly all weight on one token, and its gradients would all but vanish. Dividing by dk\sqrt{d_k} brings the variance back to 1.

Var⁡(q⋅k)=dkVar⁡ ⁣(q⋅kdk)=1\begin{aligned} \operatorname{Var}(q \cdot k) &= d_k \\ \operatorname{Var}\!\left(\frac{q \cdot k}{\sqrt{d_k}}\right) &= 1 \end{aligned}

Softmax along each row turns scores into weights. “chased” gives 0.32 to “dog” and to “ball”, and 0.12 to each of the others.

A=softmax⁡ ⁣(QK⊤dk)A = \operatorname{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_k}}\right)

The weights mix the values, not the keys. AA becomes the left operand and multiplies VV: (T, T) times (T, D) gives (T, D), one output per token. The figure uses the same layout as before: an output cell sits where its row of weights meets its column of VV.

The output row of “chased” is its weighted sum of value rows.

0.32 (vdog+vball)+0.12 (vthe+vchased+vthe)=[0.64, 0.12, 0.24, 0.48]\begin{aligned} &0.32\,(v_{\text{dog}} + v_{\text{ball}}) \\ &+ 0.12\,(v_{\text{the}} + v_{\text{chased}} + v_{\text{the}}) \\ &= [0.64,\ 0.12,\ 0.24,\ 0.48] \end{aligned}

Its last entry, 0.48, comes entirely from “dog”: 0.32 × 1.5. What a token hands over is its value, which can carry information its key never showed.

Every part of the formula now has a place in the figure.

Attention⁡(Q,K,V)=softmax⁡ ⁣(QK⊤dk)V\begin{aligned} &\operatorname{Attention}(Q, K, V) \\ &= \operatorname{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_k}}\right) V \end{aligned}

QK⊤QK^{\top} compares queries with keys. dk\sqrt{d_k}keeps the scores in a range where softmax still spreads its weight. Softmax runs along each row, so every token's weights sum to 1. Multiplying by VV mixes the values.

Q = X @ W_QK = X @ W_KV = X @ W_Vscores = Q @ K.T / np.sqrt(d_k)weights = softmax(scores, axis=-1)output = weights @ V
X(T, D)QK(T, D)Vscores(T, T)weights(T, T)output(T, D)B sentences at once, each with its own 5 × 5 weights

02 Queries, keys, values/ Code

Six lines, then a module.

In NumPy, single-head attention is six lines, with the same names as the figures. softmax is a small helper that exponentiates and divides by the sum along the given axis.

PyTorch is nearly identical. K.transpose(-2, -1) swaps the last two dimensions, and the softmax axis is called dim. dim=-1 still means along each row.

Models process a batch of BB sequences at once, so XX has shape (B, T, D). The code does not change. @ multiplies over the last two dimensions and treats the leading one as a batch, and transpose(-2, -1) leaves BB alone. Each sequence gets its own (T, T) weights.

As a module, the three weight matrices become nn.Linear layers without bias, and self.W_Q(X) computes X @ W_Q. (nn.Linear stores its matrix transposed and multiplies by weight.T. The result is the same.)

A single attention operation is one learned way of routing information between tokens. Language needs more than one.

XW_Q, W_K, W_Vthedogchasedtheballthedogchasedtheballone patternhead 1 projectionsthedogchasedtheballthedogchasedtheballpattern 1head 2 projectionsthedogchasedtheballthedogchasedtheballpattern 2concatWOoutput

03 Heads

One pattern is not enough.

One attention operation gives each token one set of weights. But a token may need several kinds of information at once. “chased” wants its subject and its object. A noun may want its article.

Multi-head attention runs several attention operations side by side. Each head has its own query, key and value projections, so each can learn its own way of routing information. Every head reads the same XX.

With our weights, head 1 links “chased” and the nouns. Head 2 links the nouns with “the”. Those are readings of a hand-built example. Some heads in trained models show patterns this clear. Many do not.

Each head produces its own output. The outputs are concatenated and multiplied by one more learned matrix, WOW_O, which mixes the heads back together.

MultiHead⁡(X)=concat⁡(head1,…,headH) WO\begin{aligned} &\operatorname{MultiHead}(X) \\ &= \operatorname{concat}(\text{head}_1, \ldots, \text{head}_H)\, W_O \end{aligned}
B, T, D = X.shapeH, D_head = self.H, D // self.HQ = self.W_Q(X)                                # (B, T, D)Q = Q.view(B, T, H, D_head)                    # (B, T, H, D_head)Q = Q.transpose(1, 2)                          # (B, H, T, D_head)# K and V are reshaped the same wayscores = Q @ K.transpose(-2, -1) / math.sqrt(D_head)weights = torch.softmax(scores, dim=-1)        # (B, H, T, T)heads = weights @ V                            # (B, H, T, D_head)heads = heads.transpose(1, 2).reshape(B, T, D) # (B, T, D)output = self.W_O(heads)                       # (B, T, D)
XWQD × DX is not splithead 1head 2Q12headsweightsweightsheadsheadsconcatWOoutput(B, T, D)

03 Heads/ Shapes

Split the projection, not the input.

In code, the heads are not separate modules. One projection computes the queries for all heads at once, and the result is split. Follow the shapes for a batch of BB sequences.

WQW_Q is a full (D, D) matrix, so every column of QQ is computed from all DD features of a token.

view regroups the last dimension into HH heads of Dhead=D/HD_{\text{head}} = D / H numbers each. Here D=4D = 4, H=2H = 2 and Dhead=2D_{\text{head}} = 2. No number changes.

The split happens after the projection. Head 1 gets the first two columns of QQ, which is XX times the first two columns of WQW_Q. Each head still sees all of XX.

transpose(1, 2) moves the head dimension in front of TT. Each pair of batch index and head now holds an ordinary (T, D_head) matrix. Everything after this treats BB and HH alike, as batch dimensions.

Scores and weights are computed exactly as before, once per head, giving (B, H, T, T). The scale is Dhead\sqrt{D_{\text{head}}}, the length of each head's queries and keys.

weights @ V gives each head its own output, (B, H, T, D_head).

transpose(1, 2) moves the heads back behind TT, and reshape lays them side by side: (B, T, H, D_head) becomes (B, T, D). That is the concatenation.

It has to be reshape rather than view. After a transpose the tensor is no longer laid out contiguously in memory, and reshape copies it when needed.

Finally WOW_O, another (D, D) matrix, mixes information across heads. The output has the shape of XX.

Because each head works in D/HD / H dimensions, HH heads cost about as much as one head of full width.

Attention is nearly a Transformer. Two pieces are missing: a sense of order, and somewhere to process what was gathered.

X = token_emb(tokens) + pos_emb(positions)    # (T, D) + (T, D)
thedogchasedtheballoutput for dog0.300.320.380.22theballchasedthedogoutput for dog0.300.320.380.22identicalthe0.00.01.00.0+pos 00.10.20.10.0=0.10.21.10.0x0the0.00.01.00.0+pos 3−0.2−0.10.10.2=−0.2−0.11.10.2x3×no extra dimension

04 Transformer

Attention does not see order.

Swap “dog” and “ball”. The sentence means something else, but the output for “dog” is exactly the same. Every step so far treats the tokens as a set. Scores depend on the vectors, never on where they sit.

For the same reason, the two copies of “the” get identical outputs.

The fix is to add a position vector to every token vector before the first block. It has the same width DD, and it is added entry by entry.

[0.0, 0.0, 1.0, 0.0]+[0.1, 0.2, 0.1, 0.0]=[0.1, 0.2, 1.1, 0.0]\begin{array}{rl} & [0.0,\ 0.0,\ 1.0,\ 0.0] \\ + & [0.1,\ 0.2,\ 0.1,\ 0.0] \\ \hline = & [0.1,\ 0.2,\ 1.1,\ 0.0] \end{array}

Position is not a new axis. The vector is still DD numbers wide. Each number now mixes what the token is with where it is.

The same word at two positions now starts from two different vectors, so attention can tell them apart.

Position vectors can be fixed sine and cosine waves, as in the original Transformer, or learned like the token embeddings. The model at the end of this essay learns them. The values here are made up.

class TransformerBlock(nn.Module):    def __init__(self, D, H):        super().__init__()        self.attn = MultiHeadAttention(D, H)        self.norm1 = nn.LayerNorm(D)        self.mlp = nn.Sequential(            nn.Linear(D, 4 * D), nn.GELU(), nn.Linear(4 * D, D))        self.norm2 = nn.LayerNorm(D)    def forward(self, X):        X = self.norm1(X + self.attn(X))        X = self.norm2(X + self.mlp(X))        return X
(T, D)multi-head attentiontokens exchange informationLayerNormadd and normalizeinput added back, rescaledMLPeach token on its ownLayerNorm(T, D)same shape outready for the next block

04 Transformer/ Block

Attention, then a small network per token.

A Transformer block wraps multi-head attention with a few standard parts. Attention is the only place in the block where tokens exchange information.

A residual connection adds the block's input to the attention output, so attention only has to learn a change to each vector. LayerNorm then rescales each token's vector to mean 0 and variance 1, followed by a learned scale and shift.

The MLP is two linear layers with a nonlinearity between them, usually 4D4D wide inside. It is applied to every token separately, with the same weights. Attention moved information between tokens. The MLP transforms what each token now holds.

A second residual connection and LayerNorm finish the block. The output has shape (T, D), like the input.

This is the arrangement of the original Transformer. Many recent models normalize before each part instead, X + self.attn(self.norm1(X)), which trains more reliably in deep stacks.

class Transformer(nn.Module):    def __init__(self, vocab, D, H, n_blocks, max_len):        super().__init__()        self.token_emb = nn.Embedding(vocab, D)        self.pos_emb = nn.Embedding(max_len, D)        self.blocks = nn.Sequential(            *[TransformerBlock(D, H) for _ in range(n_blocks)])        self.out = nn.Linear(D, vocab)    def forward(self, tokens):                    # (B, T)        positions = torch.arange(tokens.size(1), device=tokens.device)        X = self.token_emb(tokens) + self.pos_emb(positions)        X = self.blocks(X)                        # (B, T, D)        return self.out(X)                        # (B, T, vocab)
thedogchasedtheballembedding + position(B, T, D)Transformer blockn_blocks = 3(B, T, D) throughoutLinearscores over the vocabulary(B, T, vocab)

04 Transformer/ Model

Stack the blocks.

A model starts with two lookup tables, one row per vocabulary token and one row per position. Their sum is XX, shape (B, T, D). The position part has shape (T, D) and is broadcast over the batch.

XX passes through a Transformer block. Out comes a tensor of the same shape, every vector now informed by the others.

Because the shape never changes, blocks stack. Each block works on what the blocks before it gathered, so information can travel in several hops and be combined along the way.

A final linear layer turns every vector into one score per word in the vocabulary. A language model reads the scores at each position as a guess for the token that comes next.

T = scores.size(-1)ones = torch.ones(T, T, dtype=torch.bool, device=scores.device)mask = torch.triu(ones, diagonal=1)scores = scores.masked_fill(mask, float("-inf"))weights = torch.softmax(scores, dim=-1)
thedogchasedtheballthedogchasedtheball0.000.500.000.000.500.250.000.750.250.000.001.000.000.001.000.000.500.000.000.500.250.000.750.250.00−∞−∞−∞−∞−∞−∞−∞−∞−∞−∞0.210.580.210.000.001.00the scaled scores from beforechased (position 2) still sees "the ball" after itevery score above the diagonal is a later position→ set to −∞exp(−∞) = 0, so later positions get weight 0chased: exp(0, 1, 0) = 1.00, 2.72, 1.00 ÷ 4.72 = 0.21, 0.58, 0.21first token: only itself, weight 1.00last token: every position, as without the mask

04 Transformer/ Masked attention

No looking ahead.

A model that predicts the next token is trained on every position of a sentence at once. At “chased” it must guess “the”, so it may not see the words that follow. Plain attention lets every token see every other token.

The fix happens between the scaled scores and the softmax. Every score where a token looks at a later position, everything above the diagonal, is replaced by −∞-\infty.

Because e−∞=0e^{-\infty} = 0, those positions get weight 0, and softmax spreads each row over the positions that remain. The row of “chased” now covers three tokens instead of five.

e1e0+e1+e0=2.724.72=0.58\begin{aligned} &\frac{e^{1}}{e^{0} + e^{1} + e^{0}} \\ &= \frac{2.72}{4.72} = 0.58 \end{aligned}

The first token can only attend to itself. The last one sees the whole sentence, so its row is the same as without the mask. Nothing else in the model changes.

Every part of this model was already on the page. A dot product measures relevance. Softmax turns scores into weights. A weighted sum moves information.

Queries, keys and values make that learnable. Heads repeat it side by side, and blocks repeat it in depth. Large models use the same structure with far more dimensions, heads and blocks.

← Back to blog