Zhengxu Yu

Zhengxu Yu

AI Researcher, Huawei London Research Centre (ex-Alibaba)

Email: yuzxfred AT gmail.com

© 2026

深度理解“A Fix for Softmax and Cross-Entropy”

TL;DR

JAX、PyTorch、TensorFlow 的 softmax 和交叉熵在减去最大 logit 后,分母是 1 + e^(−m)。模型足够自信时 e^(−m) 小于 1 附近的浮点间距,加法被舍入,目标类梯度变成 0 而其他梯度不变(Softmax Collapse);蒸馏中学生等于老师时梯度也不为 0(Softmax Cancellation)。Adam 的归一化把这些微小误差放大成 Slingshot 尖峰和参数漂移。修复是用 softplus、expm1、log1p 在 log 空间重写,不再把 1 加到小数上。

A Fix for Softmax and Cross-Entropy

JAX、PyTorch、TensorFlow 里 softmax、log-softmax 和交叉熵的标准写法在浮点下有两个 bug。模型对正确类别非常自信时,目标 logit 的梯度被舍成 0,而其他梯度没有,梯度之和不再为 0;蒸馏时学生逼近老师,梯度本该趋于 0,却停在一个非零的噪声底上。作者用 Adam 的归一化解释了这两个 bug 如何被放大成训练中的 loss 尖峰和参数漂移,并给出只改几行代码的修复。

fp32 共 32 位,即 4 字节:1 位符号 S、8 位指数、23 位尾数。下面是 1.0 的存法(十六进制 0x3F800000):

字节 1 · 0x3F字节 2 · 0x80字节 3 · 0x00字节 4 · 0x00 00111111100000000000000000000000? S指数 8 位尾数 23 位第 24 位
  • 数值 = (−1)S × 1.尾数 × 2指数 − 127。1.0 的 S = 0,指数 = 01111111₂ = 127,尾数全 0,所以值是 1.0 × 20。
  • 指数位决定数的量级,相当于科学计数法里的“× 10 的几次方”,只是底数是 2。8 位能存 0 到 255 的整数,减去偏置 127 得到真正的指数;0 和 255 留作特殊用途(零、次正规数、无穷和 NaN),所以正常数的指数范围是 −126 到 127,对应约 10−38 到 3.4×1038。
  • 尾数位决定在同一量级内能分多细。相邻两个可表示数的间距是 2指数 − 127 − 23:数越小,间距越小。所以 fp32 在 0 附近能表示 10−38,在 1 附近的间距却是 2−23。
  • 小数点前的 1 总是存在,标准省掉不存(隐含位),所以尾数只占小数点后的 23 位。最低位代表 2−23 ≈ 1.19×10−7:1.0 的下一个可表示数是 1 + 2−23。
  • 1 + e−17.3:e−17.3 ≈ 3.05×10−8,比半个最低位 2−24 ≈ 5.96×10−8 还小,只能落在第 24 位的虚线格里,就近舍入后结果仍是 1.0。这一个舍入就是 Softmax Collapse 的起因。

1起点:softmax 的标准实现

记号。一个样本在 \(K\) 个类别上的 logit 是向量 \(\mathbf z = (z_1, \dots, z_K) \in \mathbb R^K\)(粗体表示向量);\(z_k\) 是第 \(k\) 类的 logit,是一个标量。\(t \in \{1, \dots, K\}\) 是这个样本的正确类别,即训练数据给的标签,论文称为 target class,下文简称目标类;\(z_t\) 就是目标类那一个 logit 的数值。在图像分类里,目标类是图片的真实类别;在语言模型里,是训练文本中实际出现的下一个 token。交叉熵训练的作用就是把目标类的概率 \(q_t\) 推向 1。例如 \(\mathbf z = [9, -9, -9]\)、\(t = 1\) 时,\(z_t = 9\)。

对 logit \(\mathbf z\) 直接算 \(e^{z_k} / \sum_j e^{z_j}\),只要某个 logit 超过约 88(fp32 指数上限),\(e^{z}\) 就会上溢。所以所有框架都先减去最大值 \(z_{\max} = \max_j z_j\):

\[ \tilde z_k = z_k - z_{\max}, \qquad \operatorname{logsoftmax}(\mathbf z)_k = \tilde z_k - \log \sum_j e^{\tilde z_j}, \qquad q_k = \operatorname{softmax}(\mathbf z)_k = \frac{e^{\tilde z_k}}{\sum_j e^{\tilde z_j}} \]式 (1)

对目标分布 \(\mathbf p\),交叉熵 \(\mathcal L(\mathbf z, \mathbf p) = -\sum_k p_k \operatorname{logsoftmax}(\mathbf z)_k\),精确梯度是

\[ \mathbf g = \nabla_{\mathbf z} \mathcal L = \mathbf q - \mathbf p, \qquad \textstyle\sum_k g_k = 1 - 1 = 0 . \]

梯度各分量之和为 0 是 softmax 的结构性质:把所有 logit 同时加一个常数不改变输出。下文的两个 bug 都表现为这个性质在浮点下被破坏,或者该为 0 的梯度不为 0。

2发现:两个数值 bug

2.1 Softmax Collapse:自信的预测让目标梯度变成 0

设目标类别 \(t\) 的 logit 最大。用 logit margin 衡量模型有多自信,即目标 logit 比其余 logit 的 log-sum-exp 大多少:

\[ m = z_t - \log \sum_{j \ne t} e^{z_j} . \]式 (2)

下面从 softmax 的定义出发,推出“减去最大值”之后分母恰好是 \(1 + e^{-m}\)。目标类 \(t\) 的 logit 最大,所以 \(z_{\max} = z_t\)。每一步右侧注明所用的变换:

\[ \begin{aligned} q_k &= \frac{e^{z_k}}{\sum_{i=1}^{K} e^{z_i}} && \text{softmax 的定义} \\ &= \frac{e^{z_k}\, e^{-z_t}}{\sum_{i} e^{z_i}\, e^{-z_t}} && \text{分子分母同乘正数 } e^{-z_t}\text{,比值不变} \\ &= \frac{e^{z_k - z_t}}{\sum_{i} e^{z_i - z_t}} && e^{a} e^{b} = e^{a+b}\text{:这一步就是“每个 logit 减去最大值”} \\ \sum_{i} e^{z_i - z_t} &= e^{z_t - z_t} + \sum_{j \ne t} e^{z_j - z_t} && \text{取上一行的分母,把 } i = t \text{ 的一项单独写出} \\ &= 1 + \sum_{j \ne t} e^{z_j - z_t} && z_t - z_t = 0,\; e^{0} = 1 \\ &= 1 + e^{-z_t} \sum_{j \ne t} e^{z_j} && e^{z_j - z_t} = e^{z_j} e^{-z_t}\text{,} e^{-z_t} \text{ 与 } j \text{ 无关,提到求和外} \\ &= 1 + e^{-z_t}\, e^{\log \sum_{j \ne t} e^{z_j}} && \text{任何正数 } S = e^{\log S} \\ &= 1 + e^{-\left(z_t - \log \sum_{j \ne t} e^{z_j}\right)} && \text{合并指数:} e^{a} e^{b} = e^{a+b} \\ &= 1 + e^{-m} && \text{括号里正是式 (2) 定义的 } m \end{aligned} \]

再看分子:\(k = t\) 时分子是 \(e^{z_t - z_t} = 1\)。所以目标类的概率是 \(q_t = 1/(1 + e^{-m})\),损失是 \(\mathcal L = -\log q_t = \log(1 + e^{-m})\)。以论文的例子 \(\mathbf z = [9, -9, -9]\) 验证:减去 9 后为 \([0, -18, -18]\),取指数为 \([1, e^{-18}, e^{-18}]\),其余两项之和 \(2e^{-18} = e^{-(18 - \ln 2)} \approx e^{-17.3}\);按式 (2),\(m = 9 - \log(2e^{-9}) = 18 - \ln 2 \approx 17.3\),两者一致。

对 \(\mathcal L = \log(1 + e^{-m})\) 求导,得到损失和梯度:

\[ \mathcal L = \log(1 + e^{-m}), \qquad g_t = \underbrace{\frac{1}{1+e^{-m}} - 1}_{\text{标准写法}} = \underbrace{-\frac{e^{-m}}{1+e^{-m}}}_{\text{修复写法}}, \qquad g_j = \frac{e^{z_j - z_t}}{1+e^{-m}} \;(j \ne t). \]式 (3)

两种写法在实数上相等,在浮点上不同。这里的浮点格式指计算机用二进制存储实数的方式,深度学习里常用 bf16、fp32、fp64 三种。一个浮点数存成 \(\pm 1.f \times 2^{e}\):指数 \(e\) 决定数的量级范围,尾数 \(f\) 的位数 \(p\) 决定精度。每种格式的总位数固定,按 IEEE 754 等标准分给符号、指数和尾数三部分:

格式总位数符号指数尾数 p说明
bf1616187指数位与 fp32 相同,量级范围一样大,精度低得多
fp3232182332 − 1 − 8 = 23,所以小数点后只有 23 位
fp646411152

小数点前的那个 1 不需要存:规格化的二进制数首位总是 1,标准把它省掉(隐含位),所以 fp32 实际有 24 位有效数字,但小数点后只有 23 位。给指数多分一位,能表示的量级就翻倍;给尾数多分一位,精度就翻倍。总位数固定时只能二选一,bf16 选择保住范围、牺牲精度。

这带来精度的不对称。在 \([1, 2)\) 区间里指数固定为 0,相邻两个可表示的数相差尾数最低位 \(2^{-p}\),fp32 中约为 \(1.2 \times 10^{-7}\);在 0 附近,指数可以取很小的负数,fp32 能表示到 \(10^{-38}\)。所以 \(e^{-m}\) 单独存放时很精确,加到 1 上之后只能保留到 \(2^{-p}\) 的整数倍。按就近舍入,当 \(e^{-m}\) 小于间距的一半 \(2^{-(p+1)}\) 时,\(1 + e^{-m}\) 被舍入为 1,即

\[ e^{-m} < 2^{-(p+1)} \iff m > c_d = (p+1)\ln 2 . \]
格式尾数位 p精度悬崖 cd = (p+1) ln 2此时 e−m 的大小
bf167≈ 5.5约 0.4%,模型给目标类 99.6% 的概率就到了
fp3223≈ 16.6约 6×10−8
fp6452≈ 36.7约 1×10−16

论文给出的三个悬崖值与上式一致,作者称之为 precision cliff。越过悬崖后,两种写法分道扬镳:

  • 标准写法先算 \(q_t = 1/(1+e^{-m}) = 1/1 = 1\),再减 1,得到 \(\tilde g_t = 0\);前向的 \(\log(1+e^{-m})\) 也变成 \(\log 1 = 0\)。
  • 非目标梯度 \(\tilde g_j = e^{z_j - z_t}/(1+e^{-m})\) 的分子直接由负数差算指数,不经过“加 1”,一直准确到指数下溢(\(m \approx 88\)),所以仍然为正。
  • 结果:只有目标位置被清零,梯度之和变成 \(\sum_k \tilde g_k = e^{-m}/(1+e^{-m}) > 0\)(式 4)。
  • 修复写法 \(-e^{-m}/(1+e^{-m})\) 把 \(e^{-m}\) 留在分子:分母舍成 1 也无妨,\(g_t \approx -e^{-m}\) 一直准确到下溢。

