适用读者:想理解"为什么融合能快 1.8 倍却不改变结果"、要决定"分桶还是特化"、要读 FlashAttention 源码的人。
前置:S23-J16(提交、分块、切分)。
覆盖旧文:A-4338 §5.3–§5.6(融合/缓冲/流水 + 编译器 + 运行时,402 点)→ 402 点。
扩展点:E-S23-053(融合省的是中间张量的两次搬运:120→72 MiB)、E-S23-054(FlashAttention:1 GiB 中间读写 → 片上统计量 m/ℓ/u)、E-S23-055(分桶多算 45% 却更快:MFU 20%→40%)、E-S23-056(图重放只减主机提交,不减 kernel 数)、E-S23-057(T = P + rT的交叉点:约 65 / 78 / 89 组)。
上一篇:S23-J16 · 算子的提交、搬移与分块
下一篇:S23-J18 · 从单卡到并行:六种并行方式。
1. 一句话结论
融合不减少运算,只减少"中间结果写回再读回";图重放不减少 kernel,只减少主机提交。 两者常被混为一谈,而它们解决的是完全不同的两段时间。
出处:AI Infra 教材 §5.3–§5.6(
id 4338)。
融合:减少 kernel 数与中间张量流量(数据面)
图重放:减少 launch 次数(控制面)
分桶/特化:用"多算一点"或"多编译几份"换更高的矩阵利用率
2. 融合:省的是两次搬运(E-S23-053)
FFN 两支投影 + 激活链:G = XW_g、U = XW_u、Z = SiLU(G) ⊙ U,三者形状 [1024,12288],BF16 各 24 MiB。
SiLU 与 ⊙ 都是逐元素运算:Z[i,j] 只依赖 G[i,j] 与 U[i,j],无归约 → 可以片上逐片算。
例 5-4 的账:
| 方案 | 读写明细 | 总量 |
|---|---|---|
| 分开执行 | 算 T:读 G 写 T = 48 MiB;算 Z:读 T、U 写 Z = 72 MiB | 120 MiB |
| 融合两步 | 读 G、U,写 Z | 72 MiB |
理论时间比 72/120 = 0.6 → 1.67×
实测:激活链 14.2 μs → 7.7 μs ≈ 1.8×
2.1 融合边界:每多保留一个中间量,多 48 MiB
| 方案 | 总量 |
|---|---|
| 三步完全分开(含量化到 1 byte/元素) | 156 MiB |
| 只融合前两步 | 108 MiB |
| 三步全融合 | 60 MiB |
判据(最反直觉的一条):读写减少 96 MiB,峰值只减少 12 MiB(72 → 60 MiB)。因为同一块存储可以反复读写——流量与容量是两笔不同的账。
3. FlashAttention:把平方级的中间矩阵变成统计量(E-S23-054)
标准注意力:S = QKᵀ/√d → P = softmax(S) → O = PV。
一个头、L=8192、d=128、BF16:
| 张量 | 元素数 | 大小 |
|---|---|---|
| Q / K / V / O(各) | 8192×128 | 2 MiB |
| S(FP32) | 8192×8192 | 256 MiB |
| P(FP32) | 8192×8192 | 256 MiB |
标准实现:QKV 读 + O 写 = 8 MiB;S、P 各写一次读一次 = 1 GiB
中间矩阵随 L² 增长,读写占绝对主导。
3.1 在线 Softmax 只需三个量
m = 已处理分数的最大值
ℓ = 以 m 为基准的指数和
u = 相同权重下的值加权和
换基准时:r = exp(m_old − m_new),旧 ℓ、u 同乘 r
例 5-6:分数 [0, ln2]、值 [1, 3],每块一个元素。
第一块:m=0, ℓ=1, u=1
第二块:最大值升到 ln2 → r = 1/2
ℓ' = 1/2 + 1 = 1.5, u' = 1/2 + 3 = 3.5
结果:3.5/1.5 = 7/3
一次整行算:(1 + 2×3)/(1+2) = 7/3 ✅ 相同
判据:FlashAttention 不改变数学结果,只改变"中间结果存在哪、活多久"。 这是"架构—算法协同"的典型:算法给出一致的等价形式,架构提供片上空间。
4. 编译器:先能表达,再能选(§5.4)
| 机制 | 作用 |
|---|---|
| 循环变换(分块、重排、融合) | 用统一表示描述"怎么切、怎么合" |
| 依赖与舍入约束 | 决定哪些变换合法(浮点求和顺序会变结果) |
| 多面体调度(isl) | 自动安排循环顺序、分块、片上缓冲与搬移 |
| AKG | 面向昇腾的算子自动生成(华为开源,作者曾参与) |
判据:编译器的价值不是"更快",而是"能比较多种实现并选择"。 手写只能试几种,编译器可以搜一片。
5. 运行时:重叠、重放、形状策略
5.1 图重放只减主机提交(E-S23-056)
3 次 FFN 执行的实测对照(原文实验):
| 配置 | 主机 launch | 加速器 kernel |
|---|---|---|
| 普通提交 | 18 次 kernel launch | 18 个 |
| 图重放(CUDA Graph) | 3 次 graph launch | 仍是 18 个 |
| 融合激活链 | 15 次 | 15 个 |
| 融合 + 图重放 | 3 次 graph launch | 15 个 |
判据:图重放治"主机",融合治"数据面"。 想减少 kernel 数量只能融合;想减少提交次数只能图重放——两者叠加才是完整解。
5.2 分桶 vs 特化:多算一点,换更高的利用率(E-S23-055)
例 5-12 的形状分布:10 次 FFN,8 次 256 行、1 次 1,536 行、1 次 2,048 行,实际 5,632 行。
| 策略 | 执行行数 | MFU | 有效处理率 | 每组时间 | 准备时间 |
|---|---|---|---|---|---|
| 通用 | 5,632 | 20% | 100 TFLOP/s | 17.0 ms | 100 ms |
| 分桶(512 / 2048) | 8,192(+45%) | 40% | 200 TFLOP/s | 12.4 ms | 400 ms |
| 逐形状特化 | 5,632 | 50% | 250 TFLOP/s | 6.8 ms | 900 ms |
判据:分桶多算 45%,但因为 MFU 翻倍,仍然更快。 峰值 503.8 TFLOP/s 是分母,MFU 才是能用的比例。
5.3 决策:T_total = P + r·T
通用 vs 分桶: 100 + 17.0r = 400 + 12.4r → r ≈ 65 组
分桶 vs 特化: 400 + 12.4r = 900 + 6.8r → r ≈ 89 组
通用 vs 特化: 100 + 17.0r = 900 + 6.8r → r ≈ 78 组
判据(E-S23-057):运行组数少于 ~65 组,通用实现最划算;超过 ~89 组,特化开始胜过一切。 中间地带选分桶。
5.4 Persistent Kernel
按数据块调度任务,让 kernel 常驻、自己取下一块,减少反复启动——本质是把"launch 开销"换成"kernel 内的循环"。
6. 可复算(实测)
$ cd /root/博客运营官/blog-v2 && python3 tools/s23_book_calc.py | tail -5
融合: 分开 120 MiB → 融合 72 MiB(比 0.60,理论 1.67×);三步 156/108/60 MiB
峰值: 分开 72 MiB vs 全融合 60 MiB(读写 -96 MiB,峰值只 -12 MiB)
FlashAttention(L=8192,d=128,BF16): QKV/O 各 2 MiB;S、P 各 256 MiB → 中间读写 1.0 GiB(S、P 各写读一次)
在线 softmax: [0,ln2]/[1,3] → r=0.5 → ℓ'=1.5, u'=3.5 → 2.3333(= 7/3)
交叉点: 通用↔分桶 65 组;分桶↔特化 89 组;通用↔特化 78 组
7. 前提与边界
| 结论 | 成立条件 | 不成立时 |
|---|---|---|
| 融合省流量 | 中间量只被下一个算子用一次 | 被多处引用 → 仍须落回 HBM |
| 融合保结果 | 舍入顺序不变 | 改变求和顺序 → 数值会变 |
| 在线 Softmax 等价 | 每块最大值可回缩(乘 r) | 数值溢出/精度不足 → 需更高精度 |
| 分桶划算 | 补齐浪费 < MFU 增益 | 形状极不均匀 → 浪费超过收益 |
8. 常见误区
| 误区 | 纠正 |
|---|---|
| "融合减少 FLOPs" | FLOPs 不变,少的是中间张量的写回+重读 |
| "融合也大幅降峰值显存" | 读写 −96 MiB,峰值只 −12 MiB |
| "图重放能减少 kernel" | 18 → 18,只把 18 次 launch 变 3 次 |
| "分桶是浪费算力" | 多算 45%,MFU 翻倍后仍更快 |
| "特化总是最好" | 组数少时准备成本吃掉收益(<65 组) |
9. 自测
- 例 5-4 分开执行 120 MiB、融合 72 MiB,理论加速比多少?实测多少?
- 为什么三步全融合只把峰值从 72 降到 60 MiB,而不是按流量同比例下降?
- FlashAttention 的标准实现在 L=8192、d=128、BF16 下,中间读写多少?在线方案保留哪三个量?
- 分桶执行 8,192 行、实际 5,632 行,为什么仍比通用快?
答案
120/72 = 1.67×;实测 14.2 → 7.7 μs ≈ 1.8×。- 因为存储可反复读写:总流量降 96 MiB,但同一时刻只需保存的容量只降 12 MiB。
- S、P 各 256 MiB,各写一次读一次 = 1 GiB;保留
m(最大分数)、ℓ(指数和)、u(值加权和)。 - 补齐多算 45%,但 MFU 从 20% 升到 40%(处理率翻倍),
17.0 → 12.4 ms。
10. 下一篇
S23-J18 · 从单卡到并行:六种并行方式 —— 数据、张量、流水、专家、序列、上下文并行各自切什么、通信什么、在什么规模下才划算。