PyTorch单机多卡分布式训练教程及代码示例

导师不是很懂PyTorch的分布式训练流程,我就做了个PyTorch单机多卡的分布式训练介绍,但是他觉得我做的没这篇好PyTorch分布式训练简明教程 - 知乎。这篇讲的确实很好,不过我感觉我做的也还可以,希望大家看完之后能给我一些建议。

目录

1.预备知识

1.1 主机(Host),节点(Node),进程(Process)和工作结点(Worker)。

1.2 World,Rank,Local Rank

1.2.1 World

1.2.2 Rank

1.2.3 Local Rank

2. PyTorch单机多卡数据并行

2.1 多进程启动

2.1.1 多进程启动示例

2.2 启动进程间通信

2.2.1 初始化成功示例

2.2.2 初始化失败示例

2.2.3 进程间通信示例

2.3. 单机多卡数据并行示例

后记:如何拓展到多机多卡?


1.预备知识

多卡训练涉及到多进程和进程间通信,因此有必要先解释一些进程间通信的概念。

1.1 主机(Host),节点(Node),进程(Process)和工作结点(Worker)。

众所周知,每个主机都可以同时运行多个进程,但是在通常情况下每个进程都是做各自的事情,各个进程间是没有关系的。

而在MPI中,我们可以拥有一组能够互相发消息的进程,但是这些进程可以分布在多个主机中,这时我们可以将主机称为节点(Node),进程称为工作结点(Worker)。

由于PyTorch中的主要说法还是进程,所以后面也会统一采用主机和进程的说法。

1.2 World,Rank,Local Rank

对于一组能够互相发消息的进程,我们需要区分每一个进程,因此每个进程会被分配一个序号,称作rank。进程间可以通过指定rank来进行通信。

1.2.1 World

World可以认为是一个集合,由一组能够互相发消息的进程组成。

如下图中假如Host 1的所有进程和Host 2的所有进程都可以进行通信,那么它们就组成了一个World。

因此,world size就表示这组能够互相通信的进程的总数,上图中world size为6。

1.2.2 Rank

Rank可以认为是这组能够互相通信的进程在World中的序号。

1.2.3 Local Rank

Local Rank可以认为是这组能够互相通信的进程在它们相应主机(Host)中的序号。

即在每个Host中,Local rank都是从0开始。

2. PyTorch单机多卡数据并行

数据并行本质上就是增大模型的batch size,但batch size也不是越大越好,所以一般对于大模型才会使用数据并行。

Pytorch进行数据并行主要依赖于它的两个模块multiprocessing和distributed。

所以首先介绍multiprocessing和distributed模块的基本用法。

2.1 多进程启动

由于Python多线程存在GIL(全局解释器锁),为了提高效率,Pytorch实现了一个multiprocessing多进程模块。用于在一个Python进程中启动额外的进程。

2.1.1 多进程启动示例

该程序启动了4个进程,每个进程会输出当前rank,表明与其他进程不同。

#run_multiprocess.py
#运行命令:python run_multiprocess.py
import torch.multiprocessing as mp

def run(rank, size):
    print("world size:{}. I'm rank {}.".format(size,rank))


if __name__ == "__main__":
    world_size = 4
    mp.set_start_method("spawn")
    #创建进程对象
    #target为该进程要运行的函数,args为target函数的输入参数
    p0 = mp.Process(target=run, args=(0, world_size))
    p1 = mp.Process(target=run, args=(1, world_size))
    p2 = mp.Process(target=run, args=(2, world_size))
    p3 = mp.Process(target=run, args=(3, world_size))

    #启动进程
    p0.start()
    p1.start()
    p2.start()
    p3.start()

    #当前进程会阻塞在join函数,直到相应进程结束。
    p0.join()
    p1.join()
    p2.join()
    p3.join()

输出结果:

world size:4. I'm rank 1.
world size:4. I'm rank 0.
world size:4. I'm rank 2.
world size:4. I'm rank 3.

2.2 启动进程间通信

