导读:飞桨(PaddlePaddle)致力于让深度学习技术的创新与应用更简单。在单机训练速度方面,通过高并行、低开销的异步执行策略和高效率的核心算子,优化静态图训练性能,在Paddle Fluid v1.5.0的基准测试中,在7个典型模型上进行了测试(图像领域5个,NLP领域2个),其中5个模型的速度显著优于对标框架(大于15%),2个模型与对标框架持平(5%之内)。如果想让单机训练速度更快,可以根据这篇文档的建议从网络构建、数据准备、模型训练三个方向了解飞桨单机训练中常用的优化方法。来一组测试数据先睹为快。
测试环境如下:
• PaddlePaddle version:1.5.0
• Tensorflow version:1.12.0
• PyTorch version:1.1.0
• MXNet version:1.4.1
• GPU:Tesla V100-SXM2
• CPU:Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz,38核
• Nvida driver: 418.39
• CUDNN VERSION:7.4.2.24
• CUDA VERSION:9.0.176,单卡模式
为方便用户使用,飞桨提供一些不同粒度的Layer,其中有些Layer的组合可以通过单个Layer完成。比如:
(1)fluid.layers.softmax_with_cross_entropy,该操作其实是fluid.layers.softmax和fluid.layers.cross_entropy的组合,因此如果模型中有出现fluid.layers.softmax和fluid.layers.cross_entropy的组合,可以直接用fluid.layers.softmax_with_cross_entropy替换。
(2)如果模型中需要对数据进行标准化,可以直接使用fluid.layers.data_norm,而不用通过一系列layer组合出数据的标准化操作。
因此,建议在构建模型时优先使用飞桨提供的单个Layer完成所需操作,这样减少模型中Layer的个数,并因此加速模型训练。
数据准备通常分为两部分:第一部分是数据加载,即程序从磁盘中加载训练/预测数据;第二部分是数据预处理,程序对加载的数据进行预处理,比如图像任务通常需要进行数据增强、Shuffle等。这两部分需要用户根据自己的模型需要进行设置,只需要最后得到Data Reader接口即可。Data Reader返回iterable对象,可以每次返回一条样本或者一组样本。代码示例如下:
def data_reader (width, height):
defreader():
while True:
yield np.random.uniform(-1, 1,size=width*height), \
np.random.randint(0,10)
return reader
train_data_reader = data_reader(32, 32)
飞桨提供了两种方式从Data Reader中读取数据:同步数据读取和异步数据读取。
同步数据读取是一种简单并且直观的数据准备方式,代码示例如下:
Image = paddle.layer.data("image",...)
label = paddle.layer.data("label",...)
# 模型定义
# ……
prediction = fluid.layers.fc(input= image,size=10)
loss = fluid.layers.cross_entropy(input=prediction, label= label)
avg_loss = fluid.layers.mean(loss)
# ……
# 读取数据
# paddle.dataset.mnist.train()返回数据读取的Reader,每次可以从Reader中读取一条样本,batch_size为128
train_reader =paddle.batch(paddle.dataset.mnist.train(), 128)
end = time.time()
for batch_id, batch in enumerate(train_reader):
data_time = time.time() - end
# 训练网络
executor.run(feed=[...], fetch_list=[...])
batch_time = time.time() - end
end= time.time()
用户首先需要通过fluid.layers.data定义模型的输入,然后根据输入构建模型,最后从事先自定义的Reader函数中获取一个batch的数据,并将数据传递给执行器。
可以看出,采用同步数据读取方式时,用户可通过加入计时函数来统计数据准备部分和执行部分所占用的时间。由于数据准备和执行是顺序进行的,所以程序的执行速度可能较慢。如果用户想进行模型调试的话,同步数据读取是一个不错的选择。
更多同步数据读取的介绍请参考:
https://www.paddlepaddle.org.cn/documentation/docs/en/1.5/user_guides/howto/prepare_data/reader.html
飞桨里面使用py_reader接口来实现异步数据读取,代码示例如下:
train_py_reader = fluid.layers.py_reader(
capacity=10,
shapes=((-1, 784), (-1, 1)),
dtypes=('float32', 'int64'),
name="train_reader",
use_double_buffer=True)
# 使用 read_file() 方法从py_reader中获取模型的输入
image, label = fluid.layers.read_file(reader)
# 模型定义
# ……
prediction = fluid.layers.fc(input= image,size=10)
loss = fluid.layers.cross_entropy(input=prediction, label= label)
avg_loss = fluid.layers.mean(loss)
# ……
# 读取数据
train_reader =paddle.batch(paddle.dataset.mnist.train(), 128)
train_py_reader.decorate_paddle_reader(train_reader)
# 启动py_reader
train_py_reader.start()
try:
end= time.time()
while True:
print("queue size: ", train_py_reader.queue.size())
loss, = executor.run(fetch_list=[...])
# ...
batch_time = time.time() - end
end = time.time()
batch_id += 1
except fluid.core.EOFException:
train_py_reader.reset()
用户首先需要通过fluid.layers.py_reader定义py_reader对象,并使用 read_file() 方法从py_reader中获取模型的输入,然后根据输入构建模型,再然后用decorate_paddle_reader将自定义的Reader与py_reader绑定。在训练开始之前,通过调用start()方法来启动数据读取。在数据读取结束之后,executor.run会抛出fluid.core.EOFException,表示训练已经遍历完Reader中的所有数据。
采用异步数据读取时,Python端和C++端共同维护一个数据队列,Python端启动一个线程,负责向队列中插入数据,C++端在训练/预测过程中,从数据队列中获取数据,并将该数据从对队列中移除。用户可以在程序运行过程中,监测数据队列是否为空,如果队列始终不为空,表明数据准备的速度比模型执行的速度快,这种情况下数据读取可能不是瓶颈。
另外,飞桨提供的一些FLAGS也能很好的帮助分析性能。如果用户希望评估一下在完全没有数据读取开销情况下模型的性能,可以设置一下环境变量:FLAGS_reader_queue_speed_test_mode,在该变量为True情况下,C++端从数据队列中获取数据之后,不会从数据队列中移除,这样能够保证数据队列始终不为空,从而避免了C++端读取数据时的等待开销。
需要特别注意的是,FLAGS_reader_queue_speed_test_mode只能在性能分析时打开,正常训练/预测模型时需要关闭。
为降低训练的整体时间,建议用户使用异步数据读取的方式,并开启 use_double_buffer=True 。用户可根据模型的实际情况设置数据队列的大小。如果数据准备的时间大于模型执行的时间,或者出现了数据队列为空的情况,就需要考虑对数据读取Reader进行加速。常用的方法是使用多进程准备数据,
可以参考:
https://github.com/PaddlePaddle/models/blob/develop/PaddleCV/yolov3/reader.py
更多异步数据读取的介绍请参考:
https://www.paddlepaddle.org.cn/documentation/docs/en/1.5/user_guides/howto/prepare_data/use_py_reader_en.html
目前Python API中,飞桨提供了fluid.compiler.CompiledProgram的概念,用户可以通过CompiledProgram将传入的program(飞桨中的网络模型)进行编译,如果希望采用数据并行模式训练,只需要将CompiledProgram返回的对象调用一下with_data_parallel即可,最后统一通过executor.run(…)执行compiled_program。
虽然统一通过executor.run(…)接口来执行,实际底层的执行策略有两种,对应C++部分的两个执行器,即Executor和ParallelExecutor,如果用户采用数据并行模式,C++部分使用的是ParallelExecutor,除此之外都是使用Executor。
这两个执行器的差别:
可以看出,Executor的内部逻辑非常简单,但性能可能会弱一些,因为Executor对于program中的操作是串行执行的。而ParallelExecutor首先会将program转变为计算图,并分析计算图中节点间的连接关系,对图中没有相互依赖的节点(OP),通过多线程并行执行。
因此,Executor是一个轻量级的执行器,目前主要用于参数初始化、模型保存、模型加载。ParallelExecutor是Executor的升级版本,目前ParallelExecutor主要用于模型训练,包括单机单卡、单机多卡以及多机多卡训练。
ParallelExecutor执行计算图之前,可以对计算图进行一些优化,比如使计算图中的一些操作是In-place的、将计算图中的参数更新操作进行融合等。用户还可以调整Parallel Executor执行过程中的一些配置,比如执行计算图的线程数等。这些配置分别是构建策略(BuildStrategy)和执行策略(ExecutionStrategy)参数来设置的。
一个简单的使用示例如下:
build_strategy = fluid.BuildStrategy()
build_strategy.enable_inplace = True
build_strategy.fuse_all_optimizer_ops=True
exec_strategy = fluid.ExecutionStrategy()
exec_strategy.num_threads = 4
train_program = fluid.compiler.CompiledProgram(main_program).with_data_parallel(
loss_name=loss.name,
build_strategy=build_strategy,
exec_strategy=exec_strategy)
place = fluid.CUDAPlace(0)
exe = Executor(place)
# 使用py_reader读取数据,因此执行时不需要feed
fetch_outs = exe.run(train_program, fetch_list=[loss.name],)
更多关于ParallelExecutor的介绍请参考:
https://www.paddlepaddle.org.cn/documentation/docs/zh/1.5/api_guides/low_level/parallel_executor.html
更多关于CompiledProgram的介绍请参考:
https://www.paddlepaddle.org.cn/documentation/docs/zh/1.5/api_guides/low_level/compiled_program.html
BuildStrategy中提供了一些关于计算图优化的策略,这些策略可以在不同程度上提升模型的训练速度,但是其中一些策略与模型的结构有关,比如fuse_all_optimizer_ops不支持sparse梯度,我们正在积极的完善这些策略,并在下一个版本将这些策略默认打开。
构建策略的详细介绍如下:
参数说明:
(1)关于 reduce_strategy , Parallel Executor 对于数据并行支持两种参数更新模式:AllReduce 和 Reduce 。在 AllReduce 模式下,各个节点上计算得到梯度之后,调用 AllReduce 操作,梯度在各个节点上聚合,然后各个节点分别进行参数更新。在 Reduce 模式下,参数的更新操作被均匀的分配到各个节点上,即各个节点计算得到梯度之后,将梯度在指定的节点上进行 Reduce ,然后在该节点上进行参数的更新,最后将更新之后的参数Broadcast到其他节点。
即:如果模型中有100个参数需要更新,训练使用的节点数为4,在 AllReduce 模式下,各个节点需要分别对这100个参数进行更新;在 Reduce 模式下,各个节点需要分别对这25个参数进行更新,最后将更新的参数Broadcast到其他节点。注意:如果是使用CPU进行数据并行训练,在Reduce模式下,不同CPUPlace 上的参数是共享的,所以在各个CPUPlace 上完成参数更新之后不用将更新后的参数Broadcast到其他CPUPlace。
(2)关于 enable_backward_optimizer_op_deps ,在多卡训练时,打开该选项可能会提升训练速度。
(3)关于 fuse_all_optimizer_ops ,目前只支持SGD、Adam和Momentum算法。注意:目前不支持sparse参数梯度。
(4)关于 fuse_all_reduce_ops ,多GPU训练时,可以对 AllReduce 操作进行融合,以减少 AllReduce 的调用次数。默认情况下会将同一layer中参数的梯度的 AllReduce 操作合并成一个,比如对于 fluid.layers.fc 中有Weight和Bias两个参数,打开该选项之后,原本需要两次 AllReduce 操作,现在只用一次 AllReduce 操作。此外,为支持更大粒度的参数梯度融合,飞桨提供了 FLAGS_fuse_parameter_memory_size 选项,用户可以指定融合AllReduce操作之后,每个 AllReduce 操作的梯度字节数,比如希望每次 AllReduce 调用传输64MB的梯度,export FLAGS_fuse_parameter_memory_size=64 。注意:目前不支持sparse参数梯度。
(5)关于 mkldnn_enabled_op_types ,目前飞桨的Op中可以使用mkldnn库计算的操作包括:transpose, sum, softmax,requantize, quantize, pool2d, lrn, gaussian_random, fc, dequantize,conv2d_transpose, conv2d, conv3d, concat, batch_norm, relu, tanh, sqrt, abs.
ExecutionStrategy中提供了关于计算图执行时的一些配置,这些配置可能会影响模型的训练速度。同时,这些配置与模型的结构有关,如果用户希望模型训练速度更快,可以调整一下这些配置。在后续的优化中,我们会对这部分进行优化,根据输入模型结构动态调整这些设置。
ExecutionStrategy配置选项说明:
参数说明:
(1)关于 num_iteration_per_drop_scope ,框架在运行过程中会产生一些临时变量,通常每经过一个batch就要清理一下临时变量,但是由于GPU是异步设备,在清理之前需要对所有的GPU调用一次同步操作,因此耗费的时间较长。为此我们在 execution_strategy 中添加了 num_iteration_per_drop_scope 选项。用户可以指定经过多少次迭代之后清理一次。
(2)关于 num_threads ,ParallelExecutor 根据OP之间的依赖关系确定OP的执行顺序,即:当OP的输入都已经变为ready状态之后,该OP会被放到一个队列中,等待被执行。ParallelExecutor 内部有一个任务调度线程和一个线程池,任务调度线程从队列中取出所有Ready的OP,并将其放到线程队列中。num_threads 表示线程池的大小。根据以往的经验,对于CPU任务,num_threads=2*dev_count 时性能较好,对于GPU任务,num_threads=4*dev_count 时性能较好。注意:线程池不是越大越好。
Paddle Fluid中有一些FLAGS可以有助于性能优化:
(1)FLAGS_cudnn_exhaustive_search表示在调用cuDNN中的卷积操作时,根据输入数据的shape等信息,采取穷举搜索的策略从算法库中选取到更快的卷积算法,进而实现对模型中卷积操作的加速。需要注意的是:
a. 在搜索算法过程中需要使用较多的显存,如果用户的模型中卷积操作较多,或者GPU卡显存较小,可能会出现显存不足问题。
b. 通过穷举搜索选择好算法之后,该算法会进入Cache,以便下次运行时,如果输入数据的shape等信息不变,直接使用Cache中算法。
(2)FLAGS_enable_cublas_tensor_op_math表示是否使用TensorCore加速cuBLAS等NV提供的库中的操作。需要注意的是,这个环境变量只在Tesla V100以及更新的GPU上适用,且可能会带来一定的精度损失,通常该损失不会影响模型的收敛性。
(1)尽可能的使用飞桨提供的单个layer实现所需操作。
(2)采用异步数据读取。
(3)模型训练相关优化:
a. 使用ParallelExecutor作为底层执行器,代码示例:
compiled_prog = compiler.CompiledProgram(
fluid.default_main_program()).with_data_parallel(
loss_name=loss.name)
如果是单卡训练,也可以调用with_data_parallel方法。
b. 如果模型中参数的梯度都是非sparse的,可以打开fuse_all_optimizer_ops选项,将多个参数更新操作融合为一个。
c. 如果是多卡训练,可以打开enable_backward_optimizer_op_deps、fuse_all_reduce_ops选项。如果想指定每次每次AllReduce操作的数据大小,可以设置FLAGS_fuse_parameter_memory_size,比如 export FLAGS_fuse_parameter_memory_size=1,表示每次 AllReduce 调用传输1MB的梯度。
d. 使用CPU做数据并行训练时,推荐使用Reduce模型,因为在使用CPU进行数据并行训练时,在Reduce模式下,不同CPUPlace 上的参数是共享的,所以在各个CPUPlace 上完成参数更新之后不用将更新后的参数Broadcast到其他CPUPlace上,这对提升速度也有很大帮助。
e. 如果是Reduce模式,可打开fuse_broadcast_ops选项。
f. 如果用户的模型较小,比如mnist、language_model等,可以将num_threads设为1。
g. 在显存足够的前提下,建议将 exec_strategy.num_iteration_per_drop_scope 设置成一个较大的值,比如设置为100 ,这样可以避免反复地申请和释放内存。
目前我们正在推进这些配置自动化的工作:即根据输入的模型结构自动配置这些选项,争取在下一个版本中实现,敬请期待。
(4)FLAGS设置
FLAGS_cudnn_exhaustive_search = True
FLAGS_enable_cublas_tensor_op_math = True
不同的模型计算特征不同,最优运行时配置也就不尽相同。大体来说,主要是两种情况,第一种情况:模型组网OP数量少、OP的计算量大,常见的如ResNet、VGG模型,通过设置合适的batch_size,这类模型很容易就可以将最大限度的利用GPU计算资源,因此设置不同的执行器参数对总体速度影响可能不是很明显。第二种情况:模型由大量的计算量很小的OP组成,比如RNN模型,这类模型则需要用户通过实验来选择运行时参数的最佳配置。因此,我们以典型的语言模型(language model)为例,了解一下上述优化策略的实际效果。
6.1 LSTM language model原理介绍
飞桨提供了论文《Recurrent Neural Network Regularization》中基于LSTM循环神经网络(RNN)的language model的开源实现。相比于传统的语言模型方法,基于循环神经网络的语言模型方法能够更好地解决稀疏词的问题。
该模型的目的是给定一个输入的词序列,预测下一个词出现的概率。
模型中采用了序列任务常用的RNN网络,实现了一个两层的LSTM网络,然后使用LSTM的结果去预测下一个词出现的概率。由于数据的特殊性,每一个batch的last hidden和lastcell会作为下一个batch的init hidden和init cell。
6.2 language_model单GPU训练性能优化效果
language_model中提供了4种RNN运行模式,分别为:static、padding、cudnn和lstm_basic。本案例中测试的为static模式。language_model中同样提供了small、medium、large三种模型配置,主要差别在于隐层的大小、RNN的步数、dropout比例上。我们对这个案例在模型配置、执行选项和数据读取三个方面都进行了优化,我们依次测试了如下优化版本的结果:
(1)Baseline版本
(2)设置exec_strategy.num_threads = device_count
(3)设置exec_strategy.num_iteration_per_drop_scope = 100
(4)设置build_strategy.enable_inplace = True,build_strategy.memory_optimize = False
(5)设置build_strategy.fuse_all_optimizer_ops = True
(6)使用py_reader进行异步数据读取
(7)配置优化
优化前:
for index in range(len):
input = layers.slice(input_embedding, axes=[1], starts=[index],ends=[index + 1])
…
优化后:
sliced_inputs = layers.split(input_embedding,num_or_sections=len, dim=1)
for index in range(len):
input = sliced_inputs[index]
…
优化前:
for index in range(len):
…
res.append(layers.reshape(input, shape=[1, -1,hidden_size]))
real_res = layers.concat(res, 0)
real_res = layers.transpose(x=real_res, perm=[1, 0, 2])
优化后:
for index in range(len):
…
res.append(input)
real_res = layers.concat(res, 0)
real_res = layers.reshape(real_res, shape=[len, -1, hidden_size],inplace=True)
real_res = layers.transpose(x=real_res,perm=[1, 0, 2])
经过7个版本的优化,small和large模型最终分别获得了1.64x和1.35x的加速。从实验结果可以看出,即使是类似的网络结构,调整运行参数产生加速效果也不同,如设置exec_strategy.num_threads = device_count,small模型获得了4.9%的加速,large模型只获得0.8%的加速。另外,异步数据读取对该模型总体训练时间的减少也不明显,主要是因为这个模型的所使用的PTB数据集很小,可以提前将所有数据读取到内存里,因此训练时,数据准备部分对整体时延的影响较小。
如果您想详细了解更多飞桨PaddlePaddle的相关内容,请点击阅读原文或参阅以下文档。
官网地址:https://www.paddlepaddle.org.cn?fr=gzh
本文提到的项目地址:
本文分享自 PaddlePaddle 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!