李沐动手学深度学习V2-多GPU数据并行内容和手动实现代码

本文详细介绍了如何在深度学习中利用多GPU实现数据并行训练,包括LeNet网络实例、参数同步与数据拆分策略,以及在PyTorch框架中的代码实现。通过比较不同并行方式的优缺点,强调了数据并行的简单性和效率提升。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.多GPU并行

1.1 多GPU并行方式

  1. 模型并行:在多个GPU之间拆分网络。 也就是说,每个GPU将流入特定层的数据作为输入,跨多个后续层对数据进行处理,然后将数据发送到下一个GPU。 与单个GPU所能处理的数据相比,我们可以用更大的网络处理数据。 此外,每个GPU占用的显存(memory footprint)可以得到很好的控制,虽然它只是整个网络显存的一小部分。比如前五个层用第一个GPU计算,后面五个层用第二个GPU计算
    缺点:GPU的接口之间需要的密集同步可能是很难办的,特别是层之间计算的工作负载不能正确匹配的时候, 还有层之间的接口需要大量的数据传输的时候(例如:激活值和梯度,数据量可能会超出GPU总线的带宽)。 此外,计算密集型操作的顺序对于拆分来说也是非常重要的,这仍然是一个困难的问题,目前还不清楚研究是否能在特定问题上实现良好的线性缩放。 综上所述,除非存框架或操作系统本身支持将多个GPU连接在一起,否则不建议这种方法。
  2. 通道并行:拆分层内的工作。 例如,将问题分散到 4 个GPU,每个GPU生成 16 个通道的数据,而不是在单个GPU上计算 64 个通道, 对于全连接的层,同样可以拆分输出单元的数量。以前通常用于处理显存非常小(当时为2GB)的GPU,当通道或单元的数量不太小时,使计算性能有良好的提升。 此外,由于可用的显存呈线性扩展,多个GPU能够处理不断变大的网络。
    缺点:同样需要大量的同步或屏障操作(barrier operation),因为每一层都依赖于所有其他层的结果。 此外,需要传输的数据量也可能比跨GPU拆分层时还要大。 因此,基于带宽的成本和复杂性,我们同样不推荐这种方法。
  3. 数据并行:跨多个GPU对数据进行拆分。 这种方式下,所有GPU尽管有不同的观测结果,但是执行着相同类型的工作。 在完成每个小批量数据的训练之后,梯度在GPU上聚合。 这种方法最简单,并可以应用于任何情况,同步只需要在每个小批量数据处理之后进行。 也就是说,当其他梯度参数仍在计算时,完成计算的梯度参数就可以开始交换。而且,GPU的数量越多,小批量包含的数据量就越大,从而就能提高训练效率。 但是,添加更多的GPU并不能让我们训练更大的模型,因为每个GPU仍然包含模型的全部权重参数和梯度等。 总体而言,只要GPU的显存足够大,数据并行是最方便的,三种方式如下图所示。
    多GPU并行的三种方式

1.2 详细介绍多GPU的数据并行

假设一台机器有 𝑘 个GPU。 给定需要训练的模型,虽然每个GPU上的参数值都是相同且同步的,但是每个GPU都将独立地维护一组完整的模型参数,如下图所示当GPU为两个 时基于数据并行方法训练模型。
两个GPU进行数据并行
一般来说, 𝑘 个GPU数据并行训练过程如下
1. 在任何一次训练迭代中,给定的随机的小批量样本都将被分成 𝑘 个部分,并均匀地分配到GPU上。
2. 每个GPU根据分配给它的小批量子集,计算模型的损失和参数的梯度
3. 将 𝑘 个GPU中的局部梯度聚合,以获得当前小批量的随机梯度
4. 聚合梯度被重新分发到每个GPU中
5. 每个GPU使用这个小批量随机梯度,来更新它所维护的完整的模型参数集

在实践中请注意,当在 𝑘 个GPU上训练时,需要扩大小批量的大小为 𝑘 的倍数,通常学习率也会扩大到原来的K倍,这样每个GPU都有相同的工作量,就像只在单个GPU上训练一样。 因此,在16-GPU服务器上可以显著地增加小批量数据量的大小,同时可能还需要相应地提高学习率。 注意,批量规范化也需要调整,例如,为每个GPU保留单独的批量规范化参数(对当前训练小批量数据的全局均值和方差)。

