Pytorch 单机多 GPU 分布式 DDP (DistributeDataParallel) 训练 Loss 相关问题【记录】


该文记录本人第一次利用 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 模型参数不变


简单问题排查

  1. 怀疑是 GAN 网络使用的 optimizer 的问题
  2. 怀疑是模型保存方式的问题

⇓ \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=Falsefind_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.

# 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

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. 通过优化器书写顺序进行同时更新(见简单问题排查1参考4链接)
    2. 固定一个网络更新另一个

6. DDP 训练中断后出现现存问题

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WhiteLie777

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值