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
LearnProject Deep DivesDeep Dive - FlashAttention
🚀HardInference Optimization

Deep Dive - FlashAttention

Read the Dao-AILab FlashAttention repo: exact tiled attention, online softmax, causal 2.1 alignment, FA2/FA3/FA4 packages, and when PyTorch SDPA is the better call.

31 min read
Learning path
Step 182 of 196 in the full curriculum
Deep Dive - SkyRLDeep Dive - FlashInfer

Personalize this lesson

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

A large language model (LLM) can produce the wrong next-token attention result even when its kernel computes softmax perfectly. Two new query tokens attending to five cached keys expose one such bug: aligning a causal mask to the wrong corner hides valid keys. This deep dive connects that caller contract to the tiled computation inside FlashAttention.

The FlashAttention & Memory Efficiency lesson explained why attention can wait on GPU memory traffic. Here, follow the repository from Python interfaces to online softmax, backward recomputation, and tests. You'll distinguish a mathematically equivalent schedule from a compatible package, mask, and tensor layout.

Hold the equation steady before changing the implementation. Scaled dot-product attention is still softmax of scaled QKTQK^TQKT times VVV. FlashAttention changes the schedule and the bytes that cross HBM (high-bandwidth memory), not the definition.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135

What stays mathematically unchanged when FlashAttention is used?

Answer

The operator is still softmax of scaled QKTQK^TQKT multiplied by VVV. FlashAttention changes scheduling and memory movement, not the attention definition.

Project identity

The papers name the algorithms; the Dao-AILab repository tells you what a process can actually import, test, and run. Kernels, wrappers, tests, and install paths don't all ship as one wheel.[2]Reference 2FlashAttentionhttps://github.com/Dao-AILab/flash-attention

FieldCurrent project fact
OriginThe Stanford-centered FlashAttention paper names Tri Dao, Daniel Y. Fu, Stefano Ermon, Atri Rudra, and Christopher Ré.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135
StewardshipDao AI Lab maintains the public repository. Its author file names Tri Dao.[2]Reference 2FlashAttentionhttps://github.com/Dao-AILab/flash-attention[3]Reference 3FlashAttention Authorshttps://github.com/Dao-AILab/flash-attention/blob/c75d019dea9d910312974417bc28f190dfdda6d9/AUTHORS
Contributor modelMaintainer-led GitHub issues and pull requests. Framework teams add dispatch downstream.
Source licenseBSD-3-Clause for the pinned source snapshot.[4]Reference 4FlashAttention BSD 3-Clause Licensehttps://github.com/Dao-AILab/flash-attention/blob/c75d019dea9d910312974417bc28f190dfdda6d9/LICENSE
Commercial boundaryFlashAttention is a kernel implementation, not a hosted model service. A product that embeds it keeps its own support terms.
Asset boundaryThe repository doesn't license model weights or datasets. CUDA and ROCm dependencies keep their own notices.

Three install surfaces matter more than the paper titles. These are documentation checks as of September 2, 2026, not commands executed on a GPU here:[2]Reference 2FlashAttentionhttps://github.com/Dao-AILab/flash-attention

