VerifiedarXiv:2307.0869131 min
Architecture · Systems

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

FlashAttention-2 computes the same attention as FlashAttention about twice as fast, by changing how the work is split across the GPU.

FlashAttention reached 25 to 40% of an A100's peak arithmetic rate. Three changes raise that to 50 to 73%: fewer operations outside matrix multiplies; a separate group of GPU threads (a thread block) for each chunk of query rows instead of one per attention head; and a split of each chunk of work among groups of 32 threads (warps) so they no longer pass partial results to each other through on-chip memory.

Explaining the paperFlashAttention-2: Faster Attention with Better Parallelism and Work PartitioningTri Dao · Princeton · Stanford · ICLR 2024 · arXiv:2307.08691 ↗

FlashAttention (2022) made attention fast by moving less data. It computes softmax(QK⊤QK^\top)VV in tiles small enough for a GPU's on-chip memory, so the N×NN \times N attention matrix never goes to the GPU's main memory, and it ran 2 to 4× faster than the attention implementations of the time. It still used the A100's arithmetic poorly: its forward pass ran at 30 to 50% of the GPU's peak FLOPs/s and its backward pass at 25 to 35%, (the paper's abstract summarizes these as 25 to 40%), while a well-tuned matrix multiply (GEMM) runs at 80 to 90%. The paper traces the gap to how the work was divided among the GPU's thread blocks and warps.

FlashAttention-2 is a single-author technical report by Tri Dao, published in July 2023 and at ICLR 2024. It keeps FlashAttention's math and its memory traffic analysis and changes how the computation is laid out on the hardware. The changes are:

  1. rewrite the streaming softmax so the kernel does fewer operations that are not matrix multiplies, because on an A100 each of those costs about 16 times as much as a matrix-multiply FLOP;
  2. put the loop over query blocks outside the loop over key blocks (FlashAttention had them the other way round), and give each block of query rows its own thread block (a group of threads that runs on one of the GPU's 108 processors), so a long sequence with a small batch still occupies all of them;
  3. inside a thread block, split each tile between warps (bundles of 32 threads) by query rows instead of by keys, so the warps never have to add up each other's partial results.

The output is still exactly softmax(QK⊤QK^\top)VV. In the paper's benchmarks FlashAttention-2 runs around 2× faster than FlashAttention, reaches 73% of the A100's peak in the forward pass, and trains a 2.7B-parameter GPT model end to end at 225 TFLOPs/s per GPU. The page starts with a short recap of FlashAttention, then explains the GPU cost model behind the first change, walks through the forward and backward algorithms and the two levels of work partitioning, and ends with the results.

What FlashAttention already did

One attention head takes queries QQ, keys KK and values VV, each an N×dN \times d matrix, where NN is the sequence length and dd the head dimension (64 or 128 in practice). The paper writes the computation as:

S=QK⊤∈RN×N,P=softmax(S)∈RN×N,O=PV∈RN×dS = QK^\top \in \mathbb{R}^{N\times N}, \qquad P = \mathrm{softmax}(S) \in \mathbb{R}^{N\times N}, \qquad O = PV \in \mathbb{R}^{N\times d}S=QK⊤∈RN×N,P=softmax(S)∈RN×N,O=PV∈RN×d\begin{aligned} S &= QK^\top \in \mathbb{R}^{N\times N}, \\ P &= \mathrm{softmax}(S) \in \mathbb{R}^{N\times N}, \\ O &= PV \in \mathbb{R}^{N\times d} \end{aligned}
(1)

The softmax runs along each row of SS, and row ii of OO is a weighted average of the value vectors with weights from row ii of PP. Like the paper, this page leaves out the 1/d1/\sqrt{d} scale on SS, the causal mask, and dropout until they matter. The Transformer explainer covers why attention is computed this way.

A GPU has two kinds of memory that matter here. HBM is the large main memory: 40 or 80 GB on an A100, at 1.5 to 2.0 TB/s. SRAM is on-chip memory inside each of the A100's 108 streaming multiprocessors (SMs), 192 KB per SM, roughly ten times faster. A standard implementation writes SS and PP to HBM and reads them back, which for N=8192N = 8192 is two 64M-entry matrices per head. FlashAttention instead loads a block of queries and a block of keys and values into SRAM, computes that tile of SS, folds it into the output, and discards it.

Tiling the softmax is hard: normalizing a row needs the maximum and the sum over the whole row, and a tile sees only part of it. The online softmax of Milakov and Gimelshein (2018) gets around this. Split one block of query rows into two key blocks, S(1)S^{(1)} and S(2)S^{(2)}, each Br×BcB_r \times B_c. After the first block, keep the row maximum m(1)m^{(1)}, the row sum ℓ(1)\ell^{(1)} of eS(1)−m(1)e^{S^{(1)} - m^{(1)}}, and a normalized partial output O(1)O^{(1)}. When the second block arrives, the maximum may grow, and every term computed under the old maximum is too large by a factor em(2)−m(1)e^{m^{(2)} - m^{(1)}}. FlashAttention corrects that and renormalizes:

m(2)=max⁡ ⁣(m(1),rowmax(S(2))),ℓ(2)=em(1)−m(2) ℓ(1)+rowsum(eS(2)−m(2))O(2)=diag(ℓ(2))−1(diag(ℓ(1)) em(1)−m(2) O(1)+eS(2)−m(2)V(2))\begin{aligned} m^{(2)} &= \max\!\big(m^{(1)}, \mathrm{rowmax}(S^{(2)})\big), \qquad \ell^{(2)} = e^{m^{(1)}-m^{(2)}}\,\ell^{(1)} + \mathrm{rowsum}\big(e^{S^{(2)}-m^{(2)}}\big) \\ O^{(2)} &= \mathrm{diag}(\ell^{(2)})^{-1}\Big(\mathrm{diag}(\ell^{(1)})\, e^{m^{(1)}-m^{(2)}}\, O^{(1)} + e^{S^{(2)}-m^{(2)}} V^{(2)}\Big) \end{aligned}m(2)=max⁡ ⁣(m(1),rowmax(S(2))),ℓ(2)=em(1)−m(2) ℓ(1)+rowsum(eS(2)−m(2))O(2)=diag(ℓ(2))−1(diag(ℓ(1))em(1)−m(2) O(1)+eS(2)−m(2)V(2))\begin{aligned} m^{(2)} &= \max\!\big(m^{(1)}, \mathrm{rowmax}(S^{(2)})\big), \\ \ell^{(2)} &= e^{m^{(1)}-m^{(2)}}\,\ell^{(1)} \\ &\quad + \mathrm{rowsum}\big(e^{S^{(2)}-m^{(2)}}\big) \\ O^{(2)} &= \mathrm{diag}(\ell^{(2)})^{-1}\Big(\mathrm{diag}(\ell^{(1)}) \\ &\quad e^{m^{(1)}-m^{(2)}}\, O^{(1)} + e^{S^{(2)}-m^{(2)}} V^{(2)}\Big) \end{aligned}
(2)

Here diag(ℓ)\mathrm{diag}(\ell) scales row ii by ℓi\ell_i, and em(1)−m(2)e^{m^{(1)}-m^{(2)}} is a per-row factor at most 1. Multiplying O(1)O^{(1)} by ℓ(1)\ell^{(1)} undoes its old normalization, the exponential moves it to the new maximum, and the division by ℓ(2)\ell^{(2)} normalizes the sum again. Repeated over any number of blocks, the update ends at exactly (1).

For the backward pass, FlashAttention saves only OO, mm and ℓ\ell and recomputes each tile of SS and PP from QQ and KK. That does about 13% more arithmetic and still finishes sooner, because the recomputation replaces reading an N×NN \times N matrix from HBM. Extra memory drops from O(N2)O(N^2) to O(N)O(N), a 10 to 20× saving. FlashAttention-2 keeps all of this. Its changes are about what happens once the data is on the chip.

Why non-matmul FLOPs cost 16× on a GPU

An A100 has two kinds of arithmetic units. Tensor Cores do matrix multiplies in FP16 or BF16 at up to 312 TFLOPs/s. Everything else (exponentials, maxima, sums, elementwise multiplies and divides) is limited to the 19.5 TFLOPs/s of non-matmul FP32 arithmetic. One non-matmul FLOP therefore takes as long as 16 matmul FLOPs. Kernel speed is quoted as a fraction of 312 TFLOPs/s; a kernel that spends a fifth of its time on non-matmul work, with nothing overlapping it, cannot exceed 80% of peak, however few FLOPs that work contains.

Count the work in one tile of the forward pass, per query row, for a key block of BcB_c keys. The two matrix multiplies, QiKj⊤Q_iK_j^\top and P~Vj\tilde P V_j, each cost 2Bcd2B_c d FLOPs per row (one multiply and one add per term), 4Bcd4B_c d in total. The softmax on the tile costs about 4Bc4B_c: a max, a subtraction, an exponential and an addition per score. Then the output row of length dd is rescaled by (2). In FlashAttention that is four operations per element: multiply the old output by ℓ(1)em(1)−m(2)\ell^{(1)}e^{m^{(1)}-m^{(2)}}, multiply the new product by its exponential factor (Algorithm 1 of FlashAttention shifts each tile by its own max), add, and divide by ℓ(2)\ell^{(2)}.

With Bc=128B_c = 128 and d=128d = 128, that is 65,536 matmul FLOPs, 512 softmax operations and 512 rescaling operations per row per key block. The non-matmul part is 1.5% of the FLOPs. Weighted by 16, it is 20% of the time if the two kinds of work run one after the other. Figure 1 shows how that split moves with the head dimension: the softmax cost is fixed per score while the matmul cost grows with dd, so small heads spend a larger share of their time outside the Tensor Cores.

Figure 1 · share of FLOPs against share of time
d = 128B_c
One tile of the forward pass, per query row. Drag the head dimension and switch the key block size. Each pair of bars compares the share of FLOPs with the share of time when non-matmul work runs 16× slower. The rescaling segment halves from FlashAttention to FlashAttention-2. The count treats every non-matmul operation as one FLOP and assumes no overlap; on real hardware an exponential costs more than an addition, and some work overlaps.

At d=64d = 64 and Bc=128B_c = 128, non-matmul work is 2.3% of FlashAttention's FLOPs and 27% of its time. FlashAttention-2 removes half of the rescaling work, which brings the time share to 24%. The exponentials themselves cannot be removed. By this page's count, halving the rescaling saves 4 to 10% of a tile's time across head dimensions 32 to 256, so most of the 2× has to come from the other two changes and from the new implementation.

How FlashAttention-2 works: the forward pass

The paper makes two changes to (2). The first: do not divide by ℓ\ell after every block. Keep an unscaled accumulator O~\tilde O, the numerator of the softmax average, and divide once at the end. The two-block update becomes:

O~(2)=diag(em(1)−m(2)) O~(1)+eS(2)−m(2) V(2),O=diag(ℓ(last))−1 O~(last)\tilde O^{(2)} = \mathrm{diag}\big(e^{m^{(1)}-m^{(2)}}\big)\,\tilde O^{(1)} + e^{S^{(2)}-m^{(2)}}\, V^{(2)}, \qquad O = \mathrm{diag}\big(\ell^{(\mathrm{last})}\big)^{-1}\, \tilde O^{(\mathrm{last})}O~(2)=diag(em(1)−m(2)) O~(1)+eS(2)−m(2) V(2),O=diag(ℓ(last))−1 O~(last)\begin{gathered} \tilde O^{(2)} = \mathrm{diag}\big(e^{m^{(1)}-m^{(2)}}\big)\,\tilde O^{(1)} + e^{S^{(2)}-m^{(2)}}\, V^{(2)}, \\ O = \mathrm{diag}\big(\ell^{(\mathrm{last})}\big)^{-1}\, \tilde O^{(\mathrm{last})} \end{gathered}
(3)

Here O~(1)\tilde O^{(1)} is eS(1)−m(1)V(1)e^{S^{(1)}-m^{(1)}}V^{(1)}, and mm and ℓ\ell are updated as in (2). Each block now costs two operations per output element (one multiply, one add) instead of four, and the divide happens once per row for the whole sequence. The factor em(1)−m(2)e^{m^{(1)}-m^{(2)}} multiplies; the paper prints it with an inverse in Algorithm 1 and in its two-block example, which the official kernel does not do (see the Provenance panel). Since the new maximum is never smaller than the old one, the factor is at most 1 and shrinks the old terms.

The second change: save one statistic per row for the backward pass, the logsumexp, instead of the two numbers mm and ℓ\ell:

L=m+log⁡ℓ,P=exp⁡(S−L)L = m + \log \ell, \qquad P = \exp(S - L)
(4)

This works because dividing by ℓi\ell_i is subtracting its logarithm in the exponent:

Pij=eSij−miℓi=eSij−mi−log⁡ℓi=eSij−LiP_{ij} = \frac{e^{S_{ij} - m_i}}{\ell_i} = e^{S_{ij} - m_i - \log \ell_i} = e^{S_{ij} - L_i}

One subtraction and one exponential per score recover the exact probabilities, with no division, from one stored number per row.

Figure 2 runs both versions on one query row with eight scores in four blocks of two keys and a single number as each value (d=1d = 1). Step through and compare the two accumulators. FlashAttention's OO is a valid weighted average after every block. FlashAttention-2's O~\tilde O becomes an average only at the last step; after block 4 it is 5.105, and dividing by ℓ=2.463\ell = 2.463 gives 2.073, the exact answer.

Figure 2 · normalize every block, or once
start
Press Play or drag the slider through four key blocks. The dashed teal line is the running maximum mm. When it jumps (blocks 2 and 4), both versions multiply their old output and sum by emold−mnewe^{m_{\text{old}}-m_{\text{new}}}. The output rows show FlashAttention renormalizing each time and FlashAttention-2 dividing only at the end; the operation counts are for one output element.

The row ends with mm = 4.0 and ℓ\ell = 2.463, so the logsumexp saved for the backward pass and the probability it gives for the last key (score 4.0), which matches a full softmax over the eight scores, are:

L=4.0+log⁡2.463=4.901,P8=e4.0−4.901=0.406L = 4.0 + \log 2.463 = 4.901, \qquad P_8 = e^{4.0 - 4.901} = 0.406L=4.0+log⁡2.463=4.901,P8=e4.0−4.901=0.406\begin{gathered} L = 4.0 + \log 2.463 = 4.901, \\ P_8 = e^{4.0 - 4.901} = 0.406 \end{gathered}

The full forward pass also swaps FlashAttention's loops. FlashAttention's Algorithm 1 had the outer loop over key and value blocks and the inner loop over query blocks, so every output block was read from HBM, updated and written back once per key block. FlashAttention-2 puts the query blocks outside. A thread block loads QiQ_i once, keeps O~i\tilde O_i, mim_i and ℓi\ell_i on chip while it walks across all of KK and VV, and writes OiO_i and LiL_i once. No iteration of the outer loop needs anything from another (row block 5 never reads row block 4), which the parallelism change in a later section relies on. In pseudocode:

# FlashAttention-2 forward (Algorithm 1), one head; scale, mask, dropout omitted
for i in range(T_r):                      # OUTER: one thread block per row block
    load Q_i                              # B_r x d, into SRAM
    O = zeros(B_r, d)                     # all three live on chip for the whole row
    l = zeros(B_r)
    m = full(B_r, -inf)
    for j in range(T_c):                  # INNER loop: walk across K, V
        load K_j, V_j                     # B_c x d each
        S = Q_i @ K_j.T                   # B_r x B_c, never leaves the chip
        m_new = maximum(m, rowmax(S))
        P = exp(S - m_new[:, None])       # unnormalized probabilities
        a = exp(m - m_new)                # a <= 1, shrinks the old terms
        l = a * l + rowsum(P)
        O = a[:, None] * O + P @ V_j      # multiply and add; no divide
        m = m_new
    O = O / l[:, None]                    # the only divide, once per row
    write O to HBM as O_i                 # once per row block
    write L_i = m + log(l) to HBM         # one number per query row

With N=16384N = 16384, d=128d = 128 and the A100 block sizes from the code (Br=128B_r = 128, Bc=64B_c = 64), one head has Tr=128T_r = 128 row blocks and Tc=256T_c = 256 key blocks. Each tile is 4,194,304 matmul FLOPs and 8,192 exponentials. Each row block streams 256 tiles and writes back 128 × 128 output values and 128 logsumexps. The forward pass uses O(N2d)O(N^2 d) FLOPs and O(N)O(N) memory beyond the inputs and output, the same as FlashAttention.

Skipping masked blocks

In autoregressive language modeling, token ii may attend only to tokens j≤ij \le i. A causal mask sets Sij=−∞S_{ij} = -\infty for j>ij > i, so those probabilities are zero. Because the kernel already works in blocks, it can skip a whole tile when every key in it comes after every query in it. For a long sequence that is about half the tiles, and the paper measures a 1.7 to 1.8× speedup over the same computation without the mask.

The tiles strictly below the diagonal need no mask at all, since every key precedes every query. With square blocks, only the diagonal tile of each row needs the elementwise mask. Of the 36 tiles that survive in an 8 × 8 grid, 8 are masked and 28 are computed as if unmasked. The code walks each row from its last key block backward, so the masked diagonal tile is handled first and the remaining steps run without masking logic. The next section returns to the masked grid, where it makes rows uneven in length.

The backward pass

Given the gradient of the loss with respect to the output, dO∈RN×ddO \in \mathbb{R}^{N \times d}, the chain rule through (1) gives:

dV=P⊤dO,dP=dO V⊤,dS=dsoftmax(dP),dQ=dS K,dK=dS⊤QdV = P^\top dO, \quad dP = dO\,V^\top, \quad dS = \mathrm{dsoftmax}(dP), \quad dQ = dS\,K, \quad dK = dS^\top QdV=P⊤dO,dP=dO V⊤,dS=dsoftmax(dP),dQ=dS K,dK=dS⊤Q\begin{gathered} dV = P^\top dO, \qquad dP = dO\,V^\top, \\ dS = \mathrm{dsoftmax}(dP), \\ dQ = dS\,K, \qquad dK = dS^\top Q \end{gathered}
(5)

The softmax gradient for one row pp of PP is (diag(p)−pp⊤) dp(\mathrm{diag}(p) - pp^\top)\,dp. Written out per element:

dSij=Pij (dPij−Di),Di=∑jPij dPijdS_{ij} = P_{ij}\,(dP_{ij} - D_i), \qquad D_i = \sum_j P_{ij}\,dP_{ij}dSij=Pij (dPij−Di),Di=∑jPij dPij\begin{gathered} dS_{ij} = P_{ij}\,(dP_{ij} - D_i), \\ D_i = \sum_j P_{ij}\,dP_{ij} \end{gathered}

The sum in DiD_i runs over all NN keys, which a tile cannot see. Since dPijdP_{ij} is the dot product dOi⋅VjdO_i \cdot V_j, it can be rewritten:

Di=∑jPij (dOi⋅Vj)=dOi⋅∑jPijVj=dOi⋅OiD_i = \sum_j P_{ij}\,(dO_i \cdot V_j) = dO_i \cdot \sum_j P_{ij} V_j = dO_i \cdot O_iDi=∑jPij (dOi⋅Vj)=dOi⋅∑jPijVj=dOi⋅Oi\begin{aligned} D_i &= \sum_j P_{ij}\,(dO_i \cdot V_j) \\ &= dO_i \cdot \sum_j P_{ij} V_j = dO_i \cdot O_i \end{aligned}
(6)

So DD is a row-wise dot product of two N×dN \times d matrices that are already in HBM, one number per query row, computed before the main loop. This trick comes from FlashAttention; the backward pass of FlashAttention-2 is almost the same algorithm, with the probabilities rebuilt from LL by (4):

# FlashAttention-2 backward (Algorithm 2), one head; scale, mask, dropout omitted
D = rowsum(dO * O)                        # length N: one number per query row
for j in range(T_c):                      # OUTER: one thread block per column
    load K_j, V_j
    dK_j = zeros(B_c, d); dV_j = zeros(B_c, d)   # accumulated on chip
    for i in range(T_r):
        load Q_i, O_i, dO_i, L_i, D_i
        S = Q_i @ K_j.T                   # matmul 1: recomputed scores
        P = exp(S - L_i[:, None])         # exact probabilities from L alone
        dV_j += P.T @ dO_i                # matmul 2
        dP = dO_i @ V_j.T                 # matmul 3
        dS = P * (dP - D_i[:, None])
        atomic_add(dQ_i, dS @ K_j)        # matmul 4: other columns add here too
        dK_j += dS.T @ Q_i                # matmul 5
    write dK_j, dV_j to HBM               # once per column block

The loops run the other way around from the forward pass. The outer loop is over key and value blocks, so each thread block owns one dKjdK_j and one dVjdV_j, accumulates them on chip down its column, and writes them once. The cost of that choice falls on dQdQ: row block ii of dQdQ receives a contribution from every column block. Different thread blocks add into the same dQidQ_i in HBM. A plain read, add and write from two thread blocks at once could lose one of the two updates, so they use atomic adds, which the hardware performs as one indivisible step (in fp32, in the official code).

The backward pass does five matrix multiplies per tile against two in the forward pass, which is why the benchmark counts its FLOPs as 2.5 times the forward count. It also keeps more blocks in SRAM at once, and at best it reaches 63% of peak against the forward pass's 73%.

Parallelism over the sequence length

A CUDA kernel runs as a grid of thread blocks. The GPU schedules each thread block onto one SM, where it runs to completion; a thread block is made of warps of 32 threads. Threads in a warp exchange values through fast register shuffles, and warps in the same thread block exchange values through shared memory, which is the SM's SRAM. Occupancy is the fraction of the GPU's resources a kernel keeps busy; a kernel that launches fewer thread blocks than the GPU has SMs leaves the rest of the SMs idle. A kernel that launches more runs them in waves: the first 108 start, and each SM takes the next waiting thread block when its current one finishes.

FlashAttention, as the paper describes it, uses one thread block per attention head per sequence: batch size × number of heads thread blocks in total. That fills an A100 when the product is large (the paper says about 80 or more). Long sequences break it, because with a fixed budget of tokens per batch a longer sequence means a smaller batch. In the paper's benchmark setting (16k tokens per batch, hidden size 2048, d=128d = 128, so 16 heads), a 16k-token sequence comes as a batch of one and FlashAttention launches 16 thread blocks for 108 SMs.

FlashAttention-2 makes each iteration of the outer loop its own thread block. The forward pass launches ⌈N/Br⌉\lceil N/B_r \rceil × batch × heads thread blocks, which the code builds as a three-dimensional grid with the row-block index first. The 16k-token example becomes 128 × 1 × 16 = 2,048 thread blocks, and since the row blocks of a head never communicate, nothing else in the algorithm changes. In this setting the count is 2,048 at every sequence length, because 16k tokens always make 128 row blocks per head.

Figure 3 · thread blocks on 108 SMs
N = 16k
Drag the sequence length; the batch shrinks to keep 16k tokens. Lit cells are SMs busy in the last (or only) wave of thread blocks. FlashAttention launches one thread block per head and leaves SMs idle once the batch is small; FlashAttention-2 launches one per 128 query rows. The model assumes one resident thread block per SM and measures time in tiles per thread block × waves; it shows the paper's occupancy argument, not measured speed.

At N=512N = 512 the batch is 32, and FlashAttention's 512 thread blocks keep 95% of the SMs busy on average. At N=16384N = 16384, FlashAttention's 16 thread blocks occupy 15% of the GPU, and in this model FlashAttention-2 finishes 6.7× sooner. The measured gap is much smaller, for a reason the model leaves out: the released FlashAttention code had already added a heuristic that splits each head across several thread blocks to fill the SMs (the num_splits parameter in v1.0.9), and the measured FlashAttention baseline reaches 110 TFLOPs/s at 16k tokens with d=64d = 64 (32 heads), more than the ≈92 TFLOPs/s (32 × 312/108) that one thread block per head on 32 of the 108 SMs could deliver. The paper's description of FlashAttention as one block per head is the original algorithm, not the baseline in its plots. In FlashAttention-2 the query loop is outside, so the row blocks are independent by construction and each writes its own rows of the output.

The loop swap and the sequence-length parallelism were first implemented by Phil Tillet in the Triton version of FlashAttention, which the paper credits. The backward pass is parallelized the same way, with one thread block per column block and the atomic adds into dQdQ described above.

Figure 4 shows both partitions on an 8 × 8 grid of tiles. In the forward pass each color is a thread block that owns a row, so the output column on the right fills one row at a time as each block finishes. In the backward pass each color owns a column, and every tile it finishes adds into the dQdQ entry of that row. Turn on the causal mask and watch the row lengths: row block 1 has one tile and row block 8 has eight, so with causal attention the forward thread blocks finish at different times.

Figure 4 · which thread block computes which tile
step 8 / 8
Switch between the forward and backward pass and toggle the causal mask, then press Play or drag the slider. Grey hatched tiles are skipped. The darkened corner on a diagonal tile marks the one tile per row that needs the elementwise mask. In the backward pass the counts in the dQ column are atomic adds from different thread blocks.

With the mask off, the forward pass computes 64 tiles and writes 8 output blocks once each; the backward pass computes the same 64 tiles and performs 64 atomic adds into dQdQ, 8 per row. With the mask on, 28 tiles are skipped in both passes and the backward pass does 36 atomic adds.

Splitting the work between warps

Inside a thread block, 4 or 8 warps share each tile. FlashAttention split the tile by keys: each of 4 warps took a quarter of KjK_j and VjV_j, while all of them read QiQ_i. The paper calls this the "split-K" scheme, after the GEMM technique of splitting the dimension being summed over, which in the product P~Vj\tilde P V_j is the key dimension. Walk it through for Br=Bc=128B_r = B_c = 128 and d=64d = 64: warp 1 computes SS for all 128 query rows against keys 1 to 32, a 128×32128 \times 32 slice. It multiplies its slice of P~\tilde P by rows 1 to 32 of VjV_j and gets a 128×64128 \times 64 result, but that result covers only 32 of the 128 keys. The full output is the sum of the four warps' results, so each warp writes its 8,192 values to shared memory, all four synchronize, and the sum is formed: 32,768 values written and read back per key block. The row maximum and row sum of the softmax are split across the four warps the same way.

FlashAttention-2 splits by query rows instead. Each warp takes 32 of the 128 rows of QiQ_i, and all warps read all of KjK_j and VjV_j. A warp then owns complete rows of SS, complete rows of the softmax statistics, and complete rows of the output, so no warp needs another warp's result. In the code this is a single layout choice: the matrix-multiply tile stacks its warps along the query dimension.

Figure 5 · split-K against split-Q inside one thread block
Toggle between the two partitions of one 128 × 128 tile among four warps (one color per warp, grey for data every warp reads). Under split-K each warp holds a full-size partial output that must go through shared memory; under split-Q each warp owns its own rows end to end.

The backward pass also avoids split-K, but its dependencies among QQ, KK, VV, OO, dOdO, dQdQ, dKdK and dVdV still require some synchronization between warps. The paper reports a speedup from the reduced shared-memory traffic in both passes but does not measure it separately.

Block sizes

Larger blocks mean fewer loads and stores through shared memory per FLOP, but each thread holds more of the tile in registers and the thread block needs more shared memory. Past some size the registers spill to slower memory, or the shared memory needed exceeds what the SM has and the kernel cannot launch. The paper uses blocks of {64,128}×{64,128}\{64, 128\} \times \{64, 128\}, tuned by hand per head dimension and GPU, and leaves auto-tuning to future work. The current code uses 128×128128 \times 128 for d=64d = 64 and 128×64128 \times 64 for d=128d = 128 on an A100, with 4 warps; a comment beside the d=64d = 64 choice records that 256×64256 \times 64 runs 85% slower at 2k tokens because of register spilling.

Results

The benchmarks use one A100 80GB SXM4, sequence lengths from 512 to 16k with the batch set so each run holds 16k tokens, hidden size 2048, and head dimension 64 or 128. Speed is reported in TFLOPs/s, with the forward FLOPs counted as:

4⋅N2⋅d⋅heads4 \cdot N^2 \cdot d \cdot \text{heads}
(7)

per sequence: two matrix multiplies of 2N2d2N^2d FLOPs each. With the causal mask the count is halved, and the backward pass is counted as 2.5 times the forward. Since every implementation in a panel is assigned the same count, a ratio of TFLOPs/s is a ratio of runtimes. The baselines are a standard PyTorch implementation, FlashAttention, the FlashAttention implementation in xformers ("cutlass"), and FlashAttention in Triton.

Figure 6 · attention speed on an A100
N = 8k
The paper's measurements, transcribed from its Figures 4 to 6. Choose the pass, mask and head dimension; hover or tap a sequence length (or use the slider) to read every value. The teal row under the axis is FlashAttention-2's speed divided by FlashAttention's. OOM: PyTorch ran out of memory at 16k.

The highest forward number is 227 TFLOPs/s (no mask, d=128d = 128, 2k tokens), 73% of the 312 peak; the paper rounds it to 230. The highest backward number is 196 TFLOPs/s, 63%. Over FlashAttention, the forward-plus-backward ratio ranges from 1.45× (no mask, d=64d = 64, 512 tokens) to 2.46× (no mask, d=128d = 128, 2k tokens), and the forward pass alone reaches 3.39× (224 against 66 TFLOPs/s at 1k). The paper summarizes this as "around 2×" and as 1.7 to 3.0×. The forward ratio is largest at d=128d = 128: at 16k tokens, going from d=64d = 64 to d=128d = 128 slows FlashAttention's forward pass from 104 TFLOPs/s to 73 and speeds FlashAttention-2's from 192 to 223.

Against PyTorch, the forward-plus-backward speedup is 2.3 to 9.2×, close to the paper's "3 to 10×". In the causal forward pass PyTorch runs at 10 TFLOPs/s and FlashAttention-2 at 181, an 18× gap at 8k tokens. PyTorch computes the full matrix and then masks it, so it does twice the work it is credited with.

End to end, the paper trains GPT-style models of 1.3B and 2.7B parameters on 8 A100s (Table 1, TFLOPs/s per GPU):

ModelWithout FlashAttentionFlashAttentionFlashAttention-2
GPT3-1.3B, 2k context142189196
GPT3-1.3B, 8k context72170220
GPT3-2.7B, 2k context149189205
GPT3-2.7B, 8k context80175225

At 2k context FlashAttention-2 is only 1.04× (1.3B) and 1.08× (2.7B) faster than FlashAttention, because attention is a small part of each layer and the matrix multiplies of the MLP and projections dominate. At 8k it is 1.29× faster in both models. Against training without FlashAttention, the speedup at 8k is 3.1× for the 1.3B model and 2.8× for the 2.7B model; the paper quotes the 2.8×.

The model FLOPs come from the formula used by Megatron-LM, per sequence of length ss:

6⋅s⋅params+12⋅layers⋅hidden⋅s26 \cdot s \cdot \text{params} + 12 \cdot \text{layers} \cdot \text{hidden} \cdot s^2
(8)

The first term is the forward and backward matrix multiplies against the weights, the second is attention. The paper notes that the second term is not halved for the causal mask, as the attention benchmark's count is, and keeps it unhalved for consistency with the literature. That affects how to read 225 TFLOPs/s as 72% of peak. Take the GPT-3 paper's shape for the 2.7B model, 32 layers of width 2560, at s=8192s = 8192: per token the weight term is about 1.62×10101.62 \times 10^{10} FLOPs and the attention term 8.05×1098.05 \times 10^9. Halving the attention term lowers the total by 17%, so the 225 TFLOPs/s of counted work corresponds to about 188 TFLOPs/s of work actually performed, 60% of peak.

Run unchanged on an H100, without its Tensor Memory Accelerator (TMA, a hardware unit for asynchronous copies between HBM and shared memory) or fourth-generation Tensor Cores, the same kernel reaches up to 335 TFLOPs/s. The paper expects another 1.5 to 2× from using those features.

The kernel ships as the flash-attn Python package for NVIDIA Ampere, Ada and Hopper GPUs, in fp16 or bf16, with head dimensions up to 256. Inputs are laid out as (batch, sequence, heads, head dimension). Passing fewer key and value heads than query heads runs grouped-query attention without copying KK and VV: the kernel maps each query head to its shared key-value head by index, and the backward pass sums the gradients of the query heads that share one, the variant covered in the Multi-Query Attention explainer.

# pip install flash-attn --no-build-isolation   (Ampere/Ada/Hopper; fp16 or bf16)
import torch
from flash_attn import flash_attn_func

q = torch.randn(1, 16384, 16, 128, device="cuda", dtype=torch.bfloat16)
k = torch.randn(1, 16384, 4, 128, device="cuda", dtype=torch.bfloat16)  # 4 KV heads
v = torch.randn_like(k)
out = flash_attn_func(q, k, v, causal=True)   # shape (1, 16384, 16, 128)
# softmax_scale defaults to 1/sqrt(d); q's head count must be a multiple of k's

Limits and what came next

The paper is a systems report. It has no ablation: the 2× over FlashAttention is measured for the whole package, a new implementation built on NVIDIA's CUTLASS 3.x library, and the three changes are never measured one at a time. By the count in Figure 1 the non-matmul change is worth a few percent, and the occupancy argument applies most clearly to the original FlashAttention algorithm rather than the released baseline.

The numbers are for the A100. Block sizes are tuned by hand for each head dimension, and the kernel uses none of the H100's new features. The backward pass, at 63% of peak at best, remains the slower half. The authors list their next steps as H100 features (TMA, fourth-generation Tensor Cores, FP8), other hardware such as AMD GPUs, combining the kernel with sparse or local attention patterns, and compilers that can produce such kernels from a high-level description.

FlashAttention-3 (Shah, Bikshandi, Zhang, Thakkar, Ramani and Dao, 2024) did the H100 part. It overlaps the softmax of one tile with the matrix multiplies of another, using warp specialization and the H100's asynchronous Tensor Cores and TMA, and reaches up to 740 TFLOPs/s in FP16 (75% of the H100's peak) and close to 1.2 PFLOPs/s in FP8.

Provenance Verified against primary literatureHow we verify
Rescaling in the kernelcsrc/flash_attn/src/softmax.h lines 157–160 (Dao-AILab/flash-attention): scores_scale = exp2f((scores_max_prev − scores_max_cur) · softmax_scale_log2), then row_sum and every acc_o entry are multiplied by it. The factor is e^(m_old − m_new) ≤ 1, multiplied, not divided.
Logsumexp and the final dividesoftmax.h lines 170–183: lse = row_max · scale + log(row_sum), and acc_o is multiplied by 1/row_sum once, after the loop.
One thread block per row blockcsrc/flash_attn/src/flash_fwd_launch_template.h lines 63–64: grid(num_m_block, batch, heads), num_m_block = ⌈seqlen_q / kBlockM⌉. Backward: flash_bwd_launch_template.h lines 75–82, grid_n(num_n_block, batch, heads).
Warps split along the query rowscsrc/flash_attn/src/kernel_traits.h lines 74–76: the TiledMMA stacks its kNWarps warps along the M (query) dimension, Layout<Shape<Int<kNWarps>,_1,_1>>. That is the split-Q partition.
dQ by atomic addscsrc/flash_attn/src/flash_bwd_kernel.h line 678: atomicAdd into an fp32 dQ accumulator. A deterministic mode gives each thread block its own buffer (line 124).
Block sizes in the codeflash_fwd_launch_template.h lines 203–262 (current main): d = 64 uses 128 × 128 (128 × 64 with dropout); d = 128 on A100 uses 128 × 64; a comment records 256 × 64 as 85% slower at 2k tokens because of register spilling.
FlashAttention v1 already split headsFlashAttention v1.0.9, csrc/flash_attn/src/fmha_fwd_launch_template.h lines 21–35 and 71–86: num_splits_heuristic_fwd chooses how many thread blocks share one (sequence, head) so the SMs fill, and the grid is (batch, heads, num_splits).
A100 peak ratesNVIDIA A100 datasheet: 312 TFLOPS FP16/BF16 on Tensor Cores (dense; 624 with sparsity), 19.5 TFLOPS FP32. The ratio is 16.
Benchmark valuesTranscribed from the bar labels of the paper’s Figures 4, 5 and 6 (A100 80GB SXM4). Two labels hidden by the legend were read from the PDF text layer.
Online softmax (2018)Milakov & Gimelshein, arXiv:1805.02867: running max and normalizer, rescaled by e^(m_old − m_new) when the max grows.
Loop swap and sequence parallelismFirst implemented by Phil Tillet in the Triton fused-attention tutorial, as the paper credits (Section 3.2, footnote 3).
Figure 1 operation countsOurs, from the two Algorithm 1 listings: per query row per key block, 4·B_c·d matmul FLOPs, 4·B_c softmax operations, and 4·d (FlashAttention) or 2·d (FlashAttention-2) operations on the output. Each non-matmul operation is counted as one FLOP.
correctionThe paper prints the output update with the rescaling factor inverted. Algorithm 1 line 10 and the two-block example in Section 3.1.1 write diag(e^(m_old − m_new))^(−1), which would multiply the old terms by e^(m_new − m_old) ≥ 1. The correct factor e^(m_old − m_new) ≤ 1 multiplies, and the official kernel multiplies (softmax.h lines 157–160). Section 2.3’s recap of FlashAttention has a slip of the same kind: diag(ℓ⁽¹⁾/ℓ⁽²⁾)⁻¹ O⁽¹⁾ where it should be diag(ℓ⁽¹⁾/ℓ⁽²⁾) diag(e^(m⁽¹⁾ − m⁽²⁾)) O⁽¹⁾. Three smaller slips: Algorithm 2 line 4 gives D ∈ ℝ^d where D has one entry per query row, ℝ^N; Section 4.2 says “1.3× speedup compared to FlashAttention-2” where Table 1 shows 1.3× over FlashAttention; and the causal-mask paragraph swaps rows and columns (the blocks that need no mask are those whose every column index is at most every row index). A caveat on attribution: the paper describes FlashAttention as one thread block per head, but the released FlashAttention v1 code already split each head across several thread blocks, and the measured FlashAttention baseline reaches 110 TFLOPs/s at 16k tokens with 32 heads, more than 32 of the A100’s 108 SMs could deliver (32 × 312/108 ≈ 92). The paper has no ablation, so its 2× cannot be divided among the three changes.

Questions you might still have

?

Does FlashAttention-2 compute exactly the same thing as FlashAttention?
The same function, softmax(QKᵀ)V, with no approximation. The last bits of an fp16 or bf16 result can differ because sums are accumulated in a different order. The backward pass adds into dQ with atomic adds whose order varies from run to run, so by default two runs can differ in the last bits; the code has a deterministic mode that gives each thread block its own buffer.

?

Does it use less memory than FlashAttention?
Both need O(N) memory beyond the inputs and output. FlashAttention-2 saves one number per query row (the logsumexp L) instead of two (the max m and the sum ℓ). It also writes each output block to HBM once, where FlashAttention’s loop order reads and writes the output block once per key block, but the paper does not count this saving in its argument.

?

Why is the backward pass slower than the forward pass?
It does five matrix multiplies per tile instead of two, keeps more blocks in SRAM at once, needs some synchronization between warps, and adds into dQ through HBM with atomic adds. The best backward number in the paper is 196 TFLOPs/s (63% of peak) against 227 TFLOPs/s (73%) for the forward pass.

?

Will FlashAttention-2 make my model train twice as fast?
No. Attention is only part of a Transformer layer. In the paper’s end-to-end GPT runs it is 1.04× to 1.29× faster than FlashAttention, with the larger gains at 8k context where attention is a larger share of the work, and 1.4× to 3.1× faster than training without FlashAttention.

?

Why does a long sequence make the parallelism change matter more?
With a fixed number of tokens per batch, a longer sequence means fewer sequences. If each thread block handles one whole head of one sequence, a batch of one sequence with 16 heads launches only 16 thread blocks on a GPU with 108 SMs. Splitting each head by query rows gives every head N/128 independent thread blocks.

?

Does it support multi-query and grouped-query attention?
Yes. Several query heads read the same key and value head through index arithmetic, without copying K and V. In the backward pass the dK and dV contributions of the query heads that share a key-value head are summed. The Multi-Query Attention explainer on this site covers why those variants exist.

?

What about newer GPUs?
Run unchanged on an H100, the FlashAttention-2 kernel reaches up to 335 TFLOPs/s, without using the H100’s TMA or fourth-generation Tensor Cores. FlashAttention-3 (2024) uses those features and asynchronous execution to reach up to 740 TFLOPs/s in FP16 on an H100, 75% of its peak.

?

Is attention still quadratic in sequence length?
Yes. FlashAttention-2 does O(N²d) FLOPs, the same as standard attention, and its runtime grows quadratically. It gets closer to the hardware’s peak rate for that fixed amount of work.

Footnotes & further reading

  1. The paper: Tri Dao, FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning (arXiv, July 2023; ICLR 2024). Code.
  2. The predecessor: Dao, Fu, Ermon, Rudra, Ré, FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness (NeurIPS 2022), explained on this site in FlashAttention.
  3. The streaming softmax: Milakov & Gimelshein, Online normalizer calculation for softmax (2018).
  4. The Triton implementation that first swapped the loops: Triton fused-attention tutorial. The A100 rates are from NVIDIA's A100 datasheet.
  5. The FLOP formula: Shoeybi et al., Megatron-LM (2019), explained in Megatron-LM. The model shapes: Brown et al., Table 2.1 of Language Models are Few-Shot Learners (2020).
  6. The follow-up: Shah et al., FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision (NeurIPS 2024).