LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnTransformer Deep DivesScaled Dot-Product Attention
🧠HardTransformer Architecture

Scaled Dot-Product Attention

Build scaled dot-product attention from a token sequence: Q/K/V routing, variance scaling, masks, multi-head shapes, KV-cache cost, and FlashAttention.

41 min read
Learning path
Step 97 of 196 in the full curriculum
Embedding Similarity & QuantizationVision Transformers and Image Encoders

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

The previous chapter scored stored embedding vectors with cosine and dot product. A retriever compares one query vector to many keys.

A transformer layer turns that same routing idea inward: each token supplies a query and a key, while a mask decides which positions it may read.

Take the line "The cache grew quickly because it was full." You know "it" is the cache, not the growth. If that reference reads from the wrong position, later layers receive the wrong context.

Before naming any matrices, predict the operation: each token should choose source positions, turn those scores into weights, and mix the chosen content. Scaled dot-product attention learns that routing with matrix multiplication.

Three-token causal attention on cache, grew, and quickly: Q times K transpose and scaling produce logits, future keys are set to negative infinity, row-wise softmax creates routing weights, and those weights mix V into context vectors.
Same three tokens as the rest of the chapter, with a causal mask. Query cache can only mix its own value. Query grew mixes cache and grew, matching the two-token walkthrough. Query quickly mixes all three.

The mask answers a separate question from relevance: which positions are legal to read? The −∞-\infty−∞ cells are a causal mask. Compute the unmasked two-token slice by hand first, then put the mask back.

Every active query still needs at least one allowed key. An entirely blocked row has no probability distribution, because subtracting its maximum evaluates −∞−(−∞)-\infty-(-\infty)−∞−(−∞) and produces NaN.


Three views of one token

Self-attention computes a weighted mix of token representations. Each query gets its weights from learned relevance scores against the keys.[1]Reference 1Attention Is All You Need.https://arxiv.org/abs/1706.03762 To do that, the model turns each hidden state into three roles:

VectorQuestion it answersRole in attention
Query (Q)What context does this position need?The current token's lookup vector
Key (K)When should another position read me?The addressable routing vector for a token
Value (V)What information do I contribute if selected?The content vector that gets mixed into the output

Each token hidden state xix_ixi​ is projected three ways. WQW_QWQ​ produces what this position is searching for (query), and WKW_KWK​ produces when others should read this position (key). WVW_VWV​ produces the payload that gets mixed if selected (value):

Q=XWQ,K=XWK,V=XWVQ = XW_Q, \quad K = XW_K, \quad V = XW_VQ=XWQ​,K=XWK​,V=XWV​

Here WQ,WK∈Rdmodel×dkW_Q, W_K \in \mathbb{R}^{d_{\text{model}} \times d_k}WQ​,WK​∈Rdmodel​×dk​ and WV∈Rdmodel×dvW_V \in \mathbb{R}^{d_{\text{model}} \times d_v}WV​∈Rdmodel​×dv​. dmodeld_{\text{model}}dmodel​ is the residual width; dkd_kdk​ and dvd_vdv​ are the query/key and value widths.[1]Reference 1Attention Is All You Need.https://arxiv.org/abs/1706.03762

For the running example, token cache uses these toy vectors after projection:

RoleVector
Query qqq[1.0, 0.5][1.0,\ 0.5][1.0, 0.5]
Key kkk[0.8, 0.2][0.8,\ 0.2][0.8, 0.2]
Value vvv[2.0, 1.0][2.0,\ 1.0][2.0, 1.0]

Predict the split before reading the diagram: Q and K should meet to produce a routing score, while V should travel past that score and remain content. The self-score for cache will therefore use qqq and kkk, not vvv.

The token named cache splits through three learned matrices into query [1.0, 0.5], key [0.8, 0.2], and value [2.0, 1.0]; query and key meet to produce the 0.90 self-score, while value stays the content vector that later gets mixed.
The same hidden state is projected three ways. For token cache, query and key meet at the 0.90 self-score used in the walkthrough, while value stays the content vector that attention weights later mix.

Q and K determine where to look. Their dot products produce routing scores, and softmax converts those scores into weights.

V determines what information to extract. Separating "routing" (Q, K) from "content" (V) is what lets attention route information flexibly.

In one sentence, which tensors choose where attention goes, and which tensor carries content?

Answer

Q and K choose where attention goes. Their dot products produce routing scores that softmax converts into weights. V carries the content that gets mixed by those weights.


The core formula

Read the formula as a lookup followed by a weighted read. Compatibility of queries with keys becomes weights; those weights mix values.[1]Reference 1Attention Is All You Need.https://arxiv.org/abs/1706.03762 Softmax turns each query row into non-negative weights that sum to 1:

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) VAttention(Q,K,V)=softmax(dk​​QKT​)V

When some key positions aren't allowed, add a mask matrix MMM after scaling. An allowed location has Mij=0M_{ij}=0Mij​=0; a blocked logit gets Mij=−∞M_{ij}=-\inftyMij​=−∞:

Attention(Q,K,V;M)=softmax(QKTdk+M)V\text{Attention}(Q, K, V; M) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}} + M\right)VAttention(Q,K,V;M)=softmax(dk​​QKT​+M)V

Step by step

StepOperationWhat it means
Compute scoresS=QKTS = QK^TS=QKTBuild an n×nn \times nn×n matrix where SijS_{ij}Sij​ measures how much token iii should attend to token jjj.
ScaleL=S/dkL = S / \sqrt{d_k}L=S/dk​​Keep softmax logits in a range where gradients stay useful.
MaskAdd 000 or −∞-\infty−∞ to each logitBlock future keys for causal attention or padded keys for batching.
Normalizeαij=exp⁡(Lij)∑kexp⁡(Lik)\alpha_{ij} = \frac{\exp(L_{ij})}{\sum_k \exp(L_{ik})}αij​=∑k​exp(Lik​)exp(Lij​)​Turn each row into weights that sum to 1.
Aggregateα⋅V\alpha \cdot Vα⋅VBlend value vectors according to the attention weights.

The order is the contract: scores choose locations, softmax makes each choice a distribution, and only then do those weights touch V. Predict the shapes before following the picture: two queries compared with two keys make a 2×22 \times 22×2 routing matrix, while the value width survives into the output.

The shape flow is compact enough to keep beside the formula. Queries and keys build one routing matrix; values join only after softmax:

Diagram showing Q [B, h, Nq, d_k], QKᵀ / √d_k + mask [B, h, Nq, Nk], Kᵀ [B, h, d_k, Nk], and softmax over keys [B, h, Nq, Nk].
Q [B, h, Nq, d_k], QKᵀ / √d_k + mask [B, h, Nq, Nk], Kᵀ [B, h, d_k, Nk], and softmax over keys [B, h, Nq, Nk].
A two-token attention trace for cache and grew: Q times K transpose gives raw scores, scaling gives logits, row softmax gives attention weights, and multiplying by V gives output vectors, with the grew row highlighted through each stage.
Tokens cache and grew in one pass: $QK^T$ gives raw scores, dividing by $\sqrt{2}$ shrinks them into logits, row softmax turns the grew row into a distribution, and multiplying by $V$ produces the mixed output vectors.

A trace with real numbers

Walk through the first two tokens, cache and grew, before adding the third token or a mask. Each lives in a 2-dimensional toy space:

TokenQuery vectorKey vectorValue vector
cache[1.0, 0.5][0.8, 0.2][2.0, 1.0]
grew[0.5, 1.0][0.3, 0.9][1.0, 2.0]

