1 torch.nn.DataParallel和torch.nn.parallel.DistributedDataParallel函数详解

1.1 torch.nn.DataParallel

1. 官方文档

https://pytorch.org/docs/1.8.0/generated/torch.nn.DataParallel.html?highlight=nn%20data#torch.nn.DataParallel

2. 函数形式

torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)

3. 函数功能

在模块级别实现数据并行

4. 函数参数

  • module:nn.Module,需要多卡训练的模型;
  • device_ids:int列表或者torch.device列表,cuda设备,默认值为所有设备;
  • output_device:int或者torch.device,输出的cuda设备,默认值为device_ids[0],比如初始设置的os.environ["CUDA_VISIBLE_DEVICES"] = "2, 3", 那么0卡(逻辑卡号)指的是2卡(物理卡号));
  • dim:按哪个维度进行数据的划分,默认是输入数据的第一个维度,即按batchsize划分(设数据数据的格式是B, C, H, W);

1.2 torch.nn.parallel.DistributedDataParallel

1. 官方文档

https://pytorch.org/docs/1.8.0/generated/torch.nn.parallel.DistributedDataParallel.html?highlight=torch%20nn%20parallel%20dis#torch.nn.parallel.DistributedDataParallel

2. 函数形式

torch.nn.parallel.DistributedDataParallel(module, device_ids=None, output_device=None, dim=0, broadcast_buffers=True, process_group=None, bucket_cap_mb=25, find_unused_parameters=False, check_reduction=False, gradient_as_bucket_view=False)

3. 函数功能

实现基于torch的分布式数据并行

4. 函数参数

  • module:nn.Module,需要多卡训练的模型;

  • device_ids:int列表或者torch.device列表,cuda设备,默认值为所有设备;

  • output_device:int或者torch.device,输出的cuda设备,默认值为device_ids[0],比如初始设置的os.environ["CUDA_VISIBLE_DEVICES"] = "2, 3", 那么0卡(逻辑卡号)指的是2卡(物理卡号));

  • dim:按哪个维度进行数据的划分,默认是输入数据的第一个维度,即按batchsize划分(设数据数据的格式是B, C, H, W);

  • broadcast_buffers:bool,是否在forward函数开始时启用同步(广播)缓冲区;

  • process_group:用于分布式数据缩减的进程组,如果为None,则默认为torch.distributed.init_process_group()创建的进程组,有关torch.distributed.init_process_group()函数更加详细的介绍看下文1.3节;

  • bucket_cap_mbDistributedDataParallel将bucket参数分为多个bucket,因此每个bucket的梯度缩减可能与反向计算重叠。bucket_cap_mb为以mb为单位的bucket大小。(默认值:25);

函数剩余其他参数参考官网文档。

1.3 torch.distributed.init_process_group

1. 官方文档

https://pytorch.org/docs/1.8.0/distributed.html#torch.distributed.init_process_group

2. 函数形式

torch.distributed.init_process_group(backend, init_method=None, timeout=datetime.timedelta(seconds=1800), world_size=-1, rank=-1, store=None, group_name='')

3. 函数功能

初始化默认的分布式进程组

4. 函数参数

  • backend:str或者Backend。所使用的backend。从mpigloonccl中选择,该字段既可以通过字符串进行设置也可以通过Backend的属性Backend.GLOO进行设置。如果在带有nccl的机器上使用多个进程,则每个进程必须对其所使用的每个GPU具有独占访问权,不然如在进程之间共享GPU可能会导致死锁;
  • init_method:str,可选参数。指定如何初始化进程组的URL;
  • world_size:int,可选参数。参与分布式训练的进程数;
  • rank:int,可选参数。当前进程的排名,其值应处于[0,world_size-1]的范围;
  • store:所有工作的进程都可以访问的键值存储,用于交换连接/地址信息;
  • time_out:timedelta,可选参数。针对进程组执行的操作超时时间,默认值为30分钟;

2 Pytorch官方文档中提到的两者的区别

文档地址:https://pytorch.org/docs/1.8.0/notes/cuda.html#cuda-nn-ddp-instead

官方建议使用torch.nn.parallel.DistributedDataParallel替换torch.nn.DataParallel。

Most use cases involving batched inputs and multiple GPUs should default to using DistributedDataParallel to utilize more than one GPU.

There are significant caveats to using CUDA models with multiprocessing; unless care is taken to meet the data handling requirements exactly, it is likely that your program will have incorrect or undefined behavior.

It is recommended to use DistributedDataParallel, instead of DataParallel to do multi-GPU training, even if there is only a single node.

The difference between DistributedDataParallel and DataParallel is: DistributedDataParallel uses multiprocessing where a process is created for each GPU, while DataParallel uses multithreading. By using multiprocessing, each GPU has its dedicated process, this avoids the performance overhead caused by GIL of Python interpreter.

