Attention mechanism
Scaled dot-product attention, the operation that defines the modern LLM. How a sequence refines itself by having each position look at every other. Why dot product as similarity, why softmax, why the √d_k denominator. Causal masking for autoregressive generation, and the quadratic compute cost that motivates much of the rest of the tutorial.
The transformer is mostly attention. The rest is supporting cast: embeddings, feedforward layers, normalizations, residuals. Attention is what does the work of letting one position in a sequence look at every other position and decide what to take from each.
The operation is one matrix equation that takes three inputs and produces one output. It runs in in the sequence length and costs roughly half the FLOPs of a modern LLM forward pass. It explains, simultaneously, why transformers are so capable and why they are so expensive.
This chapter derives the attention formula from first principles. By the end, the reader should be able to write scaled dot-product attention in numpy, justify the denominator from a variance calculation, and explain why causal masking is added to the scores rather than to the outputs. Everything else in the tutorial is downstream of this one operation: multi-head attention, the transformer block, scaling, RLHF, inference optimization, agentic systems.
The setup: why attention exists
Before attention, sequence models did two things. They used recurrence (RNNs and LSTMs, which pass a hidden state from one position to the next), or they used convolution (TCNs and similar, which compose locally with growing receptive fields). Both ideas worked. Both had structural limits that became increasingly hard to engineer around as sequences got longer.
The RNN limit is bandwidth. Information at position has to travel through the hidden state to reach position , and that hidden state is a fixed-size vector. By the time it has carried position ‘s contribution across hundreds of steps, the signal has been overwritten many times by intermediate positions; long-range dependencies blur into the recurrent average. Training is also sequential (position ‘s computation depends on position ‘s), so a long sequence is processed step by step, which leaves modern accelerators idle.
The CNN limit is locality. A 1D convolution with kernel width sees only tokens at a time. To see a span of length , the stack needs depth proportional to . Long-range information takes many layers to propagate, and even when it does propagate, the network has built in a structural preference for nearby positions over distant ones.
The 2017 breakthrough was to ask a sharper question. What if every position could look at every other position directly, with the strength of each look determined not by physical distance but by content? Position 7 could attend to position 1 just as cheaply as it attends to position 6, provided the two were relevant to each other. The hidden-state bandwidth bottleneck disappears because every position has direct access to every other position’s information. The locality bias disappears because there is no locality anymore; the operation is permutation-symmetric, and positional information is added back in as a separate signal (Chapter 6).
Attention as a concept predates the transformer by three years. Bahdanau, Cho, and Bengio (2014, arxiv.org/abs/1409.0473) introduced attention into neural machine translation as a learned alignment mechanism: a decoder generating a target sentence could “look at” specific positions in the source sentence rather than condensing the whole source into a single hidden vector. Luong, Pham, and Manning (2015, arxiv.org/abs/1508.04025) replaced Bahdanau’s feedforward score with the cheaper dot product. Vaswani et al. (2017, arxiv.org/abs/1706.03762) then made the structural leap: don’t use attention as an auxiliary alignment mechanism for an RNN-based translator. Use it as the entire architecture. The paper’s title (Attention Is All You Need) was a literal claim, and it has held up.
This chapter is about the operation itself: what scaled dot-product attention computes, why each piece of the formula is the way it is, and what its costs are. The operation lives inside the transformer block (Chapter 5), which wraps it with feedforward layers, residuals, and normalization. Multi-head attention (Chapter 5) runs several copies of it in parallel. Positional encoding (Chapter 6) adds back the position information the operation itself does not see. The widgets, code, and derivations in this chapter all serve a single goal: the reader should leave able to write the operation from memory and explain every term in it.
Soft database lookup: the Q, K, V framing
Attention is a soft database lookup. Hold the mental model fixed before any equations land, because , , as letters are otherwise easy to memorize and hard to feel.
A traditional database has three roles. There is a query: what you are asking for. There is an index of keys: the labels the database matches against. There is a value associated with each key: the data the database returns when a key matches. You hand the database a query; it scans the keys for one that matches; it returns the value at that key. One query, one match, one value. The matching is exact: either the key matches or it does not.
Attention does the same three-role lookup, but softly. Instead of finding the one key that matches, you compute a similarity score between the query and every key in the database. Softmax-normalize those scores so they become a probability distribution. The “retrieval” is then a weighted sum of all the values, with weights given by the softmax over similarity scores. Every value contributes to the output; the most-similar keys contribute the most.
In sequence modeling, the “database” is the sequence itself. At each position , the query says what am I looking for here? Every other position has a key saying this is what I am and a value saying this is what I have to contribute. Position ‘s output is the soft-database lookup over the whole sequence: a blend of every position’s value, weighted by how well each position’s key matched position ‘s query.
In self-attention specifically, , , and are all derived from the same input sequence . The three roles are realized as three separate learned linear projections:
where and . The same input is projected three times into three different subspaces. Each row of is one position’s embedding; after projection, each position has a query vector , a key vector , and a value vector .
and must agree on dimension because we are going to take their dot product. That product is the similarity score between position ‘s query and position ‘s key. ’s dimension can differ in principle, but in practice it is almost always set equal to . In Vaswani’s multi-head attention, where is the number of heads; Chapter 5 picks that up.
A small classroom analogy makes the three cards feel less arbitrary. Picture eight students in a room. Each holds three cards: a Query card describing what they are curious about, a Key card describing what they know, and a Value card containing the actual content they would share. To form a study cluster, each student looks at every student’s Key card (including their own) and decides how much to listen: I will listen most to the students (myself included) whose Keys match my Query. The listening is graded, not all-or-nothing: each student distributes a fixed budget of attention across everyone in the room, themselves included. After voting, each student’s resulting understanding is a weighted blend of Value cards from across the room, weighted by Query-Key alignment; it is entirely normal, and often typical, for a student’s own Value card to carry the largest weight in their own blend. Eight students, three cards each, one soft lookup per student. That is self-attention.
What turns these three projections into a working operation is the formula that combines them.
The scaled dot-product formula
This is the operation. Every modern LLM computes some version of it billions of times per forward pass. The formula was written down by Vaswani et al. in 2017 and has remained, with minor variations, the canonical statement of attention since.
The formula
Three matrices in, one matrix out. The output has shape : the same number of rows as the input sequence (one output row per position), with each row in the value-space dimension. We will refer to this as equation (4.attn) throughout the chapter.
Walking through the shapes
The formula has four operations stacked inside it. Each one has a shape and an interpretation.
The first is , a matrix multiplication of and , producing an matrix. This is the attention scores matrix. Entry is the dot product , which is large when position ‘s query and position ‘s key point in similar directions in and negative when they point in opposite directions. The scores matrix is the raw, unnormalized statement of how strongly each position wants to attend to each other position.
The second is the division by . This is the scaling, the eponymous “scaled” in scaled dot-product attention. The factor restores unit variance to the scores when queries and keys have unit-variance components; section 4 derives this from a short variance calculation. Without it, dot products grow large in magnitude as grows, and the next step (softmax) saturates.
The third is softmax over the last dimension. Each row of the scaled scores is independently passed through softmax, producing a row of non-negative numbers that sum to one. The full output is an matrix of attention weights: is the probability with which position attends to position . Each row is a probability distribution; the entire matrix is a stack of such distributions, one per output position.
The fourth is the multiplication by , a matrix multiplication of and , producing . This is the weighted sum: each output row is , a convex combination of value vectors with the weights determined by softmax. Position ‘s output is “content-addressed retrieval”: a blend of every position’s value, weighted by how well each position’s key matched position ‘s query.
Read end-to-end: a sequence of vectors comes in, gets projected three ways into queries, keys, and values, builds an matrix of pairwise content-similarities, softmaxes that matrix row-wise into a stack of probability distributions, and uses those distributions to blend the values into a new sequence of vectors. The output sequence is the same length as the input. Each output position is a content-addressed mix of the whole input.
A working implementation
The numpy version is short. Twenty lines, including the docstring and the demo. Every modern transformer implementation eventually delegates to a much faster GPU kernel (Flash Attention, vendor-specific cuBLAS calls, JAX/XLA fusions), but they all compute exactly what the lines below compute.
The widget below ties the formula to a concrete sequence. The same six-position input flows through the same four operations the code above implements: and projected from the input, their dot product producing the scores matrix, the scaling and softmax turning scores into weights, the weights blending the values into the output.
The denominator in equation (4.attn) is the one piece of the formula that is not visually obvious, so it deserves a derivation.
Why √d_k? The variance argument
The most-asked question about attention is the one the formula itself does not motivate: why divide by ? Why not by ? Why divide at all? The answer is a short, clean variance calculation.
The variance grows with d_k
Suppose and are independent random vectors in , each with components drawn i.i.d. from a distribution with mean zero and variance one. The dot product is . What is its variance?
The mean is zero by symmetry: by independence and zero means, so . The variance unfolds:
By independence between and and across components, the cross terms vanish. Specifically, , and for both factors are zero (mean-zero independent components). Only the diagonal terms () survive:
Dot products grow in variance linearly with , which means they grow in standard deviation like . For , the typical magnitude of a dot product is around 8. For , around 11. For , around 23. The raw scores get larger as the model gets wider.
Why it matters for softmax
Softmax does not care about absolute values; it cares about differences. If you shift every input by the same constant, the output is unchanged. But if you scale every input by the same constant, the output changes dramatically: large positive logits drive their corresponding probabilities toward 1, and everything else toward 0.
When a softmax distribution becomes peaked (one entry near 1, the others near 0), its gradient with respect to the logits is near zero in every direction. The model cannot redistribute attention away from a saturated state, because the gradient signal that would push it there has vanished. This is the peaked-softmax problem, and it is a generic failure mode for any architecture that pipes large logits into a softmax.
The implication for attention is direct. Without scaling: large → large dot products → peaked softmax → vanishing gradients → the model commits early in training to confident but rigid attention patterns and cannot refine them. Training stalls. Wider attention heads, counterintuitively, become harder to train than narrow ones.
The fix is to divide by the standard deviation of the unscaled scores, restoring unit variance regardless of . That standard deviation is , which is exactly the denominator in the formula. The choice of rather than is not aesthetic; it is the difference between standardizing to unit variance and over-normalizing to variance . The same logic governs Z-score normalization elsewhere in statistics (divide by std, not by variance), and attention is just applying that standardization to dot products.
Empirical verification
The variance calculation is satisfying as a derivation; it is more satisfying when the code confirms it. The block below sweeps over four orders of magnitude and reports the standard deviation of raw and scaled scores, along with the maximum softmax probability in each case.
The raw score std tracks , in the ballpark of 2.8, 8, 22.6, and 64 at respectively, though with only 5 rows of random samples in this demo, the printed numbers wander around those theoretical values (a run with seed 0 prints roughly 2.8, 7.7, 23.4, and 70). The scaled score std stays near 1 throughout. The unscaled softmax max approaches 1 as grows (the distribution saturates), while the scaled version stays moderate. The derivation and the empirical numbers agree.
That handles the denominator. The numerator’s softmax has its own justification, which is the subject of the next section.
Why softmax? The normalization choice
The attention formula calls for some function that turns the scaled scores into weights that can be used in a weighted sum. The constraints are mild: the weights should be non-negative, they should sum to one along each row (so the output is a proper convex combination of values, not a re-scaled blend), and the function should be differentiable so the model can train end-to-end through it. Softmax satisfies all three. So do several alternatives. The chapter focuses on softmax because it is by far the most common (almost every production LLM uses it), but “softmax” and “attention” are conceptually separable.
Softmax has four specific properties that motivated its adoption. First, it produces a valid probability distribution: strictly positive entries summing to one. Second, it is differentiable everywhere, unlike argmax, which is discontinuous at ties. Third, it emphasizes the maximum: the entry with the largest input gets the largest weight, but smaller inputs still contribute. Fourth, it is translation-invariant: for any scalar , which is the reason subtracting the max before exponentiating preserves the output exactly while preventing overflow.
The translation invariance is more than a numerical convenience; it tells you what softmax is sensitive to. Only differences between logits matter. and produce the same softmax. This is why “subtract the max” works as a stability trick: it is just choosing the canonical representative of an equivalence class of inputs that all softmax the same.
The definition, with the stability subtraction baked in:
The gradient of softmax with respect to its input has a closed form:
where is the Kronecker delta (1 if , 0 otherwise). Two implications fall out. On the diagonal (), the gradient is , which peaks at (gradient ) and goes to zero at . Off the diagonal (), the gradient is , largest in magnitude when both probabilities are intermediate. The picture: gradients flow well when softmax is near-uniform, and collapse when softmax becomes peaked. This is the same statement as the motivation in section 4, viewed from the gradient side rather than the variance side. They are two views of one phenomenon.
A handful of alternatives have been considered and used in specialized settings. Hardmax / argmax is the limit of softmax as the temperature goes to zero: pick the maximum, ignore the rest. It is non-differentiable and cannot be used in a gradient-based training loop. L1 normalization, , is non-differentiable at zero and does not emphasize the maximum, so it tends to spread weight too uniformly. Sparsemax (Martins and Astudillo, 2016) is a differentiable function that produces exact zeros for low-scoring entries, useful when you want attention to commit to a small subset; it shows up in some specialized attention variants but has not displaced softmax in mainstream transformers. Linear attention skips softmax entirely and uses kernel tricks to factor the attention matrix into a product of lower-rank pieces; it changes the operation’s algebraic structure and produces instead of scaling. These alternatives exist; readers who want depth can pursue them.
The expression for some controls how peaked the distribution gets. sharpens it; flattens it. Temperature shows up at inference time in LLM sampling (Chapter 19) as the knob that trades determinism against creativity. It is almost never used during training. The scaling in equation (4.attn) is effectively a fixed temperature, one that depends on the head dimension rather than being a free hyperparameter.
So far the chapter has assumed each position can attend to every other position. This is fully bidirectional attention, the variant used in BERT and other encoder-only models. For autoregressive generation, this assumption needs to change.
Causal masking: attention for autoregressive models
Autoregressive language models (the GPT family, Llama, Mistral, Claude) generate text one token at a time. At training time, the model is asked to predict the next token at every position simultaneously: given positions through , predict position . The full sequence is processed in one forward pass for efficiency, but the output at position may only depend on positions through , never on positions , which represent tokens that have not been generated yet.
The vanilla attention formula in section 3 violates this. Equation (4.attn) lets every position attend to every other position. Position 2 sees position 5; position 3 sees position 10. During training that would be catastrophic: the model would learn to “predict” the next token by looking at the next token, achieving zero loss without learning anything. At inference, the future positions do not even exist yet; there is nothing to attend to.
The fix is causal masking. Before the softmax, add a mask matrix to the scores. The mask has at every position where (attention allowed) and at every position where (attention blocked). After softmax, , so the blocked positions contribute zero weight. The allowed positions still sum to one (softmax normalizes across them), so each output position is a clean convex combination of the past.
Two implementation details matter. First, the mask is added before softmax, not after. A natural-seeming alternative (compute the full softmax, then zero out illegal positions) would break the sum-to-one constraint: the remaining probabilities would not sum to 1, and the weighted sum across the past would be miscalibrated by an unpredictable factor. Adding pre-softmax ensures the blocked positions contribute zero probability mass and the legal positions automatically renormalize.
Second, “negative infinity” in practice is often implemented as a large negative number ( is conventional) rather than literal -np.inf. The reason is purely numerical: certain softmax implementations subtract the max before exponentiating, and if every row entry is , the subtraction yields NaN. A finite large negative number behaves identically for any practical purpose ( rounds to zero in any normal floating-point format) without the NaN failure mode. The code in this chapter uses literal -np.inf, which is safe specifically because causal masking always leaves the diagonal entry () unmasked; no row is ever entirely , so the max-subtraction never produces NaN. A mask that could blank out an entire row (e.g. a padding mask on a fully-padded sequence) would need the finite-large-negative-number version instead.
In matrix form, the mask is a lower-triangular matrix with zeros below or on the diagonal and above. It is added to before the softmax is applied row-wise.
The masked attention matrix is strictly lower-triangular: row has non-zero weights only at columns through . Each row still sums to one. The first position can only attend to itself (a trivial single-position distribution); the last position can attend to anywhere in the sequence. Same shape, same softmax, same matrix multiplication. Only one additive matrix changes.
Two related masking patterns appear in practice. Padding masks zero out attention to padding tokens in batched training (variable-length sequences are right-padded to a common length for batching). Prefix-LM masks allow bidirectional attention within a prefix and causal attention thereafter, used in some encoder-decoder hybrids. Both are additive masks of the same kind (different patterns of zeros and in the same matrix slot), and combine cleanly with the causal mask by addition.
Everything so far has been self-attention. Cross-attention is the variant where comes from one sequence and from another.
Self-attention vs cross-attention
The attention formula in equation (4.attn) does not care where its three inputs come from. It is a pure function of , , . The two variants in common use differ only in which sequences feed into those three slots.
In self-attention, , , and all come from the same input sequence . Three different learned projections of the same data play three different roles. Each position attends to every other position in the same sequence. This is the variant used in every modern decoder-only LLM (GPT, Llama, Mistral, Claude) with causal masking, and in every encoder-only model (BERT, RoBERTa, DistilBERT) without it.
In cross-attention, comes from one sequence and , come from another. The classic setting is an encoder-decoder model for translation. The encoder processes the source sentence; the decoder generates the target sentence one token at a time. At each decoder layer, is computed from the decoder’s current state (the target sentence so far), while and are computed from the encoder’s output (the full source sentence). The decoder “attends to” the source: at each position in the target, the model looks across the encoded source to decide what to retrieve.
Mechanically the formula does not change. The shapes adjust ( has rows, and have rows), so the attention matrix becomes instead of , and the output is . The softmax is still row-wise; the weighted sum still produces one output row per query row.
The architectural choice is structural. Encoder-decoder models (the original 2017 transformer, T5, BART, mT5) use cross-attention in the decoder to access the encoder’s representation, plus self-attention within each stack. Decoder-only models (GPT, Llama, the modern open-source landscape) use only self-attention with causal masking. The decoder-only architecture has won most of the recent attention (so to speak) because it is structurally simpler: one stack, one operation, one training objective. The encoder-decoder split remains preferred for tasks with a clear input-output asymmetry (translation, summarization, document-conditioned generation), where it offers a cleaner inductive bias.
Modern multimodal models often use cross-attention to fuse modalities: an image encoder produces and from visual tokens, the language decoder produces from text tokens, and cross-attention lets the language model attend over the image. The variant gets reused in any setting where one sequence needs to be conditioned on another, distinct sequence.
All of this (self-attention, cross-attention, masking variants) runs on the same scaled dot-product operation, which means it inherits the same computational cost. That cost is the subject of the next section.
Computational cost: the O(n²) bottleneck
Attention is expensive. The cost shows up in two places: compute and memory. Both scale with the square of the sequence length, which is the structural reason long-context LLMs are hard.
For a sequence of length with head dimension , the per-layer compute breaks down as follows. The matrix multiplication is , or FLOPs under the standard counting (one multiply and one add per output element). The softmax over the scores matrix is , a constant number of operations per entry. The multiplication is , another FLOPs. The total is , dominated by the two matrix multiplications.
Memory is the more immediate constraint. The attention matrix has shape . For batch size , the on-device memory for this single matrix is values, which at fp32 is bytes and at fp16 is bytes. At with , that is MB at fp32, MB at fp16. Either is a substantial fraction of a single GPU’s high-bandwidth memory budget, just for the attention matrix in one layer. With multiple layers, multiple heads, and gradients during training, the multiplier compounds.
The runnable block below puts numbers on a few sequence lengths.
For a more realistic LLM-scale picture: a 7B-parameter model with and per-head dimension does roughly FLOPs for the matmul per head per layer, and another for the matmul, so FLOPs of attention compute per head per layer. With 32 heads and 32 layers, total attention compute is on the order of FLOPs per forward pass over the full sequence. The feedforward layers (each FFN multiplies by a -wide hidden layer) contribute more per layer when is small; as grows, attention’s scaling overtakes the FFN’s linear scaling, and at long enough contexts attention dominates the FLOPs.
The quadratic scaling is the structural reason context length is a major architectural choice. Doubling quadruples both compute and memory for the attention matrix. Going from 4K to 128K context is a 32× multiplier on , but a 1024× multiplier on attention’s resource demand. The full K attention matrix at fp16 is GB per head per layer; a single such matrix fits in one modern GPU’s memory, but summed across many heads and layers (and multiplied by batch size), the aggregate vastly exceeds any single GPU’s memory.
The cost is also the motivation for much of the rest of the tutorial. Chapter 12 introduces state-space models, alternative architectures with scaling that may or may not displace attention for long-context tasks. Chapter 17 introduces the KV cache: at inference time, the and matrices for past tokens are computed once and cached, so each new token’s attention costs rather than . Chapter 17 also covers Flash Attention and related techniques. The reason the rest of the tutorial spends so much time on these variants is the cost discussed here.
From single-head to multi-head
The attention this chapter has built has one , one , one . One projection per role, producing one -dimensional matching space and one -dimensional value space. Vaswani et al. observed almost immediately that a single attention “head” is leaving capacity on the table.
The natural next move is to run several attention operations in parallel. Each “head” has its own learned , , projections, into smaller subspaces (typically for heads). Each head computes its own attention pattern over the same input sequence. With different learned projections, the heads can specialize. In a trained transformer, different heads end up doing visibly different jobs: one head may focus mostly on recent positions, another on subject-verb relationships across long spans, another on coreference, another on syntax. The model gets different views of the sequence for roughly the same compute as one -dimensional attention, because the per-head dimension shrinks proportionally.
The outputs of the heads are concatenated into a single matrix and passed through one more learned linear projection that mixes the heads into the output. Chapter 5 derives all of this: the shapes, the parameter count, the empirical evidence that heads specialize, and the implementation. Everything Chapter 5 builds is single-head attention from this chapter, replicated times in parallel and stitched back together.
Exercises
The exercises build on the chapter. Each is a self-contained problem with a starting template. Hints are collapsed by default; try the problem first.
Exercise 1 (easy): Verify scaled dot-product attention
Implement scaled dot-product attention from scratch and verify two properties: (a) the attention weights for each row sum to 1, (b) without the scaling, the softmax produces a much more peaked distribution at large .
Hint
Use the implementation from section 3 as a starting point. For property (a), compute weights.sum(axis=-1); it should be all 1s. For property (b), compare softmax(scores) to softmax(scores / sqrt(d_k)) at d_k = 8 vs d_k = 512. The max weight should be much higher without scaling.
Solution
The scaling divides the raw scores by before the softmax; that’s the entire implementation. The peakedness check just compares softmax on raw scores vs. scaled scores at a large , where raw dot products have grown large enough to saturate softmax.
def scaled_dot_product_attention(Q, K, V):
d_k = Q.shape[-1]
scores = Q @ K.T / np.sqrt(d_k)
weights = softmax(scores, axis=-1)
output = weights @ V
return output, weights
out, weights = scaled_dot_product_attention(Q, K, V)
print(f"Row sums: {weights.sum(axis=-1)}") # [1. 1. 1. 1. 1. 1.]
for dk in [8, 512]:
rng2 = np.random.default_rng(1)
Qd = rng2.normal(0, 1, (6, dk))
Kd = rng2.normal(0, 1, (6, dk))
scores = Qd @ Kd.T
unscaled = softmax(scores)
scaled = softmax(scores / np.sqrt(dk))
print(f"d_k={dk}: unscaled max={unscaled.max():.4f}, scaled max={scaled.max():.4f}")
# d_k=8: unscaled max=0.9251, scaled max=0.5213
# d_k=512: unscaled max=1.0000, scaled max=0.7216At the unscaled softmax puts essentially all its weight (1.0000) on one key; scaling keeps the max in a much softer 0.72 range. The gap between unscaled and scaled widens as grows, confirming the variance argument from this chapter.
Exercise 2 (medium): Implement causal masking
Extend your scaled dot-product attention from Exercise 1 to support causal masking. Verify that each position only attends to itself and earlier positions; verify that masked rows still sum to 1.
Hint
Construct an (n, n) mask matrix with 0 on or below the diagonal and -np.inf strictly above. Add this mask to the scaled scores before softmax. np.triu(arr, k=1) produces a matrix with the upper triangle (excluding diagonal); use it to construct the -inf mask.
Solution
Build the mask with np.triu(..., k=1) to grab the strictly-upper triangle, set those entries to , and add the mask to the scores before softmax: after softmax, becomes exactly 0.
def causal_mask(n):
mask = np.zeros((n, n))
mask[np.triu_indices(n, k=1)] = -np.inf
return mask
def causal_attention(Q, K, V):
d_k = Q.shape[-1]
scores = Q @ K.T / np.sqrt(d_k)
scores = scores + causal_mask(Q.shape[0])
weights = softmax(scores, axis=-1)
output = weights @ V
return output, weights
out, weights = causal_attention(Q, K, V)
print(weights.round(3))
# [[1. 0. 0. 0. 0. ]
# [0.218 0.782 0. 0. 0. ]
# [0.571 0.342 0.087 0. 0. ]
# [0.032 0.717 0.192 0.059 0. ]
# [0.516 0.063 0.1 0.127 0.195]]
print(f"Row sums: {weights.sum(axis=-1)}") # [1. 1. 1. 1. 1.]The upper triangle is exactly 0 in every row, and each row still sums to 1; masking only redistributes probability mass among the allowed (earlier and current) positions.
Exercise 3 (medium): Quantify the O(n²) memory cost
Compute the memory required for attention at various sequence lengths. Compare to the memory required for the FFN’s largest tensor at the same sequence length. Find the crossover point where attention’s memory exceeds FFN’s.
Hint
Attention’s main memory consumer is the (n, n) scores/weights matrix. FFN’s main memory consumer is the intermediate hidden states of shape (n, 4 * d_model) where the factor of 4 is the standard FFN expansion ratio. At d_model = 4096, the crossover is approximately at n ≈ 4 * 4096 = 16384.
Solution
Attention memory is (quadratic in sequence length); FFN memory is (linear). At small the FFN’s constant factor () dominates, but attention eventually overtakes it.
def attention_memory_bytes(n, dtype_bytes=2):
return n * n * dtype_bytes
def ffn_memory_bytes(n, d_model, dtype_bytes=2, expansion=4):
return n * expansion * d_model * dtype_bytes
d_model = 4096
for n in [256, 1024, 4096, 16384, 65536]:
attn = attention_memory_bytes(n)
ffn = ffn_memory_bytes(n, d_model)
print(f"n = {n:>6d}: attn = {attn/1e9:>5.2f} GB, ffn = {ffn/1e9:>5.2f} GB, ratio = {attn/ffn:>5.2f}")
for n in range(1, 200_000):
if attention_memory_bytes(n) >= ffn_memory_bytes(n, d_model):
print("crossover n =", n)
breakOutput:
n = 256: attn = 0.00 GB, ffn = 0.01 GB, ratio = 0.02
n = 1024: attn = 0.00 GB, ffn = 0.03 GB, ratio = 0.06
n = 4096: attn = 0.03 GB, ffn = 0.13 GB, ratio = 0.25
n = 16384: attn = 0.54 GB, ffn = 0.54 GB, ratio = 1.00
n = 65536: attn = 8.59 GB, ffn = 2.15 GB, ratio = 4.00
crossover n = 16384The crossover is exactly , matching the hint’s estimate: attention memory equals FFN memory when , i.e. .
Exercise 4 (hard): Implement cross-attention
Implement cross-attention: queries come from one sequence; keys and values from another. Demonstrate it on a toy encoder-decoder setup where the decoder (length 4) attends to an encoder output (length 6). Verify shapes and that the attention works regardless of the two sequences having different lengths.
Hint
The formula is identical to self-attention, softmax(QK^T / sqrt(d_k)) V, but Q has shape (n_dec, d_k) while K and V have shape (n_enc, d_k). The attention matrix is (n_dec, n_enc), not square. There’s no causal masking in standard cross-attention; the decoder attends to the entire encoder output.
Solution
Cross-attention is the same formula as self-attention; only the source of Q vs. K/V differs, so is no longer square when the two sequences have different lengths.
def cross_attention(Q, K, V):
d_k = Q.shape[-1]
scores = Q @ K.T / np.sqrt(d_k)
weights = softmax(scores, axis=-1)
output = weights @ V
return output, weights
out, weights = cross_attention(Q_dec, K_enc, V_enc)
print(f"Output shape: {out.shape}") # (4, 8)
print(f"Weights shape: {weights.shape}") # (4, 6)
print(f"Row sums: {weights.sum(axis=-1)}") # [1. 1. 1. 1.]Output is (n_dec, d_v) = (4, 8), weights are (n_dec, n_enc) = (4, 6) (rectangular, not square), and each of the 4 decoder rows still sums to 1 over the 6 encoder positions it’s mixing.
Everything else in this tutorial is downstream of this. Chapter 5 stacks multiple parallel attentions into multi-head attention and wraps the result in the transformer block. Chapter 6 adds positional information back into a permutation-symmetric operation. Chapter 12 introduces an alternative that trades the formula for linear scaling. Chapter 17 makes inference efficient without changing the math.