Step 1: raw scores

Compute the dot product of every query with every key:

S=QKT=[1.0⋅0.8+0.5⋅0.21.0⋅0.3+0.5⋅0.90.5⋅0.8+1.0⋅0.20.5⋅0.3+1.0⋅0.9]=[0.900.750.601.05]S = QK^T = \begin{bmatrix} 1.0 \cdot 0.8 + 0.5 \cdot 0.2 & 1.0 \cdot 0.3 + 0.5 \cdot 0.9 \\ 0.5 \cdot 0.8 + 1.0 \cdot 0.2 & 0.5 \cdot 0.3 + 1.0 \cdot 0.9 \end{bmatrix} = \begin{bmatrix} 0.90 & 0.75 \\ 0.60 & 1.05 \end{bmatrix}S=QKT=[1.0⋅0.8+0.5⋅0.20.5⋅0.8+1.0⋅0.2​1.0⋅0.3+0.5⋅0.90.5⋅0.3+1.0⋅0.9​]=[0.900.60​0.751.05​]

Step 2: scale

With dk=2d_k = 2dk​=2, divide by 2≈1.414\sqrt{2} \approx 1.4142​≈1.414:

L=[0.640.530.420.74]L = \begin{bmatrix} 0.64 & 0.53 \\ 0.42 & 0.74 \end{bmatrix}L=[0.640.42​0.530.74​]

Step 3: softmax

Normalize each row so it sums to 1:

α=[0.530.470.420.58]\alpha = \begin{bmatrix} 0.53 & 0.47 \\ 0.42 & 0.58 \end{bmatrix}α=[0.530.42​0.470.58​]

Step 4: weighted values

Multiply the weights by the value vectors:

  • New "cache" vector = 0.53⋅[2.0,1.0]+0.47⋅[1.0,2.0]=[1.53,1.47]0.53 \cdot [2.0, 1.0] + 0.47 \cdot [1.0, 2.0] = [1.53, 1.47]0.53⋅[2.0,1.0]+0.47⋅[1.0,2.0]=[1.53,1.47]
  • New "grew" vector = 0.42⋅[2.0,1.0]+0.58⋅[1.0,2.0]=[1.42,1.58]0.42 \cdot [2.0, 1.0] + 0.58 \cdot [1.0, 2.0] = [1.42, 1.58]0.42⋅[2.0,1.0]+0.58⋅[1.0,2.0]=[1.42,1.58]

Each original vector is replaced by a blend of the sequence, weighted by relevance. Cache pulls slightly more from its own value (0.53) than from grew (0.47), while grew mixes both nearly evenly. A real head does this across 64 or 128 dimensions, not two.

In this toy trace, skipping the scaling step barely changes softmax. Why does the same omission hurt when dkd_kdk​ grows to 64 or 512?

Answer

At dk=2d_k = 2dk​=2, raw scores stay small. Under the initialization assumptions used below, increasing dkd_kdk​ increases the dot-product spread like dk\sqrt{d_k}dk​​. That makes saturated, low-gradient softmax rows more likely unless scores are scaled.

The same arithmetic is easy to verify without a tensor library:

a-trace-with-real-numbers.py
1import math 2 3Q = [[1.0, 0.5], [0.5, 1.0]] 4K = [[0.8, 0.2], [0.3, 0.9]] 5V = [[2.0, 1.0], [1.0, 2.0]] 6 7def softmax(row: list[float]) -> list[float]: 8 shift = max(row) 9 exps = [math.exp(x - shift) for x in row] 10 total = sum(exps) 11 return [x / total for x in exps] 12 13scores = [[sum(q_i * k_i for q_i, k_i in zip(q, k)) for k in K] for q in Q] 14scaled = [[score / math.sqrt(2) for score in row] for row in scores] 15weights = [softmax(row) for row in scaled] 16outputs = [ 17 [sum(weight * value[col] for weight, value in zip(row, V)) for col in range(2)] 18 for row in weights 19] 20 21print([[round(x, 2) for x in row] for row in weights]) 22print([[round(x, 2) for x in row] for row in outputs]) 23print([round(sum(row), 3) for row in weights])
Output
1[[0.53, 0.47], [0.42, 0.58]] 2[[1.53, 1.47], [1.42, 1.58]] 3[1.0, 1.0]

Add the causal mask

The mask changes who may be read, not how the allowed weights are computed. With a lower-triangular mask, this function becomes the two-token slice of the opening figure: token cache can only read itself, while token grew can read cache and grew, so its weights match the unmasked walkthrough.

causal-two-token-attention.py
1import math 2 3Q = [[1.0, 0.5], [0.5, 1.0]] 4K = [[0.8, 0.2], [0.3, 0.9]] 5V = [[2.0, 1.0], [1.0, 2.0]] 6causal = [[True, False], [True, True]] 7 8def softmax(row: list[float]) -> list[float]: 9 shift = max(row) 10 exps = [math.exp(x - shift) for x in row] 11 total = sum(exps) 12 return [x / total for x in exps] 13 14def scaled_dot_product_attention( 15 Q: list[list[float]], 16 K: list[list[float]], 17 V: list[list[float]], 18 mask: list[list[bool]] | None = None, 19) -> tuple[list[list[float]], list[list[float]]]: 20 d_k = len(Q[0]) 21 scores = [ 22 [sum(q_i * k_i for q_i, k_i in zip(q, k)) / math.sqrt(d_k) for k in K] 23 for q in Q 24 ] 25 if mask is not None: 26 if not all(any(row) for row in mask): 27 raise ValueError("each query row must have at least one visible key") 28 scores = [ 29 [logit if visible else float("-inf") for logit, visible in zip(row, visible_row)] 30 for row, visible_row in zip(scores, mask) 31 ] 32 weights = [softmax(row) for row in scores] 33 outputs = [ 34 [sum(weight * value[col] for weight, value in zip(row, V)) for col in range(len(V[0]))] 35 for row in weights 36 ] 37 return outputs, weights 38 39output, weights = scaled_dot_product_attention(Q, K, V, causal) 40pretty_weights = [[round(value, 3) for value in row] for row in weights] 41print("causal weights:", pretty_weights) 42print("cache future weight:", pretty_weights[0][1]) 43print("output rows:", len(output), "width:", len(output[0]))
Output
1causal weights: [[1.0, 0.0], [0.421, 0.579]] 2cache future weight: 0.0 3output rows: 2 width: 2

Two masking details matter in real code. First, softmax must run along the key axis so each query row sums to 1; normalizing along the query axis silently changes the operation.

Second, a row with no permitted key has no valid attention distribution. -inf makes that mistake visible as NaN.

Replacing it with a finite negative value hides the bug: softmax assigns weight to blocked keys because every blocked logit ties. Ensure each active query has at least one allowed key (causal attention includes its own position), or explicitly suppress outputs for padded query rows.

Predict the failure before running this case: with a finite fill value, a fully blocked row gives every blocked key the same score, so softmax invents a uniform distribution. The suppression step must handle that row explicitly.

This short failure case makes the second rule concrete:

