目录
- 简单问题排查
- 问题一:反向传播时图的改变导致报错
- 问题二:DDP 中多次调用模型生成结果问题
- 问题三:DDP 训练模型涉及 BatchNorm 的问题
- 其他问题
- 1. RuntimeError: Attempted to set the storage of a tensor on device "cuda:x" to a storage on different device "cuda:0". This is no longer allowed; the devices must match.
- 2. KeyError: 'module.xxxx'
- 3. AttributeError: 'NoneType' object has no attribute 'data'
- 4. UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed.
- 5. 多模型共用损失更新问题
- 6. DDP 训练中断后出现现存问题
该文记录本人第一次利用 pytorch 的分布式训练 DDP 遇到的问题,也欢迎大家在评论区讨论
本人代码整体参考单模型训练代码 distributed.py(源自当代研究生应当掌握的并行训练方法)
实验环境:8 Nvidia Titan Xp, CUDA 10.1, pytorch 1.7.1, python 3.7.0
训练模型:包括简单的 GAN(生成器和判别器)以及简易版 MAE(遮掩恢复自编码器)
核心问题:所有 Loss 均不为 0 且在变化,但反向传播仅 MAE 的模型参数有改变,GAN 模型参数不变
简单问题排查
- 怀疑是 GAN 网络使用的 optimizer 的问题
- 原因:
1)MAE 采用的是 AdamW,而 GAN 采用 Adam
2)前者设置了学习率 scheduler,而后者没有设置
3)反向传播更新代码是有顺序的 - 猜测:
1)Adam 优化器在 DDP 下更新可能会出现问题
2)多个优化器联合更新会出现仅更新一个的情况
3)第一个更新的模型会学习,之后的就无法更新了 - 方案:
1)更换 GAN 网络的 optimizer,使其与 MAE 相同
2)将 optimizer 写在一起,同时更新 Loss
3)改变三个模型 optimizer 的顺序 - 结果:均无影响
- 参考:
- 原因:
- 怀疑是模型保存方式的问题
- 原因:分布式模型保存涉及到 .module 和不加两种形式
- 猜测:.module 仅训练和保存了模型子网络部分
- 方案:全部在模型后边加上或删除 .module
- 结果:均无影响
- 参考:
⇓ \Downarrow ⇓ 下面是真正相关的问题及解决办法
问题一:反向传播时图的改变导致报错
核心点:要反向传播的 Loss 涉及到的变量产生变化(释放 / 数值变化 / 已经更新过)
1. 问题描述
1)Trying to backward through the graph a second time, but the buffers have already been freed
- 直译:第二次尝试反向传播图,但缓冲区已被释放
- 原因:用于反向传播的参数(包括组成它的变量)被释放了,无法进行
- 比较通俗的解释可以参考 pytorch 的 backward 计算图
2)One of the variables needed for gradient computation has been modified by an inplace operation
- 直译:梯度计算所需的变量之一已通过就地操作进行了修改
- 原因:用于反向传播的参数(包括组成它的变量)与其刚建立时的数值和结构发生了变化
- 该问题分析和多种解决办法可以参考 Inplace 报错问题分析与办法
3)Element 0 of tensors does not require grad and does not have a grad_fn
- 直译:张量的元素 0 不需要 grad,也没有 grad_fn
- 原因:用于反向传播的参数(包括组成它的变量)是不可求导的,即无法求梯度
- 对应的解释和产生原因可以参考 pytorch 关于 requires_grad_(True) 的理解
2. 解决方案
1)切断无用的反向传播参数【不推荐】
- 方法:采用 .detach() 或 .detach_() 或 .data 切断
- 注意:
- 该些方法产生的参数都是默认 requires_grad_(False)
- 【相对推荐】.detach() 生成的参数空间与原始不一样,若之后涉及该参数更改和求导时则会报错;且重新将它的 requires_grad 置为 true,它也不会具有梯度
- 【不推荐】.detach_() 与上面方式类似,但是直接改变了本身的参数,而非像前者生成新的
- 【不推荐】.data 生成的参数与原始变量指向同一个空间,将其改变后原始的也改变,且不会报错
- 缺点:
- 需要人工判断该参数对于某个模型是否需要计算梯度
- 当一个参数需要被多个模型利用计算时则会出现问题
- 参考:
2)采用函数调用赋值【推荐】
- 方法:将含有某个模型梯度更新的部分封装成自定义函数
def backward_D(self, x, y, r, f):
# get judgement
ori, adv, t_real, t_fake = x, y, r, f
_, dis_ori, _, dis_adv = self.get_gan_process(ori, adv)
# calculate loss
ori_dis_loss = self.loss_bce(dis_ori, t_real)
adv_dis_loss = self.loss_bce(dis_adv, t_fake)
dis_loss = ori_dis_loss + adv_dis_loss
# update loss
torch.distributed.barrier()
dis_loss.requires_grad_(True)
self.opt_D.zero_grad()
dis_loss.backward()
self.opt_D.step()
return
- 优点:无需考虑该参数是否需要牵扯到其他网络的更新
- 缺点:需要在计算时重新开辟空间进行赋值计算
- 参考:从 GAN 代码分析
问题二:DDP 中多次调用模型生成结果问题
核心点:多 GPU 分布式训练 DDP (DistributeDataParallel) 对同一模型进行多次调用进行反向传播
1. 问题描述
- 现象:报错 One of the variables needed for gradient computation has been modified by an inplace operation: [torch.cuda.Tensor [N]] is at version X; expected version Y instead. 这个报错和问题一中第二个的类似,但根本导致的原因不同
- 原因:由于多次调用同一模型的运算结果,即执行多个(次)forward() 然后执行一次 backward() 则会报错
2. 解决方案
- 方法:在加载分布式模型时添加
broadcast_buffers=False
和find_unused_parameters=True
的参数- broadcast_buffers:该参数默认为 True,在模型执行 forward 之前,cuda:0 会把 buffer 中的参数值全部覆盖到别的 gpu 上
- find_unused_parameters:如果模型的输出有不需要进行反向传播的,此参数需要设置为 True
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank], broadcast_buffers=False, find_unused_parameters=True)
问题三:DDP 训练模型涉及 BatchNorm 的问题
核心点:多 GPU 分布式训练 DDP (DistributeDataParallel) 对数据做 BatchNorm 时无法考虑全局 batch size 的影响
1. 问题描述
- 现象:每个进程得到的 BatchNorm 后数据不统一
- 原因:每个进程仅计算当前 gpu 分得的 batch size 的数据,不考虑全局
- 影响:当单个 gpu 分得的 batch size 较小时,无法体现普遍数据的规律,且不好拟合
2. 解决方案
- 方法:在封装成 DDP 模型前,添加 pytorch 自带的同步方式 SyncBatchNorm,合并定义的进程数量 (nprocs)
model = Model().to(device)
process_group = torch.distributed.new_group(list(range(nprocs)))
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model, process_group)
- 参考:
其他问题
非核心问题,仅此作为备份记录一下。
1. RuntimeError: Attempted to set the storage of a tensor on device “cuda:x” to a storage on different device “cuda:0”. This is no longer allowed; the devices must match.
- 原因:原始模型加载在 cuda:0 上,现在多 GPU 跑需要将其加载到各个 GPU 上
- 方法:torch.load() 加载模型时用 map_location 参数
# parser.add_argument('--local_rank', default=-1, type=int, help='node rank for distributed training')
device = torch.device('cuda', args.local_rank)
state_dict = torch.load(target_path, map_location={'cuda:0':f'{device}'}) if device else torch.load(target_path)
2. KeyError: ‘module.xxxx’
- 原因:根本 Key Error 是因为权重不匹配导致的,此处报错是分布式模型权重带 module 导致的
- 方法:在问题出现位置处添加或取消 module
3. AttributeError: ‘NoneType’ object has no attribute ‘data’
- 原因:
eval()
下生成的数据默认不具有梯度计算的能力(核心在在于要计算梯度但参数不让) - 方法:在需要梯度更新的变量,用 Variable 强制改变其属性
image = Variable(image.cuda(), requires_grad=True) # 需要梯度更新加 requires_grad
label = Variable(label.type(torch.LongTensor).cuda()) # 无需梯度更新的不用加 requires_grad 或设置成 False
- 优点:对于重新原本
requires_grad
是False
的也能重新恢复计算梯度的能力 - 缺点:需要人工判断
- 参考:
4. UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed.
- 描述:提示叶子节点不保存梯度信息
- 方法:可以用
Tensor变量名.is_leaf
查看参数是否为叶节点,若返回True
则表示是,否则为False
;前者的话就能找到这个提醒的报错地方。 - 参考:通俗讲解 Pytorch 梯度的相关问题
5. 多模型共用损失更新问题
- 描述:两个模型融合损失学习效果不好,无法同时更新两个模型
- 方法:
- 通过优化器书写顺序进行同时更新(见简单问题排查1参考4链接)
- 固定一个网络更新另一个
6. DDP 训练中断后出现现存问题
- 描述:DDP 训练出现问题后有时需要
kill
命令来释放显卡内存,但有时仍存在莫名被占用情况 - 方法:通过
fuser -v /dev/nvidia*
和ps -f -p PID
命令查看问题程序