Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 75

FlashAttention Inner Loop Implementation

Recommended resources

Introduction

In our previous lesson, you grasped the core theoretical innovations behind FlashAttention: tiling and online softmax. We established that by fusing matrix multiplications with a clever, single-pass softmax reformulation, it's possible to compute exact attention while avoiding the quadratic memory bottleneck of the standard algorithm. You now understand the what and the why.

This lesson marks the transition from theory to practice. We will take the conceptual algorithm and translate it into the structure of a CUDA C++ kernel. Our focus will be squarely on implementing the inner loop of a simplified FlashAttention kernel. This is the computational heart of the algorithm, where a block of the final output matrix is computed by iterating over the Key-Value sequence. You will see how the running statistics—the maximum, the denominator, and the output accumulator—are updated for each processed tile, bringing the online softmax algorithm to life in code.


The Anatomy of a Tiled Attention Kernel

Before we dive into the inner loop, let's establish the overall structure of our CUDA kernel. The FlashAttention algorithm is organized around two nested loops. As the diagram from the last lesson showed, the outer loop iterates over blocks of the query matrix , while the inner loop iterates over blocks of the key and value matrices.

This diagram shows the FlashAttention data flow. Our focus today is the "Outer Loop" in this diagram, which corresponds to the inner loop of the kernel code, processing blocks of K and V for a fixed block of Q.

For this lesson, we will focus on the code that executes for a single block of , which is loaded into fast on-chip memory (registers) at the beginning of a thread block's execution. The "inner loop" of our kernel will then iterate through the tiles of and along the sequence length dimension.

The following blog post provides an excellent, from-the-ground-up implementation of a FlashAttention kernel. We will use its initial, unoptimized version as a clear, pedagogical guide.

Writing Speed-of-Light Flash Attention for 5090 in CUDA C++

This article by "gau-nernst" provides a fantastic, low-level walkthrough of building a FlashAttention kernel. We will start with the first version, which prioritizes correctness and clarity over heavy optimization.

Please read the entire section titled Draft version. As you read, focus on understanding the overall structure of the attention_v1_kernel: Memory Setup: Note how shared memory (smem) and register memory (_rmem arrays) are declared for the different matrices (Q, K, V, P, O, S). Q in Registers: See how the tile of Q is loaded from global to shared, and then to register memory before the main loop begins. This tile of Q will be reused for every iteration of the kv_idx loop. Main Loop: Identify the for loop that iterates over kv_idx. This is the "inner loop" we are implementing. Observe its key stages: loading a K tile, the first MMA, online softmax (placeholder), loading a V tile, and the second MMA.


Implementing the Inner Loop

The code you just reviewed provides a complete skeleton. Now, let's detail the operations inside that main for (int kv_idx = ...) loop. For each iteration, we process one tile of and to update our running output tile .

1. Data Loading: From Global to On-Chip Memory

The first step inside the loop is to bring the current tiles of and from slow global HBM into fast on-chip memory. The process follows the standard pattern for high-performance kernels, which should be familiar from your work on GEMM optimization.

  1. Global to Shared Memory (cp.async): A tile of (and later, ) is copied from global memory to shared memory. The blog post uses the cp.async PTX instruction, which is an asynchronous copy operation. This allows the GPU to initiate a memory transfer and proceed with other work, hiding memory latency.
  2. Shared to Register Memory (ldmatrix): From shared memory, the data is loaded into registers just before it's needed for computation. The ldmatrix PTX instruction is specifically designed to load matrix fragments for use in Tensor Core operations.

The article you read previously explains this data movement flow in detail. You can refer back to the sections "Global to Shared memory data transfer" and "Shared memory to Register memory data transfer" in that resource if you need a refresher on how ldmatrix maps threads to matrix elements.

2. First MMA: Computing Scores ()

Once a tile of is in registers (K_rmem), we can perform the first matrix multiplication. We multiply our persistent tile of (held in Q_rmem) with the transpose of the current tile.

