一个看似简单的参数

当你第一次在API文档中看到seed参数时,可能会觉得它只是一个普通的整数。但这个看似简单的参数,却隐藏着大语言模型推理过程中最深层的随机性控制机制。

一个有趣的现象是:许多开发者在面试中被问到"为什么同一个问题,大模型每次回答都不一样"时,往往只能说出"因为有温度采样"这个表层原因。而当你追问"那seed参数是怎么工作的"时,大多数人就语焉不详了。

这不是一个简单的问题。要真正理解seed参数,我们需要从伪随机数生成器的底层原理出发,穿越GPU计算的非确定性迷宫,最终抵达现代推理框架的批次不变性设计。

大模型输出的随机性从何而来

自回归生成与概率分布

大语言模型的文本生成过程本质上是一个自回归过程:给定已生成的文本序列,模型预测下一个token的概率分布。这个概率分布是通过softmax函数从logits转换而来:

$$P(w_i) = \frac{e^{z_i}}{\sum_{j=1}^{n} e^{z_j}}$$

其中$z_i$是模型输出的第$i$个token的logit值,$n$是词表大小。

关键在于,模型给出的是一个概率分布,而不是一个确定的token。从概率分布中选择哪个token,这就是随机性的来源。这个选择过程被称为解码策略

flowchart LR
    A[输入文本] --> B[Tokenizer]
    B --> C[Token ID序列]
    C --> D[模型前向传播]
    D --> E[Logits向量]
    E --> F[Softmax]
    F --> G[概率分布]
    G --> H{解码策略}
    H -->|贪婪| I[选择最高概率]
    H -->|采样| J[随机选择]
    I --> K[确定性输出]
    J --> L[随机性输出]

贪婪搜索与采样

最简单的解码策略是贪婪搜索:直接选择概率最高的token。这种方法完全确定性——同样的输入,永远得到同样的输出。

但贪婪搜索有一个致命缺陷:它会让模型输出变得单调乏味。想象一下,如果每次问"今天天气怎么样",模型都回答"我不知道今天的具体天气情况,因为我无法获取实时信息",这个世界该多么无聊。

所以,现代大模型普遍采用采样策略:根据概率分布随机选择token。概率高的token更有可能被选中,但不是必然被选中。这就像掷骰子——每个面都有被选中的可能,但概率各不相同。

graph TD
    subgraph 贪婪搜索
        A1[概率分布] --> B1[选择最大值]
        B1 --> C1[Token A: 60%]
        C1 --> D1[输出: A]
        D1 --> E1[完全确定性]
    end
    
    subgraph 概率采样
        A2[概率分布] --> B2[掷骰子]
        B2 --> C2{结果?}
        C2 -->|60%概率| D2[Token A]
        C2 -->|30%概率| E2[Token B]
        C2 -->|10%概率| F2[Token C]
        D2 --> G2[输出随机]
        E2 --> G2
        F2 --> G2
    end

Seed参数的技术本质

伪随机数生成器

Seed参数的核心是伪随机数生成器(Pseudo-Random Number Generator, PRNG)。

真正的随机数需要物理熵源(如放射性衰变、热噪声),而计算机中的随机数都是"伪"的——它们由确定性算法生成,只是看起来像随机而已。

最经典的PRNG算法是Mersenne Twister(MT19937),被NumPy和Python的random模块作为默认实现。它的工作原理可以简化为:

flowchart TD
    A[Seed: 42] --> B[初始化状态数组<br/>624个32位整数]
    B --> C[状态转换<br/>确定性的数学运算]
    C --> D[输出随机数]
    D --> E{需要更多?}
    E -->|是| C
    E -->|否| F[结束]
    
    G[Seed: 123] --> H[初始化不同的状态]
    H --> I[产生完全不同的<br/>随机数序列]
    
    style A fill:#e1f5fe
    style G fill:#fff3e0

关键洞察是:给定相同的seed,PRNG将产生完全相同的随机数序列。这就是seed参数能够实现可复现性的根本原因。

PyTorch中的Seed实现

当你在PyTorch中调用torch.manual_seed(42)时,发生了以下事情:

# 概念性代码,非实际实现
def manual_seed(seed):
    # 设置CPU随机状态
    _C.set_rng_state(_hash_seed(seed))
    
    # 设置CUDA随机状态(每个GPU设备)
    if torch.cuda.is_available():
        for i in range(torch.cuda.device_count()):
            torch.cuda.manual_seed_all(seed + i)
    
    # 同步NumPy随机状态
    np.random.seed(seed)

值得注意的是,PyTorch为每个CUDA设备维护独立的随机状态。这意味着在多GPU环境下,仅设置一个全局seed可能不够——你需要确保每个设备都使用正确的seed。

graph TD
    A[torch.manual_seed 42] --> B[CPU随机状态]
    A --> C[CUDA设备0随机状态]
    A --> D[CUDA设备1随机状态]
    A --> E[NumPy随机状态]
    
    B --> F[CPU上的采样]
    C --> G[GPU 0上的采样]
    D --> H[GPU 1上的采样]
    E --> I[NumPy操作]
    
    F --> J[所有操作使用<br/>相同的随机序列起点]
    G --> J
    H --> J
    I --> J

采样过程中的Seed作用

在大模型推理中,seed影响的是从概率分布采样的过程。以PyTorch的multinomial采样为例:

import torch

logits = torch.tensor([2.5, 1.8, 0.5, -0.3])  # 模型输出的logits
probs = torch.softmax(logits / temperature, dim=-1)  # 温度缩放后的概率

# seed决定采样结果
torch.manual_seed(42)
sample1 = torch.multinomial(probs, num_samples=1)  # 可能选中token 1

torch.manual_seed(42)
sample2 = torch.multinomial(probs, num_samples=1)  # 必然与sample1相同

这里的关键是torch.multinomial内部使用的随机数生成器状态由seed控制。相同的seed意味着相同的随机数序列,从而产生相同的采样结果。

sequenceDiagram
    participant U as 用户代码
    participant PRNG as 伪随机数生成器
    participant S as 采样函数
    
    U->>PRNG: set_seed(42)
    PRNG->>PRNG: 初始化状态
    U->>S: multinomial([0.6, 0.3, 0.1])
    S->>PRNG: request_random()
    PRNG-->>S: 0.73
    S->>S: 比较概率区间
    S-->>U: Token 1
    
    Note over U,S: 第二次调用
    
    U->>PRNG: set_seed(42)
    PRNG->>PRNG: 重置为相同状态
    U->>S: multinomial([0.6, 0.3, 0.1])
    S->>PRNG: request_random()
    PRNG-->>S: 0.73
    S->>S: 相同的比较逻辑
    S-->>U: Token 1

温度参数与Seed的协同工作

温度的数学意义

温度参数$T$通过缩放logits来改变概率分布的形状:

$$P(w_i) = \frac{e^{z_i / T}}{\sum_{j=1}^{n} e^{z_j / T}}$$

当$T \to 0$时,概率分布变得极端:最高概率的token接近1,其他接近0。这实际上退化为贪婪搜索。

当$T \to \infty$时,所有token的概率趋于均匀分布,采样变得完全随机。

graph LR
    subgraph T=0.1
        A1[Token A: 99.9%]
        B1[Token B: 0.1%]
        C1[其他: ~0%]
    end
    
    subgraph T=1.0
        A2[Token A: 58%]
        B2[Token B: 29%]
        C2[Token C: 8%]
        D2[其他: 5%]
    end
    
    subgraph T=2.0
        A3[Token A: 40%]
        B3[Token B: 28%]
        C3[Token C: 15%]
        D3[Token D: 10%]
        E3[Token E: 7%]
    end
    
    style A1 fill:#4caf50
    style A2 fill:#8bc34a
    style A3 fill:#cddc39

温度与Seed的交互

这里有一个重要的技术细节:温度=0时,seed参数实际上不起作用

原因是当温度趋近于0时,概率分布趋近于一个one-hot向量——只有一个token的概率为1,其余为0。在这种情况下,采样必然选择那个概率为1的token,不需要任何随机性。

flowchart TD
    A[模型输出Logits] --> B{Temperature?}
    B -->|T = 0| C[贪婪搜索]
    B -->|T > 0| D[温度缩放]
    C --> E[选择最高概率Token]
    E --> F[确定性输出<br/>Seed无效]
    D --> G[Softmax归一化]
    G --> H[概率采样]
    H --> I{Seed设置?}
    I -->|已设置| J[使用Seed初始化PRNG]
    I -->|未设置| K[使用系统时间等初始化]
    J --> L[确定性输出]
    K --> M[随机输出]

