Skip to content
ML Academy

Core day 39 of 60 · Week 8 · Part II · application depth

Attention and Transformers

A ticket-triage model reads "bag delayed refund" and a colleague shows an attention heat map in which "refund" gets 40% of one token's weight as "the reason" for the prediction. Can you compute such weights for three tokens by hand, give the shapes through the layer, and say what the map does and does not show?

By the end: You compute query, key and value projections, scaled scores, softmax weights and outputs for three tokens with d_k = 2, apply a causal mask, state the shape at every step, and explain why attention weights are not an explanation.

At a glance

Each token mixes all value vectors with softmax weights from query-key matches; shapes are fixed by n and d_k, cost grows with n squared, and weights are not reasons.

  • Query key value

    Three learned projections of the same token vectors, each (n, d_k).

  • Softmax over keys

    Scores QK^T / sqrt(d_k) are (n, n); each row of weights sums to 1.

  • Mask before softmax

    Future positions get minus infinity, so decoders cannot peek.

  • n squared cost

    Doubling the context quadruples the score matrix per head and layer.

  • Not an explanation

    A heat map shows how values were mixed; test it by masking tokens.

Recall · 5 min

No notes: (1) the shape of ABA B when AA is (3,2)(3, 2) and BB is (2,3)(2, 3); (2) what softmax does to a vector of scores and what it is applied over; (3) one reason a recurrent encoder struggles with a sentence of 200 tokens (Day 37). Today's layer answers the third.

Open due reviews

Sign in to record this block.

Concept · 10 min

A recurrent network pushes everything through one small state. In attention, every token looks directly at every other token. Think of a soft lookup table: each token asks with a query, offers a key to be matched on and a value to hand over. The picture breaks in one place: the roles are learned linear projections of the same vector, and mean only what training made them mean.

Attention is a matrix product with a softmax over keys - change a score, a scale or the mask and watch the weights move

Predict first

With the causal mask on, what does token 0 attend to in a three-token sequence?

Text description of the figure

A three-token example (flight, was, late) showing query, key and value vectors, the scaled score matrix, the row-wise softmax weights and the output vectors. Controls edit the query, key and value entries, the scaling and a causal mask, with presets for large scores and identical keys.

Four parts of one attention head Overview
  • Query

    What this token is looking for: q = x W_Q.

  • Key

    What this token can be matched on: k = x W_K.

  • Value

    What this token hands over if matched: v = x W_V.

  • Softmax over keys

    Turns one query's scores into weights that sum to 1.

Shapes through one head (n = 3 tokens, d_model = 3, d_k = 2) Diagram
X(3, 3): one rowper tokenQ = X W_Q(3, 2)K = X W_K(3, 2)V = X W_V(3, 2)Q K^T / sqrt(d_k)(3, 3) scoresSoftmax per row(3, 3): rows sum to 1Output = A V(3, 2): mixed values
  1. X(3, 3): one row per token
    • to K = X W_K
    • to V = X W_V
  2. Q = X W_Q(3, 2)
    • to Q K^T / sqrt(d_k)
  3. K = X W_K(3, 2)
    • to Q K^T / sqrt(d_k)
  4. V = X W_V(3, 2)
    • to Output = A V
  5. Q K^T / sqrt(d_k)(3, 3) scores
  6. Softmax per row(3, 3): rows sum to 1
  7. Output = A V(3, 2): mixed values

The only (n, n) objects are the scores and the weights; everything else has one row per token.

One head on six tokens - no mask, causal mask, and no scaling Illustration
Scaled, no maskspreadparceltoIzmirwaslateagainparcel.05.10.07.43.10.244.5to.15.42.02.19.13.084.6Izmir.25.11.10.28.05.205.3was.16.31.01.34.02.164.2late.07.04.32.09.16.324.7again.04.14.19.14.26.225.4

Encoder: every token reads all six. Each row sums to 1 (checked); columns do not. Average spread 4.8 of 6 keys.

Scaled, causal maskspreadparceltoIzmirwaslateagainparcel1.00-----1.0to.27.73----1.8Izmir.54.24.22---2.7was.20.37.01.42--3.0late.10.06.47.13.24-3.9again.04.14.19.14.26.225.4

Decoder: future keys are set to minus infinity before the softmax, so the upper triangle is exactly 0 and the first token reads only itself.

Unscaled scoresspreadparceltoIzmirwaslateagainparcel.00.00.00.91.00.091.4to.02.93.00.04.01.001.4Izmir.33.01.01.53.00.122.9was.03.37.00.58.00.032.4late.00.00.46.00.03.502.3again.00.05.15.05.49.273.6