虽然启动了多进程,但是此时进程间并不能进行通信,所以PyTorch设计了另一个distributed模块用于进程间通信。

init_process_group函数是distributed模块用于初始化通信模块的函数。

当该函数初始化成功则表明进程间可以进行通信。

2.2.1 初始化成功示例

只有当world size和实际启动的进程数匹配,init_process_group才可以初始化成功。

#multiprocess_comm.py
#运行命令:python multiprocess_comm.py

import os
import torch.distributed as dist
import torch.multiprocessing as mp

def run(rank, size):
    #MASTER_ADDR和MASTER_PORT是通信模块初始化需要的两个环境变量。
    #由于是在单机上,所以用localhost的ip就可以了。
    os.environ['MASTER_ADDR'] = '127.0.0.1'
    #端口可以是任意空闲端口
    os.environ['MASTER_PORT'] = '29500'
    #通信模块初始化
    #进程会阻塞在该函数,直到确定所有进程都可以通信。
    dist.init_process_group('gloo', rank=rank, world_size=size)
    print("world size:{}. I'm rank {}.".format(size,rank))


if __name__ == "__main__":
    world_size = 4
    mp.set_start_method("spawn")
    #创建进程对象
    #target为该进程要运行的函数,args为函数的输入参数
    p0 = mp.Process(target=run, args=(0, world_size))
    p1 = mp.Process(target=run, args=(1, world_size))
    p2 = mp.Process(target=run, args=(2, world_size))
    p3 = mp.Process(target=run, args=(3, world_size))

    #启动进程
    p0.start()
    p1.start()
    p2.start()
    p3.start()

    #等待进程结束
    p0.join()
    p1.join()
    p2.join()
    p3.join()

输出结果:

world size:4. I'm rank 1.
world size:4. I'm rank 0.
world size:4. I'm rank 2.
world size:4. I'm rank 3.

2.2.2 初始化失败示例

当将world size设置为2,但是实际却启动了4个进程,此时init_process_group就会报错。

#multiprocess_comm.py
#运行命令:python multiprocess_comm.py

import os
import torch.distributed as dist
import torch.multiprocessing as mp

def run(rank, size):
    #MASTER_ADDR和MASTER_PORT是通信模块初始化需要的两个环境变量。
    #由于是在单机上,所以用localhost的ip就可以了。
    os.environ['MASTER_ADDR'] = '127.0.0.1'
    #端口可以是任意空闲端口
    os.environ['MASTER_PORT'] = '29500'
    #通信模块初始化
    #进程会阻塞在该函数,直到确定所有进程都可以通信。
    dist.init_process_group('gloo', rank=rank, world_size=size)
    print("world size:{}. I'm rank {}.".format(size,rank))


if __name__ == "__main__":
    world_size = 2
    mp.set_start_method("spawn")
    #创建进程对象
    #target为该进程要运行的函数,args为target函数的输入参数
    p0 = mp.Process(target=run, args=(0, world_size))
    p1 = mp.Process(target=run, args=(1, world_size))
    p2 = mp.Process(target=run, args=(2, world_size))
    p3 = mp.Process(target=run, args=(3, world_size))

    #启动进程
    p0.start()
    p1.start()
    p2.start()
    p3.start()

    #当前进程会阻塞在join函数,直到相应进程结束。
    p0.join()
    p1.join()
    p2.join()
    p3.join()

输出结果:

RuntimeError: [enforce fail at /opt/conda/conda-bld/pytorch_1623448224956/work/third_party/gloo/gloo/context.cc:27] rank < size. 3 vs 2

2.2.3 进程间通信示例

当init_process_group初始化成功,进程间就可以进行通信了,这里我以集体通信Allreduce为例。

#multiprocess_allreduce.py
#运行命令:python multiprocess_allreduce.py

import os
import torch
import torch.distributed as dist
import torch.multiprocessing as mp

