以下文章由Claude.ai辅助生成
问题的提出
为什么大模型在设置 temperature=0 时,同样的输入仍然会产生不同的输出?这个看似违反直觉的现象,揭示了现代推理引擎在追求极致性能时做出的工程权衡。
问题的本质
浮点运算的不结合性
计算机中的浮点运算不满足结合律。在数学上,(a + b) + c = a + (b + c) 永远成立,但在有限精度的浮点运算中,由于舍入误差的存在,这个等式可能不成立。
具体例子:
1 | 数字: x=10000000, y=1, z=-10000000 |
并行计算改变了运算顺序
GPU 并行计算为了提高效率,会将顺序计算拆分成多个并行路径,再将结果合并。不同的并行策略意味着不同的加法树结构,从而导致不同的浮点舍入路径。
串行计算:
1 | sum = 0 |
并行计算(2线程):
1 | 线程1: (((a+b)+c)+d) |
并行计算(4线程):
1 | t1: a+b, t2: c+d, t3: e+f, t4: g+h |
虽然数学上等价,但加法树的拓扑结构完全不同,导致浮点累积误差不同。
Batch-Variant 问题
推理引擎的动态优化
现代推理引擎(如 vLLM、TensorRT)为了达到极致的 GPU 利用率,会根据当前负载动态选择并行策略:
| Batch Size | 并行策略 |
|---|---|
| 小批次 | 使用简单 kernel |
| 大批次 | 使用复杂并行 kernel |
| 混合负载 | 动态切换策略 |
这意味着同一个输入在不同负载下,会走不同的计算路径。
关键算子的 Batch-Variant 特性
三个最容易产生不确定性的算子:
- RMSNorm:需要对隐藏维度做归约(reduction),不同 batch 下归约树结构不同
- MatMul:大规模矩阵乘法的累加顺序高度敏感
- Attention:softmax 中的 exp-sum-normalize 链路是数值不稳定的高发区
argmax:微小误差的放大器
什么是 argmax
argmax 返回的不是最大值本身,而是最大值的位置。
1 | logits = [5.000000, 4.999999, 3.2] |
为什么如此脆弱
argmax 是一个从连续到离散的断崖式映射:
- argmax 之前:数值变化是平滑的
- argmax 之后:结果是非黑即白的
因此,0.000001 的数值误差可以导致:
- 100% 不同的 token 选择
- 完全不同的后续生成路径
- 整段文本的彻底分叉
这就是为什么 temperature=0 反而最不稳定——它完全依赖 argmax 这把脆弱的”刀”。
解决方案:Batch-Invariant 算子
核心思想
不是消除并行,而是让并行的归约结构在任何 batch 下都保持一致。
具体做法
- 固定 reduction tree:无论 batch 大小如何变化,都使用同一棵加法树
- 禁止 kernel 自动切换:明确指定计算路径,不让引擎根据负载动态选择
- 统一归一化顺序:在 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)
为什么还未普及
- 性能代价:固定计算路径意味着放弃动态优化
- 需求优先级:大多数应用使用
temperature>0,本就允许随机性 - 设计哲学冲突:主流引擎优先考虑吞吐,而非确定性
理解方案的适用边界
这套方法容易被误解为”永久可复现性”方案,但实际上它解决的是局部时间一致性问题。
它不保证的:
- 跨版本的可复现(模型权重、tokenizer 会更新)
- 跨时间的可复现(推理引擎、CUDA 版本会变化)
- 历史归档式的重放(不记录 kernel 版本、reduction tree)
它真正保证的:
- 在同一模型版本、同一推理系统、同一部署周期内
- 推理结果不因负载与调度而漂移
- 这是”消除系统噪声”,而非”冻结历史”
用类比来说,这更像数据库的事务隔离级别,而不是永久快照——它保证同一个事务内行为一致,但不保证十年后重放同一事务。
为什么不记录完整计算路径?因为在 235B 模型上记录每个 kernel、每个 block/warp、每个浮点舍入点,在存储、回放、性能上都不可行。文章选择的是通过结构性约束保证路径等价,这是唯一工程上可行的路线。
真正的应用场景
这个方案的核心价值在于同一时间窗口内的自洽性:
强化学习训练:在一轮训练中,如果采样策略因 batch 变化而漂移,当下这轮训练就已被污染。这不是三个月后能否复现的问题,而是当前训练周期内能否保持 on-policy 的问题。
科研实验:在实验周期内需要 bitwise 级别的可复现性,排除系统噪声对实验结论的干扰。
安全审计:在审计周期内,相同输入必须产生相同输出,以支持行为追溯。
未来形态
更可能以可选模式出现在推理引擎中:
1 | vllm serve --deterministic |
类似于 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 不等于”会随机”,只有配合采样才真正引入随机性。
总结
大模型推理的不确定性问题揭示了一个深刻的工程真相:
单次前向推理是确定的,但推理引擎为了性能在不同负载下使用了不同的数值计算路径。
解决方案不是消除并行,而是冻结并行结构,让数值路径在任何情况下都保持一致。这是一个明确的工程权衡——用部分性能换取完全确定性。
这个方案目前最适合对确定性有极端要求的场景,特别是强化学习训练。它代表了一种新的工程视角:有时候,”慢而稳定”比”快而飘忽”更有价值。
参考资源: