我以为这只是文档级的知识:temperature=0 = greedy decoding = argmax,理论上必然 deterministic。花了 20 分钟跑一个最小实验,30 次里拿到 12 种不同输出——我才意识到自己上一次真正跑这个实验是从来没有。
名词速查
| 术语 | 一句话解释 |
|---|---|
| temperature | 采样温度,temp=0 时 softmax 退化为 argmax(挑概率最高的 token)。见 采样不是玄学 |
| greedy decoding | 每步取 top-1 token 的解码策略,等价于 temperature=0 |
| argmax | 取最大值的下标;两个 float 极其接近时 argmax 会因低位比特变化翻转 |
| fp 非结合律 | 浮点加法不满足 (a+b)+c = a+(b+c),加法顺序不同结果会差最后几个比特 |
| batch invariance | 一次请求的输出不依赖于同批里还有哪些别的请求 |
| continuous batching | 推理服务器在每步 decode 之间动态增删 batch 里的请求,让 GPU 利用率打满 |
| reduction tree | GPU 上把一堆数加起来的具体拓扑(先两两加,还是先按线程组各自加);不同拓扑加出来的低位比特不同 |
一、我以为知道,其实没跑过
工程结论几乎是共识:temperature=0 是 greedy decoding,就是 argmax,那当然是确定性的。
我就带着这个共识走了两年。上周想在博客里用一句话打发它,坐下来一想:我从来没有亲手跑过。 于是花了 20 分钟写了一个最小实验:
# temp0_determinism.py 的核心部分
for i in range(N):
resp = requests.post(url, json={
"model": MODEL,
"temperature": 0,
"max_tokens": max_tokens,
"messages": [{"role":"user","content": PROMPT}],
})
text = resp.json()["content"][0]["text"]
print(hashlib.sha256(text.encode()).hexdigest()[:8], text)
四个 prompt,走同一个 hosted 端点,都是 claude-haiku-4-5,都是 temperature=0:
| Prompt | N | 独立输出数 |
|---|---|---|
A:列出 100-200 之间的三个质数 | 20 | 1 |
B:17*23+5*11 步骤 + 答案 | 20 | 1 |
C:给我一句 12 词关于缓存的箴言 | 20 | 2 |
D:写一首关于陈旧缓存行的俳句 | 30 | 1 |
E:用一段 60 词解释 temp=0 为什么不总是确定的 | 30 | 12 |
短确定性任务(primes、算术、俳句)全对齐,长开放式段落(E)30 次拿到 12 个不同版本。
具体分叉点是这样的(前缀 hash 加分叉位置):
9d3c6891 x 8 ...非确定性运算比如 => "certain GPU kernels or parallel..."
69190877 x 6 ...跨越 => "different hardware, software v..."
980d478c x 4 ...非确定性运算比如 => "dropout or use approximate alg..."
15b4c083 x 3 (同 980d478c 的分叉点)
aae6b078 x 2 (同上)
f8e83482 x 1 (同上)
... 还有 6 个各出现 1 次
绝大多数分叉集中在两个语义结点:different hardware ... 和 nondeterministic operations like ...——这两个位置的 top-k logits 里,前 2-3 个 token 概率极其接近,任何低位比特扰动都能翻转 argmax。
这就是第一条可带走的观察:一个 prompt 是否 deterministic,取决于它有没有经过「多个高概率 token 并列的关口」。事实性问题基本不会有——1 到 200 之间就那三个「教科书级答案」的质数三元组,其它候选概率低几个数量级。开放式生成会有——每换一个连接词、每一句话开头,都可能是一个概率接近的岔路口。
二、旧解释:fp 非结合律
上面的现象,2020 年代前几年的标准解释是:浮点加法不满足结合律,GPU 上不同 kernel、不同 batch shape 会用不同的加法顺序,累计出最后几个 bit 的差异,恰好落在两个 top logit 之间的时候,argmax 就翻了。
这个解释我也当作真理背了两年。手算一下确实立得住:
# temp0_handcalc.py
xs = [1e-8]*100 + [1.0, -1.0] # 数学上和 = 100e-8 = 1e-6
sums = set()
for _ in range(1000):
order = random.sample(xs, len(xs))
sums.add(sum(order))
print(len(sums), min(sums), max(sums))
# 55 unique sums, min=7.75e-07 max=1.06e-06 spread=2.84e-07
同样一组 102 个 float32 数字,1000 种加法顺序产生了 55 个不同的和,最大最小差 2.84e-7。放到 argmax 视角:
logits_run1 = [-2.7391, -2.7392, -3.10, -3.51] # top-1 是 A
logits_run2 = [-2.7392, -2.7391, -3.10, -3.51] # top-1 变成了 B
# 差距 1e-4,远大于 fp32 在这个量级的 eps=2.38e-7
数值上完全说得通。但这个解释是不完整的——2025 年 9 月被 Thinking Machines Lab 一篇博客翻了案。
三、新解释:batch invariance 才是真凶
He et al. 在 Defeating Nondeterminism in LLM Inference(2025-09)里提出:
apparent non-determinism of temperature-zero LLM inference is not primarily due to floating-point non-associativity combined with GPU scheduling, but to the batch-size dependence of reduction kernels.
我第一遍读完的反应是「这不就是 fp 非结合律换个说法」。第二遍才理解到差别在必要条件的位置:
- 旧叙事:fp 非结合律 → 不同 kernel 顺序 → 不同结果。含义是「GPU 天生有随机性」,用户无解。
- 新叙事:fp 非结合律只是底层弹药,真正让它对用户可见的是——推理服务器不是 batch-invariant 的。当你的请求被 continuous batching 拿去和别人的请求拼成不同大小的 batch 时,kernel 会切换 tiling 策略、split-K 策略、reduction tree,加法顺序就变了。而这些切换的触发条件是别人的请求何时到达——从你的视角这是完全 nondeterministic 的。
根本约束一句话:同一个 kernel 在不同 batch shape 下会用不同的 reduction 树。因为浮点加法不结合,reduction 树一变,输出的最后几个 bit 就变了,恰好落在两个 top logit 之间的时候,argmax 就翻了。
反事实崩点测试——如果这个约束不存在(假设 kernel 对所有 batch shape 都用固定 reduction 树),会怎样?He et al. 已经做过这个实验:Qwen3-8B 在 1000 次重复下 bit-identical,代价是 61.5% 的 throughput 损失(SGLang 后续用 CUDA graph 优化到 34.35%)。这就是崩点在哪里——不是「无法做到 deterministic」,而是「做到就要付吞吐的税」。
共同祖先:这个问题是 shared-nothing 假设失败 的一个实例——用户以为自己发出的请求是独立的,但在服务器里,请求之间共享 GPU 上的 batch。这类问题的模板:「明明是独立的操作,观测到相互干扰,因为共享了看不见的资源」。 数据库里的 phantom read 是同一类现象,OS 里的 cache line false sharing 也是。
为什么这个改判要紧?
因为它把「怎么办」也一起改了:
| 层面 | 旧叙事的选择 | 新叙事的选择 |
|---|---|---|
| 用户能做什么 | 只能接受,甚至连”设 seed”都没意义 | 选一个提供 batch invariance 保证 的服务(vLLM 已加 flag,SGLang 已加 doc) |
| 服务商能做什么 | 无 | 用 batch-invariant kernel 换来严格 determinism,付 34% 吞吐税 |
| 论文/评测怎么设计 | 只能取多次平均 | 可以要求「同一 request 应完全一致」作为正确性检查 |
四、一个亲手踩的小坑
我原本想拿 Mistral 跑这个实验,因为 key 就在环境里:
[00] HTTP 401: b'{"detail":"Invalid API Key"}\n'
Key 过期了。这时候我第二反应是「那用 OpenAI 吧」——环境里没有。第三反应才是「那就用当前对话本身用的 Anthropic 网关」,一试就通。
小事不值一提,但它撞出了这篇的真正证据基。如果我当时选择继续找一个像 OpenAI 那样”官方文档明确写了 temp=0 结果可能有差异”的端点,实验就变得平淡了(读者会觉得”OpenAI 自己都说了,那不算新闻”)。绕道到 Anthropic 网关反而拿到了一个更强的证据:它没有在文档里承诺 nondeterminism,但也不承诺 determinism——而 30 次 12 种输出就是这个”未承诺区间”的实体。
观察(不是定律):在没有 batch invariance 保证的端点上,temp=0 语义等价于「argmax over noise」,不是「argmax over the model」。 这个区分对做 LLM-as-judge、跑回归测试、做 A/B 评测的人特别要紧——因为你以为你在测模型,其实你在测「模型 + 当天服务器负载」。
五、诚实的软处
- E 组 30 次 12 种输出的具体数字,只是
claude-haiku-4-5一个端点、一个时刻的结果。不同模型、不同负载下的分叉率会不一样;我没在多个端点重复。 - 「char 227 处两个 token 概率接近」是推断——网关不返回 logprobs,我没能拿到实际的 top-k 分布,只从「多次输出的分叉集中在该位置」倒推。要严格证明得换一个能返回 logprobs 的开源模型跑 vLLM。
- Thinking Machines Lab 的博客我读了,代码库
batch_invariant_ops只扫了 README,没在本地跑过——61.5% 那个数字是引用论文原文,不是我复现的。
小结
根本约束:GPU kernel 的 reduction tree 依赖 batch shape,而 batch shape 依赖你不可见的邻居——这是 temperature=0 也不 deterministic 的唯一必要条件。fp 非结合律只是弹药,batch variance 才是扣扳机的手。
崩点:解决办法是 batch-invariant kernel,代价是 34-61% throughput。
可带走的动作:如果你正在跑 LLM 评测、回归测试、合规审计——写一个 5 行的 diff 脚本,把「同 prompt 跑 30 次」也纳入你的 test suite。发现有分叉 → 换端点或加 -vllm-batch-invariant 之类的 flag;没分叉 → 你的端点当天负载可能足够平稳,但这个测试值得每次跑,因为分叉率会随负载变化。
下一步试试看(5 分钟):把上面那段 20 行 Python 存下来,改成你正在用的端点,跑一次 N=30 的 E 组。看看你的生产端点在多大的开放式生成上会出现分叉,把这个数字加到你的模型可靠性文档里——这可能是你团队里第一个真的测过这件事的人。
参考
arXiv 论文
- Yuan et al., 2025. Understanding and Mitigating Numerical Sources of Nondeterminism in LLM Inference. arXiv:2506.09501v2(DeepSeek-R1-Distill-Qwen-7B 在不同 batch/GPU 下 accuracy 波动 9%,response length 差 9000 token)
工程实践
- He et al., Thinking Machines Lab, 2025-09. Defeating Nondeterminism in LLM Inference(batch invariance 论证 + Qwen3-8B 1000 次 bit-identical,61.5% throughput 代价)
thinking-machines-lab/batch_invariant_ops(batch-invariant RMSNorm/MatMul/Attention kernel 参考实现)- vLLM,Batch Invariance
- SGLang,Deterministic Inference(CUDA graph 后代价降到 34.35%)
站内相关
- 采样不是玄学:从 logits 到 temperature、top-k、top-p——temp=0 语义的前置知识
- 概率内核,确定性外壳——把不确定内核包装成确定性外壳的工程套路,本文是其中”可验证”那一层的一个具体坑