DistributedDataParallel#
- class torch.nn.parallel.DistributedDataParallel(module, device_ids=None, output_device=None, dim=0, broadcast_buffers=True, init_sync=True, process_group=None, bucket_cap_mb=None, find_unused_parameters=False, check_reduction=False, gradient_as_bucket_view=False, static_graph=False, delay_all_reduce_named_params=None, param_to_hook_all_reduce=None, mixed_precision=None, device_mesh=None, skip_all_reduce_unused_params=False)[source]#
实现基于 `torch.distributed` 的模块级别分布式数据并行。
该容器通过在每个模型副本之间同步梯度来实现数据并行。用于同步的设备由输入 `process_group` 指定,默认是整个世界。请注意,`DistributedDataParallel` 不会对参与的 GPU 进行分块或分片;用户负责定义如何进行分片,例如通过使用 `DistributedSampler`。
另请参阅:基础知识 和 使用 nn.parallel.DistributedDataParallel 而不是 multiprocessing 或 nn.DataParallel。与 `torch.nn.DataParallel` 相同的输入限制适用。
创建此类需要 `torch.distributed` 已经通过调用 `torch.distributed.init_process_group()` 初始化。
`DistributedDataParallel` 在单节点多 GPU 数据并行训练中比 `torch.nn.DataParallel` 快得多。
要在具有 N 个 GPU 的主机上使用 `DistributedDataParallel`,您应该启动 `N` 个进程,确保每个进程专门处理 0 到 N-1 的单个 GPU。这可以通过为每个进程设置 `CUDA_VISIBLE_DEVICES` 或通过调用
>>> torch.cuda.set_device(i)
其中 i 从 0 到 N-1。在每个进程中,您应该参考以下内容来构建此模块
>>> torch.distributed.init_process_group( >>> backend='nccl', world_size=N, init_method='...' >>> ) >>> model = DistributedDataParallel(model, device_ids=[i], output_device=i)
为了在每个节点上启动多个进程,您可以使用 `torch.distributed.launch` 或 `torch.multiprocessing.spawn`。
注意
有关分布式训练的所有功能的简要介绍,请参阅 PyTorch 分布式概述。
注意
`DistributedDataParallel` 可以与 `torch.distributed.optim.ZeroRedundancyOptimizer` 结合使用,以减少每个 rank 的优化器状态内存占用。有关更多详细信息,请参阅 ZeroRedundancyOptimizer 示例。
注意
在使用 GPU 时,`nccl` 后端是目前最快且高度推荐的后端。这适用于单节点和多节点分布式训练。
注意
此模块还支持混合精度分布式训练。这意味着您的模型可以具有不同类型的参数,例如 `fp16` 和 `fp32` 的混合类型,这些混合类型参数的梯度归约将正常工作。
注意
如果您使用 `torch.save` 在一个进程上检查模块,并在其他进程上使用 `torch.load` 恢复它,请确保为每个进程正确配置 `map_location`。如果没有 `map_location`,`torch.load` 会将模块恢复到它从中保存的设备。
注意
当模型在 `M` 个节点上以 `batch=N` 训练时,与在单个节点上以 `batch=M*N` 训练的相同模型相比,梯度将小 `M` 倍(因为不同节点之间的梯度被平均),如果损失是跨批次中的实例求和(而不是通常的平均)。在您想要获得与本地训练对等体在数学上等效的训练过程时,您应该考虑这一点。但在大多数情况下,您可以将 `DistributedDataParallel` 包装的模型、`DataParallel` 包装的模型以及单个 GPU 上的普通模型视为相同(例如,对于等效的批次大小使用相同的学习率)。
注意
参数永远不会在进程之间广播。该模块会对梯度执行 all-reduce 操作,并假设它们将在所有进程中以相同方式被优化器修改。缓冲区(例如 BatchNorm 统计信息)在每次迭代中从 rank 0 的模块广播到系统中的所有其他副本。
注意
如果您将 `DistributedDataParallel` 与 分布式 RPC 框架 结合使用,您应该始终使用 `torch.distributed.autograd.backward()` 来计算梯度,并使用 `torch.distributed.optim.DistributedOptimizer` 来优化参数。
示例
>>> import torch.distributed.autograd as dist_autograd >>> from torch.nn.parallel import DistributedDataParallel as DDP >>> import torch >>> from torch import optim >>> from torch.distributed.optim import DistributedOptimizer >>> import torch.distributed.rpc as rpc >>> from torch.distributed.rpc import RRef >>> >>> t1 = torch.rand((3, 3), requires_grad=True) >>> t2 = torch.rand((3, 3), requires_grad=True) >>> rref = rpc.remote("worker1", torch.add, args=(t1, t2)) >>> ddp_model = DDP(my_model) >>> >>> # Setup optimizer >>> optimizer_params = [rref] >>> for param in ddp_model.parameters(): >>> optimizer_params.append(RRef(param)) >>> >>> dist_optim = DistributedOptimizer( >>> optim.SGD, >>> optimizer_params, >>> lr=0.05, >>> ) >>> >>> with dist_autograd.context() as context_id: >>> pred = ddp_model(rref.to_here()) >>> loss = loss_func(pred, target) >>> dist_autograd.backward(context_id, [loss]) >>> dist_optim.step(context_id)
注意
`DistributedDataParallel` 目前对使用 `torch.utils.checkpoint()` 的梯度检查点提供了有限的支持。如果检查点使用 `use_reentrant=False`(推荐),DDP 将按预期工作,没有任何限制。然而,如果检查点使用 `use_reentrant=True`(默认值),当模型中没有未使用的参数并且每个层最多检查点一次时,DDP 将按预期工作(确保您没有将 `find_unused_parameters=True` 传递给 DDP)。我们目前不支持一个层被检查点多次,或者检查点模型中存在未使用参数的情况。
注意
要让非 DDP 模型加载 DDP 模型的 state dict,需要应用 `consume_prefix_in_state_dict_if_present()` 来在加载之前从 DDP state dict 中剥离前缀“module.”。
警告
该模块的构造函数、forward 方法以及输出(或该模块输出的函数)的微分是分布式同步点。在不同进程可能执行不同代码的情况下,请将这一点考虑在内。
警告
此模块假定所有参数在创建时都已在模型中注册。之后不应添加或删除任何参数。这同样适用于缓冲区。
警告
此模块假定在每个分布式进程的模型中注册的所有参数顺序都相同。该模块本身将按照模型注册参数的逆序执行梯度 `allreduce`。换句话说,用户有责任确保每个分布式进程拥有完全相同的模型,从而拥有完全相同的参数注册顺序。
警告
此模块允许具有非行主存储器连续步长的参数。例如,您的模型可能包含一些参数,它们的
torch.memory_format
为torch.contiguous_format
,而其他参数的格式为torch.channels_last
。但是,不同进程中的相应参数必须具有相同的步长。警告
此模块不适用于
torch.autograd.grad()
(即,它仅在梯度要累积在参数的.grad
属性中时才起作用)。警告
如果您计划将此模块与
nccl
后端或gloo
后端(使用 Infiniband)结合使用,并使用具有多个工作进程的 DataLoader,请将多进程启动方法更改为forkserver
(仅限 Python 3)或spawn
。不幸的是,Gloo(使用 Infiniband)和 NCCL2 不是 fork 安全的,如果您不更改此设置,可能会遇到死锁。警告
在将模型包装到
DistributedDataParallel
中后,切勿尝试更改模型的参数。因为,在将模型包装到DistributedDataParallel
中时,DistributedDataParallel
的构造函数将在构造时为模型本身的所有参数注册额外的梯度规约函数。如果您之后更改模型的参数,梯度规约函数将不再匹配正确的参数集。警告
将
DistributedDataParallel
与 分布式 RPC 框架 结合使用是实验性的,可能会发生更改。- 参数
module (Module) – 要并行化的模块
device_ids (list of int or torch.device) –
CUDA 设备。1)对于单设备模块,
device_ids
可以包含一个设备 ID,该 ID 表示此进程对应的输入模块所在的唯一 CUDA 设备。或者,device_ids
也可以为None
。2)对于多设备模块和 CPU 模块,device_ids
必须为None
。当两种情况下
device_ids
均为None
时,前向传播的输入数据和实际模块都必须放置在正确的设备上。(默认:None
)output_device (int or torch.device) – 单设备 CUDA 模块的输出设备位置。对于多设备模块和 CPU 模块,它必须为
None
,并且模块本身决定输出位置。(默认:单设备模块为device_ids[0]
)broadcast_buffers (bool) – 在
forward
函数开始时启用模块缓冲区同步(广播)的标志。(默认:True
)init_sync (bool) – 初始化期间是否同步以验证参数形状并广播参数和缓冲区。警告:如果此设置为 False,用户需要自己确保所有 rank 上的权重相同。(默认:
True
)process_group – 用于分布式数据 all-reduction 的进程组。如果为
None
,则使用由torch.distributed.init_process_group()
创建的默认进程组。(默认:None
)bucket_cap_mb –
DistributedDataParallel
将把参数分桶,以便每个桶的梯度规约可以与后向计算重叠。bucket_cap_mb
控制桶大小(以 MiB 为单位)。如果为None
,则使用默认大小 25 MiB。(默认:None
)find_unused_parameters (bool) – 从包装模块的
forward
函数的返回值中包含的所有张量开始遍历 autograd 图。在图中未接收梯度的参数将被预先标记为准备好进行规约。此外,在包装模块的forward
函数中可能使用但未包含在损失计算中因此也不会接收梯度的参数,也将被预先标记为准备好进行规约。(默认:False
)check_reduction – 此参数已弃用。
gradient_as_bucket_view (bool) – 当设置为
True
时,梯度将是视图,指向allreduce
通信桶的不同偏移量。这可以减少峰值内存使用,节省的内存大小等于总梯度大小。此外,它还避免了梯度和allreduce
通信桶之间的复制开销。当梯度是视图时,不能在梯度上调用detach_()
。如果遇到此类错误,请参考torch/optim/optimizer.py
中的zero_grad()
函数作为解决方案。请注意,梯度在第一次迭代后将成为视图,因此应在第一次迭代后检查峰值内存节省情况。static_graph (bool) –
当设置为
True
时,DDP 会知道训练图是静态的。静态图意味着 1)在整个训练循环中,已使用和未使用的参数集不会改变;在这种情况下,用户设置find_unused_parameters = True
与否无关紧要。2)训练图的方式在整个训练循环中不会改变(即,没有依赖于迭代的控制流)。当 static_graph 设置为True
时,DDP 将支持过去无法支持的情况:1)可重入后向传播。2)多次激活检查点。3)模型具有未使用的参数时的激活检查点。4)模型参数位于前向函数之外。5)在有未使用的参数时可能提高性能,因为当 static_graph 设置为True
时,DDP 不会在每次迭代中搜索图来检测未使用的参数。要检查您是否可以设置 static_graph 为True
,一种方法是检查您先前模型训练结束时的 ddp 日志数据,如果ddp_logging_data.get("can_set_static_graph") == True
,那么您在很大程度上也可以将static_graph = True
。- 示例:
>>> model_DDP = torch.nn.parallel.DistributedDataParallel(model) >>> # Training loop >>> ... >>> ddp_logging_data = model_DDP._get_ddp_logging_data() >>> static_graph = ddp_logging_data.get("can_set_static_graph")
delay_all_reduce_named_params (list of tuple of str and torch.nn.Parameter) – 一组命名参数,当
param_to_hook_all_reduce
中指定的参数的梯度就绪时,它们的 all reduce 操作将被延迟。DDP 的其他参数不适用于此参数中指定的命名参数,因为 DDP 规约器会忽略这些命名参数。param_to_hook_all_reduce (torch.nn.Parameter) – 一个用于挂载
delay_all_reduce_named_params
中指定的参数的延迟 all reduce 的参数。skip_all_reduce_unused_params – 当设置为 True 时,DDP 将跳过未使用的参数的规约。这要求未使用的参数在整个训练过程中在所有 rank 上保持不变。如果不满足此条件,可能会导致不同步并导致训练挂起。
- 变量
module (Module) – 要并行化的模块。
示例
>>> torch.distributed.init_process_group(backend='nccl', world_size=4, init_method='...') >>> net = torch.nn.parallel.DistributedDataParallel(model)
- join(divide_by_initial_world_size=True, enable=True, throw_on_early_termination=False)[source]#
DDP 中训练不等量输入跨进程的上下文管理器。
此上下文管理器将跟踪已加入的 DDP 进程,并通过插入集体通信操作来“模拟”前向和后向传播,以匹配未加入的 DDP 进程创建的通信操作。这将确保每个集体调用都有一个已加入的 DDP 进程对应的调用,从而防止在跨进程训练不等量输入时发生的挂起或错误。或者,如果
throw_on_early_termination
标志设置为True
,一旦一个 rank 用完了输入,所有训练器都会抛出错误,从而允许根据应用程序逻辑捕获和处理这些错误。一旦所有 DDP 进程都加入,上下文管理器会将最后一个加入进程的模型广播到所有进程,以确保模型在所有进程之间是相同的(这由 DDP 保证)。
要使用此功能来启用跨进程训练不等量输入,只需将此上下文管理器包装在您的训练循环周围即可。无需对模型或数据加载进行进一步修改。
警告
如果此上下文管理器包装的模型或训练循环具有额外的分布式集体操作,例如模型前向传播中的
SyncBatchNorm
,则必须启用throw_on_early_termination
标志。这是因为此上下文管理器不了解非 DDP 集体通信。此标志将导致所有 rank 在任何一个 rank 用尽输入时抛出错误,从而允许跨所有 rank 捕获和恢复这些错误。- 参数
divide_by_initial_world_size (bool) – 如果为
True
,则将梯度除以 DDP 启动时的初始world_size
。如果为False
,则计算有效world_size
(尚未耗尽输入的 rank 数量),并在 allreduce 期间除以该值。设置divide_by_initial_world_size=True
以确保每个输入样本(包括不等量输入)在对全局梯度的贡献方面具有相等的权重。这是通过即使在遇到不等量输入时也始终将梯度除以初始world_size
来实现的。如果将其设置为False
,我们将梯度除以剩余的节点数。这确保了与使用较小world_size
进行训练的等效性,尽管这也意味着不等量输入将对全局梯度贡献更多。通常,您应该为训练任务的最后几个输入不等量的情况设置此值为True
。在极端情况下,当输入数量存在较大差异时,将此设置为False
可能会提供更好的结果。enable (bool) – 是否启用不等量输入检测。在您知道参与进程之间的输入是相等的情况下,请传入
enable=False
来禁用。默认值为True
。throw_on_early_termination (bool) – 当至少有一个 rank 用尽输入时,是抛出错误还是继续训练。如果为
True
,将在第一个 rank 达到数据末尾时抛出。如果为False
,将继续使用较小的有效world_size
进行训练,直到所有 rank 加入。请注意,如果指定了此标志,则divide_by_initial_world_size
标志将被忽略。默认值为False
。
示例
>>> import torch >>> import torch.distributed as dist >>> import os >>> import torch.multiprocessing as mp >>> import torch.nn as nn >>> # On each spawned worker >>> def worker(rank): >>> dist.init_process_group("nccl", rank=rank, world_size=2) >>> torch.cuda.set_device(rank) >>> model = nn.Linear(1, 1, bias=False).to(rank) >>> model = torch.nn.parallel.DistributedDataParallel( >>> model, device_ids=[rank], output_device=rank >>> ) >>> # Rank 1 gets one more input than rank 0. >>> inputs = [torch.tensor([1]).float() for _ in range(10 + rank)] >>> with model.join(): >>> for _ in range(5): >>> for inp in inputs: >>> loss = model(inp).sum() >>> loss.backward() >>> # Without the join() API, the below synchronization will hang >>> # blocking for rank 1's allreduce to complete. >>> torch.cuda.synchronize(device=rank)
- join_hook(**kwargs)[source]#
DDP join hook 通过在正向和反向传播中镜像通信来支持不等量输入的训练。
- 参数
kwargs (dict) – 一个
dict
,包含任何关键字参数,用于在运行时修改 join hook 的行为;共享同一 join 上下文管理器的所有Joinable
实例都会收到相同的kwargs
值。
- 该 hook 支持以下关键字参数
- divide_by_initial_world_size (bool, optional)
如果为
True
,则梯度除以 DDP 启动时的初始 world size。如果为False
,则梯度除以有效 world size(即非 join 进程的数量),这意味着不等量输入对全局梯度贡献更大。通常,如果不等量的程度较小,应将其设置为True
,但在极端情况下可以设置为False
以获得可能更好的结果。默认为True
。
- no_sync()[source]#
禁用 DDP 进程之间梯度同步的上下文管理器。
在此上下文内,梯度将在模块变量上累积,这些变量稍后将在退出上下文的第一个前向-后向传递中同步。
示例
>>> ddp = torch.nn.parallel.DistributedDataParallel(model, pg) >>> with ddp.no_sync(): >>> for input in inputs: >>> ddp(input).backward() # no synchronization, accumulate grads >>> ddp(another_input).backward() # synchronize grads
警告
前向传递应包含在上下文管理器内,否则梯度仍会同步。
- register_comm_hook(state, hook)[source]#
注册用于用户定义的 DDP 跨多个工作节点梯度聚合的通信 hook。
此 hook 对于研究人员尝试新想法非常有用。例如,此 hook 可用于在运行 DistributedDataParallel 训练时实现几种算法,如 GossipGrad 和梯度压缩,这些算法涉及参数同步的不同通信策略。
- 参数
state (object) –
传递给 hook,用于在训练过程中维护任何状态信息。例如,包括梯度压缩中的错误反馈,GossipGrad 中下一个要通信的对等方等。
它由每个工作节点本地存储,并在该工作节点上的所有梯度张量之间共享。
hook (Callable) –
可调用,具有以下签名:
hook(state: object, bucket: dist.GradBucket) -> torch.futures.Future[torch.Tensor]
当 bucket 就绪时调用此函数。hook 可以执行任何必要的处理,并返回一个 Future 来指示任何异步工作的完成(例如,allreduce)。如果 hook 不执行任何通信,它仍然必须返回一个已完成的 Future。Future 应该包含 grad bucket 张量的新值。当 bucket 就绪时,c10d 规约器会调用此 hook,并使用 Future 返回的张量,将梯度复制到各个参数。请注意,future 的返回类型必须是单个张量。
我们还提供了一个名为
get_future
的 API 来检索与c10d.ProcessGroup.Work
完成关联的 Future。get_future
目前支持 NCCL,并且也支持 GLOO 和 MPI 大多数操作,除了点对点操作(send/recv)。
警告
梯度 bucket 的张量不会按 world_size 进行预分。用户负责在执行 allreduce 等操作时除以 world_size。
警告
DDP 通信 hook 只能注册一次,并且应在调用 backward 之前注册。
警告
hook 返回的 Future 对象应包含一个与 grad bucket 中的张量形状相同的单个张量。
警告
get_future
API 支持 NCCL,以及部分 GLOO 和 MPI 后端(不支持点对点操作,如 send/recv),并将返回一个torch.futures.Future
。- 示例:
下面是一个 noop hook 的示例,它返回相同的张量。
>>> def noop(state: object, bucket: dist.GradBucket) -> torch.futures.Future[torch.Tensor]: >>> fut = torch.futures.Future() >>> fut.set_result(bucket.buffer()) >>> return fut >>> ddp.register_comm_hook(state=None, hook=noop)
- 示例:
下面是一个并行 SGD 算法的示例,其中梯度在 allreduce 之前被编码,然后在 allreduce 之后被解码。
>>> def encode_and_decode(state: object, bucket: dist.GradBucket) -> torch.futures.Future[torch.Tensor]: >>> encoded_tensor = encode(bucket.buffer()) # encode gradients >>> fut = torch.distributed.all_reduce(encoded_tensor).get_future() >>> # Define the then callback to decode. >>> def decode(fut): >>> decoded_tensor = decode(fut.value()[0]) # decode gradients >>> return decoded_tensor >>> return fut.then(decode) >>> ddp.register_comm_hook(state=None, hook=encode_and_decode)