训练中各个 logit 怎么变。梯度下降沿 \(-\mathbf g\) 的方向更新 logit。目标类的 \(g_t = q_t - 1 < 0\),所以 \(z_t\) 被推高;非目标类的 \(g_j = q_j > 0\),所以每个 \(z_j\) 被压低。因为 \(\sum_k g_k = 0\),推高 \(z_t\) 的量正好等于压低所有 \(z_j\) 的总量,logit 的平均值不变,变的是差值 \(z_j - z_t\):它越来越负,margin \(m\) 越来越大。推力本身是 \(q_j = e^{z_j - z_t}/(1+e^{-m})\),随 margin 指数衰减,所以 \(z_j\) 被压低得越来越慢。实际训练更新的是网络参数,logit 只是间接改变,但方向由这里的梯度决定。越过悬崖后,标准写法中 \(z_t\) 的推力变成 0,\(z_j\) 仍被压低,两边不再平衡。

“[Softmax Collapse] rounds the target logit’s gradient to zero if that one logit is much larger than the others”摘要

动手看悬崖:拖动 margin,比较两种写法

标准修复

每一步运算后都按所选格式舍入(fp32 用 Math.fround,bf16 在 fp32 基础上按就近偶数截到 7 位尾数)。初始值 m = 17.3 对应论文的例子 z = [9, −9, −9]:m = 18 − ln 2。

用 PyTorch 在 fp32 下复现论文的例子(修复函数的实现见第 5 节):

# z = [9.0, -9.0, -9.0], t = 0, fp32, PyTorch 2.11
F.log_softmax(z, dim=-1)     # -> [ 0.0,       -18.0,     -18.0    ]
F.cross_entropy(z, t)        # -> 0.0,       grad [ 0.0,       1.523e-8,  1.523e-8]   梯度和 > 0
log_softmax_fixed(z)         # -> [-3.046e-8,  -18.0,     -18.0    ]
cross_entropy_fixed(z, t)    # -> 3.046e-8,  grad [-3.046e-8,  1.523e-8,  1.523e-8]   梯度和 = 0

2.2 Softmax Cancellation:学生等于老师时梯度不为 0

知识蒸馏和 KL 正则里,目标分布来自另一组 logit:\(\mathbf p = \operatorname{softmax}(\mathbf z_T)\)。标准实现分别算出 \(\mathbf q = \operatorname{softmax}(\mathbf z_S)\) 和 \(\mathbf p\),再相减得到梯度 \(\tilde{\mathbf g} = \mathbf q - \mathbf p\)。这里有两个问题:

  1. 相近数相减。学生 logit 逼近老师时,\(\boldsymbol\delta = \mathbf z_S - \mathbf z_T\) 很小,但 \(\mathbf q\) 和 \(\mathbf p\) 先被舍入成概率,\(\boldsymbol\delta\) 的信息在相减前已经丢了。
  2. 前向与反向的公式不同。前向用 \(\operatorname{softmax}(\mathbf z_T)\) 算 \(\mathbf p\),反向用 \(\exp(\operatorname{logsoftmax}(\mathbf z_S))\) 算 \(\mathbf q\)。两条运算路径的舍入误差不同,即使 \(\mathbf z_S = \mathbf z_T\),\(\mathbf q - \mathbf p\) 也不为 0。
“slight differences between the forward-pass softmax and backward-pass log-softmax formulas leave a non-zero gradient even when zS = zT”§1

论文的例子:老师 logit \(\mathbf z_T = [2, 1, 0]\),学生在 fp32 下差一个微小扰动 \([-10^{-7}, 10^{-7}, 0]\)。

实现g1g2g3问题
标准 soft-target CE(fp32,论文中 JAX 的结果)−5.960464e-81.451589e-8−1.123343e-9第 2 项缩小约 3 倍,第 3 项符号反了,和不为 0
修复版(fp32)−4.595504e-84.144190e-84.513142e-9与 fp64 精确值一致
精确值(fp64)−4.595504e-84.144190e-84.513142e-9
Figure 1:两个 bug。左:目标梯度绝对值随 margin 指数下降,标准实现在各精度的悬崖处跌到 0;右:学生–老师 logit 差趋于 0 时,真实梯度趋于 0,标准实现停在非零噪声底。
原文 Figure 1 (a) Softmax Collapse:目标梯度 \(|g_t|\) 本应随 margin 指数下降(虚线斜坡),标准实现(实线)在 bf16、fp32、fp64 的悬崖 5.5、16.6、36.7 处直接跌到 0。(b) Softmax Cancellation:学生–老师 logit 差 \(\|\mathbf z_S - \mathbf z_T\|_\infty\) 趋于 0 时,真实梯度范数随之趋于 0;标准实现停在一个非零的平台上,bf16 约 \(10^{-3}\),fp32 约 \(10^{-8}\)。

3现象:Slingshot 尖峰

Grokking(Power et al., 2022)指网络很快记住训练集(\(q_t \to 1\)),很久以后才在测试集上泛化。Thilak et al.(2022)在模加法上用 Adam 训练时观察到 Slingshot 机制:记住训练集之后,训练 loss 在接近 0 的平台和突然的尖峰之间反复交替,尖峰常与测试准确率的跳升同时出现。这篇论文把它归因到 Softmax Collapse。

“These Slingshot spikes are a direct consequence of Softmax Collapse”§4

关键在 AdamW 的更新 \(\theta \leftarrow \theta - \eta\,\hat m / (\sqrt{\hat v} + \epsilon) - \lambda\theta\)(写法同原文):它用梯度平方的滑动平均 \(\hat v\) 做归一化。一个循环如下:

  1. 越过悬崖记住训练集后 margin 持续增长。一批样本都越过 \(c_d\) 后,目标梯度被舍为 0,参数梯度几乎消失。
  2. 二阶矩衰减梯度接近 0,Adam 的 \(\hat v\) 按 \(\beta_2\) 指数衰减到很小;权重衰减同时慢慢缩小参数。
  3. 梯度恢复权重衰减或残余的非目标梯度把部分样本拉回悬崖以下,目标梯度 \(\approx -e^{-m}\) 突然恢复。
  4. 巨步更新恢复的梯度除以极小的 \(\sqrt{\hat v} + \epsilon\),得到一个很大的参数步:训练 loss 尖峰,\(\hat v\) 被重置。