fully-masked-rows-need-explicit-handling.py
1import math 2 3scores = [[0.8, 0.1], [0.4, -0.2]] 4visible = [[True, False], [False, False]] 5 6def softmax(row: list[float]) -> list[float]: 7 shift = max(row) 8 exps = [math.exp(x - shift) for x in row] 9 total = sum(exps) 10 return [x / total for x in exps] 11 12filled = [ 13 [logit if allowed else -1e4 for logit, allowed in zip(row, mask_row)] 14 for row, mask_row in zip(scores, visible) 15] 16finite_fill_weights = [softmax(row) for row in filled] 17served_weights = [ 18 row if any(mask_row) else [0.0] * len(row) 19 for row, mask_row in zip(finite_fill_weights, visible) 20] 21 22print("finite fill, invalid row:", [round(x, 4) for x in finite_fill_weights[1]]) 23print("after padded-query suppression:", served_weights[1]) 24print("valid row ignores blocked key:", [round(x, 4) for x in served_weights[0]])
Output
1finite fill, invalid row: [0.5, 0.5] 2after padded-query suppression: [0.0, 0.0] 3valid row ignores blocked key: [1.0, 0.0]

In PyTorch, F.scaled_dot_product_attention is the fused entry point for the same math. Current docs list three CUDA implementations (FlashAttention-2, memory-efficient attention, and a C++ math fallback) and an experimental enable_gqa path.

A fused kernel isn't guaranteed for every shape and dtype. Two API traps matter: True in a boolean attn_mask means the position participates, which is the inverse of nn.MultiheadAttention's boolean key_padding_mask; and dropout_p is applied whenever it's greater than zero, so pass 0.0 at eval.[2]Reference 2torch.nn.functional.scaled_dot_product_attentionhttps://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html[3]Reference 3FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691

Common shape mistakes

When attention breaks, start with two questions: do the dimensions produce one score per query-key pair, and did the score scale stay stable? The two mistakes below produce distinct symptoms.

Forgetting to transpose K

If you multiply QQQ by KKK instead of KTK^TKT, the inner dimensions won't align. With QQQ shape (B,h,n,dk)(B, h, n, d_k)(B,h,n,dk​) and KKK shape (B,h,n,dk)(B, h, n, d_k)(B,h,n,dk​), you need (n×dk)(dk×n)(n \times d_k)(d_k \times n)(n×dk​)(dk​×n). In PyTorch that's Q @ K.transpose(-2, -1), not Q @ K.

Forgetting to scale

If you skip attn_scores / math.sqrt(d_k), the code won't crash. Under the independent unit-variance setup below, a dk=64d_k=64dk​=64 raw dot product has standard deviation 8 rather than 1. That larger spread can saturate softmax and reduce routing gradients.

Inspect score statistics and attention entropy when debugging, then restore the scale factor unless you're intentionally testing a different attention formulation.

A self-attention heatmap for the sentence The cache grew quickly, with query rows, key columns, and two highlighted query rows showing how one row distributes weight across key positions while still summing to 1.
Read attention row by row. Each query token spreads one probability distribution across key positions, so the row sums to 1 before those weights mix value vectors. These invented numbers teach orientation; they don't claim what a trained head means.

Why scale by dk\sqrt{d_k}dk​​? The variance proof

The two-token trace hides the problem because its head is only two numbers wide. Each attention score adds one feature product per dimension into one logit.

Without scaling, increasing dkd_kdk​ makes raw dot products larger and larger until softmax saturates. Dividing by dk\sqrt{d_k}dk​​ normalizes the score so the model can still distribute probability across several plausible tokens instead of locking onto one too early.

The derivation that motivated the transformer's scale factor starts with a simple assumption: entries of qqq and kkk are independent across vectors and dimensions, with mean 0 and variance 1. Learned activations won't satisfy those assumptions exactly.

The calculation still explains why unscaled logits begin with dimension-dependent spread.[1]Reference 1Attention Is All You Need.https://arxiv.org/abs/1706.03762

Assume

qi,kiq_i, k_iqi​,ki​ are independent components, each with mean 000 and variance 111, and the product terms are independent across dimensions. The calculation doesn't require a Gaussian distribution; it uses these moments and independence to add the per-dimension variances. The dot product is:

q⋅k=∑i=1dkqikiq \cdot k = \sum_{i=1}^{d_k} q_i k_iq⋅k=∑i=1dk​​qi​ki​

