pytorch分布式学习笔记

 多设备同步BN

  • 不同步时每块GPU单独计算mean/var,batch size/per gpu 小的时候,syncBN的涨点效果明显。
  • syncBN的通讯是很费时的(每层BN都要做),eg. 两块GPU训练一个epoch是5.5s,使用syncBN后是8s,而一个GPU训练一个epoch是9s。(来自B站博主霹雳吧啦Wz)

DP与DDP

  • DP:单进程多线程,只能单机。 【#TODO:进程和线程的区别】
  • DDP:多进程多线程,可以多机。在单机情况下速度也比DP快。
  • 多GPU训练的启动方式(DDP):
python -m torch.distributed.launch --help

训练过程中如果强行终止的程序,在开启下次训练前建议通过nvidia-smi指令看下GPU的显存是否全部释放了,有概率出现假死情况。

  • DDP的优势:

在各进程梯度计算完成之后,各进程需要将梯度进行汇总平均,然后再由 rank=0 的进程,将其 broadcast 到所有进程。之后,各进程用该梯度来独立的更新参数。

而 DataParallel是梯度汇总到gpu0,反向传播更新参数,再广播参数给其他的gpu。

总体而言通过减少数据通讯节省了时间。


DDP训练过程中的一些代码

Distributed communication package - torch.distributed — PyTorch 1.9.1 documentation  官方代码教程。

分布式加载数据到dataloader

torch.utils.data.distributed.DistributedSampler : 多GPU训练时需要将数据分配到各GPU,包括打乱、补齐(复制)、交叉分配。

torch.utils.data.BatchSampler: 将上述分配好的数据打包成batch

    data_transform = {
        "train": transforms.Compose([transforms.RandomResizedCrop(224),
                                     transforms.RandomHorizontalFlip(),
                                     transforms.ToTensor(),
                                     transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
        "val": transforms.Compose([transforms.Resize(256),
                                   transforms.CenterCrop(224),
                                   transforms.ToTensor(),
                                   transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])}

    # 实例化训练数据集
    train_data_set = MyDataSet(images_path=train_images_path,
                               images_class=train_images_label,
                               transform=data_transform["train"])

    # 实例化验证数据集
    val_data_set = MyDataSet(images_path=val_images_path,
                             images_class=val_images_label,
                             transform=data_transform["val"])

    # 给每个rank对应的进程分配训练的样本索引
    train_sampler = torch.utils.data.distributed.DistributedSampler(train_data_set)
    val_sampler = torch.utils.data.distributed.DistributedSampler(val_data_set)

    # 将样本索引每batch_size个元素组成一个list
    train_batch_sampler = torch.utils.data.BatchSampler(
        train_sampler, batch_size, drop_last=True)

    nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  # number of workers
    if rank == 0:
        print('Using {} dataloader workers every process'.format(nw))
    train_loader = torch.utils.data.DataLoader(train_data_set,
                                               batch_sampler=train_batch_sampler,
                                               pin_memory=True,
                                               num_workers=nw,
                                               collate_fn=train_data_set.collate_fn)

    val_loader = torch.utils.data.DataLoader(val_data_set,
                                             batch_size=batch_size,
                                             sampler=val_sampler,
                                             pin_memory=True,
                                             num_workers=nw,
                                             collate_fn=val_data_set.collate_fn)

实例化模型并同步BN:

# 实例化模型
model = resnet34(num_classes=num_classes).to(device)

# 只有训练带有BN结构的网络时使用SyncBatchNorm采用意义
# 使用SyncBatchNorm后训练会更耗时
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)

# 转为DDP模型
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu])

对多块GPU上数据求平均(例如loss):

“注意,这里对多个设备上的loss求平均不是为了backward,仅仅是查看做个记录。这里有很多人误认为,在使用多GPU时需要先求平均损失然后在反向传播,其实不是的。应该是每个GPU设备计算出各批次数据的损失后,通过backward方法计算出各参数的损失梯度,然后DDP会自动帮我们在多个GPU设备上对各参数的损失梯度求平均,最后通过optimizer.step()去更新各参数”

dist.all_reduce(value)  #此处的value必须是tensor

if average:
    value /= world_size   # 上述代码默认求和,如果是平均需要除以GPU个数
return value

 类似的还有torch.distributed.broadcasttorch.distributed.gathertorch.distributed.reducetorch.distributed.scatterscatter可以将信息从master节点传到所有的其他节点,gather可以将信息从别的节点获取到master节点。all_reduce()第一个参数为需要进行运算的变量,第二个参数op则包含了一些方法,例如求和SUM,此外还有MIN, MAX等。broadcast和all_gather好像有点类似,具体不知。注意每个进程上的tensor list长度都必须相同。