④ 之后 margin 重新增长,回到 ①,循环往复。

“Adam divides the restored gradients by a very small denominator”§4

修复后,目标梯度 \(-e^{-m}/(1+e^{-m})\) 在悬崖处不会归零,\(\hat m\) 与 \(\hat v\) 平滑地一同衰减,尖峰随之消失。

Figure 2:有无权重衰减时的 Slingshot。上:a+b mod 31,λ=1;下:a²+b² mod 97,λ=0。
原文 Figure 2 5 个随机种子,fp32。上行(\(a+b \bmod 31\),\(\lambda = 1\),\(\epsilon = 10^{-8}\)):标准 CE 的 margin 撞上 \(c_{32} \approx 16.6\) 后反复跌落、回弹,grokking 变慢变差;修复 CE 平滑越过悬崖,5 个种子都更快 grok,margin 停在 \(\approx 18.4\)。下行(\(a^2+b^2 \bmod 97\),\(\lambda = 0\),\(\epsilon = 10^{-16}\)):约第 1250 步撞上悬崖的一次尖峰,把标准 CE 的测试准确率从 61% 打到 23%,修复 CE 平滑升到 85%。

4影响

4.1 尖峰是帮倒忙还是帮忙,取决于权重范数怎么控制

LayerNorm,无权重衰减

尖峰看起来有益

输出层前有 LayerNorm 时,放大权重不改变 logit,但会缩小有效步长 \(\eta/\|\theta\|_2\)。每次尖峰都会放大 \(\|\theta\|_2\),相当于一次意外的学习率衰减。

有权重衰减(λ = 1)

尖峰减慢 grokking

权重范数已由权重衰减控制,尖峰只会拖慢并损害泛化:\(a+b \bmod 31\) 上测试准确率为 94 ± 5%,修复后为 100 ± 0%。

两者都没有

一次尖峰就是灾难

\(a^2+b^2 \bmod 97\) 上测试准确率从 61% → 23%,\(a^3+b \bmod 97\) 上从 60% → 21%;修复版继续升到 85%。

“each Slingshot spike inflates ‖θ‖2 and acts as an accidental learning-rate decay”§4

这解释了为什么先前文献对 Slingshot 的评价不一:有人看到它伴随泛化,有人看到它破坏训练,区别在于实验设置里是否有 LayerNorm 和权重衰减。

4.2 修复之后,margin 停在哪:\(m^\star \approx \log(1/\epsilon)\)

Figure 2 上行里,修复版越过悬崖后 margin 停在 18.4 附近,不再增长;没有权重衰减时(下行)则一直增长。这个平台与浮点精度无关:

“This plateau has nothing to do with fp32 precision”§5

推导分三步。

  1. 平稳点条件。参数不再变化时,滑动平均收敛到 \(\hat m_i = g_i\),\(\sqrt{\hat v_i} = |g_i|\)。令 AdamW 更新为 0:
    \[ \frac{g_i}{|g_i| + \epsilon} + \lambda\theta_i = 0 \quad \forall i . \]式 (10)
    学习率 \(\eta\) 和 \(\beta_1, \beta_2\) 都消掉了,所以 RMSProp 和 LaProp 有同样的平稳点。
  2. 归一化步长随 margin 衰减。记住训练集后梯度随 margin 指数缩小,\(|g_i| \propto e^{-m}\)。若 \(\epsilon = 0\),归一化后的步长 \(g_i/|g_i| = \operatorname{sign}(g_i)\) 恒为 1,margin 会一直涨到梯度下溢;若 \(\epsilon > 0\),当 \(|g_i| \gg \epsilon\) 时步长约为 1,当 \(|g_i| \ll \epsilon\) 时步长约为 \(g_i/\epsilon \propto e^{-m}\)。
  3. 与权重衰减平衡。缩小的步长与 \(\lambda\theta_i\) 平衡需要 \(|g_i| \approx \epsilon\lambda|\theta_i|\),代入 \(|g_i| \propto e^{-m}\):
    \[ m^\star(\epsilon) \approx \log(1/\epsilon) . \]式 (11)

把三种格式的悬崖和几个 \(\epsilon\) 的平衡点画在同一条 margin 轴上,就能看出标准 CE 什么时候出事:平衡点在悬崖右边,margin 就会在到达平衡之前先撞上悬崖。

margin 轴上的精度悬崖与平衡点 bf16 悬崖 5.5 fp32 悬崖 16.6 fp64 悬崖 36.7 ε=1e-7 → 16.1 ε=1e-8 → 18.4 ε=1e-12 → 27.6 ε=1e-16 → 36.8 橙线:标准 CE 的梯度在这里被舍为 0;蓝点:修复 CE 在 AdamW + 权重衰减下的平衡 margin log(1/ε)。横轴为 logit margin m。
  • 默认 \(\epsilon = 10^{-8}\) 恰好坏在 fp32:平衡点 18.4 在 fp32 悬崖 16.6 的右边,所以标准 CE 在 fp32 下几乎必然触发 Slingshot。若 \(\epsilon \ge 10^{-7}\),平衡点 \(\le 16.1\),在悬崖左边,标准 CE 不会出尖峰。
  • bf16 的悬崖 5.5 在所有平衡点左边:标准 CE 在 bf16 下总会卡在 5.5 附近(表 1 中 bf16 标准 CE 的最终 margin 为 5.1–5.8)。
  • fp64 也不安全:\(\epsilon = 10^{-16}\) 时平衡点 36.8 刚好越过 fp64 悬崖 36.7,fp64 标准 CE 也会出问题。