Each term qikiq_i k_iqi​ki​ has E[qiki]=0\mathbb{E}[q_i k_i] = 0E[qi​ki​]=0 (since E[qi]=E[ki]=0\mathbb{E}[q_i] = \mathbb{E}[k_i] = 0E[qi​]=E[ki​]=0 and they're independent). The variance is:

Var(qiki)=E[(qiki)2]−(E[qiki])2=E[qi2]E[ki2]−0=1⋅1=1\text{Var}(q_i k_i) = \mathbb{E}[(q_i k_i)^2] - (\mathbb{E}[q_i k_i])^2 = \mathbb{E}[q_i^2]\mathbb{E}[k_i^2] - 0 = 1 \cdot 1 = 1Var(qi​ki​)=E[(qi​ki​)2]−(E[qi​ki​])2=E[qi2​]E[ki2​]−0=1⋅1=1

By the sum of independent variances:

Var(q⋅k)=dk\text{Var}(q \cdot k) = d_kVar(q⋅k)=dk​

So the standard deviation of the raw dot product is dk\sqrt{d_k}dk​​. As dkd_kdk​ grows under this model, logits spread more widely before softmax:

So at dk=64d_k=64dk​=64, predict a raw standard deviation of 8 and a scaled standard deviation of 1. The table checks that prediction across widths.

dkd_kdk​Std(q⋅k)\text{Std}(q \cdot k)Std(q⋅k)Std(q⋅k/dk)\text{Std}(q \cdot k / \sqrt{d_k})Std(q⋅k/dk​​)
164.01.0
648.01.0
51222.61.0
409664.01.0

After dividing by dk\sqrt{d_k}dk​​

Var ⁣(q⋅kdk)=dkdk=1\text{Var}\!\left(\frac{q \cdot k}{\sqrt{d_k}}\right) = \frac{d_k}{d_k} = 1Var(dk​​q⋅k​)=dk​dk​​=1 under these assumptions. Scaling doesn't promise a particular learned attention pattern; it removes a predictable source of width-dependent logit growth.

Raw attention-logit spread grows with head width, while dividing by square root of d_k keeps the scaled spread flat; a 64-term audit shows standard deviation dropping from 8 to 1.
Under the independent mean-zero, unit-variance assumptions, raw dot-product spread grows with head width, while dividing by $\sqrt{d_k}$ keeps the scaled variance stable. For $d_k=64$, the raw standard deviation is 8 and the scaled standard deviation is 1.

See it in code. This experiment samples independent unit-variance query/key vectors and checks the standard-deviation calculation rather than choosing one dramatic softmax row:

after-dividing-by-sqrtdk.py
1import math 2import random 3import statistics 4 5rng = random.Random(7) 6 7def dot_products(width: int, samples: int = 5000) -> list[float]: 8 return [ 9 sum(rng.gauss(0, 1) * rng.gauss(0, 1) for _ in range(width)) 10 for _ in range(samples) 11 ] 12 13for width in (16, 64, 512): 14 raw = dot_products(width) 15 raw_std = statistics.pstdev(raw) 16 scaled_std = statistics.pstdev([x / math.sqrt(width) for x in raw]) 17 print(f"d_k={width:3d}: raw std={raw_std:5.2f}, scaled std={scaled_std:4.2f}")
Output
1d_k= 16: raw std= 4.07, scaled std=1.02 2d_k= 64: raw std= 8.09, scaled std=1.01 3d_k=512: raw std=22.39, scaled std=0.99

The sampled values won't be exactly the theoretical values, but their trend should match: raw spread grows with width while scaled spread stays near one.

Why divide by dk\sqrt{d_k}dk​​ instead of by dkd_kdk​?

Answer

The raw dot product has variance dkd_kdk​, so its standard deviation is dk\sqrt{d_k}dk​​. Softmax sees the scale of logits through their standard deviation, so dividing by dk\sqrt{d_k}dk​​ brings the variance back to 1. Dividing by dkd_kdk​ would over-shrink the logits as dimensions grow.


Three types of attention

The formula doesn't decide who may read whom. The mask and the sources of QQQ, KKK, and VVV do.

Bidirectional attention lets every visible position read every other visible position. Causal attention processes left to right, so each new token can only use earlier tokens. Cross-attention lets a target sequence read from a separate source sequence, such as a decoder reading encoder states.

1. Bidirectional self-attention (encoder)

Every non-padding token may attend to every other non-padding token; there's no future-token mask. Encoder architectures such as BERT, and the Vision Transformer you'll meet next, use this pattern when the full input is already available:

text
1"The cache grew quickly" 2 Token "grew" attends to: [The, cache, grew, quickly] (full context)

2. Causal self-attention (decoder)

Each token can only attend to itself and previous tokens. Future positions are masked with −∞-\infty−∞. Decoder architectures such as GPT-style and other autoregressive language models use this pattern for generation tasks where the model must predict the next token without seeing the future.

When processing a sequence step-by-step, the model progressively builds context but remains strictly blind to upcoming words:

text
1"The cache grew quickly" 2 Token "grew" attends to: [The, cache, grew] (only past + self) 3 Token "quickly" attends to: [The, cache, grew, quickly] (full history)

The causal mask is a lower-triangular matrix that lets each position look only at itself and the positions before it. This minimal Python version takes the sequence length as input and outputs a boolean matrix where True indicates an allowed connection and False indicates a masked one.

2-causal-self-attention-decoder.py
1def create_causal_mask(seq_len: int) -> list[list[bool]]: 2 return [[key_pos <= query_pos for key_pos in range(seq_len)] for query_pos in range(seq_len)] 3 4mask = create_causal_mask(4) 5for row in mask: 6 print(row) 7print(mask[0] == [True, False, False, False]) 8print(mask[1] == [True, True, False, False]) 9print(mask[3] == [True, True, True, True])
Output
1[True, False, False, False] 2[True, True, False, False] 3[True, True, True, False] 4[True, True, True, True] 5True 6True 7True

Why must token 0 be unable to attend to token 1 in causal self-attention?

Answer

During next-token training, token 0 is only allowed to use itself and earlier context. If it can attend to token 1, the model can leak the future answer during training. Loss can look good, but generation fails because that future token isn't available at inference time.

3. Cross-attention (encoder-decoder)

Queries come from one sequence, Keys and Values from another. The original Transformer uses this pattern in its decoder: encoder outputs provide keys and values, while current decoder states provide queries.[1]Reference 1Attention Is All You Need.https://arxiv.org/abs/1706.03762

The resulting weights choose which source positions contribute to each target representation:

text
1Encoder output (source): "cache memory pressure" provides K, V 2Decoder state (target): "decode slows ___" provides Q 3 4Q from decoder times K from encoder gives attention weights 5Weights times V from encoder give decoder context

In practice, cross-attention usually applies a source padding mask so decoder tokens don't attend to padded encoder positions. It doesn't use a causal mask over the source sequence, because the encoder has already seen the whole input.

Self-attention produces a square query-by-key matrix. Cross-attention doesn't have to: two target queries reading three source positions produce a 2 x 3 routing matrix.

cross-attention-can-be-rectangular.py
1import math 2 3decoder_queries = [[1.0, 0.0], [0.0, 1.0]] 4encoder_keys = [[1.0, 0.0], [0.2, 0.8], [0.0, 1.0]] 5encoder_values = [[1.0, 0.0], [0.5, 0.5], [0.0, 1.0]] 6 7def softmax(row: list[float]) -> list[float]: 8 exps = [math.exp(x - max(row)) for x in row] 9 return [value / sum(exps) for value in exps] 10 11logits = [ 12 [sum(q_i * k_i for q_i, k_i in zip(q, k)) / math.sqrt(2) for k in encoder_keys] 13 for q in decoder_queries 14] 15weights = [softmax(row) for row in logits] 16context = [ 17 [sum(weight * value[d] for weight, value in zip(row, encoder_values)) for d in range(2)] 18 for row in weights 19] 20 21print(f"routing shape: {len(weights)} x {len(weights[0])}") 22print("row sums:", [round(sum(row), 3) for row in weights]) 23print("context:", [[round(x, 3) for x in row] for row in context])
Output
1routing shape: 2 x 3 2row sums: [1.0, 1.0] 3context: [[0.623, 0.377], [0.393, 0.607]]
Attention TypeQ SourceK, V SourceMaskArchitecture
Bidirectional SelfSame sequenceSame sequencePadding mask only (if needed)BERT, Vision Transformer (ViT)
Causal SelfSame sequenceSame sequenceLower-triangular, plus padding if neededGPT, Llama
CrossTarget sequenceSource sequenceSource padding mask commonT5, original Transformer
Three attention patterns side by side: bidirectional self-attention shows all links visible, causal self-attention blocks future positions above the diagonal, and cross-attention lets target tokens read from a separate source sequence.
Two choices define attention type: where queries and keys come from, and which score cells stay visible. Bidirectional self-attention keeps the full cache / grew / quickly grid. Causal self-attention keeps only the lower triangle. Cross-attention lets decoder queries read a separate source sequence.

Multi-head attention

Multi-head attention gives the model several learned routing views of the same token sequence, then lets a final projection combine those views. Each head has its own query, key, and value projections. The formula describes hhh parallel heads, each with dk=dmodel/hd_k = d_{\text{model}} / hdk​=dmodel​/h:[1]Reference 1Attention Is All You Need.https://arxiv.org/abs/1706.03762

A trained head may acquire a recognizable routing pattern, but the architecture doesn't assign jobs such as "syntax head" or "copy head" in advance.

MultiHead(Q,K,V)=Concat(head1,…,headh)WO\text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) W^OMultiHead(Q,K,V)=Concat(head1​,…,headh​)WO

Reading the formula

Instead of running one big attention operation, we project into hhh narrower heads that attend independently. After each head produces its output, we concatenate them and multiply by a final matrix WOW^OWO so information from those routes rejoins the residual stream.

For self-attention with input XXX, each head computes: headi=Attention(XWiQ,XWiK,XWiV)\text{head}_i = \text{Attention}(XW_i^Q, XW_i^K, XW_i^V)headi​=Attention(XWiQ​,XWiK​,XWiV​). Cross-attention uses target states for the query projection and source states for the key/value projections.

Multi-head attention shown as one width-8 residual stream projected into four parallel width-2 heads, then concatenated back to width 8 and mixed by the output projection.
Multi-head attention doesn't create width from nowhere. It splits one width-8 stream into four width-2 routes, runs those routes in parallel, then concatenates them back to width 8 before $W^O$ mixes information across heads.

What do attention heads learn?

These interpretability results are evidence about specific trained models, not a promise about every transformer. Voita et al. found positional and syntactic patterns among heads in neural machine translation encoders.[4]Reference 4Analyzing Multi-Head Self-Attention: Specialized Heads Do the Heavy Lifting.https://arxiv.org/abs/1905.09418

Michel et al. found that many heads in the models they tested could be removed at inference with limited quality loss.[5]Reference 5Are Sixteen Heads Really Better than One?.https://arxiv.org/abs/1905.10650 Olsson et al. studied induction heads, circuits that support copying patterns in autoregressive models under their experiments.[6]Reference 6In-context Learning and Induction Heads.https://arxiv.org/abs/2209.11895