If you use DistributedDataParallel, you could use torch.distributed.launch utility to launch your program, see Third-party backends.

3 torch.nn.DataParallel和torch.nn.parallel.DistributedDataParallel两者区别

1) 底层实现原理不同

torch.nn.parallel.DistributedDataParallel使用多进程实现,在训练开始会为每一个GPU创建一个进程,从而避免Python解释器GIL带来的性能开销;而torch.nn.DataParallel是通过使用Python的多进程来实现的,熟悉Python的都知道,由于Python的全局锁,Python的多线程实际上只用到了单核的性能,这就导致了torch.nn.DataParallel的效率较低。

2) 并行处理机制不同

DataParallel系统通过将整个小型批处理加载到主线程上,然后将子小型批处理分散到整个GPU网络中来工作。具体是将输入一个 batch 的数据均分成多份,分别送到对应的 GPU 进行计算。与 Module 相关的所有数据也都会以浅复制的方式复制多份。每个 GPU 在单独的线程上将针对各自的输入数据独立并行地进行 forward 计算。然后在主GPU上收集网络输出,并通过将网络输出与批次中每个元素的真实数据标签进行比较来计算损失函数值。接下来,损失值分散给各个GPU,每个GPU进行反向传播以计算梯度。最后,在主GPU上归约梯度、进行梯度下降,并更新主GPU上的模型参数。由于模型参数仅在主GPU上更新,而其他从属GPU此时并不是同步更新的,所以需要将更新后的模型参数复制到剩余的从属 GPU 中,以此来实现并行。DataParallel会将定义的网络模型参数默认放在GPU 0上,所以dataparallel实质是可以看做把训练参数从GPU拷贝到其他的GPU同时训练,这样会导致内存和GPU使用率出现很严重的负载不均衡现象,即GPU 0的使用内存和使用率会大大超出其他显卡的使用内存,因为在这里GPU0作为master来进行梯度的汇总和模型的更新,再将计算任务下发给其他GPU,所以他的内存和使用率会比其他的高。

与 DataParallel 的单进程控制多 GPU 不同,在 distributed 的帮助下,我们只需要编写一份代码,torch.nn.parallel.DistributedDataParallel就会自动将其分配给n个进程,分别在 n 个 GPU 上运行。不再有主GPU,每个GPU执行相同的任务。对每个GPU的训练都是在自己的过程中进行的。每个进程都从磁盘加载其自己的数据。分布式数据采样器可确保加载的数据在各个进程之间不重叠。损失函数的前向传播和计算在每个GPU上独立执行。因此,不需要收集网络输出。在反向传播期间,梯度下降在所有GPU上均被执行,从而确保每个GPU在反向传播结束时最终得到平均梯度的相同副本。

4 torch.nn.parallel.DistributedDataParallel的优势

相比于torch.nn.DataParallel,torch.nn.parallel.DistributedDataParallel具有以下优势,

1) 每个进程对应一个独立的训练过程,且只对梯度等少量数据进行信息交换

在每次迭代中,每个进程具有自己的 optimizer ,并独立完成所有的优化步骤,进程内与一般的训练无异。在各进程梯度计算完成之后,各进程需要将梯度进行汇总平均,然后再由 rank=0 的进程,将其 broadcast 到所有进程。之后,各进程用该梯度来独立的更新参数。而 DataParallel是梯度汇总到gpu0,反向传播更新参数,再广播参数给其他的gpu。由于各进程中的模型,初始参数一致 (初始时刻进行一次 broadcast),而每次用于更新参数的梯度也一致,因此,各进程的模型参数始终保持一致。而在 DataParallel 中,全程维护一个 optimizer,对各 GPU 上梯度进行求和,而在主 GPU 进行参数更新,之后再将模型参数 broadcast 到其他 GPU。相较于 DataParalleltorch.distributed 传输的数据量更少,因此速度更快,效率更高。

2) 每个进程包含独立的解释器和 GIL

一般使用的Python解释器CPython:是用C语言实现Pyhon,是目前应用最广泛的解释器。全局锁使Python在多线程效能上表现不佳,全局解释器锁(Global Interpreter Lock)是Python用于同步线程的工具,使得任何时刻仅有一个线程在执行。由于每个进程拥有独立的解释器和 GIL,消除了来自单个 Python 进程中的多个执行线程,模型副本或 GPU 的额外解释器开销和 GIL-thrashing ,因此可以减少解释器和 GIL 使用冲突。这对于严重依赖 Python runtimemodels 而言,比如说包含 RNN 层或大量小组件的 models 而言,这尤为重要。

参考链接