Figure 3:ε=1e-8 时 Adam 归一化步长随 margin 的变化。
原文 Figure 3 \(\epsilon = 10^{-8}\) 时 Adam 的归一化步长 \(|g_t|/(|g_t|+\epsilon)\) 随 margin 的变化。修复 CE 的步长在 \(\log(1/\epsilon) \approx 18.4\) 附近平滑衰减;标准 CE 在 \(c_{16} \approx 5.5\) 和 \(c_{32} \approx 16.6\) 处直接归零。
Figure 4:不同 ε 下修复 CE 的 margin 轨迹,有无权重衰减。
原文 Figure 4 修复 CE + LaProp,\(a+b \bmod 31\),fp32。实线为 3 个种子,虚线为式 (11) 的预测。(a) 有权重衰减(\(\lambda = 1\)):每个 \(\epsilon > 0\) 都沿着 \(\epsilon = 0\) 的轨迹上升,然后停在预测的 \(\log(1/\epsilon)\);\(\epsilon = 0\) 一直涨到 fp32 梯度下溢的 \(m \approx 81.2\)。(b) 无权重衰减:没有东西平衡 \(g_i/\epsilon\) 的步长,margin 越过 \(\log(1/\epsilon)\) 后继续漂移上升。

4.3 跨精度、跨基准

设置沿用 Varma et al.(2023):1 层、宽 64 的 ReLU Transformer,\(a+b \bmod 31\),50% 训练集,全批量 AdamW 训练 10,000 epoch。只改变损失函数、浮点格式和 \(\epsilon\),另外对比两种已有的补救方法。

方法ε = 1e-81e-121e-16最终 margin(1e-8 / 1e-12 / 1e-16)
fp32 修复 CE100.0 ± 0.099.6 ± 0.799.0 ± 2.317.9 / 27.4 / 36.8
fp32 标准 CE94.3 ± 5.496.4 ± 5.196.5 ± 4.313.0 / 15.1 / 12.7
bf16 修复 CE100.0 ± 0.199.8 ± 0.298.9 ± 2.417.9 / 27.4 / 36.8
bf16 标准 CE93.3 ± 7.494.9 ± 4.994.2 ± 8.35.1 / 5.6 / 5.8
fp64 修复 CE100.0 ± 0.099.5 ± 1.199.0 ± 2.017.9 / 27.4 / 36.8
fp64 标准 CE100.0 ± 0.0100.0 ± 0.197.2 ± 3.217.9 / 27.4 / 30.1
fp32 零和投影(Liu et al., 2026)97.9 ± 3.248.9 ± 2.649.3 ± 2.517.0 / 18.9 / 18.9
fp32 StableMax(Prieto et al., 2025)97.3 ± 4.795.6 ± 2.097.3 ± 4.33200 / 5661 / 6313
fp64 StableMax(Prieto et al., 2025)97.4 ± 4.296.7 ± 5.395.6 ± 2.73170 / 21252 / 2322

原文 Table 1,测试准确率(%),5 个种子。修复 CE 的最终 margin 与预测的 \(\log(1/\epsilon) = 18.4, 27.6, 36.8\) 吻合,且三种精度完全相同;标准 CE 停在各自悬崖附近。

读表的三个要点:

  • 修复后,bf16 和 fp64 的结果一样好。这里的失败来自实现,而非低精度本身。
  • 零和投影把梯度均值减掉,强制和为 0。但目标梯度已经是 0 时,它只分到非目标梯度总和的 \(-1/K\),margin 卡在 18 附近,\(\epsilon \le 10^{-12}\) 时准确率跌到 50% 以下。
  • StableMax 把 softmax 里的 \(\exp(x)\) 换成分段有理函数(\(x \ge 0\) 时为 \(x+1\),\(x < 0\) 时为 \(1/(1-x)\))。它线性增长,margin 涨到 \(10^3\)–\(10^4\),准确率在所有 \(\epsilon\) 下都低于修复 CE。
Figure 5:不同精度下标准 CE 与修复 CE 的 margin、训练 loss、测试准确率。
原文 Figure 5 上:\(\epsilon = 10^{-8}\),标准 CE 在 bf16 卡在 \(c_{16}\),在 fp32 于 \(c_{32}\) 反复尖峰;修复 CE 三种精度都平滑到达 18.4。下:\(\epsilon = 10^{-16}\),平衡点升到 36.8,连 fp64 标准 CE 也在 \(c_{64}\) 处失败。
Figure 6:四个 grokking 基准上的结果。
原文 Figure 6 文献中的四个 grokking 基准,\(\epsilon = 10^{-12}\):1 层 Transformer(\(a+b \bmod 113\)、\(a+b \bmod 31\))和 2 层 MLP(\(a^2+b^2 \bmod 97\)、\(a^3+b \bmod 97\))。fp32 修复 CE(蓝)与 fp64 标准 CE(红)重合,fp32 标准 CE(橙)在 \(c_{32}\) 处反复失败。

4.4 蒸馏:学生从老师身边漂走

实验设计很直接:先把老师训到 \(a+b \bmod 53\) 上 100% 测试准确率,再用老师的权重初始化学生(\(\theta_S^{(0)} = \theta_T\)),在温度 \(T = 4\) 下用 Adam(\(\eta = 2\times10^{-3}\),\(\lambda = 0\),\(\epsilon = 10^{-8}\))蒸馏 2000 步。初始时 \(\mathbf z_S = \mathbf z_T\),真实梯度为 0,学生本该一动不动。

标准实现在 \(\mathbf z_S = \mathbf z_T\) 处留有残余梯度。只要残余大于 \(\epsilon\),Adam 就会把它归一化成大小约为 \(\eta\) 的步长:

格式残余梯度与 ε = 1e-8 比结果
fp64~1e-17远小于 ε学生基本不动,KL ≈ 0
fp32~1e-8与 ε 相当KL 短暂尖峰
bf16~1e-3远大于 ε学生被推离老师,测试准确率下降

修复版在 \(\mathbf z_S = \mathbf z_T\) 时梯度精确为 0,三种精度下学生都停在 KL = 0、准确率 100%。