def run(rank, size):
    os.environ['MASTER_ADDR'] = '127.0.0.1'
    os.environ['MASTER_PORT'] = '29500'
    #通信模块初始化
    #进程会阻塞在该函数,直到确定所有进程都可以通信。
    dist.init_process_group('gloo', rank=rank, world_size=size)
    #每个进程都创建一个Tensor,Tensor值为该进程相应rank。
    param = torch.tensor([rank])
    print("rank {}: tensor before allreduce: {}".format(rank,param))
    #对该Tensor进行Allreduce。
    dist.all_reduce(param.data, op=dist.ReduceOp.SUM)
    print("rank {}: tensor after allreduce: {}".format(rank,param))


if __name__ == "__main__":
    world_size = 4
    mp.set_start_method("spawn")
    #创建进程对象
    #target为该进程要运行的函数,args为target函数的输入参数
    p0 = mp.Process(target=run, args=(0, world_size))
    p1 = mp.Process(target=run, args=(1, world_size))
    p2 = mp.Process(target=run, args=(2, world_size))
    p3 = mp.Process(target=run, args=(3, world_size))

    #启动进程
    p0.start()
    p1.start()
    p2.start()
    p3.start()

    #当前进程会阻塞在join函数,直到相应进程结束。
    p0.join()
    p1.join()
    p2.join()
    p3.join()

输出结果:

rank 0: tensor before allreduce: tensor([0])
rank 2: tensor before allreduce: tensor([2])
rank 3: tensor before allreduce: tensor([3])
rank 1: tensor before allreduce: tensor([1])

rank 0: tensor after allreduce: tensor([6])
rank 3: tensor after allreduce: tensor([6])
rank 2: tensor after allreduce: tensor([6])
rank 1: tensor after allreduce: tensor([6])

2.3. 单机多卡数据并行示例

当可以启动多进程,并进行进程间通信后,实际上就已经可以进行单机多卡的分布式训练了。

但是Pytorch为了便于用户使用,所以在这之上又增加了很多更高层的封装,如DistributedDataParallel,DistributedSampler等。

所以为了便于理解这中间的一些流程,这里演示一下不使用这些封装时的单机多卡数据并行。

该示例代码和单机训练主要有两个区别:

(1)需要基于每个进程的rank将模型参数放置到不同的GPU。

(2) 在参数更新前需要对梯度进行Allreduce。

#multiprocess_training.py
#运行命令:python multiprocess_training.py
import os
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
#用于平均梯度的函数
def average_gradients(model):
    size = float(dist.get_world_size())
    for param in model.parameters():
        dist.all_reduce(param.grad.data, op=dist.ReduceOp.SUM)
        param.grad.data /= size
#模型
class ConvNet(nn.Module):
    def __init__(self, num_classes=10):
        super(ConvNet, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2),
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2))
        self.layer2 = nn.Sequential(
            nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2))
        self.fc = nn.Linear(7*7*32, num_classes)
    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = out.reshape(out.size(0), -1)
        out = self.fc(out)
        return out
    
def accuracy(outputs,labels):
    _, preds = torch.max(outputs, 1) # taking the highest value of prediction.
    correct_number = torch.sum(preds == labels.data)
    return (correct_number/len(preds)).item()


