temperature=0、seed=0 也不等于完全确定, 工程实践的过程中总会有取舍, 请允许理想与现实的偏差.

大佬的镇楼图

导读

温度=0 + 种子固定,最多是“更接近 deterministic”,而不是“数学意义的 deterministic”。介不就是光速与绝对零度嘛。

在真实工程环境下,即使做到:

  • temperature = 0
  • 随机种子(seed)固定为 0
  • 模型权重不变
  • 输入 Prompt 字节级一致

LLM 输出依然无法保证严格的确定性。 原因来自多个层级:采样配置陷阱、浮点数值误差、Batch 动态调度、MoE 路由竞争以及底层算子差异。
工程实践中,更现实的目标不是追求数学意义上的“位级完全一致(Bit-wise Deterministic)”,而是通过参数控制、架构设计与缓存机制,把模型行为收敛在业务可接受的稳定性范围内。

理想化推理链路:
固定权重 + 固定输入 + Greedy 解码(每步选 argmax) + 完全确定的数值计算 ⇒ 输出必然完全一致。

  • temperature (温度):
    • 越高 → 分布越“平”,强随机采样;
    • 越低 → 分布越“尖”,趋向于 Greedy;
    • 理论上 temperature=0 对应“总是选最大值(Argmax)”,但工程上常被框架处理为“极小噪声采样”或依赖特定的实现逻辑。
  • seed (随机种子):
    • 仅控制“伪随机数生成器(RNG)”的初始化序列;
    • 只对依赖随机采样的步骤生效;
    • 对并行计算中的归约顺序(浮点误差)、Batch 调度策略等物理层面的随机性完全无效。

下文将按照 “排查优先级” 对这些干扰因素进行分层解析,并给出相应的规避策略。

推理过程与参数参与情况

工程中概率排行

请求配置层

我不是像 AI 一样让你检查 temperature=0 , 是要理解配置层不只是请求参数, 这是高频误区:

  • 仍然在用采样:top_p < 1top_k > 1,只是温度降到 0;
  • 请求 n > 1,服务端对多条采样结果再做内部选择;
  • 模型供应商 对 temperature=0 做了特殊处理或者忽略0,比如强制改成一个很小但非 0 的温度;
  • 部分供应商接口文档应该写的是 “mostly deterministic” 或 “best effort reproducibility”,并未承诺严格一致。

这些都意味着你以为关掉了随机性,实际上还在采样,而且 seed 只保证“这条采样序列可复现”,并不保证不采样。工程上遇到的多数“T=0 还在变”的案例,根因都在这一层。

参数建议
1
2
3
4
5
6
7
8
9
10
11
{
"model": "xxx",
"temperature": 0.0, // 或一个非常小的值,例如 0.1
"top_p": 1.0, // 不再做 nucleus 截断
"n": 1, // 不要生成多候选再让服务端挑
"seed": 42, // 固定为某个整数,在同一测试中保持不变
"presence_penalty": 0.0, // 禁用额外的随机去重行为
"frequency_penalty": 0.0,
"max_tokens": 256, // 评估/回归测试时也建议固定
"stop": null // 如无特殊需要,不要动态变化
}

请求调度层

Batch 批次请求打包产生差异

在云端 API 上,请求通常如下情况:

  • 被和其他用户的请求一起打包进不同大小的 batch;
  • 由底层推理引擎根据 batch 维度选择不同的并行 kernel 或归约策略。

这会导致:

  • 并行计算的归约顺序不确定导致浮点加法结果差异(不满足结合律)=> (a+b)+c!=a+(b+c)
  • attention / matmul / RMSNorm 的归约路径不同;
  • logits 在 1e-6 量级上产生差异;
  • 若两个 token 概率本来就非常接近,argmax 可能翻转,后续生成路径完全分叉。

《Non-Determinism of “Deterministic” LLM Settings》在理论“应当确定”的配置下反复测试,发现输出字符串的一致率明显小于100%,下游任务准确率在不同 run 间可以差十几个百分点。

对云 API 来说,这是最常见且几乎不可控的非确定性来源。用户也不可能决定好每个批次元素的归约顺序.

模型算子层

浮点并行本身就是非确定性的

即便你在本地单机推理,只要使用 GPU / 并行 kernel,也会碰到:

  • 并行归约(atomicAdd 等)导致累加顺序未定义;
  • cudnn/cublas/自定义 kernel 采用了不同实现路径;
  • 多线程抢占导致不同 run 间执行顺序略有差异。

数值差异微小,但 softmax + argmax 会放大这些差异;自回归生成会进一步放大第一步 argmax 的差异。

PyTorch 论坛中多次讨论:即使 model.eval(),也需要额外开启 deterministic 模式,
否则多次 inference 仍然无法 bit-wise 一致。

批次与浮点计算可以合在一起看,下图是一个简单的归因链路示意:

浮点非结合性效应

模型解码层

不同框架/服务对于 “temperature=0” 的实现并不统一:

  • 有的分支直接走 greedy,不采样;
  • 有的把 0 改成一个很小的正数,仍然进行采样;
  • 有的在低温下仍然允许 nucleus / top-k 筛选后采样。

再叠加 tie-breaking 策略:

  • 概率相等或近似相等时,是按 token id 排序选第一个
  • 还是仍然用 RNG 做一次随机决策

这些实现级细节,很容易在“理论上相同配置”的两次调用之间,积累成肉眼可见的输出差异。

实际代码里,几个主流高 star 项目对 temperature=0 的处理就已经完全不一样:Transformers 直接视为非法值, vLLM 把它解释成“强制 greedy 并重写 top_p/top_k”, llama.cpp 则在不同版本中先后把非正温度当作 greedy 的捷径、后来又要求配合 top‑k 才能得到真正的 greedy 行为。
这本身就说明:“temperature=0”的语义强依赖具体框架实现,目前并没有统一标准。

