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
LearnInference & Production ScaleFlashAttention & Memory Efficiency
🚀HardInference Optimization

FlashAttention & Memory Efficiency

Understand how FlashAttention cuts auxiliary attention memory from O(n²) to O(n) with tiling and online softmax, and analyze its IO complexity.

36 min read
Learning path
Step 140 of 196 in the full curriculum
Prefix Caching and Prompt CachingContinuous Batching & Scheduling

Personalize this lesson

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

Prefix caching reuses KV state across requests that share a prompt prefix. It removes repeated work between requests, not the all-pairs work inside one uncached attention call. FlashAttention attacks the layer below that: it makes each dense attention kernel move far less data through GPU memory.

A coding assistant still compares every new prompt token with every earlier token while it prefills. Before looking at the number, predict what scales: each query-key pair contributes one saved score, so doubling sequence length squares the score storage. If the implementation writes the full score matrix to GPU RAM, an 8,192-token prompt at batch 8 with 32 heads in 16-bit floats needs about 32 GiB just for those scores.

FlashAttention computes the same attention operator, but it doesn't park the full score or probability matrix in HBM (High Bandwidth Memory, the large GPU RAM pool).[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135 When attention IO is the bottleneck, that can raise throughput or let a longer context fit.

What is the main promise of FlashAttention?

Answer

It computes exact dense attention while avoiding the full n×nn \times nn×n score and probability matrices in HBM. The win is lower memory traffic and lower auxiliary memory, not a different attention formula.

The memory wall

Materialized attention first

Make the baseline explicit before looking at the optimization. In the scaled dot-product attention article, each query token scores every key token, normalizes those scores with softmax, and blends the corresponding value vectors.

Suppose you have only three prompt tokens from a failing-test message, and a head dimension of two:

  • Query QQQ is a 3×23 \times 23×2 matrix (one row per token).
  • Key KKK and Value VVV are also 3×23 \times 23×2.

The score matrix S=QKTS = QK^TS=QKT is 3×33 \times 33×3. That's nine numbers. Now make the prediction before scaling up: at 8,192 tokens, the same all-pairs layout is the memory problem.

For batch=8batch=8batch=8, heads=32heads=32heads=32, seq_len=8192seq\_len=8192seq_len=8192 in 16-bit floating point (FP16):

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

That product is exactly 34,359,738,36834{,}359{,}738{,}36834,359,738,368 bytes, which is 32 GiB or about 34 GB depending on whether you count in binary or decimal units. If an attention implementation saves this intermediate, it consumes a large part of an 80 GB A100 before counting model weights, other activations, or gradients.

Check that prediction with a small calculation before blaming model weights for an out-of-memory error:

score-matrix-size.py
1batch, heads, sequence, bytes_per_value = 8, 32, 8192, 2 2score_bytes = batch * heads * sequence * sequence * bytes_per_value 3 4print(f"score values: {batch * heads * sequence * sequence:,}") 5print(f"binary size: {score_bytes / 1024**3:.2f} GiB") 6print(f"decimal size: {score_bytes / 1000**3:.2f} GB")
Output
1score values: 17,179,869,184 2binary size: 32.00 GiB 3decimal size: 34.36 GB

Why does an 8,192-token attention matrix become huge even before model weights are counted?

Answer

Each head compares every query token with every key token, so storage grows with seq_len2seq\_len^2seq_len2. With batch 8, 32 heads, 8,192 by 8,192 scores, and 2 bytes per FP16 score, the score matrix alone is about 34 GB (32 GiB).

GPU memory hierarchy

The score count explains capacity. Now follow those values across the memory hierarchy. A GPU isn't a flat memory space. It has layers:

  • On-chip SRAM (static random-access memory) is the tiny, very fast scratchpad right next to the compute cores.
  • HBM is the large pool of GPU memory (VRAM) that holds model weights and tensors.
  • CPU DRAM is host memory outside the GPU, used only when data must leave the card entirely.

Memory hierarchy intuition: On-chip SRAM is a tiny, fast scratchpad beside the compute cores. HBM is much larger, but every revisit costs bandwidth. A materializing attention baseline keeps returning to HBM because the full n×nn \times nn×n score matrix doesn't fit in the scratchpad. FlashAttention keeps only active tiles there at a time.

In the original FlashAttention paper, the motivating A100 numbers are 192 KB of on-chip SRAM per streaming multiprocessor across 108 SMs (about 20 MB aggregate) at roughly 19 TB/s, versus 40-80 GB of HBM at 1.5-2.0 TB/s.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135 A kernel doesn't get that full 20 MB as one giant scratchpad: practical tile sizes are bounded by much smaller per-SM shared-memory and register budgets. Chip layouts change by generation, but the qualitative gap stays: on-chip memory is tiny and fast, off-chip memory is large and much slower to revisit.

Memory tierTypical role in attentionCapacity / bandwidth intuition
On-chip SRAMHold the current Q/K/V tiles and running softmax statisticsTiny, but fast enough to reuse the same tile many times
HBMHold Q, K, V, O, model weights, and other activationsMuch larger, but expensive to touch for every intermediate
CPU DRAMHost memory outside the GPULarger still, but not suitable for the inner loop of an attention kernel
Diagram showing SRAM (on-chip) ~192 KB per A100 SM ~20 MB aggregate, ~19 TB/s, HBM (GPU memory) 40-80 GB on A100 1.5-2.0 TB/s, and CPU DRAM Larger host memory Far lower bandwidth to the GPU.
SRAM (on-chip) ~192 KB per A100 SM ~20 MB aggregate, ~19 TB/s, HBM (GPU memory) 40-80 GB on A100 1.5-2.0 TB/s, and CPU DRAM Larger host memory Far lower bandwidth to the GPU.

Use this as a capacity pyramid, not a one-way data pipe. Compute wants tiles in SRAM, so ask what happens when the same tile is reused. A materializing baseline keeps bouncing the full n×nn \times nn×n score and probability matrices through HBM, while FlashAttention pays for local bookkeeping to avoid those round-trips.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135

Materialized attention can become IO-limited

In a materializing baseline, the n×nn \times nn×n score and probability matrices are written to HBM and read again. A fused backend may already avoid those intermediates, so the useful comparison is FlashAttention versus the backend the system would otherwise execute, not versus every call named "attention."

Why is HBM traffic the bottleneck even though GPUs have enormous FLOPs?

Answer

Matrix math can run very fast once data is on-chip, but repeatedly writing and rereading n2n^2n2 score and probability matrices through HBM stalls the compute units. FlashAttention improves arithmetic intensity by reusing tiles in SRAM before writing only final outputs and row statistics.

The FlashAttention algorithm

Core idea: tiling + online softmax

The baseline keeps the attention formula, but its temporary intermediates live too long. FlashAttention combines three ideas instead of materializing the full n×nn \times nn×n attention matrix:

  1. Tiles the computation into blocks that fit in SRAM.
  2. Uses online softmax to compute exact softmax without seeing all values at once.
  3. Never materializes the full attention matrix in HBM.

