资讯详情

PyTorch随机种子 torch.manual_seed 详解:彻底搞定训练可复现性

📅 2026/10/20 5:09:45 | 华诺云谱 👁 阅读
PyTorch随机种子 torch.manual_seed 详解:彻底搞定训练可复现性
我见过很多训练脚本开头必然躺着三行代码random.seed、np.random.seed、torch.manual_seed有的还会补一行 torch.cuda.manual_seed_all。问作者为什么这么写答案多半是“大家都这么写”。真正遇到过一次“同一个脚本两次跑出来的结果对不上”的人才知道这几行代码的分量。这篇专门聊 torch.manual_seed() 这个函数它管什么、不管什么、怎么用才算是真正把随机源锁住。搞懂这一行后面再遇到复现类的坑基本都能自己排查。适合被“结果复现不了”折磨过的同学、准备写实验对比报告的人、以及想把训练代码整理出来分享的开发者。1. 为什么训练脚本里总躺着这一行代码1.1 随机性到底藏在哪训练一个模型从参数初始化开始就是随机的。PyTorch 里的 nn.Linear、nn.Conv2d 在创建时会用均匀分布或正态分布初始化权重Dropout 要按概率随机丢弃神经元DataLoader 每个 epoch 要打乱样本顺序优化器本身虽然确定但 batch 顺序一变梯度累积路径就全变了。这些随机性来自同一个底层机制——伪随机数生成器PRNG。PyTorch 在每个设备CPU、GPU上维护自己的 RNG 状态torch.manual_seed() 就是往这个状态里写入一个确定的种子让后续的随机序列从同一起点开始。种子相同生成的随机数序列就相同这是“可复现”的数学基础。理解了这一层你就不难明白所谓“锁种子”本质上是在操纵一串确定性的伪随机数序列而不是引入什么玄学。很多新手有个误解以为种子设了之后“整个训练就固定了”。实际上随机源有多个torch.manual_seed() 只是其中最核心的那个。Python 内置的 random 模块管数据加载和部分默认的 shufflenumpy 管数据预处理里各种各样的噪声与增强它们各自维护独立的随机状态。所以完整的种子方案必然是多个随机库一起设置这一点后面详细展开。1.2 结果对不上的痛只有经历过才懂某同学跑一个实验昨天 loss 曲线收敛到 0.32今天重跑变成 0.35他的第一反应是“我改了什么超参”。查了半天什么都没改那就是随机性在捣鬼。这种事情在对比实验里尤其致命你想验证“加了这个模块到底有没有用”如果两次实验的基线都不一样对比就没有意义写论文要提交实验数据审稿人问“你的实验可以复现吗”回答含糊是要出事的。另外就是调 bug。某个 batch 出现 NaN你想复现现场逐行调试结果每次都随机到不同的 batch根本定位不了。设了种子从头到尾走的路径是一样的问题就好查多了。我还有一次印象很深的经历模型训练到第 200 个 epoch 时偶发崩溃不设种子根本抓不住现场后来固定种子后连续跑三次都在同一个地方崩顺着代码一找果然是某个采样逻辑在边界条件下越界了。所以说 torch.manual_seed() 不是给代码“加点仪式感”它是把实验环境里最不可控的那部分变量给摁住让问题暴露得更稳定让对比变得更有说服力。2. torch.manual_seed() 到底做了什么2.1 全局 RNG 与设备 RNGPyTorch 的随机机制要区分两个概念全局 RNG 和设备 RNG。CPU 上只有一个全局 RNG调用 torch.manual_seed(42) 会同时设置 CPU 的全局 RNG。CUDA 上每个 GPU 设备也有自己的 RNGtorch.manual_seed(42) 在默认情况下也会设置当前设备的 RNG这一点很多人不知道——它其实等价于同时设置了 CPU 和当前 CUDA 设备。也就是说单卡训练时只调 torch.manual_seed() 就已经覆盖了模型权重初始化的随机源因为当前设备就是 cuda:0。但这里有个坑它只设置“当前选中”的设备。如果你有多张 GPU或者代码里显式用 torch.device(cuda:1) 切到了其他设备只有 cuda:0 的 RNG 被更新了cuda:1 还是乱的。这种情况就得用 torch.cuda.manual_seed_all(42)它会把所有可见 GPU 的 RNG 都设置一遍。在日常训练脚本里我习惯把它们封装成一个函数放工程公共模块def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)这个函数是我所有训练任务里的标配。三个库的随机源分开管Python 内置 random 管 shuffle 等操作numpy 管数据预处理里可能出现的噪声、增强torch 管模型初始化和张量运算里采样的随机数。只设 torch 不设另外两个严格来说复现是不完整的。2.2 manual_seed 和 manual_seed_all 的边界torch.manual_seed() 负责 CPU 和当前设备torch.cuda.manual_seed() 只负责指定设备torch.cuda.manual_seed_all() 负责所有可见设备。三者之间的关系我用几个场景说明。单卡训练torch.manual_seed(42) 就够了因为 CPU 和当前 CUDA 设备都覆盖了。很多人还是会顺手加一行 torch.cuda.manual_seed_all(42)没什么坏处多一层保险代码可读性也更好——读者一眼就知道 CUDA 设备也被管住了。多卡数据并行DataParallel 或单机多卡必须用 torch.cuda.manual_seed_all(42)。否则每张卡的初始化参数来自不同的随机序列模型参数在每个卡上就不一致。虽然同步梯度的过程中会收敛但第一次前向传播就埋下了不确定性复现的难度陡增。分布式训练DistributedDataParallel每个进程自己调一遍 set_seed()而且通常配合启动时传入相同的种子。要注意的是分布式场景下 DataLoader 的每个 worker 和每个进程还需要额外处理这部分我放到第 3 节细说。2.3 种子数值本身不影响模型好坏很多人纠结“seed 到底该设成 42 还是 2024”。答案很简单种子数值本身不会改变模型的能力上限它只是决定随机序列的起点。不同种子跑出来的最终指标会有波动那是因为随机初始化不同导致收敛到不同的局部最优不是因为某个种子“更好”。一个常见的误区是调参时发现 seed42 效果特别好就觉得找到了“神种”后面所有实验都用它。真相往往是这个种子碰巧让你的模型逃过了某个不利的初始化区域。在对比实验里严谨的做法是同一个配置跑 3 到 5 个不同的种子报告均值加减方差而不是守着某一个种子报最好结果。如果一个结论只在特定种子下成立那它本质上是个不稳定的结论。3. 实操搭一套真正可复现的训练环境3.1 基础三件套与 PYTHONHASHSEED前面说过训练过程涉及 Python random、numpy、torch 三套随机源。它们各自独立维护状态互不同步所以必须分别 seed。只调 torch.manual_seed() 而忘记 np.random.seed()数据增强里的 numpy 随机操作就还是乱的一旦数据增强引入了随机翻转、随机裁剪结果照样对不上。我写的 set_seed 比基础三件套多一点内容增加了哈希随机化的处理import os import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) os.environ[PYTHONHASHSEED] str(seed)最后一行 PYTHONHASHSEED 是给 Python 的哈希随机化用的。Python 解释器默认对字符串启用随机哈希某些场景下会影响 set 和 dict 的迭代顺序间接影响喂给模型的数据顺序也会影响某些自定义采样器的行为。不过要说明一点这个环境变量必须在解释器启动前设置才对当前进程生效写在 set_seed 里其实影响的是后续启动的子进程比如 DataLoader 的 worker。更规范的做法是在运行脚本的命令行里加或者写进启动脚本统一管理export PYTHONHASHSEED42 python train.py --seed 42把这个和命令行参数结合起来每个实验的种子都留痕后面追溯起来很省事。3.2 DataLoader 的 worker 也必须管住torch.manual_seed() 管住了主进程的 RNG但 DataLoader 开启多进程加载数据时每个 worker 是独立的进程它们默认从各自的随机状态开始不受主进程种子直接控制。PyTorch 其实做了个隐式约定每个 worker 进程会用“基础种子加 worker_id”的方式生成自己的种子基础种子来自主进程的 RNG。所以只要主进程设了 torch.manual_seed()DataLoader 的 worker 也基本可复现前提是你用的是 PyTorch 默认的 RandomSampler。但这里有个很隐蔽的坑如果你的训练循环里自己也用了随机数比如 Mixup 的 lambda 采样、或者自定义采样器依赖了主进程的随机状态那么主进程 RNG 的消耗顺序就会影响 DataLoader 的 shuffle 结果。最稳妥的做法是给 DataLoader 单独指定一个 Generator把数据加载的随机性独立出来g torch.Generator() g.manual_seed(0) train_loader DataLoader( dataset, batch_size32, shuffleTrue, num_workers4, generatorg, )这样 shuffle 和每个 worker 的采样随机数都从 g 里取与你训练主流程里的 RNG 互不干扰。只要你不在中途重新 seed 这个 Generator每个 epoch 的打乱顺序就完全一致。这个习惯我强烈建议养成它能避免掉大量“莫名其妙复现不了”的问题。顺带提一个细节shuffleTrue 时每个 epoch 会重新打乱但如果你希望每个 epoch 用同一份样本顺序反而不能只设一次种子要靠保存和恢复 Generator 的状态来控制。这个技巧在特定实验设计里偶尔用到。3.3 cuDNN 的两个开关设了种子、管好了 DataLoaderGPU 上跑两次还是会有一点点不同。大部分时候是 cuDNN 在捣乱。PyTorch 在 GPU 上执行卷积等算子时会调用 GPU 厂商提供的加速库 cuDNN。cuDNN 内部有各种算法实现PyTorch 默认用 benchmark 模式也就是先跑几个算法试试速度选最快的那个。问题在于“试算法”这个过程本身有随机性它来自 GPU 上并行归约的执行顺序不同次运行可能选出不同算法结果自然有微小的数值差异。标准解法是设置两个后端开关torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark FalsedeterministicTrue 强制 cuDNN 选择确定性算法benchmarkFalse 关闭自动调优。代价是某些模型的卷积运算会慢一点需要你自己权衡速度与可复现性。注意deterministic 不是万能的。有些算子比如某些 attention 实现、sparse 操作在 CUDA 上本身就没有确定性实现PyTorch 会直接报错或者警告。如果你看到“Deterministic mode was enabled but...”之类的提示说明代码里用了不支持确定性模式的算子。另外某些第三方算子库根本不响应这两个开关这种情况下只能换实现或者接受必要的数值误差。3.4 多卡与分布式环境的种子策略多卡场景的关键是每张卡的 RNG 要独立但同源。早年的做法是在每个进程里调一遍 set_seed然后用不同的种子偏移来制造卡间差异比如 rank 为 i 的进程用 seed 加 i。现在更推荐反过来所有进程用同一个种子模型初始化保持一致数据划分交给 DistributedSampler 处理。用 PyTorch 官方推荐的写法seed 42 torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) train_sampler torch.utils.data.distributed.DistributedSampler( dataset, num_replicasworld_size, rankrank, )DistributedSampler 内部会根据 seed 加 epoch 计算当前进程拿到的数据切片这样每个 epoch 不同进程之间的数据不重叠同时整体是确定的。需要注意分布式场景下每个进程都要调用 set_seed而且这个调用必须在进程初始化之后、任何随机操作之前完成。多卡还有个现实问题不同 GPU 的型号、驱动与加速库版本不同即使种子一致也没法保证结果完全一致。做可复现实验的默认前提是同一套软硬件环境这点要心里有数。跨卡复现本身就是个伪需求你能做到的是“同一环境下多次运行一致”而不是“任何显卡上都严格一致”。4. 常见问题与排查实录4.1 GPU 上结果还是不固定这是被问得最多的情况CPU 上设了种子能复现换到 GPU 就跑两次不一样。排查顺序按这几步来。第一步确认是否设置了 torch.cuda.manual_seed_all(seed)以及是否设置 cudnn.deterministicTrue 和 cudnn.benchmarkFalse。八成是这一步没做。第二步检查代码里有没有在训练过程中重新调用随机数生成的地方比如某些采样策略、Mixup、CutMix 这类数据增强里的随机逻辑它们可能用了独立的 Generator 但没固定种子。第三步看是否用了不支持确定性模式的算子认真看日志里的 warning别忽略。第四步检查 DataLoader 的 shuffle 是否在 epoch 内被其他操作改变。还有一类比较隐蔽模型里存在 in-place 操作或者依赖未定义的执行顺序比如并行计算 batch 内各样本时浮点加法顺序不一致也会造成微小差异。这类差异通常出现在小数点后第五、六位一般不影响最终指标但如果你追求严格复现就得从算子层面逐个排查了。4.2 每次 DataLoader 顺序都不一样确认了主进程设了 torch.manual_seed()但 shuffle 完的数据顺序还是每次不同大概率是你用了自定义 Sampler或者自定义 Dataset 里面自带随机逻辑。比如 Dataset 的getitem里做了随机裁剪用的随机数是 Python random 或 numpy但没在主进程里 seed 它们。DataLoader 的 worker 进程是 fork 出来的能不能继承主进程的随机状态取决于操作系统和 worker 启动方式。在 Linux 下 fork 会继承随机状态Windows 下 spawn 不会这就是为什么同一套代码换到 Windows 上种子失效的现象比比皆是。最省心的做法是Dataset 里的随机操作不要直接用全局 random 或 numpy而是用一个或多个专属的 Generator 传进去。或者在getitem里通过 torch.initial_seed() 派生局部种子。前者实现简单可读性好推荐优先用。另外还要注意自定义 Sampler 里的随机打乱逻辑必须使用固定种子的 Generator否则 DataLoader 层面就不可控了。4.3 loss 曲线始终有微小抖动充分设置了种子、deterministic 也开了两次训练 loss 曲线还是偶发差个 1e-6。这种属于浮点非确定性的正常范围。显卡驱动的调度、GPU 上的原子操作顺序、多核 CPU 的并行归约都会引入这种级别的差异。强行追求全比特一致除了折磨自己没什么意义。工业界的标准做法是以“多次运行均值加减方差”作为实验结论而不是依赖单次运行结果。像前面说的跑 3 到 5 个种子求平均报告的标准差自然包含了一部分环境抖动结论更可信。在论文可复现性评审里这种级别的差异完全在可接受范围内审稿人关心的是趋势和结论稳定不是小数点后六位的分毫之差。4.4 排查思路速查症状可能原因解决方向CPU 上结果不同三套随机源只 seed 了一部分补上 random.seed、np.random.seedGPU 上结果不同没关 benchmark、没开 deterministic设置两个 cudnn 后端开关DataLoader 顺序不同worker 进程未继承种子或全局 RNG 被污染用独立 Generator 传给 DataLoader多卡结果不同只用 manual_seed 未覆盖所有设备改用 manual_seed_all训练中途变化代码里重新调用了 seed全局搜索 manual_seed 调用点极小浮点抖动并行归约顺序非确定接受用多次均值评估这张表我贴在了工位旁边每次有人问我复现问题先让他对着表自查一遍大部分问题都能在十分钟内定位。5. 几个值得记住的小技巧5.1 保存和恢复 RNG 状态有些场景下你希望模型训练到第 100 个 epoch 时暂停保存状态之后能断点续跑并且后续的随机序列跟不中断时一致。光保存模型权重是不够的随机状态也得存。torch 里用 get_rng_state 和 set_rng_state 来操作# 保存 rng_state torch.get_rng_state() cuda_rng_state torch.cuda.get_rng_state_all() # 恢复 torch.set_rng_state(rng_state) torch.cuda.set_rng_state_all(cuda_rng_state)numpy 和 Python random 的对应方法分别是 np.random.get_state()、np.random.set_state() 和 random.getstate()、random.setstate()。完整实现断点续训时把这几份状态都序列化到 checkpoint 里恢复的时候再写回去随机序列就能无缝衔接。实操中还有个省事的思路如果不需要精确续接随机流恢复时重新用同一个 seed 调一遍 set_seed 就够了。虽然随机序列会从头开始但至少每次续跑后起点相同结果也基本可对比。业务上多数时候不需要更严格的续接这个方案性价比最高。5.2 什么时候别追求绝对可复现做超参搜索的时候我强烈建议别开 deterministic。benchmarkFalse 会让卷积慢不少超参搜索本来就要跑几百组实验每一组都慢一点累积起来时间成本非常高。这个阶段要的是快速评估相对效果不是产出最终指标。另外使用 AutoAugment、RandAugment 这类随机性很强的增强策略时严格复现的意义也不大。因为增强空间本身就很大模型不可能对每一条增强路径都过拟合。该锁的种子锁上但别指望两次训练得到一模一样的曲线。可复现性是一种实验工具不是目的。理解了这句话就知道什么时候该较真、什么时候该放手。5.3 给种子做版本管理我见过一个挺常见的翻车现场实验结果记录表里只写了“seed42”但代码已经改过三轮了。同一个种子、同一份数据因为代码不同结果根本没法比。种子脱离了代码版本和数据版本就是一堆毫无意义的数字。建议把 seed 和代码版本、数据版本绑在一起记录。轻量做法是在实验日志里同时写入版本标识和 seed 值更完整的做法是把训练配置包含 seed序列化成一个字典随 checkpoint 一起存档。这样回溯时能精确还原曾经跑出某个结果的环境。种子本身也可以参与实验设计。要验证模型稳定性可以用 seed 0 到 9 跑十次把结果存成表格。要复现某个特定结果只需查表找到对应 seed 和代码版本一键重跑。这套流程搭好之后实验管理的效率会有质的提升。我个人用了很久才总结出一个习惯seed 设置不是训练代码里的一行装饰而是实验设计的一部分。写新项目的第一件事不是搭模型结构而是先把 set_seed、cudnn 开关、DataLoader 的 generator 一次性全部配好。前期花十分钟写稳后面省下的是无数个“为什么跑不对”的夜晚。这些配置最好固化到一个公共模块里所有实验统一调用别每个脚本各写一份否则版本一多照样乱。如果你刚接触这个词从今天起就把这个公共函数存进自己的代码仓库所有新项目都从它开始。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