模型架构层

对采用 Mixture-of-Experts(MoE)的模型:

  • 每个 token 会先经过 gating 网络决定路由到哪些专家子网络;
  • 为了负载均衡,路由逻辑中可能包含截断、近似甚至随机裁剪;
  • 当不同 batch 下竞争同一专家时,调度顺序变化会改变路由结果。

链路复现性

模型/系统版本漂移, 部署的集群非一致, 请求的链路不一定落到系统、驱动、算子全一致的软硬件上。

在云端服务里,以下情况都很常见:

  • 模型权重热更新、系统 prompt 调整;
  • 不同 region / 集群挂载了略有差异的模型快照;
  • 路由策略在多个版本间做灰度。

同一个 model name 在不同时刻/不同 region 调用,底层实际上可能已经不是同一个模型实例。
这更常见于“隔一段时间”再次调用发现结果不同,而非“连续两次立刻不同”。

输入一致性

常见人祸因素:

  • prompt 拼装引入 time、userid、skill 等隐变量;
  • system prompt / few-shot demo 在不同调用间细微变化;
  • 不可见字符(BOM、零宽空格)或换行差异。

日志里看上去完全一样,但序列化出来并不一样。这一类问题本身不深,只说明排查非确定性前先校验输入字节级是否一致。

llm-Indeterminacy-why

如何尽量确定性

下面区分两种场景:云 API 调用 / 自建推理。

防御策略

黑盒对抗赛

  1. 配置层:参数去随机化
    • 配置:temperature 非 0 但极低(如 0.01–0.1),避免 0 被特殊处理;
    • 采样:锁定 top_p = 1、不使用 top_k、n = 1;
    • 文档:查阅各家关于 deterministic 字段的说明,按官方建议传参。
  2. 输入层:严格归一化 (针对“输入一致性”)
    • 确保在计算签名或调用前,对 Prompt 进行 字节级清洗对比, 严查时间戳等变量注入;
    • 剔除零宽空格、统一换行符 (\n vs \r\n)、并在 JSON 序列化时保证 key 顺序固定。
  3. 业务层:利用缓存构造“伪确定性” (针对“链路复现性”)
    • 定义一个请求签名: (model_version, temperature, top_p, system prompt, user prompt, seed);
    • 命中即返回:确保“相同参数 + 输入 ⇒ 永远返回同一条缓存结果”,在业务侧屏蔽底层的微小波动。
  4. 兜底策略:把 LLM 视为噪声组件
    • 结构化输出:LLM 生成候选 → Schema/Parser 严格校验 → 失败则重试;
    • 多数票机制:对关键分类任务发起 3 次调用取共识(Majority Vote);
    • 预期管理:在业务逻辑里显式兼容小范围的不一致。

控制策略

白盒自控主场赛

  1. 解码层:强制 Greedy
    • 在 vLLM / Transformers 中显式设置 do_sample=False;
    • 彻底禁用 top_p / top_k,防止框架内部的“低温采样”行为;
    • 若用 Beam Search,禁用所有涉及随机扰动的参数。
  2. 配置层:全链路种子固定
    • 代码级固定:torch.manual_seed、torch.cuda.manual_seed_all、numpy.random.seed;
    • 这只能解决“采样层”的随机性,无法解决“算子层”的并发噪声。
  3. 算子层:开启确定性模式 (牺牲性能)
    • 开启 PyTorch 确定性算法:torch.use_deterministic_algorithms(True);
    • 禁用优选 Benchmark:torch.backends.cudnn.benchmark = False;
    • 配置环境:设置 CUBLAS_WORKSPACE_CONFIG 使用确定性算法实现。
    • 代价:吞吐量显著下降,且部分高性能算子(FlashAttention 等)可能无法在确定性模式下运行。
  4. 调度层:牺牲 Batching (针对“批次差异”)
    • 独占推理:不跨请求拼 Batch,每个请求单独执行;
    • Batch-invariant 库:仅使用明确承诺了“批量无关性”的推理库版本。
    • 代价:GPU 利用率暴跌,成本激增。通常仅适用于离线审计或基准评测。

自建确定性策略带来的延迟倍率对比

数值精度与管线设计在重现性、性能与显存代价上的折中示意图

llm-Indeterminacy-do

总结

从研究与工程实践看:

  • 即使在 配置层 (temperature=0) 和 配置层 (seed) 拉满的情况下,算子层 和 调度层 的噪声依然存在;
  • 在云端 API 上追求 Bit-wise Deterministic 是不切实际的;
  • 在自建推理中,获得严格确定性的代价是大幅牺牲 吞吐与算力成本。

更合理的心智模型是:

LLM = 强大但带噪声的推理器
——在设计系统时,默认它“不完全可复现”,通过参数、缓存和上层逻辑来吸收这种噪声。

适合追求强一致性的环节(计费、风控、合规决策),应优先考虑确定性模型或规则系统;
LLM 更适合作为“辅助决策 + 文本代理”,而不是唯一的“权威判官”。

这样设计出来的系统,更符合当下大模型技术的真实边界。


文章主要借助 Perplexity + NotebookLM 检索文献和生成配图,对其中一篇核心参考强烈推荐原文阅读 Defeating Nondeterminism in LLM Inference


本站由 钟意 使用 Stellar 1.33.1 主题创建。
又拍云 提供CDN加速/云存储服务
vercelnetlifycloudflare 提供托管服务
湘ICP备2023019799号-1
总访问