1.3 使用LeNet网络来演示多GPU数据并行训练

  1. 模型定义
import d2l.torch
import torch
from torch import nn
from torch.nn import functional as F
scale = 0.01
# 初始化模型参数
#自定义网络每个层初始化参数,注意在神经网络中创建tensor张量时,不需要requires_grad=True的属性(这个属性表示需要进行梯度计算),因为神经网络是自动求梯度,不能有requires_grad属性
#w1 = torch.randn(size=(20,1,3,3),requires_grad=True)*scale
w1 = torch.randn(size=(20,1,3,3))*scale
b1 = torch.zeros(20)
w2 = torch.randn(size=(50,20,5,5))*scale
b2 = torch.zeros(50)
w3 = torch.randn(size=(800,128))*scale
b3 = torch.zeros(128)*scale
w4 = torch.randn(size=(128,10))*scale
b4 = torch.zeros(10)*scale
params = [w1,b1,w2,b2,w3,b3,w4,b4]

#定义LeNet网络
def LeNet(X,params):
    l1_conv2d = F.conv2d(input=X,weight=params[0],bias=params[1])
    l1_relu = F.relu(input=l1_conv2d)
    l1_avgpool2d = F.avg_pool2d(input=l1_relu,kernel_size=2,stride=2)
    l2_conv2d = F.conv2d(input=l1_avgpool2d,weight=params[2],bias=params[3])
    l2_relu = F.relu(input=l2_conv2d)
    l2_avgpool2d = F.avg_pool2d(input=l2_relu,kernel_size=2,stride=2)
    l3_flatten = l2_avgpool2d.reshape(l2_avgpool2d.shape[0],-1)
    l4_linear = torch.mm(l3_flatten,params[4])+params[5]
    l4_relu = F.relu(input=l4_linear)
    l5_linear = torch.mm(l4_relu,params[6])+params[7]
    y_hat = l5_linear
    return y_hat
#损失函数
loss = nn.CrossEntropyLoss(reduction='none')
  1. 数据同步
    对于高效的多GPU并行训练,我们需要两个基本操作。 首先,我们需要向多个设备分发刚开始模型初始化的参数并包含梯度;第二,需要跨多个设备对梯度求和,然后再将求和的梯度广播到所有GPU上面,从而更新权重参数。
#将网络所有层的参数全部复制到device这个设备上,params是一个list类型,里面包含每一层的权重和偏差参数
def get_params(params,device):
    new_params = [ param.to(device) for param in params]
    #必须明确指出每层权重参数需要梯度
    for p in new_params:
        p.requires_grad_()
    return new_params
new_params = get_params(params,d2l.torch.try_gpu(0))
print('第一层偏差:',new_params[1])
print('第一层偏差梯度:',new_params[1].grad) #由于还没有进行任何计算,因此权重参数的梯度仍然为零。 
'''
输出结果:
第一层偏差: tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       device='cuda:0', requires_grad=True)
第一层偏差梯度: None
'''

假设现在有一个向量分布在多个GPU上,下面的allreduce()函数将所有向量相加,并将结果广播给所有GPU。 注意我们需要将数据复制到累积结果的设备,才能使函数正常工作。

#数据同步,data是一个list列表,表示将data中所有元素都复制到gpu0上面,然后在gpu0上面进行data中所有元素求和,然后再把求和的结果广播(复制)到其他设备上面,这里是对每个设备在每个批量中的子批量求出的梯度求和然后把求和的结果复制到所有gpu上面
def allreduce(data):
    for i in range(1,len(data)):
        data[0][:] += data[i].to(data[0].device)
    for i in range(1,len(data)):
        data[i][:] = data[0].to(data[i].device)

#data = [torch.ones(size=(1,2),device=d2l.torch.try_gpu(i))*(i+1) for i in range(1)]
#通过在不同设备上创建具有不同值的向量并聚合它们。
data1 = [torch.ones(size=(1,2),device=torch.device('cuda:0')),torch.ones(size=(1,2),device=torch.device('cpu'))]
print('allreduce之前:\n',data1[0],'\n',data1[1])
allreduce(data1)
print('allreduce之后:\n',data1[0],'\n',data1[1])
'''
输出结果:
allreduce之前:
 tensor([[1., 1.]], device='cuda:0') #张量在GPU0上面创建的
 tensor([[2., 2.]])#张量在CPU上面创建的
allreduce之后:
 tensor([[3., 3.]], device='cuda:0') #张量在GPU0上面创建的
 tensor([[3., 3.]])#张量在CPU上面创建的
'''
  1. 数据分发
    需要定义一个简单的工具函数,将一个小批量数据均匀地分布在多个GPU上。 例如,有两个GPU时,我们希望每个GPU可以复制一半的数据。 因为深度学习框架的内置函数编写代码更方便、更简洁,所以在 4×5 矩阵上使用它进行尝试。
