4. 可复现性核心原则

在自动驾驶实验中,“可复现”不是一句口号,而是一组可以被工程化验证的约束条件。一个训练实验如果今天跑出 mAP 0.412、明天跑出 0.398,即使代码没改,也不能称为可复现。可复现性的本质,是把实验中所有会影响结果的“自由度”逐一收敛:让计算过程确定、让代码与数据版本可追溯、让运行环境可重建、让随机性可控制、让依赖可锁定,最后用等级划分来度量复现的强度。

可复现性 = 确定性计算 + 版本可追溯 + 环境可重建 + 随机性可控 + 依赖可锁定。五者缺一,复现强度就会下降一个等级。

一、核心原理:把“不确定性”逐层剥离

自动驾驶训练链路长、组件多,不确定性来源可分为五层。第一层是算法随机性:权重初始化、数据打乱、数据增强、Dropout、采样策略。第二层是计算非确定性:GPU 并行归约顺序、cuDNN 自动算法选择、原子加操作、多线程调度。第三层是环境漂移:CUDA、驱动、PyTorch、Python 版本变化,甚至同一版本在不同机器上的编译差异。第四层是数据漂移:数据集被追加、清洗、重新标注,但版本号未变。第五层是代码漂移:未提交的本地修改、分支切换、依赖隐式升级。

可复现性工程就是针对这五层分别建立控制手段:随机种子控制第一层,确定性开关控制第二层,环境隔离控制第三层,数据与代码版本控制控制第四、五层,依赖锁定则贯穿第三、五层。下图给出五层不确定性与五类控制手段的对应关系。

可复现性五层不确定性 → 五类控制手段 不确定性来源 ① 算法随机性 初始化 / 打乱 / 增强 / Dropout ② 计算非确定性 GPU 归约 / cuDNN / 原子加 ③ 环境漂移 CUDA / 驱动 / 框架版本 ④ 数据漂移 追加 / 清洗 / 重标注 ⑤ 代码漂移 未提交修改 / 分支切换 控制手段 随机种子固定 seed / 各库种子 / 数据顺序 确定性计算开关 deterministic / benchmark=False 环境隔离 容器 / 镜像 / 虚拟环境 数据与代码版本控制 Git commit / 数据集指纹 依赖锁定 lock 文件 / 哈希校验 可复现性等级 L1 可重跑 同机同环境,结果近似 L2 可复现 种子+确定性,指标一致 L3 可重建 环境+依赖锁定,跨机一致 L4 可审计 全链路版本+数据指纹 L5 可迁移复现 跨硬件/跨集群,误差有界

二、方法流程:从种子到锁定的落地顺序

落地时建议按“先固定随机性,再固定计算,再固定环境,最后固定版本”的顺序推进,因为前面的问题会掩盖后面的问题。典型流程如下:

  1. 统一入口设置种子:在训练脚本最开头设置 Python、NumPy、PyTorch、CUDA 的种子,并记录到实验元数据。
  2. 开启确定性计算:设置 torch.use_deterministic_algorithms(True),关闭 cudnn.benchmark,必要时设置 CUBLAS_WORKSPACE_CONFIG。
  3. 固定数据顺序与增强:DataLoader 的 shuffle 使用带种子的 generator,数据增强使用可复现的随机源。
  4. 环境隔离:用容器镜像或 conda 环境固化 Python、CUDA、驱动、框架版本。
  5. 依赖锁定:生成 lock 文件并记录哈希,禁止运行时隐式升级。
  6. 版本记录:记录 Git commit、数据集指纹、配置哈希,写入实验元数据。
# 训练入口的种子与确定性设置示例
import os, random, numpy as np, torch

SEED = 20240517
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed_all(SEED)

os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True

g = torch.Generator()
g.manual_seed(SEED)
# DataLoader(..., shuffle=True, generator=g)

三、案例/场景:一次“指标漂移”的复盘

某团队在 BEV 检测模型上遇到问题:同一份代码、同一份数据,两次训练 mAP 相差 0.9 个点。排查发现三个原因叠加:一是 DataLoader 的 shuffle 未使用固定 generator,导致每个 epoch 数据顺序不同;二是 cudnn.benchmark=True 让卷积算法随输入尺寸自动选择,不同 batch 组合下选择了不同算法;三是数据增强中的随机裁剪使用了全局随机源,受前序代码调用次数影响。修复后,两次训练指标完全一致,复现等级从 L1 提升到 L2。

排查随机性问题的实用技巧:固定种子后连续跑两次短训练(如 200 step),对比 loss 曲线。若前 10 step 就分叉,通常是初始化或数据顺序问题;若 100 step 后缓慢分叉,通常是 GPU 非确定性归约或 cuDNN 算法选择问题。

四、可复现性等级划分与检查清单

为了团队协作和验收,建议把可复现性分为五级,每级有明确的判定标准。等级不是越高越好,而是要与实验目的匹配:日常调参 L2 足够,论文复现和量产交付至少 L4。

等级 名称 判定标准 适用场景
L1 可重跑 同机同环境可再次运行,指标近似但不保证一致 快速探索、临时验证
L2 可复现 固定种子与确定性开关后,同机指标完全一致 消融实验、调参对比
L3 可重建 环境与依赖锁定,换机器可重建并得到一致结果 团队协作、CI 回归
L4 可审计 代码、数据、配置、环境全链路版本可追溯 论文复现、量产交付
L5 可迁移复现 跨硬件/跨集群复现,误差在预设容差内 多平台部署、异构训练
常见误区:只固定 torch.manual_seed 就认为可复现。实际上 NumPy、Python random、CUDA、DataLoader worker、数据增强库都可能有独立随机源;此外,多卡训练中的梯度归约顺序、混合精度缩放、分布式采样器也会引入非确定性。必须逐项检查,而不是只设一个种子。

最后给出一个可落地的检查清单:种子是否覆盖所有随机源;确定性算法是否开启;cuDNN benchmark 是否关闭;DataLoader 是否使用固定 generator;环境是否容器化;依赖是否有 lock 文件;Git commit 与数据集指纹是否写入实验元数据;是否定义了可接受的误差容差。把这八项做成实验启动前的自动检查,可复现性才能从“靠人记得”变成“靠系统保证”。