这个图揭示了seed参数工作的前置条件:只有在温度大于0时,seed才能发挥作用

一个具体的数值例子

假设模型对下一个token的预测如下(简化为5个token):

Token Logit T=0.1概率 T=1.0概率 T=2.0概率
‘A’ 2.5 99.91% 58.23% 40.38%
‘B’ 1.8 0.09% 28.91% 28.46%
‘C’ 0.5 0.00% 7.88% 14.86%
‘D’ -0.3 0.00% 3.54% 9.96%
‘E’ -1.2 0.00% 1.44% 6.35%

在T=0.1时,token ‘A’有99.91%的概率被选中。无论seed如何设置,几乎总是选择’A’。

在T=2.0时,概率分布更加平坦。seed=42可能选择’B’,而seed=123可能选择’A’。

graph TD
    subgraph 低温度T=0.1
        A1[Seed=42: A]
        A2[Seed=123: A]
        A3[Seed=456: A]
        A4[几乎总是选A]
    end
    
    subgraph 中温度T=1.0
        B1[Seed=42: B]
        B2[Seed=123: A]
        B3[Seed=456: A]
        B4[开始出现差异]
    end
    
    subgraph 高温度T=2.0
        C1[Seed=42: B]
        C2[Seed=123: C]
        C3[Seed=456: A]
        C4[差异明显]
    end

这就是为什么在实际应用中,我们通常说:

  • 温度=0:获得最确定的输出
  • 温度>0 + 固定seed:获得可复现的创意输出
  • 温度>0 + 随机seed:获得不可复现的创意输出

GPU计算的非确定性陷阱

理解了seed的基本原理后,你可能会认为:只要设置了seed,就能获得完全可复现的输出。但现实远比这复杂。

浮点数的非结合性

浮点数运算的一个基本性质是:不满足结合律

$$(a + b) + c \neq a + (b + c)$$

这不是bug,而是IEEE 754浮点数标准的固有特性。当两个数量级相差很大的数相加时,精度损失不可避免。

graph TD
    A["三个数: 1e10, 1.0, -1e10"] --> B{加法顺序}
    
    B --> C["顺序1: 1e10+1.0 = 1e10<br/>1e10+(-1e10) = 0<br/>结果: 0"]
    B --> D["顺序2: 1e10+(-1e10) = 0<br/>0+1.0 = 1.0<br/>结果: 1.0"]
    B --> E["顺序3: 1.0+(-1e10) ≈ -1e10<br/>1e10+(-1e10) = 0<br/>结果: 0"]
    
    C --> F[相同输入<br/>不同结果!]
    D --> F
    E --> F
    
    style F fill:#ffcdd2

GPU并行计算的顺序问题

在GPU上进行大规模并行计算时,多个线程同时执行浮点运算。求和操作的顺序取决于线程调度,而线程调度是非确定性的。

# 简化的示例
# 假设要计算 sum([1e10, 1.0, -1e10, 1.0])

# 顺序1:(1e10 + 1.0) + (-1e10 + 1.0) = 1e10 + (-1e10) + 2 = 2
# 顺序2:(1e10 + -1e10) + (1.0 + 1.0) = 0 + 2 = 2
# 顺序3:(1e10 + (-1e10 + 1.0)) + 1.0 = (1e10 - 1e10) + 1.0 = 1.0

# 结果可能不同!

在GPU上,当使用原子操作(atomic operations)进行归约时,加法顺序由硬件决定,每次运行可能不同。

原子操作与非确定性

深度学习框架中广泛使用原子操作来加速并行计算,特别是在注意力机制的softmax计算中。这直接导致了非确定性:

“As demonstrated in the previous section, any kernels PyTorch uses that rely on atomic operations will not be deterministic, leading to output variability.” — arXiv:2408.05148

flowchart TD
    A[GPU并行计算] --> B[多个线程同时执行]
    B --> C[需要归约求和]
    C --> D{使用原子操作?}
    
    D -->|是| E[线程调度决定加法顺序]
    E --> F[顺序非确定性]
    F --> G[浮点非结合性]
    G --> H[输出存在微小差异]
    
    D -->|否| I[需要同步开销]
    I --> J[性能下降]
    
    H --> K[即使Seed相同<br/>输出也可能不同]
    
    style K fill:#fff3e0

