4、FlashAttention算法细节:前向与反向的tiling策略、online softmax、累积更新与数值稳定性
FlashAttention的核心思想并非对注意力计算本身做数学近似,而是通过精细的分块(Tiling)策略与在线(Online)计算范式,在不损失精度的前提下,将注意力矩阵的计算与存储开销从显存转移到计算单元(SRAM/寄存器)中。这一章我们将深入剖析其前向传播与反向传播中的分块细节,重点讲解online softmax的数学原理、累积更新的实现方式,以及如何通过算法设计保证数值稳定性。理解这些细节,是后续进行算子融合、内核调优和推理部署优化的基础。
4.1 核心原理:从标准注意力到分块计算
标准注意力计算中,对于长度为 N 的序列,需要先构造 N×N 的注意力分数矩阵 S 和概率矩阵 P,再与值矩阵 V 相乘得到输出 O。这一过程在长序列场景下会产生 O(N²) 的显存开销,且 HBM(高带宽内存)访问量同样为 O(N²)。FlashAttention 的出发点非常直接:既然 GPU 的 SRAM 容量远小于 HBM,但带宽远高于 HBM,那么能否将计算切分成多个块,让每个块的计算完全在 SRAM 内完成,从而避免中间矩阵的 HBM 往返?
答案是可以,但需要解决一个关键数学问题:softmax 的归一化分母(即 softmax 的求和项)依赖于整个行的所有元素,而分块计算时,每个块只能看到部分列。如果简单地对每个块独立计算 softmax,再拼接结果,会导致归一化不一致。这正是 online softmax 要解决的难题。
4.2 前向传播的 Tiling 策略与 Online Softmax
4.2.1 分块参数与循环结构
设序列长度为 N,头维度为 d,SRAM 可用容量为 M。前向传播将 Q 划分为大小为 B_r×d 的行块,将 K、V 划分为大小为 B_c×d 的列块。典型取值为 B_r = 64 或 128,B_c = 64 或 128,具体取决于 SRAM 大小和 d 的取值。外层循环遍历 Q 的行块,内层循环遍历 K/V 的列块,每次内层迭代处理一个 (Q_i, K_j, V_j) 三元组。
对于每个内层块,计算流程如下:
- 计算分数矩阵:S_ij = Q_i · K_j^T,形状为 B_r × B_c。
- 更新行最大值:m_new = max(m_i, rowmax(S_ij)),其中 m_i 是之前所有块累积的行最大值。
- 更新归一化项:l_new = l_i · e^(m_i - m_new) + rowsum(e^(S_ij - m_new))。
- 修正输出累积:O_i = O_i · e^(m_i - m_new) + e^(S_ij - m_new) · V_j。
- 更新状态:m_i = m_new,l_i = l_new。
内层循环结束后,将 O_i 除以 l_i 得到最终输出。整个过程只需要在 SRAM 中维护 O_i(B_r×d)、m_i(B_r)、l_i(B_r)以及当前块的 S_ij(B_r×B_c),无需将任何中间矩阵写回 HBM。
4.2.2 Online Softmax 的数学推导
标准 softmax 对第 i 行的定义为:P_i = softmax(S_i) = e^(S_i - m) / Σ_j e^(S_j - m),其中 m = max_j S_ij。当数据被分块后,我们无法一次性获得全局最大值 m。Online softmax 的策略是:维护一个运行中的最大值 m_i 和归一化项 l_i,每处理一个新块,就用当前块的最大值去更新全局估计,并对之前累积的输出进行缩放修正。
具体推导如下。假设已经处理了前 j-1 个块,累积的最大值为 m_old,归一化项为 l_old,输出为 O_old。现在处理第 j 个块,得到当前块的行最大值 m_cur = rowmax(S_ij)。新的全局最大值为 m_new = max(m_old, m_cur)。此时,之前累积的每个 exp 项都需要乘以 e^(m_old - m_new) 进行缩放,而当前块的 exp 项则使用 e^(S_ij - m_new)。因此:
l_new = l_old · e^(m_old - m_new) + rowsum(e^(S_ij - m_new))
O_new = O_old · e^(m_old - m_new) + e^(S_ij - m_new) · V_j
这一修正保证了无论分块顺序如何,最终结果与一次性计算完全一致。值得注意的是,缩放因子 e^(m_old - m_new) 始终小于等于 1,因为 m_new ≥ m_old,这从数值上避免了指数爆炸。
4.2.3 数值稳定性设计
FlashAttention 的数值稳定性体现在三个层面:
- 最大值减除:每个块计算 exp 前都减去当前全局最大值 m_new,确保 exp 的输入 ≤ 0,从而避免浮点溢出。
- 缩放修正的单调性:由于 m_new 单调不减,缩放因子始终在 (0, 1] 区间,不会引入额外的大数。
- 浮点累加顺序:l 和 O 的更新采用累加方式,与标准 softmax 的求和顺序不同,但误差在浮点精度范围内可接受。
4.3 反向传播:重计算策略与梯度流
4.3.1 为什么需要重计算
标准反向传播需要保存前向的 S 和 P 矩阵来计算梯度,这同样需要 O(N²) 显存。FlashAttention 的反向传播采用重计算(Recomputation)策略:不保存 S 和 P,而是在反向传播时重新加载 Q、K、V 块,重新计算 S_ij 和 P_ij,然后计算梯度。虽然增加了计算量,但避免了 O(N²) 的显存开销。
反向传播的梯度公式如下(省略缩放因子):
dV_j = P_ij^T · dO_i
dP_ij = dO_i · V_j^T
dS_ij = P_ij ⊙ (dP_ij - rowsum(dP_ij ⊙ P_ij))
dQ_i = dS_ij · K_j
dK_j = dS_ij^T · Q_i
其中 ⊙ 表示逐元素乘。注意 dS 的计算中包含了 softmax 的 Jacobian 修正项 rowsum(dP ⊙ P),这是 softmax 反向传播的标准形式。
4.3.2 反向传播的 Tiling 与内存访问
反向传播同样采用分块策略,但循环顺序与前向相反。外层循环遍历 K/V 的列块,内层循环遍历 Q 的行块。这样设计的原因在于:对于每个 K_j 块,需要累积所有 Q_i 块对 dK_j 和 dV_j 的贡献。每个块的计算流程为:
- 从 HBM 加载 Q_i, K_j, V_j, dO_i 以及前向保存的 m_i, l_i。
- 重算 S_ij = Q_i · K_j^T,P_ij = exp(S_ij - m_i) / l_i。
- 计算 dV_j 和 dP_ij。
- 计算 dS_ij,进而得到 dQ_i 和 dK_j。
- 将 dQ_i 和 dK_j 累积到 HBM 中的对应位置。
反向传播的 HBM 访问量与前向相同,均为 O(N²·d²/M)。但计算量约为前向的 2 倍,因为需要重算 S 和 P。
4.4 案例拆解:长序列推理场景下的显存与速度优化
假设我们有一个 类模型,序列长度 N = 8192,头维度 d = 128,头数 H = 32,batch size B = 8。在标准注意力实现中,单个头的 S 矩阵大小为 8192×8192×4 字节 = 256 MB,P 矩阵同样为 256 MB,加上中间梯度,显存占用轻松超过 1 GB。对于 8 个 batch 和 32 个头,仅注意力中间变量就需要约 256 GB,远超任何单卡显存。
采用 FlashAttention 后,假设 SRAM 容量 M = 192 KB(A100 的典型值),取 B_r = 128,B_c = 128。前向传播中,每个块需要存储:Q_i (128×128×4 = 64 KB)、K_j (64 KB)、V_j (64 KB)、S_ij (128×128×4 = 64 KB)、O_i (64 KB) 以及 m、l 向量(约 1 KB)。总计约 320 KB,略超 SRAM,因此实际实现中会调整 B_r 或 B_c 为 64。最终显存占用仅为 O(N·d) 级别的输出和统计量,约 8192×128×4×32×8 ≈ 1 GB,完全可接受。
在实际推理部署中,我们曾对一个 7B 模型进行长序列生成测试。使用标准注意力时,序列长度超过 4096 即出现 OOM;改用 FlashAttention 后,序列长度可扩展至 16384,且单 生成延迟降低了约 35%。这主要得益于 HBM 访问量的减少——在长序列下,标准注意力的 HBM 访问量随 N² 增长,而 FlashAttention 的增长更接近线性。
4.5 常见误区与总结复盘
| 误区 | 正确理解 | 实际影响 |
|---|---|---|
| 认为 FlashAttention 是近似算法 | 数学上等价于标准注意力,仅浮点累加顺序不同 | 精度损失在 1e-6 量级,可忽略 |
| 认为分块越大越好 | 分块大小受 SRAM 容量限制,且需考虑 bank conflict | 过大的块导致 SRAM 溢出或性能下降 |
| 忽略反向传播的重计算开销 | 反向计算量约为前向的 2 倍 | 训练吞吐下降,但显存节省显著 |
| 认为 online softmax 只适用于注意力 | 任何需要分块计算 softmax 的场景均可使用 | 可推广到 MoE、多模态融合等场景 |
| 忽视数值稳定性细节 | m 的更新必须单调不减,l 的更新需使用缩放因子 | 错误实现会导致 NaN 或精度严重下降 |
总结复盘:FlashAttention 的成功在于将算法设计与硬件特性紧密结合。Tiling 策略解决了 SRAM 容量限制,online softmax 解决了分块归一化问题,重计算策略解决了反向传播的显存瓶颈。三者缺一不可。在实际工程中,还需要关注以下细节:
- Bank Conflict 优化:在 GPU 上,S_ij 的访问模式可能导致共享内存 bank conflict,需要通过 padding 或调整数据布局来规避。
- 向量化加载:Q、K、V 的加载应使用 float4 等向量化指令,提高内存带宽利用率。
- Warp 级编程:每个 warp 处理一行或一列,需要精心设计线程映射以减少同步开销。
- 与算子融合的协同:FlashAttention 常与 causal mask、alibi 等结合,这些操作可以在计算 S_ij 时直接融入,无需额外 kernel。
理解这些细节后,你不仅能够正确使用 FlashAttention,还能针对特定硬件(如昇腾、寒武纪)或特定模型结构进行定制化优化。下一阶段,我们将探讨如何将 FlashAttention 与 KV Cache 管理、分页推理等技术结合,进一步释放长序列推理的潜力。