前言

随着模型训练需要使用多卡(GPUs)的性能,为此本文记录了关于分布式训练DP和DDP方式。详细介绍了从流程讲解到如何在代码中使用。


提示:以下是本篇文章正文内容,下面案例可供参考

一、DP(DataParallel)

DP是单进程多线程实现方式。
在这里插入图片描述
假设参数量为M,节点数为N.
GUP0:
传入梯度为:(N-1)M
传出参数为:(N-1)M
其他GPU:
传出梯度为M
传入参数为M
缺点:
单进程,多线程,Python GIL只能利用一个CUP核。
GPU0负责收集梯度,更新参数,同步参数。通信,计算压力大。

二、DP使用

# 以yolov5为例
if cuda and RANK == -1 and torch.cuda.device_count()>1:
	LOGGER.warning('WARNING: ......')
	model = torch.nn.DataParallel(model)
# 以自己搭建模型为例
import torch
from torch import nn
from Net import model_

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
if torch.cuda.device_count()>1:
	model = torch.nn.DataParallel(model_)

三、DDP(DistributedDataParallel)

DDP是多进程工作方式,针对多机多卡(也包括单机多卡)。与DP不同的是,不需要全程使用主GPU来汇总和分发数据。
在这里插入图片描述
传递数据,求和,形成一个闭环。使得每张卡同时再收发数据。
Scatter-Reduce:通过分发让每个节点有不同的值,并计算求和。
在这里插入图片描述
在这里插入图片描述
AllGather:使得每张卡上有相同的梯度数据。
在这里插入图片描述
总体流程:
在这里插入图片描述
DDP总结:
各个GPU独立从硬盘加载输入数据,加载相同的模型,数据均匀划分;
各个GPU独立的进行前向传播,损失计算,反向传播;
每两个相邻的GOU进行梯度的汇总和分发;
各个GPU进行独立的更新参数;
核心:
分为多个桶,将前向传播梯度每满一个桶时候,在进行一次Ring-AllReduce;同时每个GPU仍在进行梯度计算。
在这里插入图片描述
在这里插入图片描述
当所有的桶都同步之后(梯度相同),每个GPU再使用各自的优化器等进行优化反向传播。
在这里插入图片描述

四、DDP使用

1.准备DDP环境
在使用DDP训练时,首先要初始化一下DDP环境,设置后通信后端,进程组等。如下代码所示:

def setup(rank, world_size):
	# MASTER_ADDR指定负责协调初始化过程的主机地址,这是表示单机多GPU的所有进程都将连接到本地机器上
	os.environ['MASTER_ADDR'] = 'localhost'
	#MASTER_PORT制定了主机监听的端口号,用于进程间通信。
	os.environ['MASTER_PORT'] = '123456'
	#初始化分布式进程组。rank是当前进程再进程组中的编号,world_size是总进程数(GPU数量)即进程组大小。
	dist.init_process_grop(backend=('nccl' if dist.is_nccl_available() else 'gloo'), rank=rank, world_size=world_size)

2.准备数据加载器

def get_loader(trainset, testset, batch_size, rank, world_size):
    train_sampler = DistributedSampler(train_set, num_replicas=world_size, rank=rank)
	train_loader = DataLoader(train_set, batch_size=batch_size, sampler=train_sampler)
    # 对于测试集来说,可以选择使用DistributedSampler,也可以选择不使用,这里选择使用
    test_sampler = DistributedSampler(test_set, num_replicas=world_size, rank=rank)
	test_loader = DataLoader(test_set, batch_size=batch_size, sampler=train_sampler)
    # 不使用的代码很简单, 如下所示
    # test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False)
    rerturn train_loader, test_loader

3.准备DDP模型和优化器
在定义好模型之后,需要再所有进程中复制模型并用DDP封装,代码如下:

def prepare_model_and_optimizer(model, rank, lr):
    # 设置设备为当前进程的GPU。这里`rank`代表当前进程的编号,
    # `cuda:{rank}` 指定模型应该运行在对应编号的GPU上。
    device = torch.device(f"cuda:{rank}") 
    # 包装模型以使用分布式数据并行。DDP将在多个进程间同步模型的参数,
    # 并且只有指定的`device_ids`中的GPU才会被使用。
    model = model.to(device)
    model = DDP(model, device_ids=[rank])
    optimizer = torch.optim.SGD(model.parameters(), lr=lr)
    return model, optimizer

DDP中不同进程之间指挥同步梯度,为了确保训练时的参数同步,需要再训练开始前确保不同进程上模型和优化器的初始状态相同。确保模型的初始状态相同有如下两种方式:
1)参数初始化方法
每个GPU上都会有一个模型,为此利用统一的初始化方法,确保不同GPU上的参数统一性。

