Attention 流水线:单 Q 双 S 与双 Q 的计算重叠
Attention 前向由两次矩阵乘和一段 softmax 交替组成。串行实现里,Tensor Core 算完 QK 要等 softmax,softmax 做完又要等 PV,两类运算单元轮流出空。把不同数据块上的工作交错排开,让一边等待时另一边继续算,是这个算子提高吞吐的主要来源。
在单个 CTA 内安排这种交错,有两种常见做法。单 Q 双 S 从同一个 query 块的相邻 KV 轮次里找重叠;双 Q 把两个 query 块放进同一个 CTA,让两条计算链交替推进。两者要回答的问题相同:哪些数据能提前算,哪些状态必须按顺序更新,多出来的中间结果放在哪里,为此占用的片上空间又是否划算。
本文假设读者了解 FlashAttention-2 的 dense 前向计算和在线 softmax,讨论范围限于单个 CTA(CUDA 线程块)内的流水线。硬件背景以 Hopper 和 Blackwell 的异步矩阵指令为主,片上存储的例子按 Blackwell 的口径给。文中的 softmax 阶段包括 max、exp、sum、类型转换和 P 的交接,exp 只是其中一段。时间线里的格子是相对时间单位,字节数是布局算例,都不是某个 kernel 的实测值。
一、一轮 attention 的工作量与硬件分工
一次 QK、一次 PV、一段 softmax
固定一个 query 块 Q,沿序列方向依次读 K₀/V₀、K₁/V₁……。这里的 tile 是矩阵块;“单 Q”指一个 query 块,可以包含 128 个 token。
第 j 轮的主要工作是:
m 是每行的运行最大值,α 是最大值变化时对已有累加结果使用的重缩放系数。U 是尚未归一化的输出累加器;ℓ 是这一行已处理的全部 KV 块的指数和,以当前 m 为基准在线累积并重缩放,最后用 O=U/ℓ 逐行归一化。
下文把 QKᵀ 简写为 QK,把 P×V 简写为 PV。
QK 与 PV 都落到 MMA(Matrix Multiply-Accumulate,矩阵乘加)上。softmax 不同,它由逐元素运算、最大值与求和归约、指数计算组成,跑在通用运算单元和 SFU 上。
指数一般用 exp2 实现,因为 exp(x)=2^(x·log₂e),硬件指数路径通常编译到 MUFU.EX2。MUFU/SFU 是执行这类特殊函数的单元,吞吐上限和 Tensor Core 完全不同。不过一段源码里的 exp 未必只对应一条机器指令,具体数学函数可能额外包含几条。FlashAttention-3 对硬件分工的解释、PTX 的 ex2 定义
运算量差 512 倍,时间却可能一样
设 Q、K、V 的块尺寸都是 128×128,head dimension D=128,一次乘加按 2 FLOPs 计:
| 每轮工作 | 运算量 |
|---|---|
| QK | 2 × 128³ = 4,194,304 FLOPs |
| PV | 2 × 128³ = 4,194,304 FLOPs |
| 逐元素 exp | 128² = 16,384 次 |
两次 MMA 的 FLOPs 是 exp 次数的 512 倍。这个比值本身没有多少意义,FLOPs 和指数次数不是同一种量,能比的只有各自除上硬件吞吐之后的时间。
按 FlashAttention-4 设计说明中的 B200 每 SM 理想吞吐口径,BF16 矩阵运算为 8,192 FLOPs/cycle,指数为 16 次/cycle。把上面的工作量除下去:
两次 MMA 合计约 1024 周期,指数运算也约 1024 周期,量级上正好相当。这个估算只做了工作量除以峰值吞吐,没有计入访存、归约、重缩放、同步和指令调度,不能当实际耗时。FlashAttention-4 的吞吐分析
低精度优化里的一个现象可以由此理解:MMA 加快之后,exp 的时间未必同比缩短,它在总耗时中的占比反而上升。
二、串行安排:两类硬件轮流空闲
最直接的写法是每轮做完 QK 做 softmax,做完 softmax 做 PV,然后进入下一轮。按上一节的比例,一轮里 QK 与 PV 合起来约等于一段 softmax 的长度,把每次 QK、PV 各记 1 格,E 记 2 格,轮流出空的位置就看得出来。
图 2 里 E₀ 执行期间,Tensor Core 在等 P₀。但 QK₁ 只需要 Q 和 K₁,不需要 P₀ 或 U₀,本来可以提前算出来。
要让这种提前发生,还需要异步指令。Hopper 的 wgmma.mma_async、Blackwell 的 tcgen05.mma 属于这一类:发起矩阵运算之后,线程可以继续推进其他允许的工作,到读取结果时再确认完成。源码里把两段代码相邻写出并不会自动带来并行,这些异步能力才是前提。FlashAttention-3 论文的异步流水线、PTX 的异步 MMA 完成机制
有了异步指令,还要有人把不同的工作分头推进。常见做法是 Warp Specialization(Warp 分工):不同 Warp 或 Warp 组分别负责加载、发起 MMA、处理 softmax 和校正输出,通过缓冲区和屏障交接数据。这样同一个 SM 上,异步矩阵硬件和处理逐元素运算的线程就能交错工作。Hopper 与 Blackwell 在发射粒度、分组方式和存储位置上都有差别,不能把一代的角色分配直接搬到另一代。
图中的 E 一直是完整的分数处理阶段:从可以读取的 S,到可以交给 PV 的 P。能重叠的是这一段工作与另一个 tile 的 MMA;其中 exp、归约、类型转换和写 P 各能藏起来多少,取决于实际调度和资源争用。
只要 S 有第二处可写,QK₁ 就能在 E₀ 期间启动。剩下的问题是怎么管这个多出来的槽,以及输出状态要怎样跟着更新。
三、单 Q 双 S:让下一轮的分数提前算
两份 S,一个 Q
如果只留一个 S 槽,softmax 还在读 S₀ 时,QK₁ 没有地方写新结果。再加一个槽,问题就解决了。
单 Q 双 S(single-Q + S ping-pong)保留一个 Q、两份逻辑分数缓冲、一套在线 softmax 与输出状态。两个槽交替接收相邻轮次的分数:
下面按一种保守实现来画:S 和 P 复用同一区域,必须等 PV 完成才能释放槽。
按图走一遍:
- QK₀ 写槽 A,完成后 softmax 开始处理。
- softmax 处理槽 A 时,QK₁ 写槽 B。
- P₀ 就绪,做 PV₀;softmax 转去处理槽 B。
- PV₀ 用完槽 A,QK₂ 才能写回槽 A。此后不断交替。
重叠发生在 E₀ 与 QK₁ 之间,以及 E₁ 与 PV₀、QK₂ 之间。同一轮的 QK、E、PV 仍然要守数据依赖。
分数可以早算,输出必须按顺序校正
QK₁ 能早算,但如果它带来更大的行最大值,之前累积的结果就要重新缩放:
这些运算逐行进行,α 也按行广播。举一行的例子:旧最大值是 2,新最大值变成 4,旧分母和旧输出累加器都要乘 exp(-2),才能和本轮贡献处在同一尺度上。
所以双 S 放宽的只是分数缓冲的读写冲突,max、sum 和输出累加器各自保留依赖链。普通有限分数可以从第一块初始化;整行都被 mask 的情况要单独处理,避免 -∞-(-∞) 产生 NaN。
槽什么时候能还回去
实现里需要能表达三件事:QK 已写完 S,softmax 已写好 P,PV 已用完 P。如果 S 和 P 共用同一区域,让 softmax 读完 S 就释放整个槽,下一次 QK 会覆盖 PV 还没用完的 P。
协议可以概括成三句话:写之前等空闲,读之前等就绪,用完之后再归还。循环使用时还要区分槽里的数据属于第几代,常见做法是维护 phase。初始值和屏障怎么用,取决于所用的流水线 API,不能只照抄 j % 2。
Blackwell 上还要分清 MMA 的提交与完成。tcgen05.commit 配合 barrier 跟踪异步操作的结束,访问结果时必须满足对应的可见性规则。PTX 的 tcgen05 同步规则
四、双 Q:两条独立的计算链
单 Q 双 S 只在同一个 Q 的前后轮次之间找重叠。双 Q(dual-Q)换一个方向:让一个 CTA 同时处理 Qᴬ、Qᴮ 两个 query 块,各自保留 m、ℓ、U。
例如 Qᴬ 对应第 0~127 行 query,Qᴮ 对应第 128~255 行。dense 非因果 attention 里,它们遍历同一段 KV,但两组分数按各自的行归一化,最后分别写出 Oᴬ 和 Oᴮ。
Eᴬ₀ 运行时,Tensor Core 可以算 QKᴮ₀;Eᴮ₀ 运行时,又可以做 PVᴬ₀ 和 QKᴬ₁。A 路的输出缩放不依赖 B 路的最大值,B 路也不必等 A 路走完全部 KV 才开始。
与单 Q 双 S 相比,双 Q 多出一套独立的输出状态,给调度留了更多选择。dense 情况下,同一块 K/V 如果留在片上供两路共用,还可能省掉一次重复加载;能不能真的复用,要看搬运顺序、布局和缓冲的生命周期。
FlashAttention-4 公开的前向设计就采用每 CTA 两个 Q tile 的 ping-pong,并配一个独立的校正阶段处理输出重缩放。它还会协调两组 softmax 的 exp 阶段,减少对同一个指数单元的争抢。这里利用的是 exp 与 MMA 的互补;反过来,两路 exp 同时跑,也会竞争同一份吞吐。FlashAttention-4 的前向流水线
两种设计可以叠加,但要付出资源代价
| 对比项 | 单 Q 双 S | 本文图示的双 Q |
|---|---|---|
| 一个 CTA 负责的 query 块 | 1 个 | 2 个 |
| 可交错的工作来自哪里 | 同一 Q 的不同 KV 轮次 | 两个 Q 的工作交错 |
| 逻辑分数槽 | 同一 Q 使用 A/B 两槽 | 每个 Q 各一槽 |
| 在线 max/sum/输出状态 | 一套,按 query 行维护 | 两套,各自按行维护 |
| 主要资源增量 | 额外的 S/P 在途数据 | 额外的 Q、输出和状态 |
“几个 Q”和“每个 Q 有几级 S 缓冲”是两个独立的参数。双 Q 也可以再给每个 Q 加分数缓冲,代价是更多片上存储和同步。所以只凭名字判断不了某个 kernel 的流水线深度。
除了计算,还有搬运
TMA(Tensor Memory Accelerator,张量存储加速器)能把后续的 K/V 块异步送进片上,搬运本身也可以和计算重叠。把这几类放在一起,各自的作用就清楚了:
| 可重叠的阶段 | 为什么有机会并行 | 仍要满足的条件 |
|---|---|---|
| 本轮 softmax 与下一轮 QK | 下一轮 QK 不依赖本轮 P | K 已到达,且有空闲 S 槽 |
| 本轮 softmax 与上一轮 PV | 上一轮 P 已就绪 | 上一轮输出校正已完成,输入未被覆盖 |
| TMA 预取后续 K/V 与当前计算 | 搬运走异步数据通路 | 有空缓冲,计算前确认传输完成 |
| 一路输出校正与另一路 MMA,或后续 QK | 操作的是不同数据区域 | 校正与逐元素运算仍可能争资源 |
前两行是前面几条时间线的主角。后两行同样影响实际吞吐,但 TMA 预取属于搬运与计算的重叠,单 Q 双 S 和双 Q 改变的只是计算之间可交错的部分。输出校正和 PV 如果访问同一份 U,仍要按图 5 的依赖关系排队。
这里的双 Q 都在一个 CTA 内,和前文的 2CTA 矩阵协作 是两类不同的组织方式。Warp 与 SM 的对应关系可以参看 CUDA 执行模型。
五、多一条计算链的代价
双 Q 多留了一份 Q、一份输出暂存和一套在线状态,同时把两路 query 绑在同一个 CTA 里。两件事都会反过来影响吞吐。
片上空间会压缩预取深度
寄存器保存线程正在操作的分数片段、max、sum 和地址;SMEM(共享内存)常用来放 Q/K/V 和输出暂存;Blackwell 的 TMEM(Tensor Memory)存放矩阵累加结果,也可以承接 S/P。这三处各有容量上限,不能互相顶替。
先算一笔简化的 SMEM 布局预算。取 128×128 的 FP8 输入块和 BF16 输出暂存,假设可用于这部分布局的空间是 224 KiB,每个 query 块占 16 KiB,每份输出暂存占 32 KiB,每个 K 或 V 缓冲槽占 16 KiB:
双 Q 多了可交错的计算,却可能把 KV 预取距离从 11 个槽压到 8 个。如果 MMA 此后更频繁地等数据,计算重叠省下的时间就会被抵消。
TMEM 也要算。按每块 128 列的逻辑布局,单 Q 双 S 的 S₀ + S₁ + U 占 384 列,双 Q 的 Sᴬ + Sᴮ + Uᴬ + Uᴮ 占 512 列,都假设 P 复用 S 的区域。常规、非 exclusive 的 TMEM 分配按 2 的幂申请列数,384 列的布局也可能申请到 512 列。把剩余的逻辑空间利用起来当然可以,但那会带来额外的寄存器、SMEM 或同步开销。PTX 的 TMEM 分配规则
并行度集中到更少的 CTA
一个 CTA 处理两个 Q,覆盖同样一批 query 需要的 CTA 数大约减半。batch 小、query 总块数本来就不多的时候,GPU 可能凑不出足够的任务铺满 SM。寄存器、SMEM 或 TMEM 用量上升,还会压低同时驻留的 CTA 数。
资源预算变小,不等于一定发生寄存器溢出。活跃值会不会被写到 local memory,要看编译产物和性能计数,不能因为用了双 Q 就下结论。
两路长度不同,会多出空轮次
不加 mask 的时候,两路遍历的 KV 块数相同。一旦有 causal mask、变长序列或稀疏清单,长度就可能不一样。
比如 A 路有 3 个有效 KV 块,B 路只有 1 个。实现如果为了配对而让两路都跑 3 轮,4 个有效配对就变成 6 个实际配对。被 mask 掉的额外计算数学贡献为零,却照样花掉加载和 MMA 时间。能跳过多少,取决于实现。
所以双 Q 应该和已经优化过的单 Q 双 S 比,并为两者分别选合适的 tile、stage 和资源配置。只赢过完全串行的基线,不足以说明它优于现有的单 Q 流水线。
六、重叠最多能省多少
把一轮里两次 MMA 占 Tensor Core 的时间记作 Tₘ,softmax 占相关运算单元的时间记作 Tₑ,其他成本暂时忽略。完全串行需要:
如果两类工作能在不同 tile 之间充分重叠,稳态下的理想处理间隔只能接近:
两边各需 1 个时间单位时,理想间隔从 2 降到 1;MMA 需要 1、softmax 需要 3 时,充分重叠也只能把 4 降到 3,剩下的要靠减少 softmax 的工作量或提高它的有效吞吐。
图 4 和图 6 画的工作量相同,都是四个 Q-KV 配对、合计 16 格有效工作,各自走完用了 10 格。离 2 倍还差的那部分,是模型里的启动、排空和数据依赖。
这个“最多 2 倍”只属于“固定工作量、两类阶段、与完全串行相比”这一个模型,没有算 KV 复用之类的其他优化,也不是从单 Q 双 S 换到双 Q 的加速承诺。真实 kernel 还要付出数据移动、校正、屏障等成本,资源变化又会改变这些成本。
要判断一个实现有没有真的受益,可以依次看三件事。先看结果对不对:和参考输出比对,覆盖多个 KV 轮次、尾块、mask 和极端分数,确认每个 Q 的 max、sum 和输出都正确。再看等待有没有减少:把 Tensor Core、指数路径、访存和屏障的等待分开统计,确认省下的时间没有转移到别的瓶颈上。最后看整个算子:固定 GPU、dtype、形状和精度要求,用设备计时重复测量,比较中要包含加载、归一化和最终写出。
单 Q 双 S 让一个 query 块的下一轮分数提前就位,双 Q 再多开一条独立的 query 计算链。两者起作用的前提相同:一类硬件卡在当前依赖上时,手边恰好有另一份输入齐备的工作。收益多少,取决于这些工作能不能及时就绪,以及把它们留在片上要花多少空间。
技术资料核对日期:2026 年 10 月 28 日。图示均为本文绘制;吞吐参数取自文中链接的公开设计说明,其余数字都是在文中假设下的估算。