Same Q and K without dividing by sqrt(16) = 4: scores 4x larger, rows sharper (average spread 2.3). Nothing was learned; only the scale changed.

Seeded random token vectors (d_model = 16) and random projections (d_k = 16); no training, so which token attends to which is arbitrary. Spread = 2 to the power of the row's entropy in bits: 1 = one key, 6 = uniform.

Rows are queries, columns keys. The mask zeroes the upper triangle; dropping the scale sharpens every row without anything being learned.

One transformer block, bottom to top Stack
  1. Token vectors plus positionsEmbeddings (Day 38) plus sinusoidal or learned position vectors.
  2. Multi-head self-attentionh heads in parallel, d_k = d_model / h, concatenated and mixed by W_O.
  3. Residual plus LayerNormAdd the input back, then normalize each token's vector.
  4. Feed-forward networkThe same two-layer MLP (often GELU, Day 32) applied to each position.
  5. Residual plus LayerNormAdd and normalize again; the output has the input's shape.

Stacks of these form encoders and decoders (Vaswani et al., 2017). Many current models normalize before each sublayer instead (pre-LN).

When does the n-squared part dominate a layer's compute? Illustration
1e71e91e111e131281k8k64kMultiply-adds per layer (log-log)n = 6kprojections + FFNscores + A Vcontext length n (tokens)multiply-adds

d_model = 1024: the per-token parts grow like n, the score parts like n squared, and they meet at n = 6 d = 6,144 tokens. Below that, longer context costs roughly linearly.

0%25%50%75%100%1281k8k64kShare of compute in the n-squared partd_model = 768: half the compute at n = 6 d = 4,608d_model = 1024: half the compute at n = 6 d = 6,144d_model = 4096: half the compute at n = 6 d = 24,576half at n = 6 dcontext length n (tokens)share of multiply-adds
  • d_model = 768 (47% at 4k tokens)
  • d_model = 1,024 (40% at 4k tokens)
  • d_model = 4,096 (14% at 4k tokens)

Share of a layer's multiply-adds spent on the n x n part. Wider models push the crossover out: at 4,096 tokens a d_model = 4,096 layer spends only a seventh of its compute there.

Exact multiply-add counts for the forward pass of one layer, batch 1, ignoring softmax, normalisation and memory traffic. Memory is a separate story: a naive implementation also stores the n x n weights per head.

The per-token projections and feed-forward network grow linearly; scores and the weighted sum grow with n squared and take over at about six times d_model.

Recurrence versus self-attention Comparison
CriterionRecurrent network (Day 37)Self-attention
Steps between two distant positionsUp to n steps, a product of factorsOne step: every pair is scored directly
Parallel over positionsNo: step t waits for step t - 1Yes, within a layer
Memory of the pastOne fixed-size stateEvery position, in the context window
Cost growth with length nLinear in nQuadratic in n per head and layer
  • Project the token matrix XX (n,dmodel)(n, d_{model}) into QQ, KK, VV, each (n,dk)(n, d_k); scores QK⊤/dkQK^\top / \sqrt{d_k} are (n,n)(n, n); softmax over keys makes each row sum to 1; the output is weights times VV.
  • The dk\sqrt{d_k} keeps scores in a range where the softmax is not saturated.
  • Multi-head attention runs hh maps in parallel and mixes them with WOW_O. Self-attention ignores order, so positions are added; blocks add residuals, Layer normalization and a feed-forward network.
  • A Causal mask sets future scores to −∞-\infty before the softmax.
  • Cost: n2n^2 score entries per head and layer.
  • Weights are not explanations by default (Jain and Wallace, 2019; Wiegreffe and Pinter, 2019, show it depends on the definition): treat a heat map as a hypothesis and test it by perturbation.

Assumptions

  • Pairwise interactions between all positions are worth their n2n^2 cost; cheap for short sequences, not for very long ones.

  • Order information is supplied (positions) and, for generation, future tokens are masked.

  • The training objective rewards the roles you hope heads play; interpretation of a head is a hypothesis.

Sign in to record this block.

Application and workplace decision · 10 min

Three tokens "bag", "delayed", "refund" are one-hot rows of XX (3×33 \times 3), so each token's query is the matching row of WQW_Q (hand-set, not learned). With dk=2d_k = 2: WQW_Q rows (1,0),(0,1),(1,1)(1,0), (0,1), (1,1); WKW_K rows (1,0),(1,1),(0,1)(1,0), (1,1), (0,1); WVW_V rows (2,0),(0,2),(1,1)(2,0), (0,2), (1,1).

By hand, for token 0:

  • (a) the raw scores q0⋅kjq_0 \cdot k_j and (b) the scaled scores;
  • (c) the softmax weights and (d) the output ∑ja0jvj\sum_j a_{0j} v_j.