def run(rank, size):
    #MASTER_ADDR和MASTER_PORT是通信模块初始化需要的两个环境变量。
    #由于是在单机上,所以用localhost的ip就可以了。
    os.environ['MASTER_ADDR'] = '127.0.0.1'
    #端口可以是任意空闲端口
    os.environ['MASTER_PORT'] = '29500'
    dist.init_process_group('gloo', rank=rank, world_size=size)

    #1.数据集预处理
    train_dataset = torchvision.datasets.MNIST(root='../data',
                                               train=True,
                                               transform=transforms.ToTensor(),
                                               download=True)
    training_loader = torch.utils.data.DataLoader(train_dataset, batch_size=100, shuffle=True)

    #2.搭建模型
    #device = torch.device("cuda:{}".format(rank))
    device = torch.device("cpu")
    print(device)
    torch.manual_seed(0)
    model = ConvNet().to(device)
    torch.manual_seed(rank)
    criterion = nn.CrossEntropyLoss() 
    optimizer = torch.optim.SGD(model.parameters(), lr = 0.001,momentum=0.9) # fine tuned the lr
    #3.开始训练
    epochs = 15
    batch_num = len(training_loader)
    running_loss_history = []
    for e in range(epochs):
        for i,(inputs, labels) in enumerate(training_loader):
            inputs = inputs.to(device) 
            labels = labels.to(device)
            #前向传播
            outputs = model(inputs) 
            loss = criterion(outputs, labels) 
            optimizer.zero_grad() 
            #反传
            loss.backward() 
            #记录loss
            running_loss_history.append(loss.item())
            #参数更新前需要Allreduce梯度。
            average_gradients(model)
            #参数更新
            optimizer.step() 
            if (i + 1) % 50 == 0 and rank == 0:
                print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f},acc:{:.2f}'.format(e + 1, epochs, i + 1, batch_num,loss.item(),accuracy(outputs,labels)))


if __name__ == "__main__":
    world_size = 4
    mp.set_start_method("spawn")
    #创建进程对象
    #target为该进程要运行的函数,args为target函数的输入参数
    p0 = mp.Process(target=run, args=(0, world_size))
    p1 = mp.Process(target=run, args=(1, world_size))
    p2 = mp.Process(target=run, args=(2, world_size))
    p3 = mp.Process(target=run, args=(3, world_size))

    #启动进程
    p0.start()
    p1.start()
    p2.start()
    p3.start()

    #当前进程会阻塞在join函数,直到相应进程结束。
    p0.join()
    p1.join()
    p2.join()
    p3.join()

后记:如何拓展到多机多卡?

在多机多卡环境中初始化init_process_group还需要做一些额外的处理,主要考虑两个问题

(1)需要让其余进程知道rank=0进程的 IP:Port 地址,此时rank=0进程会在相应端口进行监听,其余进程则会给这个IP:Port发消息。这样rank=0进程就可以进行统计,确认初始化是否成功。这一步在PyTorch中是通过设置os.environ['MASTER_ADDR']和os.environ['MASTER_PORT']这两个环境变量来做的。

(2)需要为每个进程确定相应rank,通常采用的做法是给主机编号,因此多机多卡启动时给不同主机传入的参数肯定是不同的。此时参数可以直接手动在每个主机的代码上修改,也可以通过argparse模块在运行时传递不同参数来做。


 

  • 22
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
PyTorch支持多卡分布式训练,可以利用多个GPU进行模型的并行计算。下面是一个简单的示例代码,展示了如何使用PyTorch进行多卡分布式训练: ```python import torch import torch.nn as nn import torch.optim as optim import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel # 初始化进程组 dist.init_process_group(backend='nccl') # 设置设备和参数 device = torch.device('cuda') batch_size = 64 learning_rate = 0.01 num_epochs = 10 # 定义模型 model = nn.Linear(10, 1).to(device) model = DistributedDataParallel(model) # 定义数据加载器 train_dataset = ... train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset) train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, sampler=train_sampler) # 定义损失函数和优化器 criterion = nn.MSELoss() optimizer = optim.SGD(model.parameters(), lr=learning_rate) # 训练模型 for epoch in range(num_epochs): for inputs, labels in train_loader: inputs = inputs.to(device) labels = labels.to(device) outputs = model(inputs) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() # 清理进程组 dist.destroy_process_group() ``` 在这个示例中,我们首先使用`dist.init_process_group`初始化进程组,然后将模型包装在`DistributedDataParallel`中以实现多卡并行计算。接下来,我们创建了一个数据加载器,并使用`DistributedSampler`进行数据的分布式采样。然后,我们定义了损失函数和优化器,并在每个epoch内进行训练。最后,我们使用`dist.destroy_process_group`清理进程组。 请注意,这只是一个简单的示例,实际的多卡分布式训练可能还涉及到其他内容,如数据并行、模型的同步等。具体的实现方式可能因具体的情况而有所不同,请根据自己的需求进行相应的调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值