Kernel Dev 训练营/ 提示与讲解
T2 提示与讲解 · Track T · Triton · 对照 solutions/t2_softmax.py 逐行读

T2 讲解:一行一个 program 的融合 softmax

读一次、写一次的数值稳定 softmax。逐行读参考解:为什么要减 max、padding 位置为什么填 -inf、row stride 是干什么的、tl.max 和 tl.sum 背后其实就是 C2 的 reduction,以及行太长时为什么要换成 online softmax。

怎么读这一页

对照 drillst2_softmax.py 读。kernel 只有十几行,但每一行背后都有一个“为什么”。第 ⑤ 节的 online softmax 是 T5 FlashAttention 的前置知识。

① 先看全貌:融合省在哪

softmax 对每一行 x 做:y = exp(x − max(x)) / Σ exp(x − max(x))。不融合的写法(官方 tutorial 里的 naive_softmax)要把同一行反复从显存读出来:

不融合 vs 融合:一行数据进出显存几次悬停节点查看连接
softmax 是 memory-bound 的,时间约等于搬运的字节数除以带宽。融合版每个元素只读一次、写一次,这就是下限。题目页的实测:N = 4096 时融合版 1540 GB/s,torch.softmax 1025 GB/s。

前提是整行放得进一个 program。每个 program 负责一行,BLOCK_SIZE 取不小于 N 的 2 的幂。

类比 · 收银台

每个收银员(program)负责一位顾客的整个购物车(一行)。不融合的做法是:先让顾客把东西全部放到传送带上找出最贵的那件,收回去;再放一遍算折扣,收回去;再放一遍结账。融合的做法是:东西放上传送带一次,收银员把所有计算都在脑子里(寄存器)做完,直接打出小票。购物车太大、收银员记不住的时候(N 很大),才需要分批处理(第 ⑤ 节)。

② 为什么要减 max

数学上 softmax(x) = softmax(x − c) 对任意常数 c 成立(分子分母同时乘以 e⁻ᶜ)。取 c = max(x) 是为了不溢出:

x 直接算 exp(x) 减 max 之后
[1, 2, 3] 2.7, 7.4, 20.1 exp([−2, −1, 0]) = 0.14, 0.37, 1
[1000, 1001] fp32 的 exp 超过 88.7 就是 inf,inf / inf = NaN exp([−1, 0]) = 0.37, 1 → 结果 [0.27, 0.73]

减完之后最大的指数是 0,exp 的结果都在 (0, 1] 之间,分母至少是 1,既不会上溢也不会除以 0。

类比 · 量身高用相对值

全班同学比身高,如果都从海平面量起(几千米),数字很大、容易写不下。以全班最高的同学为基准,每个人记“比他矮多少”,数字都在 0 附近,比较起来一样准确。softmax 只关心相对大小,所以减去最大值不影响结果。

③ kernel 逐行讲

drillssoftmax_kernel(12–30 行)

我负责哪一行

row  = tl.program_id(0).to(tl.int64)
cols = tl.arange(0, BLOCK_SIZE)
mask = cols < n_cols

读:stride 和 -inf

drills21 行

x = tl.load(in_ptr + row * in_row_stride + cols, mask=mask, other=-float("inf")).to(tl.float32)

N = 5,BLOCK_SIZE = 8 时,一行的处理过程:

cols
0
1
2
3
4
5
6
7
x(load 之后)
−1
−3
−2
−5
−4
−inf
−inf
−inf
x − max(= −1)
0
−2
−1
−4
−3
−inf
−inf
−inf
exp
1
.135
.368
.018
.050
0
0
0
÷ sum(= 1.571)
.636
.086
.234
.012
.032
不写
不写
不写

这一行全是负数。如果 padding 填 0,max 会变成 0 而不是 −1,结果依然正确吗?数学上减任何常数都对,但 padding 的 exp(0) = 1 会被加进分母,结果就错了。

算:tl.max 和 tl.sum 就是 C2

drills25–28 行

x   = x - tl.max(x, axis=0)    # 向量 → 标量 → 广播回向量
num = tl.exp(x)
den = tl.sum(num, axis=0)
y   = num / den