Then run the code, compare, and read the causal weights.

Python

import numpy as np
np.set_printoptions(precision=3, suppress=True)
X = np.eye(3)                      # 3 tokens, d_model = 3: bag, delayed, refund (one-hot)
Wq = np.array([[1, 0], [0, 1], [1, 1]], dtype=float)   # (d_model, d_k) = (3, 2), hand-set
Wk = np.array([[1, 0], [1, 1], [0, 1]], dtype=float)
Wv = np.array([[2, 0], [0, 2], [1, 1]], dtype=float)
Q, K, V = X @ Wq, X @ Wk, X @ Wv   # each (3, 2)
d_k = Q.shape[1]

def softmax(z):
    z = z - z.max(axis=-1, keepdims=True)
    e = np.exp(z)
    return e / e.sum(axis=-1, keepdims=True)

scores = Q @ K.T / np.sqrt(d_k)    # (3, 3): query i against key j
A = softmax(scores)                # softmax over keys: each row sums to 1
out = A @ V                        # (3, 2)
print("scores / sqrt(d_k):\n", scores)
print("weights (rows sum to 1):\n", A)
print("output:\n", out)

mask = np.triu(np.ones((3, 3), dtype=bool), k=1)   # True above the diagonal = future positions
A_causal = softmax(np.where(mask, -np.inf, scores))
print("causal weights:\n", A_causal)
print("token 0 attends only to itself:", bool(A_causal[0, 0] == 1.0))
print("shapes: Q", Q.shape, "scores", scores.shape, "out", out.shape)

This code is executed in the repository's checks (pnpm lesson:py / lesson:sql). It is not run on this site; copy it into your notebook.

Sign in to record this block.

Mini-assessment · 8 min

Your first answer is your evidence. You can re-answer later in practice mode; that is labelled practice.

Question 1 · easy

Which statement about attention weights is best supported?
Options

How confident are you? (optional, used for calibration)

Question 2 · medium

A model's context is raised from 2,000 to 8,000 tokens. By what factor does the attention score matrix ($n \times n$ per head and layer) grow?
times larger

How confident are you? (optional, used for calibration)

Question 3 · hard

A team trains a small decoder-only model on operational event logs to predict the next event. Training and validation loss both fall to almost zero within a few hundred steps, yet generation from a prompt is nonsense. Which diagnosis fits best, and what is the first check?
Options

How confident are you? (optional, used for calibration)

Question 4 · medium

A transformer layer uses multi-head self-attention with $d_{model} = 768$ and 12 heads, so $d_k = 64$ per head. Each head has its own $W_Q$, $W_K$ and $W_V$ of shape (768, 64); the 12 head outputs are concatenated and mixed by one $W_O$ of shape (768, 768). Ignoring biases, how many parameters does the attention sublayer have?
parameters

How confident are you? (optional, used for calibration)

Question 5 · hard

A teammate removes the $1/\sqrt{d_k}$ factor from a from-scratch transformer ($d_k = 64$), arguing that $W_Q$ and $W_K$ are learned and will absorb any constant. Before training, you sample one head on random unit-variance inputs with 16 tokens and print: the standard deviation of the scores is 8.0 without the factor (1.0 with it), and the mean of each row's largest attention weight is 0.84 without it (0.25 with it; uniform would be 0.06). How do you assess the change?
Options

How confident are you? (optional, used for calibration)

Sign in to complete lessons and keep your progress.

Technical depth · optional · 32 min in total

Open only what you need. Nothing here is required to complete the day; the assessment covers the core above.