Figure 7:学生用老师权重初始化时的蒸馏,held-out KL 与测试准确率。
原文 Figure 7 (a) held-out \(\mathrm{KL}(\mathbf p_T \,\|\, \mathbf q_S)\);(b) 测试准确率。修复 soft-CE 在所有精度下都保持 KL = 0;标准 soft-CE 在 bf16 下 KL 持续上升、准确率下降,在 fp32 下出现一次 KL 尖峰。

5修复:在 log 空间里重写,永远不把 1 加到小数上

5.1 修 Softmax Collapse:用 softplus 代替 1 + e−m

记号 LSE。LSE 是 log-sum-exp 的缩写:先对每个数取指数,求和,再取对数,

\[ \operatorname{LSE}(x_1, \dots, x_n) = \log \sum_{i=1}^{n} e^{x_i}, \qquad \max_i x_i \;\le\; \operatorname{LSE}(x_1, \dots, x_n) \;\le\; \max_i x_i + \log n . \]

右边的不等式说明 LSE 是“平滑的最大值”:结果总落在最大值和最大值加 \(\log n\) 之间。框架里的 logsumexp 会先减去最大值再取指数,所以不会上溢。用 LSE 可以把式 (2) 的 margin 改写成只含 logit 差的形式:

\[ \begin{aligned} \operatorname*{LSE}_{j \ne t}(z_j - z_t) &= \log \sum_{j \ne t} e^{z_j - z_t} && \text{LSE 的定义} \\ &= \log\Big(e^{-z_t} \sum_{j \ne t} e^{z_j}\Big) && e^{z_j - z_t} = e^{z_j} e^{-z_t}\text{,提到求和外} \\ &= -z_t + \log \sum_{j \ne t} e^{z_j} && \log(ab) = \log a + \log b,\; \log e^{-z_t} = -z_t \\ &= -m && \text{式 (2):} m = z_t - \log \textstyle\sum_{j \ne t} e^{z_j} \end{aligned} \]

所以 \(m = -\operatorname{LSE}_{j \ne t}(z_j - z_t)\)。以 \(\mathbf z = [9, -9, -9]\)、\(t = 1\) 为例:两个非目标类的差都是 \(-18\),\(\operatorname{LSE}(-18, -18) = \log(2e^{-18}) = -18 + \ln 2 \approx -17.3\),于是 \(m \approx 17.3\),与前面一致。

只有最大的 logit 被平移成 0(贡献分母里的那个 1)。修复的做法是把它从 log-sum-exp 中拿出来,单独算 margin,再用数值稳定的 \(\operatorname{softplus}(x) = \log(1+e^x)\) 合并:

\[ \tilde z_k = \mathbb 1[k \ne i_{\max}]\,(z_k - z_{i_{\max}}), \qquad m = -\operatorname*{LSE}_{j \ne i_{\max}} \tilde z_j, \qquad \operatorname{logsoftmax}(\mathbf z)_k = \tilde z_k - \operatorname{softplus}(-m) . \]式 (5)

硬目标交叉熵可以直接用 \(t\) 代替 \(i_{\max}\),省掉 argmax:

\[ m = -\operatorname*{LSE}_{j \ne t}(z_j - z_t), \qquad \mathcal L(\mathbf z, t) = \operatorname{softplus}(-m) . \]式 (6)

关键在于:第 \(t\) 个位置被替换成常数 0,而不是 \(z_t - z_t\),于是 \(z_t\) 只通过 \(K-1\) 个差 \(z_j - z_t\) 进入损失。自动微分给出的目标梯度自然等于非目标梯度之和的相反数,和为 0 由构造保证,不依赖舍入:

\[ g_j = \frac{e^{z_j - z_t}}{1+e^{-m}} \;(j \ne t), \qquad g_t = -\sum_{j \ne t} g_j = -\frac{e^{-m}}{1+e^{-m}} . \]式 (7)

把两种实现逐步摆在一起,以 \(\mathbf z = [9, -9, -9]\)、\(t = 1\)、fp32 为例,每格下方是实际算出的数值(PyTorch 2.11)。橙色格是舍入出错的步骤:

步骤修复前(标准实现)修复后
① 平移\(\tilde z_k = z_k - z_t\),目标位置 \(\tilde z_t = z_t - z_t = 0\)
[0, −18, −18]
只对非目标位置做差 \(z_j - z_t\);目标位置直接放常数 0
[0, −18, −18],其中 0 不依赖 z_t
② 合并\(S = \sum_k e^{\tilde z_k} = 1 + e^{-m}\):把 \(e^{-m}\) 加到 1 上
1 + 3.046e-8 → 舍入为 1.0
\(m = -\operatorname{LSE}_{j \ne t}(z_j - z_t)\):只对非目标项求 log-sum-exp,不出现 1
m = 18 − ln 2 = 17.307
③ 损失\(\mathcal L = -\tilde z_t + \log S = \log(1 + e^{-m})\)
log 1.0 = 0(真值 3.046e-8)
\(\mathcal L = \operatorname{softplus}(-m) = \operatorname{log1p}(e^{-m})\)
3.046e-8
④ 目标梯度\(g_t = q_t - 1 = 1/S - 1\)
1.0 − 1 = 0(真值 −3.046e-8)
\(g_t = -\sum_{j \ne t} g_j\),由自动微分经 \(z_j - z_t\) 得到
−3.046e-8
⑤ 非目标梯度\(g_j = e^{z_j - z_t}/S\)
1.523e-8(分子准确)
\(g_j = e^{z_j - z_t}/(1 + e^{-m})\)
1.523e-8
⑥ 梯度和\(\sum_k g_k = e^{-m}/(1+e^{-m})\)
3.046e-8 ≠ 0
\(\sum_k g_k = 0\),由构造保证
0

左边是 PyTorch 内置函数(修复前),右边是按论文式 (5)–(7) 写的修复版。两边接口相同,可以直接替换。

修复前 · PyTorch 内置
import torch
import torch.nn.functional as F

# 直接调用内置函数
logp = F.log_softmax(z, dim=-1)
loss = F.cross_entropy(z, t)

