文章

LLM 推理可复现性生产实战:用 Seed 契约、Batch Invariance 与确定性 Kernel 控制输出漂移

同一模型、同一提示词和相同随机种子仍可能产生不同输出。本文从采样状态、动态批次、浮点归约、并行拓扑和版本指纹出发,给出可复现推理的分级目标、实现方案、回放门禁与上线检查清单。

背景问题:相同 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 版本;
  • temperaturetop_ptop_kmin_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 复现。可以保存请求、响应、模型别名、时间和供应商返回的版本标识,并采用多次采样、语义指标和业务断言做统计级回归。

参考资料

  1. vLLM Reproducibility
  2. vLLM Batch Invariance
  3. PyTorch Reproducibility
  4. NVIDIA cuBLAS Results Reproducibility
  5. The Silent Hyperparameter: Quantifying the Impact of Inference Backends on LLM Reproducibility
  6. Deterministic Inference across Tensor Parallel Sizes That Eliminates Training-Inference Mismatch

常见问题

temperature 设置为 0,是否就能保证输出完全一致?
不能。贪心解码仍可能因动态批次、并行归约顺序、Kernel 选择和低精度数值误差导致临界 Logit 排名变化,从而改变首个 Token 的选择。
设置相同 seed 后,为什么线上请求仍可能出现不同结果?
Seed 只约束随机数序列,不能固定请求进入的批次、浮点归约顺序、并行拓扑、推理后端版本和 Kernel 实现。这些因素的任何变化都可能引入数值漂移。
生产环境是否应该为全部请求开启确定性模式?
通常不应。确定性模式可能牺牲吞吐、延迟和可用优化,更适合评测、回归、审计、强化学习 Rollout 和高风险业务的专用通道。建议按业务等级提供双通道:高性能通道服务普通请求,确定性通道服务关键流量。
为什么同一个请求在低并发时稳定,高并发时却偶尔不同?
高并发会改变请求组合、批次大小、执行形状和归约路径。若推理引擎没有提供 Batch Invariance,相同请求可能因同批次的其他请求不同而出现数值漂移。