背景问题:相同 Seed 不等于相同输出
很多团队把大模型推理的可复现性简化为三个条件:模型权重相同、提示词相同、seed 相同。这个判断只覆盖了采样随机性,没有覆盖推理系统本身的数值和调度变化。
线上推理通常同时存在动态批次、连续调度、多 CUDA Stream、Tensor Parallel、低精度计算、融合算子和自动 Kernel 选择。浮点加法不满足结合律,同一组数值以不同顺序累加,结果可能出现细微差异。大多数时候这些差异不会改变答案;但当两个候选 Token 的 Logit 非常接近时,极小的数值扰动也可能改变首个 Token,之后的自回归生成会迅速分叉。
因此,推理可复现性不是一个布尔开关,而是一组需要明确约束的工程契约。
先定义可复现性的目标等级
上线前应先确定业务真正需要哪一类稳定性,避免为了”绝对一致”承担不必要的性能成本。
| 等级 | 定义 | 适用场景 |
|---|---|---|
| 语义级一致 | 输出文字可以不同,但结论、结构和关键事实一致 | 普通问答、创作、摘要 |
| Token 级一致 | 相同请求必须生成完全相同的 Token 序列 | 回归测试、自动评分、缓存键验证、审计复盘、RL Rollout |
| 数值容差一致 | 不强求 Token 一致,但每步 Logit、Top-K 排名或概率分布差异处于阈值内 | 定位 Kernel、量化或并行拓扑变化引起的数值漂移 |
| Bitwise 一致 | 输出张量逐位相同 | 仅在固定硬件、固定软件版本、固定执行路径和固定并行配置下讨论 |
输出漂移从哪里产生
1. 请求契约不完整
只记录 Prompt 和 Seed 不够。生产回放至少要冻结以下字段:
- 模型与权重制品指纹;
- Tokenizer、Chat Template 和特殊 Token 版本;
temperature、top_p、top_k、min_p、重复惩罚等采样参数;- Seed 的作用域,是每个请求独立 Seed,还是 Worker 全局随机状态;
- Logits Processor、停止词和最大输出长度;
- 推理后端、量化格式与 Attention Backend。
缺少任意一项,都可能让”同一个请求”在系统看来并不是同一个计算任务。
2. 动态批次改变执行形状
线上调度会把请求组合成不同批次。同一个请求可能与短请求、长请求或不同阶段的请求共同执行,导致矩阵形状、Padding、归约路径和 Kernel 选择变化。
vLLM 官方文档明确说明,默认配置以性能为优先,并不保证结果可复现。最新文档提供 Batch Invariance 能力,使输出不受批次大小和批次内请求顺序影响,但该功能仍处于 Beta 阶段,并可能带来性能损失。
3. CUDA Kernel 与浮点归约顺序变化
即使使用贪心解码,计算图中的 GEMM、Attention、All-Reduce 和融合算子也可能因为执行顺序不同产生数值偏差。
PyTorch 官方文档指出,完全可复现的结果不能保证跨 PyTorch 版本、提交、平台或 CPU/GPU 环境成立。torch.use_deterministic_algorithms(True) 可以强制使用已知的确定性实现,并在没有确定性实现时抛出错误,但通常会降低性能。
cuBLAS 对相同 Toolkit 版本、相同 GPU 架构和相同 SM 数量提供一定的逐位复现保证;但当多个 CUDA Stream 并发时,该保证可能失效。官方建议使用每个 Stream 独立 Workspace、独立 Handle,或设置 CUBLAS_WORKSPACE_CONFIG 等方式降低不确定性。
4. 并行拓扑和后端版本变化
Tensor Parallel 数量、Collective Reduction Tree、Custom All-Reduce、量化 Kernel 和 CUDA Graph 捕获路径都会影响计算顺序。
近期研究进一步指出,即使模型权重、硬件和解码参数保持不变,不同推理后端也可能产生显著的输出和评测差异。该结论来自特定模型与基准实验,不能直接外推到所有系统,但足以说明:推理后端本身应被视为评测和回归的版本化变量。
工程落地:建立确定性专用通道
不建议把全部线上流量强制切换到确定性模式。更合理的做法是保留两条执行通道:
- 高性能通道:服务普通生产请求,允许动态批次和高性能 Kernel;
- 确定性通道:服务评测、回归、审计、问题复盘和高风险请求,冻结调度、Kernel 和环境。
这样可以把性能成本限制在真正需要复现的流量上。
第一步:定义 Request Reproducibility Envelope
每个可回放请求都应保存一个不可变的执行信封:
{
"request_id": "req-20260714-001",
"model_artifact_sha256": "...",
"tokenizer_sha256": "...",
"chat_template_sha256": "...",
"prompt_token_ids": [128000, 2028, 374],
"sampling": {
"temperature": 0.7,
"top_p": 0.95,
"max_tokens": 256,
"seed": 42
},
"runtime": {
"engine": "vllm",
"engine_version": "pinned-version",
"torch_version": "pinned-version",
"cuda_version": "pinned-version",
"gpu_model": "pinned-model",
"tensor_parallel_size": 1,
"attention_backend": "pinned-backend",
"quantization": "none"
}
}
优先保存 Prompt Token IDs,不要只保存原始字符串。这样可以把 Tokenizer 和模板变化从推理数值问题中分离出来。
第二步:开启 Batch Invariance 或固定调度
根据 vLLM 最新文档,在线服务要获得不受批次影响的输出,需要开启 Batch Invariance:
VLLM_BATCH_INVARIANT=1 \
CUBLAS_WORKSPACE_CONFIG=:4096:8 \
vllm serve /models/your-model \
--tensor-parallel-size 1
调用时仍应显式传递 Seed:
from openai import OpenAI
client = OpenAI(
api_key="EMPTY",
base_url="http://127.0.0.1:8000/v1",
)
response = client.completions.create(
model="your-model",
prompt="Explain floating-point non-associativity.",
temperature=0.7,
top_p=0.95,
max_tokens=256,
seed=42,
)
print(response.choices[0].text)
注意:Batch Invariance 目前有硬件、模型覆盖和性能方面的边界。上线前必须在自己的模型、GPU、Tensor Parallel 配置和并发模式下验证,不能只因为环境变量成功设置就认为已经满足确定性要求。
第三步:冻结 Kernel 与并行配置
确定性通道应固定以下项目:
- GPU 型号、Compute Capability 和驱动版本;
- CUDA、cuBLAS、cuDNN、PyTorch 和推理引擎版本;
- Tensor Parallel、Pipeline Parallel 和 Data Parallel 数量;
- Attention Backend、All-Reduce 实现和 CUDA Graph 策略;
- 量化格式、Scale 文件和量化 Kernel;
- CUDA Stream、Workspace 和 Worker 数量。
对于自研 PyTorch 推理程序,可在测试通道启用:
import os
import random
import numpy as np
import torch
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
seed = 42
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.use_deterministic_algorithms(True, warn_only=False)
torch.backends.cudnn.benchmark = False
环境变量最好在 Python 进程启动前设置。确定性设置可能导致算子报错或性能下降,因此应在镜像构建和预发布阶段完成兼容性扫描。
第四步:建立多维回放矩阵
只把同一个请求连续调用十次,覆盖不了生产中的主要漂移来源。回放矩阵至少应包含:
| 维度 | 变化项 |
|---|---|
| 批次 | 单请求、固定小批次、高并发动态批次 |
| 顺序 | 不同请求到达顺序 |
| 输入 | 不同输入长度组合 |
| CUDA Graph | 开启 / 关闭 |
| Attention Backend | 不同后端切换 |
| Tensor Parallel | 不同 TP 配置 |
| 版本 | 旧版本与候选版本并行回放 |
每组请求至少记录:
- Token 序列是否完全一致;
- 首个分叉 Token 的位置;
- 分叉前一步的 Top-K Token 与 Logprob;
- 最大 Logit 绝对差和相对差;
- 最终文本的语义与业务指标;
- 吞吐、P95 延迟和显存变化。
首个分叉位置比”最终答案是否一样”更有诊断价值。分叉出现在第一个 Token,通常指向输入、模板或前向计算差异;分叉发生在长输出后段,则更可能是微小数值误差逐步累积。
第五步:把环境指纹纳入发布门禁
模型制品之外,还要为推理环境生成指纹。建议至少包含:
model_sha256
tokenizer_sha256
chat_template_sha256
engine_version
torch_version
cuda_version
cublas_version
gpu_name_and_compute_capability
attention_backend
quantization_method
tensor_parallel_size
batch_invariance_flag
sampling_contract_version
任何字段变化都应触发回放,而不是等线上出现”偶发不同答案”后再追查。
适用场景
推理可复现性治理特别适合以下场景:
- 模型、推理后端和 CUDA Kernel 升级回归;
- LLM 自动评测和基准复测;
- 强化学习 Rollout 与训练—推理一致性检查;
- 金融、保险、医疗等需要审计复盘的高风险流程;
- Agent 工作流失败重放;
- 缓存、幂等与数字签名验证;
- 多供应商或多后端结果差异定位。
普通聊天和创作场景通常不需要 Bitwise 一致,只需通过业务评测保障语义稳定。
常见误区
误区一:temperature=0 就一定确定。 temperature=0 通常意味着贪心选择,但 Logit 本身仍可能因批次和 Kernel 改变。两个 Token 分数接近时,微小误差仍会改变选择结果。
误区二:Seed 是完整的复现凭证。 Seed 只约束随机数状态。没有固定 Token IDs、后端版本、批次行为、并行拓扑和 Kernel,Seed 只能解决问题的一部分。
误区三:跨 GPU、跨版本也应该逐位一致。 PyTorch、cuBLAS 和 vLLM 官方文档都对复现范围设置了明确边界。跨版本、跨平台、跨硬件逐位一致不应作为默认承诺。
误区四:全量开启确定性模式最安全。 确定性实现可能降低吞吐,禁用部分优化,甚至不支持某些算子。生产系统应按业务等级提供确定性通道,而不是无差别牺牲性能。
误区五:只比较最终文本。 最终文本一致不代表数值稳定;最终文本不同也不一定代表业务质量退化。应同时检查 Token、首个分叉点、Logprob 和业务指标。
上线检查清单
- 明确定义语义级、Token 级、数值容差级或 Bitwise 级目标。
- 请求日志包含 Token IDs、完整采样参数和 Seed。
- 模型、Tokenizer、Chat Template 和推理环境均有不可变指纹。
- 确定性通道与普通高性能通道隔离。
- Batch Invariance 已在目标模型和硬件上验证。
- 多批次大小、多请求顺序和多并发场景已回放。
- Tensor Parallel、Attention Backend 和量化配置已纳入矩阵。
- 首个分叉 Token、Top-K Logprob 和最大 Logit 差可查询。
- 已量化确定性模式带来的吞吐、延迟和显存成本。
- 升级失败时可回滚到完整环境指纹对应的镜像和模型制品。
常见问题
能否只在评测时启用确定性,在正式服务中关闭?
可以,而且通常更合理。但要明确评测通道和生产通道存在执行差异,关键版本仍应在真实生产配置下补充影子回放,避免评测结果无法代表线上行为。
供应商 API 没有提供 Seed 或环境版本怎么办?
此时无法承诺 Token 级或 Bitwise 复现。可以保存请求、响应、模型别名、时间和供应商返回的版本标识,并采用多次采样、语义指标和业务断言做统计级回归。