老火是城里最有名的餐厅主厨,他有一道招牌菜叫“注意力炖菜”。这道菜的特殊之处在于:每一片食材都得和桌上所有其他食材打个照面、互相打分,才能炖出恰到好处的味道。可问题出在厨房布局上——老火的操作台(SRAM)只有巴掌大,而食材仓库(HBM,高带宽内存)又大又远。传统做法是让徒弟把全部食材一次性搬上桌,结果食材一多就堆不下,内存溢出;即便勉强放下,搬食材的时间也比做菜的时间多得多。 老火被逼着发明了一套“边做边搬”的绝活:每次只端一小盘食材上操作台,做成一小份半成品,再和锅里的主料精确融合——这成了 Flash Attention V1。后来他又让一排灶台同时开工、简化了翻锅动作——这是 V2。最后他换上了精度够用的快速秤、把备菜和炒菜改成流水线——这是 V3。今天,几乎所有大模型训练和推理都被这位主厨的智慧悄悄加速。
对训练与推理工程师而言,看懂 Flash Attention 的三代演进,等于拿到了一张定位性能瓶颈、优化显存与吞吐的通用地图。
可视化演示
展示Flash Attention通过分块Tiling将注意力计算从HBM移至SRAM的IO优化原理,以及V1/V2/V3三个版本的性能演进与内存优化对比。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+captions.tiling+'
背景与痛点
老火的招牌菜需要一张巨大的“食材关系表”:每一片食材要和桌上所有其他食材配对打分。过去,徒弟们从仓库把三箱原料(Q、K、V)全部搬到操作台,摊开算出整张表,再把表搬回仓库;要取用时又搬回来。操作台只有桌面大,食材稍微一多,桌子彻底堆满,这就是“内存溢出”。
这段故事映射的正是标准注意力。为了算出结果,必须先在显存(HBM)中构造并保存 N×N 的得分矩阵 S,再把 softmax 之后的概率矩阵 P 也存进 HBM。序列长度 N 从 512 涨到 4096、再到上万时,中间矩阵以 N² 的速度膨胀,显存很快爆掉。算一遍还要把 S 写一遍、读一遍、写 P、读 P,搬运量巨大。
老火盯着堆满桌面的食材意识到:真正该改的不是切菜手法,而是“搬运路线”。一次搬得再多,桌面就那么大;只有改变搬运策略,才能让这道菜真正做大。
- 查询与键点乘Q 中每一行与 K 中每一行做点积,得到 N×N 的得分矩阵 S,每一格表示“两个位置该不该互相注意”。
- 除以缩放因子除以维度 d 的平方根,防止点积数值过大把 softmax 推入饱和区、梯度消失。
- 行方向做归一化对每行做 softmax,让所有位置的关注度之和为 1,形成概率权重。
- 加权聚合值用归一化后的权重对 V 加权求和,得到最终输出 O。
核心思想
老火做了个实验:把一次搬一百种配料改成一次只搬十种,来回多走几趟,结果反而更快了。原来一次搬得多,路上堵车、堆在桌上也运转不开;只要一次搬“刚好算得完”的分量,勤搬反而高效。这就是“IO 感知”的直觉起点。
在 GPU 里,HBM 就是那个又大又远的仓库,容量几十 GB,但带宽有限;SRAM 是巴掌大的操作台,只有几十 KB,却带宽惊人。过去的算法把中间矩阵反复写回 HBM——等于在仓库门口排队。真正聪明的做法是:让计算紧贴着 SRAM 进行,把 HBM 只当作“源源不断的原料库”,而不是“临时中转站”。
IO 感知算法有三个具体抓手:第一是分块计算,只把需要的块搬上 SRAM;第二是算子融合,把 softmax 和矩阵乘合并成一个内核,中间结果不落 HBM;第三是重计算,反向传播时重新算一次得分,用计算换 IO。这三招后来成为 GPU 内核优化的通用方法论,也决定了 Flash Attention 每一代的设计走向。
老火在菜谱扉页上写下厨训:“最快的搬运,是少搬运。”这句训话成为三代改进的共同纲领。
V1分块原理
老火的新方案是“分盘备菜”:把整桌食材按一盘菜的份量分成小盘,每次只端一盘上操作台。做熟一小份后,马上和锅里已有的半成品精确拌匀;拌完再去端下一盘。这样操作台永远只放当下需要的一小盘,永远不会堆满。
在注意力计算里,这对应把 Q、K、V 按行切成小块:Q 切成若干 $B_r\times d$ 的小块,K、V 切成 $B_c\times d$ 的小块。每次只计算一小块得分 $S_{ij}$,再与之前累积的输出合并。只要块的大小能被 SRAM 装下,整张 N×N 的中间矩阵就永远不需要写进 HBM。
分块带来的最直接收益是显存:标准算法要保存完整的 N×N 中间矩阵,V1 只保存 N×d 的输出 O 和极小的运行统计量 m、l。HBM 访问量从 O(N²) 量级大幅下降,长序列训练终于有了喘息空间。
老火端出第一盘“V1 版炖菜”,发现桌面不再堆满,出菜速度还快了一倍多。但这只是开始——一个“看不见的调味难题”正等着他。
- 切分查询块把 Q 按行切成若干 $B_r\times d$ 的小块,每块只含 $B_r$ 个位置。
- 切分键值块把 K 和 V 按行切成 $B_c\times d$ 的小块,与 Q 块配对使用。
- 块内点乘得分小块之间的得分矩阵只有 $B_r\times B_c$,放在 SRAM 里绰绰有余。
在线Softmax
分盘备菜撞上了一个新矛盾:传统炖菜要求“所有食材到齐才能调味”,因为归一化必须知道全局的最大鲜度值。可老火现在一盘一盘地做,怎么知道后面会不会出现更鲜的食材?如果急着调味,后面鲜度更高的食材一来,前面不就白调了吗?
他灵机一动:边做边调。先按当前所见的最大值临时调味;如果后面来了更鲜的食材,就把锅里已经调好的部分乘以一个缩放系数“等比缩小”,再融入新食材。这就是在线 Softmax——始终维护三个状态:运行最大值 m、归一化分母 l、运行输出 o。每块到来时,用新块信息更新这三个状态。
这套在线更新在数学上与标准 softmax 完全等价:每当最大值更新,旧部分都被精确地乘以一个小于 1 的系数,而不是丢弃重算。V1 还在反向传播时“重算”得分矩阵,不保存 N×N 的 S——多花一点计算,换回大量 HBM 访问的节省。
老火把这招称作“看菜下锅、随时回勺”。不管后面的食材多惊艳,锅里的味道永远能精确对齐,绝不翻车。
- 当前块得分先算当前 Q 块与 K 块的得分矩阵,这个操作在分块基础上进行。
- 更新运行最大值取“历史最大值 m”和“当前块行最大值”中更大的一个,作为新的鲜度标尺。
- 更新运行分母旧分母按最大值的差值做指数缩放,再加上当前块的指数和。
- 更新运行输出旧输出同样缩放后,加上当前块对 V 的加权贡献。
V2并行优化
V1 的炖菜虽然不占桌面了,老火却看出新问题:主厨一次只盯一个锅,旁边灶台全空着;而且每端一盘菜回来,都要停下来把锅里的酱汁重新搅一遍(归一化),搅汁时所有人都在等。这是对 GPU 并行能力的巨大浪费。
V2 的解法是“多灶同开”:不再让一个线程块串行处理所有 K/V 块,而是让每个线程块独立负责一个 Q 块,各锅各炒、互不干扰。同时把“搅汁”从每盘都做的精细活改成“出锅时统一调味”——只在最终写回前做一次缩放,而不是在每个分块内反复做 rescaling。这一下就砍掉了大量非矩阵乘运算。
更进一步,V2 把 Q 的缩放从 kernel 内部挪到 kernel 外部预计算;把 mask 和 bias 合并进得分计算阶段。核心里剩下的几乎全是 GPU 张量核心最擅长的矩阵乘法。论文数据显示,在 A100 上 V2 相比 V1 实现了约 1.7~3.8 倍的加速。
老火看着一排灶台同时开火,总结道:以前是一锅一锅等,现在是一排一排炒。真正快的厨房,不是把动作做得更快,而是让所有灶台都在干活。
V3增强
到了第三代,老火换了新厨房——Hopper 架构 GPU。新厨房里添了一套“高精度快速秤”(FP8 张量核心):称料速度快一大截,刻度稍微粗一点点,但对做菜来说足够。更妙的是,他把整个备菜流程改成流水线:一个帮厨专管端菜,一个专管炒菜,一个专管把成品送出去,三人各忙各的,互不等候。
技术上,V3 用 FP8 做矩阵乘法输入,在 FP32 中累加,并配合精细的缩放因子管理数值范围——这是“快速秤加校验秤”的组合。同时,V3 用 warp specialization 实现异步流水线:一部分 warp(线程束)专门负责从 HBM 加载数据、用 TMA 异步写回结果,另一部分 warp 专心做矩阵计算,加载与计算完全重叠,GPU 等待内存的时间被压到最低。
V3 还把前向和反向统一成单一内核,减少了内核启动次数;针对 Hopper 的硬件特性重新分配了共享内存的读写节奏。在 H100 上,V3 相比 V2 的 BF16 版本能再提升约 1.5~2 倍的吞吐,刷新了注意力计算的速度纪录。
老火的新厨房里三个人同时开工,成品源源不断送出。他终于明白:当搬运无法避免时,让它和做饭同时发生,就是最好的等待。
性能对比
三代菜谱摊开看:标准做法是整桌食材一次搬上桌,桌面爆炸;V1 是分盘端菜,桌面清爽但厨师反复回勺;V2 是多灶同开、出锅统一调味,后厨火力全开;V3 是快速秤加流水线,产量再创新高。三代的共同点:菜谱(输出结果)几乎没变,变的只是搬运策略。
用数据说话:Flash Attention 几乎没有减少计算量——FLOPs 与标准注意力相同;真正的收益全部来自 HBM 访问量的下降。标准注意力要读写 O(N²) 大小的中间矩阵,而分块之后,每次只搬一个能装进 SRAM 的小块。
精度方面,在线 softmax 数学等价,实测误差通常在 1e-6 量级;V3 的 FP8 模式用 FP32 累加加缩放因子,也基本保持 BF16 质量。如今 Flash Attention 已内置于 PyTorch SDPA、HuggingFace Transformers、vLLM 等主流框架,普通开发者几乎无感地享受着这份加速。
老火把三年厨房账本挂上墙,写下结论:“算得少不如搬得少,搬得少不如搬得巧。”这就是 Flash Attention 的全部秘密。
- 计算量不变矩阵乘和 softmax 的浮点运算量(FLOPs)无论在标准版还是分块版里都是这个量级。
- 标准注意力的搬运量要保存并反复读写 N×N 的中间矩阵,外加 N×d 的 Q/K/V。
- Flash Attention 的搬运量中间矩阵不落 HBM,搬运总量由 SRAM 容量 M 决定,块越大搬得越少。
AI 应用场景
大模型长序列训练提速
阶段:模型训练
解决的问题:训练 128k 超长上下文时,标准注意力需要为每层构造 N×N 中间矩阵,显存随 N 平方爆炸,8 卡 H100 也常被塞满。Flash Attention 让中间矩阵不落 HBM,显存峰值大幅下降,训练吞吐显著提升。
具体案例:某团队预训练 128k 上下文模型时,使用 PyTorch 自带的 scaled_dot_product_attention(底层自动选择 FlashAttention 内核),相比手动实现的 BF16 标准注意力,在 8×H100 上训练吞吐提升约 2 倍,长序列批大小也从 8 提到 32。
推理阶段 KV Cache 显存与延迟优化
阶段:模型推理与部署
解决的问题:推理时每生成一个 token 都要读取全部 KV Cache,HBM 带宽成为延迟瓶颈。Flash Attention 的分块合并策略能减少 KV 的无效搬运,降低首 token 延迟和吞吐压力。
具体案例:某对话机器人服务在 batch=64、8k 上下文下,把 attention 内核从标准实现换成 FlashAttention-2 后,单 token 生成时间 TPOT 从 65ms 降到 38ms,同成本下支撑的并发用户数提升了约 70%。
AI 系统内核设计的方法论借鉴
阶段:系统设计与内核开发
解决的问题:传统内核按算子逐个实现,每个算子都要读写一遍 HBM。Flash Attention 证明了“IO 感知”设计原则的价值——把内存访问当作第一优先级,用算子融合减少中间往返。
具体案例:某 AI 编译器团队借鉴 Flash Attention 的融合模式,把 LayerNorm、激活和矩阵乘三个算子融合为单一内核,端到端训练速度提升 1.3 倍,显存占用降低约 20%。
Python 演示
import numpy as np def flash_attention_v1(Q, K, V, block_size=32): """Flash Attention V1 简化教学版(CPU numpy 模拟)""" n, d = Q.shape O = np.zeros((n, d)) # 未归一化输出累积 m = np.full(n, -np.inf) # 每行运行最大值 l = np.zeros(n) # 每行运行分母 # 外层循环: 遍历 Q 的块 for i in range(0, n, block_size): qi = Q[i:i + block_size] m_i = m[i:i + block_size].copy() l_i = l[i:i + block_size].copy() o_i = O[i:i + block_size].copy() # 内层循环: 遍历 K/V 的块 for j in range(0, n, block_size): kj = K[j:j + block_size] vj = V[j:j + block_size] # 当前块的得分矩阵 (Br, Bc) s = qi @ kj.T / np.sqrt(d) # 更新运行最大值 m_new = np.maximum(m_i, s.max(axis=1)) # 当前块的 exp 值(用新最大值做平移) p = np.exp(s - m_new[:, None]) # 更新运行分母: 旧值按指数缩放 + 新块和 l_i = l_i * np.exp(m_i - m_new) + p.sum(axis=1) # 更新运行输出: 旧值按指数缩放 + 新块贡献 o_i = o_i * np.exp(m_i - m_new)[:, None] + p @ vj m_i = m_new # 写回该块: 未归一化输出除以最终分母 O[i:i + block_size] = o_i / l_i[:, None] m[i:i + block_size] = m_i l[i:i + block_size] = l_i return O def softmax_attention(Q, K, V): """标准注意力(对照实现)""" d = Q.shape[1] s = Q @ K.T / np.sqrt(d) p = np.exp(s - s.max(axis=1, keepdims=True)) return (p / p.sum(axis=1, keepdims=True)) @ V if __name__ == "__main__": np.random.seed(42) n, d = 128, 16 Q = np.random.randn(n, d) K = np.random.randn(n, d) V = np.random.randn(n, d) out = flash_attention_v1(Q, K, V) ref = softmax_attention(Q, K, V) err = np.abs(out - ref).max() print(f"max error: {err:.2e}") assert err < 1e-6, "分块实现与标准实现不一致" print("OK: Flash Attention V1 matches standard attention")
O = np.zeros((n, d))初始化未归一化输出累积,这是最终结果 O 的“锅”。m = np.full(n, -np.inf)每行运行最大值初始化为负无穷,表示“还没见到任何食材”。for i in range(0, n, block_size)外层循环:遍历 Q 的块,相当于一桌一桌翻台。for j in range(0, n, block_size)内层循环:遍历 K/V 的块,把当前 Q 块和所有 K/V 块配对。s = qi @ kj.T / np.sqrt(d)计算当前块的得分矩阵,对应公式 $S_j=Q_iK_j^\top/\sqrt{d}$。m_new = np.maximum(m_i, s.max(axis=1))更新运行最大值,判断新块是不是新的“鲜度上限”。p = np.exp(s - m_new[:, None])用新最大值做平移,得到数值稳定的指数值。l_i = l_i * np.exp(m_i - m_new) + p.sum(axis=1)更新运行分母:旧分母按高度差缩放,加上新块和。o_i = o_i * np.exp(m_i - m_new)[:, None] + p @ vj更新运行输出:旧输出缩放后叠加新块对 V 的加权贡献。O[i:i + block_size] = o_i / l_i[:, None]该块全部处理完后,未归一化输出除以最终分母,写出最终结果。
常见误区
误区“Flash Attention 这么火,一定是把矩阵乘法的运算量大大减少了,所以训练才快。”
正解恰恰相反,Flash Attention 的 FLOPs 与标准注意力几乎相同,甚至因为反向重计算还略多一点。它加速的根源是“少搬数据”:把 N×N 中间矩阵留在 SRAM 里算完,不写回 HBM。就像老火没有把切菜动作变少,而是减少了从仓库到操作台的搬运次数。
为何常见:“加速”这个词天然让人联想到“少算”,但在 IO 瓶颈主导的场景里,真正的提速来自“少搬运”。
误区“分块算 softmax 肯定是个近似算法,精度多少会损失一点吧?”
正解在线 Softmax 与标准 Softmax 在数学上完全等价。每次最大值更新时,旧部分会被精确地乘以指数缩放系数,整个归一化的最终结果一模一样,只有浮点舍入误差。上面的 Python 演示也验证了误差在 1e-6 量级。
为何常见:直觉上“分块”像“抽样”或“近似”,但其实分块只是改变了计算顺序,没有改变计算内容。
误区“FP8 只有 8 位精度,用上去模型效果肯定大幅下降。”
正解V3 不是简单地把所有数都降到 FP8。它用 FP8 做矩阵乘法输入,在 FP32 里做累加,还配了精细的缩放因子管理数值范围,端到端精度接近 BF16。而且 FP8 模式是可选项,用户可以在对精度敏感的场合关掉它。
为何常见:听到“低精度”就联想到 int8 量化带来的大误差,忽略了“低精度输入 + 高精度累加”这种折中设计。
误区“V2 优化了分块大小,把块切得更碎,所以并行度更高、速度更快。”
正解V2 的关键不是把块切得更小,而是“让每个线程块独立负责一个 Q 块”来提升并行度,同时把 rescaling 等非矩阵乘运算从每个分块内部挪到写回前统一做。块大小仍然由 SRAM 容量决定,V2 只是把块内的低效操作清空了。
为何常见:“优化”常被理解成“拆得更细”,但 V2 的实际思路是“减少等待、消除多余动作”。
误区“这是那些内核大佬写的黑科技,我做应用开发根本接触不到。”
正解Flash Attention 已经集成进 PyTorch 的 scaled_dot_product_attention、HuggingFace Transformers 和 vLLM 等框架。任何用 Transformer 训练或推理的程序,只要调用标准 API,底层就默认走了 FlashAttention 内核。理解它的原理,能帮你判断什么场景该调什么参数、为什么长序列显存够用。
为何常见:前沿技术看起来“离生产很远”,但实际上它早已成为基础设施的一部分,只是包装在标准 API 后面让人无感。
面试题
Q1. Flash Attention 主要解决的问题是?
- A.减少注意力计算量
- B.减少HBM内存访问← 正确答案
- C.提升计算精确度
- D.替代KV缓存机制
答案:B。Flash Attention 的 FLOPs 与标准注意力几乎相同,真正的收益是让 N×N 中间矩阵不落 HBM,大幅减少内存访问。A 是常见误解,C 不是目标,D 与 KV Cache 无关。
Q2. Flash Attention V1 中“分块”的作用是?
- A.让矩阵乘法更快
- B.让中间结果适配SRAM← 正确答案
- C.减少序列长度
- D.增加模型并行度
答案:B。分块把 N×N 的中间矩阵切成 $B_r\times B_c$ 的小块,正好装进 SRAM 操作台,中间结果无需写回 HBM。它不改变矩阵乘法本身的 FLOPs,也不改变序列长度。
Q3. 在线 Softmax 维护的三个运行状态是?
- A.最大值、分母、输出← 正确答案
- B.均值、方差、输出
- C.查询、键、值
- D.长度、维度、批次
答案:A。在线 Softmax 每处理一个新块,都更新运行最大值 m、归一化分母 l 和未归一化输出 o。Q/K/V 是输入而非运行状态,均值和方差与 softmax 无关。
Q4. V2 相比 V1 的关键优化方向是?
- A.增大SRAM的容量
- B.提高序列维并行度← 正确答案
- C.减半分块的大小
- D.改用FP16的精度
答案:B。V2 让每个线程块独立负责一个 Q 块,沿序列维度大幅提升并行度,同时把非矩阵乘运算移出内核。它没有改变 SRAM 容量,也不是减小分块或改精度。
Q5. Flash Attention V2 减少非矩阵乘运算的措施包括?
- A.将Q的缩放放到核外← 正确答案
- B.增加softmax重计算
- C.延迟到写回再归一化← 正确答案
- D.使用更大的分块
答案:AC。V2 把 Q 的缩放预计算移到 kernel 外,并把 rescaling 推迟到最终写回时统一进行,核心里只剩矩阵乘法。重计算在 V1 就有,增大分块不是减少非矩阵乘的手段。
Q6. V3 的 FP8 模式如何保障精度?
- A.在FP32中做累加← 正确答案
- B.使用更大的SRAM
- C.采用混合精度存K
- D.降低量化缩放因子
答案:A。V3 用 FP8 做矩阵乘法的输入,但累加在 FP32 中进行,并配合 per-block 缩放因子管理数值范围。这样既拿到 FP8 的高吞吐,又保住了接近 BF16 的精度。
Q7. Flash Attention V3 异步流水线的含义包括?
- A.加载与计算重叠进行← 正确答案
- B.每个warp专岗专职← 正确答案
- C.使用多块GPU互连
- D.写入HBM异步完成← 正确答案
答案:ABD。V3 用 warp specialization 让部分 warp 专门负责数据加载、部分负责计算、部分负责异步写回,加载与计算完全重叠。多块 GPU 互连是分布式训练范畴,不属于 V3 异步流水线的定义。
Q8. Flash Attention 数学上与标准注意力等价的原因是?
- A.使用近似softmax函数
- B.重缩放保持归一化一致← 正确答案
- C.分块足够小误差小
- D.使用了更高精度浮点
答案:B。每当运行最大值更新,旧输出和旧分母都被精确地乘以指数缩放系数,保证最终归一化结果和标准 softmax 完全一致。这不是近似,而是“分块后精确重组”。
Q9. 关于 Flash Attention 三代演进的理解,正确的有?
- A.V1首创分块与重计算← 正确答案
- B.V2改进并行减少非矩阵乘← 正确答案
- C.V3引入FP8与异步流水线← 正确答案
- D.V3完全抛弃了SRAM分块
答案:ABC。V1 提出分块 Tiling 和反向重计算,V2 提升序列维并行度并砍掉非矩阵乘运算,V3 引入 FP8 和异步流水线。V3 仍然基于 SRAM 分块,只是让搬运和计算重叠进行而已。
Q10. 为什么说 Flash Attention“加速训练而不改变结果”?
- A.减少计算FLOPs总量
- B.数学等价但减少IO等待← 正确答案
- C.降低了注意力头数
- D.使用了近似softmax
答案:B。Flash Attention 的计算量(FLOPs)几乎没有减少,但通过分块和在线 Softmax 把中间矩阵留在 SRAM,让 GPU 不再等 HBM 搬运——数学上等价,速度上更快。它不改变模型结构或注意力头数。