LAB / FLOATING-POINT EXPERIMENT

Contents / 目录

一个反直觉实验:同加一个常数,为什么FP16的Softmax会变?

结论:Softmax在实数中对共同平移不变,但“先平移、再转FP16”可能抹掉Logit之间的差。后面即使用float64算稳定Softmax,也无法恢复已经丢失的区分。

类型:趣味实验 / 数值工程。发布与实验:2026-10-26。本篇只运行NumPy CPU玩具实验,没有调用LLM,没有测GPU性能,没有证明某个模型必然答错。研究支持来自浮点标准与官方数值文档,而不是把本实验包装成模型论文。

1|直觉预期:数学上这个操作应该什么都不改变

三个候选答案Logits为[0,0.25,0.5]。Stable Softmax约为:

候选索引 0 1 2
概率 0.254275 0.326496 0.419229

所有Logits同加c,分子分母同时乘exp(c),所以概率不变。理论上同加256、512、1024都一样。[1]

实验问题: 如果加完常数后转成FP16呢?这里c是对已有数字的统一平移,不是给Prompt添加一句话;修改Prompt会改变模型计算,不能类比为所有Logit同加常数。

2|实验设置:故意把误差位置隔离出来

本机环境:macOS27.0.1 arm64、Python3.12.2、NumPy2.4.6;CPU,无随机模型权重、无GPU、无API费用。

实验链路:

  1. 在float64中建立精确可表示的[0,0.25,0.5]。
  2. 同加c,转一次float16,再转回float64。
  3. 后续指数和归一化全部在float64中进行,避免把Half指数误差混进来。
  4. 对照组:全float64;另一个对照:先减行最大值,再转float16。

这不是原生全FP16 Softmax实现,而是Half输入表示对结果的影响实验。故意用高精度后处理,是为了说明丢失发生在转换时。

3|本地实测:1024让三个数“变成同一个数”

c FP16舍入后,再转float64的Logits 概率p₀ / p₁ / p₂ 最大概率误差 NumPy argmax
0 [0,0.25,0.5] .254275 / .326496 / .419229 0 2
256 [256,256.25,256.5] 同上 0 2
512 [512,512,512.5] .274069 / .274069 / .451863 .052427 2
1024 [1024,1024,1024] .333333 / .333333 / .333333 .085896 0

最后的“答案变了”是玩具候选argmax变为Tie,NumPy选第一个最大位置。不是证实真实模型会固定选错某个词。更准确的现象是:原本明确的Preference消失,Tie-breaking决定输出。

两个对照均通过: 对本组输入,float64全程平移误差为0;先居中再转FP16误差也为0。后者结果恰好精确,是因为本例居中值[−0.5,−0.25,0]均能精确表示;不保证任意随机输入为零。

4|为什么会这样:越大不等于越精细

IEEE binary16有10个显式Fraction Bit,加上正常数隐藏首位,有11个有效二进制位。对于正正常数区间[2ᵉ,2ᵉ⁺¹),相邻可表示值的间隔为:

ULP = 2^(e − 10)

在256附近e=8,ULP=0.25;在512附近ULP=0.5;在1024附近ULP=1。0.25、0.5的差逐渐不够分辨:

  • 512.25在512和512.5之间,舍入到最近偶数策略选512。
  • 1024.25更靠近1024。
  • 1024.5是1024与1025的中点,本实验舍入到1024。

这样输入进入Softmax时已经全相同。稳定Softmax再减最大值会得到[0,0,0];它能避免exp溢出,不能重建原始Logit差值。[2]

原始差值0 / .25 / .5
→
+1024后转Half1024 / 1024 / 1024
→
高精度也救不回概率全为1/3

教学示意:信息丢失发生在Half转换,不是最后的指数运算。

5|第二个小实验:只有三个正数,括号也能影响结果

每一步加法结果都显式舍入到float16:

(2048 + 1) + 1 = 2048
2048 + (1 + 1) = 2050

2048附近ULP=2,2048+1处于中点,Round-to-even回到2048;再加1又回到2048。另一边先把1+1=2精确算出,再加2048得到可表示的2050。

这不是负数抵消、NaN或随机性。纯正数有限精度加法也不结合。 GPU Reduction Tree、Tile形状、Batch、Split-K与All-reduce顺序变化都有可能改变舍入,但是否足以影响模型决策,需要真正的模型实验,而不能由这三个数直接推出。[2][3]