#数据平均拆分到不同gpu上面
data = torch.arange(20).reshape(4,5)
#devices = [torch.device('cuda:0'),torch.device('cpu')]#本机只有一个GPU
devices = [torch.device('cuda:0')]
scatter = nn.parallel.scatter(data,devices)
print('data = ',data)
print('devices = ',devices)
print('scatter = ',scatter)

为了方便下面复用,我们定义了可以同时拆分数据和标签的split_batch函数。

#将小批量样本数据和标签平均拆分到不同gpu上面,让每个gpu处理每个小批量中子样本数据
def split_batch(X,y,devices):
    assert X.shape[0] == y.shape[0]
    return (nn.parallel.scatter(X,devices),nn.parallel.scatter(y,devices))
  1. 训练
    可以在一个小批量上实现多GPU训练。 在多个GPU之间同步数据将使用上面定义的函数allreduce()和split_batch()。 我们不需要编写任何特定的代码来实现并行性, 因为计算图在小批量内的设备之间没有任何依赖关系,因此它是“自动地”并行执行。
#将小批量样本用多个gpu并行训练
def train_batch(X,y,AllDevices_params,All_devices,lr):
    # 将小批量样本数据和标签平均拆分到不同gpu上面,让每个gpu处理每个小批量中子样本数据
    X_shareds,y_shareds = split_batch(X,y,All_devices)
    #每个gpu训练自己上面的子样本数据,然后对训练出来的结果与对应的标签求loss,然后再对子样本的每个样本的loss求和。ls是一个列表类型
    # 在每个GPU上分别计算损失
    ls = [loss(LeNet(X_shared,device_params),y_shared).sum() for X_shared,y_shared,device_params in zip(X_shareds,y_shareds,AllDevices_params)]
    for l in ls:# 反向传播在每个GPU上分别执行
        #对每个gpu上面的权重参数求梯度
        l.backward()
    # 将每个GPU的所有梯度相加,并将其广播到所有GPU
    with torch.no_grad():
        #遍历网络每一个层
        for i in range(len(AllDevices_params[0])):
            #将每个gpu中对应的层的梯度求和,通常在第一个gpu上面计算,然后再广播到所有gpu上面
            allreduce([AllDevices_params[c][i].grad for c in range(len(All_devices))])
    # 在每个GPU上分别更新模型参数
    for device_params in AllDevices_params:
        #将每个gpu上面的网络权重参数进行权重更新,因此达到了所有gpu上面的权重参数值都是相同的
        d2l.torch.sgd(device_params,lr,X.shape[0])
  1. 定义训练函数
    训练函数需要分配GPU并将所有模型参数复制到所有设备。 显然,每个小批量都是使用train_batch()函数来处理多个GPU。 测试数据时我们只在一个GPU上计算模型的精确度,而让其他GPU保持空闲,尽管这是相对低效的,但是使用方便且代码简洁。
