音乐
暂未播放
FlashAttention 完全拆解(三):Warp 特化异步流水线与 FP8 低精度
系列回顾:从 Ampere 到 Hopper 的转折点#
本系列前两篇把 FlashAttention 的前两代讲透了:第一篇(FlashAttention-1,2022 年,NeurIPS 2022)的核心是 IO 感知——用分块、在线 softmax、反向重计算三件套,把注意力计算的 HBM 访问量从 Θ(N2) 压到 Θ(N2d2/M),并证明了这是精确注意力的下界;第二篇(FlashAttention-2,2023 年)是并行性的工程外科手术——推迟归一化削减非矩阵乘操作、外层循环换成 Q 块把序列维变成并行维、warp 分工从”切 K、V“改成”切 Q“消除 warp 间通信,在 A100 上把利用率从 25–40% 推到 50–73%,同年底的 FlashDecoding 又补上了解码阶段的最后一公里。
但这两代有一个共同的隐含前提:GPU 是”同步执行”的。每加载一块数据、每发一次矩阵乘指令,warp 都要等它完成才能继续。这个模型在 Ampere(A100)上够用,到了 Hopper(H100)就严重拖后腿——FA-2 论文自己报告,同一份代码在 H100 上约 335 TFLOPS,而 H100 的 FP16 峰值是 989 TFLOPS,利用率只有 35%。这个数字正是本文主角 FlashAttention-3(FA-3)的出发点。
FA-3 于 2024 年 7 月 12 日由 Colfax Research 的 Jay Shah、Ganesh Bikshandi,Meta 的 Ying Zhang,NVIDIA 的 Vijay Thakkar、Pradeep Ramani 与普林斯顿大学/Together AI 的 Tri Dao 联合发布于 arXiv(arXiv:2407.08608),后被 NeurIPS 2024 接收为 Spotlight 论文。它的三大贡献——生产者-消费者异步、把 softmax 藏在 GEMM 下面、FP8 低精度——把 H100 上注意力 kernel 的利用率推到了 75%(FP16 前向最高 740 TFLOPS)和接近 1.2 PFLOPS(FP8),并直接为 Blackwell 上的 FlashAttention-4(本站已单独拆解)铺平了道路。本文把 FA-3 从设计动机到每个 kernel 细节彻底拆开。
背景:为什么同一份代码,A100 上 73%,H100 上只剩 35%#
FA-2 在 A100 上能到 73% 的利用率,因为它已经把 Ampere 的并行模型用到了极致。H100 不是 A100 的简单增强版,而是引入了三种 FA-2 完全没用上的硬件能力——理解它们,才能理解 FA-3 的每一个设计决策。
Hopper 的三种新硬件能力#
第一,TMA(Tensor Memory Accelerator,张量内存加速器)。Hopper 为”HBM 与共享内存(SMEM)之间的数据搬运”单独造了一个硬件单元 Hopper 架构深入解析。之前 GPU 上搬数据要由线程亲自执行拷贝指令(cp.async 也要线程逐条发指令、按行计算地址),TMA 则不同:只需要一个线程向 TMA 描述”搬哪块、搬到哪”(含多维索引计算、越界处理、甚至内嵌 swizzle 布局变换),TMA 硬件就自动把整块数据从全局内存搬到共享内存。发起搬运的线程不阻塞,可以立刻去干别的。数据搬完由硬件置位一个屏障(barrier),其他 warp 可以”等这个屏障”。
第二,WGMMA(warpgroup MMA,warpgroup 级矩阵乘加指令)。Ampere 的 mma.sync 指令粒度是一个 warp(32 线程);Hopper 的 WGMMA 把粒度扩大到 warpgroup(4 个 warp、128 线程),一次指令完成更大的矩阵乘,吞吐约为前代的两倍。更重要的是它是异步的:发出 WGMMA 后,张量核心(Tensor Core)在执行,执行它的 warp 不必等结果出来就能继续发射其他指令,之后再用”提交-等待”(commit-and-wait)原语同步结果。
第三,FP8 张量核心。WGMMA 支持 FP8(e4m3)操作数,吞吐是 FP16 的两倍。但 FP8 的使用有一堆布局约束(后面第三节细讲)。
此外还有一个容易被忽略但被 FA-3 用上的能力:setmaxnreg 指令允许 kernel 在运行时把寄存器从一组 warp 动态重新分配给另一组 warp——做数据搬运的 warp 用不了几个寄存器,做矩阵乘的 warp 越多越好。
论文把这些硬件能力总结成了下面这张内存层次表(论文表 1):
| 硬件层次 | 并行单元 | 数据位置 | 容量 @ 带宽 |
|---|---|---|---|
| 芯片 | Grid | GMEM(HBM) | 80 GiB @ 3.35 TB/s |
| GPC | 线程块簇 | L2 | 50 MiB @ 12 TB/s |
| SM | 线程块(CTA) | SMEM | 228 KiB/SM,整卡约 31 TB/s |
| 线程 | 线程 | RMEM(寄存器) | 256 KiB/SM |
注意 SMEM 的带宽(31 TB/s)比 HBM(3.35 TB/s)高一个数量级——分块算法的全部意义就是把数据尽量留在 SMEM 里反复用。
同步执行模型的代价:非矩阵乘操作吃掉一半周期#
FA-2 的 kernel 里,所有 warp 一视同仁:都要搬数据、都要算矩阵乘、都要算 softmax。在”同步”的执行模型下,这些操作串行发生:加载完 K、V 才能做 QKT,QKT 完成才能做 softmax,softmax 完成才能做 PV。问题在于,矩阵乘有专用硬件(张量核心),softmax 里的指数运算只有吞吐低得多的 SFU(特殊函数单元)。
论文 §3.1 给出了两个关键数字(H100 SXM5,FP16):
- FP16 矩阵乘峰值:989 TFLOPS;
- 指数(exponential)运算吞吐:3.9 TFLOPS——两者相差 256 倍。
再算前向注意力的操作构成:对 head dimension 128 的注意力,每行分数矩阵要做一次 QKT(∝Nd 次乘加)和一次行 softmax(∝N 次指数)。矩阵乘的 FLOPs 是指数操作 FLOPs 的 512 倍,但指数的吞吐只有矩阵乘的 1/256。于是:
矩阵乘耗时指数耗时=256512=2指数计算花费的时间是矩阵乘的两倍——softmax 的指数运算可以占据 kernel 一半的执行周期。而且注意这里的 3.9 TFLOPS 是整个 GPU 的 SFU 吞吐,指数与矩阵乘虽在不同单元上可以部分重叠,但 FA-2 的同步模型让它们几乎没有机会重叠。FP8 下局面更糟:矩阵乘吞吐翻倍到 1978 TFLOPS,指数吞吐纹丝不动,比例失衡翻倍。
这就是 FA-2 在 H100 上只有 35% 利用率的直接原因:它把”吞吐差距 256 倍的两种操作”串行安排,让最慢的操作决定了整个 kernel 的速度。FA-3 的全部设计,一句话概括就是:让矩阵乘、指数、数据搬运这三种发生在不同硬件单元上的操作,尽可能同时进行。
核心思想:三个贡献,对应三种异步性#
FA-3 的三大贡献可以对应到三个层面:
- 生产者-消费者异步(producer-consumer asynchrony):把”搬运数据”和”计算”分配给不同的 warp(warp specialization),让数据搬运(TMA,由硬件完成)与矩阵乘(张量核心)同时进行——这是数据搬运与计算的重叠;
- 把 softmax 藏在 GEMM 下面(hiding softmax under asynchronous block-wise GEMMs):利用 WGMMA 的异步性,让 softmax 与矩阵乘在不同 warp(或同一 warp 的不同时刻)交错执行——这是两种计算的重叠;
- 硬件加速的 FP8 低精度 GEMM:让矩阵乘吞吐直接翻倍,并解决 FP8 的布局与精度问题——这是把瓶颈本身变小。
下面逐节拆解。
贡献一:生产者-消费者异步(Warp 特化)#
角色划分:只搬数据的 warp 和只算数的 warp#
warp specialization 的思想并不新鲜——论文引用的是 2011 年的 CudaDMA 工作,NVIDIA 的 CUTLASS 库也一直在用;新鲜的是 Hopper 硬件让专职化变得几乎免费:TMA 搬运只需要一个线程发一条指令,其余 31 个线程可以完全不用干活;而 WGMMA 的累加器放在寄存器里,消费者 warp 的寄存器越多越好。setmaxnreg 让这种分工在运行时动态调整。
FA-3 前向 kernel 把一个线程块内的 warp 分成两个角色(论文 Algorithm 1):
- 生产者 warpgroup(producer):专职发起 TMA 加载。执行
setmaxnreg把自己的寄存器上限压得很低(TMA 搬运只需要一个线程发指令,其余线程几乎不用寄存器),把省下的寄存器让给消费者。它只做一件事:把 Qi(一次)和 {Kj,Vj}j=0Tc−1(循环)加载进环形共享内存缓冲(circular SMEM buffer,s 级流水),每次加载完成后通过命名屏障”提交”(commit)通知消费者; - 消费者 warpgroup(consumer):执行
setmaxnreg把寄存器上限提到很高(WGMMA 的累加器全在寄存器里,具体数量按块大小调优),全部资源用来算:初始化 Oi、ℓi、mi,等 Qi 和 K0 就位后,循环执行两个 WGMMA(Si(j)=QiKjT 和 Oi=P~i(j)Vj)与 softmax 的状态更新。
两个角色之间用共享内存的 mbarrier(命名屏障)同步:生产者写满缓冲的第 j 阶段后 commit,消费者等这个屏障、消费完数据后释放该阶段,生产者才能覆盖它。环形缓冲的深度 s 决定了生产者可以领先消费者多少次迭代——生产者永远比消费者快几步,把 HBM 的延迟藏在后面。
这里有一个值得细想的点:为什么让生产者用 TMA、而不是让消费者自己加载?因为 TMA 是异步的——消费者发一条 TMA 指令后不需要等数据到位,可以先去算上一块数据,等真正需要新数据时再等屏障。FA-2 的同步模型里,“加载”是阻塞的:warp 发了拷贝指令就得等,等的时候张量核心空转。FA-3 把”等”从计算路径上拆走了:等数据的是生产者(它本来也没别的活干),消费者永远面对”数据已就位”的缓冲。
完整的前向主循环#
把角色划分落实到算法上,就是论文的 Algorithm 1(前向,无消费者内部重叠版本)。它的骨架与 FA-2 相同——外层遍历 Q 行块(序列维并行)、内层遍历 K、V 列块——但每个动作都换成了异步版本:
1输入:HBM 中的 Q_i ∈ R^{B_r×d},K, V ∈ R^{N×d},键块大小 B_c,T_c = ⌈N/B_c⌉2 1: 初始化流水线对象,管理 s 级环形共享内存缓冲的屏障同步3 2: if 在生产者 warpgroup 中 then4 3: 释放预定数量的寄存器(setmaxnreg)5 4: 发起从 HBM 到共享内存的 Q_i 加载(TMA)6 5: 完成后 commit,通知消费者 Q_i 已加载7 6: for j = 0 to T_c-1 do8 7: 等待缓冲第 (j % s) 阶段被消费完9 8: 发起 K_j、V_j 的 TMA 加载到第 (j % s) 阶段10 9: 完成后 commit,通知消费者1110: end for1211: else(消费者 warpgroup)1312: 按消费者 warp 数量重新分配寄存器(setmaxnreg)1413: 在片上初始化 O_i = 0、ℓ_i = 0、m_i = -∞1514: 等待 Q_i 加载完成1615: for j = 0 to T_c-1 do1716: 等待 K_j 加载完成1817: 计算 S_i^(j) = Q_i K_j^T(SS-GEMM,提交并等待)1918: 保存 m_i_old,计算 m_i = max(m_i_old, rowmax(S_i^(j)))2019: 计算 P̃_i^(j) = exp(S_i^(j) - m_i),ℓ_i = e^{m_i_old - m_i}·ℓ_i + rowsum(P̃_i^(j))2120: 等待 V_j 加载完成2221: 计算 O_i = diag(e^{m_i_old - m_i})^{-1}·O_i + P̃_i^(j)·V_j(RS-GEMM,提交并等待)2322: 释放缓冲第 (j % s) 阶段给生产者2423: end for2524: 计算 O_i = diag(ℓ_i)^{-1}·O_i 和 L_i = m_i + log(ℓ_i)2625: 把 O_i、L_i 写回 HBM2726: end if对比 FA-2 的算法,逐行的变化值得注意:
- 第 3、12 行:寄存器在运行时按角色重新分配——生产者用
setmaxnreg把上限压低,消费者提高。WGMMA 的 FP32 累加器(Br×Bc 的 S 块)要放在寄存器里,这是消费者寄存器需求的大头; - 第 4、8 行:TMA 加载只由生产者发起,单线程发指令,硬件完成搬运——消费者完全不需要感知数据搬运的发生;
- 第 5、9 行:
commit通知通过命名屏障实现,生产者与消费者的握手全部基于硬件屏障而非线程同步(__syncthreads); - 第 17、21 行:两个 GEMM 都标注了”SS”(第一个操作数从共享内存取)或”RS”(第一个操作数从寄存器取)前缀——S=QKT 的两个操作数都在共享内存,而 O=P~V 的概率块 P~ 在寄存器里(它是上一步算出来的,没写回共享内存)。这是 FA-2 没有的寄存器-共享内存混合数据流;
- 第 7、22 行:生产者等”缓冲被消费”,消费者等”数据被加载”——两个方向的等待互相错开,这就是环形缓冲的意义。前 s 次迭代生产者不会等待(缓冲从空开始填,没有东西可等)。
这套框架中,每个 warp 的指令流都极其简单:生产者是一连串 TMA 和屏障,消费者是一连串 WGMMA、逐元素操作和屏障。编译器面对这种简单的指令流更容易生成好的调度——这也是 warp specialization 的隐性收益。
反向传播:三种角色#
反向传播(论文附录 B.1,Algorithm 3)在前向的生产者-消费者之外增加了一个角色——dQ 写入 warp。原因是 FA-2 的反向里我们讲过:dQi 的梯度需要跨线程块累加(每个线程块给同一位置的 dQ 贡献一份),FA-2 用原子加处理。在 FA-3 里,多个线程块同时原子写全局内存的 dQ 会造成内存竞争——如果让消费者 warpgroup 自己去做这些原子加,它在等原子操作完成时就没法发射下一个矩阵乘。
FA-3 的解法是把原子加职责单独拆给一个 warp:消费者算完局部的 dQi(local) 后写进共享内存、通知 dQ 写入 warp;dQ 写入 warp 用信号量(semaphore)串行地把这些局部结果原子累加到全局 dQ。“谁负责跟全局内存打交道”从计算路径上被彻底拆走,消费者 warpgroup 只管在片上算。
反向前半段还有一处结构变化:FA-2 需要预计算 D=rowsum(dO∘O) 并写回 HBM。FA-3 把它放进独立的预处理 kernel(Algorithm 3 第 1 行),避免在主 kernel 里多一趟同步。主 kernel 的消费者循环与 FA-2 的 Algorithm 2 结构一致(重算 S、P、五个矩阵乘),但全部换成异步 WGMMA。
贡献二:把 softmax 藏在 GEMM 下面#
生产者-消费者异步解决了”搬运 vs 计算”的重叠,但消费者 warpgroup 内部还有另一对矛盾:softmax 依赖第一个 GEMM 的输出,第二个 GEMM 依赖 softmax 的输出——一条严格的串行链:GEMM1 → softmax → GEMM2。矩阵乘期间张量核心满载、普通核心(CUDA cores)闲着;softmax 期间反过来。FA-3 要做的是让这对”跷跷板”里的两边同时下落:softmax 在计算时,张量核心不能闲着。
伪依赖的本质:跨迭代流水#
关键观察:j 次迭代的 softmax 只依赖第 j 次迭代的 S 块,j 次迭代的第二个 GEMM 只依赖第 j 次迭代的 P~ 块。迭代之间并没有数据依赖——所以可以把”GEMM1(j+1) → softmax(j) → GEMM2(j)“这三个来自不同迭代的操作叠在一起:前者的输出不是后者的输入,流水线就成立了。FA-3 用了两级手段实现这个重叠。
第一级:乒乓调度(pingpong scheduling)——两个消费者 warpgroup 交替#
FA-3 把消费者拆成两个 warpgroup,让它们一个做 GEMM、一个做 softmax,交替轮换(乒乓球式):