The attention_v1_kernel code uses a triple-nested loop to perform this mma.m16n8k16 operation, accumulating the results in the S_rmem register array. This array now holds a [WARP_Q, BLOCK_KV] tile of the attention scores, which we'll call . This corresponds to one block of the full matrix.

3. Online Softmax and Output Update

This is where the magic happens. Instead of writing to memory, we immediately process it to update our running statistics. Let's trace the logic, which you saw in the previous lesson's numerical example.

For each row of our S_rmem tile, we must:

  1. Find Local Max: Compute the maximum value in the current score tile, .
  2. Find New Global Max: Compare the local max with our running maximum from previous blocks, , and find the new true maximum: .
  3. Rescale Previous Results: If the maximum has changed (), we must rescale our previous running denominator sum and our previous output accumulator . The update rules are:
  4. Compute and : We compute the exponentiated values for the current score tile, normalized by the new maximum: . The sum of these values gives us the local denominator contribution, .
  5. Update Global Denominator: Add the local contribution to the (potentially rescaled) running sum: .
  6. Second MMA (): Now, we load the corresponding tile of into registers and immediately perform the second matrix multiplication. This gives us the output contribution for this block.
  7. Update Global Output: We add this new contribution to our (potentially rescaled) running output accumulator: .

After this sequence, m_new, d_new, and o_new become m_old, d_old, and o_old for the next iteration of the kv_idx loop.

To see how this abstract algorithm is implemented in a real, albeit simplified, kernel, let's examine a pedagogical CUDA C++ implementation.

FlashAttention Pedagogical Implementation in CUDA C++ - GitHub

This GitHub repository contains simplified, educational implementations of FlashAttention. We will focus on the first attempt, which implements the core logic without advanced optimizations.

Navigate to the file FlashAttentionFirstAttempt.cu. Once there, find the main kernel function flash_attention_kernel. Carefully study the main for loop that iterates over the key/value blocks. Within this loop, map the code to the online softmax steps we just discussed: Step 1-2: The first matrix multiplication (Q @ K^T) and finding the local block maximum (m_j_new). Step 3: The if (m_j_new > m_i) block performs the crucial rescaling of the previous running statistics (O_i and l_i). Step 4-5: The computation of P_j and the update to the running denominator l_i. Step 6-7: The second matrix multiplication (P_j @ V_j) and the final update to the output accumulator O_i. Pay close attention to the variables: m_i corresponds to m_{\text{old}}, l_i to d_{\text{old}}, and O_i to o_{\text{old}}.

By tracing the code in FlashAttentionFirstAttempt.cu, you can see the direct, line-by-line implementation of the online softmax algorithm we've been discussing. The code doesn't use low-level PTX and is structured for clarity, making it an ideal bridge from the mathematical formulation to a working kernel.


Conclusion

In this lesson, you have dissected the inner loop of a FlashAttention kernel and seen how its abstract principles map to concrete CUDA C++ code. You are no longer just looking at a block diagram; you are now reading the implementation.

Key Takeaways:

  • Kernel Structure: A FlashAttention kernel is typically structured with an outer loop over tiles and an inner loop over / tiles.
  • Data Flow: Within the inner loop, tiles of and are loaded from global memory into shared memory and then registers for computation.
  • Fused Computation: The core of the inner loop is a fused sequence of operations: QK^T matrix multiplication, online softmax update of running statistics (), and the PV matrix multiplication.
  • Online Update in Code: You have seen how if statements and rescaling factors in the code directly implement the update rules for the running maximum, denominator, and output accumulator.

You have now built a solid understanding of the most critical part of the FlashAttention algorithm's implementation. The next step is to put all the pieces together.

In the next lesson, we will expand our focus from the inner loop to the complete forward pass. You will learn how to initialize the loop, handle the final normalization after the loop concludes, and write the final output back to global memory. This will culminate in a simplified but complete FlashAttention kernel that you can verify against a standard implementation.