这就是为什么即使设置了seed,在不同的硬件或不同的批处理配置下,模型输出仍可能不同。

批次不变性:确定性的关键

什么是批次不变性

批次不变性(Batch Invariance)是指:模型的输出应该独立于批处理的大小和顺序。也就是说,同一个请求,无论是单独处理还是与其他请求一起批处理,都应该产生相同的输出。

flowchart LR
    subgraph Scenario1["场景1:单独处理"]
        A1[请求A] --> B1[模型推理]
        B1 --> C1[输出A']
    end
    
    subgraph Scenario2["场景2:批处理"]
        A2[请求A] --> B2[批处理推理]
        X2[请求X] --> B2
        B2 --> C2[输出A'']
    end
    
    C1 -.->|批次不变性要求相同| C2

为什么缺乏批次不变性会导致非确定性

现代LLM推理服务器使用动态批处理(dynamic batching):多个用户请求被组合成一个批次一起处理以提高效率。问题在于:

  1. 不同批次大小导致不同的并行模式:批次越大,GPU线程的调度方式越不同
  2. 填充(padding)影响计算:不同长度的请求需要不同的填充策略
  3. 注意力掩码的处理:不同批次配置可能导致不同的注意力计算路径

这些都可能触发前面提到的浮点数非结合性问题,导致输出差异。

graph TD
    A[动态批处理服务器] --> B[请求队列]
    B --> C{批次大小}
    
    C -->|小批次| D[少量线程并行]
    C -->|大批次| E[大量线程并行]
    
    D --> F[加法顺序A]
    E --> G[加法顺序B]
    
    F --> H[浮点累积误差α]
    G --> I[浮点累积误差β]
    
    H --> J[输出差异<br/>即使输入相同]
    I --> J
    
    style J fill:#ffcdd2

vLLM的批次不变性实现

vLLM通过以下方式实现批次不变性:

  1. 确定性内核实现:使用不依赖原子操作的注意力计算内核
  2. 一致的数值行为:确保不同批次大小下的计算顺序一致
  3. 禁用某些优化:关闭可能引入非确定性的优化(如自定义all-reduce操作)
flowchart TD
    A[vLLM批次不变性] --> B[确定性内核]
    A --> C[一致数值行为]
    A --> D[禁用非确定优化]
    
    B --> E[避免原子操作]
    B --> F[固定计算顺序]
    
    C --> G[统一填充策略]
    C --> H[一致的注意力掩码]
    
    D --> I[关闭自定义All-Reduce]
    D --> J[使用确定性算法]
    
    E --> K[可复现输出]
    F --> K
    G --> K
    H --> K
    I --> K
    J --> K
    
    style K fill:#c8e6c9

启用方式:

import os
os.environ["VLLM_BATCH_INVARIANT"] = "1"

需要注意的是,批次不变性目前有硬件要求:需要NVIDIA H100/H200或B100/B200等计算能力9.0以上的GPU。

API层面的可复现性

OpenAI API的Seed参数

OpenAI在2023年11月引入了seed参数,官方文档说明:

“To receive (mostly) deterministic outputs across API calls, you can: Set the seed parameter to any integer of your choice and use the same value across requests you’d like deterministic outputs for.”

注意关键词"mostly"——OpenAI不保证100%的可复现性。

system_fingerprint的作用

OpenAI响应中包含system_fingerprint字段,这是一个标识符,代表当前运行模型的具体配置组合:

  • 模型权重版本
  • 基础设施配置
  • 其他后端设置

system_fingerprint发生变化时,即使seed相同,输出也可能不同。这是追踪模型版本变化的重要机制。

sequenceDiagram
    participant C as 客户端
    participant A as API网关
    participant S as 模型服务器v1
    participant S2 as 模型服务器v2
    
    C->>A: 请求 + seed=42
    A->>S: 转发请求
    S->>S: 生成响应
    S-->>C: 输出A + fingerprint=X
    
    Note over C,S: 一周后,模型更新
    
    C->>A: 请求 + seed=42
    A->>S2: 转发请求
    S2->>S2: 生成响应
    S2-->>C: 输出B + fingerprint=Y
    
    Note over C: fingerprint不同<br/>输出可能不同
import openai

response = openai.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Hello"}],
    seed=42,
    temperature=0.7
)

