为什么固定了Seed,结果依然无法复现
复现性的第一步通常是设置随机种子,比如在PyTorch中调用torch.manual_seed(42),在NumPy中调用np.random.seed(42)。很多人以为只要Seed相同,随机数序列就完全一致。这个假设在同一个环境、同一个版本下基本成立,但一旦底层模型或框架发生了更新,这个假设就会被悄悄打破。

原因在于,随机种子的作用只是初始化伪随机数生成器的内部状态,后续生成的数字序列完全取决于生成器算法本身的实现。如果模型更新后,框架的随机数实现方式发生了变化,比如从MT19937换成了Philox算法,或者采样接口内部调整了调用随机数的次数和方式,那么即使Seed相同,实际产生的序列也完全不同。换句话说,Seed锁定的是起点,而模型更新改变的是整个行进路线。
除了随机数生成器本身,模型结构的变化也会放大随机性的影响。比如新版模型把某个全连接层拆成了两个更小的层,参数初始化时消耗随机数的数量就变了,后续所有依赖该生成器的采样行为全部错位。这种连锁反应经常让使用者误以为自己的代码出了问题,实际上是版本演化带来的必然结果。
模型更新引入随机性变化的三个典型来源
第一个来源是参数初始化逻辑的变化。模型作者可能在更新中替换了初始化方法,例如从正态分布初始化改为Kaiming初始化,或者调整了初始化的维度顺序。即使随机数序列完全相同,作用到不同的初始化公式上,得到的权重也不同,最终训练轨迹自然无法对齐。
第二个来源是算子实现和并行调度。在GPU上,很多操作(如浮点加法的归约)的执行顺序取决于线程调度,结果存在非确定性。框架更新后,某个算子的CUDA实现被重写,或者内核融合策略改变,浮点误差累积方式随之变化。这种差异虽然单步看极其微小,但经过成千上万步迭代会被不断放大,最终导致loss曲线出现肉眼可见的偏离。
第三个来源是采样和 dropout 的实现细节。比如某些生成式模型在更新后改变了噪声的采样方式,或者调整了dropout在计算图中的位置,导致相同随机数被用在不同的地方。以Stable Diffusion类模型为例,社区版本迭代频繁,采样器、文本编码器、VAE任何一个组件升级,都可能让同一个Seed生成的图片完全不同,这也是AI绘图社区中经典Seed复现难题的根源。
import torch
import numpy as np
import random
def set_seed(seed):
# 固定Python、NumPy、PyTorch三个层面的种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
# 开启确定性模式(会牺牲部分性能)
set_seed(42)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# 如需更严格的确定性,可使用如下接口
# torch.use_deterministic_algorithms(True)如何彻底解决模型更新带来的复现失败
最直接也最可靠的办法是锁定整个依赖环境。把框架版本、CUDA版本、cuDNN版本、模型权重的哈希值全部记录下来,用requirements.txt锁定pip包版本,用conda环境或Docker镜像固化系统依赖。复现实验时严格按照记录的环境重建,而不是只记录一个Seed。经验法则是:Seed只能保证同环境下的可复现,环境快照才能保证跨时间、跨机器的可复现。
其次是开启框架提供的确定性开关。PyTorch中设置torch.backends.cudnn.deterministic = True并关闭benchmark模式,可以让cuDNN选择确定性实现;调用torch.use_deterministic_algorithms(True)可以进一步强制所有算子使用确定性版本,遇到不支持确定性的算子会直接报错提示。需要注意的是,这些开关通常带来10%到30%的性能损失,建议在验证复现性时开启,日常训练时按需取舍。
对于使用第三方托管模型的场景,应当显式固定模型版本号或下载指定revision的权重,避免总是拉取最新版。同时建议在代码中对关键中间结果做校验,比如记录初始化后第一层权重的前几个值、第一步训练后的loss,一旦环境漂移就能快速发现,而不是等到几千步之后才发现结果对不上。
from diffusers import StableDiffusionPipeline
import torch
# 固定模型revision,避免自动更新到新版本
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
revision="fp16", # 锁定权重版本
torch_dtype=torch.float16
)
pipe = pipe.to("cuda")
generator = torch.Generator("cuda").manual_seed(42)
image = pipe("a cat on the moon", generator=generator).images[0]
# 显式传入generator对象,比全局seed更可控最后要建立正确的复现性观念:可复现是一个工程问题而非单纯的随机数问题。一个完整的复现记录应当包含代码commit哈希、依赖清单、模型版本、硬件信息、随机Seed、确定性开关状态这几项。把这些信息与实验结果一起存档,即使未来模型继续更新,你也能随时回到当时的环境重新验证结论,这才是科学实验管理的正确姿势。