This block-wise processing strategy avoids quadratic auxiliary score and probability storage. The input, output, and saved row-statistic tensors still scale with sequence length. Before looking at the schedule, predict what should disappear from HBM: temporary score and probability tiles, not the final output.

Trace two data lifetimes in the figure. A materializing baseline writes the full score matrix to HBM, reads it back for softmax, writes the probability matrix, and reads it again for the final multiply. FlashAttention streams small tiles through SRAM and saves only row-wise statistics.

Materialized attention writes an 8 by 32 by 8192 FP16 score matrix (32 GiB) and a matching probability matrix to HBM. FlashAttention keeps score tiles in SRAM and writes the output plus an 8 MiB FP32 LSE row-stat tensor.
For the running fixture (batch 8, 32 heads, 8,192 tokens), a materializing baseline parks a 32 GiB FP16 score matrix in HBM and then rereads it as probabilities. FlashAttention keeps those tiles on-chip and writes the output plus an 8 MiB FP32 LSE vector.

The left path changes memory traffic by revisiting n×nn \times nn×n intermediates in HBM. The right path reuses small blocks in SRAM, then writes the output and compact row statistics. Same operator, different data lifetime.

Why doesn't tiling make FlashAttention approximate?

Answer

Tiling changes the order of computation, not the attention definition. Online softmax rescales previous partial sums whenever a new block changes the row maximum, so the final normalized output matches dense attention up to normal floating-point ordering differences.

How tiling fits in SRAM

Tiling changes data lifetime. Pull a block of QQQ, KKK, and VVV from HBM into SRAM, compute local scores there, and throw the score tile away. During forward execution the kernel writes final output OOO and, for training, compact row statistics for backward. It doesn't write the full score or probability matrices.

Now the math problem appears. Softmax over a row needs a global max and a global sum. If SRAM holds one K/V tile at a time, you can't see the rest of the row yet. Online softmax is the fix that keeps the result exact.

Ask what must fit at the same time. Tile size is bounded by on-chip capacity. This simplified payload check counts four FP16 tile-shaped arrays (Q, K, V, and a partial output), while production kernels also budget for statistics, registers, and implementation overhead:

tile-working-set.py
1block_rows, head_dimension, arrays, bytes_per_value = 128, 64, 4, 2 2payload_bytes = block_rows * head_dimension * arrays * bytes_per_value 3 4print(f"simplified tile payload: {payload_bytes / 1024:.0f} KiB") 5print("also budget: row statistics, registers, and kernel overhead")
Output
1simplified tile payload: 64 KiB 2also budget: row statistics, registers, and kernel overhead

Online softmax with a concrete example

Suppose SRAM fits only two scores at a time. A longer row can still be normalized if you keep a running max mmm and denominator ℓ\ellℓ, then rescale whenever a later tile raises the max.

Standard softmax on three scores

Suppose one query token sees key scores [1.0,  2.0,  0.5][1.0,\; 2.0,\; 0.5][1.0,2.0,0.5].

  1. Find the max: m=2.0m = 2.0m=2.0.
  2. Exponentiate relative to the max: [e−1.0,  e0,  e−1.5]≈[0.368,  1.0,  0.223][e^{-1.0},\; e^{0},\; e^{-1.5}] \approx [0.368,\; 1.0,\; 0.223][e−1.0,e0,e−1.5]≈[0.368,1.0,0.223].
  3. Sum: ℓ=0.368+1.0+0.223=1.591\ell = 0.368 + 1.0 + 0.223 = 1.591ℓ=0.368+1.0+0.223=1.591.
  4. Normalize: [0.231,  0.629,  0.140][0.231,\; 0.629,\; 0.140][0.231,0.629,0.140].

A materializing baseline would store the full score matrix in HBM just to run that four-step process for every row.

Online softmax with two blocks

Now split the same scores into Block A [1.0,  2.0][1.0,\; 2.0][1.0,2.0] and Block B [0.5][0.5][0.5]. Predict Block B's job: it must contribute its weight without forcing us to retain Block A's raw scores.

Processing Block A:

  • Local max: mA=2.0m_A = 2.0mA​=2.0.
  • Local denominator: ℓA=e−1.0+e0≈1.368\ell_A = e^{-1.0} + e^{0} \approx 1.368ℓA​=e−1.0+e0≈1.368.
  • Local unnormalized numerator: NA=e−1.0⋅V1+e0⋅V2N_A = e^{-1.0} \cdot V_1 + e^{0} \cdot V_2NA​=e−1.0⋅V1​+e0⋅V2​.

Processing Block B:

  • Local max: mB=0.5m_B = 0.5mB​=0.5.
  • New global max: mnew=max⁡(2.0,  0.5)=2.0m_{new} = \max(2.0,\; 0.5) = 2.0mnew​=max(2.0,0.5)=2.0.
  • Rescale the old denominator: ℓnew=e2.0−2.0⋅1.368+e0.5−2.0=1.368+0.223=1.591\ell_{new} = e^{2.0 - 2.0} \cdot 1.368 + e^{0.5 - 2.0} = 1.368 + 0.223 = 1.591ℓnew​=e2.0−2.0⋅1.368+e0.5−2.0=1.368+0.223=1.591.
  • Update the numerator: Nnew=e0⋅NA+e−1.5⋅V3N_{new} = e^{0} \cdot N_A + e^{-1.5} \cdot V_3Nnew​=e0⋅NA​+e−1.5⋅V3​.

The final row output is O=Nnew/1.591O = N_{new} / 1.591O=Nnew​/1.591, the same as dense softmax. We never held all three scores in the fast workspace at once. In this split the max didn't move, so the rescale factor is 111. Change Block B's score to 3.03.03.0 and the next checkpoint makes the rescaling rule visible.

Online softmax over two tiles for scores 1.0 and 2.0, then 0.5. Block A stores max 2.0 and denominator 1.368. Block B has max 0.5, so the global max stays 2.0, old terms keep scale 1, and the combined denominator 1.591 matches dense softmax.
Block A holds 1.0 and 2.0 with max 2.0 and denominator 1.368. Block B's 0.5 doesn't raise the max, so old terms keep scale 1 and the combined denominator 1.591 matches the dense row.

Test the same invariant with scalar values, independent of any GPU kernel:

online-softmax-two-blocks.py
1import math 2 3scores_a, values_a = [1.0, 2.0], [10.0, 20.0] 4scores_b, values_b = [0.5], [40.0] 5 6def local_state(scores, values): 7 max_score = max(scores) 8 weights = [math.exp(score - max_score) for score in scores] 9 return max_score, sum(weights), sum(weight * value for weight, value in zip(weights, values)) 10 11m_a, l_a, n_a = local_state(scores_a, values_a) 12m_b, l_b, n_b = local_state(scores_b, values_b) 13m = max(m_a, m_b) 14l = math.exp(m_a - m) * l_a + math.exp(m_b - m) * l_b 15n = math.exp(m_a - m) * n_a + math.exp(m_b - m) * n_b 16online = n / l 17 18all_scores = scores_a + scores_b 19all_values = values_a + values_b 20dense_weights = [math.exp(score - max(all_scores)) for score in all_scores] 21dense = sum(w * v for w, v in zip(dense_weights, all_values)) / sum(dense_weights) 22 23print(f"online output: {online:.6f}") 24print(f"dense output: {dense:.6f}") 25print(f"match: {abs(online - dense) < 1e-12}")
Output
1online output: 20.492649 2dense output: 20.492649 3match: True

If Block B had contained a score of 3.03.03.0 instead of 0.50.50.5, what must happen to Block A's previous contributions?

Answer

They must be rescaled by e2.0−3.0e^{2.0 - 3.0}e2.0−3.0 because the global max changed from 2.0 to 3.0. Without that down-weighting, the denominator and numerator would mix values normalized against different maxima.

The general update rule

The two-block example scales to every row and every K/V tile. For each new block of scores snews_{\text{new}}snew​ and value vectors VblockV_{\text{block}}Vblock​:

mnew=max⁡(mold,  max⁡(snew))m_{\text{new}} = \max(m_{\text{old}},\; \max(s_{\text{new}}))mnew​=max(mold​,max(snew​))

ℓnew=emold−mnew⋅ℓold+∑jesj−mnew\ell_{\text{new}} = e^{m_{\text{old}} - m_{\text{new}}} \cdot \ell_{\text{old}} + \sum_j e^{s_j - m_{\text{new}}}ℓnew​=emold​−mnew​⋅ℓold​+∑j​esj​−mnew​

Nnew=emold−mnew⋅Nold+∑jesj−mnewVjN_{\text{new}} = e^{m_{\text{old}} - m_{\text{new}}} \cdot N_{\text{old}} + \sum_j e^{s_j - m_{\text{new}}} V_jNnew​=emold​−mnew​⋅Nold​+∑j​esj​−mnew​Vj​

Onew=NnewℓnewO_{\text{new}} = \frac{N_{\text{new}}}{\ell_{\text{new}}}Onew​=ℓnew​Nnew​​

Where mmm is the running max score (for numerical stability), ℓ\ellℓ is the running softmax denominator, NNN is the running unnormalized numerator accumulator, and OOO is the normalized output.

The rescaling terms emold−mnewe^{m_{\text{old}} - m_{\text{new}}}emold​−mnew​ keep previous results correct even though the max changed. This is the mathematical trick that eliminates the need for a second pass over the full row.[2]Reference 2Online normalizer calculation for softmax.https://arxiv.org/abs/1805.02867

The equations explain correctness. Loop order determines how often QQQ, KKK, VVV, OOO, and row statistics cross HBM. A materializing baseline makes multiple round-trips for the full n×nn \times nn×n matrix; FlashAttention performs tile-local score and softmax work on-chip, writing output and compact saved statistics to HBM.

Pseudocode

Now connect the state update to a hardware schedule. The original FlashAttention paper loops over K/V tiles in the outer loop and writes each Q tile's running state back to HBM between inner steps.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135 FlashAttention-2 keeps a Q tile on-chip and streams K/V tiles past it, which cuts extra HBM traffic for OOO and the row stats.[3]Reference 3FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691 The sketch below follows that later loop nest.

Read it as a correctness sketch, not a production kernel. It takes QQQ, KKK, and VVV as nested Python lists and a block size. The last tile can be shorter than block_size; the test uses a block size that doesn't divide nnn, so remainder tiles get exercised too.

flash-attention-sketch.py
1import math 2 3def matmul(a, b): 4 cols = len(b[0]) 5 out = [[0.0] * cols for _ in range(len(a))] 6 for i, row in enumerate(a): 7 for k, aik in enumerate(row): 8 bk = b[k] 9 oi = out[i] 10 for j in range(cols): 11 oi[j] += aik * bk[j] 12 return out 13 14def transpose(matrix): 15 return [list(col) for col in zip(*matrix)] 16 17def scale_rows(matrix, factors): 18 return [[factor * value for value in row] for factor, row in zip(factors, matrix)] 19 20def add_matrices(left, right): 21 return [[x + y for x, y in zip(a, b)] for a, b in zip(left, right)] 22 23def flash_attention(Q, K, V, block_size): 24 n, d = len(Q), len(Q[0]) 25 scale = 1.0 / math.sqrt(d) 26 O = [[0.0] * d for _ in range(n)] 27 for i0 in range(0, n, block_size): 28 Qi = Q[i0:i0 + block_size] 29 bq = len(Qi) 30 Oi = [[0.0] * d for _ in range(bq)] 31 li = [0.0] * bq 32 mi = [float("-inf")] * bq 33 for j0 in range(0, n, block_size): 34 Kj = K[j0:j0 + block_size] 35 Vj = V[j0:j0 + block_size] 36 Sij = [[value * scale for value in row] for row in matmul(Qi, transpose(Kj))] 37 m_local = [max(row) for row in Sij] 38 m_new = [max(old, local) for old, local in zip(mi, m_local)] 39 exp_old = [math.exp(old - new) for old, new in zip(mi, m_new)] 40 exp_new = [ 41 [math.exp(score - new_max) for score in row] 42 for row, new_max in zip(Sij, m_new) 43 ] 44 Oi = add_matrices(scale_rows(Oi, exp_old), matmul(exp_new, Vj)) 45 li = [ 46 old_scale * old_l + sum(new_row) 47 for old_scale, old_l, new_row in zip(exp_old, li, exp_new) 48 ] 49 mi = m_new 50 for row_index, (row, denom) in enumerate(zip(Oi, li)): 51 O[i0 + row_index] = [value / denom for value in row] 52 return O 53 54def dense_attention(Q, K, V): 55 d = len(Q[0]) 56 scale = 1.0 / math.sqrt(d) 57 scores = [[value * scale for value in row] for row in matmul(Q, transpose(K))] 58 output = [] 59 for row in scores: 60 max_score = max(row) 61 weights = [math.exp(score - max_score) for score in row] 62 total = sum(weights) 63 probs = [weight / total for weight in weights] 64 output.append([ 65 sum(prob * V[j][dim] for j, prob in enumerate(probs)) 66 for dim in range(d) 67 ]) 68 return output 69 70def fill(n, d, seed): 71 return [ 72 [math.sin((i + 1) * 1.7 + (j + 1) * 0.9 + seed) for j in range(d)] 73 for i in range(n) 74 ] 75 76n, d = 8, 4 77Q, K, V = fill(n, d, 0.0), fill(n, d, 1.3), fill(n, d, 2.7) 78got = flash_attention(Q, K, V, block_size=3) 79expected = dense_attention(Q, K, V) 80max_diff = max(abs(a - b) for ra, rb in zip(got, expected) for a, b in zip(ra, rb)) 81print(f"max difference: {max_diff:.2e}") 82print(f"match: {max_diff < 1e-12}")
Output
1max difference: 1.11e-16 2match: True