Result from a studyUseful inferenceUnsafe inference
Some heads show consistent patternsInspect heads when debugging or researching a trained modelEvery head has a named human-readable purpose
Some tested models tolerate head pruningRedundancy can exist and can be measuredArbitrarily deleting heads preserves a new model's quality
Induction-head circuits can emergeAttention can implement copy-like sequence algorithmsAn attention heatmap alone proves causal model behavior

Same asymptotic attention FLOPs

Multi-head attention doesn't increase the asymptotic cost of the attention core when you keep dmodeld_{\text{model}}dmodel​ fixed. It restructures the work.

Single-head attention on dmodel=512d_{\text{model}}=512dmodel​=512 uses roughly the same leading-order FLOPs (Floating Point Operations) for score computation and value mixing as 8-head attention with dk=64d_k=64dk​=64 each, because h×dk=dmodelh \times d_k = d_{\text{model}}h×dk​=dmodel​.

The dense Q/K/V and output projections still cost O(ndmodel2)O(n d_{\text{model}}^2)O(ndmodel2​) either way.

Predict before running the arithmetic: with fixed dmodeld_{\text{model}}dmodel​, one 512-wide head and eight 64-wide heads should do the same leading score-and-value work. Splitting a fixed width into more heads doesn't change the total number of score-and-value multiply-adds in the attention core:

multi-head-core-work-at-fixed-width.py
1seq_len = 2048 2d_model = 512 3 4for heads in (1, 8, 16): 5 d_head = d_model // heads 6 score_and_value_work = 2 * heads * seq_len**2 * d_head 7 print(f"heads={heads:2d}, d_head={d_head:3d}, core units={score_and_value_work:,}")
Output
1heads= 1, d_head=512, core units=4,294,967,296 2heads= 8, d_head= 64, core units=4,294,967,296 3heads=16, d_head= 32, core units=4,294,967,296

The usual layout dv=dk=dmodel/hd_v = d_k = d_{\text{model}} / hdv​=dk​=dmodel​/h is a reshape, not extra width. After a dense projection to DDD, slice into hhh heads and attend. Then concatenate back to DDD before WOW^OWO:

multi-head-reshape-and-concat.py
1B, N, D, n_heads = 1, 4, 8, 2 2d_k = D // n_heads 3assert D % n_heads == 0 4 5# (B, N, D) -> (B, h, N, d_k) 6heads = [ 7 [[[0.0] * d_k for _ in range(N)] for _ in range(n_heads)] 8 for _ in range(B) 9] 10# (B, h, N, d_k) -> (B, N, D) 11concat = [ 12 [ 13 [coord for head in range(n_heads) for coord in heads[b][head][n]] 14 for n in range(N) 15 ] 16 for b in range(B) 17] 18print("input shape:", (B, N, D)) 19print("head shape:", (len(heads), len(heads[0]), len(heads[0][0]), len(heads[0][0][0]))) 20print("concat width:", len(concat[0][0])) 21print("width preserved:", len(concat[0][0]) == D)
Output
1input shape: (1, 4, 8) 2head shape: (1, 2, 4, 4) 3concat width: 8 4width preserved: True

From-scratch checklist

Before relying on torch.nn.MultiheadAttention, make sure you can implement the pieces above by hand. High-level modules are useful later, but they hide the exact shape and masking mistakes that break production attention code.

For a decoder-only block, your implementation should do each step explicitly:

  1. Project x into Q, K, and V.
  2. Reshape (B, N, D) into (B, h, N, d_k).
  3. Compute Q @ K.transpose(-2, -1) / sqrt(d_k).
  4. Apply the causal mask before softmax.
  5. Use numerically stable softmax along the key dimension.
  6. Multiply attention weights by V.
  7. Concatenate heads back to (B, N, D).
  8. Apply the output projection.

Two shape assertions catch many bugs:

from-scratch-checklist.py
1assert Q.shape == (B, n_heads, N, d_k) 2assert attn_weights.shape == (B, n_heads, N, N)

One mask assertion catches leakage: assert not causal[0][1]. Token 0 must not see token 1.

If a model can read future tokens during training, the loss can look excellent while generation fails. That's why causal masking isn't a detail. It's the contract that makes next-token prediction honest.

Gradient flow through attention

Attention has three gradient paths:

PathWhat receives gradientWhy it matters
Output to Vvalue projection and upstream token representationsteaches what information each token should carry
Output to attention weightssoftmax probabilitiesteaches which source positions should matter
Weights back to Q and Kquery/key projectionsteaches the routing function itself

The scale factor helps keep the Q/K path trainable. If scores become too large, softmax saturates, attention weights become nearly one-hot, and the gradient through the routing path becomes tiny. If the causal mask is wrong, gradients flow through illegal future positions and the model learns a shortcut it can't use at inference time.

When debugging attention, don't only print the final output. Inspect the score range, the mask, one row of attention weights, and the gradient norm on W_q and W_k. Those four checks tell you whether the model is learning routing or only moving values through a broken router.


Complexity analysis

There are two bottlenecks to keep separate: quadratic query-key pairs and width-heavy projections. Big-O notation describes how work or storage grows as sequence length nnn and model width dmodeld_{\text{model}}dmodel​ increase.

It hides constant factors, so use it for growth trends and use concrete byte/FLOP arithmetic for capacity planning.

MetricComplexityExplanation
Time (attention core, single head)O(n2⋅dk)O(n^2 \cdot d_k)O(n2⋅dk​)Both QKTQK^TQKT and αV\alpha VαV touch all n2n^2n2 query-key pairs
Time (attention core, full multi-head)O(n2⋅dmodel)O(n^2 \cdot d_{\text{model}})O(n2⋅dmodel​)Across hhh heads, h⋅dk=dmodelh \cdot d_k = d_{\text{model}}h⋅dk​=dmodel​
Time (Q/K/V + output projections)O(n⋅dmodel2)O(n \cdot d_{\text{model}}^2)O(n⋅dmodel2​)Dense linear layers before and after the attention core
Memory (naive weights)O(n2)O(n^2)O(n2) per headThe score or weight matrix is n×nn \times nn×n
ParametersO(dmodel2)O(d_{\text{model}}^2)O(dmodel2​)WQ,WK,WV,WOW_Q, W_K, W_V, W_OWQ​,WK​,WV​,WO​ are dense projections
Attention score growth shown as four larger n-by-n grids from 512 to 32768 tokens, where each 4x increase in sequence length causes 16x more query-key pairs, plus a bottom memory wall panel showing that one naive FP16 score tensor at batch 8, 32 heads, and sequence length 8192 is about 32 GiB.
The growth rule is visual: every 4x jump in sequence length makes the score grid 16x larger. By 8,192 tokens, one naive FP16 score-sized tensor at batch 8 and 32 heads is already about 32 GiB, before gradients, activations, or weights.

Concrete memory example

Batch of 8, 32 heads, n=8192n = 8192n=8192, FP16 (16-bit floating point format):

Attention matrix=8×32×81922×2 bytes≈34.4 GB≈32 GiB\text{Attention matrix} = 8 \times 32 \times 8192^2 \times 2 \text{ bytes} \approx 34.4 \text{ GB} \approx 32 \text{ GiB}Attention matrix=8×32×81922×2 bytes≈34.4 GB≈32 GiB

How the numbers work

8 sequences in the batch x 32 attention heads x 8192² entries per attention map x 2 bytes per number (FP16) = about 34.4 GB of raw storage, or about 32 GiB, for one attention-score tensor before values needed for backpropagation, model weights, or optimizer state.