# 记录fingerprint以便追踪变化
fingerprint = response.system_fingerprint

为什么API不能保证完全确定性

即使设置了seed,云API服务仍可能因为以下原因产生不同输出:

graph TD
    A[API请求+Seed] --> B{不可控因素}
    
    B --> C[后台模型更新]
    B --> D[负载均衡到不同服务器]
    B --> E[基础设施变化]
    B --> F[动态批处理差异]
    
    C --> G[权重版本不同]
    D --> H[GPU/驱动版本不同]
    E --> I[CUDA/cuDNN版本不同]
    F --> J[计算路径不同]
    
    G --> K[输出可能不同]
    H --> K
    I --> K
    J --> K
    
    style K fill:#fff3e0

这就是为什么OpenAI使用"mostly deterministic"这样的表述。

生产环境中的最佳实践

测试与调试场景

在需要可复现输出的场景(如单元测试、回归测试)中:

def test_model_output():
    # 设置所有随机种子
    import random
    import numpy as np
    import torch
    
    SEED = 42
    random.seed(SEED)
    np.random.seed(SEED)
    torch.manual_seed(SEED)
    if torch.cuda.is_available():
        torch.cuda.manual_seed_all(SEED)
    
    # 对于CUDA操作,设置确定性模式
    torch.use_deterministic_algorithms(True)
    torch.backends.cudnn.deterministic = True
    
    # 设置CUBLAS工作空间配置
    import os
    os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':4096:8'
    
    # 现在可以预期可复现的输出
    output = model.generate(...)
    assert output == expected_output

API调用场景

使用云API时的推荐做法:

import openai
import hashlib

def reproducible_completion(prompt, seed=42, temperature=0.7):
    response = openai.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        seed=seed,
        temperature=temperature
    )
    
    # 记录fingerprint用于后续验证
    return {
        "content": response.choices[0].message.content,
        "fingerprint": response.system_fingerprint,
        "seed": seed
    }

# 存储结果用于对比
results = []
for _ in range(10):
    result = reproducible_completion("Tell me a joke")
    results.append(result)
    
# 验证所有fingerprint相同(模型版本未变)
fingerprints = set(r["fingerprint"] for r in results)
if len(fingerprints) > 1:
    print("警告:检测到模型配置变化!")

本地部署场景

使用vLLM等框架本地部署时的推荐配置:

import os

# 启用批次不变性(需要H100/B100等硬件)
os.environ["VLLM_BATCH_INVARIANT"] = "1"

# 或者禁用多进程以获得确定性调度
os.environ["VLLM_ENABLE_V1_MULTIPROCESSING"] = "0"

from vllm import LLM, SamplingParams

llm = LLM(model="meta-llama/Llama-3.1-8B-Instruct")
params = SamplingParams(
    temperature=0.7,
    top_p=0.95,
    seed=42,
    max_tokens=100
)

# 相同输入,相同输出
output1 = llm.generate(["Hello"], params)
output2 = llm.generate(["Hello"], params)
assert output1[0].outputs[0].text == output2[0].outputs[0].text
flowchart TD
    A[可复现性需求] --> B{部署方式}
    
    B -->|本地部署| C[设置PyTorch Seed]
    C --> D[启用确定性算法]
    D --> E[启用批次不变性]
    E --> F[可复现输出]
    
    B -->|云API| G[设置Seed参数]
    G --> H[监控system_fingerprint]
    H --> I[检测配置变化]
    I --> J[部分可复现]
    
    style F fill:#c8e6c9
    style J fill:#fff3e0

常见误区与澄清

误区一:设置seed就能获得完全确定性

这是最常见的误解。实际上,seed只控制采样过程的随机性,不解决GPU计算的非确定性问题。要获得完全确定性,还需要:

  • 设置确定性算法模式
  • 确保批次不变性
  • 使用相同的硬件和软件环境

误区二:温度=0等价于设置seed

温度=0确实使输出确定,但这是通过使用贪婪搜索实现的,而不是通过控制随机性。两种方式的输出可能完全不同:

  • 温度=0:永远选择最高概率token
  • 温度>0 + seed:在概率分布中采样,但采样序列可复现