图里同一种颜色代表同一次迭代 j:可以看到 warpgroup A 在处理迭代 j 的 GEMM 时,warpgroup B 正在处理迭代 j−1 的 softmax——两者在不同的硬件单元上同时执行(GEMM 用张量核心,softmax 用 CUDA 核心/SFU)。论文承认”实际中乒乓调度并不如图示这么干净”(迭代长度不同、单元负载不均衡),但实测收益明确:FP16 前向、head dim 128、序列长度 8192 时,单靠这一项就把吞吐从 570 TFLOPS 提到 620–640 TFLOPS。
第二级:warpgroup 内 2 级流水(2-stage pipelining)——提交但不等待#
乒乓调度只在”两个 warpgroup”层面重叠,每个 warpgroup 内部仍是串行。FA-3 进一步在同一 warpgroup 内部做流水(论文 Algorithm 2)。核心技巧是 WGMMA 的提交-不等待(commit but do not wait)语义:
1 7: for j = 1 to T_c-1 do2 8: 等待 K_j 加载完成3 9: 计算 S_next = Q_i K_j^T(WGMMA,提交但不等待)410: 等待 V_{j-1} 加载完成511: 计算 O_i = O_i + P̃_cur·V_{j-1}(WGMMA,提交但不等待)612: 等待 S_next 的 WGMMA 完成713: 基于 S_next 计算 m_i、P̃_next、ℓ_i ← softmax(迭代 j)814: 等待 O 的 WGMMA 完成,然后重缩放 O_i915: 释放缓冲第 (j % s) 和第 ((j-1) % s) 阶段1016: 把 S_next 复制到 S_cur1117: end for对照图理解(下面这张是论文的 Figure 2,2-stage 流水线的示意):

