跳到主要内容

你的评估套件设置了确定性种子,但供应商却悄悄忽略了它

· 阅读需 13 分钟
Tian Pan
Software Engineer

你设置了 seed=42。你设置了 temperature=0。你记录了运行情况,发布了仪表板,并在模型更换上签了字。第二天早上,重新运行相同的提示词却返回了不同的数字,你给出的解释——“肯定是采样噪声”——错得离谱:根本没有采样,而且噪声是结构性的。Seed 离开了你的客户端,网关将其丢弃,内核将你的请求与 17 个不相关的请求打包在一起,浮点归约(floating-point reduction)顺序在你眼皮底下发生了变化。你所谓的“可复现”基准测试,距离变成另一个基准测试其实只差一个批次(batch)。

这种失败模式是悄无声息的,因为技术栈中的每一层在技术上都是正确的。SDK 接受了 seed。供应商记录了 seed。模型返回了 system_fingerprint。评估工具记录了这三者。没有 5xx 错误,没有警告,没有抗议。仪表板上的数字只是发生了偏移,而团队将这种偏移合理化为一直存在的某种抖动——因为他们没有工具能告诉他们,他们看到的究竟是随机解码(stochastic decoding),还是导致三周的对比实验全部失效的后端轮换(backend rotation)。

Seed 可能消失的三个地方,而且没有一处会报警

当你调用 seed=42 的聊天补全(chat completion)时,这个整数会经过你的客户端 SDK,经过你和模型宿主之间的任何网关或代理,经过推理服务器,最后进入采样器。每一跳都有理由把它弄丢。

SDK 通常没问题。现代 OpenAI、Anthropic 和 Google 的 SDK 都会原样转发 seed。但 SDK 并不总是最后一个接触你请求的代码。如果你通过一个与供应商无关的网关路由——例如 LiteLLM、Portkey 或你平台团队编写的内部抽象层——seed 的存亡取决于该网关针对你所选供应商的适配器(adapter)是否知晓它。适配器的覆盖范围参差不齐。OpenAI 适配器会转发 seed;Bedrock 适配器可能不会将其透传;六个月前针对旧模式编写的自定义代理可能有一个硬编码的白名单,将其排除在外。无论哪种情况,请求都会返回 200。你收不到任何负面信号。

供应商自己的网关是第二个沉默的“丢弃者”。截至 2026 年中期,Anthropic 的 Opus 4.7 迁移指南更改了 temperature 和 top_p 的语义——非默认值现在会返回 400 错误——但各供应商对 seed 的处理情况依然复杂:既有“支持但尽力而为”(OpenAI 的某些模型),也有“不支持但默许接受”(历史上 Anthropic 的某些端点),还有“支持但未记录”(各种托管的开源权重模型)。最令人沮丧的是那些“默许接受”的情况:网关在进入时剥离了该字段,模型没有收到 seed,响应正常返回,而你的评估工具记录了一个毫无影响的 seed 值。

第三个地方是推理服务器本身,seed 确实到达了那里,但它只能控制极小部分的方差。vLLM 用户在过去两年的大部分时间里都在提交问题,内容大致是“我设置了 temperature=0, top_p=1, seed=42,但输出仍然不同”——而标准的回答是,seed 仅控制采样器的伪随机抽取。当采用贪婪采样(greedy sampling)时,seed 基本上不起任何作用。你在 temperature 为零时看到的差异完全来自其他地方。

批次不变性是你无法在日志中看到的方差

Thinking Machines 团队在 2025 年底发布了一项结果,重塑了严肃的从业者讨论 LLM 确定性的方式。在托管的 vLLM 上以 temperature 0 运行 Qwen3-235B,他们发送了 1,000 个相同的查询,并得到了 80 个唯一的补全结果。不是 80 个带有细微变化的补全,而是 80 个截然不同的字符串。Seed 已设置。Temperature 为零。贪婪解码已开启。模型仍然生成了 80 个答案。

