想象一家小有名气的餐厅「Flash 小馆」,主厨有个怪癖:他从不给菜品拍照片。每道菜做好端上桌,配料、火候、用量就全凭脑子里的两行账——本桌的「最高分」和「总分」。客人如果说不满意,主厨不慌不忙,照着小账本把菜重新做一遍,边做边估摸每种调料到底放多放少。这套「不存档、现做现算」的做法,正是 Flash Attention 反向传播的精髓。 大模型的注意力机制,本质上就是让每个词(顾客)看看其他所有词(菜品)「有多投缘」,再把相关内容按投缘程度加权汇总。标准做法会把所有「投缘分数」存成一张 N×N 的大表,反向传播时再翻出来用;而 Flash Attention 只在计算时临时用一用,转头就扔,只留下 m(最高分)和 l(总分)两行台账。当误差信号回流时,它按台账把注意力权重「重建」出来,再分块给 Q、K、V 三位调味师算账。这就是重计算、分块与数值稳定三者的精妙合奏。
读懂它,你就能把注意力训练的显存占用从 O(N²) 降到 O(N),是手写高性能注意力 kernel、训练超长上下文大模型的必修课。
可视化演示
展示FCFS、SJF、Round Robin三种CPU调度算法的执行过程,通过甘特图对比不同调度策略的等待时间和周转时间。
本节配有交互动效(进入视口后自动加载,约 3KB)。
FCFS先来先服务调度 甘特图展示进程执行顺序,每个色块代表一个进程的CPU占用时段 下方圆点表示进程队列,高亮为正在执行的进程 FCFS按到达顺序调度,简单但可能导致短作业等待 等待时间 = 开始时间 - 到达时间 FCFS是非抢占式调度,按进程到达顺序依次执行
显存困境
传统后厨的规矩是「做一道、拍一张」。每道菜出锅,立刻有人举着相机拍证件照,照片按桌号、菜名归档进仓库。客人 N 位,菜品 N 道,两两之间的「合拍程度」都要留档,于是仓库里堆了 N² 张照片。当 N 从几千涨到几万甚至几十万,仓库立刻爆炸——这就是标准 Attention 反向传播的隐忧:它必须保存完整的注意力矩阵 P。
为什么反向传播非要这些照片?因为误差信号回流时,需要知道「当初这桌顾客给每道菜投了多少关注」,才能把梯度公平地送回 Q、K、V。于是 P 矩阵不仅前向要用,反向还要原封不动地从显存里再读一遍。对一张 8192 长度的序列,P 矩阵就有 8192×8192 个元素,仅这一张「照片」就要占掉上百兆字节的显存。
如果不想让仓库爆掉,就不能按老规矩存照片。Flash 主厨决定:照片不拍了,只记两行台账。
- 打分矩阵 S先让 Q 与 K 做内积,衡量每个顾客和每道菜的「投缘分」,除以 √d 防止维度变大方差过大。
- softmax 归一化对每一行做 softmax,把投缘分变成「好感占比」,总和为 1。
- 存储规模结果是一个 N×N 的稠密矩阵,显存随序列长度平方级增长。
前向台账
Flash 主厨发现,客人真正在意的是「哪道菜最合口味」和「所有菜的总分值」。于是每到一桌,他只在账本上记两行数:m_i 表示本桌目前的最高评分,l_i 表示把分数换算成「好感指数」后的总分。至于每道菜具体得了多少好感分?算完就忘。
这正是 FlashAttention 前向在做的事:把序列切成小块,逐块计算评分 S,用当前块的最大值更新 m,用 e^{S-m} 的和更新 l,同时把输出 O 的累积器更新掉。P 矩阵本身不落显存,只在片上缓存里短暂停留。反向传播需要的全部「档案」,就只有 m 和 l 这两行。
有了这两行台账,反向传播时就能像按菜谱重做一样,把 P 完完整整地重建出来。
\displaystyle m_i=\max_{j}s_{ij},\qquadl_i=\sum_{j}\exp(s_{ij}-m_i),\qquadP_{ij}=\exp(s_{ij}-m_i)- 行最大值 m记录本桌评分的最高分,它是后续数值稳定的保险丝。
- 未归一化权重 P每个评分减去最高分后再取指数,得分再高也不会突破 e^0=1。
- 行和 l把这一行的所有指数求和,反向传播时要靠它把 P 还原成真正的 softmax 概率。
反向总览
现在客人投诉了(误差 dO 传入)。Flash 主厨的应对分两轮:第一轮,按台账把菜重新炒一遍——重算每个分块的 P;第二轮,一边重炒一边给三位调味师记「用量偏差」——Q 管顾客口味,K 管菜品特征,V 管菜品内容。
反向传播遵循链式法则:从输出梯度 dO 出发,先还原 P(经由 softmax 梯度 dS),再沿着 S=QK^T/√d 这条线把梯度分别推到 Q、K,同时把 dV 直接从 P^T dO 得到。整个过程不需要翻任何照片,只需要 Q、K、V 本身和两份台账。
三位调味师各拿一张「用量偏差单」,但这单子不是从照片里找出来的,而是现场重算出来的。
- 重算概率 P̂用台账 m、l 和分块评分 S,重建归一化注意力权重。
- dQ 回推把 dS 与 K 块相乘,再乘缩放因子,得到 Q 的梯度片段。
- dK 回推把 dS 转置后与 Q 块相乘,得到 K 的梯度片段。
- dV 直达dO 经由 P̂ 的转置直接回流到 V,不需要经过 softmax 梯度。
Softmax梯度
要算账,先要知道「这一桌的注意力分布误差」是如何经过 softmax 被放大的。softmax 的本质是归一化,归一化有个特点:某一项变高,其他项必然被压低。这种「此消彼长」的连带效应,正是反向传播中最容易算漏的部分——你不能只盯着某一项自己的梯度,还要算上它对整行归一化的挤压力。
为此 FlashAttention 反向先算一个行辅助量 D_i,它代表「输出误差与输出本身的点积」。原始的 dP=dO V^T 只考虑了「输出直接依赖 P」这一条路径,而 D_i 捕捉了「分母 l_i 随 P 变化」的间接路径。两者结合,才能得到对 S 的真实梯度 dS。
这步就像先尝一口整桌菜的平均口味,再判断每一道菜该加盐还是加糖,避免把「归一化的连带影响」算错。
- 行辅助量 D把输出梯度 dO 与输出 O 逐元素相乘再按行求和,衡量归一化分母的连带影响。
- 原始梯度 dPdO 与 V 做矩阵乘,得到「输出直接依赖 P」的梯度。
- 修正与加权用 D 修正 dP,再乘上归一化权重 P̂,得到对打分 S 的完整梯度。
QKV梯度
有了 dS,就能给三位调味师各写一张偏差单。主厨在小小的灶台(SRAM)上一次只能同时处理几个分块,所以他的办法是:外层循环固定一个 K/V 块,内层循环扫描所有 Q 块,把当前块的梯度累加到对应的全局账页上。
分块计算的本质是「流式累加」:每个 (i,j) 块算出的 dV、dQ、dK 片段,立即加到全局梯度矩阵的对应位置。因为每个块只依赖重算出的 P 块,不需要看到全表,所以片上内存始终只放一个小分块,显存里只需要维护最终的梯度累积器。
就这样,一桌一桌地算,最后三张完整的「用量偏差单」就诞生了。
- dV 累加每个 Q 块都会向同一个 K/V 块的 dV 贡献一份梯度,因此用加等号累加。
- dQ 累加遍历所有 K 块时,同一个 Q 块的 dQ 从多个 K 块收集梯度。
- dK 累加同理,同一个 K 块从多个 Q 块收集梯度。
数值稳定
台账上那个「最高分 m」还有一个救命用途:防爆表。假设某道菜的好感分高达 1000,直接算 e^{1000},任何浮点数都得爆成 inf,整个反向传播立刻坍塌成一堆 NaN。这不是理论风险——长序列训练中,早期未收敛的模型完全可能产生很大的 logits。
FlashAttention 的解决方式无比朴素:所有指数都减去本行最大值 m_i,让指数输入落在 (-∞,0] 区间,e^0=1 封顶,既不会溢出,精度也足够。关键在于:反向重算 P 时,必须使用前向记录的那一份 m,否则重算出的 P 和当初不一致,梯度就会失真。
所以那份台账不只是省显存,更是「安全重算」的保险丝。
- 重算指数反向分块时,用当前块评分 s_{ij} 减去台账中的 m_i 再做指数。
- 归一化除以台账中的 l_i,还原成与标准 softmax 完全一致的权重。
- 范围保证因为 m_i 是本行最大分,s_{ij}-m_i≤0,exp 结果必然落在 (0,1]。
算力对比
有人会问:重新做一遍菜,岂不是多花时间?是的,FLOPs 确实增加了一些——反向要多算一次注意力得分和指数,整体训练浮点运算大约增加三分之一。但省下的显存和 HBM 带宽是决定性的,因为显存带宽往往是训练真正的瓶颈。
标准 attention 需要把 N² 的 P 写入显存再读回;Flash 只把 O、m、l 写回,反向也是块内重算。显存从 O(N²) 降到 O(N),意味着序列长度可以成倍增长。对大模型训练而言,带宽就是生命线:少搬一次数据,往往比少算几个乘法更关键。
主厨用「多炒一遍菜」换来了「不用建仓库」,在大模型动辄百万 token 的今天,这笔账非常划算。
\displaystyle M_{\text{standard}}=O(N^{2}),\qquad M_{\text{flash}}=O(N);\qquadT_{\text{flash}}\approx 1.33\times T_{\text{standard}}- 标准显存完整 P 矩阵让显存随 N 平方增长,序列一长就触顶。
- Flash 显存只存 O、m、l 和梯度累积器,随 N 线性增长。
- 时间代价重算 P 使总 FLOPs 增加约三分之一,但不搬大矩阵反而快。
AI 应用场景
大模型训练工程师的显存急救
阶段:训练优化
解决的问题:训练 128K 上下文时,标准 Attention 的 N² P 矩阵会瞬间耗尽 HBM。FlashAttention 反向传播通过重算 P、只保留 O/m/l,把注意力显存从 O(N²) 降到 O(N),让超长序列训练在单卡上成为可能。
具体案例:一位工程师在训练 7B 模型时,上下文长度一直卡在 4K。改用基于 FlashAttention 反向传播的 kernel 后,长序列训练不再需要保存完整注意力矩阵,配合激活检查点,单卡即可扩展到 128K 上下文。
CUDA/Triton kernel 开发者实现自定义注意力
阶段:推理与部署优化
解决的问题:手写融合注意力 kernel 时,最大的难点不是前向,而是反向如何在分块框架下重建 softmax 中间结果。理解 m/l 台账和 dS 修正项后,开发者才能写出既省显存又数值稳定的反向 kernel。
具体案例:开发者在 Triton 中实现 Flash Attention 反向,每个 kernel 只加载一个 Q 块和 K/V 块,内部重算 P、累积 dQ/dK/dV,最终训练吞吐比标准 PyTorch 实现提升约 2-3 倍。
Python 演示
import numpy as np def flash_fwd(Q, K, V, block=2): # 分块前向: 只记录 m/l 台账, 不保存完整 P N, d = Q.shape O = np.zeros_like(Q) m = np.full(N, -1e9) l = np.zeros(N) scale = 1.0 / np.sqrt(d) for i in range(0, N, block): for j in range(0, N, block): s = (Q[i:i + block] @ K[j:j + block].T) * scale m_new = np.maximum(m[i:i + block], s.max(axis=1)) p = np.exp(s - m_new[:, None]) corr = np.exp(m[i:i + block] - m_new) l[i:i + block] = l[i:i + block] * corr + p.sum(axis=1) O[i:i + block] = O[i:i + block] * corr[:, None] + p @ V[j:j + block] m[i:i + block] = m_new return O / l[:, None], m, l def flash_bwd(Q, K, V, dO, block=2): # 反向: 分块重算 P, 累加 dQ/dK/dV N, d = Q.shape O, m, l = flash_fwd(Q, K, V, block) D = (dO * O).sum(axis=1) dQ = np.zeros_like(Q) dK = np.zeros_like(K) dV = np.zeros_like(V) scale = 1.0 / np.sqrt(d) for i in range(0, N, block): for j in range(0, N, block): s = (Q[i:i + block] @ K[j:j + block].T) * scale p = np.exp(s - m[i:i + block, None]) p = p / l[i:i + block, None] dV[j:j + block] += p.T @ dO[i:i + block] dp = dO[i:i + block] @ V[j:j + block].T ds = p * (dp - D[i:i + block, None]) dQ[i:i + block] += ds @ K[j:j + block] * scale dK[j:j + block] += ds.T @ Q[i:i + block] * scale return dQ, dK, dV def standard_bwd(Q, K, V, dO): # 标准反向: 保存完整 softmax, 用于对比验证 d = Q.shape[1] scale = 1.0 / np.sqrt(d) S = (Q @ K.T) * scale m = S.max(axis=1, keepdims=True) P = np.exp(S - m) l = P.sum(axis=1, keepdims=True) P = P / l O = P @ V D = (dO * O).sum(axis=1, keepdims=True) dP = dO @ V.T dS = P * (dP - D) dQ = dS @ K * scale dK = dS.T @ Q * scale dV = P.T @ dO return dQ, dK, dV def main(): rng = np.random.default_rng(0) N, d = 6, 8 Q = rng.normal(size=(N, d)) K = rng.normal(size=(N, d)) V = rng.normal(size=(N, d)) dO = rng.normal(size=(N, d)) dQ, dK, dV = flash_bwd(Q, K, V, dO, block=2) gQ, gK, gV = standard_bwd(Q, K, V, dO) print('dQ match:', np.allclose(dQ, gQ, atol=1e-9)) print('dK match:', np.allclose(dK, gK, atol=1e-9)) print('dV match:', np.allclose(dV, gV, atol=1e-9)) if __name__ == '__main__': main()
O, m, l = flash_fwd(Q, K, V, block)前向分块扫描,只返回输出和两行台账,P 不落显存。D = (dO * O).sum(axis=1)计算行辅助量 D,捕获 softmax 归一化的连带效应。p = np.exp(s - m[i:i + block, None])反向分块中用台账 m 重算未归一化权重,防止指数溢出。ds = p * (dp - D[i:i + block, None])用 D 修正原始梯度,得到对打分 S 的真实梯度。dQ[i:i + block] += ds @ K[j:j + block] * scale分块流式累加,每个 Q 块从所有 K 块收集梯度。print('dQ match:', np.allclose(dQ, gQ, atol=1e-9))与标准反向对比,验证分块重算的梯度完全一致。
常见误区
误区反向传播计算梯度时,softmax 的权重 P 必须完整保存在显存里,不然没法算 dV 和 dS。
正解FlashAttention 证明这是「照片依赖」。反向只需按 Q、K 和台账 m/l 重算每个分块的 P,算完即弃。就像主厨凭菜谱重做一遍菜,不需要当初的菜品照片。显存里始终只有一个小分块和梯度累积器,整体占用从 O(N²) 降到 O(N)。
为何常见:标准 Attention 的实现把 P 矩阵显式存下来,让人误以为这是数学上的必然,其实只是「存储换方便」的实现选择。
误区反向重算 P 要重新做一遍矩阵乘和 exp,太浪费了,不如直接存 P。
正解重算确实让 FLOPs 增加约三分之一,但大模型训练的瓶颈往往是 HBM 带宽而不是算力。省掉 N² 矩阵的写入和读回,节省的带宽远大于多算的浮点运算,实际训练反而更快。这是典型的「用计算换访存」。
为何常见:只看 FLOPs 数字会得出错误结论,忽略了显存带宽这个真正的瓶颈。
误区m 和 l 只是前向为了数值稳定和归一化临时用的,反向传播中没它们什么事。
正解反向重算 P 时,必须用前向保存的 m 减掉再取 exp,再用 l 归一化。如果不用同一份 m,重算出的 P 会与实际计算路径不一致,梯度就会出错;l 更是把未归一化权重还原成 softmax 概率的关键。
为何常见:前向推导中 m/l 看起来只是中间变量,容易忽略它们跨过了前向与反向的整个时间边界。
误区为了省显存而重算 P,得到的梯度应该和标准 Attention 有偏差,训练效果会打折。
正解只要重算时使用相同的 m/l 台账,反向的 dS、dQ、dK、dV 在数学上与标准 Attention 完全一致,不存在近似误差。分块只是改变计算顺序和存储位置,不改变数值结果。
为何常见:把「重计算」和「近似估计」混为一谈,其实重算是精确复现同一个中间量。
面试题
Q1. FlashAttention 反向传播时,需要重建以下哪个中间矩阵?
- A.完整 QK^T 打分矩阵
- B.归一化注意力权重 P← 正确答案
- C.嵌入层输出向量
- D.前馈网络权重矩阵
答案:B。反向传播算 dV 和 dS 都需要注意力权重 P。FlashAttention 不保存 P,而是用 m/l 台账重算出来。QK^T 打分矩阵是中间态,不需要整体重建。
Q2. FlashAttention 前向为反向留下了哪两条台账记录?
- A.行最大值 m 与行和 l← 正确答案
- B.列最小值与列和
- C.完整注意力矩阵
- D.梯度累积器数值
答案:A。m 是本行评分最大值,l 是指数求和。反向重算 P 时用 m 防溢出、用 l 做归一化,两者缺一不可。
Q3. 反向重算注意力权重时,为什么要先用 m_i 减去评分?
- A.防止 exp 溢出为无穷大← 正确答案
- B.加快矩阵乘法速度
- C.减少梯度的随机噪声
- D.让分块可以完全并行
答案:A。评分可能很大,直接 exp 会爆成 inf。减去本行最大值 m_i 后指数输入落在 (-∞,0],exp 结果不超过 1,从而保证数值稳定。
Q4. 反向传播中 D_i = sum(dO_i * O_i) 的作用是什么?
- A.计算输出误差的自相关
- B.捕获 softmax 归一化的连带效应← 正确答案
- C.缩放学习率
- D.更新 V 的梯度
答案:B。softmax 的归一化分母 l 随每个 P 变化,D_i 捕捉这条间接路径。修正 dP 后才能真正还原对 S 的梯度,避免只算直接路径而漏算连带效应。
Q5. FlashAttention 反向计算 dV 时,用哪个矩阵乘 dO?
- A.Q 与 K 的内积矩阵
- B.归一化注意力权重 P^T← 正确答案
- C.V 自身的协方差矩阵
- D.恒等单位矩阵 I
答案:B。O = P̂V,所以 dV = P̂^T dO。P̂ 是反向重算并归一化后的注意力权重,这一路不需要经过 softmax 梯度。
Q6. 相比标准 Attention,FlashAttention 反向因重计算导致训练 FLOPs 大约如何变化?
- A.几乎不增加
- B.减少约三分之一
- C.增加约三分之一← 正确答案
- D.增加整整一倍
答案:C。反向要多算一次注意力得分和 exp,整体训练浮点运算约增加三分之一。但因为省去 N² 矩阵的显存读写,实际训练往往更快。
Q7. FlashAttention 能训练超长序列的根本原因是什么?
- A.使用混合精度训练
- B.不保存完整 P 矩阵← 正确答案
- C.采用梯度累积策略
- D.减小了 batch size
答案:B。注意力显存从 O(N²) 降到 O(N) 的关键就是不再保存 P 矩阵,反向时按 m/l 台账重算。混合精度和梯度累积只是辅助手段,不是本质原因。
Q8. FlashAttention 反向传播需要哪些输入数据?
- A.Q、K、V 矩阵← 正确答案
- B.输出梯度 dO← 正确答案
- C.台账 m 与 l← 正确答案
- D.完整 P 矩阵
答案:ABC。反向重算 P 需要 Q、K 和 m,归一化需要 l,dO 是误差来源。完整 P 矩阵恰恰是 FlashAttention 刻意不保存的东西。
Q9. 关于 FlashAttention 反向传播的正确说法有哪些?
- A.P 矩阵在反向时重算← 正确答案
- B.显存占用依然 O(N²)
- C.使用分块流式累加梯度← 正确答案
- D.必须保存 S 矩阵
答案:AC。反向按分块重算 P,并把 dQ/dK/dV 片段累加到全局梯度矩阵,显存降为 O(N)。S 和 P 都不需要保存,只需台账 m/l。
Q10. 实现 FlashAttention 反向 kernel 时,数值稳定的关键处理包括哪些?
- A.前向记录行最大值 m← 正确答案
- B.反向重算时再次减 m← 正确答案
- C.用 l 归一化注意力权重← 正确答案
- D.将 exp 结果存入 float16
答案:ABC。m 贯穿前向与反向,保证重算的 P 与真实路径一致;l 负责把未归一化权重还原成 softmax 概率。float16 存储不是数值稳定的手段,反而可能丢精度。