引子
这篇文章改自我“文本理解”课程的pre。
参考的文章有两篇,第一篇是NeurIPS 2025 Oral的论文Give Me FP32 or Give Me Death? Challenges and Solutions for Reproducible Reasoning,第二篇是Thinking Machines提出的技术报告Defeating Nondeterminism in LLM Inference。
一个现象
我在一台服务器上,应用Llama 3.2做了实验,观察到了一个非常有趣的现象,当设置了temperature
为0时,并且固定一切随机数时,对于同一输入,输出10次,结果出现了截然不同的两类输出:
输入: What is Touhou Project?
输出1:The Touhou Project is a series of Japanese bullet hell shooter video games created by ZUN, a solo game developer and founder of Team Shanghai Alice. The series is known for its challenging gameplay, intricate
输出2:The Touhou Project is a series of Japanese bullet hell shooter video games created by ZUN, a solo game developer and the founder of Team Shanghai Alice. The series is known for its challenging gameplay,
这个现象和我们的一贯认知不同,因为,所有随机数都确定的情况下,输出就应该是一定的。
猜想之一:底层数字运算的不可靠性
模型底层的运算是数字的运算,那么既然输出结果有问题,就表明底层数字运算出现了问题。我们知道,浮点数没有办法表达出所有的数字来,因为浮点数本身依然是离散的。除此之外,浮点数也有舍入带来的误差,这就表明,底层的浮点数运算是不满足结合律的。在不同精度下,不同顺序的表现也不尽相同,比如当a,b,c分别是0.1,-0.1,和0.2时:
1 | a = 0.1 - 0.1 + 0.2 |
这告诉我们——不同的运算顺序,会带来不同的运算结果。CUDA的底层运算通常是使用所谓原子加法的,原子加法有利于保证每次运算的原子性,但是由于CUDA本身是“单指令多线程”的,同一个运算可能涉及多个加法,但是每个原子加法的完成时刻是不一样的,所以没有办法保证相加次序的确定性,这就导致了模型运算的不确定性,也就是输出的不确定性。那么很容易想到,一个浮点数表示法,越贴近结合律,则结果越好。
于是,作者在BF16、FP16和FP32三种精度上,在12种不同的硬件组合上,在三个数据集上,进行了测试,评价指标是运算准确性的标准差和输出长度的标准差。结果是,在所有的数据集上FP32由于精度更高,标准差也更低,这表示了,FP32能够在不同的设备之间,维持一个比较高的确定性。
此外,由于大模型是一个token一个token预测的,那么也容易想到,在贪心解码的过程中,如果出现了不确定的情况,就表明token的输出概率出现了问题,作者做了一个实验,观察前两个预测词之间的预测概率,发现二者的差值很低,这表明,如果出现了扰动,很容易就会对下一个token的预测带来影响。前两个词之间概率的差值分布,主要的差值集中在0.0附近,这也说明了之前的结论。
除此之外,作者在GPU数量、Batch Size和GPU类别上做了实验,发现,多卡会带来更不稳定的性能、低Batch Size更稳定,GPU型号也有影响。这三张图的评测指标依旧是标准差,越低越好。
好,到现在我们验证了三个观点——浮点数的表达不精确、下一个token的前两个词之间的概率差异很小、硬软件环境的影响,这三个痛点就是问题的原貌。浮点数的表达不精确,导致不满足结合律,底层的运算和运算次序有关,而下一个token的前两个,概率差很小,那么计算带来的扰动就会带来误差,导致了输出的不确定性。
猜想一的问题
还没结束,因为我们目前讨论的是不同机器上的情况,而我们的现象是在同一台机器上进行试验。我们可以通过这样一个程序验证我们推理的正确性,这段代码首先随机生成两个矩阵,然后100000次相加,如果我们的推理是正确的,如果底层的加法运算是不可靠的,每次运行都有自己的顺序,那么这段代码应该会报错——但是,结果看来,并没有:
1 | A = torch.randn(2048, 2048, device='cuda', dtype=torch.bfloat16) |
这是因为,现代的机器学习库用不上原子加法,通过一系列策略来实现确定性,比如对于加法的规约以及拆分等。能够保证每次矩阵乘法的运算是确定的。这就说明,我们的猜想不太对,因为在同一台机器上,底层的运算是精准的,这些问题根本带不来多样化的表现。
猜想之二:并行策略
第二篇文章的作者提供了一个很有意思的观点,在PyTorch中,以下这两种运算根本不相等:取左矩阵的第一行和矩阵相乘,以及两个大矩阵相乘完,取第一行,这两个运算在数学中是相等的,但是在PyTorch中不相等。矩阵的第一行的每一个元素都是第一行乘右矩阵的每一列得到的,所以,计算的次序不应该对结果带来影响,但是在PyTorch中,并不相等。通过下面这段代码,我们能够看到这一点,首先生成两个矩阵,然后按照上面的两个运算进行运算,求二者之差:
1 | import torch |
这表明,两个运算之间是有差值的。
这说明了很有意思的一点,batch_size对于结果是有影响的,这里的batch_size就好比矩阵的乘法的计算,取左矩阵第一行乘矩阵,和矩阵相乘完取第一行,分别对应着不同batch_size的运算。比如这个实验,我设置了两个输入,分别按照不同的batch_size输出,输出100次取最普遍的输出,结果如下,二者的输出并不相同:
输入:
- What is Touhou Project?
- What is Serial Experiments Lain?
输出(Batch_size = 1):Touhou Project is a series of bullet hell shooter games created by ZUN, a Japanese game developer. The series is known for its challenging gameplay, intricate storyline, and unique characters.
输出(Batch_size = 2):The Touhou Project is a series of bullet hell shooter games created by ZUN, a Japanese game developer. The series is known for its challenging gameplay, intricate storyline, and unique characters.
在模型中,Batch_size来源于两组,一组是外部人为设置的batch_size,一种是模型内部的batch_size,比如同一台服务器上,如果有两个人同时输入了问题,那么模型推理的batch_size可能就是2,这个是外部的batch_size。而内部的计算也有很多抽象的batch_size,比如内部处理时的并行策略。batch_size带来的影响,我们叫做批次的不确定性。这时,batch_size不仅来源于内部,也来源于各种底层的并行策略。同一个运算,会放在不同的core上进行运算,如果批次数大于core数,则会每一个core处理一个数据,运算是串行的,结果不变。如果小于core数,就会不同的core处理同一个数据,导致运算次序发生了改变。这导致了运算的不确定性。这个时候,底层运算不满足结合率就是最重要的原因了。
一台机器上输出不同的原因是,底层运算收到Batch_size/并行策略影响。
作者做了实验,应用Qwen3-235B1000次推理,输出了80种结果,在103token处有分歧。将所有运算修改为批次不变的,则没有误差,但是带来了性能损失,除此之外,作者也发现,批次可变性是强化学习性能有损失的原因。
总结
在不同机器上,运算不一致的原因是浮点数的表达是不精确的、下一个token的概率差很小以及硬软件环境的影响。而在同一台机器上,就是底层运算受到并行策略影响的原因。