Attention Mechanism Explained: How Transformers Learn to Focus
The Bottleneck That Broke Machine Translation
Attention lets every token in a sequence ask "what else is relevant to me?" β dynamically weighting relationships across all positions simultaneously.
π― System Scenario & Requirements
System Goal
Attention lets every token in a sequence ask "what else is relevant to me?" β dynamically weighting relationships across all positions simultaneously.
Actors & Components
- Edge Traffic Ingress
- Stateless Microservices
- Datastore Infrastructure
- API Gateway
- Domain Service
- Redis Cache
- Transactional DB
Functional Requirements
- TLDR: Attention lets every token in a sequence ask "what else is relevant to me?" β dynamically weighting relationships across all positions simultaneously.
- It replaced the fixed-size hidden-state bottleneck of RNNs and is the engine behind every GPT, BERT, ViT, and AlphaFold model you have ever used.
- In 2014, Google's sequence-to-sequence translation model had a serious problem.
Constraints & Assumptions
- Constraints are derived from the rebuilt scenario stages.
β‘ Execution Stages & Decision Trace
Stage 1 β The Database Query Analogy: Queries, Keys, and Values
Before touching a single matrix multiply, let us build the right mental model.
The diagram highlights two paths at the masking decision point: encoder self-attention skips the causal mask and every token can attend to every other token freely, while decoder self-attention applies the upper-trianguβ¦
Deep Dive
Scenario context
Attention lets every token in a sequence ask "what else is relevant to me?" β dynamically weighting relationships across all positions simultaneously. It replaced the fixed-size hidden-state bottleneck of RNNs and is the engine behind every GPT, BERT, ViT, and AlphaFold model you have ever used.
In 2014, Google's sequence-to-sequence translation model had a serious problem. To translate a long French sentence iβ¦
This walkthrough focuses on The Bottleneck That Broke Machine Translation.
Before making a design choice, clarify:
- In 2014, Google's sequence-to-sequence translation model had a serious problem.
- To translate a long French sentence into English, its LSTM encoder read every word one by one and compressed the entire sentence into a single fixed-size vector β a 512-dimensional hidden state.
- The decoder then reconstructed the English translation from that one vector alone.
Concepts in play: deep-learning-foundations, quantization-and-llm-inference, vector-embeddings-and-similarity, caching
Stage 1 β The Database Query Analogy: Queries, Keys, and Values
- Before touching a single matrix multiply, let us build the right mental model.
- The diagram highlights two paths at the masking decision point: encoder self-attention skips the causal mask and every token can attend to every other token freely, while decoder self-attention applies the upper-trianguβ¦
- The residual connection and LayerNorm at the end are not part of the attention mechanism itself but are inseparable from how it is deployed in practice β they stabilise gradient flow through the dozens of stacked layersβ¦
flowchart TD
TK[Raw input tokens] --> EMB[Token embeddings plus positional encodings]
EMB --> QP[W_Q projection per head]
EMB --> KP[W_K projection per head]
EMB --> VP[W_V projection per head]
QP --> QV[Query vectors Q]
KP --> KV[Key vectors K]
VP --> VV[Value vectors V]
QV --> SCORES[QK-T divided by sqrt d_k - raw scores]
KV --> SCORES
SCORES --> MASK{Causal mask needed?}
MASK -->|decoder yes| MASKED[Apply minus-inf to future positions]
MASK -->|encoder no| SM[Softmax over key dimension]
MASKED --> SM
SM --> ATTN[Attention weight matrix A]
ATTN --> WSUM[Weighted sum A times V]
VV --> WSUM
WSUM --> WO[Output projection W_O - merge all heads]
WO --> RESID[Add residual connection plus LayerNorm]
RESID --> NEXT[Feed-forward sublayer or next encoder block]
Observable outcome: The residual connection and LayerNorm at the end are not part of the attention mechanism itself but are inseparable from how it is deployed in practice β they stabilise gradient flow through the dozens of stacked layersβ¦
Stage 2 β Where Attention Powers Modern AI: From Text to Proteins
- Attention is not a niche text trick.
- In the eight years since the original Transformer paper, it has become the dominant mechanism across virtually every AI domain.
- BERT (Google, 2018): Uses 12 stacked self-attention encoder layers.
- The bidirectional attention β every token sees every other token simultaneously β enabled state-of-the-art results on 11 NLP benchmarks on release.
Observable outcome: The bidirectional attention β every token sees every other token simultaneously β enabled state-of-the-art results on 11 NLP benchmarks on release.
Stage 3 β Choosing the Right Attention Variant for Your Architecture
- Not every task needs full dense self-attention, and picking the wrong variant adds complexity without benefit.
Observable outcome: Not every task needs full dense self-attention, and picking the wrong variant adds complexity without benefit.
Stage 4 β PyTorch and HuggingFace: How They Implement Attention
- Building Scaled Dot-Product Attention from Scratch The best way to internalise the mechanism is to implement it yourself before relying on library abstractions.
- This PyTorch implementation is fully runnable and adds only standard library imports.
- What to observe when you run this: - output.shape equals input.shape β the attention mechanism is shape-preserving; it contextualises each token but keeps dimensionality constant.
- weights.shape is (batch, heads, seq q, seq k) β you can visualise row 0 of any head as the attention distribution for token 0 over all tokens.
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
def scaled_dot_product_attention(
Q: torch.Tensor,
K: torch.Tensor,
V: torch.Tensor,
mask: torch.Tensor = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Scaled Dot-Product Attention.
Args:
Q: (batch, heads, seq_q, d_k)
K: (batch, heads, seq_k, d_k)
V: (batch, heads, seq_k, d_v)
mask: (batch, 1, 1, seq_k) or (batch, 1, seq_q, seq_k) β 0 = keep, 1 = mask out
Returns:
output: (batch, heads, seq_q, d_v)
attn_weights: (batch, heads, seq_q, seq_k)
"""
d_k = Q.size(-1)
# Step 1: raw attention scores β (batch, heads, seq_q, seq_k)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
# Step 2: apply mask (e.g., causal mask for decoder, padding mask)
if mask is not None:
scores = scores.masked_fill(mask == 1, float("-inf"))
# Step 3: softmax over key dimension
attn_weights = F.softmax(scores, dim=-1)
# Step 4: weighted sum of values
output = torch.matmul(attn_weights, V)
return output, attn_weights
class MultiHeadAttention(nn.Module):
"""
Multi-Head Attention from 'Attention Is All You Need' (Vaswani et al., 2017).
"""
def __init__(self, d_model: int, num_heads: int):
super().__init__()
assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads # head dimension
# Separate projections for Q, K, V, and output
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)
self.W_o = nn.Linear(d_model, d_model, bias=False)
def split_heads(self, x: torch.Tensor) -> torch.Tensor:
"""(batch, seq, d_model) -> (batch, heads, seq, d_k)"""
batch, seq, _ = x.shape
x = x.view(batch, seq, self.num_heads, self.d_k)
return x.transpose(1, 2)
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
mask: torch.Tensor = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Args:
query: (batch, seq_q, d_model)
key: (batch, seq_k, d_model)
value: (batch, seq_k, d_model)
mask: optional attention mask
Returns:
output: (batch, seq_q, d_model)
attn_weights: (batch, heads, seq_q, seq_k)
"""
batch = query.size(0)
# Project inputs to Q, K, V and split into heads
Q = self.split_heads(self.W_q(query)) # (batch, heads, seq_q, d_k)
K = self.split_heads(self.W_k(key)) # (batch, heads, seq_k, d_k)
V = self.split_heads(self.W_v(value)) # (batch, heads, seq_k, d_k)
# Scaled dot-product attention across all heads in parallel
attn_output, attn_weights = scaled_dot_product_attention(Q, K, V, mask)
# attn_output: (batch, heads, seq_q, d_k)
# Merge heads: (batch, heads, seq_q, d_k) -> (batch, seq_q, d_model)
attn_output = attn_output.transpose(1, 2).contiguous()
attn_output = attn_output.view(batch, -1, self.d_model)
# Final linear projection
output = self.W_o(attn_output)
return output, attn_weights
# ββ Quick smoke test ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
torch.manual_seed(42)
batch, seq_len, d_model, num_heads = 2, 6, 64, 8
x = torch.randn(batch, seq_len, d_model)
mha = MultiHeadAttention(d_model=d_model, num_heads=num_heads)
output, weights = mha(x, x, x) # self-attention: Q=K=V=x
print(f"Input shape: {x.shape}") # (2, 6, 64)
print(f"Output shape: {output.shape}") # (2, 6, 64)
print(f"Attention weights: {weights.shape}") # (2, 8, 6, 6)
print(f"Weights sum to 1.0: {weights[0, 0].sum(dim=-1).allclose(torch.ones(seq_len))}")
def causal_mask(seq_len: int) -> torch.Tensor:
"""
Upper-triangular mask: positions where mask == 1 are masked out.
Shape: (1, 1, seq_len, seq_len) β broadcasts over batch and heads.
"""
mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool()
return mask.unsqueeze(0).unsqueeze(0).long()
# Usage β decoder self-attention on 6 tokens
mask = causal_mask(6)
output, weights = mha(x, x, x, mask=mask)
# Token 0 can only attend to itself; token 5 can attend to all 6.
print("Causal weights row 0 (only position 0 non-zero):", weights[0, 0, 0])
print("Causal weights row 5 (all 6 positions non-zero):", weights[0, 0, 5])
Observable outcome: - weights.shape is (batch, heads, seq q, seq k) β you can visualise row 0 of any head as the attention distribution for token 0 over all tokens.
Stage 5 β Common Bugs That Will Waste Your Weekend
- Attention is simple in theory but has several sharp edges in practice.
- These are the mistakes that show up most often in from-scratch implementations and fine-tuning pipelines.
- Forgetting to scale by $\sqrt{d k}$ This is the most common first-implementation bug.
- Without the scaling factor, dot products between 64-dimensional vectors can reach magnitudes of 30β50, which pushes softmax to output near-zero weights for all tokens except the one with the highest score (essentially aβ¦
Observable outcome: Without the scaling factor, dot products between 64-dimensional vectors can reach magnitudes of 30β50, which pushes softmax to output near-zero weights for all tokens except the one with the highest score (essentially aβ¦
End-to-end outcome
In 2014, Google's sequence-to-sequence translation model had a serious problem.
Alternatives considered
The O(nΒ²) Problem: Cost, Memory, and Sparse Alternatives
Attention is powerful, but its complexity profile creates real engineering constraints.
Failure and degraded modes
The source does not isolate a separate failure mode. Limits that matter for the scenario are called out in the stage decisions above.