逐行解读这套流水线的时序:
- 第 9 行:发出迭代 j 的第一个 WGMMA(QiKjT),不等待结果——张量核心开始算 Sj,warp 立即继续;
- 第 11 行:发出迭代 j−1 的第二个 WGMMA(P~j−1Vj−1),也不等待——此时张量核心上有两个矩阵乘排队执行;
- 第 12 行:等到 Sj 算完——注意此时第 11 行的 WGMMA 应该也快完成了(它更早发出);
- 第 13 行:对 Sj 做 softmax(最大值、指数、和)——这是纯 CUDA core 操作,与此同时张量核心正在执行第 11 行那个矩阵乘。这就是”把 softmax 藏在 GEMM 下面”的字面含义;
- 第 14 行:等第 11 行的矩阵乘完成,重缩放 O。
妙处在于:第 13 行的 softmax 与第 11 行的矩阵乘在时间上重叠——一个用 CUDA core,一个用张量核心,互不抢占。论文正文的原话是:“主循环中,迭代 j 的第二个 WGMMA(第 11 行)与迭代 j+1 的 softmax 操作(第 13 行)重叠执行。”
这个设计有两个工程上的坑,论文都点到了:
坑一:编译器重排。 NVCC 经常重新排列指令以优化调度,可能打乱精心编排的 WGMMA/非 WGMMA 交错序列,导致重叠消失或行为异常。论文在附录 B.2 里对 SASS(汇编)代码做了分析,确认编译器生成的代码确实如预期重叠——但这份确认是针对当时 CUDA 12.3 编译器的,换个编译器版本可能就得重新验证。这也是手写 CUDA 异步 kernel 的普遍痛点。
坑二:寄存器压力。 2 级流水要求同时持有 Scur 和 Snext 两个分数块(迭代 j−1 的还没消费完,迭代 j 的已经算出来),每个线程块多占 Br×Bc×4 字节的寄存器(FP32)。这会与”用更大块大小提升效率”竞争——块越大寄存器越紧张,论文的建议是”根据 profiling 结果做权衡”。
3 级流水:为什么反而更差#
论文还尝试了 3 级流水(附录 B.3,Algorithm 4),理想目标是同时重叠三个操作:迭代 j+2 的第一个 WGMMA、迭代 j+1 的 softmax、迭代 j 的第二个 WGMMA:

结果:3 级流水表现反而更差。论文给了两个原因:
- 编译器不配合:SASS 分析显示,编译器只把”第一个 WGMMA”与 softmax 重叠了,“第二个 WGMMA”没有被重叠——编译器认为的调度顺序与设计意图不一致,且论文作者也说不清编译器为什么这样重排;
- 寄存器压力更大:3 级流水需要在寄存器里额外保存 P~i 和缩放因子 scale_o(Br×Bc× 数据类型大小 + Br×4 字节),为容纳这些寄存器只能把块调小,块变小又损失了 GEMM 效率。
这个负面结果很有价值:它说明了异步流水线的深度不是越大越好,受制于寄存器文件容量和编译器的实际行为。2 级流水是 FA-3 在 Hopper 上找到的甜点。
消融实验:两项技术各贡献多少#
论文表 2 给了三组消融数据(FP16 前向,head dim 128):
| 配置 | 耗时 | 吞吐 |
|---|---|---|
| 完整 FA-3 | 3.538 ms | 661 TFLOPS |
| 无 GEMM-softmax 流水线,保留 warp 特化 | 4.021 ms | 582 TFLOPS |
| 保留 GEMM-softmax 流水线,无 warp 特化 | 4.105 ms | 570 TFLOPS |
三个数字告诉我们:warp 特化(生产者-消费者异步)单独贡献约 570→582(约 2%),而 GEMM-softmax 流水线在与 warp 特化合体后贡献了 582→661(约 13%)。两者合力约 16%。流水线(把 softmax 藏起来)是更大的单项贡献——这与前文”指数运算占一半周期”的分析一致:解决了最大的瓶颈,收益最大。注意消融里的”570”是基线,与乒乓调度一节提到的 570 一致。
贡献三:FP8 低精度#
Hopper 的 FP8 张量核心吞吐是 FP16 的两倍(约 1978 TFLOPS)。把前向的两个 GEMM 换成 FP8,是让注意力提速最直接的路——但 FP8 的布局约束和精度损失是两个硬骨头。
布局约束一:FP8 WGMMA 只接受 k-major 操作数#
WGMMA 要求矩阵操作数以特定方式在内存中连续。对一个 M×K 矩阵 A 来说,若 A 在 M 维连续则称为 mn-major,在 K 维连续则称为 k-major。FP16 WGMMA 对两种布局都接受;FP8 WGMMA 只接受 k-major。
注意力里出问题的是第二个 GEMM:O=P~V,其中 V 是序列维连续的(按惯例 Q、K、V 都以 head 维为连续维,即每行是一个头的 d 个元素)。第二个 GEMM 需要 V 的 tile 在序列长度维连续——TMA 加载本身不能改变连续维(它只是搬运)。论文列出了三个候选方案:
- 1a:把转置融合到前一步的 epilogue(如 RoPE 的输出直接写成转置后的布局)——但这对标准库集成太困难;
- 1b:独立的转置预处理 kernel——在内存受限的推理场景太浪费(多一趟 HBM 往返);
- 内核内转置:数据加载进 SMEM 后用指令转置——FA-3 选了这个。
内核内转置用的是 LDSM(ldmatrix)/STSM(stmatrix) 指令:一个 warp 的线程集体从共享内存加载到寄存器、或从寄存器存回共享内存,以 128 字节为粒度,拷贝的同时可以改变布局(转置)。关键工程细节:这个转置放在生产者 warpgroup 里执行(生产者反正只发 TMA,有大量闲置的指令发射能力),且第一次迭代之后,对下一个 V tile 的转置可以隐藏在涉及当前 V 和下一个 K 的两个 WGMMA 的阴影里——转置不需要额外时间。
布局约束二:FP8 WGMMA 的累加器与操作数布局不一致#
前向的两个 GEMM 是背靠背的:第一个 GEMM(S=QKT)的输出是 FP32 累加器,直接作为第二个 GEMM(P~V)的 FP8 操作数。问题在于:FP8 WGMMA 的 FP32 累加器在寄存器里的布局,与它作为操作数 A 时要求的布局不一样(论文图 3、图 4 画了具体的线程-寄存器映射)。如果直接喂,第二个 WGMMA 会算出错误的结果。
FA-3 的解法是用 byte-permute(字节置换)指令在寄存器里就地变换布局:FP8 WGMMA 的 FP32 累加器在寄存器里的列分组顺序是 {d0,d1,d2,d3,d4,d5,d6,d7},而作为 FP8 操作数 A 时要求 {d0,d1,d4,d5,d2,d3,d6,d7} 的顺序(论文图 3、图 4 画了两种布局的线程-寄存器映射)——用字节置换指令把 d2,d3 与 d4,d5 交换即可,该置换每 8 字节重复一次。逻辑效果是置换 P 块的列顺序;为了让第二个 GEMM 仍然算对,内核内转置在写 V tile 时写出匹配的行置换——两个置换互相抵消,数学结果不变。
这两个布局问题的解决让 FP8 WGMMA 可以连续地执行两个背靠背 GEMM——论文明确感谢了 cuDNN 团队贡献的内核内转置思路。
精度问题一:块量化(block quantization)#
FP8(e4m3)只有 3 位尾数,动态范围极窄。简单地对整个 Q、K、V 用一个全局缩放因子(per-tensor scaling)量化,精度损失严重。FA-3 采用块量化:对每个 Qi、Kj、Vj 块分别维护一个缩放因子:
Si(j)=QiKjT≈(sQiQ~i)(sKjK~j)T=sQisKj(Q~iK~jT)其中 Q~i、K~j 是量化后的 FP8 块,sQi、sKj 是各自的缩放标量。块级缩放与 FA 的块迭代结构天然契合:每块一个缩放因子不需要任何额外全局操作,而且分数矩阵 S 的块缩放 sQisKj 可以直接融合进在线 softmax 的重缩放步骤——几乎零成本。
精度问题二:非相干处理(incoherent processing)#
块量化解决”动态范围”问题,但 LLM 的激活值里还有离群值(outlier)维度:极少数维度上的值比其他维度大一个量级(这在很多 LLM 里被反复观察到)。量化时这些离群值吞掉大量缩放预算,导致其他维度精度崩溃。
FA-3 采用 QuIP/QuIP# 等量化工作的”非相干处理”思路:量化前把 Q 和 K 乘以同一个随机正交矩阵 M。因为 M 正交(MMT=I):
(QM)(KM)T=QMMTKT=QKT注意力输出数学上完全不变。而 QM 的每个元素都是 Q 各元素的随机加权和——离群值的能量被”摊平”到所有维度,每个维度的动态范围都变得均匀,量化误差随之下降。实践中 M 取”随机 ±1 对角矩阵 × Hadamard 矩阵”的乘积,与一个 d 维向量相乘只需 O(dlogd)(Hadamard 的快速变换),且可以与 RoPE 旋转融合,零额外计算成本。论文引用了 Chee et al. 的 QuIP 和 Tseng et al. 的 QuIP# 作为这一技术的来源。
数值误差验证#
论文用带离群值的分布验证精度(§4.3):Q、K、V 的元素按 N(0,1)+N(0,100)⋅Bernoulli(0.001) 生成——正常分布上叠加 0.1% 的”大离群项”,模拟真实 LLM 激活。以 FP64 计算为参考,比较 RMSE:
| 方法 | RMSE |
|---|---|
| 标准注意力(FP16) | 3.2e-4 |
| FlashAttention-2(FP16) | 1.9e-4 |
| FlashAttention-3(FP16) | 1.9e-4 |
| 基线 FP8(per-tensor 缩放) | 2.4e-2 |
| FlashAttention-3(FP8,无块量化) | 9.3e-3 |
| FlashAttention-3(FP8,无非相干处理) | 2.4e-2 |
| FlashAttention-3(FP8,完整) | 9.1e-3 |
读这张表有三个发现。第一,FP16 下 FA-2 和 FA-3 完全一致(1.9e-4),且都比标准实现好 1.7 倍——因为它们的 softmax 中间结果(指数和、累加器)保持 FP32,标准实现把中间结果存成 FP16。第二,FA-3 的 FP8 相比 per-tensor 基线误差低 2.6 倍(9.1e-3 vs 2.4e-2)。第三(表格里最有信息量的一点):去掉非相干处理后误差直接回到 2.4e-2——非相干处理是 FP8 精度的主要贡献者,块量化单独只有微弱影响(9.3e-3 vs 9.1e-3)。论文正文的表述是”块量化与非相干处理共同把 FP8 的数值误差降低 2.6 倍”,但消融揭示了主次。
另外一个容易忽略的要点:FP8 只用于前向。论文明确写”我们改造前向算法以利用 FP8 张量核心”——反向的五个矩阵乘仍用 FP16。附录 C.2 的 FP8 基准也只有前向。原因是反向的梯度累加对精度更敏感,且反向的布局约束更复杂(dQ 的跨块累加在 FP8 下几乎不可行)。所以”FA-3 是 FP8 注意力”这个说法要精确为”FA-3 的前向支持 FP8”。
性能表现:数字说话#
论文在 H100 80GB SXM5(700W)上评测,设置如下(附录 C.1):CUDA 12.3、cuDNN 9.1.1.17、CUTLASS 3.5、FlashAttention 2.5.8、Triton nightly、PyTorch 2.3.0;GPU 时钟固定 1830 MHz(正是 989 TFLOPS 峰值对应的频率,消除 boost 抖动);每项基准重复 100 次取平均。序列长度 512 到 16K,batch 随序列长度缩小保持总 token 数 16K,hidden size 2048,head dimension 64/128/256(对应 32/16/8 头)。FLOPs 按公式 4⋅seqlen2⋅d⋅heads 计算(两个 GEMM 各 2N2d),因果掩码下除以 2,反向乘 2.5(前向 2 个 GEMM、反向 5 个)。
FP16 前向:最高 740 TFLOPS#
下面这张是论文的 Figure 5(FP16/BF16 前向,6 个子图覆盖三种头维度 × 有无因果掩码):

