可复现性 ¶
在 PyTorch 版本、单个提交或不同平台之间,完全可复现的结果不能保证。此外,即使在使用相同的随机种子的情况下,CPU 和 GPU 执行的结果也可能不可复现。
然而,您可以采取一些步骤来限制特定平台、设备和 PyTorch 版本中非确定性行为的来源数量。首先,您可以控制可能导致您的应用程序多次执行行为不同的随机性来源。其次,您可以配置 PyTorch 避免在某些操作中使用非确定性算法,这样在给定相同输入的情况下,多次调用这些操作将产生相同的结果。
警告
确定性操作通常比非确定性操作慢,因此模型的单次运行性能可能会降低。然而,确定性可能通过促进实验、调试和回归测试来节省开发时间。
控制随机性的来源
PyTorch 随机数生成器
您可以使用 torch.manual_seed()
为所有设备(CPU 和 CUDA)设置 RNG 的种子:
import torch
torch.manual_seed(0)
一些 PyTorch 操作可能内部使用随机数。例如, torch.svd_lowrank()
就是这样做的。因此,如果连续多次使用相同的输入参数调用它,可能会得到不同的结果。然而,只要在应用程序开始时将 torch.manual_seed()
设置为常数,并且消除了所有其他非确定性来源,每次在相同的环境中运行应用程序时,都会生成相同的随机数序列。
通过在后续调用之间将 torch.manual_seed()
设置为相同的值,也可以从使用随机数的操作中获得相同的结果。
Python¶
对于自定义操作符,可能还需要设置 Python 种子:
import random
random.seed(0)
其他库中的随机数生成器
如果你或你使用的任何库依赖于 NumPy,你可以使用以下方式对全局 NumPy RNG 进行初始化种子:
import numpy as np
np.random.seed(0)
然而,某些应用程序和库可能使用 NumPy 随机生成器对象,而不是全局 RNG(https://numpy.org/doc/stable/reference/random/generator.html),这些也需要一致地进行初始化种子。
如果你使用任何其他使用随机数生成器的库,请参考这些库的文档以了解如何为它们设置一致的种子。
CUDA 卷积基准测试
cuDNN 库,用于 CUDA 卷积操作,可能是应用程序多次执行中的非确定性的来源。当使用一组新的尺寸参数调用 cuDNN 卷积时,一个可选功能可以运行多个卷积算法,对它们进行基准测试以找到最快的算法。然后,在后续过程中,将始终使用最快的算法来处理相应的尺寸参数集。由于基准测试噪声和不同硬件,基准测试可能在后续运行中选择不同的算法,即使在同一台机器上也是如此。
使用 torch.backends.cudnn.benchmark = False
禁用基准测试功能会导致 cuDNN 确定性地选择一个算法,这可能会以降低性能为代价。
然而,如果您不需要在应用程序多次执行之间保持可重复性,那么如果启用基准测试功能 torch.backends.cudnn.benchmark = True
,性能可能会提高。
注意此设置与下面讨论的 torch.backends.cudnn.deterministic
设置不同。
避免非确定性算法
torch.use_deterministic_algorithms()
允许您配置 PyTorch 在可能的情况下使用确定性算法而不是非确定性算法,并在已知操作是非确定性的(且没有确定性替代方案)时抛出错误。
请查阅 torch.use_deterministic_algorithms()
的文档以获取受影响操作的全列表。如果操作不符合文档描述,或者需要某个没有确定性实现的操作的确定性实现,请提交问题:https://github.com/pytorch/pytorch/issues?q=label:%22module:%20determinism%22
例如,运行 torch.Tensor.index_add_()
的非确定性 CUDA 实现将抛出错误:
>>> import torch
>>> torch.use_deterministic_algorithms(True)
>>> torch.randn(2, 2).cuda().index_add_(0, torch.tensor([0, 1]), torch.randn(2, 2))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: index_add_cuda_ does not have a deterministic implementation, but you set
'torch.use_deterministic_algorithms(True)'. ...
当 torch.bmm()
使用稀疏-稠密 CUDA 张量时,通常使用非确定性算法,但打开确定性标志后,将使用其备选的确定性实现:
>>> import torch
>>> torch.use_deterministic_algorithms(True)
>>> torch.bmm(torch.randn(2, 2, 2).to_sparse().cuda(), torch.randn(2, 2, 2).cuda())
tensor([[[ 1.1900, -2.3409],
[ 0.4796, 0.8003]],
[[ 0.1509, 1.8027],
[ 0.0333, -1.1444]]], device='cuda:0')
此外,如果您正在使用 CUDA 张量,并且您的 CUDA 版本为 10.2 或更高版本,您应根据 CUDA 文档设置环境变量 CUBLAS_WORKSPACE_CONFIG:https://docs.nvidia.com/cuda/cublas/index.html#results-reproducibility
CUDA 卷积确定性
禁用 CUDA 卷积基准测试(如上所述)可以确保 CUDA 每次运行应用程序时都选择相同的算法,但该算法本身可能是非确定性的,除非设置了 torch.use_deterministic_algorithms(True)
或 torch.backends.cudnn.deterministic = True
。后者仅控制这种行为,而 torch.use_deterministic_algorithms()
将使其他 PyTorch 操作也表现出确定性。
CUDA RNN 和 LSTM
在某些版本的 CUDA 中,RNN 和 LSTM 网络可能具有非确定性行为。有关详细信息和工作程序,请参阅 torch.nn.RNN()
和 torch.nn.LSTM()
。
填充未初始化的内存
操作符 torch.empty()
和 torch.Tensor.resize_()
可能会返回包含未初始化内存的 tensor,这些内存中的值是未定义的。如果需要确定性,将此类 tensor 用作另一个操作的输入是无效的,因为输出将是非确定性的。但实际上并没有什么可以阻止这种无效代码的运行。因此,为了安全起见,默认将 torch.utils.deterministic.fill_uninitialized_memory
设置为 True
,如果设置了 torch.use_deterministic_algorithms(True)
,它将用已知值填充未初始化的内存。这将防止这种类型的非确定性行为的发生。
然而,填充未初始化的内存会对性能产生不利影响。所以如果你的程序是有效的,并且没有将未初始化的内存用作操作的输入,那么可以关闭此设置以获得更好的性能。
DataLoader¶
DataLoader 将在多进程数据加载算法中重新播种工作进程的随机性。使用 worker_init_fn()
和生成器以保持可重现性:
def seed_worker(worker_id):
worker_seed = torch.initial_seed() % 2**32
numpy.random.seed(worker_seed)
random.seed(worker_seed)
g = torch.Generator()
g.manual_seed(0)
DataLoader(
train_dataset,
batch_size=batch_size,
num_workers=num_workers,
worker_init_fn=seed_worker,
generator=g,
)