大模型推理的不确定性:从浮点运算到工程实现

以下文章由Claude.ai辅助生成

问题的提出

为什么大模型在设置 temperature=0 时,同样的输入仍然会产生不同的输出?这个看似违反直觉的现象,揭示了现代推理引擎在追求极致性能时做出的工程权衡。

问题的本质

浮点运算的不结合性

计算机中的浮点运算不满足结合律。在数学上,(a + b) + c = a + (b + c) 永远成立,但在有限精度的浮点运算中,由于舍入误差的存在,这个等式可能不成立。

具体例子

1
2
3
4
5
6
7
8
9
数字: x=10000000, y=1, z=-10000000

顺序1: (x + y) + z
= 10000001 + z // 精度丢失,变成10000000
= 0

顺序2: x + (y + z)
= x + (-9999999)
= 1

并行计算改变了运算顺序

GPU 并行计算为了提高效率,会将顺序计算拆分成多个并行路径,再将结果合并。不同的并行策略意味着不同的加法树结构,从而导致不同的浮点舍入路径。

串行计算

1
2
3
sum = 0
for i in data:
sum += i # 顺序固定

并行计算(2线程)

1
2
3
线程1: (((a+b)+c)+d)
线程2: (((e+f)+g)+h)
最后: thread1 + thread2

并行计算(4线程)

1
2
t1: a+b,  t2: c+d,  t3: e+f,  t4: g+h
然后: (t1+t2) + (t3+t4)

虽然数学上等价,但加法树的拓扑结构完全不同,导致浮点累积误差不同。

Batch-Variant 问题

推理引擎的动态优化

现代推理引擎(如 vLLM、TensorRT)为了达到极致的 GPU 利用率,会根据当前负载动态选择并行策略:

Batch Size 并行策略
小批次 使用简单 kernel
大批次 使用复杂并行 kernel
混合负载 动态切换策略

这意味着同一个输入在不同负载下,会走不同的计算路径

关键算子的 Batch-Variant 特性

三个最容易产生不确定性的算子:

  1. RMSNorm:需要对隐藏维度做归约(reduction),不同 batch 下归约树结构不同
  2. MatMul:大规模矩阵乘法的累加顺序高度敏感
  3. Attention:softmax 中的 exp-sum-normalize 链路是数值不稳定的高发区

argmax:微小误差的放大器

什么是 argmax

argmax 返回的不是最大值本身,而是最大值的位置

1
2
3
4
5
6
logits = [5.000000, 4.999999, 3.2]
argmax(logits) = 0 # 返回第0个token

# 但如果并行路径变化导致微小误差
logits = [4.999998, 4.999999, 3.2]
argmax(logits) = 1 # 返回第1个token

为什么如此脆弱

argmax 是一个从连续到离散的断崖式映射

  • argmax 之前:数值变化是平滑的
  • argmax 之后:结果是非黑即白的

因此,0.000001 的数值误差可以导致:

  • 100% 不同的 token 选择
  • 完全不同的后续生成路径
  • 整段文本的彻底分叉

这就是为什么 temperature=0 反而最不稳定——它完全依赖 argmax 这把脆弱的”刀”。

解决方案:Batch-Invariant 算子

核心思想

不是消除并行,而是让并行的归约结构在任何 batch 下都保持一致

具体做法

  1. 固定 reduction tree:无论 batch 大小如何变化,都使用同一棵加法树
  2. 禁止 kernel 自动切换:明确指定计算路径,不让引擎根据负载动态选择
  3. 统一归一化顺序:在 attention 和 softmax 中强制固定计算顺序

权衡

  • ✅ 获得了完全的确定性(bitwise identical)
  • ❌ 牺牲了部分 GPU 吞吐和动态优化能力

实验验证

在 Qwen3-235B 模型上:

  • 修正前:同一 prompt 推理 1000 次产生 80 种不同输出
  • 修正后:1000 次推理产生完全相同的输出

强化学习中的致命影响

On-Policy vs Off-Policy