how-the-numbers-work.py
1batch = 8 2heads = 32 3seq_len = 8192 4bytes_per_fp16 = 2 5 6bytes_total = batch * heads * seq_len**2 * bytes_per_fp16 7gb = bytes_total / 1_000_000_000 8gib = bytes_total / 1024**3 9 10print(round(gb, 1), "GB") 11print(round(gib, 1), "GiB") 12print(round(gb, 1) == 34.4, round(gib, 1) == 32.0)
Output
134.4 GB 232.0 GiB 3True True

In the naive formulation, O(n2)O(n^2)O(n2) temporary memory becomes a bottleneck for long sequences. Doubling sequence length makes one materialized score tensor four times larger. Fused kernels can avoid storing that full tensor, but exact dense attention still computes interactions across all query-key pairs.

If sequence length grows from 512 to 2048, why does naive attention memory grow by 16x instead of 4x?

Answer

The attention score matrix is n×nn \times nn×n. Increasing nnn by 4x makes the matrix (4n)×(4n)(4n) \times (4n)(4n)×(4n), which has 16x as many entries. This is the core quadratic memory wall.

Training vs. inference bottlenecks

During training, or in a naive implementation, the temporary n×nn \times nn×n score matrix is the obvious O(n2)O(n^2)O(n2) memory problem.

During autoregressive decoding, optimized kernels often avoid materializing that matrix. Each new token has only one query row, but the model still has to repeatedly read the accumulated KV cache (past keys and values). That makes incremental inference heavily constrained by memory bandwidth, not FLOPs alone.[7]Reference 7Fast Transformer Decoding: One Write-Head is All You Need.https://arxiv.org/abs/1911.02150

Architectural variants attack that persistent cache cost directly. Multi-query attention (MQA) shares one key/value head across all query heads.[7]Reference 7Fast Transformer Decoding: One Write-Head is All You Need.https://arxiv.org/abs/1911.02150

Grouped-query attention (GQA) uses fewer key/value heads than query heads.[8]Reference 8GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.https://arxiv.org/abs/2305.13245

Both shrink KV-cache bytes. The Llama 3 paper uses GQA with 8 key-value heads.[9]Reference 9The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783 Whether that improves latency enough for a workload is a measurement question, because kernel choice, batch size, and quality requirements also matter.

For a simplified decoder cache, the storage count is proportional to layers x tokens x kv_heads x head_dim x 2 (the final factor stores both K and V). Keeping 32 query heads but reducing KV heads changes this count directly:

kv-head-count-controls-cache-bytes.py
1layers = 32 2tokens = 8192 3head_dim = 128 4bytes_per_value = 2 # FP16 5 6def cache_gib(kv_heads: int) -> float: 7 bytes_total = layers * tokens * kv_heads * head_dim * 2 * bytes_per_value 8 return bytes_total / 1024**3 9 10mha = cache_gib(32) 11for label, kv_heads in [("MHA", 32), ("GQA", 8), ("MQA", 1)]: 12 size = cache_gib(kv_heads) 13 print(f"{label}: kv_heads={kv_heads:2d}, cache={size:.2f} GiB, reduction={mha / size:.0f}x")
Output
1MHA: kv_heads=32, cache=4.00 GiB, reduction=1x 2GQA: kv_heads= 8, cache=1.00 GiB, reduction=4x 3MQA: kv_heads= 1, cache=0.12 GiB, reduction=32x

FlashAttention and MQA/GQA solve different problems. FlashAttention cuts temporary attention I/O, while MQA/GQA cut persistent KV-cache size.

Multi-head latent attention (MLA) compresses KV state further, as in DeepSeek-V2. That's a later serving layout, not a change to softmax(QKT/dk)V\mathrm{softmax}(QK^T/\sqrt{d_k})Vsoftmax(QKT/dk​​)V.[10]Reference 10DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Modelhttps://arxiv.org/abs/2405.04434

Which optimization reduces temporary attention-matrix I/O, and which reduces persistent KV-cache bandwidth during decoding?

Answer

FlashAttention reduces temporary attention-matrix I/O by tiling attention and avoiding materializing the full n×nn \times nn×n matrix. MQA and GQA reduce persistent KV-cache bandwidth by sharing or grouping key/value heads during autoregressive decoding.


Softmax numerical stability

The two-token walkthrough already subtracted the row max before exp. That isn't a style choice.

Naive softmax overflows: exe^xex grows so fast that a large positive logit becomes Inf, and Inf / Inf is NaN. Attention kernels need a stable form so large but valid scores don't corrupt routing weights.

The max-shift technique

Subtract the maximum value from the input vector before exponentiating:

softmax(xi)=exi−max⁡(x)∑jexj−max⁡(x)\text{softmax}(x_i) = \frac{e^{x_i - \max(\mathbf{x})}}{\sum_j e^{x_j - \max(\mathbf{x})}}softmax(xi​)=∑j​exj​−max(x)exi​−max(x)​

Before running the example, predict what stays invariant: shifting [1000,1001,999][1000, 1001, 999][1000,1001,999] by its maximum changes the numbers sent to exp, not the relative probabilities. The largest shifted logit should receive the largest weight, and the row should still sum to 1.

Subtracting max⁡(x)\max(\mathbf{x})max(x) shifts every logit by the same constant. That constant factors out of the exponentials and cancels between numerator and denominator, so the probabilities don't change.

The largest exponent is e0=1e^0 = 1e0=1, which blocks positive overflow. Very negative shifted values may underflow toward zero, which is harmless.[11]Reference 11Online normalizer calculation for softmax.https://arxiv.org/abs/1805.02867

Online softmax (Milakov and Gimelshein, 2018) extends the same identity to a streaming pass. It tracks a running maximum and a running sum of exponentials, then rescales the sum when a larger maximum appears. That's the algebra FlashAttention uses to tile a query row without storing every score.[11]Reference 11Online normalizer calculation for softmax.https://arxiv.org/abs/1805.02867

why-shifting-doesnt-change-the-answer.py
1import math 2 3def stable_softmax(logits: list[float]) -> list[float]: 4 shift = max(logits) 5 exps = [math.exp(x - shift) for x in logits] 6 total = sum(exps) 7 return [x / total for x in exps] 8 9probs = stable_softmax([1000.0, 1001.0, 999.0]) 10print([round(p, 4) for p in probs]) 11print(round(sum(probs), 6)) 12print(probs[1] == max(probs))
Output
1[0.2447, 0.6652, 0.09] 21.0 3True

FlashAttention: tiled exact attention

The n×nn \times nn×n score matrix is the memory problem. If it's temporary, the useful question is whether we need to write the whole thing to HBM at all.

FlashAttention (Dao et al., 2022) keeps the definition exact and changes the execution order. Auxiliary attention state is linear in sequence length; the algorithm still does quadratic work.[12]Reference 12FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135

Instead of writing the full score matrix to GPU HBM, it computes attention in tiles that fit in on-chip SRAM and uses online softmax so each tile can update a running row max, sum, and output accumulator.[12]Reference 12FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135[11]Reference 11Online normalizer calculation for softmax.https://arxiv.org/abs/1805.02867

PropertyStandard attentionFlashAttention
Memory for attention computationO(n2)O(n^2)O(n2)O(n)O(n)O(n) extra beyond Q,K,V,OQ,K,V,OQ,K,V,O
HBM trafficWrites and rereads large score matricesKeeps tiles on chip; no full SSS or PPP in HBM
ExactYesYes
Wall-clock speedBaselineDepends on hardware, shapes, dtype, and kernel availability