起因是批次大小依赖(batch-size dependency)。现代推理服务器将并发请求打包进同一个前向传播中,以摊销 GPU 成本。在该前向传播中运行的矩阵乘法、RMSNorm 和注意力归约(attention reductions)并不是批次不变的——当批次形状(batch shape)改变时,它们的数值结果也会随之改变。浮点加法不满足结合律,因此归约过程中累加部分和(partial sums)的顺序会影响结果的最后几位,而这个顺序是由内核如何对批次进行分块(tiles)决定的。在他们的测量中,同一输入的单次矩阵乘法在 batch size 为 1 和 2048 之间相差了 1669.25。这种差异经过数十层的传播,足以翻转至少一个 token 的 argmax,而这种分歧随后会产生级联效应。

这意味着真正控制你输出的“seed”并不是你发送的那个整数。它是你的请求到达服务器那一刻隐式的批次组成——还有谁在调用 API、调度器如何将你分组、内核是否针对你的 token 数量选择了 split-K 策略。所有这些都不在你的控制之下。所有这些都不会出现在你的日志中。

同一篇论文表明,通过使用批次不变内核(batch-invariant kernels)——将矩阵乘法、RMSNorm 和注意力路径替换为无论批次形状如何,其分块和归约策略都固定的版本——所有 1,000 个补全结果都变得完全相同。代价是实打实的:运行耗时从 26 秒增加到了 42 秒,慢了约 60%。这就是实现真正可复现性的代价,而没有一家托管供应商收取这项费用,这一事实足以说明你实际购买的是什么样的保证。

system_fingerprint 是你拥有的唯一信号,而你可能根本没有在关注它

OpenAI 针对更大的结构性问题提供了一个部分解决方案。每个聊天补全(chat completion)响应都包含一个 system_fingerprint 字段,这是一个针对产生输出的权重、基础设施和配置组合的不透明标识符。如果你的指纹在多次运行中保持稳定,你就有机会获得近似的确定性。如果它发生了轮换,你就是在拿苹果比橘子 —— 即使使用相同的种子(seed)、相同的提示词(prompt)、相同的采样温度(temperature),后端也已经发生了足够的改变,以至于 OpenAI 自己都不再承诺可复现性。

对于每个模型家族,后端轮换每年会发生几次,但它们并不会被正式宣布。它们会以新的指纹哈希值的形式出现在你的一部分流量中。如果你的评估框架记录了指纹但没有基于它进行过滤,你可能会在周二早上看到看起来像模型退化(regression)的漂移,而实际上那只是部署更替。如果你的评估框架根本不记录指纹,你看到的漂移将毫无头绪。

大多数团队都会跳过的防御性模式:按 system_fingerprint 对每次评估运行进行分桶,报告每个桶的分数,并拒绝进行跨桶比较。如果两次运行是由两个不同的指纹提供服务的,那么出于基准测试的目的,它们就是两个不同的模型,即使 URL 上的模型名称完全相同。将指纹视为模型身份的一部分。供应商不会为你做这件事,因为供应商的产品故事是“你两次调用的都是 Opus 4.7”。你的评估故事必须更加具体。

Azure 的文档对于相同的参数表现得更诚实:它告诉你要预见漂移,监控指纹,并将任何与之前值不匹配的情况视为一次全新的评估。非 Azure 的文档说的也大致相同,但将其掩盖在“尽力而为(best-effort)”之下。无论哪种方式,操作行为都是一样的 —— 你的基准测试是一个数字加上一个指纹再加上一个日期,丢掉这三者中的任何一个,数字就会变成孤证。

伪装成噪声的供应商特性偏差

除了种子丢失和批处理依赖之外,还有第三类非确定性问题,它伪装成了前两者。在不同的供应商中,相同的逻辑请求会根据细微的输入触发不同的代码路径。当你设置了种子时,GPT-4o 对固定文本提示词是确定性的;一旦引入图像附件,确定性就会崩溃,即使种子和温度已锁定。专家混合(Mixture-of-Experts)模型在不同的批处理组成下会以不同的方式路由 token,而这种路由是不受种子控制的 —— 它是整个批处理中 token 的函数,因此你的单次提示词运行取决于批处理中还有什么其他内容。投机解码(Speculative decoding)又增加了一层:草稿模型和验证模型会随流量负载而随机互动。