【踩坑】上述传递方式的发送变量都是tensor,一些长度不定的list可能很难转成tensor(要补齐什么的)。目前看到的一种非tensor的GPU间传递方法:先存储,后读取。

torch.distributed.broadcast(tensor, src, group=group, async_op=False) # 将tensor从src(rank)广播到group中

torch.distributed.all_reduce(tensor, op=ReduceOp.SUM, group=group, async_op=False) # 对tensor进行原地in-pllace的reduce,op是torch.distributed.ReduceOp中的一个,指定了某种确定的element-wise的操作

torch.distributed.reduce(tensor, dst, op=ReduceOp.SUM, group=, async_op=False)

torch.distributed.all_gather(tensor_list, tensor, group=, async_op=False) # 将group中的tensor集中到tensor_list中

torch.distributed.gather(tensor, gather_list, dst, group=, async_op=False) # 将group中的tensor集中到dst(rank)处

torch.distributed.scatter(tensor, scatter_list, src, group=, async_op=False)

torch.distributed.broadcasr_multigpu(tensor_list, group=, async_op=False, src_tensor=0) # 将tensor广播到group中每个节点的每个GPU(进程)

torch.distributed.all_reduce_multigpu(tensor_list, op=ReduceOp.SUM, group=, async_op=False) # 减少所有机器上的张量数据,从而得到最终的结果。这个函数减少了每个节点上的张量数量,而每个张量位于不同的gpu上。因此,张量列表中的输入张量需要是GPU张量。另外,张量列表中的每个张量都需要驻留在不同的GPU上。

torch.distributed.reduce_multigpu(tensor_list, dst, op=ReduceOp.SUM, group=, async_op=False, dst_tensor=0) # 减少所有机器上多个gpu上的张量数据。tensor_list中的每个张量应该位于一个单独的GPU上只有有秩dst的进程上tensor_list[dst_张量]的GPU会收到最终结果。目前只支持nccl后端张量,应该只支持GPU张量

torch.distributed.all_gather_multigpu(output_tensor_lists, input_tensor_list, group=, async_op=False) # 从列表中的整个组收集张量。tensor_list中的每个张量应该位于一个单独的GPU上目前只支持nccl后端张量,应该只支持GPU张量
 
  

torch.distributed.barrier(group=, async_op=False) ,设置一个障碍,即所有rank都运行到这里时才会进行下一步。


 随机数种子

pytorch分布式学习笔记_第1张图片

 为了使模型性能稳定可比较,需要设置如上SEED且随时间变化。(比如seed=seed+epoch)

在DDP的distributed_sampler中会设置当shuffle=false时,seed=seed+epoch 。(这边存疑,具体的还需要再看看,实际训练中多卡固定seed后出现了梯度爆炸)

 deterministic也是一个使模型可确定的函数。

    # 通过维持各个进程之间的相同随机数种子使不同进程能获得同样的shuffle效果。
    trainloader.sampler.set_epoch(epoch)

上述代码能是各个进程的随机数种子都为epoch。但这个代码是否必须不知道。 


 distributed/launch.py

    >>> python -m torch.distributed.launch --nproc_per_node=NUM_GPUS_YOU_HAVE
               --nnodes=2 --node_rank=0 --master_addr="192.168.1.1"
               --master_port=1234 YOUR_TRAINING_SCRIPT.py (--arg1 --arg2 --arg3
               and all other arguments of your training script)

主要是master_port,单机时官方给的例子不需要设置master_port(猜测是用于多机通讯,但是单机不需要通讯?)。实际操作中在远程调试里不设置port会出错。猜测是由于即使不调用也会随机生成port,随机生成的port对于远程调试环境不存在。 


并行数据加载

流行的深度学习框架(例如Pytorch和Tensorflow)为分布式培训提供内置支持。从广义上讲,从磁盘读取输入数据开始,加载数据涉及四个步骤:

  1. 将数据从磁盘加载到主机(num_workers控制数据加载的进程数,多个进程能够更快地加载数据,并且当数据处理时间足够长时,流水线数据加载几乎可以完全隐藏数据加载延迟。应该设置此参数,以使从磁盘读取批处理数据的速度比GPU处理当前批处理的速度更快(但不能更高,因为这只会浪费多个进程使用的系统资源))
  2. 将数据从可分页内存传输到主机上的固定内存。(pin_memory
  3. 将数据从固定内存传输到GPU
  4. 在GPU上向前和向后传递

参考资料

Pytorch中多GPU并行计算教程_霹雳吧啦Wz-CSDN博客_pytorch 多gpu并行训练

pytorch(分布式)数据并行个人实践总结——DataParallel/DistributedDataParallel - fnangle - 博客园

Pytorch DDP分布式训练学习笔记 - 知乎


你可能感兴趣的:(pytorch,深度学习,分布式)