tl.max(x, axis=0) 把整个 program 的向量归约成一个标量。你写了一个函数调用,编译器生成的就是 C2 里手写的那一套:

tl.max / tl.sum 编译出来的东西悬停节点查看连接
这正是 C2 的前三层(没有第四层,因为一行只在一个 program 里)。num_warps = 1 时,warp 之间那一层就省掉了。G1 会讲编译器怎么根据 layout 决定这三层各做多少。

写

drills30 行 用输出的 row stride 算地址,转回输入的 dtype,mask 保证 padding 位置不写。

④ host 端:BLOCK_SIZE 和 num_warps

drillssoftmax(33–46 行)

if x.stride(1) != 1: x = x.contiguous()       # 只要求行内连续
BLOCK_SIZE = triton.next_power_of_2(N)        # tl.arange 要 2 的幂
num_warps = 4 if BLOCK_SIZE <= 2048 else (8 if BLOCK_SIZE <= 8192 else 16)
softmax_kernel[(M,)](y, x, x.stride(0), y.stride(0), N, BLOCK_SIZE=BLOCK_SIZE, num_warps=num_warps)
N BLOCK_SIZE num_warps 线程数 每线程持有的 fp32
1000 1024 4 128 8
4096 4096 8 256 16
16384 16384 16 512 32

每个线程持有的值都在寄存器里,x、exp 之后的 num 等中间结果还要多占几份。行越长,寄存器压力越大,超出时会溢出到 local memory,题目页 N = 16384 的测试就在观察这个现象。

⑤ 行太长怎么办:online softmax

一行放不进一个 program 时,只能分段处理。最直接的办法是两遍:第一遍求 max 和 sum,第二遍归一化,多读一次。online softmax 可以一遍完成:每读一段,维护到目前为止的最大值 m 和“以 m 为基准”的指数和 l。

遇到新的一段时:

m_new = max(m, max(段))
l     = l · exp(m − m_new) + Σ exp(段 − m_new)      # 旧的和要换算到新的基准

算一个例子,一行 [1, 2, 3, 0] 分成两段:

步骤 m l
第 1 段 [1, 2] 2 e⁻¹ + e⁰ = 1.368
第 2 段 [3, 0]:m_new = 3 3 1.368 · e⁻¹ + (e⁰ + e⁻³) = 0.503 + 1.050 = 1.553
直接算验证 3 e⁻² + e⁻¹ + e⁰ + e⁻³ = 0.135 + 0.368 + 1 + 0.050 = 1.553 ✓

l · exp(m − m_new) 这一项是关键:之前的和是以旧的最大值为基准算的,基准变了,要乘一个“换算系数”。T5 的 FlashAttention 每处理一块 K、V 就做一次这个更新。

类比 · 换汇

你在旅行,每到一个国家就用当地货币记账(以当前的最大值为基准)。换到一个汇率更高的国家(遇到更大的 max)时,之前记的总额要按汇率换算成新货币(乘 exp(m − m_new)),再加上新国家的花销。走完全程,账本上的总额和你一开始就用同一种货币记账的结果完全相同。

⑥ 对照真实实现

⑦ 常见坑与自测

自测 1:x = [0, 0, 0, 0],N = 4,输出是多少?中间每一步的值?

max = 0,x − max = [0, 0, 0, 0],exp = [1, 1, 1, 1],sum = 4,y = [0.25, 0.25, 0.25, 0.25]。

自测 2:一行里全是 -inf(比如 attention 里整行被 mask 掉),会发生什么?

max = -inf,x − max = -inf − (-inf) = NaN,结果全是 NaN。真实的 attention kernel 会对这种情况特判(例如把 max 设成 0,或者最后把 0/0 处理成 0)。T5 和 L2 里会遇到这个边界情况。

源码与资料索引

本仓库:drillssolutions/t2_softmax.py · drillsmine/t2_softmax.py · drillstests/test_t2_softmax.py · drillscontent/t2.md · 前置:C2 reduction 讲解

参考实现:tritonTriton tutorial 02 · pytorchPyTorch SoftMax.cu · cutlassCuTe DSL softmax notebook

API:tl.max · tl.sum · tl.exp · triton.jit