接下来介绍在paddlepaddle中如何使用多CPU来加速训练。
接着前面几节讲的手写数字识别部分,在启动训练前,加载数据和网络结构的代码部分均不变。
1 # 加载相关库 2 import os 3 import random 4 import paddle 5 import paddle.fluid as fluid 6 from paddle.fluid.dygraph.nn import Conv2D, Pool2D, FC 7 import numpy as np 8 from PIL import Image 9 10 import gzip 11 import json 12 13 # 定义数据集读取器 14 def load_data(mode=‘train‘): 15 16 # 读取数据文件 17 datafile = ‘./work/mnist.json.gz‘ 18 print(‘loading mnist dataset from {} ......‘.format(datafile)) 19 data = json.load(gzip.open(datafile)) 20 # 读取数据集中的训练集,验证集和测试集 21 train_set, val_set, eval_set = data 22 23 # 数据集相关参数,图片高度IMG_ROWS, 图片宽度IMG_COLS 24 IMG_ROWS = 28 25 IMG_COLS = 28 26 # 根据输入mode参数决定使用训练集,验证集还是测试 27 if mode == ‘train‘: 28 imgs = train_set[0] 29 labels = train_set[1] 30 elif mode == ‘valid‘: 31 imgs = val_set[0] 32 labels = val_set[1] 33 elif mode == ‘eval‘: 34 imgs = eval_set[0] 35 labels = eval_set[1] 36 # 获得所有图像的数量 37 imgs_length = len(imgs) 38 # 验证图像数量和标签数量是否一致 39 assert len(imgs) == len(labels), 40 "length of train_imgs({}) should be the same as train_labels({})".format( 41 len(imgs), len(labels)) 42 43 index_list = list(range(imgs_length)) 44 45 # 读入数据时用到的batchsize 46 BATCHSIZE = 100 47 48 # 定义数据生成器 49 def data_generator(): 50 # 训练模式下,打乱训练数据 51 if mode == ‘train‘: 52 random.shuffle(index_list) 53 imgs_list = [] 54 labels_list = [] 55 # 按照索引读取数据 56 for i in index_list: 57 # 读取图像和标签,转换其尺寸和类型 58 img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype(‘float32‘) 59 label = np.reshape(labels[i], [1]).astype(‘int64‘) 60 imgs_list.append(img) 61 labels_list.append(label) 62 # 如果当前数据缓存达到了batch size,就返回一个批次数据 63 if len(imgs_list) == BATCHSIZE: 64 yield np.array(imgs_list), np.array(labels_list) 65 # 清空数据缓存列表 66 imgs_list = [] 67 labels_list = [] 68 69 # 如果剩余数据的数目小于BATCHSIZE, 70 # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch 71 if len(imgs_list) > 0: 72 yield np.array(imgs_list), np.array(labels_list) 73 74 return data_generator 75 76 77 # 定义模型结构 78 class MNIST(fluid.dygraph.Layer): 79 def __init__(self, name_scope): 80 super(MNIST, self).__init__(name_scope) 81 name_scope = self.full_name() 82 # 定义卷积层,输出通道20,卷积核大小为5,步长为1,padding为2,使用relu激活函数 83 self.conv1 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act=‘relu‘) 84 # 定义池化层,池化核为2,采用最大池化方式 85 self.pool1 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type=‘max‘) 86 # 定义卷积层,输出通道20,卷积核大小为5,步长为1,padding为2,使用relu激活函数 87 self.conv2 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act=‘relu‘) 88 # 定义池化层,池化核为2,采用最大池化方式 89 self.pool2 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type=‘max‘) 90 # 定义全连接层,输出节点数为10,激活函数使用softmax 91 self.fc = FC(name_scope, size=10, act=‘softmax‘) 92 93 # 定义网络的前向计算过程 94 def forward(self, inputs): 95 x = self.conv1(inputs) 96 x = self.pool1(x) 97 x = self.conv2(x) 98 x = self.pool2(x) 99 x = self.fc(x) 100 return x
单GPU训练
现实生活中,我们可能会遇到更复杂的机器学习、深度学习任务,需要运算速度更高的硬件(GPU、TPU),甚至同时使用多个机器共同训练一个任务(多卡训练和多机训练)。
飞桨动态图通过fluid.dygraph.guard(place=None)里的place参数,设置在GPU上训练还是CPU上训练,比如:
1 with fluid.dygraph.guard(place=fluid.CPUPlace()) #设置使用CPU资源训神经网络。 2 with fluid.dygraph.guard(place=fluid.CUDAPlace(0)) #设置使用GPU资源训神经网络,默认使用机器的第一个GPU。
下面是采用GPU训练实例(就是前三行):
1 #仅前3行代码有所变化,在使用GPU时,可以将use_gpu变量设置成True 2 use_gpu = True 3 place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace() 4 5 with fluid.dygraph.guard(place): 6 model = MNIST("mnist") 7 model.train() 8 #调用加载数据的函数 9 train_loader = load_data(‘train‘) 10 11 #四种优化算法的设置方案,可以逐一尝试效果 12 optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01) 13 #optimizer = fluid.optimizer.MomentumOptimizer(learning_rate=0.01) 14 #optimizer = fluid.optimizer.AdagradOptimizer(learning_rate=0.01) 15 #optimizer = fluid.optimizer.AdamOptimizer(learning_rate=0.01) 16 17 EPOCH_NUM = 2 18 for epoch_id in range(EPOCH_NUM): 19 for batch_id, data in enumerate(train_loader()): 20 #准备数据,变得更加简洁 21 image_data, label_data = data 22 image = fluid.dygraph.to_variable(image_data) 23 label = fluid.dygraph.to_variable(label_data) 24 25 #前向计算的过程 26 predict = model(image) 27 28 #计算损失,取一个批次样本损失的平均值 29 loss = fluid.layers.cross_entropy(predict, label) 30 avg_loss = fluid.layers.mean(loss) 31 32 #每训练了100批次的数据,打印下当前Loss的情况 33 if batch_id % 200 == 0: 34 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 35 36 #后向传播,更新参数的过程 37 avg_loss.backward() 38 optimizer.minimize(avg_loss) 39 model.clear_gradients() 40 41 #保存模型参数 42 fluid.save_dygraph(model.state_dict(), ‘mnist‘)
loading mnist dataset from ./work/mnist.json.gz ...... epoch: 0, batch: 0, loss is: [3.2412765] epoch: 0, batch: 200, loss is: [0.3588875] epoch: 0, batch: 400, loss is: [0.21310554] epoch: 1, batch: 0, loss is: [0.34854925] epoch: 1, batch: 200, loss is: [0.22530955] epoch: 1, batch: 400, loss is: [0.20724224]
多CPU分布式训练
在工业实践中,许多较复杂的任务需要使用更强大的模型。强大模型加上海量的训练数据,经常导致模型训练耗时严重。比如在计算机视觉分类任务中,训练一个在ImageNet数据集上精度表现良好的模型,大概需要一周的时间,因为我们需要不断尝试各种优化的思路和方案。如果每次训练均要耗时1周,这会大大降低模型迭代的速度。在机器资源充沛的情况下,我们可以采用分布式训练,大部分模型的训练时间可压缩到小时级别。
分布式训练有两种实现模式:模型并行和数据并行。
1. 模型并行
模型并行是将一个网络模型拆分为多份,拆分后的模型分到多个设备上(GPU)训练,每个设备的训练数据是相同的。 模型并行的方式一般适用于:
- 模型架构过大,完整的模型无法放入单个GPU。2012年ImageNet大赛的冠军模型AlexNet是模型并行的典型案例。由于当时GPU内存较小,单个GPU不足以承担AlexNet。研究者将AlexNet拆分为两部分放到两个GPU上并行训练。
- 网络模型的设计结构可以并行化时,采用模型并行的方式。例如在计算机视觉目标检测任务中,一些模型(YOLO9000)的边界框回归和类别预测是独立的,可以将独立的部分分在不同的设备节点上完成分布式训练。
说明:当前GPU硬件技术快速发展,深度学习使用的主流GPU的内存已经足以满足大多数的网络模型需求,所以大多数情况下使用数据并行的方式。
2. 数据并行
数据并行与模型并行不同,数据并行每次读取多份数据,读取到的数据输入给多个设备(GPU)上的模型,每个设备上的模型是完全相同的。数据并行的方式与众人拾柴火焰高的道理类似,如果把训练数据比喻为砖头,把一个设备(GPU)比喻为一个人,那单GPU训练就是一个人在搬砖,多GPU训练就是多个人同时搬砖,每次搬砖的数量倍数增加,效率呈倍数提升。但是注意到,每个设备的模型是完全相同的,但是输入数据不同,每个设备的模型计算出的梯度是不同的,如果每个设备的梯度更新当前设备的模型就会导致下次训练时,每个模型的参数都不同了,所以我们还需要一个梯度同步机制,保证每个设备的梯度是完全相同的。
数据并行中有一个参数管理服务器(parameter server)收集来自每个设备的梯度更新信息,并计算出一个全局的梯度更新。当参数管理服务器收到来自训练设备的梯度更新请求时,统一更新模型的梯度。
飞桨有便利的数据并行训练方式,仅改动几行代码即可实现多GPU训练,如果想了解飞桨数据并行的基本思想,可以参考官网文档-https://www.paddlepaddle.org.cn/documentation/docs/zh/user_guides/howto/training/cluster_howto.html。
用户只需要对程序进行简单修改,即可实现在多GPU上并行训练。飞桨采用数据并行的实现方式,在训练前,需要配置如下参数:
1.从环境变量获取设备的ID,并指定给CUDAPlace
1 device_id = fluid.dygraph.parallel.Env().dev_id 2 place = fluid.CUDAPlace(device_id)
2.对定义的网络做预处理,设置为并行模式
1 strategy = fluid.dygraph.parallel.prepare_context() ## 新增 2 model = MNIST("mnist") 3 model = fluid.dygraph.parallel.DataParallel(model, strategy) ## 新增
3.定义多GPU训练的reader,将每批次的数据平分到每个GPU上
1 valid_loader = paddle.batch(paddle.dataset.mnist.test(), batch_size=16, drop_last=true) 2 valid_loader = fluid.contrib.reader.distributed_batch_reader(valid_loader)
4.收集每批次训练数据的loss,并聚合参数的梯度
1 avg_loss = mnist.scale_loss(avg_loss) ## 新增 2 avg_loss.backward() 3 mnist.apply_collective_grads() ## 新增
完整程序如下所示。
1 def train_multi_gpu(): 2 3 ##修改1-从环境变量获取使用GPU的序号 4 place = fluid.CUDAPlace(fluid.dygraph.parallel.Env().dev_id) 5 6 with fluid.dygraph.guard(place): 7 8 ##修改2-对原模型做并行化预处理 9 strategy = fluid.dygraph.parallel.prepare_context() 10 model = MNIST("mnist") 11 model = fluid.dygraph.parallel.DataParallel(model, strategy) 12 13 model.train() 14 15 #调用加载数据的函数 16 train_loader = load_data(‘train‘) 17 ##修改3-多GPU数据读取,必须确保每个进程读取的数据是不同的 18 train_loader = fluid.contrib.reader.distributed_batch_reader(train_loader) 19 20 optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01) 21 EPOCH_NUM = 5 22 for epoch_id in range(EPOCH_NUM): 23 for batch_id, data in enumerate(train_loader()): 24 #准备数据 25 image_data, label_data = data 26 image = fluid.dygraph.to_variable(image_data) 27 label = fluid.dygraph.to_variable(label_data) 28 29 predict = model(image) 30 31 loss = fluid.layers.square_error_cost(predict, label) 32 avg_loss = fluid.layers.mean(loss) 33 34 # 修改4-多GPU训练需要对Loss做出调整,并聚合不同设备上的参数梯度 35 avg_loss = mnist.scale_loss(avg_loss) 36 avg_loss.backward() 37 model.apply_collective_grads() 38 # 最小化损失函数,清除本次训练的梯度 39 optimizer.minimize(avg_loss) 40 model.clear_gradients() 41 42 if batch_id % 200 == 0: 43 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 44 45 #保存模型参数 46 fluid.save_dygraph(model.state_dict(), ‘mnist‘)
启动多GPU的训练,需要在命令行设置一些参数变量。打开终端,运行如下命令:
1 $ python -m paddle.distributed.launch --selected_gpus=0,1,2,3 --log_dir ./mylog train_multi_gpu.py
- paddle.distributed.launch表示启动分布式运行。
- 通过selected_gpus设置使用的GPU的序号。当前机器需要是多GPU卡的机器,通过命令watch nvidia-smi可以查看GPU的序号。
- log_dir用于存放训练的log,如果不设置,每个GPU上的训练信息都会打印到屏幕。
- 多GPU运行的脚本是:train_multi_gpu.py,包含上述修改过的train_multi_gpu()函数。
训练完成后,程序会在指定的./mylog文件夹下产生四个worklog文件。每个文件存放对应设备的训练过程日志,其中worklog.0的内容如下:
grep: warning: GREP_OPTIONS is deprecated; please use an alias or script
dev_id 0
I1104 06:25:04.377323 31961 nccl_context.cc:88] worker: 127.0.0.1:6171 is not ready, will retry after 3 seconds...
I1104 06:25:07.377645 31961 nccl_context.cc:127] init nccl context nranks: 3 local rank: 0 gpu id: 1?
W1104 06:25:09.097079 31961 device_context.cc:235] Please NOTE: device: 1, CUDA Capability: 61, Driver API Version: 10.1, Runtime API Version: 9.0
W1104 06:25:09.104460 31961 device_context.cc:243] device: 1, cuDNN Version: 7.5.
start data reader (trainers_num: 3, trainer_id: 0)
epoch: 0, batch_id: 10, loss is: [0.47507238]
epoch: 0, batch_id: 20, loss is: [0.25089613]
epoch: 0, batch_id: 30, loss is: [0.13120805]
epoch: 0, batch_id: 40, loss is: [0.12122715]
epoch: 0, batch_id: 50, loss is: [0.07328521]
epoch: 0, batch_id: 60, loss is: [0.11860339]
epoch: 0, batch_id: 70, loss is: [0.08205047]
epoch: 0, batch_id: 80, loss is: [0.08192863]
epoch: 0, batch_id: 90, loss is: [0.0736289]
epoch: 0, batch_id: 100, loss is: [0.08607423]
start data reader (trainers_num: 3, trainer_id: 0)
epoch: 1, batch_id: 10, loss is: [0.07032011]
epoch: 1, batch_id: 20, loss is: [0.09687119]
epoch: 1, batch_id: 30, loss is: [0.0307216]
epoch: 1, batch_id: 40, loss is: [0.03884467]
epoch: 1, batch_id: 50, loss is: [0.02801813]
epoch: 1, batch_id: 60, loss is: [0.05751991]
epoch: 1, batch_id: 70, loss is: [0.03721186]
.....
原文地址:https://www.cnblogs.com/yuzaihuan/p/12348467.html