SurfaceHow you get itHardware target
FlashAttention-2pip install flash-attn --no-build-isolation, then from flash_attn import flash_attn_funcCUDA path: Ampere, Ada, or Hopper, CUDA 12.0+, PyTorch 2.2+; separate ROCm Composable Kernel or AIter Triton paths
FlashAttention-3Install the hopper/ package, then from flash_attn_3 import flash_attn_interfaceH100 / H800, CUDA 12.3+, still documented as beta
FlashAttention-4pip install flash-attn-4, then from flash_attn.cute import flash_attn_funcHopper and Blackwell, written in CuTe-DSL (NVIDIA's Python-embedded domain-specific language)

A paper name in a citation doesn't tell you which code a wheel will import. FA3 is a separate Hopper path, FA4 is a separate CuTe-DSL package, and FA2 remains the default flash-attn import.[2]Reference 2FlashAttentionhttps://github.com/Dao-AILab/flash-attention Before benchmarking, print the package and function that actually loaded; otherwise a result can wear the wrong generation's label.

The source walkthrough is pinned to commit ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820. A wheel release needn't match that commit. Read these files in order:[5]Reference 5FlashAttention Source Snapshot (ce088ab)https://github.com/Dao-AILab/flash-attention/tree/ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820

FileQuestion to answer
README.mdWhich package, toolchain, GPU, and operation are documented?
flash_attn/flash_attn_interface.pyWhat does the public FA2 wrapper accept, save for backward, and return?
tests/test_flash_attn.pyWhich shapes, masks, dtypes, and reference errors are tested?
hopper/setup.pyWhy is FA3 a separate install and import?
flash_attn/cute/interface.pyWhich FA4 architecture and head-dimension guards apply to this operation?

The table's hardware targets summarize the README, not an exhaustive runtime selector. In particular, the pinned FA4 source has architecture-specific guards beyond the README's Hopper/Blackwell summary, including selected value dimensions of 512. Don't copy a universal head_dim <= 256 gate from FA2 into a FA4 caller.

Exact attention without an n×nn \times nn×n workspace

For batch size 8, 32 heads, and 8,192 tokens, one 16-bit score tensor occupies 8×32×81922×2=328\times32\times8192^2\times2=328×32×81922×2=32 GiB. That's one intermediate, not the whole model. A materializing baseline can write scores, reread them for softmax, write probabilities, and reread them for the value multiply. Each round trip crosses HBM, the large GPU memory pool. On-chip static random-access memory (SRAM) is much smaller, so a tile that stays there can be revisited cheaply.

A query-owned forward loop, as used in FA2's work partitioning, keeps a query tile QiQ_iQi​ while walking key/value tiles (Kj,Vj)(K_j, V_j)(Kj​,Vj​). For each pair, it computes Sij=QiKjT/dS_{ij}=Q_i K_j^T / \sqrt{d}Sij​=Qi​KjT​/d​ on chip, updates a row-wise softmax state, then discards SijS_{ij}Sij​. The dot products are still dense, so arithmetic remains O(n2d)O(n^2 d)O(n2d) FLOPs. The live score workspace drops from n×nn \times nn×n to one tile.[6]Reference 6FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691

The original FA1 analysis gives O(n2d2/M)O(n^2 d^2 / M)O(n2d2/M) HBM accesses for d≤M≤ndd\le M\le ndd≤M≤nd, with MMM measured in scalar elements, versus Ω(nd+n2)\Omega(nd+n^2)Ω(nd+n2) for its materializing baseline. This is an IO bound under that memory model, not a byte count or a runtime prediction for every later kernel.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135

IntermediateShape for one headBaseline lifetimeFlashAttention lifetime
Query, key, valuen×dn \times dn×d eachRead from HBM as neededStreamed into SRAM tiles
Scoresn×nn \times nn×nWritten and reread in HBMOne tile, then discarded
Probabilitiesn×nn \times nn×nOften written and rereadNever materialized globally
Outputn×dn \times dn×dWritten to HBMWritten after the tile loop
Row max and normalizernnn eachUsed by softmaxLive tile state; save their LSE summary for backward

After one tile, each query row retains mim_imi​ (largest score so far), ℓi\ell_iℓi​ (sum of shifted exponentials), and a vector uiu_iui​ (the unnormalized, value-weighted numerator). Only at the end do we divide: Oi=ui/ℓiO_i=u_i/\ell_iOi​=ui​/ℓi​. For a nonempty row, LSE⁡i=mi+log⁡ℓi\operatorname{LSE}_i=m_i+\log\ell_iLSEi​=mi​+logℓi​ is the log-sum-exp summary. At the example's shape, FP32 LSE needs 8×32×8192×4=88\times32\times8192\times4=88×32×8192×4=8 MiB, not 32 GiB.

The dependency is easier to see as a loop:

Diagram showing retain Q tile initialize m, ℓ, u, read K/V tile score + mask, merge m, ℓ, u discard scores, and more K/V tiles?.
retain Q tile initialize m, ℓ, u, read K/V tile score + mask, merge m, ℓ, u discard scores, and more K/V tiles?.

The “yes” edge advances to a new key/value tile. The query tile and softmax state stay live. This conceptual forward loop omits parallel split reductions and hardware-specific pipelines.

Why can a fused attention kernel still be compute-heavy at long context?

Answer

It avoids storing the full score matrix, but it still evaluates the allowed query-key pairs. IO savings remove a large memory cost; they don't make the quadratic dot-product work disappear.

Online softmax is what makes the tiles exact

Ordinary softmax looks like it needs every score before it can form a denominator. Suppose tile two has a larger row maximum than tile one. Throwing away tile one's state would lose its contribution, but keeping it on the old scale would misweight it. Online softmax merges partial summaries. Let the retained state be (mold,ℓold,uold)(m_{\text{old}},\ell_{\text{old}},u_{\text{old}})(mold​,ℓold​,uold​) and the new tile's row scores be sss. Then:

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

ℓnew=emold−mnewℓold+∑kesk−mnew\ell_{\text{new}}=e^{m_{\text{old}}-m_{\text{new}}}\ell_{\text{old}}+\sum_k e^{s_k-m_{\text{new}}}ℓnew​=emold​−mnew​ℓold​+∑k​esk​−mnew​

unew=emold−mnewuold+∑kesk−mnewVj,ku_{\text{new}}=e^{m_{\text{old}}-m_{\text{new}}}u_{\text{old}}+\sum_k e^{s_k-m_{\text{new}}}V_{j,k}unew​=emold​−mnew​uold​+∑k​esk​−mnew​Vj,k​

When a later tile contains a larger score, the old accumulator is rescaled instead of thrown away. Milakov and Gimelshein describe this streaming normalizer, which FlashAttention uses inside its tiled kernel.[7]Reference 7Online normalizer calculation for softmax.https://arxiv.org/abs/1805.02867

Initialize m=−∞m=-\inftym=−∞, ℓ=0\ell=0ℓ=0, and u=0u=0u=0. Skip a tile with no allowed key before evaluating exponentials: −∞−(−∞)-\infty-(-\infty)−∞−(−∞) isn't a usable shift. If the entire row is masked, return zero by the kernel's documented convention; ordinary softmax on an all-−∞-\infty−∞ row is undefined. Exactness here means the same operator in real arithmetic, not bitwise equality after floating-point reordering.

Two tiles by hand

Use one query row and four keys split into two tiles. Tile one has scores [1,2][1,2][1,2] and values [[10,0],[0,10]][[10,0],[0,10]][[10,0],[0,10]]. Tile two has scores [3,0][3,0][3,0] and values [[5,5],[9,1]][[5,5],[9,1]][[5,5],[9,1]]. The common 1/d1/\sqrt{d}1/d​ scale is omitted so the state update stays readable. Before calculating, predict what a new maximum of 3 should do to the state built from tile one.

For tile one, m1=2m_1=2m1​=2 and ℓ1=e−1+1=1.3679\ell_1=e^{-1}+1=1.3679ℓ1​=e−1+1=1.3679. The numerator is u1=e−1[10,0]+[0,10]=[3.6788,10]u_1=e^{-1}[10,0]+[0,10]=[3.6788,10]u1​=e−1[10,0]+[0,10]=[3.6788,10]. Keep that numerator, rather than dividing it after every tile.

Tile two contains a larger score, so m2=3m_2=3m2​=3. Old state is rescaled by e2−3=0.3679e^{2-3}=0.3679e2−3=0.3679:

ℓ2=0.3679×1.3679+(1+e−3)=1.5530\ell_2=0.3679\times 1.3679+(1+e^{-3})=1.5530ℓ2​=0.3679×1.3679+(1+e−3)=1.5530

0.3679[3.6788,10]+[5,5]+e−3[9,1]=[6.8014,8.7286]0.3679[3.6788,10]+[5,5]+e^{-3}[9,1]=[6.8014,8.7286]0.3679[3.6788,10]+[5,5]+e−3[9,1]=[6.8014,8.7286]

The final result is O2=u2/ℓ2=[4.380,5.620]O_2=u_2/\ell_2=[4.380,5.620]O2​=u2​/ℓ2​=[4.380,5.620], rounded to three decimals. Ordinary softmax over all four scores matches. The tile boundary changed storage, not the answer: no 1×41\times41×4 score row had to survive between tiles.

Tile one retains m=2, normalizer 1.3679, and numerator [3.6788,10]. Tile two raises m to 3, so multiply only the old normalizer and numerator by exp(-1) before adding new contributions. The final output is [4.380,5.620], rounded.
Rescale the old $\ell$ and $u$, not $m$ or the normalized output. Update $m$ by taking a maximum, then divide the merged numerator by its normalizer once.

What error would appear if a later tile had a larger maximum but the old numerator wasn't rescaled?

Answer

The old contributions would be compared in the wrong exponential scale. Their relative probability would be too large, so the result wouldn't equal dense softmax. Rescaling by emold−mnewe^{m_{\text{old}}-m_{\text{new}}}emold​−mnew​ is the invariant that makes tile order exact.

This standard-library CPU reference accepts finite values and scores that are finite or -inf (masked). It rejects malformed shapes instead of silently truncating zip. Moving tile boundaries or adding a large constant to every score should preserve the result. It tests the recurrence, not a GPU kernel.

two-tile-online-softmax.py
1import math 2 3scores = [1.0, 2.0, 3.0, 0.0] 4values = [[10.0, 0.0], [0.0, 10.0], [5.0, 5.0], [9.0, 1.0]] 5 6def validate(scores, values): 7 if not scores or len(scores) != len(values) or not values[0]: 8 raise ValueError("scores and nonempty value rows must match") 9 dim = len(values[0]) 10 if any(len(row) != dim for row in values): 11 raise ValueError("ragged values") 12 if any(math.isnan(x) or x == math.inf for x in scores): 13 raise ValueError("scores must be finite or -inf") 14 if any(not math.isfinite(x) for row in values for x in row): 15 raise ValueError("values must be finite") 16 return dim 17 18def full(scores, values): 19 dim = validate(scores, values) 20 m = max(scores) 21 if m == -math.inf: 22 return [0.0] * dim 23 weights = [math.exp(x - m) for x in scores] 24 return [sum(w * v[d] for w, v in zip(weights, values)) / sum(weights) 25 for d in range(dim)] 26 27def tiled(scores, values, tile_size): 28 dim = validate(scores, values) 29 if type(tile_size) is not int or tile_size < 1: 30 raise ValueError("tile_size must be a positive integer") 31 m = float("-inf") 32 total = 0.0 33 numerator = [0.0] * dim 34 for start in range(0, len(scores), tile_size): 35 current_scores = scores[start:start + tile_size] 36 current_values = values[start:start + tile_size] 37 if max(current_scores) == -math.inf: 38 continue 39 new_m = max(m, max(current_scores)) 40 old_scale = 0.0 if m == float("-inf") else math.exp(m - new_m) 41 tile_weights = [math.exp(x - new_m) for x in current_scores] 42 total = old_scale * total + sum(tile_weights) 43 for d in range(dim): 44 numerator[d] = old_scale * numerator[d] + sum( 45 w * v[d] for w, v in zip(tile_weights, current_values) 46 ) 47 m = new_m 48 return [x / total for x in numerator] if total else [0.0] * dim 49 50dense = full(scores, values) 51split = tiled(scores, values, 2) 52print("dense:", [round(x, 6) for x in dense]) 53print("tiled:", [round(x, 6) for x in split]) 54assert max(abs(a - b) for a, b in zip(dense, split)) < 1e-12 55for size in [1, 2, 3, 4, 10]: 56 for shift in [-1000.0, 0.0, 1000.0]: 57 result = tiled([s + shift for s in scores], values, size) 58 assert max(abs(a - b) for a, b in zip(dense, result)) < 1e-12 59assert tiled([-math.inf] * 4, values, 2) == [0.0, 0.0] 60assert tiled([-math.inf, -math.inf, 0.0, -math.inf], values, 2) == values[2]
Output
1dense: [4.379542, 5.620458] 2tiled: [4.379542, 5.620458]

The public interface is several kernels

The FA2 flash_attn_func accepts tensors shaped (batch, sequence, heads, head_dim) for QQQ, and the same layout for KKK and VVV except sequence length and head count may differ from QQQ. PyTorch SDPA puts heads before sequence: (batch, heads, sequence, head_dim). A layout mismatch can run without a shape error when those dimensions happen to fit.[5]Reference 5FlashAttention Source Snapshot (ce088ab)https://github.com/Dao-AILab/flash-attention/tree/ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820[8]Reference 8torch.nn.functional.scaled_dot_product_attentionhttps://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html

Separate public and private returns. The public FA2 function normally returns only the output tensor. Its private CUDA forward helper returns output, FP32 row LSE shaped (batch, heads, seqlen_q), a probability/dropout buffer, and random-number-generator (RNG) state for autograd. Public return_attn_probs=True instead returns a three-tuple (out, lse, S_dmask) for testing. The source warns that this debug probability buffer may have incorrect scaling; requesting it can also allocate a quadratic-size buffer. Don't enable it in a memory benchmark.[5]Reference 5FlashAttention Source Snapshot (ce088ab)https://github.com/Dao-AILab/flash-attention/tree/ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820

Even identical function names don't imply identical returns: the pinned FA4 flash_attn.cute.flash_attn_func returns (out, lse), with lse potentially None when it isn't needed or requested. Check the installed signature and source before substituting it for FA2's tensor-returning call.

ROCm has two backends. Composable Kernel is the default, while FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE selects the AIter Triton path. The pinned wrapper attempts that path after a HIP extension import failure, with a warning. It still needs compatible AIter dependencies and hardware; the attempt isn't a guarantee of successful fallback.[5]Reference 5FlashAttention Source Snapshot (ce088ab)https://github.com/Dao-AILab/flash-attention/tree/ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820

FA2 CUDA documents 16-bit floating point (FP16) or brain floating point (BF16) and head dimensions up to 256 on its supported GPUs. Consumer-GPU backward at dimension 256 without dropout is documented from flash-attn 2.5.5. Those are FA2-specific qualifications, not universal limits for every generation.[2]Reference 2FlashAttentionhttps://github.com/Dao-AILab/flash-attention

Multi-query attention (MQA) uses one K/V head; grouped-query attention (GQA) uses several, but fewer than Q. FA2 maps consecutive groups of Q heads onto K/V heads. Six Q heads and two K/V heads map as 0,0,0,1,1,1, not alternating 0,1,0,1,0,1. This CPU check validates that caller-side contract. It doesn't predict which GPU kernel will run.[5]Reference 5FlashAttention Source Snapshot (ce088ab)https://github.com/Dao-AILab/flash-attention/tree/ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820

gqa-head-mapping.py
1def kv_head_for_queries(query_heads, key_heads, value_heads): 2 if any(type(h) is not int or h <= 0 3 for h in (query_heads, key_heads, value_heads)): 4 raise ValueError("head counts must be positive integers") 5 if key_heads != value_heads or query_heads % key_heads: 6 raise ValueError("K/V heads must match and divide Q heads") 7 group_size = query_heads // key_heads 8 return [q_head // group_size for q_head in range(query_heads)] 9 10print("GQA:", kv_head_for_queries(6, 2, 2)) 11print("MQA:", kv_head_for_queries(6, 1, 1)) 12assert kv_head_for_queries(3, 3, 3) == [0, 1, 2] 13for invalid in [(0, 1, 1), (5, 2, 2), (6, 2, 1), (True, 1, 1)]: 14 try: 15 kv_head_for_queries(*invalid) 16 except ValueError: 17 pass 18 else: 19 raise AssertionError(f"accepted invalid head counts: {invalid}")
Output
1GQA: [0, 0, 0, 1, 1, 1] 2MQA: [0, 0, 0, 0, 0, 0]

The equivalent reference repeats each K/V head consecutively. That checks kernel semantics for fixed Q/K/V. Whether a model trained with fewer K/V heads preserves task quality is a separate architectural evaluation.

Causal 2.1 is the decode gotcha

Autoregressive decoders can't read future tokens. A causal tile treats those scores as −∞-\infty−∞ before softmax. Before FlashAttention 2.1, causal=True aligned the triangle to the top-left; from 2.1 onward, it aligns to the bottom-right. This matches incremental decode when the query rows correspond to the final positions of the key sequence. It isn't the right alignment for every arbitrary short-query layout.[2]Reference 2FlashAttentionhttps://github.com/Dao-AILab/flash-attention

For seqlen_q = 2 and seqlen_k = 5, ask whether the first query sees one old key or four. Keep-cells look like this (1 means the key participates):

Two 2-by-5 causal masks for two query rows and five cached keys. FlashAttention 2.0 keeps only the top-left triangle, so query 0 sees key 0. FlashAttention 2.1 and later align to the bottom-right, so query 0 sees keys 0 through 3 and query 1 sees every cached key.
Here the two queries occupy positions 3 and 4 of the five-key sequence. FA 2.0's top-left causal mask lets q0 see only key 0. FA 2.1's bottom-right mask lets q0 see keys 0 through 3 and q1 see the full prefix.

If every cell in a query row is masked, the documented output for that row is zero. That shows up when seqlen_q > seqlen_k under bottom-right alignment: the first rows have no legal keys.

Equal query and key lengths hide the distinction: top-left and bottom-right masks are the same lower triangle. A short query over a long cache exposes it. PyTorch's scaled_dot_product_attention(..., is_causal=True) uses a lower-triangular mask on square inputs and documents an upper-left causal bias for non-square inputs. Matching FlashAttention 2.1 from SDPA means passing an explicit lower-right causal bias, not assuming is_causal=True did that for you.[8]Reference 8torch.nn.functional.scaled_dot_product_attentionhttps://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html

Before running the helper, predict the seqlen_q > seqlen_k case: early rows should have no legal keys and therefore be all zero. It prints both corners.

causal-align.py
1def causal_keep(seqlen_q: int, seqlen_k: int, align: str) -> list[list[int]]: 2 if any(type(n) is not int or n <= 0 for n in (seqlen_q, seqlen_k)): 3 raise ValueError("sequence lengths must be positive integers") 4 if align not in {"top-left", "bottom-right"}: 5 raise ValueError("unknown causal alignment") 6 rows = [] 7 for i in range(seqlen_q): 8 row = [] 9 for j in range(seqlen_k): 10 if align == "top-left": 11 keep = j <= i 12 else: 13 keep = j <= i + seqlen_k - seqlen_q 14 row.append(1 if keep else 0) 15 rows.append(row) 16 return rows 17 18print("FA 2.0 q=2,k=5:", causal_keep(2, 5, "top-left")) 19print("FA 2.1 q=2,k=5:", causal_keep(2, 5, "bottom-right")) 20print("FA 2.1 q=5,k=2:", causal_keep(5, 2, "bottom-right")) 21assert causal_keep(2, 5, "top-left") == [[1, 0, 0, 0, 0], [1, 1, 0, 0, 0]] 22assert causal_keep(2, 5, "bottom-right") == [[1, 1, 1, 1, 0], [1, 1, 1, 1, 1]] 23assert causal_keep(5, 2, "bottom-right") == [ 24 [0, 0], 25 [0, 0], 26 [0, 0], 27 [1, 0], 28 [1, 1], 29]
Output
1FA 2.0 q=2,k=5: [[1, 0, 0, 0, 0], [1, 1, 0, 0, 0]] 2FA 2.1 q=2,k=5: [[1, 1, 1, 1, 0], [1, 1, 1, 1, 1]] 3FA 2.1 q=5,k=2: [[0, 0], [0, 0], [0, 0], [1, 0], [1, 1]]

⚠️ Common mistake: Comparing a FlashAttention 2.1 decode call against an SDPA is_causal=True reference on unequal query and key lengths, then treating the mismatch as a kernel bug. Check the causal corner before you check numerics.

Local attention is related, but not identical. In FA2, finite window_size=(left, right) keeps key indices from i+S−L−lefti+S-L-\text{left}i+S−L−left through i+S−L+righti+S-L+\text{right}i+S−L+right, inclusive, clipped to valid keys, where LLL and SSS are query and key lengths. A side set to -1 is unbounded, not a negative radius. Causal masking can further restrict that set. A smaller window changes the operator and may affect model quality; fewer allowed scores don't guarantee lower latency for every tile shape.[5]Reference 5FlashAttention Source Snapshot (ce088ab)https://github.com/Dao-AILab/flash-attention/tree/ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820

Paged KV is a lookup, not a cache manager

Training batches can pack unequal lengths through flash_attn_varlen_func. Its int32 cumulative-length arrays (cu_seqlens_q, cu_seqlens_k) start at zero, are nondecreasing, and end at the corresponding packed token count. They have batch + 1 entries. An incorrect offset can leak attention across examples even if every dot product is right; maximum lengths must also agree with the sequences.[5]Reference 5FlashAttention Source Snapshot (ce088ab)https://github.com/Dao-AILab/flash-attention/tree/ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820

Decode has a different boundary. Serving engines store K/V in fixed-size physical pages and map each request through a block table. FA2's flash_attn_with_kvcache accepts that table and documents a page block size divisible by 256. This is that API's constraint, not a universal paged-cache rule. The call can update the cache in place and apply rotary embeddings; it doesn't support backward. The caller must reserve space before updates. Repeated cache_batch_idx entries during writes can produce ambiguous competing updates, so don't use them as a cache-sharing protocol.[5]Reference 5FlashAttention Source Snapshot (ce088ab)https://github.com/Dao-AILab/flash-attention/tree/ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820

FlashAttention handles the attention math and page lookup. A serving engine still owns allocation, eviction, prefix sharing, and request scheduling. Don't treat the kernel's paged path as a complete KV-cache manager. vLLM made that seam explicit, and FlashInfer widens it for serving-shaped work.

ModeWhat changesMain production question
CausalMask future keysIs the mask bottom-right for unequal QQQ and KKK lengths?
LocalKeep a bounded windowDoes the window preserve quality and tile occupancy?
VarlenPack unequal sequencesAre cumulative lengths and max lengths correct?
Paged KVIndirect K/V pagesWho owns page allocation and eviction?
MQA / GQAShare K/V across Q-head groupsDoes the head mapping match the model's trained architecture?

Backward recomputes tiles on purpose

Training needs gradients for QQQ, KKK, and VVV. Saving the full probability matrix would recreate the quadratic memory problem. FlashAttention saves QQQ, KKK, VVV, output OOO, row-wise LSE, and any required RNG state, then rebuilds score and probability tiles during backward.

For a nonempty row without dropout, let S=QKT/dS=QK^T/\sqrt dS=QKT/d​, P=softmax⁡(S)P=\operatorname{softmax}(S)P=softmax(S), and G=∂L/∂OG=\partial\mathcal L/\partial OG=∂L/∂O. Then dV=PTGdV=P^TGdV=PTG, dP=GVTdP=GV^TdP=GVT, and

dS=P⊙(dP−rowsum⁡(P⊙dP)),dQ=dSKd,dK=dSTQd.dS=P\odot\left(dP-\operatorname{rowsum}(P\odot dP)\right),\qquad dQ=\frac{dS K}{\sqrt d},\quad dK=\frac{dS^TQ}{\sqrt d}.dS=P⊙(dP−rowsum(P⊙dP)),dQ=d​dSK​,dK=d​dSTQ​.

The row sum broadcasts over keys. Since O=PVO=PVO=PV, its value is also rowsum⁡(G⊙O)\operatorname{rowsum}(G\odot O)rowsum(G⊙O), computable from saved output without retaining PPP. Rebuild a probability tile as exp⁡(Sij−LSE⁡i)\exp(S_{ij}-\operatorname{LSE}_i)exp(Sij​−LSEi​), apply the same mask, and accumulate gradients. Masked probabilities and their gradients are zero. Dropout requires reproducing the forward random mask, not drawing a new one.[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135[6]Reference 6FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691

Backward retains Q, K, V, output O, and row LSE. With upstream gradient G it rebuilds one masked score and probability tile, accumulates dQ, dK, and dV, then discards the probability tile. No full probability matrix is saved. Dropout additionally needs matching RNG state.
The probability tile is temporary in backward too. The saved output supplies the row reduction needed by the softmax derivative, while LSE supplies each row's normalization.

Recomputing costs extra FLOPs and HBM reads. It avoids storing and loading a full PPP, so long-context training can fit when a materializing kernel doesn't. Memory headroom isn't a free speedup: the extra work still has to be measured.

FlashAttention-2 partitions query tiles across thread blocks and reduces communication between warps.[6]Reference 6FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691 The FA2 deterministic flag applies to backward and can cost memory and time. “Forward is deterministic” in the README doesn't promise identical results across different kernels, devices, or dropout RNG states.[2]Reference 2FlashAttentionhttps://github.com/Dao-AILab/flash-attention

The test contract is numerical, not bitwise. Several pinned FA2 tests bound error by twice a lower-precision PyTorch reference's error against a higher-precision path; some assertions include an extra absolute allowance. Read the specific output and gradient assertions instead of adopting “2× error” as a universal tolerance.[5]Reference 5FlashAttention Source Snapshot (ce088ab)https://github.com/Dao-AILab/flash-attention/tree/ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820

This small example, checked on PyTorch 2.13.0 CPU, compares the derivative above with SDPA's math backend in float64. An explicit keep-mask handles both unequal-length directions, including fully masked rows. True means allowed in SDPA, is_causal=False avoids applying another causal rule, and dropout_p=0.0 removes randomness. The reference materializes scores only because these tensors are tiny. It does not execute or benchmark FlashAttention.[8]Reference 8torch.nn.functional.scaled_dot_product_attentionhttps://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html

attention-forward-backward-reference.py
1import math 2import torch 3import torch.nn.functional as F 4from torch.nn.attention import SDPBackend, sdpa_kernel 5 6torch.manual_seed(7) 7for query_len, key_len in [(2, 5), (5, 2), (4, 4)]: 8 q = torch.randn(1, 2, query_len, 3, dtype=torch.float64, requires_grad=True) 9 k = torch.randn(1, 2, key_len, 3, dtype=torch.float64, requires_grad=True) 10 v = torch.randn(1, 2, key_len, 4, dtype=torch.float64, requires_grad=True) 11 keep = (torch.arange(key_len)[None, :] <= 12 torch.arange(query_len)[:, None] + key_len - query_len) 13 with sdpa_kernel(SDPBackend.MATH): 14 out = F.scaled_dot_product_attention( 15 q, k, v, attn_mask=keep, is_causal=False, dropout_p=0.0) 16 upstream = torch.randn_like(out) 17 actual_grads = torch.autograd.grad(out, (q, k, v), upstream) 18 19 with torch.no_grad(): 20 scores = (q @ k.transpose(-2, -1) / math.sqrt(3)).masked_fill(~keep, -torch.inf) 21 has_key = keep.any(dim=-1, keepdim=True) 22 # Don't evaluate softmax(-inf, -inf, ...) on empty rows. 23 safe_scores = torch.where(has_key, scores, torch.zeros_like(scores)) 24 p = torch.softmax(safe_scores, dim=-1).masked_fill(~keep, 0.0) 25 expected = p @ v 26 dp = upstream @ v.transpose(-2, -1) 27 ds = p * (dp - (p * dp).sum(dim=-1, keepdim=True)) 28 expected_grads = ( 29 ds @ k / math.sqrt(3), 30 ds.transpose(-2, -1) @ q / math.sqrt(3), 31 p.transpose(-2, -1) @ upstream, 32 ) 33 torch.testing.assert_close(out, expected, atol=1e-12, rtol=1e-12) 34 for actual, reference in zip(actual_grads, expected_grads): 35 torch.testing.assert_close(actual, reference, atol=1e-12, rtol=1e-12) 36 assert torch.all(ds.masked_select(~keep) == 0) 37 assert torch.all(out[..., ~keep.any(dim=-1), :] == 0) 38 print(f"Q={query_len}, K={key_len}: output and Q/K/V gradients match")
Output
1Q=2, K=5: output and Q/K/V gradients match 2Q=5, K=2: output and Q/K/V gradients match 3Q=4, K=4: output and Q/K/V gradients match

Why does backward recomputation often make sense for training?

Answer

Saving a quadratic probability matrix can exceed activation memory at long context. Recomputing score tiles spends extra arithmetic and reads, but keeps only linear-size output and LSE state, often enabling a larger batch or sequence length.

FA1 through FA4 retune the same IO idea

Each release pairs the tiled operator with a hardware bottleneck. Paper numbers are snapshots from a GPU, dtype, sequence, and baseline. They shouldn't be copied into a service SLO without reproducing those conditions.

GenerationPrimary sourceHardware or software focusCore change
FA1FlashAttention, NeurIPS 2022[1]Reference 1FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.https://arxiv.org/abs/2205.14135A100-era CUDAIO-aware tiling and exact online softmax avoid materialized intermediates.
FA2FlashAttention-2, ICLR 2024[6]Reference 6FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691Ampere CUDAParallelize query tiles across thread blocks; reduce non-matmul overhead and inter-warp communication.
FA3FlashAttention-3, 2024[9]Reference 9FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision.https://arxiv.org/abs/2407.08608Hopper H100/H800Asynchrony, warp specialization, async copies overlapping warpgroup matmuls, and an FP8 forward path.
FA4FlashAttention-4, 2026[10]Reference 10FlashAttention-4: Algorithm and Kernel Pipelining Co-Design for Asymmetric Hardware Scalinghttps://arxiv.org/abs/2603.05451Blackwell, also HopperPipelines for asymmetric scaling: tensor cores got faster than shared memory and exponential units.

The FA4 paper's Figure 4 reports forward BF16 benchmarks at head dimension 128, sequence lengths 1k through 32k, and 32k total tokens per batch. It labels the GPU B200 and compares against cuDNN 9.13 and a Triton implementation, reporting up to 1.3× and 2.7× respectively, with a peak of 1,613 TFLOPs/s. These are published measurements, not results reproduced here. The same caption says a newer cuDNN version reaches similar performance, so the ratios aren't a current vendor ranking.[10]Reference 10FlashAttention-4: Algorithm and Kernel Pipelining Co-Design for Asymmetric Hardware Scalinghttps://arxiv.org/abs/2603.05451

There is also a reproducibility caveat in the reviewed paper: its main benchmark text names B200, while Appendix A.1 lists B100. Record that inconsistency rather than inventing one clean hardware manifest. A local comparison needs its own exact GPU, software versions, shape, mask, and baseline.

FA4 is implemented in CuTe-DSL embedded in Python. The paper also reports shorter single-kernel compile times than its C++ template baseline, but compile time and kernel runtime answer different questions.[10]Reference 10FlashAttention-4: Algorithm and Kernel Pipelining Co-Design for Asymmetric Hardware Scalinghttps://arxiv.org/abs/2603.05451

That implementation changes more than syntax. It includes software-emulated exponentials, conditional softmax rescaling, tensor memory, and backward matrix-multiply-accumulate (MMA) operations spanning two cooperative thread arrays (CTAs). These are hardware-specific scheduling techniques, not changes to which keys a query may attend.[10]Reference 10FlashAttention-4: Algorithm and Kernel Pipelining Co-Design for Asymmetric Hardware Scalinghttps://arxiv.org/abs/2603.05451

🔬 Research insight: FA4's claim isn't "Blackwell is twice as fast, so attention is twice as fast." Tensor-core throughput doubled from H100 to B200 in that paper's comparison, while shared-memory bandwidth and exponential units didn't. The kernel is redesigned around those slower units.

Call SDPA unless you need a repo feature

PyTorch scaled dot-product attention (SDPA) can select among FlashAttention, memory-efficient, and math implementations from the input and device.[8]Reference 8torch.nn.functional.scaled_dot_product_attentionhttps://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html A model library should call that API when it wants portability and let the dispatcher pick a valid backend. It should call the Dao-AILab package when it needs a generation, mask, or layout the framework path doesn't provide.

The fused kernels have input limitations. Forcing only FlashAttention through sdpa_kernel can leave no usable backend, producing diagnostics and a runtime error rather than an automatic math fallback. SDPA's backend name also doesn't identify a separately installed Dao-AILab wheel. Outputs can differ because floating-point operations aren't associative. The math backend supports float64 and keeps intermediates in FP32 for FP16/BF16 inputs.[8]Reference 8torch.nn.functional.scaled_dot_product_attentionhttps://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html

Set dropout_p=0.0 explicitly for evaluation: SDPA applies the passed probability even when the surrounding model is in evaluation mode. Match scaling, masks, head mapping, and dropout before comparing outputs.

The repository usage page lists integrations in PyTorch, Transformers, DeepSpeed, Megatron-LM, diffusion systems, and protein-structure models.[2]Reference 2FlashAttentionhttps://github.com/Dao-AILab/flash-attention Those examples cover two workloads:

  1. Training: cut activation memory and raise attention throughput so longer sequences or larger batches fit.
  2. Inference: accelerate prompt processing, and some decode attention, when the shape reaches an efficient regime. KV-cache layout and the scheduler still dominate many decode traces.

"Flash" isn't a guarantee that a tiny or unusual tensor wins. Measure against the backend production would otherwise choose, using the same shape, mask, dtype, and correctness tolerance.

Benchmark the path you will ship

A backend label isn't a measurement. Build a shape matrix from real traffic: long-query prefill, one-token decode over a long cache, unequal query and key lengths, MQA or GQA head counts, head dimensions, causal and local masks, and any variable-length packing. A kernel can win for one row and lose for another because tile occupancy and KV reads change.

Warm each implementation with identical inputs before timing. Discard compilation and first-allocation runs, then synchronize the GPU before and after the measured region, or use CUDA events. CUDA launches are asynchronous, so a CPU timer around an unsynchronized call can measure launch time instead of the kernel. Report a distribution such as median and p95, plus peak memory, rather than one favorable run.

Correctness is a gate, not a footnote. Compare the same outputs against a stable FP32 or math-backend reference with recorded absolute and relative tolerances. Exercise equal and unequal lengths, causal corners, local windows, varlen offsets, and paged indices before accepting a speed result. Raising tolerance until a mismatch disappears turns a fast wrong answer into a misleading benchmark.

When timing changes, profile a representative shape instead of guessing from the kernel name. Nsight Systems can expose CUDA API calls, kernel launches, memory operations, and stream gaps; Nsight Compute can separate memory-workload pressure from compute throughput with metrics and roofline views.[11]Reference 11Nsight Systems User Guidehttps://docs.nvidia.com/nsight-systems/UserGuide/[12]Reference 12Nsight Compute Profiling Guidehttps://docs.nvidia.com/nsight-compute/ProfilingGuide/ Use that evidence to decide whether HBM or shared-memory traffic, tensor-core work, launch overhead, occupancy, or register pressure is limiting the path.

Production checklist

Before enabling a FlashAttention backend, record the workload and the hardware:

  • Shape: batch, query length, key length, head count, K/V head count, and head dimension.
  • Numerics: FP16 or BF16 input, FP32 accumulation where supported, scale, dropout, and error versus a stable reference.
  • Mask: causal corner, local window, padding, varlen offsets, and page-table indexing.
  • Device: GPU family, CUDA or ROCm version, driver, PyTorch version, and which package actually imported.
  • Dispatch: which backend ran, whether a fallback occurred, and why.
  • Evidence: p50 and tail latency, memory peak, throughput, and correctness error on representative prompts.

If CUDA, PyTorch, GPU generation, or FlashAttention changes, rerun correctness and performance checks. Test an observable fallback within an explicit memory budget. A math fallback may materialize the quadratic intermediates you removed; at long context it can cause an out-of-memory failure. Reject or reroute shapes that lack a safe path rather than blindly retrying them.

⚠️ Common mistake: Treating "no quadratic allocation" as "constant-time attention." FlashAttention still computes dense query-key pairs, and long-context decode can remain dominated by KV-cache reads and scheduler effects.

Architectural summary

  • FlashAttention computes exact scaled dot-product attention while controlling HBM traffic. The live score workspace is a tile, not n×nn \times nn×n.
  • Online softmax rescales the old normalizer ℓ\ellℓ and numerator uuu when a later tile raises mmm. Divide only after merging.
  • FA2, FA3, and FA4 are different install surfaces. Import paths don't upgrade themselves.
  • FA2's causal mask is bottom-right from 2.1, matching queries at the end of a cached sequence. SDPA is_causal=True documents upper-left alignment for non-square inputs.
  • Paged KV in the kernel is a page-table lookup. Allocation and eviction stay with the serving engine.
  • Backward saves LSE and recomputes tiles. Check both output and gradient tolerances; bitwise equality isn't the contract.
  • Production confidence needs a recorded backend, representative measurements, and a memory-safe fallback or rejection path.

What a kernel integration review should defend

  • Derive the two-tile numerator and normalizer, including rescaling and an entirely masked row, without materializing the complete score row.
  • Identify the installed package and its public return contract; translate between FA2 and SDPA layouts and justify the causal corner and GQA head mapping.
  • Separate CPU reference agreement from GPU kernel correctness, and kernel timings from end-to-end service latency; include a memory budget for unsupported shapes.

Review questions

Why can a one-token decode test miss the causal-alignment bug?

If it tests only one key too, both corners produce the same mask. Use one query over several cached keys: bottom-right allows all keys, while upper-left allows only the first. Also test longer queries than keys to expose fully masked rows.

Can a faster FA4 paper result justify replacing FA2 in a service?

No. First match the installed API, return values, device support, shapes, masks, and numerical tolerances. Then benchmark the actual fallback baseline, including compile warmup and cache behavior. A paper's peak throughput neither validates that integration nor predicts service tail latency.

Mask, state, and package checks

Complete the lesson

Mastery Check

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

1.Which intermediate does FlashAttention avoid keeping as a global HBM-resident matrix while preserving exact attention?

Correct answer: The n by n score and probability matrices, which are recomputed per tile from retained Q, K, and V.

Q, K, V, outputs, and compact row state remain part of the computation. FlashAttention keeps score tiles on chip, applies online softmax, and writes the finished output without materializing full score or probability matrices.

2.A later score tile raises a row's running maximum from 2 to 3. What must the online-softmax recurrence do to the previous accumulator?

Correct answer: Multiply its normalizer and numerator by exp(2 - 3) before adding the new tile's contributions.

The old state was normalized using max 2, while the merged state uses max 3. Rescaling old terms by exp(m_old - m_new) puts both tiles on one stable exponential scale and keeps the tiled result equal to dense softmax.

3.For seqlen_q=2 and seqlen_k=5, which causal row is legal under FlashAttention 2.1's bottom-right alignment?

Correct answer: Query row 0 can attend keys 0 through 3, while query row 1 can attend all five keys.

Bottom-right alignment matches a short decode query to the end of a longer cached sequence. The first row sees four older keys and the newest row sees the full five-key prefix; the older top-left triangle would be wrong for this layout.

4.Which import path corresponds to the FA4 CuTe-DSL package described in the article?

Correct answer: from flash_attn.cute import flash_attn_func

FA4 is a separate flash-attn-4 package using the flash_attn.cute path. The default flash_attn import is FA2, flash_attn_3 is the Hopper FA3 package, and SDPA is PyTorch's dispatcher.

5.A serving engine passes a page table to flash_attn_with_kvcache. Which responsibility remains outside the attention kernel?

Correct answer: Managing physical KV pages: allocation, eviction, and scheduling.

The kernel consumes page indirection and performs attention math, but a serving engine owns request admission, page lifetime, eviction, and prefix sharing. A paged kernel isn't a complete cache manager.

6.Why can FlashAttention backward recompute score tiles instead of saving the full probability matrix?

Correct answer: Recompute score and probability tiles from saved Q, K, V, output, LSE, and RNG state instead of storing all probabilities.

Backward saves Q, K, V, output, row-wise LSE, and RNG state, then rebuilds tiles. The extra FLOPs and HBM reads are exchanged for linear-size saved state and long-context memory headroom.

7.A shape misses the forced FlashAttention backend in PyTorch SDPA. What production behavior best preserves correctness and diagnosis?

Correct answer: Use a memory-safe fallback or reject the shape, recording the backend and validating supported outputs.

A math fallback can allocate quadratic intermediates and run out of memory. Use a validated fallback only inside its memory budget; otherwise reject or reroute the shape. Backend and shape telemetry keep compatibility gaps visible.

7 questions remaining.

Next Step
Continue to Deep Dive - FlashInfer

FlashAttention's public kernel still describes attention as tiles over Q, K, and V. FlashInfer is the next layer: ragged batches, paged KV, and serving-shaped kernel plans where those tiles stop being rectangular.

PreviousDeep Dive - SkyRL
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

FlashAttention

Dao-AILab · 2026

https://github.com/Dao-AILab/flash-attention

FlashAttention Authors

Dao AI Lab · 2026

https://github.com/Dao-AILab/flash-attention/blob/c75d019dea9d910312974417bc28f190dfdda6d9/AUTHORS

FlashAttention BSD 3-Clause License

Dao AI Lab · 2026

https://github.com/Dao-AILab/flash-attention/blob/c75d019dea9d910312974417bc28f190dfdda6d9/LICENSE

FlashAttention Source Snapshot (ce088ab)

Dao AI Lab and Contributors · 2026

https://github.com/Dao-AILab/flash-attention/tree/ce088ab9ce0fc0434dcd8afa0a791da9fcc3a820

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

Dao, T. · 2023 · ICLR 2024

https://arxiv.org/abs/2307.08691

Online normalizer calculation for softmax.

Milakov, M. & Gimelshein, N. · 2018

https://arxiv.org/abs/1805.02867

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-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

Nsight Systems User Guide

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/nsight-systems/UserGuide/

Nsight Compute Profiling Guide

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/nsight-compute/ProfilingGuide/

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.