def train(num_gpu,batch_size,lr):
    train_iter,test_iter = d2l.torch.load_data_fashion_mnist(batch_size)
    devices = [d2l.torch.try_gpu(i) for i in range(num_gpu)]
    #给每个gpu上面复制相同的网络权重参数
    # 将模型参数复制到num_gpu个GPU
    devices_params = [get_params(params,device)for device in devices]
    epochs = 10
    animator = d2l.torch.Animator('epoch','test acc',xlim=[1,epochs])
    timer = d2l.torch.Timer()
    for epoch in range(epochs):
        #处理一个epoch开始时间
        timer.start()
        for X,y in train_iter:
            # 为单个小批量执行多GPU训练
            train_batch(X,y,devices_params,devices,lr)
            torch.cuda.synchronize()
        #处理完一个epoch(一轮)所花的时间,计算训练完一遍数据集所花的时间
        timer.stop()
        #训练数据集每轮训练完后对测试数据集进行测试,输出测试精度大小
        # 在GPU0上评估模型
        animator.add(epoch+1,d2l.torch.evaluate_accuracy_gpu(lambda x : LeNet(x,devices_params[0]),test_iter,devices[0]))
    print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,在{str(devices)}')
  1. 在单个GPU上运行结果(批量大小是 256 ,学习率是 0.2,轮数epochs为10轮
train(1,256,0.2)#使用一个gpu设备,batch_size为256,lr为0.2
'''
补充:
#train(num_gpus=2, batch_size=256, lr=0.2)#在两个GPU上面并行
#结果:测试精度:0.81,2.8秒/轮,在[device(type='cuda', index=0), device(type='cuda', index=1)]
在两个GPU上面并行:保持批量大小和学习率不变,并增加为2个GPU,可以看到测试精度与之前的实验基本相同。 不同的GPU个数在算法寻优方面是相同的。 但是在这里没有任何有意义的加速:模型实在太小了;而且数据集也太小了,在这个数据集中,我们实现的多GPU训练的简单方法受到了巨大的Python开销的影响。 在未来,我们将遇到更复杂的模型和更复杂的并行化方法,使用多个GPU并行会有更明显的加速,GPU性能提升。 
'''

测试结果如下图所示
 在单个GPU上运行LeNet结果

1.4 小结

  1. 有多种方法可以在多个GPU上拆分深度网络的训练。拆分可以在层之间、跨层或跨数据上实现。前两者需要对数据传输过程进行严格编排,而最后一种则是最简单的策略。
  2. 数据并行训练本身是不复杂的,它通过增加有效的小批量数据量的大小提高了训练效率。
  3. 在数据并行中,数据需要跨多个GPU拆分,其中每个GPU执行自己的前向传播和反向传播,随后所有的梯度需要聚合(求和)在一起,之后聚合结果向所有的GPU广播。
  4. 小批量数据量更大时,学习率也需要稍微提高一些。

2. 多GPU数据并行全部代码

import d2l.torch
import torch
from torch import nn
from torch.nn import functional as F
scale = 0.01
#自定义网络每个层初始化参数,注意在神经网络中创建tensor张量时,不需要requires_grad=True的属性(这个属性表示需要进行梯度计算),因为神经网络是自动求梯度,不能有requires_grad属性
#w1 = torch.randn(size=(20,1,3,3),requires_grad=True)*scale
w1 = torch.randn(size=(20,1,3,3))*scale
b1 = torch.zeros(20)
w2 = torch.randn(size=(50,20,5,5))*scale
b2 = torch.zeros(50)
w3 = torch.randn(size=(800,128))*scale
b3 = torch.zeros(128)*scale
w4 = torch.randn(size=(128,10))*scale
b4 = torch.zeros(10)*scale
params = [w1,b1,w2,b2,w3,b3,w4,b4]

#定义LeNet网络
def LeNet(X,params):
    l1_conv2d = F.conv2d(input=X,weight=params[0],bias=params[1])
    l1_relu = F.relu(input=l1_conv2d)
    l1_avgpool2d = F.avg_pool2d(input=l1_relu,kernel_size=2,stride=2)
    l2_conv2d = F.conv2d(input=l1_avgpool2d,weight=params[2],bias=params[3])
    l2_relu = F.relu(input=l2_conv2d)
    l2_avgpool2d = F.avg_pool2d(input=l2_relu,kernel_size=2,stride=2)
    l3_flatten = l2_avgpool2d.reshape(l2_avgpool2d.shape[0],-1)
    l4_linear = torch.mm(l3_flatten,params[4])+params[5]
    l4_relu = F.relu(input=l4_linear)
    l5_linear = torch.mm(l4_relu,params[6])+params[7]
    y_hat = l5_linear
    return y_hat
loss = nn.CrossEntropyLoss(reduction='none')
#将网络所有层的参数全部复制到device这个设备上,params是一个list类型,里面包含每一层的权重和偏差参数
def get_params(params,device):
    new_params = [ param.to(device) for param in params]
    #必须明确指出每层权重参数需要梯度
    for p in new_params:
        p.requires_grad_()
    return new_params
new_params = get_params(params,d2l.torch.try_gpu(0))
print('第一层偏差:',new_params[1])
print('第一层偏差梯度:',new_params[1].grad)

#数据同步,data是一个list列表,表示将data中所有元素都复制到gpu0上面,然后在gpu0上面进行data中所有元素求和,然后再把求和的结果广播(复制)到其他设备上面,这里是对每个设备在每个批量中的子批量求出的梯度求和然后把求和的结果复制到所有gpu上面
def allreduce(data):
    for i in range(1,len(data)):
        data[0][:] += data[i].to(data[0].device)
    for i in range(1,len(data)):
        data[i][:] = data[0].to(data[i].device)

#data = [torch.ones(size=(1,2),device=d2l.torch.try_gpu(i))*(i+1) for i in range(1)]
data1 = [torch.ones(size=(1,2),device=torch.device('cuda:0')),torch.ones(size=(1,2),device=torch.device('cpu'))]
print('allreduce之前:\n',data1[0],'\n',data1[1])
allreduce(data1)
print('allreduce之后:\n',data1[0],'\n',data1[1])

#数据平均拆分到不同gpu上面
data = torch.arange(20).reshape(4,5)
#devices = [torch.device('cuda:0'),torch.device('cpu')]
devices = [torch.device('cuda:0')]
scatter = nn.parallel.scatter(data,devices)
print('data = ',data)
print('devices = ',devices)
print('scatter = ',scatter)
#将小批量样本数据和标签平均拆分到不同gpu上面,让每个gpu处理每个小批量中子样本数据
def split_batch(X,y,devices):
    assert X.shape[0] == y.shape[0]
    return (nn.parallel.scatter(X,devices),nn.parallel.scatter(y,devices))
#将小批量样本用多个gpu并行训练
def train_batch(X,y,AllDevices_params,All_devices,lr):
    # 将小批量样本数据和标签平均拆分到不同gpu上面,让每个gpu处理每个小批量中子样本数据
    X_shareds,y_shareds = split_batch(X,y,All_devices)
    #每个gpu训练自己上面的子样本数据,然后对训练出来的结果与对应的标签求loss,然后再对子样本的每个样本的loss求和。ls是一个列表类型
    # 在每个GPU上分别计算损失
    ls = [loss(LeNet(X_shared,device_params),y_shared).sum() for X_shared,y_shared,device_params in zip(X_shareds,y_shareds,AllDevices_params)]
    for l in ls:# 反向传播在每个GPU上分别执行
        #对每个gpu上面的权重参数求梯度
        l.backward()
    # 将每个GPU的所有梯度相加,并将其广播到所有GPU
    with torch.no_grad():
        #遍历网络每一个层
        for i in range(len(AllDevices_params[0])):
            #将每个gpu中对应的层的梯度求和,通常在第一个gpu上面计算,然后再广播到所有gpu上面
            allreduce([AllDevices_params[c][i].grad for c in range(len(All_devices))])
    # 在每个GPU上分别更新模型参数
    for device_params in AllDevices_params:
        #将每个gpu上面的网络权重参数进行权重更新,因此达到了所有gpu上面的权重参数值都是相同的
        d2l.torch.sgd(device_params,lr,X.shape[0])

def train(num_gpu,batch_size,lr):
    train_iter,test_iter = d2l.torch.load_data_fashion_mnist(batch_size)
    devices = [d2l.torch.try_gpu(i) for i in range(num_gpu)]
    #给每个gpu上面复制相同的网络权重参数
    # 将模型参数复制到num_gpu个GPU
    devices_params = [get_params(params,device)for device in devices]
    epochs = 10
    animator = d2l.torch.Animator('epoch','test acc',xlim=[1,epochs])
    timer = d2l.torch.Timer()
    for epoch in range(epochs):
        #处理一个epoch开始时间
        timer.start()
        for X,y in train_iter:
            # 为单个小批量执行多GPU训练
            train_batch(X,y,devices_params,devices,lr)
            torch.cuda.synchronize()
        #处理完一个epoch(一轮)所花的时间,计算训练完一遍数据集所花的时间
        timer.stop()
        #训练数据集每轮训练完后对测试数据集进行测试,输出测试精度大小
        # 在GPU0上评估模型
        animator.add(epoch+1,d2l.torch.evaluate_accuracy_gpu(lambda x : LeNet(x,devices_params[0]),test_iter,devices[0]))
    print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,在{str(devices)}')

train(1,256,0.2)#使用一个gpu设备,batch_size为256,lr为0.2

3. 下一节多GPU数据并行使用Pytorch框架简洁实现链接

李沐动手学深度学习V2-多GPU数据并行使用Pytorch框架简洁实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值