误区三:不同的seed值会导致完全不同的输出

seed只影响随机数序列的起点。对于温度较低的情况,概率分布已经相当集中,不同的seed可能产生相同的输出。只有当温度较高、概率分布较平坦时,不同seed的影响才明显。

误区四:云API的seed参数没用

虽然云API不能保证100%确定性,但seed参数仍然有价值:

  • 在短期内(模型版本未变),提供可复现性
  • 通过system_fingerprint追踪变化
  • 对于调试和测试仍然有用
graph TD
    subgraph 误区
        A1[Seed = 完全确定性]
        A2[T=0 等价 Seed]
        A3[不同Seed必然不同输出]
        A4[API的Seed没用]
    end
    
    subgraph 事实
        B1[Seed只控制采样随机性<br/>GPU非确定性仍存在]
        B2[T=0用贪婪搜索<br/>T>0+Seed用采样]
        B3[低温度时不同Seed<br/>可能输出相同]
        B4[短期可复现<br/>fingerprint追踪变化]
    end
    
    A1 -.->|纠正| B1
    A2 -.->|纠正| B2
    A3 -.->|纠正| B3
    A4 -.->|纠正| B4
    
    style A1 fill:#ffcdd2
    style A2 fill:#ffcdd2
    style A3 fill:#ffcdd2
    style A4 fill:#ffcdd2
    style B1 fill:#c8e6c9
    style B2 fill:#c8e6c9
    style B3 fill:#c8e6c9
    style B4 fill:#c8e6c9

技术面试中的相关问题

理解seed参数是许多大模型相关面试的基础。以下是一些常见问题:

Q1: 为什么同一个问题,大模型每次回答都不一样?

A: 大模型采用概率采样的解码策略。模型输出的是每个token的概率分布,采样过程引入随机性。这是特性而非bug——它让模型输出更自然、多样。

Q2: 如何让大模型输出可复现?

A: 多层保障:

  1. 设置seed参数控制采样随机性
  2. 设置temperature=0使用贪婪搜索
  3. 对于本地部署,启用批次不变性
  4. 使用相同的硬件和软件环境
  5. 对于API,监控system_fingerprint变化

Q3: seed参数在什么情况下无效?

A:

  • 当temperature=0时,使用贪婪搜索,seed不起作用
  • 当模型后端配置变化(system_fingerprint改变)时
  • 当硬件或软件环境不同时,GPU非确定性可能导致输出差异

Q4: 解释批次不变性及其重要性?

A: 批次不变性指模型输出独立于批处理配置。缺乏批次不变性会导致:同一请求在不同批处理配置下产生不同输出。这在动态批处理的生产环境中是严重问题。实现批次不变性需要使用确定性的内核实现,避免原子操作带来的非确定性。

总结

Seed参数是大模型推理中一个看似简单却内涵丰富的组件。它的工作原理涉及:

  1. 伪随机数生成器:通过确定性算法产生看似随机的序列,seed决定序列起点
  2. 概率采样:seed控制从概率分布采样的随机性
  3. GPU非确定性:浮点数非结合性和原子操作带来额外挑战
  4. 批次不变性:确保输出独立于批处理配置

在实际应用中,获得可复现输出需要多层保障:设置seed只是第一步,还需要考虑温度参数、GPU计算模式、批次处理配置等多个因素。

理解这些机制,不仅有助于写出更可靠的代码,也能在面试中展现出对大模型推理过程的深入理解。毕竟,在大模型时代,“知道模型如何工作"和"知道如何让模型按预期工作"是两个不同层次的能力。

参考资料

  1. OpenAI API Documentation - Advanced Usage: Reproducible outputs
  2. vLLM Documentation - Reproducibility and Batch Invariance
  3. Dylan Castillo - “Controlling randomness in LLMs: Temperature and Seed”
  4. arXiv:2408.05148 - “Impacts of floating-point non-associativity on reproducibility for HPC and deep learning applications”
  5. Thinking Machines Lab - “Defeating Nondeterminism in LLM Inference”
  6. PyTorch Documentation - Random Number Generation
  7. NumPy Documentation - Mersenne Twister (MT19937)
  8. Hugging Face Transformers - Generation Configuration
  9. NVIDIA Developer Blog - “Controlling Floating-Point Determinism in NVIDIA CCCL”