The layer, defined3 minScaled dot-product attention as a matrix formula and row by row, what is learned, and the assumptions written as properties of the equation.
Scaled dot-product attention Formula
Attention⁡(Q,K,V)=softmax⁡ ⁣(QK⊤dk)V\operatorname{Attention}(Q, K, V) = \operatorname{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V
QK⊤QK^\top
(n, n): dot product of query i with key j
dk\sqrt{d_k}
rescales scores so their spread does not grow with d_k
softmax⁡\operatorname{softmax}
applied to each row, that is over keys
VV
(n, d_k): the vectors being mixed
Attention for one query, written row by row Formula
oi=∑j=1naij vj,aij=exp⁡(sij)∑lexp⁡(sil),sij=qi⋅kjdk,qi=xiWQo_i = \sum_{j=1}^{n} a_{ij}\, v_j, \qquad a_{ij} = \frac{\exp(s_{ij})}{\sum_{l} \exp(s_{il})}, \qquad s_{ij} = \frac{q_i \cdot k_j}{\sqrt{d_k}}, \qquad q_i = x_i W_Q
xix_i
the d_model-vector of token i (embedding plus position)
qi,kj,vjq_i, k_j, v_j
x_i W_Q, x_j W_K, x_j W_V: three learned linear views of the same tokens
sijs_{ij}
how well query i matches key j, scaled so its spread does not grow with d_k
aija_{ij}
softmax over j: non-negative, sums to 1 across keys for each query i
oio_i
a weighted average of the values: it always lies inside their convex hull

The matrix form in the formula figure is these n rows stacked. What is learned: W_Q, W_K, W_V (and W_O with several heads); the weights a_ij are recomputed for every input and carry no parameters.

One head as an algorithm, with the cost of each step Sequence
  1. 1. Project

    Q = X W_Q, K = X W_K, V = X W_V: one row per token, about 3 n d_model d_k multiply-adds

  2. 2. Score

    S = Q K^T / sqrt(d_k), an (n, n) matrix: about n^2 d_k multiply-adds

  3. 3. Mask

    Set forbidden entries of S (future keys, padding) to minus infinity

  4. 4. Softmax per row

    A = softmax(S) over keys: every row is non-negative and sums to 1

  5. 5. Mix

    O = A V: each output row is a weighted average of value rows, about n^2 d_k multiply-adds

Steps 2 and 5 are the n-squared part; steps 1 and the output projection are per token.

Three bets the formula makes Overview
  • Pairwise relevance

    Every pair (i, j) is scored: any token may matter to any other. The price is n^2.

  • Order from outside

    The formula never reads i or j as numbers, so positions must enter through x_i.

  • A learned similarity

    q_i . k_j is a dot product in a learned space: 'relevant' means only what training rewarded.

Softmax weighting by hand on three tokens4 minOne query pushed through scores, scale, softmax and value mixing on a three-event bank session, then the application's own weights to check your hand work.
Softmax weighting by hand: a bank session of three events (d_k = 4) Sequence
  1. Tokens

    login, new-device, transfer. Keys: k0 = (1, 0, 0, 0), k1 = (1, 1, 0, 1), k2 = (0, 1, 1, 0)

  2. Query of transfer

    q2 = (1, 1, 0, 1). Values (2-d risk features): v0 = (1, 0), v1 = (0, 1), v2 = (1, 1)

  3. Raw scores

    q2 . k0 = 1, q2 . k1 = 1 + 1 + 0 + 1 = 3, q2 . k2 = 0 + 1 + 0 + 0 = 1

  4. Scale by 1 / sqrt(4)

    (0.5, 1.5, 0.5)

  5. Exponentiate

    e^0.5 = 1.649, e^1.5 = 4.482, e^0.5 = 1.649; sum 7.779

  6. Normalise

    Weights (0.212, 0.576, 0.212): transfer reads mostly from new-device, and the row sums to 1

  7. Mix the values

    o2 = 0.212 (1, 0) + 0.576 (0, 1) + 0.212 (1, 1) = (0.424, 0.788)

  8. Without the scale

    softmax(1, 3, 1) = (0.107, 0.787, 0.107): sharper from the same vectors

Hand-set vectors (not a trained fraud model), verified in numpy; on paper only the exponentials need a calculator. Read o2 as an average: mostly v1, a little of v0 and v2. Nothing in the weights says which event made the session risky. The application starts one step earlier, from one-hot tokens and hand-set projections.

Check your hand result: today's weights without a mask (rows sum to 1) Grid
key →
query ↓bagdelayedrefund
bag0.4010.4010.198
delayed0.1980.4010.401
refund0.2480.5030.248

Printed by the application code. Outputs: bag (1.000, 1.000), delayed (0.797, 1.203), refund (0.745, 1.255). 'refund' attends most to 'delayed', not to itself.

Why divide by the square root of d_k4 minThe variance of a dot product derived term by term, what it does to the softmax and its gradient, then measured on random vectors.
The square-root argument, derived Sequence
  1. Assume

    Components q_c and k_c independent, mean 0, variance 1 (roughly true at initialisation)

  2. One term

    E[q_c k_c] = E[q_c] E[k_c] = 0 and Var(q_c k_c) = E[q_c^2] E[k_c^2] = 1

  3. Sum of d_k terms

    q . k has mean 0 and variance d_k (independent terms add their variances)

  4. Spread

    SD(q . k) = sqrt(d_k): 2 at d_k = 4, 4 at d_k = 16 (20,000 random pairs in numpy: 3.99)

  5. Rescale

    Divide by sqrt(d_k): the scaled score has variance 1 at every width

  6. Why it matters

    Softmax slopes are a_i (1 - a_i) and -a_i a_j: max 0.22 for a uniform 3-way row, 0.02 for (0.98, 0.01, 0.01)

Saturated rows pass almost no gradient to the scores, so the projections barely learn. Vaswani et al. (2017) give this variance argument for the scale.

Spread (standard deviation) of q . k for unit-variance components Chart
  • d_k = 42
  • d_k = 164
  • d_k = 648
  • d_k = 25616

A sum of d_k unit-variance products has variance d_k, so its spread is sqrt(d_k). Dividing by sqrt(d_k) brings it back to 1.

Without the scale, rows collapse toward one key as d_k grows Illustration
0481216248163264128256Spread of raw scores q . kd_k = 2: measured 1.34, sqrt = 1.41d_k = 4: measured 2.06, sqrt = 2.00d_k = 8: measured 2.87, sqrt = 2.83d_k = 16: measured 3.96, sqrt = 4.00d_k = 32: measured 5.67, sqrt = 5.66d_k = 64: measured 8.40, sqrt = 8.00d_k = 128: measured 11.28, sqrt = 11.31d_k = 256: measured 16.03, sqrt = 16.00dashed: sqrt(d_k)d_k (log scale)standard deviation

A dot product of d_k unit-variance products has standard deviation sqrt(d_k): measured 1.3, 2.1, 2.9, 4.0, 5.7, 8.4, 11.3, 16.0.

02468248163264128256Effective keys per row (of 8)divided by sqrt(d_k)raw scoresd_k (log scale)keys a row reads

Scaled rows stay spread over 5.5 to 6.2 of 8 keys at every d_k. Unscaled rows collapse to 1.2 keys at d_k = 256: near one-hot, so almost no gradient reaches the other keys.

Seeded random queries and keys with independent unit-variance components (the situation at initialisation), 40 draws of 8 tokens per d_k. Effective keys = 2 to the power of the row entropy in bits.

Measured on random queries and keys, the situation at initialisation. The scale keeps the softmax in its responsive range at every width.

What a larger score spread does to the softmax Sequence
  1. Scaled scores (0.707, 0.707, 0)

    Weights (0.401, 0.401, 0.198): today's token 0

  2. Unscaled (1, 1, 0)

    Weights (0.422, 0.422, 0.155): slightly sharper

  3. Eight times larger (8, 8, 0)

    Weights (0.500, 0.500, 0.0002): the third key is ignored

  4. Consequence

    Near-one-hot rows pass almost no gradient to the other keys

The causal mask by hand, and the library defaultsadvanced4 minMasking worked on the same three events, causal and padding masks, the two opposite boolean conventions in PyTorch, and the head-averaged weight map.
The causal mask by hand, on the same session Sequence
  1. Row of new-device

    Query q1 = (0, 1, 1, 0); scaled scores against keys 0, 1, 2: (0, 0.5, 1.0)

  2. Unmasked softmax

    (0.186, 0.307, 0.506): the largest weight goes to transfer, an event that has not happened yet

  3. Mask the future

    Set s_12 to minus infinity; exp(minus infinity) = 0, so the key drops out of the sum

  4. Masked softmax

    Over keys 0 and 1 only: e^0 = 1, e^0.5 = 1.649, so (0.378, 0.622, 0)

  5. Row of login

    Only key 0 is allowed, so its weight is exactly 1 whatever the scores

  6. Row of transfer

    The last row sees every earlier key and is unchanged by the mask

Verified in numpy. The mask is applied to the scores before the softmax; zeroing weights after it would leave rows that no longer sum to 1.

The causal mask on today's three tokens: weights after masking Grid
key →
query ↓bagdelayedrefund
bag (token 0)1.0000.0000.000
delayed (token 1)0.3300.6700.000
refund (token 2)0.2480.5030.248

Masked scores become minus infinity BEFORE the softmax, so the upper triangle is exactly 0 and every row still sums to 1. Token 0 gives itself weight 1; the last row sees every key and is unchanged. Computed by the application code.

Two masks, one mechanism: minus infinity before the softmax Comparison
CriterionCausal maskPadding mask
RemovesKeys after the query positionPadded keys
Protects againstA decoder reading the token it must predictOutputs that depend on what a sentence was batched with
Unit testChange a later token; earlier outputs stay fixedAdd padding; real tokens' outputs stay fixed
PyTorch attention defaults that change results Table
SettingDefaultWhy it matters
SDPA scaleNone = 1 / sqrt(E)Pass your own only on purpose; the default is the paper's scale
SDPA is_causalFalseDecoders must ask for the mask; passing it with attn_mask is an error
SDPA boolean attn_maskTrue = may attendThe opposite of MultiheadAttention's key_padding_mask
MHA key_padding_maskTrue = padding, ignoreInvert with ~mask when moving between the two APIs
MHA batch_firstFalse: (seq, batch, feature)A silent transpose if your tensors are batch first
MHA need_weights, average_attn_weightsTrue, TrueThe returned map is averaged over heads, not one head

Read from the PyTorch 2.14 pages for scaled_dot_product_attention and MultiheadAttention. A row whose keys are all masked gives a softmax of nothing, which comes out as NaN. Because returned maps are head averages, a 'head that attends to refund' may not exist.

Properties and the tests that check them3 minPermutation equivariance, convex mixing, input-dependent weights, parameter count and quadratic cost, each written in symbols and tied to a cheap test.
Properties of self-attention, each with the test that checks it Table
PropertyIn symbolsConsequenceTest
Permutation equivariantAttn(PX) = P Attn(X) without positionsWord order is invisible until positions are addedPermute tokens; outputs permute (the lab)
Convex mixingo_i in the convex hull of the valuesOne head cannot output beyond its values; W_O and the MLP mustCheck o_i against min and max of V
Input-dependent weightsa_ij is a function of XThe map changes per example; parameters do notRecompute the map on two inputs
Parameters independent of nW_Q, W_K, W_V, W_O sized by d_model onlyLonger context costs activations, not weightsCount parameters at two context lengths
Quadratic scoresn x n per head and layerTime and naive memory grow with n squaredProfile at n and 2n
Masked rowsa_ij = 0 for masked j, rows still sum to 1No leak from the future or from paddingChange a later token; earlier outputs fixed

Run these tests before reading any heat map. Equivariance follows from the row formula: permuting X's rows permutes Q, K and V alike, so S and A permute on both axes and O on its rows. Convex mixing is why blocks need the residual path and the MLP: attention alone only averages what is there.

Multiple heads, and sharing keys and valuesadvanced3 minHow heads split the width, what concatenation and W_O do, and why decoders share key-value heads.
Multi-head attention: shapes (d_model = 8, two heads) Diagram
X(n, 8)Head 1own W_Q, W_K, W_V;output (n, 4)Head 2own W_Q, W_K, W_V;output (n, 4)Concatenate(n, 8)Mix with W_O(8, 8); output (n, 8)
  1. X(n, 8)
    • to Head 2
  2. Head 1own W_Q, W_K, W_V; output (n, 4)
    • to Concatenate
  3. Head 2own W_Q, W_K, W_V; output (n, 4)
  4. Concatenate(n, 8)
  5. Mix with W_O(8, 8); output (n, 8)

With d_k = d_model / h, all heads together cost about as much as one full-width head. Heads can specialise; nothing forces them to.

How heads share the width Comparison
CriterionOne full-width headMulti-head, h headsGrouped-query
Query heads1, width d_modelh, width d_model / h eachh
Key-value heads1, width d_modelh, one per query headg < h, each shared by h / g query heads
Attention weights (no biases)4 d_model^24 d_model^2: the width is split, not multipliedW_K and W_V shrink to g / h of their width
Decoder cacheKeys and values at full widthSame as one headShrinks by h / g, at a small quality cost

Heads change how the width is used, not how many weights there are. Grouped-query attention is Ainslie et al. (2023); the decoder cache is Day 42's topic.

Positions and order3 minSelf-attention is order-blind until positions are added; what sinusoidal codes encode and one alternative.
Sinusoidal positions - similarity that depends only on distance Illustration
Cosine between position codes (0 to 31)0088161624243131

Strong diagonal band: nearby positions have similar codes, distant ones less so (colour runs from cosine 0.45 to 1). The pattern repeats along every diagonal.

00.250.50.751010203040PE(p) . PE(p + k), scaled to 1 at k = 0three starts, one curveoffset k (positions apart)similarity
  • start at p = 0
  • start at p = 25
  • start at p = 200

Similarity depends only on the distance k, not on where you start: the three curves differ by at most 8e-16. The original paper chose sinusoids so that relative offsets would be easy to attend to.

Exact sinusoidal encodings (Vaswani et al., 2017) with d = 64; no data. Other schemes differ: learned position vectors have no built-in structure, and ALiBi (Press et al., 2022) adds a distance penalty to the scores instead.

Without positions, attention is order-blind. Sinusoidal codes make the similarity of two positions a function of their offset alone.

Sinusoidal positions (Vaswani et al., 2017) Formula
PE(pos,2i)=sin⁡ ⁣(pos100002i/dmodel),PE(pos,2i+1)=cos⁡ ⁣(pos100002i/dmodel)PE_{(pos, 2i)} = \sin\!\left(\frac{pos}{10000^{2i/d_{model}}}\right), \quad PE_{(pos, 2i+1)} = \cos\!\left(\frac{pos}{10000^{2i/d_{model}}}\right)
pospos
position of the token in the sequence
ii
index of the dimension pair: low i varies fast, high i slowly

Without positions, permuting the input tokens only permutes the outputs (the lab checks this), so word order would be invisible. ALiBi (Press et al., 2022) adds no position vectors: it subtracts a distance-growing penalty from each score, and extrapolated to longer inputs in their experiments.

Cost, memory and the n-squared part3 minScore-matrix memory per head, and why exact attention can still be fast.
Score-matrix entries per head and layer, stored at 2 bytes each Chart
  • 1,024 tokens1,048,576 entries2.1 MB
  • 2,048 tokens4,194,304 entries8.39 MB
  • 4,096 tokens16,777,216 entries33.55 MB

Each doubling of the length quadruples the entries. FlashAttention (Dao et al., 2022) computes exact attention in tiles without storing the matrix; compute still grows with n squared, but dominates a layer only past about 6 d_model tokens. Generation adds a key-value cache that grows with context and layers (Day 42).

Attention weights and explanations3 minWhat a heat map shows, and the perturbation test that checks it.
Testing a heat map instead of believing it Diagram
Highlightedtokenfor example refundMask or replace itkeep everything elseChange inprobabilityover many ticketsLarge andconsistent?Evidence of reliancenot causationDecorationdo not present it as whyyesno
  1. Highlighted tokenfor example refund
  2. Mask or replace itkeep everything else
  3. Change in probabilityover many tickets
  4. Large and consistent?
    • yes: to Evidence of reliance
  5. Decorationdo not present it as why
  6. Evidence of reliancenot causation

Compare with a simple attribution method as well. Jain and Wallace (2019): weights often disagree with gradient-based importance, and different weights can give the same prediction. Wiegreffe and Pinter (2019): whether weights explain depends on the definition of explanation; test, do not assume.

What a staff-level answer addsadvanced2 minNuances and misreadings that separate an experienced answer from a textbook one.
The textbook answer and what a staff-level answer adds Comparison
CriterionCommon readingStaff-level reading
What attention isA learned map of what mattersA data-dependent mixing matrix; projections are the parameters
CostOne n-squared numberCompute is n squared always; memory only without a fused kernel
Heads'Multi-head means more parameters'The width is split, not multiplied
The heat map'The model attends to X, so X is the reason'One head, one layer, averaged maps and residuals sit in between
Raising the context'More context cannot hurt'First measure where the label evidence sits in your inputs

Memory anchor

Softmax over keys, scaled by sqrt(d_k), masked before the softmax, n squared in cost - and a weight is not a reason.

Useful for

  • Reading a transformer diagram or a paper and knowing what each matrix, shape and mask is doing.

  • Reasoning about context-length cost before changing a limit (long contracts, claim files, call transcripts), and about why decoder models need a causal mask.

  • Setting honest expectations for attention heat maps shown to users (agents triaging tickets, clinicians reading risk flags, analysts reviewing flagged transactions).

Not suitable for

  • Explaining individual predictions - attention weights alone do not do this.

  • Training or tuning a production transformer; this day is the mechanism and its shapes at three tokens.

  • Judging model quality from the architecture name; a transformer must still beat strong baselines on your task (Days 27 and 35).

Traps

Presenting attention heat maps as the reason

A highlighted word looks like an explanation, and users, auditors and sometimes the team itself treat it as one, although the same prediction can come from different weights.

How to avoid it

Label it as a diagnostic, run a perturbation test (mask or remove the highlighted tokens), and compare with a second attribution method before showing it as a reason.

A missing or mismatched mask

Without a causal mask a decoder leaks the target and reports excellent loss; with padding unmasked, padded positions take attention weight and results change with batch composition.

How to avoid it

Unit-test that changing a later token does not change earlier outputs, and that adding padding does not change real tokens' outputs.

Senior reasoning

A senior reads attention as a data-dependent mixing matrix with a known cost and known failure modes: missing masks, order blindness, quadratic memory, and interpretation traps. They reduce a claim about a model to a shape check, a unit test (mask, padding, permutation) or a perturbation experiment, and they ask where the label evidence sits in the input before paying for context.

Interview question

"Walk me through scaled dot-product attention for three tokens. Why scale, why mask, and why not trust the heat map?"

Recall cards scheduled after completion

  • Give the shapes in single-head attention for n tokens, and say what the softmax is taken over.
  • What does quadratic complexity mean for context length, and what is the explanation caveat?

Reviewed on a 1 · 3 · 7 · 14 · 30-day schedule; a miss returns the card to 1 day.

References

Optional sections

Deep dive · 10 min

Why dk\sqrt{d_k}. If the components of qq and kk are independent with zero mean and unit variance, q⋅kq \cdot k has mean 0 and variance dkd_k. For dk=64d_k = 64 the standard deviation is 8, so raw scores differ by many units, the softmax approaches a one-hot vector, and its gradients vanish. Dividing by dk\sqrt{d_k} restores unit variance.

Parameter counts. For dmodeld_{model} with hh heads of size dmodel/hd_{model}/h, the four attention matrices WQ,WK,WV,WOW_Q, W_K, W_V, W_O hold 4dmodel24 d_{model}^2 weights (without biases) whatever hh is; a feed-forward block with a 4x hidden expansion holds 8dmodel28 d_{model}^2. At dmodel=512d_{model} = 512 that is about 1.0 million and 2.1 million per block.

Masks. A padding mask removes padded keys so that batch composition does not change results; a causal mask removes future keys. Both are added to the scores before the softmax. Cross-attention takes queries from the decoder and keys and values from the encoder.

Where this goes. Decoder-only models stacked from these blocks and trained on next-token prediction are the LLMs of Days 42-45; the cost of attention is why context length, caching and retrieval are engineering decisions (Days 42, 48 and 52).

Hands-on lab: One transformer block in numpy - heads, positions, residual + LayerNorm, mask tests, scaling · 20 min

Build a two-head block for six tokens and test its properties: output shape, normalised rows, permutation behaviour without positions, loss of that property with positions, causal-mask isolation of earlier outputs, the effect of the dk\sqrt{d_k} scale, and the size of the score matrix at three context lengths (2 bytes per entry is an assumption for half precision). Then remove the residual connection and describe what changes in the output statistics.

import numpy as np

rng = np.random.default_rng(39)
n, d_model, heads = 6, 8, 2
d_k = d_model // heads

def softmax(z):
    z = z - z.max(axis=-1, keepdims=True)
    e = np.exp(z)
    return e / e.sum(axis=-1, keepdims=True)

def layer_norm(x, eps=1e-5):
    return (x - x.mean(-1, keepdims=True)) / np.sqrt(x.var(-1, keepdims=True) + eps)

Wq, Wk, Wv = (rng.normal(0, 0.5, (heads, d_model, d_k)) for _ in range(3))
Wo = rng.normal(0, 0.5, (heads * d_k, d_model))

def block(X, causal=False):
    outs = []
    for h in range(heads):
        Q, K, V = X @ Wq[h], X @ Wk[h], X @ Wv[h]            # each (n, d_k)
        s = Q @ K.T / np.sqrt(d_k)                           # (n, n)
        if causal:
            s = np.where(np.triu(np.ones((len(X), len(X)), bool), 1), -np.inf, s)
        outs.append(softmax(s) @ V)                          # (n, d_k)
    mixed = np.concatenate(outs, axis=-1) @ Wo               # (n, d_model)
    return layer_norm(X + mixed)                             # residual, then LayerNorm

def sinusoid(n, d):
    pos, i = np.arange(n)[:, None], np.arange(d // 2)[None]
    ang = pos / 10000 ** (2 * i / d)
    return np.concatenate([np.sin(ang), np.cos(ang)], axis=-1)

X = rng.normal(size=(n, d_model))
Y = block(X)
print("block output shape:", Y.shape, "| row means ~0:", np.allclose(Y.mean(-1), 0, atol=1e-8))
perm = rng.permutation(n)
print("no positions - permuting tokens permutes outputs:", np.allclose(block(X[perm]), Y[perm]))
P = sinusoid(n, d_model)
print("with positions - still only permutes:", np.allclose(block(X[perm] + P), block(X + P)[perm]))
X2 = X.copy()
X2[-1] += 5.0                                                # change only the last token
print("causal: earlier outputs unchanged:", np.allclose(block(X2, True)[:-1], block(X, True)[:-1]))
print("non-causal: earlier outputs change:", not np.allclose(block(X2)[:-1], Y[:-1]))
q, k = rng.normal(size=(2000, 64)), rng.normal(size=(2000, 64))
dots = (q * k).sum(1)
print("std of q.k for d_k=64: raw %.1f, scaled %.1f" % (dots.std(), (dots / 8).std()))
for n_tok in (1_000, 8_000, 32_000):
    print(f"n={n_tok:6d}: {n_tok**2:.1e} score entries per head = {n_tok**2 * 2 / 1e6:7.0f} MB at 2 bytes")
Interview practice · 10 min

Your team's insurance claims-document classifier gets a 4x longer context. Latency and memory jump by more than 4x. Explain why and what you would try before buying bigger GPUs.

Content version 6 · last verified 2026-10-18 · depends on numpy