一个被广泛接受的观点是:只要将大模型的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经过了大量优化,大部分情况下可以通过其他策略避免原子操作:

  1. 数据并行策略:将每个batch元素分配给独立的核心处理,每个核心完成完整的归约,不需要跨核心通信。

  2. 分块归约策略:将大归约拆分成多个小块,每块独立处理,最后用一个确定性的"清理"归约合并结果。

  3. 信号量策略:使用信号量确保并发线程块按确定性的顺序累加。

这些策略在保持高性能的同时实现了确定性。研究者明确指出:

在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需要决定:

  1. 如何划分输出矩阵:将$M \times N$的输出划分成tile,分配给不同的核心。

  2. 是否使用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长度,归约顺序就会变化

批量不变的注意力实现需要:

  1. 固定的split大小:而不是固定的split数量。这样,无论处理多少个query,每个split的归约顺序都相同。

  2. 一致处理KV cache:确保已缓存的key-value和新生成的key-value使用相同的归约策略。

  3. 对齐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启用。

实现要点:

  1. 确定性kernel:使用batch-invariant的RMSNorm、矩阵乘法和注意力实现。

  2. 禁用动态优化:关闭某些可能引入非确定性的优化,如自定义的all-reduce操作。

  3. 硬件要求:目前仅支持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,但做了更多优化:

  1. CUDA Graph加速:通过CUDA graph将多个kernel启动合并,减少开销。这使得确定性推理的性能开销从Thinking Machines Lab原始实现的61.5%降低到约34.35%。

  2. 支持非贪婪采样:引入了multinomial_with_seed操作,通过种子化的Gumbel噪声实现确定性的多项式采样,即使在temperature > 0时也能保证可复现性。

  3. 多后端支持:支持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

关键观察:

  1. 不一致是稀疏的:实验表明,在大多数情况下,即使使用非确定性路径,生成的token序列也是一致的。不一致通常只发生在少数几个位置。

  2. 验证是廉价的:验证器使用固定的输入形状(固定窗口大小),可以利用优化的确定性kernel。而且验证是批量并行计算,类似prefill阶段,效率远高于decode阶段。

  3. 选择性应用:只有明确标记为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=0seed,以下情况仍会导致非确定性:

  1. 动态批处理:你的请求与其他请求混合处理
  2. 跨GPU运行:不同GPU型号产生不同结果
  3. 软件版本差异:PyTorch、CUDA、cuDNN版本变化
  4. 量化差异:FP16与BF16的结果不完全一致

PyTorch官方文档明确警告:

完全可复现的结果不能保证跨PyTorch版本、不同提交或不同平台。即使使用相同的种子,结果也可能在CPU和GPU执行之间不可复现。

总结

Temperature=0不等于确定性输出,这个看似简单的声明背后,是一系列深刻的技术问题:

根本原因:浮点数的非结合性结合GPU kernel对输入形状的动态适应。当批量大小变化时,kernel的归约策略变化,累加顺序不同,最终结果不同。

解决方案:批量不变性——强制kernel使用单一的归约策略,无论输入形状如何变化。这需要主动设计,牺牲部分性能。

工程实现:vLLM和SGLang已支持批量不变推理,性能开销约25-50%。LLM-42提出的选择性验证方法有望降低这个开销。

实际意义:确定性推理对强化学习训练、系统调试、安全审计等场景至关重要。但对需要创造性和多样性的应用,非确定性反而是一个特性。

理解这些原理,才能在正确的场景做出正确的选择。确定性不是默认的,它是一种需要主动设计、付出代价、并根据需求权衡的工程决策。

参考资料

  1. He, Horace et al. “Defeating Nondeterminism in LLM Inference.” Thinking Machines Lab, September 2025.

  2. Atil, Berk et al. “Non-Determinism of ‘Deterministic’ LLM Settings.” arXiv:2408.04667, August 2024.

  3. SGLang Team. “Towards Deterministic Inference in SGLang and Reproducible RL Training.” LMSYS Blog, September 2025.

  4. vLLM Documentation. “Batch Invariance.” https://docs.vllm.ai/en/latest/features/batch_invariance/

  5. PyTorch Documentation. “Reproducibility.” https://docs.pytorch.org/docs/stable/notes/randomness.html

  6. LLM-42 Team. “Enabling Determinism in LLM Inference with Verified Speculation.” arXiv:2601.17768, January 2026.

  7. EigenCloud. “EigenAI: Deterministic Inference, Verifiable Results.” arXiv:2602.00182, January 2026.

  8. NVIDIA Developer Forums. “Reproducibility of atomic operations.” 2019.

  9. CUDA Programming Guide. “Floating-Point Computation.” NVIDIA, 2025.