MNIST 手写数字分类 Data Parallel (DP)

数据并行 vs. 模型并行
数据并行:模型拷贝(per device),数据 split/chunk(batch 上)
- the module is replicated on each device, and each replica handles a portion of the input.
- During the backwards pass, gradients from each replica are summed into the original module.
模型并行:数据拷贝(per device),模型 split/chunk(显然是单卡放不下模型的情况下)
DP 说的直白点,就是把通常意义上的 batch 切分到各个卡上, 在主卡的控制下实现前向传播和反向传播。
官方文档: https://pytorch.org/docs/stable/generated/torch.nn.DataParallel.html
参数:
- module, 需要 DP 的 torch.nn.module, i.e., 你的 model
- device_ids=None, 参与训练的 GPU 有哪些,device_ids=gpus
- output_device=None, 用于汇总梯度的 GPU 是哪个,output_device=gpus[0]
- dim=0, 数据切分的维度, 一般就是第一维度 batch 维度来切分, dim = 0 [30, xxx] -> [10, …], [10, …], [10, …] on 3 GPUs
The parallelized module must have its parameters and buffers on device_ids[0] before running(forward/backward) this DataParallel module. 模型参数必须先缓存在给定卡中的第一张卡上。
如果我只执行了 nn.DataParallel, 没有执行 model.to(device) device 必须是选中的第一张卡, 否则会报错 RuntimeError: module must have its parameters and buffers on device cuda:4 (device_ids[0]) but found one of them on device: cpu. PS: 你没先缓存到正确的卡上
1 | class ConvNet(nn.Module): |
单卡 forward
1 | device = torch.device(f"cuda:4" if torch.cuda.is_available() else "cpu") |
DP forward
1 | CUDA_DEVICE_IDS = [4,5] |
forward 参数对比 (单卡和DP-2卡)
1 | 单卡 |
用DP情况下虽然循环里每次的 batch 大小还是一样的, 但模型 forward 确实将 batch / len(device_ids), 原来 512 的 batch 变为 256, 两个卡上各自有一个模型分别跑了 1 / len(device_ids) 的数据。
1 | 单卡 |
每个卡上都有一份模型参数和 batch / len(device_ids) 的数据
DP 训练
实际只是增加了 model = nn.DataParallel(model, device_ids=CUDA_DEVICE_IDS)
, 后续 forward 和 backpropagation 都不需要改变
1 | CUDA_DEVICE_IDS = [4,5] |
看下 nn.DataParallel 的内部 forward 函数, 有几行代码显示了大致流程
1 | inputs, module_kwargs = self.scatter(inputs, kwargs, self.device_ids) # 分散数据 inputs |
模型输出(outputs)来自多个子 GPU,但会 在主卡上 gather,因为 torch.nn.DataParallel 的默认行为是把所有子 GPU 的输出,gather 回主 GPU(device[0])。
为什么要 gather, 因为 label (targets)通常在主卡上,所以为了计算 loss,需要把输出也 gather 到主卡,才能和 labels 对应。PS:计算损失是要求参数在同一个 device 上。
loss.backward() 触发 autograd,它会根据 gather 的结构把 grad_output 自动 scatter 回子卡,每张子卡用自己的输出执行 backward。最终每个 gpu 的 gradient 都还要进行统一的更新,将梯度聚合再下方梯度,即 all-reduce。
实现 DP 的一种经典编程框架叫 “参数服务器” parameter server,在这个框架里,计算 GPU 称为 Worker,**梯度聚合 GPU 称为 Server。**在实际应用中,为了尽量减少通讯量,一般可选择一个 Worker 同时作为 Server。比如可把梯度全发到 GPU0 上做聚合。DP 的通信瓶颈在于 server 的通信开销,sever 没法一次性立马接受所有数据,所以当 worker 无法传输数据且已经计算完成时,它就只能摸鱼。
!!! 注意:DP 已经不被推荐了
完整代码
1 | import torch |