k1_bwd_prepare_deep_dive
K1_bwd “prepare” kernel 逐行精讲(小白可推导版)
对应源码:
csrc/smxx/bwd_kernel1.cuh的_flash_kda_bwd_prepare目标读者:没接触过自动微分 / CUDA kernel 的人。读完应能自己推出 L2 归一化反向、数值稳定的 、以及门控的逆向 cumsum,并理解代码为何这样写。
约定:
d<量>表示 loss 对该量的梯度。exp(x)实际是 (exp2)。公式用 LaTeX,伪代码用代码块。
0. 它在整条反向链里的位置
1 | Backward: K2_bwd (反向递推, 后->前) -> K1_bwd (本文, 还原输入梯度) |
K2_bwd 算出了一堆"派生量的梯度" 。 K1_bwd 是前向 K1 的逆过程:把这些梯度还原回原始输入的梯度—— (门)、,以及参数 、。
0.1 前向 K1 做了什么(理解反向的前提)
设每行(一个 token)的原始 ,门控相关原始量 、参数 、。前向:
所以反向要做三件事:把 收拢成 和 ;把 过 L2 反向得 ;把 (+)过 cumsum 和门函数得 。
1. 线程模型(line 10-62)
1 | constexpr int ELEMS_PER_THREAD = 8; |
- Grid = (total_tiles, H):
blockIdx.x = global_tile_idx,blockIdx.y = head_idx。每个 CTA 处理一个 chunk 的一个 head(line 7)。 - 每行 16 个线程,每线程负责 8 个元素:
这个布局和前向 K1 完全一致(line 60),保证归约顺序相同 → 数值可复现。
2. tile → 序列定位(line 64-99)
把 global_tile_idx 映射到 seq_idx / local_t,并算出:
越界 tile(local_t >= t_tiles_this_seq)直接 return(line 95)。
a_log_exp = exp(A_log[head])(line 101)。
shared memory 只有一块:dgc_smem[C*D](line 105)。
3. Phase 1+2:融合算 dgc 与 dq/dk(line 129-218)
这是全 kernel 最核心、最讲数值稳定的部分。
3.1 重算 L2 范数(line 136-157)
每线程读自己负责的 8 个 元素,累加平方和;尾块越界行读 0;再用 16 线程的 __shfl_xor 归约()把整行平方和汇总:
归约方式必须与前向逐位一致。
3.2 重算门控指数、算 dqn/dkn 与 dgc(line 163-193)
对每个元素(列 ):
(代码里 gt_tile[col] 存的就是 ,所以 exp_gt_gc = gt_tile[col]*exp_neg_gc。)
(a) dqn / dkn(供 L2 反向)
前向 ,所以对 :
同时进了 三个分支,梯度相加:
(b) dgc —— 数值稳定写法(关键 trick)
下面先给从零详解(b.0–b.4),已熟悉的读者可直接看 b.3 的结论框。
(b.0) 先搞清 是什么、要求什么
是"门控累加和",一个标量(对每行每维)。前向它出现在四个派生量的指数里:
上游已经给了这四个量的梯度 。一个变量被用到多处(扇出 / fan-out),它的总梯度 = 各处回传之和(多元链式法则)。所以:
(b.1) 求每个偏导(指数函数求导)
对指数函数 。这里底数 ,所以 。本 kernel 约定把公共因子 推迟到 Phase 4 统一乘(因为 对所有项线性,提一个常数出来最后乘即可),故这里先省略 。导数符号跟着指数的 号走:
( 指数是 ,链式带出一个负号。)代回得天真公式(数学正确):
(b.2) 为什么天真公式在 fp32 里会崩(灾难性抵消)
两者相差约
- 大 + 小:小项落在大项的有效位之外,被直接舍掉 → 信息丢失;
- 大 − 大:若两个大项接近,相减后高位全抵消、只剩低位舍入噪声 → 灾难性抵消(catastrophic cancellation)。
结果
(b.3) 稳定写法:把 提到括号外
注意
其中
(b.4) 为什么这样就稳了
关键观察:括号里的乘积
- 括号内是"几个正常大小的数相减" → 良态,没有大数吞小数;
- 再乘一个
的 (单位向量的分量)→ 仍然正常。
全程不出现
两者数学恒等,只是运算顺序不同,浮点结果天差地别。这是典型的"重新结合以避免中间量爆炸"的数值技巧。
注意区分两个量(来源相同、符号不同):
进 不带 的符号(前向 里 是线性因子,故 全加);而 进指数带 号(故 带符号)。两者只差中间两项的正负号,别混用。
3.3 L2 归一化的反向 → dq/dk(line 195-216)
前向
下面 3.3.0 是写给线代基础薄弱读者的从零详解;已熟悉的读者可直接看 3.3.1 的结论。
3.3.0 从零推导
为什么不简单:若
第一块:
第二块:
两部分含义:
第三块:链式法则汇总。
第一项里
第四块:用
代回 (◇):
3.3.1 结论
对应代码:
点积 my_row < actual_len 才写出(line 207)。这个归约就是在算第二块说的"耦合标量"——所以归一化反向必须先做一次行内归约,无法纯逐元素。
直觉:
是单位向量,沿 方向拉伸 不改变 (只改长度),所以梯度里"沿 的径向分量"无效。 是 在 上的投影长度, 是该径向分量,减掉它只留切向分量;再乘 ( 越长,同样的 对 影响越小)。一句话:把梯度投影到 方向,再按 长度 缩放。
3.3.2 小数字验证(2 维)
取
自检:
可作为写单测时的快速断言。
4. Phase 3:dv 直接搬运(line 220-224)
actual_len*D 个元素。
5. Phase 4:门控反向 = dgc 的逆向 cumsum + dgT(line 226-261)
下面 5.0 是写给基础薄弱读者的从零详解(核心就一句:前缀和的反向是后缀和);已熟悉的读者可直接看 5.1。
5.0 从零理解"cumsum 的反向"
(a) 什么是 cumsum(前缀和)
前向把一串数
展开看就是(以
(b) 关键观察:每个输入"扇出"到哪些输出
竖着看上面这张表:
© 反向:把扇出的梯度收回来 = 后缀和
多元链式法则:
即"从
一句话规律:前向是前缀和(从前往后累加),反向就是后缀和(从后往前累加)。 方向正好反过来,所以叫"逆向 cumsum"。
(d) 怎么 算出来:一个滚动变量
不用对每个 rev_sum,每步把当前行的
1 | rev_sum = 0 |
走到 row 时,rev_sum 恰好就是
(e) 数字小例( )
设
| 步 row | 加入 |
rev_sum | 即 |
|---|---|---|---|
| 3 | 3 | 3 | 3 |
| 2 | 1 | 4 | 4 |
| 1 | 5 | 9 | 9 |
| 0 | 2 | 11 | 11 |
可手动验证
(f) 再叠加 那条路
小结:cumsum 反向 = 后缀和(一个滚动累加器搞定);
再贡献一个对每行都相同的常数。下面 5.1 把这两件事正式写出来。
5.1 g_total 扇出到两个去处
前向
多元链式法则:一个量被用到多处(扇出),其总梯度 = 各下游路径回传之和。
- gc 这条:因
仅当 ,所以是"后缀和"。代码从row=C-1往下走,维护rev_sum += dgc[row],到 时rev_sum正是 (这就是"逆向 cumsum")。 - gT 这条:因
对所有 ,所以每行都加同一个常数dgT_val(line 232 循环外读一次,line 239 每行加)。
(此处仍是"未乘
5.2 过门函数反传到 dg_raw 与参数(line 241-260)
把 dg_nat)沿
下面 5.2.0 是从零详解(单变量链式法则 + sigmoid 求导 + 一个量被多处使用怎么办);已熟悉的读者可直接看 (a)(b)©。
5.2.0 从零理解这条链
(i) 前向是一串"嵌套函数"。 从最里到最外:
原始输入
(ii) 单变量链式法则(唯一要会的)。 若
(iii) sigmoid 的导数为什么是
(用了 dsig = sig*(1-sig)。
(iv) dg_nat 也是"少乘了一个 dg_nat dg_nat 是线性的,补在哪一步都行,代码补在算
(v) 一个量被用在多处怎么办(
每个偏导再乘上游
(a) 过
(代码 line 247 把
(b) 过
© 过
代码 line 252-253 先累加 ddt_bias_acc += dz*a、dA_log_acc += dz*(g_raw+dt),循环后:
为什么用 atomicAdd(line 259-260):
尾块掩码:row >= actual_len 的行 dg_out=0(line 255),且不累加参数梯度。
6. Phase 5:dbeta 从 chunk 空间转回 logit 空间(line 263-272)
K2_bwd 给的
仅前 actual_len 个线程各写一个 token 行。
7. 全部梯度公式速查表
8. 设计要点总结
| 主题 | 做法 |
|---|---|
| 数值稳定 dgc | 把量级悬殊( |
| 16 线程/行 + shfl | 严格对齐前向 K1 的归约顺序,保证 bit 级可复现 |
| dkn vs dkn_signed | 前者全加(对 |
| 门控逆向 cumsum | |
| ln2 延后 | |
| atomicAdd | |
| 尾块掩码 | actual_len 控制越界行读 0 / 写 0 / 不累加参数 |
配套阅读:上游 K2_bwd 的逐行精讲见
k2_bwd_recurrence_deep_dive.md。本 kernel 的输入全部来自那里。
- 标题: k1_bwd_prepare_deep_dive
- 作者: 鱿鱼圈
- 创建于 : 2026-09-12 22:40:00
- 更新于 : 2026-09-12 22:26:40
- 链接: https://yuyanqi.com/2026/09/12/k1_bwd_prepare_deep_dive/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。