挑最常用的 head dim 128 报具体数字(无掩码,图 5c;单位 TFLOPS):
| 序列长度 | 512 | 1K | 2K | 4K | 8K | 16K |
|---|---|---|---|---|---|---|
| 标准注意力 | 74 | 100 | 119 | 133 | 139 | OOM |
| FlashAttention-2 | 309 | 350 | 362 | 368 | 370 | 370 |
| Triton(FA-2 移植) | 323 | 372 | 389 | 389 | 392 | 395 |
| cuDNN | 497 | 574 | 617 | 609 | 600 | 595 |
| FlashAttention-3 | 467 | 565 | 625 | 638 | 646 | 648 |
四个观察:
- 短序列(512)FA-3 略低于 cuDNN(467 vs 497),1K 起反超——论文正文的原话是”中长序列(1K 及以上)FA-3 甚至超过 NVIDIA 闭源、专门为 H100 优化的 cuDNN”。短序列下流水线还没填满,cuDNN 的深度调优占优;
- 相对 FA-2,FA-3 在 16K 时 648/370 = 1.75 倍;论文摘要给的整体区间是 1.5–2.0 倍(head dim 256 时差距更大);
- 序列越长相对优势越大——长序列下 K、V 块的遍历次数多,流水线和 TMA 的收益被放大;
- 加因果掩码后(图 5d)趋势不变:16K 时 FA-3 616 vs FA-2 335,约 1.8 倍;掩码跳过的块让两个实现都变快,但 FA-3 的异步流水线在变长的跳过块循环里同样保持优势。
head dim 256(图 5e,无掩码)时 FA-3 达到 756 TFLOPS——这就是论文摘要”FP16 最高 740 TFLOPS、约 75% 利用率”的来源。为什么 head dim 越大效率越高?因为单个 WGMMA 的矩阵更大,张量核心利用更充分,流水线更不容易被小矩阵的启动开销打断。反过来 head dim 64 时最高只有 497 TFLOPS——注意力 kernel 的效率对 head dim 极其敏感,这也是后来 DeepSeek 选 (192, 128) 这类非标准 head dim 时各家都要重新调 kernel 的原因。
反向:1.5–1.75 倍#
反向(FP16/BF16,图 6)相对 FA-2 快 1.5–1.75 倍。head dim 128、无掩码:FA-3 在 16K 时 474 vs FA-2 的 291 TFLOPS。反向的收益小于前向,因为反向有 5 个矩阵乘,softmax 重算只占其中一小部分,“藏 softmax”的空间更小;反向的主要收益来自 warp 特化(dQ 写入 warp 消除了原子加阻塞)和 TMA。
FP8 前向:接近 1.2 PFLOPS#
论文图 7 是 FP8 前向(head dim 256,无掩码/有掩码):