# 内置实现等价于:
#   前向  log_softmax = (z - z_max) - log Σ exp(z - z_max)
#         目标位置 = 0 - log(1 + e^-m)
#         m 越过悬崖后 1 + e^-m 舍成 1,loss = 0
#   反向  grad = softmax(z) - onehot(t)
#         q_t 舍成 1,目标梯度 = 1 - 1 = 0
#         非目标梯度 q_j 仍为正,梯度和 > 0
修复后
import torch
import torch.nn.functional as F

def log_softmax_fixed(z):
    i_max = z.argmax(dim=-1, keepdim=True)
    z_max = z.gather(-1, i_max)
    is_max = torch.zeros_like(z, dtype=torch.bool)
    is_max.scatter_(-1, i_max, True)
    # argmax 处放常数 0,不经过 z_max - z_max
    shifted = torch.where(is_max, torch.zeros_like(z), z - z_max)
    # 把 argmax 排除出求和,单独算 margin
    others = shifted.masked_fill(is_max, float("-inf"))
    m = -torch.logsumexp(others, dim=-1, keepdim=True)
    # softplus(-m) = log(1 + e^-m),不把 e^-m 加到 1 上
    return shifted - F.softplus(-m)

def cross_entropy_fixed(z, t):
    z_t = z.gather(-1, t[..., None])
    is_t = F.one_hot(t, z.shape[-1]).bool()
    # 把目标排除出求和:m = -LSE_{j≠t}(z_j - z_t)
    others = (z - z_t).masked_fill(is_t, float("-inf"))
    m = -torch.logsumexp(others, dim=-1)
    return F.softplus(-m)    # = -log_softmax(z)[t]

在 fp32 下用论文的例子 \(\mathbf z = [9, -9, -9]\)、\(t = 0\) 运行(PyTorch 2.11,CPU):

# loss 及 z.grad
F.cross_entropy(z, t)        # -> 0.0,       [ 0.0,       1.523e-8,  1.523e-8]   梯度和 = 3.046e-8
cross_entropy_fixed(z, t)    # -> 3.046e-8,  [-3.046e-8,  1.523e-8,  1.523e-8]   梯度和 = 0
F.log_softmax(z, dim=-1)     # -> [ 0.0,       -18.0,     -18.0    ]
log_softmax_fixed(z)         # -> [-3.046e-8,  -18.0,     -18.0    ]

一个容易误解的细节:如果把标准公式 (z - z_max) - log(exp(z - z_max).sum()) 自己写出来再交给 autograd,前向的 loss 仍会舍成 0,但 autograd 会经过 z_max 的梯度把目标梯度算对。梯度被清零的是内置函数的融合反向 softmax(z) - onehot(t),它先把 \(q_t\) 舍成 1 再减 1。

5.2 修 Softmax Cancellation:直接从 logit 差算 q − p

不再分别算两个概率再相减,而是把老师 logit 直接传进损失函数,用 \(\boldsymbol\delta = \mathbf z_S - \mathbf z_T\) 在 log 空间里算 \(\mathbf q - \mathbf p\)。借助 \(\operatorname{expm1}(x) = e^x - 1\) 和 \(\operatorname{log1p}(x) = \log(1+x)\),全程不出现“加 1”或“减 1”:

\[ d_k = \delta_k - \sum_i p_i\delta_i, \qquad s_k = \log\frac{q_k}{p_k} = d_k - \operatorname{log1p}\Big(\sum_i p_i \operatorname{expm1}(d_i)\Big), \qquad u_k = q_k - p_k = p_k \operatorname{expm1}(s_k) . \]式 (9)

因为 \(\sum_i p_i d_i = 0\) 且 \(\operatorname{expm1}(x) \ge x\),log1p 的输入总是非负。最后再做一次 \(g_k = u_k - q_k \sum_i u_i\),消去剩余的浮点漂移,保证和为 0。\(\boldsymbol\delta = 0\) 时 \(\mathbf d = \mathbf s = \mathbf u = \mathbf 0\),梯度精确为 0。

同样逐步对照,以 §2.2 的例子为例(\(\mathbf z_T = [2, 1, 0]\),fp32,PyTorch 2.11);第 ⑥ 行换成学生等于老师的情形:

步骤修复前(标准实现)修复后
① 起点先分别算概率 \(\mathbf q = \exp(\operatorname{logsoftmax}(\mathbf z_S))\),\(\mathbf p = \operatorname{softmax}(\mathbf z_T)\),各自舍入
p ≈ [0.665, 0.245, 0.090]
直接在 logit 上做差 \(\boldsymbol\delta = \mathbf z_S - \mathbf z_T\)
δ = [−1e-7, 1e-7, 0]
② 居中—\(d_k = \delta_k - \sum_i p_i \delta_i\)
③ 对数比—\(s_k = \log(q_k/p_k) = d_k - \operatorname{log1p}\big(\sum_i p_i \operatorname{expm1}(d_i)\big)\)
④ 求差\(g_k = q_k - p_k\):两个约 0.1–0.7 的数相减,结果约 \(10^{-8}\),有效位几乎全部抵消
[−5.960e-8, 4.470e-8, 7.451e-9]
\(u_k = q_k - p_k = p_k \operatorname{expm1}(s_k)\):expm1 对小量精确
[−4.596e-8, 4.144e-8, 4.513e-9]
⑤ 零和不保证
和 = −7.5e-9
\(g_k = u_k - q_k \sum_i u_i\)
和 ≈ 2e-15
⑥ \(\mathbf z_S = \mathbf z_T\) 时两条路径舍入不同,\(\mathbf q \ne \mathbf p\)
[5.960e-8, 1.490e-8, 7.451e-9]
\(\boldsymbol\delta = 0 \Rightarrow \mathbf d = \mathbf s = \mathbf u = \mathbf g = 0\)
[0, 0, 0]
修复前 · 标准写法
import torch
import torch.nn.functional as F