Oi stays an unnormalized numerator until every K/V tile for that Q block has landed. Then we divide by li. The 8×48 \times 48×4 check uses block_size=3, so the last tiles are remainder-sized, and the tiled loop still matches dense attention.

Common mistake: Don't normalize Oi inside the inner loop. The final denominator isn't known until every K/V block has contributed. Early division locks in the wrong scale before a later tile can change the running max.

Production kernels implement the same algebra much more aggressively, while also handling batching, multiple heads, masks, and dropout.

In the pseudocode, why does Oi stay unnormalized until all K/V blocks for a Q tile are processed?

Answer

The final denominator li isn't known until every K/V block has contributed to the row. Normalizing early would lock in the wrong scale before later blocks can change the running max and denominator.

The backward pass: recomputation can win

Training creates a second memory question: backward needs score and probability information. A materializing implementation can save the massive n×nn \times nn×n attention matrices SSS and PPP from forward to compute gradients later. Those saved intermediates can become a major source of out-of-memory (OOM) errors.

FlashAttention solves this by recomputing the needed score and probability tiles during the backward pass instead of storing them all from the forward pass.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135 Because it saves compact row-wise softmax statistics instead, the saved attention state grows as O(n)O(n)O(n) rather than O(n2)O(n^2)O(n2).

The alternative is deliberate recomputation: keep compact running statistics, reconstruct local blocks when backward needs them, and avoid storing the full matrix. That's FlashAttention's training trade-off.

Recomputation isn't free. It adds arithmetic in backward. The point of the FlashAttention paper is that, on the evaluated GPU workloads, avoiding much larger HBM reads and writes more than paid for that arithmetic cost.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135 Measure the trade-off on the model shape and hardware you deploy.

This calculator makes the saved-state difference concrete. It compares one FP16 score matrix with the FP32 softmax_lse tensor saved by the current flash-attention interface, not every tensor in training:

saved-state-comparison.py
1batch, heads, sequence = 8, 32, 8192 2materialized_scores = batch * heads * sequence * sequence * 2 3softmax_lse = batch * heads * sequence * 4 4 5print(f"one saved score matrix: {materialized_scores / 1024**3:.2f} GiB") 6print(f"FP32 softmax LSE: {softmax_lse / 1024**2:.2f} MiB") 7print(f"size ratio: {materialized_scores / softmax_lse:,.0f}x")
Output
1one saved score matrix: 32.00 GiB 2FP32 softmax LSE: 8.00 MiB 3size ratio: 4,096x
PropertyMaterializing baselineFlashAttention
Saved attention state for backwardStore SSS and PPP explicitly: O(n2)O(n^2)O(n2)Store compact row-wise softmax statistics: O(n)O(n)O(n)
Backward strategyRead large intermediates from HBMRecompute local tiles from Q,K,VQ, K, VQ,K,V plus saved stats
Trade-offLess recomputation, much higher memoryMore recomputation, much lower memory

Why can recomputing attention tiles during backward be faster than storing them during forward?

Answer

On modern GPUs, rereading huge saved matrices from HBM can cost more wall-clock time than recalculating small tiles from QQQ, KKK, and VVV. FlashAttention saves row-wise softmax statistics, then spends cheap compute to avoid expensive memory movement.

Complexity analysis

With the mechanism in hand, separate three quantities: mathematical work, extra attention state, and memory traffic. FlashAttention changes the IO complexity by tiling the computation. Let MMM denote the amount of fast SRAM available to hold a tile's working set.

PropertyMaterializing baselineFlashAttention
Auxiliary attention memoryMaterialize SSS and PPP: O(n2)O(n^2)O(n2)Keep row stats and the current output tile: O(n)O(n)O(n)
FLOPsO(n2d)O(n^2 d)O(n2d)O(n2d)O(n^2 d)O(n2d) (same)
HBM reads/writesΘ(nd+n2)\Theta(nd + n^2)Θ(nd+n2)Θ(n2d2/M)\Theta(n^2 d^2 / M)Θ(n2d2/M)
ExactYesYes
For batch 8 and 32 heads, materialized FP16 scores grow from 0.5 GiB at 1K tokens to 8 GiB at 4K and 128 GiB at 16K, while FP32 FlashAttention LSE state grows from 1 MiB to 4 MiB to 16 MiB.
Watch the units. For the same B=8, H=32 fixture, FP16 scores grow 256× from 1K to 16K (0.5 GiB to 128 GiB) while FP32 LSE state grows 16× (1 MiB to 16 MiB).

FLOPs stands for floating-point operations. The memory row refers to extra state created by the attention kernel itself, not the shared QQQ, KKK, VVV, and OOO tensors that both approaches still need to hold.

FlashAttention doesn't reduce the asymptotic mathematical work: both paths still do O(n2d)O(n^2 d)O(n2d) operations. The win is avoiding n2n^2n2 score and probability transfers through HBM. Tile size, scheduling, datatype, and hardware still change observed speed.

Under the paper's SRAM model, tiling changes HBM reads and writes from Θ(nd+n2)\Theta(nd + n^2)Θ(nd+n2) for the materializing baseline to Θ(n2d2/M)\Theta(n^2 d^2 / M)Θ(n2d2/M).[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135 A larger usable SRAM budget MMM lets each tile reuse more data before returning to HBM. This is an IO-model result, not a wall-clock guarantee: both paths still perform O(n2d)O(n^2 d)O(n2d) arithmetic, and kernel schedules determine how much of the bound appears in a benchmark.

Because FlashAttention computes the same dense operator (via online softmax), it doesn't change the model's attention rule. Different floating-point association can still cause small numeric differences.

Predict the curves before running the small scaling check: a score matrix should grow with the square of sequence length, while one row statistic per token should grow linearly.

These numbers distinguish score-matrix scaling from row-statistic scaling:

auxiliary-state-scaling.py
1base_sequence = 1024 2for sequence in [1024, 2048, 4096, 8192, 16384]: 3 materialized_relative = (sequence / base_sequence) ** 2 4 row_stats_relative = sequence / base_sequence 5 print( 6 f"{sequence:>5} tokens: materialized={materialized_relative:>5.0f}x, " 7 f"row-stats={row_stats_relative:>2.0f}x" 8 )
Output
11024 tokens: materialized= 1x, row-stats= 1x 2 2048 tokens: materialized= 4x, row-stats= 2x 3 4096 tokens: materialized= 16x, row-stats= 4x 4 8192 tokens: materialized= 64x, row-stats= 8x 516384 tokens: materialized= 256x, row-stats=16x

Which complexity changes with FlashAttention: FLOPs, auxiliary attention memory, or model quality?

Answer