这是打破比较直觉的部分。如果你的评估同时包含视觉和文本提示词,即使种子完全相同,视觉提示词也会产生更大的漂移。如果你在非高峰时段访问 MoE 模型,批处理形状会更细长,每条提示词的输出将与给你原始基准的高峰时段运行有所不同。如果你正在评估一个供应商悄悄启用了投机解码的模型,在相同温度下相同的提示词在不同运行之间会产生分歧,因为草稿模型偶尔会猜错。这些都不是 bug。它们都使“相同的输入、相同的种子”产生“相同的输出”这一假设失效了。

一个有用的框架:你正在吸收三种不同类型的偏差,它们需要不同的缓解措施。

  • 采样器偏差 (Sampler variance)。由种子和温度控制。容易锁定。
  • 批处理偏差 (Batch variance)。由批处理无关的内核控制,但供应商并不提供这种内核。只能通过将其接受为底噪并运行足够的样本来估算它,从而缓解。
  • 后端偏差 (Backend variance)。由 system_fingerprint 控制。通过分桶和拒绝跨指纹比较来缓解。

大多数评估框架将这三者混为一谈,统称为“分数是否改变了”。这种混淆正是基准测试数字失效的地方。

尊重这一事实的评估框架会是什么样

最小可行升级不是“使用更好的种子”。而是让评估框架诚实地反映它拥有哪种确定性。

每次评估调用记录五件事:你发送的种子、响应负载中的种子(如果返回了)、system_fingerprint、挂钟时间以及带有版本后缀的模型名称。在撰写报告时,按指纹分组,并为每个组给出一个分数。如果单次运行跨越了两个指纹,请将其作为警告浮现出来,而不是作为一个脚注。除此之外的任何做法都会向读者隐藏偏差来源。

针对同一个指纹运行每个评估提示词三到五次,并报告偏差,而不仅仅是平均值。Thinking Machines 的数据 —— 在温度为 0 的情况下,1,000 次运行中有 80 个唯一的补全 —— 意味着单次调用之间的不一致率足以淹没你比较的大多数候选模型之间的差距。如果你不测量底噪,你就会将其归因于最近发生的任何更改。

维护指纹历史。当出现新指纹时,将之前的基准视为已过期,并针对新指纹重新建立基准。一些团队保留了一组包含 200 个提示词的“参考集”,在每次指纹轮换后重新评分,这样他们关心的比较(模型 A 与模型 B)始终是在单个后端快照内进行的,而不是跨快照的。

并探测种子路径。每周一次,发送一批完全相同的提示词,分别使用相同的种子和不同的种子,并验证相同种子的批次产生的分布比不同种子的批次更紧密。如果两个分布看起来相同,说明你的种子在某处上游被丢弃了,你应该从探测中了解到这一点,而不是从一个无法复现的回归中去瞎猜。

你报告的数字是一个快照,而不是一个测量值

到 2026 年,对 LLM 评估(eval)数字的诚实定性应该是:它是你的提示词(prompts)针对特定的模型名称、特定的 system_fingerprint、在特定日期、以及供应商当时运行的任何批处理(batch)条件下所获得的分数。seed 参数缩小了由采样(sampling)带来的偏差。但它对于由批处理、指纹轮换(fingerprint rotation)、MoE 路由,或是供应商网关悄悄剔除本应通过的字段而产生的偏差无能为力。

设置了 seed 就声称其具有可复现性的评估套件并没有撒谎 —— 它只是认为自己测量的是一个比实际情况更稳定的东西。供应商也没有撒谎;文档里写着“尽力而为(best effort)”。差距在于技术栈中无人认领的那部分:即“我告诉了你 seed”与“你使用了 seed”之间的契约,而中间隔着三层无声的失败。

那些基准测试(benchmark)数据能在数周内保持稳定的团队,并不是因为选对了 seed。而是因为他们很早就决定将比较单位设定为 (model, fingerprint, batch-class) 而不仅仅是 (model)。其他所有人都在发布快照并将其视为测量值,而仪表盘的波动则完全随他人的部署计划共振。

References:Let's stay in touch and Follow me for more thoughts and updates