将电脑装成Ubuntu、Windows双系统,并在Ubuntu上继续学习。
在现代深度学习中,多主机多GPU训练已经变得非常常见,尤其是对于大规模模型和数据集。最简单和早期的并行计算比如NVIDIA的SLI,从NVIDIA 450系列驱动开始,NVIDIA官方停止了对SLI配置的支持,特别是在CUDA计算方面。现代深度学习框架通常可以通过多GPU配置来利用多块显卡,而不需要启用SLI。
下面学习在pytorch中的并行训练。
一、DataParallel(数据并行)
DataParallel是一种在深度学习中用于并行处理数据的技术。它可以将一个模型复制到多个设备(如多个 GPU)上,然后将数据分割并分配到这些设备上进行并行计算,以加快模型的训练速度。
优点
加速训练过程
在深度学习中,训练大规模的神经网络往往需要处理海量的数据。DataParallel 技术可以将数据划分成多个小批次,同时在多个计算设备(如多个 GPU)上进行处理。例如,一个具有数百万参数的图像分类模型,在处理包含数万张图像的数据集时,如果使用单个 GPU 可能需要花费数天时间来完成一个训练周期。但通过 DataParallel 将数据分配到 4 个 GPU 上并行处理,理论上可以将训练速度提高近 4 倍,大大缩短了训练时间。
易于实现和使用
以 PyTorch 为例,使用 DataParallel 相对简单。只需要将模型用torch.nn.DataParallel进行包装,然后像往常一样将数据输入模型进行训练即可。代码修改量较小,不需要对模型架构本身进行复杂的改动。
硬件资源利用率高
可以充分利用多个计算设备的计算能力。在具有多个 GPU 的服务器或计算集群中,DataParallel 能够使这些 GPU 同时工作,避免了部分硬件资源闲置的情况。这样可以更有效地利用硬件投资,特别是在处理大规模深度学习任务时,能够最大化地发挥硬件的性能。
缺点
负载不均衡问题
当数据划分不均匀或者模型在不同设备上的计算复杂度因数据而异时,可能会出现负载不均衡的情况。例如,在处理文本数据时,如果不同批次的文本长度差异很大,那么在处理长文本批次的设备上可能会花费更多的时间,导致各个设备的计算进度不一致,从而影响整体性能。这种负载不均衡可能会降低并行效率,使得加速比达不到理想的水平。
通信开销较大
在多个设备之间进行数据划分和结果合并需要一定的通信开销。设备之间需要频繁地交换数据和梯度信息,这在网络带宽有限或者设备间通信速度较慢的情况下,会成为性能瓶颈。特别是当模型参数非常多或者数据批次较大时,通信开销可能会抵消掉并行计算带来的部分性能提升。
模型复制导致内存占用增加
DataParallel 会在每个设备上复制一份模型,这会导致内存占用成倍增加。对于内存资源有限的设备来说,这可能会限制能够处理的模型规模或者数据批次大小。例如,在一些边缘计算设备或者小型 GPU 服务器上,可能无法承受模型的多份复制,从而无法使用 DataParallel 技术。
项目实践
DataParallel的实现较为简单,只需要将网络简单定义即可。将本项目中的train.py部分的代码修改为如下:
import timefrom load_imags import train_loader, train_num, test_loader, test_num
from nets import *
from torch.nn.parallel import DataParalleldef main():# 定义网络print('Please choose a network:')print('1. ResNet18')print('2. VGG')# 选择网络while True:net_choose = input('')if net_choose == '1':net = resnet18_model()net = net.to(device)net_name = 'ResNet18'print('You have chosen the ResNet18 network, start training.')breakelif net_choose == '2':net = vgg_model()# net = net.to(device) # 不使用DataParallelnet = DataParallel(net).to(device) # 使用DataParallelnet_name = 'VGG_simple'print('You have chosen the VGG network, start training.')breakelse:print('Please input a correct number!')# 定义损失函数和优化器loss_func = nn.CrossEntropyLoss() # 交叉熵损失函数optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate) # 优化器使用Adamscheduler = torch.optim.lr_scheduler.StepLR(optimizer,step_size=5,gamma=0.9) # 学习率衰减, 每5个epoch,学习率乘以0.9# 训练模型for epoch in range(num_epoches):trained_num = 0 # 记录训练过的图片数量total_correct = 0 # 记录正确数量print('-' * 100)print('Epoch {}/{}'.format(epoch + 1, num_epoches))begin_time = time.time() # 记录开始时间net.train() # 训练模式for i, (images, labels) in enumerate(train_loader):images = images.to(device) # 每batch_size个图像的数据labels = labels.to(device) # 每batch_size个图像的标签trained_num += images.size(0) # 记录训练过的图片数量outputs = net(images) # 前向传播loss = loss_func(outputs, labels) # 计算损失optimizer.zero_grad() # 梯度清零loss.backward() # 反向传播optimizer.step() # 优化器更新参数_, predicted = torch.max(outputs.data, 1) # 预测结果correct = predicted.eq(labels).cpu().sum() # 计算本batch_size的正确数量total_correct += correct # 记录正确数量# 每5个epoch,学习率衰减scheduler.step()end_time = time.time() # 记录结束时间print('Each train_epoch take time: {} s'.format(end_time - begin_time))print('This train_epoch accuracy: {:.2f}%'.format(100 * total_correct / train_num))print('-' * 60)tested_num = 0 # 记录测试过的图片数量total_correct = 0 # 记录正确数量begin_time = time.time() # 记录开始时间net.eval() # 测试模式for i, (images, labels) in enumerate(test_loader):images = images.to(device) # 每batch_size个图像的数据labels = labels.to(device) # 每batch_size个图像的标签tested_num += images.size(0) # 记录测试过的图片数量outputs = net(images) # 前向传播loss = loss_func(outputs, labels) # 计算损失_, predicted = torch.max(outputs.data, 1) # 预测结果correct = predicted.eq(labels).cpu().sum() # 计算本batch_size的正确数量total_correct += correct # 记录正确数量if (i + 1) % 10 == 0: # 每10个batch_size打印一次print('tested: {}/{}'.format(tested_num, test_num))print('Loss: {:.4f}, Accuracy: {:.2f}%'.format(loss.item(), 100 * correct / images.size(0)))print('tested: {}/{}'.format(tested_num, test_num))print('-' * 30)end_time = time.time() # 记录结束时间print('Each test_epoch take time: {} s'.format(end_time - begin_time))print('This test_epoch accuracy: {:.2f}%'.format(100 * total_correct / test_num))# 保存模型torch.save(net.state_dict(),os.path.join(model_path,time.strftime("%Y%m%d-%H-%M-", time.localtime()) +net_name + '.pkl')) # 按结束时间和网络类型保存模型print('Finished Training')if __name__ == '__main__':main()
只有一个地方修改:net = DataParallel(net).to(device),可以看到简单修改之后就可以实现数据并行。
下面是数据并行修改前和修改后的运行截图对比
修改前的GPU占用率:
两个GPU只有一个在工作。
训练用时:
修改后的GPU占用率:
两个GPU均参与了训练。
但是,训练时长比单显卡的时候变长了,原因是当前的batch_size设定较小,两个GPU之间的通信和等待同步占用时间比较多,GPU占用率很低,大部分时间都处于等待和空闲中。 将batch_size设为当前的4倍:
训练速度明显提升。