Auxiliary attention memory drops from O(n2)O(n^2)O(n2) saved score/probability matrices to O(n)O(n)O(n) row statistics, and HBM IO drops sharply. FLOPs remain O(n2d)O(n^2d)O(n2d), and model quality doesn't change because the dense attention result is still exact.

Causal masking in FlashAttention

For autoregressive transformers, attention is causal: token iii can only attend to tokens j≤ij \leq ij≤i. FlashAttention handles this efficiently without materializing a dense causal mask in HBM. The useful question is schedule-level: which K/V tiles are entirely past, on the boundary, or entirely future for a Q tile?

Block-level skipping

If a block of K/V tokens is entirely in the "future" relative to a Q block, the entire block multiplication is skipped. No compute is wasted on keys the query can't see.

Within-block masking

For blocks that straddle the causal boundary, FlashAttention applies the mask after computing scores but before the softmax update. Masked positions become −∞-\infty−∞.

A 4 by 4 causal tile schedule labels past tiles run, diagonal tiles mask, and future tiles skip. Six past plus four diagonal tiles launch, and six future tiles never multiply. The right panel zooms a diagonal tile into a lower-triangular mask.
Count the tiles: 6 past run fully, 4 diagonal apply an in-tile triangle, 6 future never launch. The right grid is one diagonal tile, with future positions set to −∞ before softmax.

Causal masking is part of the tile schedule, not a dense mask matrix in HBM. Skipping future tiles saves work, but the exact speedup depends on sequence length, tile shape, and the kernel. The same skip rule also works for local windows: tiles completely outside the window never multiply.

You can audit causal tile decisions without any GPU code:

causal-tile-schedule.py
1tiles = 4 2counts = {"past": 0, "boundary": 0, "future": 0} 3 4for query_tile in range(tiles): 5 for key_tile in range(tiles): 6 if key_tile < query_tile: 7 decision = "past" 8 elif key_tile == query_tile: 9 decision = "boundary" 10 else: 11 decision = "future" 12 counts[decision] += 1 13 14print(counts) 15print(f"computed tiles: {counts['past'] + counts['boundary']} of {tiles * tiles}")
Output
1{'past': 6, 'boundary': 4, 'future': 6} 2computed tiles: 10 of 16

How does FlashAttention avoid materializing a full causal mask?

Answer

The tile scheduler skips K/V blocks that are entirely in the future and applies an in-tile mask only for blocks that cross the causal boundary. The mask becomes part of the tiled kernel schedule instead of a dense matrix stored in HBM.

FlashAttention-2, FlashAttention-3, and FlashAttention-4

Once tiling and online softmax reduce large HBM transfers, the bottleneck moves into work partitioning and hardware pipelines. Each generation keeps the exact operator while changing that schedule for its target GPU.

FlashAttention-2[3]Reference 3FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691 asks how to keep more independent work in flight:

  • Parallelizes along sequence length as well as batch and heads, so occupancy stays higher when sequences are long and batch size is small.
  • Splits QQQ across warps (groups of 32 GPU threads) instead of splitting KKK/VVV, which cuts shared-memory traffic inside a thread block.
  • Reduces non-matmul FLOPs such as extra softmax rescales.
  • On A100 it reaches 50-73% of theoretical peak (up to 73% forward and 63% backward in the paper body), about 2× FlashAttention-1, with kernel throughput up to 230 TFLOPs/s.[3]Reference 3FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691

On Hopper, the next question is whether data movement, matrix multiply, and softmax can overlap. FlashAttention-3[4]Reference 4FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision.https://arxiv.org/abs/2407.08608 is that redesign:

  • Overlaps TMA (Tensor Memory Accelerator) loads with WGMMA (Warpgroup Matrix-Multiply Accumulate) using warp specialization.
  • Interleaves softmax with the next GEMM so the slow exponential sits under async matmul.
  • Adds an FP8 forward path with block quantization.
  • On H100, the paper reports 1.5-2.0× vs FlashAttention-2 in FP16 forward, up to 740 TFLOPs/s (75% utilization), and close to 1.2 PFLOPs/s in FP8.[4]Reference 4FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision.https://arxiv.org/abs/2407.08608

Blackwell changes that balance again: tensor-core throughput doubled while shared-memory bandwidth and exponential units didn't. FlashAttention-4[5]Reference 5FlashAttention-4: Algorithm and Kernel Pipelining Co-Design for Asymmetric Hardware Scalinghttps://arxiv.org/abs/2603.05451 retunes the pipeline for Blackwell B200 and GB200.

The March 2026 paper reports up to 1,613 TFLOPs/s in FP16/BF16 on B200 (about 71% of peak), with up to 1.3× vs cuDNN 9.13 and 2.7× vs the paper's Triton baseline. Those are kernel numbers from that paper's setup, not guaranteed application speedups.

The implementation is CuTe-DSL embedded in Python, not CUDA C++ templates.

The tiled online-softmax core stays the same. Peak kernels still need hardware-aware scheduling. FlashAttention-1 through 3 are CUDA/CUTLASS-family kernels; FlashAttention-4 is CuTe-DSL. Triton is a productive way to write attention kernels and is the slower baseline in the FlashAttention-4 B200 comparison.

Most application teams should start with a framework SDPA API, verify which backend actually ran, and profile before writing a custom kernel.

What changed after FlashAttention-1?

Answer

The core exact tiled attention idea stayed the same. FlashAttention-2 improved work partitioning and reduced non-matmul overhead. FlashAttention-3 targeted Hopper features such as TMA, WGMMA, asynchronous overlap, and an FP8 forward path. FlashAttention-4 retuned the pipeline for Blackwell and implemented it in CuTe-DSL.

Measured performance

Training throughput

Treat benchmark rows as scoped evidence, not universal multipliers.

During model training, saving quadratic attention intermediates can sharply restrict the maximum sequence length a model can process. As sequence length increases, a materializing baseline may run out of memory even when a fused attention path can still fit.

Common mistake: Assuming FlashAttention is only a "long-sequence hack." The original paper reports a 15% end-to-end wall-clock speedup for BERT-large at sequence length 512 against the MLPerf 1.1 training speed record, so even a moderate evaluated sequence can benefit when attention IO matters.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135

For each result below, keep workload, sequence length, hardware, and baseline attached to the number.

Compared with a baseline that saves full attention intermediates, FlashAttention's auxiliary attention memory grows linearly rather than quadratically. That can enable longer sequences and improve throughput even while a materializing baseline still fits in memory.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135[3]Reference 3FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691