FP8 前向在长序列下达到接近 1.2 PFLOPS(论文摘要原话)。相对 FP16 的 740 TFLOPS 又提了约 60%——没有完全翻倍,因为 softmax 仍在 FP16/FP32 下计算、块量化和转置有开销,但方向明确:FP8 把注意力的上限推到了 PFLOPS 级别。注意图 7 里 FP8 的序列长度用的是 512、1024、2048、4224、8448、16896——≥4K 时取 132 的倍数(H100 SXM5 的 SM 数),避免最后一波线程块不满载导致的性能波动(wave quantization 效应,附录 C.2 解释了这点)。
与标准实现对比:3–16 倍#
论文摘要还报了一个区间:FA-3 相对 PyTorch 标准注意力实现快 3–16 倍(标准实现短序列 16K 时直接 OOM——32 头 × 16K² 的分数矩阵已经超出显存)。这个对比说明 IO 感知 + 异步 + 低精度三代的收益是乘法叠加的:IO 分块省掉了 Θ(N2) 的 HBM 流量,异步把剩余时间里的利用率从 35% 提到 75%,FP8 又把每个周期的计算量翻倍。
对后续的影响:FA-3 打开了什么#
FA-3 的影响远不止 H100 上的几组基准:
它确立了”异步优先”的 kernel 编程范式。 FA-3 之前,主流注意力 kernel 的优化围绕”访存量”打转(IO 感知);FA-3 之后,新硬件的优化围绕”重叠什么”打转——warp 特化、异步拷贝、乒乓调度成为 Blackwell 上一切 kernel(包括 FA-4)的默认语言。FA-4 的”每 SM 16 warp、6 种角色”的流水线,本质上是 FA-3 生产者-消费者思想在更多硬件并发度上的延伸;FA-4 用 FMA 单元的多项式近似替代 SFU 算指数,也是 FA-3”softmax 是瓶颈”分析的直接后果——本站的 FlashAttention 完全拆解(四) 讲了完整故事。
它把 FP8 注意力变成了现实选项。 块量化 + 非相干处理后来成为 FP8 注意力(以及更广义的 FP8 GEMM)的标准配方,DeepSeek 的 FP8 注意力、各家推理框架的 FP8 kernel 都能在这篇里找到原型。非相干处理本身来自量化社区(QuIP/QuIP#),FA-3 把它带进了注意力 kernel,是”量化技术反哺训练/推理 kernel”的典型案例。
它在工程上验证了 Triton 路线的可行性。 论文发表同期,Triton 的 Fused Attention 官方教程 更新为 FlashAttention-3 的 Triton 实现(含 FP8),证明 FA-3 的思想可以用可移植的 DSL 复现;FA-3 本身则基于 CUTLASS 原语(WGMMA/TMA 抽象)实现,并集成进 Dao-AILab/flash-attention 仓库,Hopper 架构上自动分派。论文致谢还提到 Driss Guessous 把 FA-3 集成进了 PyTorch。
局限也值得记录(论文 §5):FA-3 没有专门优化 LLM 推理(解码场景的 batch 小、KV 缓存读多算少,FlashDecoding 那套需要另行整合);FP8 kernel 没有做 persistent kernel 设计;低精度注意力在大规模训练中的长期影响未验证。这些空白后来分别由 FA-4、推理框架的融合 kernel 和大量实证工作填补——但这都是后话。
小结#
FA-3 是 FlashAttention 系列里”从算法优化转向硬件协同设计”的分水岭。把它浓缩成三句话:
- 异步是主线:TMA 与 WGMMA 的异步性让 FA-3 把数据搬运、矩阵乘、softmax 三种操作分别重叠——生产者-消费者 warp 特化解决搬运 vs 计算,乒乓调度与 2 级流水解决 softmax vs GEMM,把指数运算从”占一半周期”压到被完全隐藏;
- FP8 是倍增器:k-major 布局与累加器布局两个约束用内核内转置(LDSM/STSM)和 byte-permute 解决;块量化 + 非相干处理把 FP8 的数值误差做到比 per-tensor 基线低 2.6 倍——代价是前向专用,反向保持 FP16;
- 数字是硬道理:FP16 前向 740 TFLOPS(75% 利用率,FA-2 的 2 倍)、反向 1.5–1.75 倍、FP8 接近 1.2 PFLOPS、相对标准实现 3–16 倍——FA-2 在 A100 上达到的 73% 利用率,FA-3 在吞吐翻倍的 H100 上重新追了回来。
至此,FlashAttention 这条主线的三代演进(算法 → 并行 → 硬件异步)在本站全部讲完:本系列第一篇讲 IO 感知(FA-1),第二篇讲并行与解码(FA-2 + FlashDecoding),本篇讲 Hopper 异步与 FP8(FA-3),Blackwell 上的终极形态(FA-4)在 另一篇 已完整拆解。读完这四篇,从”为什么注意力慢”到”怎么把注意力推到 GEMM 速度”的完整逻辑链就齐了。
参考资料#
- FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision — FA-3 论文(2024 年 7 月 arXiv,NeurIPS 2024 Spotlight),本文主要依据
- FlashAttention-3(NeurIPS 2024 Proceedings 页面) — NeurIPS 2024 官方收录页
- Dao-AILab/flash-attention GitHub 仓库 — FA-3 官方开源实现,Hopper 架构自动分派
- Triton Fused Attention 教程(06-fused-attention) — FlashAttention-3 的 Triton 官方教程实现(Phil Tillet),含 FP8 与基准
- NVIDIA Hopper Architecture In-Depth — NVIDIA 官方博客,TMA/WGMMA 硬件背景
- Delivering 1 PFLOP/s of Performance with FP8 FlashAttention-2(Colfax Research 博客) — FA-3 作者团队(Colfax)的 FP8 注意力先期工作,被 FA-3 论文引用
- 深入解析 FlashAttention-3:异步与 FP8 如何为 Transformer 加速 — 中文解读,含三大贡献的逐条分析
- FlashAttention 全系列深度解析(博客园) — 中文社区对 FA 全系列(含 FA-3)的整合解读
- NVIDIA/cutlass GitHub 仓库 — FA-3 实现所依赖的 CUTLASS 原语(WGMMA/TMA 抽象)
- FlashAttention 完全拆解(二):序列维并行、Warp 工作划分与 FlashDecoding — 本站文章,本系列第二篇
- FlashAttention 完全拆解(一):IO 感知与分块注意力算法 — 本站文章,本系列第一篇
- FlashAttention 完全拆解(四):FlashAttention-4——面向 Blackwell 的算法-流水线协同设计 — 本站文章,FA-3 思想在 Blackwell 上的延续
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
部分内容可能已过时
评论区
分享你的想法,与大家交流讨论
音乐
暂未播放



