Search Tech Journey

Find topics, journeys and posts

6-month learning plan107 / 130
back to blog
llmadvanced 50m read

S107 · Attention Intuition — Why RNNs Failed, Why Attention Won

The 2017 paper that killed RNNs. What 'attention' actually means, the bottleneck it fixes in seq2seq, and why parallelism made Transformers eat NLP.

LLMsM13 · NLP & Transformers· Session 107 of 130 90 min

🎯 Understand attention as a soft dictionary lookup — WHY it exists, WHAT problem it fixes in RNN encoder-decoders, and HOW it scales to a full Transformer.

Why this session exists

"Attention Is All You Need" (2017) is the single most-cited ML paper of the decade. It killed RNNs for NLP in three years and became the foundation of every LLM, image generator, and multimodal model that followed. But the paper is dense math, and most tutorials jump straight to Q/K/V equations without explaining WHY attention was invented or what it replaces. This session builds the intuition first — you'll see attention as a soft dictionary lookup that fixes a specific bottleneck in seq2seq translation. The math (Session S108) makes sense once you feel the problem.

You will be able to
  • Explain the seq2seq bottleneck — why the last LSTM hidden state can't carry a 100-word sentence.
  • Describe attention as a weighted average, and 'soft dictionary lookup' as its unifying metaphor.
  • Draw an attention pattern between an English sentence and its French translation.
  • Explain WHY attention parallelises across the sequence axis when RNNs cannot.
  • Distinguish 'attention' (as an add-on to RNNs, Bahdanau 2014) from 'self-attention' (the Transformer's core, 2017).

Prerequisites

  • S103 · RNNs & LSTMs — you MUST understand seq2seq and the vanishing gradient to appreciate what attention fixes.
  • S104 · Embeddings — attention operates on vector representations of tokens.
  • S106 · Tokenization — the input tokens attention sees.


(a) Intuition · 5 min

Reading a translation, but glancing back at the original
🌍 Real world

You're translating a French sentence to English. Do you memorise the entire French sentence, then look away and generate the English from memory? Of course not — you glance back at the French for each English word you write. Sometimes the current English word depends on French word 3; sometimes on word 15; sometimes on a pair of words across the sentence.

The glancing is the trick. You dynamically decide, for each output word, which input words matter right now.

💻 Code world

Before 2014, seq2seq models did the opposite: encode the whole source sentence into ONE fixed vector, then decode from that vector alone. This is the 'memorise and look away' approach. It works for 10-word sentences and fails miserably for 30-word ones — the vector can't carry that much information.

Attention gives the decoder the ability to glance back. At each decoder step, it computes a weighted average of ALL the encoder hidden states, with weights determined by 'which source words are most relevant right now.' The bottleneck disappears.

The three ideas that make attention work

Everything about attention reduces to these
  • Soft dictionary lookup — think of attention as a 'fuzzy hash table.' You have a query (what you want to know), and a set of key-value pairs. Instead of matching one exact key, you compute similarity with every key and return a weighted average of the values.
  • Content-based routing — the weights come from the CONTENT of the query and keys (usually a dot product), not from fixed positional rules. This is why attention can handle any pair of positions equally well.
  • Parallelism — all query-key comparisons can be done in parallel via a single matrix multiplication. RNNs must process token 1 before token 2 before token 3; attention does all N² comparisons at once.

A short history

  1. 2014
    Seq2Seq · Sutskever et al.
    Encoder-decoder LSTMs for translation. Works well up to ~20 words, degrades sharply beyond.
  2. 2014
    Attention · Bahdanau et al.
    Bolt attention onto seq2seq. Fixes the long-sentence bottleneck. BLEU score jumps.
  3. 2015
    Luong attention
    Simpler, faster variants. Multiplicative attention becomes the standard.
  4. 2017
    Attention Is All You Need · Vaswani et al.
    Drop the RNN. Use self-attention. Parallelisable, faster to train, wins WMT translation benchmarks.
  5. 2018
    BERT · Google
    Transformer encoder pretrained on masked language modelling. NLP is transformed.
  6. 2020
    GPT-3 · OpenAI
    175B-parameter Transformer decoder shows in-context learning. LLM era begins.

(b) Visual walkthrough · 15 min

The seq2seq bottleneck

The red box is the entire information bottleneck. Everything the encoder learned about a 30-word input must fit in one d-dimensional vector. It doesn't — long sentences translate badly.

The fix: attention over all encoder states

At every decoder step, the decoder queries ALL encoder states. Softmax over similarity scores gives attention weights (which sum to 1). Weighted sum of encoder states = the context vector for THIS decoder step. Bottleneck gone.

The three roles: query, key, value

The three flavours: cross-attention, self-attention, causal self-attention

Cross-attention

Query from A, keys/values from B

  • Original Bahdanau attention
  • Decoder queries encoder outputs
  • Used in translation, T5, Whisper
  • Q = decoder state, K/V = encoder outputs
Self-attention

Q, K, V all from same sequence

  • Transformer's core innovation
  • Every token attends to every other token
  • Bidirectional — used in BERT encoders
  • Q, K, V = 3 projections of same input
Causal self-attention

Self-attention with future masked

  • Used in GPT-style decoders
  • Token t can only see tokens 1..t
  • Enables next-token prediction
  • Mask = upper triangle of attention matrix set to -∞

Anatomy of one attention head

How attention actually computes

1. Project Q, K, V
Take input X (shape [T, d]) and compute Q = X·W_q, K = X·W_k, V = X·W_v. Three learned matrices project X into three role-specific vectors.
project
2. Score Q against every K
scores = Q · K^T (shape [T, T]). Element (i, j) = 'how much should token i attend to token j?'
score
3. Scale by √d_k
scores /= sqrt(d_k) to keep softmax gradients healthy. Session S108 derives why exactly this constant.
scale
4. Softmax across keys
For each row, softmax converts scores to weights summing to 1. Now weights[i, j] = 'fraction of attention token i pays to token j.'
normalise
5. Weighted sum of V
output = weights · V (shape [T, d_v]). Each output row is a weighted average of value vectors, with weights specific to that token.
aggregate
6. (Optional) apply causal mask before softmax
Set scores[i, j] = -∞ for j \> i. After softmax those become 0 — token i can't attend to future. Turns bidirectional attention into GPT-style.
causal

What an attention pattern actually looks like

Different attention heads learn different patterns: some look at nearby tokens (like a CNN would), some look at syntactic parents, some look at co-referring entities. In a 12-layer Transformer with 12 heads, you have 144 different learned attention patterns.


Common misconception
✗ What most people think

"Attention weights tell me what the model is looking at. If I visualise them, I get an explanation of the prediction."

✓ What is actually true

Attention weights are a routing distribution over values, not an attribution of importance. A head can place 90% of its mass on a token whose value vector contributes almost nothing to the output, and the residual stream carries information around attention entirely. Attention maps are a useful debugging signal; they are not an explanation.

Why the myth is so sticky

Because the myth is true in the case everyone meets first: seq2seq translation attention, where a single head genuinely aligned source and target words and the picture was beautiful and correct. That image is what made attention famous. In a 32-layer, 32-head transformer, no single head owns a concept — the computation is distributed across heads and layers, and the softmax normalisation forces mass somewhere even when the head has nothing to say (hence attention sinks on the first token).

Prove it to yourself

Show that high attention weight does not imply high output contribution: scale a value vector to zero and watch the output barely move while the weight stays high.

import torch
T, d = 6, 8
Q = torch.randn(T, d); K = torch.randn(T, d); V = torch.randn(T, d)
A = torch.softmax(Q @ K.T / d**0.5, dim=-1)
out = A @ V

V2 = V.clone(); V2[0] = 0          # kill the value of token 0
out2 = A @ V2
print('attn mass on tok0:', A[:, 0].mean().item())
print('output change     :', (out - out2).norm().item())
# high mass, and yet the delta is bounded entirely by |V[0]|, not by A
From first principles
Start with the question

Why divide by √dk before the softmax? Everyone repeats "to stop gradients vanishing" — derive it.

  1. 1
    Take query and key components as roughly independent, zero-mean, unit-variance after normalisation. The score is a dot product of two d-dimensional vectors.
    forced by · this is what LayerNorm plus standard initialisation actually gives you at the start of training
  2. 2
    A sum of d independent zero-mean unit-variance products has variance d, hence standard deviation √d.
    forced by · variances add for independent terms; that is the whole content of the step
  3. 3
    So raw scores grow as √d. At d=128 that is a spread of roughly ±11 between typical scores, and larger for outliers.
    forced by · the score magnitude is set by dimension, not by anything semantic
  4. 4
    Softmax over logits separated by ~10 is numerically one-hot: the largest logit takes essentially all the mass.
    forced by · e^10 is about 22,000, so a 10-unit gap gives a 22,000:1 ratio
  5. 5
    The gradient of softmax is p(1−p) in the diagonal term. As p approaches 1 or 0, that goes to zero — the head stops learning where to look, and it is stuck wherever random initialisation pointed it.
    forced by · a saturated softmax has vanishing Jacobian, so no gradient reaches Q and K
⇒ Therefore

Therefore dividing by √dk restores unit-variance logits at initialisation, keeping the softmax in its responsive regime. It is a variance-normalisation, not a magic constant.

And note what this predicts: (1) the scaling matters most at initialisation — a trained model learns Q/K magnitudes that keep logits sane, which is why alternative scalings can still train, just worse and slower; (2) anything else that inflates logit variance — very long sequences, unnormalised inputs, fp16 overflow in the score matrix — reproduces the same saturation. That is exactly why production kernels compute the softmax in fp32 and subtract the row max.

Mental modelA soft, differentiable dictionary lookup

Every token emits a query ("what do I need?"), advertises a key ("what do I have?"), and offers a value ("here is the thing"). A hard dictionary would return the one value whose key matches. Attention returns a weighted blend of all values, weights given by key–query similarity.

The blend is the entire point: hard lookup has no gradient, so nothing could learn what to look up. Softmax is what turns a lookup into something trainable.

  • Q, K, V are three different learned projections of the same input. Separating them lets "what I search for" differ from "what I expose" and from "what I hand over".
  • The output for each position is a convex combination of value vectors — so attention can only mix values, never invent information outside their span.
  • Cost is O(T²) in time and, if materialised, O(T²) in memory. That single fact drives every long-context technique you will meet later.
  • Attention is permutation-invariant by construction. Order exists only because positional information was added to the inputs.
🔔 Fires when you see

Fire this model the moment you see: an attention map being presented as an explanation · a question about why context length is expensive · a model that mysteriously ignores token order · a masked-attention bug where the future leaks · anyone proposing to "just remove the softmax".

The tradeoff

You need a sequence model for a task with long-range dependencies. Attention, recurrence, or convolution?

Self-attention
+ you gain constant path length between any two positions, so long-range dependencies are learnable in one layer; and the whole sequence is processed in parallel during training, which is what actually made scale possible
− you pay O(T²) compute and memory in sequence length; and autoregressive inference is sequential anyway, with a KV cache that grows linearly in tokens and dominates serving memory
pick when sequences up to a few thousand positions where any pair of tokens may need to interact, and you have the compute to train in parallel
Recurrence (RNN / LSTM / SSM)
+ you gain O(T) compute and O(1) state per step — inference cost is independent of how much you have already generated, which is a decisive advantage for very long streams
− you pay information must survive a fixed-size bottleneck across every intervening step, so distant dependencies are lossy; classic RNNs also cannot parallelise over time during training
pick when streaming or unbounded-length input, or edge inference where a growing KV cache simply will not fit
Convolution
+ you gain fully parallel, extremely hardware-efficient, and the strong locality prior means far less data is needed when the signal really is local
− you pay receptive field grows only with depth or dilation, so genuinely long-range interactions need many layers and are never as direct
pick when the dependency structure is known to be local — audio frames, short windows of time series, image patches
What a senior engineer actually does

Attention won because it traded memory for path length, and path length is what gradient descent actually needs: one layer between any two tokens means the credit-assignment signal does not have to survive a hundred sequential multiplications. Everything since — sparse attention, linear attention, FlashAttention, state-space hybrids — is an attempt to keep the short path while paying less than O(T²).

In practice the senior read is: use attention, and treat the quadratic term as a budget line you manage (chunking, retrieval, caching) rather than an architecture you replace. Replace it only when the sequence length is genuinely unbounded.


(c) Hands-on · 25 min

Implement a minimal attention layer in PyTorch and visualise the attention pattern on a real sentence. This is the exact code that becomes multi-head attention in Session S109.

# mini_attention.py — a from-scratch self-attention layer with visualisation.
# Run: uv run mini_attention.py
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
 
 
class SingleHeadAttention(nn.Module):
    """One head of self-attention. Q, K, V from same input; attention weights returned."""
 
    def __init__(self, d_model: int, causal: bool = False):
        super().__init__()
        self.d_model = d_model
        self.causal = causal
        # Three linear projections — no bias, following the Transformer paper.
        self.W_q = nn.Linear(d_model, d_model, bias=False)
        self.W_k = nn.Linear(d_model, d_model, bias=False)
        self.W_v = nn.Linear(d_model, d_model, bias=False)
 
    def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
        # x: [B, T, d]. Project to Q, K, V.
        B, T, D = x.shape
        Q = self.W_q(x)  # [B, T, D]
        K = self.W_k(x)  # [B, T, D]
        V = self.W_v(x)  # [B, T, D]
 
        # Compute scaled dot-product attention scores.
        # scores[b, i, j] = how much token i attends to token j.
        scores = Q @ K.transpose(-2, -1)               # [B, T, T]
        scores = scores / math.sqrt(D)                 # scale — see Session S108 for why
 
        # Causal mask: token i cannot see token j > i.
        if self.causal:
            mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool()
            scores = scores.masked_fill(mask, float("-inf"))
 
        # Softmax across the KEYS (last dim).
        weights = F.softmax(scores, dim=-1)            # [B, T, T]
 
        # Weighted sum of values.
        output = weights @ V                            # [B, T, D]
        return output, weights
 
 
def demo() -> None:
    torch.manual_seed(42)
 
    # A tiny vocabulary + embedding for demonstration.
    sentence = ["The", "cat", "sat", "on", "the", "mat"]
    vocab = {word: i for i, word in enumerate(sorted(set(sentence)))}
    ids = torch.tensor([[vocab[w] for w in sentence]])  # [1, 6]
 
    d_model = 16
    embed = nn.Embedding(len(vocab), d_model)
    attn = SingleHeadAttention(d_model, causal=False)
 
    x = embed(ids)                                        # [1, 6, 16]
    out, weights = attn(x)
    print(f"input shape:  {x.shape}")
    print(f"output shape: {out.shape}")
    print(f"weights shape: {weights.shape} (should be [1, 6, 6])")
 
    # Pretty-print the attention matrix.
    w = weights[0].detach().numpy()
    print("\nAttention pattern (rows = query token, cols = key token):")
    print(f"          {'  '.join(w_.rjust(4) for w_ in sentence)}")
    for i, word in enumerate(sentence):
        row = "  ".join(f"{v:.2f}" for v in w[i])
        print(f"  {word:<7}  {row}")
 
    # Compare causal vs non-causal.
    print("\n--- Causal (GPT-style) ---")
    attn_causal = SingleHeadAttention(d_model, causal=True)
    _, weights_c = attn_causal(x)
    w = weights_c[0].detach().numpy()
    for i, word in enumerate(sentence):
        row = "  ".join(f"{v:.2f}" for v in w[i])
        print(f"  {word:<7}  {row}")
    print("(Notice the upper-right triangle is 0 — the future is masked.)")
 
 
if __name__ == "__main__":
    demo()

Anatomy of the script

What the interesting lines do

W_q, W_k, W_v = nn.Linear(d, d, bias=False)
Three separate learned projections. Same input, three different roles. In a full Transformer these become the ONLY learnable weights of attention (plus the output projection W_o).
params
scores = Q @ K.transpose(-2, -1)
Batch matmul: [B, T, D] × [B, D, T] = [B, T, T]. Element (i, j) is the raw compatibility score between query i and key j.
score
scores / math.sqrt(D)
Scaling. Without it, dot products in high dim have huge variance, softmax saturates, gradients vanish. Session S108 derives this constant.
scale
mask = torch.triu(ones(T, T), diagonal=1)
Upper-triangular matrix of 1s (above diagonal). True = 'this position is future — mask it.' For causal self-attention only.
mask
scores.masked_fill(mask, -inf)
Replace masked positions with -∞. After softmax those become exactly 0. Now token i's attention weights only cover positions 1..i.
mask
F.softmax(scores, dim=-1)
Normalise each row to sum to 1. dim=-1 means 'across keys, per query.' This gives per-query probability distributions over the T tokens.
normalise
output = weights @ V
[B, T, T] × [B, T, D] = [B, T, D]. Each output token is a weighted average of ALL value tokens, weighted by the attention pattern.
aggregate
Try itVerify that attention parallelises where RNNs can't
import time
T = 512
x = torch.randn(1, T, 16)
 
# LSTM baseline
lstm = nn.LSTM(16, 16, batch_first=True)
t0 = time.time()
for _ in range(50): _ = lstm(x)
print(f"LSTM 50 iters: {time.time() - t0:.3f}s")
 
# Attention
attn = SingleHeadAttention(16, causal=True)
t0 = time.time()
for _ in range(50): _ = attn(x)
print(f"Attention 50 iters: {time.time() - t0:.3f}s")

On CPU LSTM wins for small T. On GPU with larger T (say 2048), attention is 5-10× faster — this is the entire reason Transformers replaced RNNs.

💡 Hint · Compare wall-clock time. Time our attention on a sequence of length 512 vs an nn.LSTM on the same input. On GPU, attention should be dramatically faster despite doing O(T²) work — because it's all one matmul, while LSTM must sequentially unroll 512 steps.

(d) Production reality · 15 min

War story Google · Neural Machine Translation attention· 2016all Google Translate
🔥 What broke

Google's 2016 NMT system used 8-layer stacked LSTMs with Bahdanau attention. It shipped and dramatically improved translation quality. But engineers noticed inference was slow and hard to scale — the attention had to be computed at every decoder step, which added ~15% latency, AND the LSTM stacks couldn't be parallelised.

🧯 The fix
When "Attention Is All You Need" dropped in 2017, Google's own team was one of the first to switch. Removing the LSTMs entirely (Transformer) trained 5× faster and reached higher BLEU. By 2018, most of Google Translate was Transformer-based. LSTMs still linger in the low-resource-language stack.
🎓 Lesson to steal
Attention as an add-on to RNNs was a stepping stone. The real win came from realising attention alone was enough. Ideas often need years of incremental use before someone sees they can stand alone.
Post-mortem
War story OpenAI · GPT-3 attention scaling· 2020175B params
🔥 What broke
Attention is O(T²) in memory — a 2048-token context needs 2048² = 4M attention scores per head per layer. GPT-3 with 96 heads × 96 layers × batch 32 = infeasible on any single GPU. Early GPT-3 experiments would OOM before finishing a batch.
🧯 The fix
Multiple engineering wins: (1) FlashAttention (Tri Dao 2022) — never materialises the full T×T matrix, computes attention in tiles. (2) Sparse attention (Longformer, Sparse Transformer) — most tokens don't need to see all others. (3) Grouped-query attention (LLaMA-2) — share K/V heads across multiple Q heads. Every modern LLM uses at least one of these tricks.
🎓 Lesson to steal
The theoretical O(T²) is not a hard wall — it's a research target. Every modern LLM has some flavour of 'don't actually compute all T² scores.' Watch what FlashAttention does.
Post-mortem
War story Anthropic · Constitutional AI interpretability· 2023research
🔥 What broke
Anthropic's interpretability team tried to understand why Claude sometimes hallucinated facts. Looking at attention patterns in the middle layers, they discovered specific attention heads that reliably attended to a system prompt when generating factual claims. When those heads were suppressed (patching), the model hallucinated far more often.
🧯 The fix
Not really a 'fix' — a research direction. They published mechanistic interpretability findings showing that specific attention heads specialise (e.g. 'induction heads' that look for repeated patterns, 'name mover heads' that route entity names). This work is now the foundation of activation-patching and steering techniques.
🎓 Lesson to steal
Attention patterns are not random — they specialise during training. Interpretability research uses this specialisation to reverse-engineer what the model 'knows.' You'll hear 'induction heads' and 'circuits' constantly.
Post-mortem

Where this shows up in the rest of the plan

Attention is the foundation of every LLM, image gen, and multimodal system
S108 · Q/K/V Math
The exact derivation of scaled dot-product attention, including why we divide by √d_k.
S109 · Multi-Head Attention
Run N attention heads in parallel, each learning a different pattern. The Transformer's real trick.
S110 · Positional Encoding
Attention is permutation-invariant — it needs a positional signal to know word ORDER. That's what positional encoding provides.
S111 · Full Transformer
Attention + FFN + residual + layernorm × N = the Transformer block. Everything comes together.
S127 · CLIP + Multimodal
Cross-attention between image patches and text tokens — same attention math, different Q/K/V sources.
S117 · Diffusion Models
Cross-attention conditions diffusion denoising on text prompts. Same primitive, image gen application.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

If you can't teach these three without notes, redo the session:

  1. What is the seq2seq bottleneck, and how does attention fix it?
  2. What are Q, K, V — and what does the metaphor 'soft dictionary lookup' mean?
  3. Why can attention parallelise across the sequence axis when RNNs can't?

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.