在线 softmax 单步
softmax 要「整行一次算」,分块计算时每块只看到行的一部分——FlashAttention 靠三个随身统计量拆掉这道拦路虎:最大值 m、指数和 l、加权累加 acc。每来一块先更新 m,旧累积按 exp(m_old−m_new) 缩放再并入新贡献。单步走完 4 块,看块 2 的新最大值把旧贡献压到 ~4.4%、块 3/4 缩放系数恰为 1,末尾两列并排对上——「缩放过」也不差分毫。
纯前端真实浮点计算:流式更新(m←max → 旧累积×exp(m_old−m_new) → 并入 exp(块−m_new))与全行 softmax 逐步实算对照,分数为知识库《GPU Kernel 入门》动手节 seed=7 高斯分数照抄,搬运账按 N=4096 · bf16 文章口径实算(往返 64 MiB / 显存 32 MiB),零 API、零随机。
softmax 要「整行一次算」才知道最大值和总和——分块计算时每块只看到行的一部分,怎么办? FlashAttention 的答案是让每块计算带着三个运行统计量: 最大值 m、指数和 l、 加权累加 acc。点「单步」让一块分数流入,看新最大值到来时旧贡献怎么被按比例缩放——缩放过也没算错,末步与全行一次算并排对上。
一行打分 · 16 个分数分 4 块流入(文章 seed=7 实测分数)
已流入 0/4 块块 1待流入
k10.00
k20.90
k3-0.82
k4-2.67
块 2
k5-1.36
k6-2.97
k70.18
k84.02
块 3
k9-1.48
k10-1.86
k111.47
k121.07
块 4
k130.32
k14-2.79
k15-0.09
k162.09
四个块的最大值分别是 0.90 → 4.02 → 1.47 → 2.09: 块 2 会带来真正的新最大值,块 3/4 则没有——两种缩放形态一行看全。
三个随身统计量 · 每块更新一次
m · 见过最大值−∞防指数溢出的基准线
l · 指数和(分母)0.0000收尾时 acc ÷ l = softmax
acc · 加权累加(分子)全 016 格逐块写入、随 m 变化缩放
acc 16 格实时形貌(琥珀 = 刚写入的块)块 1–4 各 4 格
还没有块流入——点「单步」让块 1 进来,或「播放」自动走完 4 块。
还剩 4 块
搬运账 · 为什么 FlashAttention 靠它省时间(N=4096 · bf16)
普通注意力64 MiBN×N 打分矩阵写 HBM 再读回(2 趟往返)另占显存 32 MiB
在线 softmax0 趟m/l/acc 随身带,N×N 在 SRAM 算完即弃、从不落地只回写最终输出(线性于 N)
省的是搬运不是计算乘加一次没少——省的全是「中间矩阵上下楼」。账式: 2·N²·2 B ÷ 2²⁰;论文口径注意力算子提速 2~4 倍、显存最高省 20 倍。
「省显存」与「省 IO」是一件事的两面:不落地的中间结果,既不占地方、也不花搬运费——而这一切成立的前提,正是上面验证的「分块零近似」。
缩放不是近似exp(m_old−m_new) 是「早知道最大值」的精确换算:旧贡献压小多少,分母就压小多少,末尾一除比例不变——流式结果与全行 softmax 精确等价。
三个统计量随身带m 管防溢出、l 管分母、acc 管分子——每块更新一次(max → 缩放 → 并入),块与块之间只带这三个数,不需要整行。
先证等价,再算搬运「省 IO」成立的前提是「不算错」——1e-16 的舍入差之外分毫不少;这是读任何内核优化的标准姿势。