一个被广泛接受的观点是:只要将大模型的Temperature参数设为0,就能获得确定性的输出。这个直觉看起来很合理——Temperature=0意味着贪婪采样,模型总是选择概率最高的那个token,没有随机性,结果应该可复现。
然而事实并非如此。
即使Temperature=0,相同的prompt在不同的运行中仍然可能产生完全不同的输出。这不是bug,而是现代GPU计算架构的一个根本特性。2024年8月,一篇发表在arXiv上的论文通过系统实验揭示了这一现象的普遍性:在Temperature=0设置下,主流LLM服务的非确定性输出比例从24%到100%不等。
这个发现震撼了整个AI工程社区。为什么?
因为这意味着即使在最严格控制的条件下,大模型的推理结果依然无法复现。对于需要可验证性的应用场景——自动化测试、安全审计、强化学习训练、金融决策——这是一个致命的问题。
要理解这个问题的本质,我们必须深入到底层:从浮点数的数学特性,到GPU的并行计算机制,再到推理服务器的调度策略。在这个过程中,我们会发现一个更深层的原因,它远比大多数人想象的更加根本。
浮点数的"原罪":非结合性
一切要从浮点数说起。
在数学中,加法满足结合律:$(a + b) + c = a + (b + c)$。这个性质如此基本,以至于我们很少质疑它。但对于浮点数而言,这个等式不再成立。
考虑这个简单的例子:
(0.1 + 1e20) - 1e20 # 结果是 0
0.1 + (1e20 - 1e20) # 结果是 0.1
为什么会有这样的差异?
浮点数的表示方式决定了这一切。以十进制为例,一个浮点数可以表示为 $\text{mantissa} \times 10^{\text{exponent}}$。假设我们只有3位有效数字的精度:
- 1230 可以精确表示为 $1.23 \times 10^3$
- 但 1234.5 只能表示为 $1.23 \times 10^3$,最后两位被丢弃了
这种精度损失发生在每次加法运算中,特别是当两个数的"尺度"(指数)不同时。更大的问题是:加法的顺序决定了精度损失的模式。
graph LR
subgraph "浮点数加法精度损失示意"
A["大数: 1×10^20"] --> B["加 0.1"]
B --> C["结果: 1×10^20<br/>0.1 完全丢失"]
end
subgraph "精度损失的传递"
D["第一步结果"] --> E["减去大数"]
E --> F["结果: 0<br/>而非 0.1"]
end
Thinking Machines Lab的研究者给出了一个令人印象深刻的演示:
import random
vals = [1e-10, 1e-5, 1e-2, 1]
vals = vals + [-v for v in vals]
results = []
random.seed(42)
for _ in range(10000):
random.shuffle(vals)
results.append(sum(vals))
results = sorted(set(results))
print(f"There are {len(results)} unique results")
# 输出:There are 102 unique results
这6个数的求和,仅仅改变加法顺序,就能产生102种不同的结果。差异可能很小——在$10^{-17}$量级——但它们确实是不同的值。
graph TB
A["6个浮点数求和"] --> B["随机打乱顺序10000次"]
B --> C["统计不同结果"]
C --> D["102种不同结果!"]
subgraph "原因分析"
E["浮点数不满足结合律"] --> F["加法顺序影响结果"]
F --> G["累加误差模式不同"]
end
D --> G
在神经网络的前向传播中,这种情况无处不在。矩阵乘法的核心就是大量的累加操作:$y_i = \sum_j w_{ij} x_j$。一个7B参数的模型,一次前向传播涉及数十亿次浮点加法。如果加法的顺序不确定,最终结果的差异就会累积放大。
问题在于:是什么决定了加法的顺序?
一个流行但错误的解释
在讨论GPU计算的非确定性时,最常见的解释是"并发+浮点数"假说:
GPU拥有大量并行计算核心,它们的执行顺序是不确定的。当多个核心同时执行浮点加法时,由于浮点数不满足结合律,不同的执行顺序会导致不同的结果。
这个解释看起来很有道理,而且在某些情况下确实是正确的。但它并不是LLM推理非确定性的主要原因。
Thinking Machines Lab的研究者做了一个简单的实验来验证这一点:
A = torch.randn(2048, 2048, device='cuda', dtype=torch.bfloat16)
B = torch.randn(2048, 2048, device='cuda', dtype=torch.bfloat16)
ref = torch.mm(A, B)
for _ in range(1000):
assert (torch.mm(A, B) - ref).abs().max().item() == 0
这段代码在GPU上执行矩阵乘法1000次,使用的是bfloat16精度——这正是LLM推理中常用的数据类型。结果呢?每次都完全相同,逐位精确匹配。
这证明了一个重要的事实:GPU上的矩阵乘法是运行到运行确定性(run-to-run deterministic)的。即使有大量并发计算,只要输入相同、GPU相同、软件版本相同,输出就相同。
那么,并发和浮点数非结合性的组合,在什么情况下会导致非确定性?
答案是:原子操作(atomic operations)。
当多个GPU核心需要向同一个内存位置写入数据时,需要使用原子操作来保证正确性。以atomicAdd为例,它保证每次加法都会被执行,但不保证执行的顺序——哪个核心先完成,就先执行哪个核心的加法。
graph TB
subgraph "原子操作导致非确定性"
C1["Core 1: 计算部分和 A"] --> M["共享内存位置"]
C2["Core 2: 计算部分和 B"] --> M
C3["Core 3: 计算部分和 C"] --> M
M --> Q{"哪个先完成?"}
Q -->|"不确定"| R["累加顺序不确定"]
R --> S["结果不确定"]
end
style Q fill:#f9f,stroke:#333
style S fill:#f66,stroke:#333
这就是"并发+浮点数"假说的真实含义:如果GPU kernel使用了原子操作进行浮点累加,而原子操作的执行顺序取决于并发核心的完成时间(这是非确定性的),那么结果就是非确定性的。
但关键的问题是:LLM前向传播中使用的kernel,真的依赖原子操作吗?
答案是:绝大多数情况下,不依赖。
现代深度学习框架的kernel经过了大量优化,大部分情况下可以通过其他策略避免原子操作:
-
数据并行策略:将每个batch元素分配给独立的核心处理,每个核心完成完整的归约,不需要跨核心通信。
-
分块归约策略:将大归约拆分成多个小块,每块独立处理,最后用一个确定性的"清理"归约合并结果。
-
信号量策略:使用信号量确保并发线程块按确定性的顺序累加。
这些策略在保持高性能的同时实现了确定性。研究者明确指出:
在LLM的前向传播中,通常不存在任何原子加法操作。因此,LLM的前向传播实际上是运行到运行确定性的。
这句话可能会让很多人感到困惑。如果前向传播是确定性的,为什么我们还是观察到非确定性的输出?
问题的答案隐藏在一个被忽视的维度中:批量大小。
真正的元凶:批量不变性的缺失
让我们重新审视问题。
假设你向一个LLM推理服务发送一个请求。在服务器端,这个请求可能被单独处理(batch size = 1),也可能与其他用户的请求打包处理(batch size = 10)。从你的角度看,这两种情况应该产生相同的输出——毕竟,你的输入完全相同。
但实际上,它们可能产生不同的输出。
Thinking Machines Lab的研究者用一个简洁的实验展示了这一点:
import torch
torch.set_default_device('cuda')
B = 2048
D = 4096
a = torch.linspace(-1000, 1000, B*D).reshape(B, D)
b = torch.linspace(-1000, 1000, D*D).reshape(D, D)
# 单独处理第一个元素
out1 = torch.mm(a[:1], b)
# 作为批量的一部分处理第一个元素
out2 = torch.mm(a, b)[:1]
print((out1 - out2).abs().max())
# 输出:tensor(1669.2500, device='cuda:0')
同样的输入数据,同样的权重矩阵,仅仅因为batch size不同,输出就有了超过1600的差异。这是一个巨大的数值差异,远超浮点精度误差的范畴。
这个现象的原因在于:GPU kernel的归约策略取决于输入的形状。
以矩阵乘法为例。当batch size很大时,GPU可以高效地将工作分配给各个核心,每个核心处理输出矩阵的一部分,在本地完成归约。但当batch size很小时,可能没有足够的并行度来充分利用GPU,kernel会切换到另一种策略——例如"split-K",将归约维度切分给多个核心,每个计算部分结果,最后合并。
不同的归约策略意味着不同的累加顺序,而不同的累加顺序意味着不同的浮点结果。
graph TB
subgraph "Batch Size = 1"
A1["输入: [1, 4096]"] --> B1["归约策略 A<br/>单核心本地归约"]
B1 --> C1["输出路径: 单一"]
end
subgraph "Batch Size = 2048"
A2["输入: [2048, 4096]"] --> B2["归约策略 B<br/>Split-K 多核心并行"]
B2 --> C2["输出路径: 分块+合并"]
end
C1 --> D["不同的累加顺序"]
C2 --> D
D --> E["不同的浮点结果<br/>差异可达数千"]
style E fill:#f66,stroke:#333
这就解释了为什么Temperature=0不能保证确定性输出:你的请求在服务器端被如何批处理,决定了输出结果。而批处理的方式取决于服务器当前的负载——这是一个你无法控制、也无法预测的因素。
研究者将这种现象总结为:
如果我们将kernel不具备不变性的某个属性(如batch size),与该属性的非确定性变化(如服务器负载)组合起来,就得到了一个非确定性的系统。
这就是LLM推理非确定性的真正根源。它不是GPU并发的问题,也不是原子操作的问题,而是批量不变性(batch invariance)的缺失。
什么是批量不变性?
批量不变性是一个比确定性更强的概念。
确定性只要求:相同的输入产生相同的输出。这意味着如果运行两次完全相同的代码,得到相同的结果,就算是确定性的。
批量不变性要求更强:某个输入的输出,不依赖于与它一起处理的其他输入的数量或内容。
用数学语言表述:假设函数$f$处理一个batch,$x_i$是batch中的第$i$个元素。如果$f$满足批量不变性,那么对于任何两个batch $B_1$和$B_2$,只要$x_i \in B_1$且$x_i \in B_2$,就有:
$$\text{output}_i(f, B_1) = \text{output}_i(f, B_2)$$这里的$\text{output}_i$表示batch中第$i$个元素的输出。
graph LR
subgraph "确定性 vs 批量不变性"
subgraph "确定性"
A1["输入X"] --> B1["运行1"]
A1 --> B2["运行2"]
B1 --> C1["结果A"]
B2 --> C2["结果A"]
C1 -.->|"相同"| C2
end
subgraph "批量不变性"
D1["输入X + 其他请求A"] --> E1["服务器"]
D2["输入X + 其他请求B"] --> E2["服务器"]
E1 --> F1["输出: 相同"]
E2 --> F2["输出: 相同"]
end
end
这个性质对于LLM推理服务至关重要。因为用户无法控制自己的请求会被与其他哪些请求一起批处理,唯一能保证输出一致性的方法就是:让推理引擎的kernel具备批量不变性。
但这里有一个关键的认知转变:
批量不变性不是kernel的默认属性。它需要主动设计才能实现。
实现批量不变性:技术原理
要理解如何实现批量不变性,我们需要深入分析LLM前向传播中涉及归约操作的三个核心组件:归一化层(如RMSNorm)、矩阵乘法和注意力机制。
RMSNorm的批量不变性
RMSNorm的核心操作是对每个token计算其隐藏维度的平方均值,然后归一化:
$$\text{RMSNorm}(x) = \frac{x}{\sqrt{\text{mean}(x^2)}} \cdot w$$这里的归约操作是计算mean(x^2)。在GPU上,这个归约可以按不同方式并行化。
最简单的批量不变策略是数据并行:将每个batch元素(每个token)分配给一个独立的核心,每个核心在本地完成归约。这样,无论batch中有多少其他元素,每个元素的归约策略都相同。
graph LR
subgraph "Batch Size = 4 (数据并行)"
T1["Token 1"] --> C1["Core 1"]
T2["Token 2"] --> C2["Core 2"]
T3["Token 3"] --> C3["Core 3"]
T4["Token 4"] --> C4["Core 4"]
C1 --> R1["本地归约"]
C2 --> R2["本地归约"]
C3 --> R3["本地归约"]
C4 --> R4["本地归约"]
R1 --> O1["输出1"]
R2 --> O2["输出2"]
R3 --> O3["输出3"]
R4 --> O4["输出4"]
end
这个策略在batch size较大时工作良好,但当batch size小于GPU核心数量时,部分核心会闲置,性能下降。此时,高性能的kernel可能会切换到分块归约策略,将单个token的归约拆分给多个核心。
这就是批量不变性被破坏的根源:当batch size变化时,kernel可能会切换归约策略,导致累加顺序不同。
要实现批量不变性,需要强制使用单一的归约策略,即使在某些batch size下性能不是最优的。
矩阵乘法的批量不变性
矩阵乘法是LLM推理中计算量最大的操作。在标准实现中,矩阵乘法的性能高度依赖于并行化策略,而这又取决于输入矩阵的形状。
对于一个$(M, K) \times (K, N)$的矩阵乘法,GPU kernel需要决定:
-
如何划分输出矩阵:将$M \times N$的输出划分成tile,分配给不同的核心。
-
是否使用Split-K:当$M$和$N$都较小时,输出划分无法提供足够的并行度。此时可以将归约维度$K$切分,让多个核心并行计算部分结果,最后合并。
Split-K策略的问题在于:它改变了归约顺序。如果batch size(对应M维度)变化导致kernel启用或禁用Split-K,输出就会不同。
实现批量不变性的方法是:禁用Split-K,或者使用固定的split数量,而不是根据batch size动态调整。
这会带来性能代价。根据LLM-42论文的实验数据,batch-invariant的GEMM kernel相比高度优化的cuBLAS实现,性能下降可达63%。
注意力机制的批量不变性
注意力机制是最复杂的部分,因为它涉及两个维度的归约:特征维度和序列维度。
在解码阶段,query长度通常为1(每次生成一个token),无法在query维度上并行化。此时,kernel需要在key-value维度上切分以充分利用GPU——这就是Split-KV或FlashDecoding策略。
问题与矩阵乘法类似:如果split的数量或大小取决于batch size或KV cache长度,归约顺序就会变化。
批量不变的注意力实现需要:
-
固定的split大小:而不是固定的split数量。这样,无论处理多少个query,每个split的归约顺序都相同。
-
一致处理KV cache:确保已缓存的key-value和新生成的key-value使用相同的归约策略。
-
对齐chunk边界:在chunked prefill中,确保chunk的边界与split的边界对齐,避免不一致的切分。
graph TB
subgraph "标准注意力策略"
A1["动态调整split数量"] --> B1["根据batch size优化性能"]
B1 --> C1["非确定性归约顺序"]
end
subgraph "批量不变注意力策略"
A2["固定split大小"] --> B2["一致的归约顺序"]
B2 --> C2["确定性输出"]
A2 --> D2["性能代价: 约30-50%"]
end
style C1 fill:#f66,stroke:#333
style C2 fill:#6f6,stroke:#333
生产环境解决方案:vLLM和SGLang
理解了原理之后,让我们看看主流推理引擎是如何实现确定性推理的。
vLLM的批量不变性支持
vLLM从2025年9月开始支持批量不变性,通过环境变量VLLM_BATCH_INVARIANT=1启用。
实现要点:
-
确定性kernel:使用batch-invariant的RMSNorm、矩阵乘法和注意力实现。
-
禁用动态优化:关闭某些可能引入非确定性的优化,如自定义的all-reduce操作。
-
硬件要求:目前仅支持NVIDIA H系列和B系列GPU(计算能力9.0+),因为这些架构支持必要的确定性计算特性。
使用示例:
# 启动确定性推理服务器
VLLM_BATCH_INVARIANT=1 python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-8B-Instruct
# 客户端调用
from openai import OpenAI
client = OpenAI(api_key="EMPTY", base_url="http://localhost:8000/v1")
# 多次调用将产生完全相同的输出
for _ in range(10):
response = client.completions.create(
model="meta-llama/Llama-3.1-8B-Instruct",
prompt="The future of AI is",
max_tokens=100,
temperature=0,
seed=42
)
print(response.choices[0].text) # 每次输出完全相同
SGLang的确定性推理
SGLang同样基于Thinking Machines Lab的batch-invariant kernel,但做了更多优化:
-
CUDA Graph加速:通过CUDA graph将多个kernel启动合并,减少开销。这使得确定性推理的性能开销从Thinking Machines Lab原始实现的61.5%降低到约34.35%。
-
支持非贪婪采样:引入了
multinomial_with_seed操作,通过种子化的Gumbel噪声实现确定性的多项式采样,即使在temperature > 0时也能保证可复现性。 -
多后端支持:支持FlashInfer、FlashAttention-3和Triton三种注意力后端。
性能测试结果(Qwen3-8B, H200 GPU):
| 配置 | 输入1024/输出1024 | 输入4096/输出4096 | 输入8192/输出8192 |
|---|---|---|---|
| 非确定性模式 | 30.85秒 | 332.32秒 | 1623.87秒 |
| 确定性模式 | 43.99秒 (+42.6%) | 485.16秒 (+46.0%) | 2020.13秒 (+24.4%) |
使用CUDA graph后,确定性推理可以达到接近非确定性模式的性能:
| 后端 | 无CUDA Graph | 有CUDA Graph | 加速比 |
|---|---|---|---|
| FlashInfer | 441.73 tokens/s | 1245.51 tokens/s | 2.82x |
| FlashAttention-3 | 447.64 tokens/s | 1247.64 tokens/s | 2.79x |
| Triton | 419.64 tokens/s | 1228.36 tokens/s | 2.93x |
强化学习训练的可复现性
SGLang团队与slime框架合作,实现了100%可复现的强化学习训练。这是一个重要的里程碑,因为非确定性推理一直困扰着LLM的RL训练。
在PPO和GRPO等on-policy算法中,策略的rollout需要与训练数据保持一致性。如果推理是非确定性的,实际上是"off-policy"训练——这会导致训练不稳定甚至发散。
通过确定性推理,两个独立的训练运行可以产生完全相同的损失曲线,为科学实验提供了必要的可复现性。
性能权衡:确定性的代价
确定性不是免费的。它需要牺牲性能来换取可复现性。
性能代价主要来自三个方面:
1. 归约策略的约束
高性能GPU kernel通过动态选择最优的归约策略来适应不同的输入形状。批量不变性强制使用固定策略,无法根据实际情况优化。
以矩阵乘法为例,当batch size很小时,Split-K策略可以显著提升GPU利用率。但批量不变性要求禁用Split-K(或使用固定配置),导致计算效率下降。
LLM-42论文测量的数据:
- 非确定性cuBLAS GEMM:峰值527 TFLOPS
- 批量不变Triton GEMM:峰值194 TFLOPS
- 性能下降:63%
2. Tensor Core利用率的限制
现代GPU的Tensor Core对数据布局和计算块大小有严格要求。批量不变的实现可能无法完全利用Tensor Core的性能优势。
例如,当batch size不能被Tensor Core的tile大小整除时,标准实现可以使用padding或特殊的边界处理来保持效率,而批量不变实现可能需要强制对齐,造成计算浪费。
3. 并行度的损失
某些优化技术(如stream-k矩阵乘法)通过不平衡分配工作来提高负载均衡,但这会引入batch-position依赖——同一batch中不同位置的元素可能使用不同的归约策略。批量不变性需要放弃这些优化。
综合来看,确定性推理的典型性能开销在**25-60%**之间,具体取决于模型架构、batch size和序列长度。
graph LR
subgraph "性能开销分布"
A["归约策略约束: 20-40%"]
B["Tensor Core利用率: 10-20%"]
C["并行度损失: 5-15%"]
end
A --> D["总计: 25-60%"]
B --> D
C --> D
新范式:LLM-42验证推测方法
2026年1月,Microsoft Research提出了LLM-42——一种全新的确定性推理方法,试图在保持性能的同时实现确定性。
核心思想:不要让所有请求都付出确定性的代价,只对需要确定性的请求付出代价。
LLM-42借鉴了投机解码(speculative decoding)的架构,但目的不同:
- 投机解码:用小模型快速生成候选token,大模型验证
- LLM-42:用高性能非确定性路径生成候选token,确定性验证器验证
工作流程:
sequenceDiagram
participant User as 用户请求
participant Fast as 快速路径<br/>(非确定性)
participant Verifier as 验证器<br/>(确定性)
User->>Fast: 生成候选tokens
Fast->>Verifier: 提交候选窗口(如64个tokens)
Verifier->>Verifier: 用固定形状重放计算
alt 验证通过
Verifier->>User: 返回所有tokens
else 验证失败
Verifier->>Verifier: 定位不一致位置
Verifier->>Fast: 从一致点重新生成
Fast->>Verifier: 提交新的候选窗口
end
关键观察:
-
不一致是稀疏的:实验表明,在大多数情况下,即使使用非确定性路径,生成的token序列也是一致的。不一致通常只发生在少数几个位置。
-
验证是廉价的:验证器使用固定的输入形状(固定窗口大小),可以利用优化的确定性kernel。而且验证是批量并行计算,类似prefill阶段,效率远高于decode阶段。
-
选择性应用:只有明确标记为
is_deterministic=True的请求才会触发验证流程,其他请求完全不受影响。
性能结果:
当只有10%的请求需要确定性时:
- 批量不变方法:所有请求性能下降56%
- LLM-42:整体性能下降仅3%
当50%的请求需要确定性时:
- 批量不变方法:所有请求性能下降56%
- LLM-42:整体性能下降约25%
这代表了确定性推理的一个重要范式转变:确定性不再是全局的、全有或全无的特性,而是可以按请求选择的、成本可控的特性。
另一种路径:EigenAI的位精确方案
2026年1月,EigenCloud发布了EigenAI方案,声称在GPU上实现了位精确(bit-exact)的确定性推理,性能开销仅约1.8%。
EigenAI的方法更加底层:
1. 硬件层约束
EigenAI强制执行单一架构策略:操作者和验证者必须使用完全相同的GPU型号。实验表明,A100和H100即使在相同的软件栈下,也会产生不同的结果——这是硬件层面浮点运算行为的差异。
2. 数学库重写
标准cuBLAS和cuDNN库默认使用原子操作和非确定性累加。EigenAI使用自定义kernel,核心特点:
- 无浮点原子操作
- warp同步归约,固定线程顺序
- 达到标准cuBLAS 95-98%的吞吐量
3. 推理引擎简化
基于llama.cpp构建,禁用动态图融合等引入变异性优化,解码使用固定种子的PRNG。
验证结果:10,000次推理运行,SHA256哈希100%匹配。跨主机、独立H100节点、后台负载压力测试——全部保持位精确一致性。
这种方法更适合需要强可验证性的场景(如区块链上的AI预言机),但代价是更高的工程复杂性和硬件绑定。
graph TB
subgraph "EigenAI确定性技术栈"
A["硬件层: 单一GPU架构"] --> B["数学库: 自定义kernel"]
B --> C["推理引擎: 简化配置"]
C --> D["结果: 位精确可复现"]
end
subgraph "性能特性"
E["开销: ~1.8%"]
F["100% 匹配率"]
G["跨节点一致性"]
end
D --> E
D --> F
D --> G
实际应用场景
什么时候需要确定性推理?
1. 强化学习训练
这是最重要的应用场景。PPO、GRPO等on-policy算法要求rollout数据与当前策略保持一致。非确定性推理实际上将这些算法变成了off-policy,可能导致训练不稳定。
slime框架的实践证明,确定性推理可以消除这个问题,使得训练曲线完全可复现。
2. 系统调试
当排查推理框架或模型实现的bug时,确定性行为使得问题可以精确复现。非确定性环境下,“无法复现的bug"是最令人沮丧的情况。
3. 安全审计与合规
在医疗、金融等受监管领域,AI决策过程需要可审计。如果推理结果不可复现,审计就无法进行。
4. 基准测试
模型评估的一致性是科学研究的基础。如果同一模型在同一数据集上的评估结果波动,就无法公平比较不同模型或方法。
5. 自动化测试
将LLM集成到CI/CD流水线中时,确定性输出是可预测测试的前提。非确定性会导致测试不稳定,降低开发效率。
6. 高风险决策
当AI用于自动交易、自动驾驶或医疗诊断时,决策的可复现性和可追溯性至关重要。用户需要能够验证"系统当时确实做出了这个决策”。
graph LR
subgraph "确定性推理应用场景优先级"
A["高优先级<br/>强化学习训练"] --> B["中高优先级<br/>安全审计/合规"]
B --> C["中等优先级<br/>系统调试/基准测试"]
C --> D["较低优先级<br/>自动化测试"]
D --> E["场景相关<br/>高风险决策"]
end
最佳实践指南
如果你的项目需要确定性推理,以下是最新的实践建议:
对于推理服务部署
选项A:使用vLLM或SGLang的批量不变模式
# vLLM
VLLM_BATCH_INVARIANT=1 python -m vllm.entrypoints.openai.api_server \
--model your-model \
--gpu-memory-utilization 0.9
# SGLang
python -m sglang.launch_server \
--model-path your-model \
--enable-deterministic-inference \
--attention-backend flashinfer
选项B:等待LLM-42方案成熟
如果大部分请求不需要确定性,LLM-42的验证推测方法可能更合适。目前该方案仍在开发中,预计2026年内会有开源实现。
对于强化学习训练
# SGLang + slime 示例
from sglang import RuntimeEndpoint
# 启用确定性推理
runtime = RuntimeEndpoint(
"http://localhost:30000",
is_deterministic=True # 关键参数
)
# 设置全局种子和确定性模式
import torch
torch.use_deterministic_algorithms(True, warn_only=False)
torch.manual_seed(42)
# 环境变量
import os
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
os.environ["NVTE_ALLOW_NONDETERMINISTIC_ALGO"] = "0"
对于本地实验
import torch
import random
import numpy as np
def set_seed(seed: int):
"""设置所有随机种子"""
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
random.seed(seed)
np.random.seed(seed)
def enable_determinism():
"""启用PyTorch确定性模式"""
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# 注意:这不能解决批量不变性问题!
# 如果需要完全确定性,必须禁用动态批处理
# 或使用支持批量不变性的推理引擎
重要警告
即使设置了temperature=0和seed,以下情况仍会导致非确定性:
- 动态批处理:你的请求与其他请求混合处理
- 跨GPU运行:不同GPU型号产生不同结果
- 软件版本差异:PyTorch、CUDA、cuDNN版本变化
- 量化差异:FP16与BF16的结果不完全一致
PyTorch官方文档明确警告:
完全可复现的结果不能保证跨PyTorch版本、不同提交或不同平台。即使使用相同的种子,结果也可能在CPU和GPU执行之间不可复现。
总结
Temperature=0不等于确定性输出,这个看似简单的声明背后,是一系列深刻的技术问题:
根本原因:浮点数的非结合性结合GPU kernel对输入形状的动态适应。当批量大小变化时,kernel的归约策略变化,累加顺序不同,最终结果不同。
解决方案:批量不变性——强制kernel使用单一的归约策略,无论输入形状如何变化。这需要主动设计,牺牲部分性能。
工程实现:vLLM和SGLang已支持批量不变推理,性能开销约25-50%。LLM-42提出的选择性验证方法有望降低这个开销。
实际意义:确定性推理对强化学习训练、系统调试、安全审计等场景至关重要。但对需要创造性和多样性的应用,非确定性反而是一个特性。
理解这些原理,才能在正确的场景做出正确的选择。确定性不是默认的,它是一种需要主动设计、付出代价、并根据需求权衡的工程决策。
参考资料
-
He, Horace et al. “Defeating Nondeterminism in LLM Inference.” Thinking Machines Lab, September 2025.
-
Atil, Berk et al. “Non-Determinism of ‘Deterministic’ LLM Settings.” arXiv:2408.04667, August 2024.
-
SGLang Team. “Towards Deterministic Inference in SGLang and Reproducible RL Training.” LMSYS Blog, September 2025.
-
vLLM Documentation. “Batch Invariance.” https://docs.vllm.ai/en/latest/features/batch_invariance/
-
PyTorch Documentation. “Reproducibility.” https://docs.pytorch.org/docs/stable/notes/randomness.html
-
LLM-42 Team. “Enabling Determinism in LLM Inference with Verified Speculation.” arXiv:2601.17768, January 2026.
-
EigenCloud. “EigenAI: Deterministic Inference, Verifiable Results.” arXiv:2602.00182, January 2026.
-
NVIDIA Developer Forums. “Reproducibility of atomic operations.” 2019.
-
CUDA Programming Guide. “Floating-Point Computation.” NVIDIA, 2025.