FlashAttention-2 keeps that math and changes how work is partitioned across GPU thread blocks.[3]Reference 3FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691 FlashAttention-3 adds asynchrony and low-precision paths on Hopper-class GPUs.[13]Reference 13FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision.https://arxiv.org/abs/2407.08608

Later kernels still implement softmax(QKT/dk)V\mathrm{softmax}(QK^T/\sqrt{d_k})Vsoftmax(QKT/dk​​)V; they don't introduce a new attention formula.

FlashAttention keeps full Q, K, V, and output tensors in HBM, streams small tiles through SRAM, updates online softmax state on chip, and never stores the full score matrix in HBM.
FlashAttention keeps one query tile and the running row state in SRAM while streaming key and value tiles. Each transient score tile updates the row max, softmax sum, and output accumulator, so the exact output is written without materializing an $n \times n$ score or probability matrix in HBM.

You don't write a custom CUDA kernel to use this in PyTorch. F.scaled_dot_product_attention dispatches among FlashAttention-2, memory-efficient attention, and a C++ math fallback when the CUDA backend can. It may enable grouped-query attention through enable_gqa.

The public contract is the numeric result, not which kernel ran.[2]Reference 2torch.nn.functional.scaled_dot_product_attentionhttps://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html[3]Reference 3FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691


Attention in modern architectures

Once Q/K/V roles and mask semantics are separate, architecture differences become a small lookup: where do Q, K, and V come from, and which score cells remain visible?

ArchitectureSelf-attentionCross-attentionTypical objective
BERTBidirectionalNoMasked language modeling
Decoder-only LMCausalNoNext-token prediction
T5Bidirectional encoder + causal decoderYesSpan corruption
WhisperBidirectional encoder + causal decoderYesSpeech-to-text seq2seq
Vision Transformer (ViT)BidirectionalNoImage classification / self-supervision

BERT and ViT use bidirectional self-attention because the full input is already there. Decoder-only language models use causal self-attention because they generate left to right; looking at future tokens during training would leak the answer.

Encoder-decoder models such as T5 and Whisper mix both: a bidirectional encoder reads the source, a causal decoder writes the target, and cross-attention is the bridge.

ViT is the same attention operation after a different tokenizer. An image becomes patch tokens, then bidirectional self-attention mixes those patches with no causal mask. That's the next chapter.


Derivation checklist

TierDefense target
FoundationalDerive Attention(Q,K,V)=softmax(QKT/dk)V\text{Attention}(Q,K,V)=\text{softmax}(QK^T/\sqrt{d_k})VAttention(Q,K,V)=softmax(QKT/dk​​)V and annotate the dimensions of each tensor.
IntermediateDerive why dividing by dk\sqrt{d_k}dk​​ normalizes score variance under independent unit-variance assumptions.
AdvancedDistinguish bidirectional self-attention, causal self-attention, and cross-attention by Q/K/V source and mask.
AdvancedExplain why multi-head attention runs several lower-dimensional attention heads without changing leading attention-core asymptotic FLOPs when dmodeld_{\text{model}}dmodel​ is fixed.
AdvancedSeparate O(n2d)O(n^2 d)O(n2d) attention-core time, O(ndmodel2)O(n d_{\text{model}}^2)O(ndmodel2​) projection time, and O(n2)O(n^2)O(n2) naive attention memory.
AdvancedExplain why FlashAttention cuts temporary attention I/O while MQA/GQA shrink persistent KV-cache traffic during decoding.
AdvancedDescribe max-shift softmax, online softmax, and why numerical stability matters inside attention kernels.

What happens if you remove the scaling factor for a large head dimension such as dk=512d_k=512dk​=512?

Answer

Under the independent unit-variance calculation, the variance of q⋅kq \cdot kq⋅k grows with dkd_kdk​, so its standard deviation grows as dk\sqrt{d_k}dk​​. At dk=512d_k=512dk​=512, unscaled logits are much more likely to saturate softmax. That can weaken gradients through Q and K and make routing behave like a brittle hard lookup rather than a smooth weighted average.

Why use multiple attention heads instead of one large head?

Answer

Multiple heads give the model several separately projected routing spaces in parallel. Studies have measured recognizable patterns in some trained heads, but you must inspect a particular model before naming what its heads do. With fixed dmodeld_{\text{model}}dmodel​, each head is narrower, so the leading attention-core FLOPs stay in the same asymptotic class.

How can transformer systems handle very long contexts such as 100K tokens?

Answer

There isn't one trick. FlashAttention reduces temporary memory and HBM traffic for exact attention, but exact attention still has quadratic work. MQA and GQA shrink KV-cache bandwidth during decoding. Long-context systems may also use sliding-window attention, distributed cache placement, retrieval, or linear/state-space alternatives when they accept a different quality-efficiency tradeoff.

Mistakes that break attention

MistakeSymptomFix
Forgetting the K transposeMatmul shape error before softmaxCompute Q @ K.transpose(-2, -1) so scores have shape (B, h, N, N).
Skipping dk\sqrt{d_k}dk​​ scalingScore spread rises with head width under the initialization model; attention may saturateDivide scores by math.sqrt(d_k) before masking and softmax.
Mixing up attention memory and timeBad long-context sizing estimatesTrack attention-core FLOPs, projection FLOPs, temporary score memory, and KV-cache memory separately.
Treating FlashAttention and GQA as the same optimizationWrong performance diagnosisUse FlashAttention for temporary attention I/O; use MQA/GQA for persistent KV-cache traffic.
Forgetting padding masks in cross-attentionDecoder attends to fake source tokensMask padded encoder positions even though source positions don't need a causal mask.
Confusing Q/K/V rolesHard-to-debug routing behaviorRemember: Q and K choose where information flows; V carries the content being mixed.
Using naive softmaxInf, NaN, or unstable probabilitiesSubtract the row max, or use a framework primitive that already applies stable softmax.
Softmax on the wrong axisRows don't sum to 1; routing is meaninglessApply softmax over the key dimension (dim=-1), not the query dimension.
Allowing a fully masked query rowNaN with -inf, or silent blocked-key mixing with a finite fillGuarantee one valid key per active query, or zero/skip outputs for padded queries.

Going deeper

"Why not use additive attention instead of dot-product?"

Bahdanau et al. (2015)[14]Reference 14Neural Machine Translation by Jointly Learning to Align and Translate.https://arxiv.org/abs/1409.0473 used additive attention: score(q,k)=vTtanh⁡(Wqq+Wkk)\text{score}(q, k) = v^T \tanh(W_q q + W_k k)score(q,k)=vTtanh(Wq​q+Wk​k). In the Transformer paper, Vaswani et al. note that additive and dot-product attention behave similarly at small dimensions, but dot-product maps much better to batched matrix multiplication and is much faster at the larger dimensions used in transformers.[1]Reference 1Attention Is All You Need.https://arxiv.org/abs/1706.03762 The 1dk\frac{1}{\sqrt{d_k}}dk​​1​ scaling is what keeps dot-product attention stable as dkd_kdk​ grows.

"Can attention attend to nothing / everything equally?"

If all scores are equal, softmax produces a uniform distribution 1/n1/n1/n, and the output is the average of all value vectors. If one score dominates, softmax approximates an argmax, so the output is approximately one value vector. The sharpness of the logits controls where on this spectrum you land.

Practice drill

Build a tiny attention-debug notebook or trace for three tokens:

  1. Print Q, K, score, mask, softmax row sums, and output shapes after every operation.
  2. Add one padded key and one causal mask case, then verify blocked positions receive zero probability.
  3. Trigger one wrong-axis softmax failure and record the symptom you would see in row sums or output behavior.
  4. Write a checklist for reviewing production attention code: tensor shapes, mask semantics, stable softmax, and fully masked rows.

This makes routing errors observable before they become mysterious model-quality bugs.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A layer computes scores with Q @ K.T, applies softmax to those scores, and then multiplies by V. If a query token should pull information from a source token, which tensors choose the source position and which tensor supplies the content?

Correct answer: Q and K determine the source position through their dot product; V supplies the content mixed into the output.

Attention weights come from compatibility scores between each query and each key. Those weights route information, but the vectors being averaged after softmax are the values. Swapping K and V confuses routing metadata with the content being transported.

2.Q has shape (B, h, Nq, d_k), K has shape (B, h, Nk, d_k), and V has shape (B, h, Nk, d_v). Which computation gives scaled dot-product attention with the right output shape?

Correct answer: softmax((Q @ K.transpose(-2, -1)) / sqrt(d_k), dim=-1) @ V, output (B, h, Nq, d_v)

K must be transposed so Q @ K^T produces one score for each query-key pair, with shape (B, h, Nq, Nk). Softmax runs over the key axis, and multiplying those weights by V replaces the key axis with d_v, giving (B, h, Nq, d_v).

3.Assume each q_i and k_i is independent with mean 0 and variance 1. For d_k = 64, what standard deviations should you expect for q dot k before and after dividing by sqrt(d_k)?

Correct answer: Raw standard deviation 8, scaled standard deviation 1.

Each product q_i k_i has variance 1, so the variance of the sum over 64 independent dimensions is 64. The raw standard deviation is therefore sqrt(64) = 8. Dividing by sqrt(d_k) divides the standard deviation by 8, leaving it near 1.

4.While implementing masked attention with scores shaped (B, h, Nq, Nk), which sequence preserves a valid distribution for each active query?

Correct answer: Fill blocked key logits with -inf, use stable softmax over the key axis, and handle rows with no visible key explicitly.

Attention normalizes across keys for each query, so softmax belongs on the last axis. A blocked key should receive zero probability, which -inf enforces after softmax. A row with no visible key has no valid distribution, so it must be rejected, skipped, or zeroed as a padded-query case rather than hidden with a finite fill value.

5.A speech-to-text model encodes the full audio sequence, then generates text left to right while consulting encoder states. Which attention arrangement permits source access without leaking future target tokens?

Correct answer: Use bidirectional encoder self-attention, causal decoder self-attention, and decoder queries over encoder keys and values.

The encoder may use the complete source because the full audio is available. Decoder self-attention must block future target positions to keep generation autoregressive. Cross-attention then uses decoder queries with encoder keys and values, allowing every generated position to consult the encoded source without exposing future target tokens.

6.With d_model = 512 fixed, compare one head with d_head = 512 to 8 heads with d_head = 64 at the same sequence length. What happens to the leading attention-core score-and-value work?

Correct answer: It stays in the same leading order because h x d_head = d_model in both configurations.

The attention core work is proportional to h x n^2 x d_head for score computation and value mixing. Holding d_model fixed means h x d_head stays equal to d_model, so splitting into more narrower heads restructures the work without increasing the leading attention-core asymptotic cost.

7.A naive attention implementation materializes one score tensor with shape (B, h, n, n). If B and h stay fixed and n grows from 512 to 2048, how does that tensor's memory change?

Correct answer: It grows 16x because both the query axis and key axis become four times longer.

The materialized score tensor has one entry for every query-key pair. Increasing n from 512 to 2048 multiplies n by 4 on both axes, so the number of score entries grows by 4 x 4 = 16.

8.For a full multi-head attention block at sequence length n and fixed d_model, which sizing statement separates the main costs correctly?

Correct answer: The attention core is O(n^2 d_model), Q/K/V and output projections are O(n d_model^2), and a naive score tensor uses O(n^2) memory per head.

The attention core touches every query-key pair and does width-dimension dot products and value mixing, giving O(n^2 d_model) across heads. The dense projections operate once per token across d_model by d_model matrices, giving O(n d_model^2). A materialized score matrix is n by n for each head.

9.A team sees two bottlenecks: training long sequences runs out of memory from materialized attention scores, and autoregressive serving is bandwidth-bound reading the growing KV cache. Which pair of changes targets those bottlenecks?

Correct answer: Use FlashAttention for temporary attention I/O, and use MQA or GQA to reduce persistent KV-cache bytes.

FlashAttention changes the execution order of exact attention so the full n x n score matrix is not materialized in high-bandwidth memory. MQA and GQA change how many key/value heads are stored and read during decoding, so they reduce persistent KV-cache traffic rather than the temporary score-matrix problem.

10.An attention kernel processes a long row of logits in tiles. Which softmax strategy can preserve exact probabilities without exponentiating huge values or storing the whole row at once?

Correct answer: Track a running row maximum and exponential sum, rescaling the sum when a larger maximum appears.

Subtracting a row maximum preserves softmax ratios because the same exponential factor cancels from numerator and denominator, while keeping the largest exponent at exp(0). Online softmax extends that idea by maintaining a running maximum and sum as tiles stream through, correcting the sum when the maximum changes so the final probabilities match full-row softmax.

10 questions remaining.

Next Step
Continue to Vision Transformers and Image Encoders

Scaled dot-product attention, including the mask and the $\sqrt{d_k}$ scale, is now a working operator. The next chapter reuses that same bidirectional operation after an image is cut into patch tokens.

PreviousEmbedding Similarity & Quantization
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Attention Is All You Need.

Vaswani, A., et al. · 2017

https://arxiv.org/abs/1706.03762

torch.nn.functional.scaled_dot_product_attention

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html

FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.

Dao, T. · 2023 · ICLR 2024

https://arxiv.org/abs/2307.08691

Analyzing Multi-Head Self-Attention: Specialized Heads Do the Heavy Lifting.

Voita, E., et al. · 2019 · ACL 2019

https://arxiv.org/abs/1905.09418

Are Sixteen Heads Really Better than One?.

Michel, P., Levy, O., & Neubig, G. · 2019 · NeurIPS 2019

https://arxiv.org/abs/1905.10650

In-context Learning and Induction Heads.

Olsson, C., et al. · 2022

https://arxiv.org/abs/2209.11895

Fast Transformer Decoding: One Write-Head is All You Need.

Shazeer, N. · 2019 · arXiv preprint

https://arxiv.org/abs/1911.02150

GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.

Ainslie, J., et al. · 2023 · EMNLP 2023

https://arxiv.org/abs/2305.13245

The Llama 3 Herd of Models.

Dubey, A., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2407.21783

DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model

DeepSeek-AI · 2024

https://arxiv.org/abs/2405.04434

Online normalizer calculation for softmax.

Milakov, M. & Gimelshein, N. · 2018

https://arxiv.org/abs/1805.02867

FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.

Dao, T., Fu, D. Y., Ermon, S., Rudra, A., & Ré, C. · 2022 · NeurIPS 2022

https://arxiv.org/abs/2205.14135

FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision.

Shah, J., Bikshandi, G., Zhang, Y., Thakkar, V., Ramani, P., & Dao, T. · 2024

https://arxiv.org/abs/2407.08608

Neural Machine Translation by Jointly Learning to Align and Translate.

Bahdanau, D., Cho, K., & Bengio, Y. · 2015 · ICLR 2015

https://arxiv.org/abs/1409.0473

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.