在强化学习中,on-policy 要求:

1
采样策略 π_sample = 训练假设策略 π_train

但由于推理不确定性:

  • 你以为在做 greedy sampling(temperature=0
  • 实际上 argmax 边界不断翻转
  • 导致 π_sample ≠ π_train
  • 变成了 pseudo off-policy

KL 散度验证

在使用 batch-invariant 算子后,训练过程中的 KL 散度始终为 0,证明了采样和训练的完全一致性。这在传统大模型强化学习中几乎不可能实现。

工程现状与展望

当前状态

  • ✅ 已有可运行的研究原型(GitHub 仓库
  • ✅ 在 235B 规模模型上验证可行
  • ❌ 尚未集成到主流推理引擎(vLLM、TensorRT)

为什么还未普及

  1. 性能代价:固定计算路径意味着放弃动态优化
  2. 需求优先级:大多数应用使用 temperature>0,本就允许随机性
  3. 设计哲学冲突:主流引擎优先考虑吞吐,而非确定性

理解方案的适用边界

这套方法容易被误解为”永久可复现性”方案,但实际上它解决的是局部时间一致性问题。

它不保证的

  • 跨版本的可复现(模型权重、tokenizer 会更新)
  • 跨时间的可复现(推理引擎、CUDA 版本会变化)
  • 历史归档式的重放(不记录 kernel 版本、reduction tree)

它真正保证的

  • 在同一模型版本、同一推理系统、同一部署周期内
  • 推理结果不因负载与调度而漂移
  • 这是”消除系统噪声”,而非”冻结历史”

用类比来说,这更像数据库的事务隔离级别,而不是永久快照——它保证同一个事务内行为一致,但不保证十年后重放同一事务。

为什么不记录完整计算路径?因为在 235B 模型上记录每个 kernel、每个 block/warp、每个浮点舍入点,在存储、回放、性能上都不可行。文章选择的是通过结构性约束保证路径等价,这是唯一工程上可行的路线。

真正的应用场景

这个方案的核心价值在于同一时间窗口内的自洽性

  1. 强化学习训练:在一轮训练中,如果采样策略因 batch 变化而漂移,当下这轮训练就已被污染。这不是三个月后能否复现的问题,而是当前训练周期内能否保持 on-policy 的问题。

  2. 科研实验:在实验周期内需要 bitwise 级别的可复现性,排除系统噪声对实验结论的干扰。

  3. 安全审计:在审计周期内,相同输入必须产生相同输出,以支持行为追溯。

未来形态

更可能以可选模式出现在推理引擎中:

1
2
3
vllm serve --deterministic
vllm serve --batch-invariant
vllm serve --rl-training-mode

类似于 PyTorch 的 torch.use_deterministic_algorithms(True),让用户在性能和确定性之间自主选择。

Temperature 与随机性

Temperature 的作用

Temperature 不直接控制”是否随机”,而是调整概率分布的陡峭程度

1
p_i = exp(z_i / T) / Σ exp(z_j / T)
Temperature 概率分布 行为特征
0 [1, 0, 0] 完全确定(argmax)
1 [0.5, 0.3, 0.2] 原始模型分布
2 [0.41, 0.32, 0.27] 更加平滑
5 [0.36, 0.33, 0.31] 接近均匀分布

关键区分

  • Temperature:改变概率分布
  • Sampling:根据概率分布掷骰子

temperature>0 不等于”会随机”,只有配合采样才真正引入随机性。

总结

大模型推理的不确定性问题揭示了一个深刻的工程真相:

单次前向推理是确定的,但推理引擎为了性能在不同负载下使用了不同的数值计算路径。

解决方案不是消除并行,而是冻结并行结构,让数值路径在任何情况下都保持一致。这是一个明确的工程权衡——用部分性能换取完全确定性。

这个方案目前最适合对确定性有极端要求的场景,特别是强化学习训练。它代表了一种新的工程视角:有时候,”慢而稳定”比”快而飘忽”更有价值。


参考资源