6|可以自己玩:无模型、无网络依赖的重现实验

下载完整Python脚本与本次测量JSON。脚本还检验Online Softmax和两个Block合并的正确性,不计时GPU。

依赖为NumPy。使用已有Python环境;想固定本次版本时,可在独立虚拟环境安装numpy==2.4.6。运行:

python experiment.py

核心操作如下;完整脚本没有省略断言:

import numpy as np

logits = np.array([0.0, 0.25, 0.5], dtype=np.float64)
rounded = (logits + 1024).astype(np.float16).astype(np.float64)
exponentials = np.exp(rounded - rounded.max())
probabilities = exponentials / exponentials.sum()
print(rounded, probabilities)

三种安全延伸:

  1. 将差值改成1、2、4,观察多大Offset才抹去;先预测ULP再运行。
  2. 尝试随机Logits,统计平移前后Max Probability Error与Top-1 Margin变化;即使argmax不变,分布也可能变。
  3. 用float32重复,比较它更大的有效精度如何推迟而非消除问题。

这里不建议无理由提高整个真实推理流水线精度;应先定位信息在哪一层丢失,用最小范围修复。

7|反例与边界:这个Trick什么时候不成立?

  • Offset小、差值能表示:如256的本组数据,结果不变。
  • 在转低精度前居中:本例差值保留;但许多模型里大Logit已经由低精度算出,不能倒流恢复。
  • Kernel内部使用FP32累加/Softmax:可减少运算误差,但不能消除低精度输入原本的量化误差。
  • 真实LM greedy输出常直接对Logits argmax,不需要Softmax。本实验同样说明输入Tie变化,但不能归因于Softmax采样。
  • 差值够大的Top-1未必变化;Near-tie敏感并不等于所有请求敏感。
  • 用BF16时ULP不同:BF16有效精度低于FP16但指数范围更大,不能照搬本表阈值。
  • Deterministic模式控制执行选择/随机性,不等于提高浮点精度,也不保证不同Batch/后端Bitwise相等。

PyTorch官方明确说明数学等价计算、Batch与Slice、不同后端不保证逐位一致;其SDPA Math路径可能把FP16/BF16输入升为FP32再算,具体行为要看版本和Backend,而非把所有FP16模型称为“全Half加法”。[3]

8|对实际LLM工程有什么用?

不是Prompt魔法,是排查框架。 当不同Batch/Kernel让输出变化,先区分:

层次 应检查 本实验覆盖了吗?
输入表示 转换/量化是否抹掉差值 是,受控Half Cast
算术执行 累加精度、Reduction Order、FMA 仅三个Half加法例子
模型传播 误差经层数放大/抵消 没有跑模型
解码 Top-1 Margin、Tie-break、Sampling 只展示NumPy argmax Tie
系统评测 Seed、Batch、硬件、Runtime、Harness 提供方法,未得LLM Benchmark

具体可以记录每层误差、最终Top-1/Top-2间隔与有限值;固定模型、Prompt、Tokenizer、dtype、Backend与Batch做A/B。如果要声称某个改动导致模型准确率下降,需要足够任务样本、固定条件和对照,不是一条输出Screenshot。

与Online Softmax机制文章一起看,会得到同一个边界:数学Exact、数值稳定、Bitwise确定性、任务正确性是四件不同的事。

9|自己的判断与真正该记住的结论

有趣之处在于:一个在代数中完全无害的变换,经过“表示”这一层就不再等价。数值问题经常不在最后看起来最复杂的Softmax,而在此前一次看似普通的Cast或分块归约。

不要把实验当成“LLM不可靠”的泛化证明;把它当成Kernel/Quantization/Benchmark工作的直觉训练。最值得记住的是:高精度后处理无法恢复已经丢掉的输入信息;稳定算法也无法自动修复表示误差。

来源

  1. Online normalizer calculation for softmax,原文:标准Softmax和平移稳定形式;本文不是该论文的GPU性能重现。
  2. NVIDIA Floating Point and IEEE 754:有限表示、舍入与非结合性。
  3. PyTorch Numerical Accuracy:Batch/后端/SDPA与精度边界。
  4. 本地实验:Python脚本、原始结果JSON,2026-10-26,CPU NumPy;这是本篇自有证据,不是社区传闻或LLM论文实测。