def soft_target_ce_std(z_S, z_T):
    # 前向用 softmax 算 p
    p = torch.softmax(z_T, dim=-1)
    return -(p * F.log_softmax(z_S, dim=-1)).sum(-1)

# 反向:grad = q - p
#   q 来自 exp(log_softmax(z_S)),p 来自 softmax(z_T)
#   两个已舍入的概率相减,z_S ≈ z_T 时丢精度
#   两条路径舍入不同,z_S = z_T 时也不为 0
修复后
def soft_target_ce_fixed(z_S, z_T):
    log_q = log_softmax_fixed(z_S)
    q, p = log_q.exp(), log_softmax_fixed(z_T).exp()
    delta = z_S - z_T
    # 在 p 下居中
    d = delta - (p * delta).sum(-1, keepdim=True)
    # s = log(q / p),用 expm1 / log1p 避免加减 1
    s = d - torch.log1p((p * torch.expm1(d)).sum(-1, keepdim=True))
    # u = q - p
    u = p * torch.expm1(s)
    # 消去浮点漂移,保证 sum(g) = 0
    g = u - q * u.sum(-1, keepdim=True)
    # 前向值不变,反向梯度替换成 g
    loss = -(p * log_q.detach()).sum(-1)
    return loss + (g.detach() * (z_S - z_S.detach())).sum(-1)

用 §2.2 的例子运行(fp32,PyTorch 2.11,CPU)。PyTorch 标准写法的数值与论文的 JAX 结果不完全相同,但问题一样:第 3 项偏大约 65%,梯度和不为 0,学生等于老师时梯度也不为 0。

# z_T = [2, 1, 0],z_S = z_T + [-1e-7, 1e-7, 0]:z_S.grad
soft_target_ce_std     # -> [-5.960e-8,  4.470e-8,  7.451e-9]   和 = -7.5e-9
soft_target_ce_fixed   # -> [-4.596e-8,  4.144e-8,  4.513e-9]   和 ≈ 2e-15
# 精确值(fp64)           [-4.596e-8,  4.144e-8,  4.513e-9]

# z_S = z_T:真实梯度为 0
soft_target_ce_std     # -> [ 5.960e-8,  1.490e-8,  7.451e-9]
soft_target_ce_fixed   # -> [ 0.0,       0.0,       0.0     ]

5.3 与已有补救方法比较

方法做法局限
转成 fp64(Nanda et al., 2023;Varma et al., 2023)在更高精度下算 softmax 和 CE只是把悬崖从 16.6 推到 36.7;\(\epsilon = 10^{-16}\) 时 fp64 也失败
零和投影(Liu et al., 2026)梯度减去均值,强制和为 0目标梯度为 0 时只分到 \(-1/K\),margin 卡在约 18
StableMax(Prieto et al., 2025)用线性增长的函数代替 exp改变了损失本身,margin 涨到 \(10^3\)–\(10^4\),准确率更低
本文用 softplus / expm1 / log1p 在 log 空间重写损失在数学上不变,可以直接替换,没有明显的额外开销

6讨论

作者自己也指出了适用范围:

“Though the extreme margins and near-zero student–teacher gaps probed by our experiments are uncommon in standard large-scale training, our fixed functions are easy drop-in replacements”§8
  1. 大模型训练会碰到悬崖吗?实验全部在小型 grokking 任务上。LLM 预训练里 CE 通常在什么精度下算?高频、确定性强的 token 的 margin 能否超过 16.6?如果 logits 在 bf16 下算 CE,悬崖只有 5.5,即目标类概率约 99.6%。
  2. RL 微调里的 KL 项是 Softmax Cancellation 的典型场景。训练初期策略 \(\pi\) 与参考模型 \(\pi_{\text{ref}}\) 几乎相同,KL 及其梯度本应接近 0。PPO / GRPO 的重要性比率 \(\pi/\pi_{\text{old}}\) 在每轮第一个更新步也恰好等于 1。我们的实现是怎么算这两项的?
  3. Adam 的 \(\epsilon\) 隐式限定了自信程度。式 (11) 说明 \(\epsilon\) 与权重衰减共同决定 margin 的上限 \(\log(1/\epsilon)\)。它和 label smoothing、z-loss 这类显式控制 logit 的手段是什么关系?调 \(\epsilon\) 是否等价于调一个正则项?
  4. “低精度不稳定”有多少是实现问题?修复后 bf16 与 fp64 结果一样。我们见过的 bf16 训练尖峰里,有多少可以归到类似的“加 1 再减 1”式写法?

7参考

  • Barron, Gnanapragasam, Tschannen. A Fix for Softmax and Cross-Entropy. arXiv 2610.31642, 2026.
  • Thilak et al. The Slingshot Mechanism: An Empirical Study of Adaptive Optimizers and the Grokking Phenomenon. arXiv 2206.04817, 2022.
  • Power et al. Grokking: Generalization Beyond Overfitting on Small Algorithmic Datasets. arXiv 2201.02177, 2022.
  • Prieto et al. Grokking at the Edge of Numerical Stability(StableMax). ICLR 2025.
  • Liu et al. Grokking or Glitching? How Low-Precision Drives Slingshot Loss Spikes(零和投影). arXiv 2605.06152, 2026.
  • Varma et al. Explaining Grokking through Circuit Efficiency. arXiv 2309.02390, 2023.
  • Nanda et al. Progress Measures for Grokking via Mechanistic Interpretability. ICLR 2023.
  • Doshi et al. Grokking Modular Polynomials. arXiv 2406.03495, 2024.
  • Hinton, Vinyals, Dean. Distilling the Knowledge in a Neural Network. arXiv 1503.02531, 2015.
图、表和引文均来自原文(arXiv 2610.31642v1),代码由论文的 JAX 实现改写为 PyTorch 并实际运行验证,点击可跳到原文对应位置。精度悬崖的公式 (p+1) ln 2、margin 轴示意图和交互演示为本页所加,演示中的数值由浏览器按所选格式逐步舍入算出。
- views

Comments