def weights_init(m):
    if isinstance(m, nn.Conv2d):
        nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
    elif isinstance(m, nn.Linear):
        nn.init.xavier_uniform_(m.weight)
        m.bias.data.fill_(0.01)
torch.manual_seed(42)  # 设置随机种子以确保可重复性
# 设置所有GPU的随机种子
torch.cuda.manual_seed_all(42)
model = MyModel()
model.apply(weights_init)

2)加载相同的与训练权重

model = MyModel()
model.load_state_dict(torch.load("path_to_weights.pth"))

注:如果没有设置初始化方法同时也没有模型权重。一个可行方法是手动同步,将rank=0的进程上模型文件临时保存,然后其他进程加载,最后删除临时文件。

def synchronize_model(model, rank, root='temp_model.pth'):
    if rank == 0:
        # 保存模型到文件
        torch.save(model.state_dict(), root)
    torch.distributed.barrier()  # 等待rank=0保存模型

    if rank != 0:
        # 加载模型权重
        model.load_state_dict(torch.load(root))
    torch.distributed.barrier()  # 确保所有进程都加载了模型

    if rank == 0:
        # 删除临时文件
        os.remove(root)

模型同步可以省略,再使用torch.nn.parallel.DistributeDataParallel封装模型时,它会在内部处理所需的同步操作。
4.开始训练
训练时的代码,和单卡没有什么区别。最主要的就是再每个epoch开始的时候,要设置一下sampler的epoch,确保每个epoch的残阳数据的顺序是不一样的。

def train(model, optimizer, criterion, rank, train_loader, num_epochs):
    sampler = train_loader.sampler
    for epoch in range(num_epochs):
        # 在每个epoch开始时更新sampler
        sampler.set_epoch(epoch)
        model.train()
        for batch_idx, (data, targets) in enumerate(dataloader):
            data, targets = data.cuda(rank), targets.cuda(rank)
            optimizer.zero_grad()
            outputs = model(data)
            loss = criterion(outputs, targets)
            loss.backward()
            optimizer.step()
            # 只在rank为0的进程中打印信息
            if rank == 0 and batch_idx % 100 == 0:
                print(f"Epoch {epoch}, Batch {batch_idx}, Loss: {loss.item()}")

注:这里打印的loss只是rank0上的loss,如果要打印所有卡上的平均loss,需要使用all_reduce方法。

	# 将损失从所有进程中收集起来并求平均
    # 创建一个和loss相同的tensor,用于聚合操作
    reduced_loss = torch.tensor([loss.item()]).cuda(rank)
    # all_reduce操作默认是求和
    dist.all_reduce(reduced_loss)
    # 求平均
    reduced_loss = reduced_loss / dist.get_world_size()

    # 只在rank为0的进程中打印信息
    if rank == 0 and batch_idx % 100 == 0:
        print(f"Epoch {epoch}, Batch {batch_idx}, Average Loss: {reduced_loss.item()}")

评估代码如果使用了DistributedSampler,计算指标时,需要gather每个进程上的preds和gts,然后计算全局指标。如果没有设置DistributedSampler,和单卡代码一致即可。

def evaluate(model, test_loader, rank):
    model.eval()
    total_preds = []
    total_targets = []

    with torch.no_grad():
        for data, targets in test_loader:
            data, targets = data.to(rank), targets.to(rank)
            outputs = model(data)
            _, preds = torch.max(outputs, 1)

            # 收集当前进程的结果
            total_preds.append(preds)
            total_targets.append(targets)

    # 将所有进程的preds和targets转换为全局列表
    total_preds = torch.cat(total_preds).cpu()
    total_targets = torch.cat(total_targets).cpu()

    # 使用all_gather将所有进程的数据集中到一个列表中
    gathered_preds = [torch.zeros_like(total_preds) for _ in range(dist.get_world_size())]
    gathered_targets = [torch.zeros_like(total_targets) for _ in range(dist.get_world_size())]
    
    dist.all_gather(gathered_preds, total_preds)
    dist.all_gather(gathered_targets, total_targets)

    if rank == 0:
        # 只在一个进程中进行计算和输出
        gathered_preds = torch.cat(gathered_preds)
        gathered_targets = torch.cat(gathered_targets)
        
        # 计算全局性能指标
        accuracy = (gathered_preds == gathered_targets).float().mean()
        print(f'Global Accuracy: {accuracy.item()}')

总结

参考文章:DDP

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