Pytorch GPU多卡并行训练实战总结(附代码)

来源 l 记忆的迷谷 出品 l 对白的算法屋
今天分享给大家一份Pytorch GPU多卡并行训练实战细节总结。
为什么要使用多GPU并行训练?
简单来说,有两种原因:第一种是模型在一块GPU上放不下,两块或多块GPU上就能运行完整的模型(如早期的AlexNet)。第二种是多块GPU并行计算可以达到加速训练的效果。想要成为“炼丹大师“,多GPU并行训练是不可或缺的技能。
常见的多GPU训练方法:


误差梯度如何在不同设备之间通信?
BN如何在不同设备之间同步?

两种GPU训练方法:DataParallel 和 DistributedDataParallel:
DataParallel是单进程多线程的,仅仅能工作在单机中。而DistributedDataParallel是多进程的,可以工作在单机或多机器中。 DataParallel通常会慢于DistributedDataParallel。所以目前主流的方法是DistributedDataParallel。
pytorch中常见的GPU启动方式:

def init_distributed_mode(args):# 如果是多机多卡的机器,WORLD_SIZE代表使用的机器数,RANK对应第几台机器# 如果是单机多卡的机器,WORLD_SIZE代表有几块GPU,RANK和LOCAL_RANK代表第几块GPUif'RANK'in os.environ and'WORLD_SIZE'in os.environ:args.rank = int(os.environ["RANK"])args.world_size = int(os.environ['WORLD_SIZE'])# LOCAL_RANK代表某个机器上第几块GPUargs.gpu = int(os.environ['LOCAL_RANK'])elif'SLURM_PROCID'in os.environ:args.rank = int(os.environ['SLURM_PROCID'])args.gpu = args.rank % torch.cuda.device_count()else:print('Not using distributed mode')args.distributed = Falsereturnargs.distributed = Truetorch.cuda.set_device(args.gpu) # 对当前进程指定使用的GPUargs.dist_backend = 'nccl'# 通信后端,nvidia GPU推荐使用NCCLdist.barrier() # 等待每个GPU都运行完这个地方以后再继续
def main(args):if torch.cuda.is_available() isFalse:raise EnvironmentError("not find GPU device for training.")# 初始化各进程环境=args)rank = args.rankdevice = torch.device(args.device)batch_size = args.batch_sizenum_classes = args.num_classesweights_path = args.weights*= args.world_size # 学习率要根据并行GPU的数倍增
#给每个rank对应的进程分配训练的样本索引train_sampler=torch.utils.data.distributed.DistributedSampler(train_data_set)val_sampler=torch.utils.data.distributed.DistributedSampler(val_data_set)#将样本索引每batch_size个元素组成一个listtrain_batch_sampler=torch.utils.data.BatchSampler(train_sampler,batch_size,drop_last=True)


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)
model = resnet34(num_classes=num_classes).to(device)if os.path.exists(weights_path):weights_dict = torch.load(weights_path, map_location=device)load_weights_dict = {k: v for k, v in weights_dict.items()if model.state_dict()[k].numel() == v.numel()}model.load_state_dict(load_weights_dict, strict=False)else:checkpoint_path = os.path.join(tempfile.gettempdir(), "initial_weights.pt")if rank == 0:torch.save(model.state_dict(), checkpoint_path)dist.barrier()model.load_state_dict(torch.load(checkpoint_path, map_location=device))
# 是否冻结权重if args.freeze_layers:for name, para in model.named_parameters():# 除最后的全连接层外,其他权重全部冻结if"fc"notin name:para.requires_grad_(False)else:# 只有训练带有BN结构的网络时使用SyncBatchNorm采用意义if args.syncBN:# 使用SyncBatchNorm后训练会更耗时model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)# 转为DDP模型model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu])# optimizer使用SGD+余弦淬火策略pg = [p for p in model.parameters() if p.requires_grad]optimizer = optim.SGD(pg, lr=args.lr, momentum=0.9, weight_decay=0.005)lf = lambda x: ((1 + math.cos(x * math.pi / args.epochs)) / 2) * (1 - args.lrf) + args.lrf # cosinescheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)
for epoch in range(args.epochs):mean_loss = train_one_epoch(model=model,optimizer=optimizer,data_loader=train_loader,device=device,epoch=epoch)scheduler.step()sum_num = evaluate(model=model,data_loader=val_loader,device=device)acc = sum_num / val_sampler.total_size
def train_one_epoch(model, optimizer, data_loader, device, epoch):model.train()loss_function = torch.nn.CrossEntropyLoss()mean_loss = torch.zeros(1).to(device)optimizer.zero_grad()if is_main_process():data_loader = tqdm(data_loader)for step, data in enumerate(data_loader):images, labels = datapred = model(images.to(device))loss = loss_function(pred, labels.to(device))loss.backward()loss = reduce_value(loss, average=True)mean_loss = (mean_loss * step + loss.detach()) / (step + 1)if is_main_process():data_loader.desc = "[epoch {}] mean loss {}".format(epoch, round(mean_loss.item(), 3))ifnot torch.isfinite(loss):print('WARNING: non-finite loss, ending training ', loss)sys.exit(1)optimizer.step()optimizer.zero_grad()if device != torch.device("cpu"):torch.cuda.synchronize(device)return mean_loss.item()def reduce_value(value, average=True):world_size = get_world_size()if world_size < 2:return valuewith torch.no_grad():dist.all_reduce(value)if average:value /= world_sizereturn value
@torch.no_grad()def evaluate(model, data_loader, device):model.eval()# 用于存储预测正确的样本个数,每块GPU都会计算自己正确样本的数量sum_num = torch.zeros(1).to(device)# 在进程0中打印验证进度if is_main_process():data_loader = tqdm(data_loader)for step, data in enumerate(data_loader):images, labels = datapred = model(images.to(device))pred = torch.max(pred, dim=1)[1]sum_num += torch.eq(pred, labels.to(device)).sum()# 等待所有进程计算完毕if device != torch.device("cpu"):torch.cuda.synchronize(device)sum_num = reduce_value(sum_num, average=False) # 预测正确样本个数return sum_num.item()
if rank == 0:print("[epoch {}] accuracy: {}".format(epoch, round(acc, 3)))tags = ["loss", "accuracy", "learning_rate"]tb_writer.add_scalar(tags[0], mean_loss, epoch)tb_writer.add_scalar(tags[1], acc, epoch)tb_writer.add_scalar(tags[2], optimizer.param_groups[0]["lr"], epoch)torch.save(model.module.state_dict(), "./weights/model-{}.pth".format(epoch))
if rank == 0:
分享
收藏
点赞
在看

评论