SourceWorkloadReported result
FlashAttention (2022)BERT-large, sequence length 51215% end-to-end vs MLPerf 1.1 speed record[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135
FlashAttention (2022)GPT-2, sequence length 1K3× vs HuggingFace and Megatron-LM baselines[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135
FlashAttention (2022)Long Range Arena, sequence length 1K-4K2.4× speedup[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135
FlashAttention-2 (2023)GPT-style training on A100Up to 225 TFLOPs/s per GPU, 72% model FLOPs utilization[3]Reference 3FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691

These rows answer different questions, so don't transfer one multiplier to another model or GPU. The shared mechanism is the memory path: once the attention kernel stops writing giant intermediates to HBM, longer-sequence training becomes more practical. Measure end-to-end throughput on your own shape.

Inference impact

During inference, FlashAttention helps most when the workload still looks like dense attention over many prompt tokens:

  • Prefill phase benefits the most, because the model still performs full prompt self-attention. That work sits on the time to first token (TTFT) path. An 8,192-token coding-assistant prompt (shared repo guidelines plus a failing test log) still needs every prompt token to score every other prompt token before the first generated token.
  • Decode phase benefits less from dense full-sequence FlashAttention alone, because each step introduces one new query token and reuses the KV cache, so weight streaming and other bottlenecks often dominate.
  • Long prompts benefit more than short prompts, because avoiding a materialized n×nn \times nn×n score matrix matters more as nnn grows.

FlashAttention has its biggest impact when attention itself is the bottleneck. That's usually training and prefill, not naive single-token decode over a contiguous cache.

Production decode uses a related but distinct kernel contract

Serving engines rarely keep one contiguous KV tensor per request. PagedAttention stores K/V in blocks and maps logical positions through a block table. Production decode therefore needs kernels that apply FlashAttention-style tiling and online softmax over non-contiguous KV blocks, beyond the training and prefill dense path.

Examples of that contract (names and APIs evolve):

  • flash_attn_with_kvcache and related FlashAttention decode entry points that accept a packed or paged cache layout
  • FlashInfer and engine-specific paged attention kernels used by stacks such as vLLM

Those kernels are complementary to the PagedAttention allocator: paging decides where blocks live and how they are shared; the decode kernel decides how Q attends to those blocks without materializing full scores. GQA/MQA further shrinks bytes per token that those kernels read. Measure prefill TTFT and decode ITL on your engine; don't assume "FlashAttention = prefill only" or that a training FA kernel is a drop-in for multi-tenant decode.

This shape check shows why prefill creates far more score work per request than one decode step:

prefill-versus-decode.py
1prompt_tokens = 8192 2prefill_scores = prompt_tokens * prompt_tokens 3decode_scores = 1 * prompt_tokens 4 5print(f"prefill scores: {prefill_scores:,}") 6print(f"one decode step scores: {decode_scores:,}") 7print(f"ratio: {prefill_scores // decode_scores:,}x")
Output
1prefill scores: 67,108,864 2one decode step scores: 8,192 3ratio: 8,192x

Hardware compatibility

Generation names are not dispatch guarantees. The algorithmic idea is general, but the fastest kernels are hardware-specific. FlashAttention-2 describes better parallelism and work partitioning for modern GPUs. FlashAttention-3 is a Hopper-focused redesign that targets features such as TMA, WGMMA, and an FP8 forward path.[3]Reference 3FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691[4]Reference 4FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision.https://arxiv.org/abs/2407.08608 FlashAttention-4[5]Reference 5FlashAttention-4: Algorithm and Kernel Pipelining Co-Design for Asymmetric Hardware Scalinghttps://arxiv.org/abs/2603.05451 retunes the pipeline for Blackwell B200 and GB200 GPUs and implements it in CuTe-DSL.

Availability in an application depends on its framework build, device, datatype, tensor shapes, and attention features. Treat backend selection as something to verify, not something to infer from the model name.

When should you expect FlashAttention to help most during inference?

Answer

Expect the biggest inference gain during prefill, especially for long prompts, because the model computes dense attention over the full prompt. Decode often benefits less because each step has one new query token and the KV cache or memory bandwidth elsewhere may dominate.

Using FlashAttention in practice

In modern deep learning frameworks, you rarely implement FlashAttention from scratch. PyTorch exposes torch.nn.functional.scaled_dot_product_attention (SDPA), which may choose an optimized CUDA implementation when the inputs and build support it. Its sdpa_kernel context manager lets you select permitted implementations while testing or profiling. Eligibility and fallback behavior depend on the installed PyTorch build, device, datatype, layout, and attention features, so consult the documentation for that build and measure the actual path.[6]Reference 6torch.nn.functional.scaled_dot_product_attentionhttps://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html

Keep three claims separate: the operator is correct, the requested backend was selected, and the workload got faster. The next checks test them in that order.

First check the causal operator on a tiny row, in plain Python. This is the math SDPA is supposed to implement, not a FlashAttention dispatch test. Query token 1 may see keys 0 and 1 only:

causal-softmax-row.py
1import math 2 3scores = [1.0, 2.0, 0.5, 3.0] 4values = [10.0, 20.0, 40.0, 80.0] 5 6def weighted_sum(scores, values, allowed): 7 masked = [score if ok else float("-inf") for score, ok in zip(scores, allowed)] 8 max_score = max(masked) 9 weights = [ 10 0.0 if score == float("-inf") else math.exp(score - max_score) 11 for score in masked 12 ] 13 total = sum(weights) 14 return sum(weight * value for weight, value in zip(weights, values)) / total 15 16causal = weighted_sum(scores, values, [True, True, False, False]) 17full = weighted_sum(scores, values, [True, True, True, True]) 18print(f"causal output: {causal:.6f}") 19print(f"full output: {full:.6f}") 20print(f"masking changed the blend: {causal != full}")
Output
1causal output: 17.310586 2full output: 58.029621 3masking changed the blend: True

Causal masking changes the blend because the high score 3.0 is future context for query 1. PyTorch SDPA with is_causal=True is supposed to implement that rule. Whether it does so with a FlashAttention kernel is a separate, hardware-dependent question.

Once the math is right, test dispatch. On CUDA hardware, backend restriction is an availability probe, not a speedup proof. This snippet is unmarked because it needs a suitable installed CUDA build and GPU. Current PyTorch documents sdpa_kernel with SDPBackend.FLASH_ATTENTION as the way to request the fused FlashAttention implementation. If that is the only permitted backend and the inputs aren't eligible, PyTorch warns with the reasons and can raise RuntimeError; it doesn't silently switch to the math backend inside that restricted context.[6]Reference 6torch.nn.functional.scaled_dot_product_attentionhttps://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html PyTorch's SDPA tutorial demonstrates the same boundary by catching the error around a forced-backend call.[7]Reference 7Accelerating PyTorch 2 Transformers with torch.compile and scaled_dot_product_attentionhttps://docs.pytorch.org/tutorials/intermediate/scaled_dot_product_attention_tutorial.html For a resilient serving path, catch the probe failure, record it, and make a second, unrestricted SDPA call so PyTorch can choose an available implementation.

request-cuda-flash-backend.py
1import torch 2import torch.nn.functional as F 3from torch.nn.attention import SDPBackend, sdpa_kernel 4 5Q = torch.randn(2, 16, 1024, 64, device="cuda", dtype=torch.float16) 6K = torch.randn(2, 16, 1024, 64, device="cuda", dtype=torch.float16) 7V = torch.randn(2, 16, 1024, 64, device="cuda", dtype=torch.float16) 8 9def attention_with_flash_probe(Q, K, V): 10 try: 11 with sdpa_kernel(SDPBackend.FLASH_ATTENTION): 12 output = F.scaled_dot_product_attention( 13 Q, K, V, is_causal=True, dropout_p=0.0 14 ) 15 return output, "flash_probe_succeeded" 16 except RuntimeError as error: 17 print(f"FlashAttention unavailable: {error}") 18 print("Retrying with unrestricted SDPA backend selection.") 19 output = F.scaled_dot_product_attention( 20 Q, K, V, is_causal=True, dropout_p=0.0 21 ) 22 return output, "automatic_fallback" 23 24output, dispatch_path = attention_with_flash_probe(Q, K, V) 25print("dispatch policy:", dispatch_path)

This exception and retry are deliberate. Keep the warning visible while recording the failed FlashAttention probe; the second call uses PyTorch's normal backend selection. A returned dispatch_path describes policy, not proof of the kernel that executed. Record operator correctness, selected-kernel evidence, and a before/after measurement:

backend-verification-record.py
1verification = { 2 "requested_backend": "flash_attention", 3 "unavailable_policy": "retry_unrestricted_sdpa", 4 "operator_correctness_checked": True, 5 "profiler_shows_selected_kernel": False, 6 "latency_measured": False, 7} 8 9active = ( 10 verification["operator_correctness_checked"] 11 and verification["profiler_shows_selected_kernel"] 12 and verification["latency_measured"] 13) 14print(f"unavailable policy: {verification['unavailable_policy']}") 15print(f"enough evidence to claim speedup: {active}") 16print("next check: capture backend/profiler output on target GPU")
Output
1unavailable policy: retry_unrestricted_sdpa 2enough evidence to claim speedup: False 3next check: capture backend/profiler output on target GPU

For example, current Hugging Face Transformers attention backends accept attn_implementation="flash_attention_2" or "flash_attention_3" as a load-time request. FlashAttention-2 still expects fp16 or bf16. The string is a request, not a profiler trace:

request-hf-flash-backend.py
1import torch 2from transformers import AutoModelForCausalLM 3 4model = AutoModelForCausalLM.from_pretrained( 5 "your-org/your-supported-causal-lm", 6 dtype=torch.bfloat16, 7 attn_implementation="flash_attention_2", 8 device_map="auto", 9)

If you set attn_implementation="flash_attention_2", what should you verify before assuming the speedup is active?

Answer

Check that your GPU, datatype, head dimension, mask pattern, framework version, and installed kernel package support the requested backend. Then profile or inspect backend diagnostics on that exact build before reporting a speedup.

Common mistakes

"FlashAttention is an approximation"

  • Symptom: You hear FlashAttention grouped with sparse or low-rank attention approximations and assume it drops some connections to save memory.

  • Cause: The word "efficient" often implies approximation in other contexts.

  • Fix: FlashAttention is exact. Thanks to the online softmax trick, it computes the same dense attention formula without using sparse or low-rank shortcuts. Numeric outputs can differ slightly from a reference implementation because floating-point operations are associated in a different order, but the mathematical operator is the same. If you need proof, run the tiled Python sketch above and check that the max difference is near zero.

"FlashAttention reduces the number of compute operations"

  • Symptom: You claim that FlashAttention cuts FLOPs.

  • Cause: It's natural to equate "faster" with "fewer operations."

  • Fix: The forward attention computation still has O(n2d)O(n^2 d)O(n2d) floating-point operations (FLOPs). The speedup comes from reduced memory operations (IO), not from changing dense attention into a cheaper mathematical operator. In training, the backward pass can perform more operations because it recomputes tiles. The win is that compute is cheap and memory movement is expensive.

"GPU memory is one big pool"

  • Symptom: You only compare total VRAM capacity and miss why attention still runs slowly on large GPUs.

  • Cause: HBM, on-chip SRAM, shared memory, and registers have very different capacity and bandwidth profiles.

  • Fix: Ask where each tensor lives and how often it crosses the HBM/SRAM boundary. FlashAttention wins because it keeps Q/K/V tiles and softmax state on-chip long enough to reuse them, then writes only the final output and row statistics back to HBM.

"IO complexity is the same as time complexity"

  • Symptom: You explain FlashAttention as if it changes attention from quadratic time to linear time.

  • Cause: The memory table and the FLOP table get mixed together.

  • Fix: Keep the dimensions separate. Dense attention still does quadratic compute in sequence length. FlashAttention reduces HBM reads and writes, so wall-clock time improves when the workload is memory-bound.

"Online softmax is optional bookkeeping"

  • Symptom: You tile attention but normalize each block independently.

  • Cause: The running max and denominator updates look like an implementation detail.

  • Fix: Online softmax is the correctness mechanism. The running max rescales old contributions when a later tile contains a larger score, and the running denominator keeps all blocks normalized against the same global row.

"FlashAttention is only useful for long sequences"

  • Symptom: You skip enabling it on short-context models.

  • Cause: The OOM headlines make FlashAttention look like a long-sequence-only tool.

  • Fix: FlashAttention supports long sequences by avoiding memory limits, and it can still speed up shorter sequences because it reduces HBM access. Dao et al. reported a 15% BERT-large speedup at 512 tokens against the MLPerf 1.1 speed record.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135

"You have to write custom CUDA kernels to use it"

  • Symptom: You avoid FlashAttention because you assume it requires low-level GPU programming.

  • Cause: The original paper describes kernel-level details, which can give the impression that users must write CUDA.

  • Fix: Use a framework SDPA API or supported model integration, then check backend selection and measure on the target GPU. A request flag is configuration, not proof that an optimized kernel ran.

Complete the lesson

Mastery Check

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

1.A materializing attention implementation stores one FP16 score matrix for batch=8, heads=32, and seq_len=8192. Why can that intermediate alone use about 34 GB before model weights are counted?

Correct answer: Each head stores every query-key score, so storage grows with seq_len^2. With batch 8, 32 heads, 8192 by 8192 scores, and 2 bytes per FP16 score, the score matrix alone is about 34 GB, or 32 GiB.

The stored score tensor has shape batch x heads x seq_len x seq_len. Here that's 8 * 32 * 8192 * 8192 = 17,179,869,184 FP16 scores. At 2 bytes each, the matrix is 34,359,738,368 bytes, which is 32 GiB or about 34 GB. KV tensors scale with seq_len * d, and the attention score is one scalar per query-key pair, not one scalar per feature.

2.A teammate says, "FlashAttention is faster because it reduces dense attention from quadratic compute to linear compute." Which correction is technically accurate?

Correct answer: It keeps dense-attention FLOPs at O(n2d)O(n^2 d)O(n2d), but reduces HBM traffic and auxiliary attention memory by tiling into SRAM and avoiding materialized score and probability matrices.

FlashAttention still computes dense attention, so the all-pairs query-key dot products remain quadratic in sequence length. The win isn't fewer mathematical comparisons. The win is that score and probability tiles are used while on-chip, then discarded, so the kernel avoids large HBM round-trips and reduces auxiliary attention state from quadratic matrices to row-wise statistics and current tiles.

3.Under the tiled SRAM model, M is the usable fast memory for a tile working set. Which statement correctly separates FlashAttention's IO complexity from its compute complexity?

Correct answer: It still performs O(n^2 d) FLOPs, while tiled HBM reads/writes are O(n^2 d^2 / M); larger M lets each tile reuse more data before returning to HBM.

M controls how much tile state can be kept and reused in fast memory. FlashAttention still evaluates dense all-pairs attention, so the arithmetic remains O(n^2 d). Under the paper's SRAM model, tiling reduces HBM reads and writes from the materializing O(n d + n^2) pattern to O(n^2 d^2 / M); larger usable SRAM improves reuse rather than removing the quadratic computation.

4.In online softmax, Block A has maximum score 2.0 and its saved denominator and numerator are expressed relative to that max. The next block has maximum score 3.0. What must happen to Block A's previous contributions before combining the blocks?

Correct answer: They must be rescaled by e2.0−3.0e^{2.0 - 3.0}e2.0−3.0 because the global max changed from 2.0 to 3.0. Without that down-weighting, the denominator and numerator would mix values normalized against different maxima.

Softmax terms are represented as exp(score - global_max). Block A's stored terms were relative to 2.0, but after seeing Block B the global max is 3.0. Converting old terms from exp(score - 2.0) to exp(score - 3.0) requires multiplying by exp(2.0 - 3.0). The same scale factor must apply to both the denominator and the numerator accumulator.

5.A simplified tile payload holds four FP16 arrays, each shaped 128 x 64. Ignoring statistics, registers, and overhead, how large is the payload and what sizing conclusion follows?

Correct answer: 64 KiB; a real kernel needs additional capacity for row statistics, registers, and overhead.

The simplified payload is 128 * 64 * 4 * 2 = 65,536 bytes, or 64 KiB. That isn't the complete kernel working set. Production tile selection must also leave capacity for running softmax statistics, registers, synchronization needs, and implementation overhead.

6.During training, a materializing attention implementation can save the score matrix S and probability matrix P for backward. Why can FlashAttention recompute attention tiles during backward and still run faster or fit longer contexts?

Correct answer: On modern GPUs, rereading huge saved matrices from HBM can cost more wall-clock time than recalculating small tiles from QQQ, KKK, and VVV. FlashAttention saves row-wise softmax statistics, then spends cheap compute to avoid expensive memory movement.

The backward pass still needs local score and probability information to compute gradients, but FlashAttention doesn't have to store those full nnn by nnn matrices from forward. It saves compact row-wise softmax statistics, then reconstructs the needed tiles from Q, K, and V during backward. This can add arithmetic, but it avoids much larger HBM reads and writes and reduces saved attention state from O(n2)O(n^2)O(n2) to O(n)O(n)O(n).

7.Causal attention allows query token i to attend only to key token j <= i. In a causal FlashAttention kernel, a query tile covers tokens 128-255 and key/value tiles use the same 128-token width. How should the scheduler treat key/value tiles 0-127, 128-255, and 256-383?

Correct answer: Run 0-127 fully, apply an in-tile mask to 128-255, and skip 256-383 entirely.

For query tokens 128-255, all keys 0-127 are in the past, so that tile can run normally. Tile 128-255 crosses the causal diagonal, so some positions are valid and some must be set to -infinity before the softmax update. Tile 256-383 is entirely in the future, so FlashAttention can skip it instead of computing and masking it later.

8.For an 8192-token prompt, prefill uses every prompt token as a query, while one decode step uses one new query against 8192 cached keys. Which comparison is correct?

Correct answer: Prefill computes 67,108,864 scores; decode computes 8,192, so long-prompt prefill usually benefits more.

Prefill has 8192 query rows and 8192 key columns, giving 8192 * 8192 = 67,108,864 scores. A single decode step has one new query row against 8192 cached keys, giving 1 * 8192 scores. FlashAttention therefore tends to have greater inference impact on dense long-prompt prefill, although actual gains still depend on the active bottleneck.

9.An engineer restricts SDPA to SDPBackend.FLASH_ATTENTION on a shape that the installed CUDA build can't support. What should a production wrapper and its performance claim do?

Correct answer: Catch RuntimeError, record the failed probe, retry unrestricted SDPA if needed, and claim a FlashAttention speedup only after profiler evidence and measurement.

A restricted sdpa_kernel context permits only the named backend. When FlashAttention is ineligible, PyTorch reports the reasons and can raise RuntimeError rather than silently falling back inside that context. A wrapper can make fallback explicit by recording the failed probe and retrying an unrestricted SDPA call. The request itself still isn't proof of dispatch: check the GPU, dtype, shapes, masks, and build, then inspect profiler evidence and measure the target workload.

10.Three kernel teams are choosing a FlashAttention generation. Team A wants better work partitioning and lower non-matmul overhead. Team B targets Hopper TMA/WGMMA plus an FP8 forward path. Team C wants CuTe-DSL kernels co-designed for Blackwell. Which mapping fits?

Correct answer: Team A: FlashAttention-2; Team B: FlashAttention-3; Team C: FlashAttention-4.

FlashAttention-2 improves GPU work partitioning, warp-level parallelism, and non-matmul overhead. FlashAttention-3 is the Hopper-focused redesign using features such as TMA, WGMMA, asynchronous overlap, and an FP8 forward path. FlashAttention-4 retunes the pipeline for Blackwell and implements it in CuTe-DSL. All retain the tiled online-softmax foundation.

10 questions remaining.

Next Step
Continue to Continuous Batching & Scheduling

FlashAttention makes one dense attention kernel cheaper in HBM traffic. Next, move up to the serving scheduler: how request slots, chunked prefills, and decode work share each forward pass while protecting TTFT and <span data-glossary="itl">inter-token latency</span>.

PreviousPrefix Caching and Prompt Caching
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

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

Online normalizer calculation for softmax.

Milakov, M. & Gimelshein, N. · 2018

https://arxiv.org/abs/1805.02867

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

Dao, T. · 2023 · ICLR 2024

https://arxiv.org/abs/2307.08691

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

FlashAttention-4: Algorithm and Kernel Pipelining Co-Design for Asymmetric Hardware Scaling

Ted Zadouri, Markus Hoehnerbach, Jay Shah, Timmy Liu, Vijay Thakkar, Tri Dao · 2026 · arXiv

https://arxiv.org/abs/2603.05451

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

Accelerating PyTorch 2 Transformers with torch.compile and scaled_dot_product_attention

PyTorch Contributors · 2026

https://docs.pytorch.org/tutorials/intermediate/scaled_dot_product_attention_tutorial.html

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.