单机训练最佳实践
开始优化您的单机训练任务
PaddlePaddle Fluid可以支持在现代CPU、GPU平台上进行训练。如果您发现Fluid进行单机训练的速度较慢,您可以根据这篇文档的建议对您的Fluid程序进行优化。
神经网络训练代码通常由三个步骤组成:网络构建、数据准备、模型训练。这篇文档将分别从这三个方向介绍Fluid训练中常用的优化方法。
1. 网络构建过程中的配置优化
这部分优化与具体的模型有关,在这里,我们列举出一些优化过程中遇到过的一些示例。
1.1 cuDNN操作的选择
cuDNN是NVIDIA提供的深度神经网络计算库,其中包含了很多神经网络中常用算子,Paddle中的部分Op底层调用的是cuDNN库,例如 conv2d
:
- paddle.fluid.layers.conv2d(input,
- num_filters,
- filter_size,
- stride=1,
- padding=0,
- dilation=1,
- groups=None,
- param_attr=None,
- bias_attr=None,
- use_cudnn=True,
- act=None,
- name=None)
在 use_cudnn=True
时,框架底层调用的是cuDNN中的卷积操作。
通常cuDNN库提供的操作具有很好的性能表现,其性能明显优于Paddle原生的CUDA实现,比如 conv2d
。但是cuDNN中有些操作的性能较差,比如: conv2d_transpose
在 batch_size=1
时、pool2d
在 global_pooling=True
时等,这些情况下,cuDNN实现的性能差于Paddle的CUDA实现,建议手动设置 use_cudnn=False
。
1.2 使用融合功能的API
Paddle提供一些粗粒度的API,这些API融合了多个细粒度API的计算,比如:
- logits = fluid.layers.softmax(logits)
- loss = fluid.layers.cross_entropy(logits, label, ignore_index=255)
和
- loss = fluid.layers.softmax_with_cross_entropy(logits, label, ignore_index=255, numeric_stable_mode=True)
用户网络配置中使用融合功能的API,通常能取得更好的计算性能。
2. 数据准备优化
2.1 分析数据准备部分的耗时
数据准备部分通常分为两个部分:数据读取部分和预处理部分。
- 数据读取部分:用户需要在Python端从磁盘中加载数据,然后将数据feed到Fluid的执行器中。
- 数据预处理部分:用户需要在Python端进行数据预处理,比如图像任务通常需要进行数据增强、裁剪等。Fluid提供了两种数据读取方式:同步数据读取 和 异步数据读取,详情请参考文档 如何准备数据 。
2.1.1 同步数据读取
同步数据读取是一种简单并且直观的数据准备方式,代码示例如下:
- # 读取数据
- 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()
用户通过调用自己编写的reader函数,reader每次输出一个batch的数据,并将数据传递给执行器。因此数据准备和执行是顺序进行的,用户可通过加入Python计时函数 time.time() 来统计数据准备部分和执行部分所占用的时间。
2.1.2 异步数据读取
Paddle里面使用py_reader接口来实现异步数据读取,代码示例如下:
- # 启动py_reader
- train_py_reader.start()
- batch_id = 0
- 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()
使用异步数据读取时,Paddle的C++端会维护一个数据队列,Python端通过单独的线程向C++端的数据队列传入数据。用户可以在训练过程中输出数据队列中数据的个数,如果queue size始终不为空,表明Python端数据准备的速度比模型执行的速度快,这种情况下Python端的数据读取可能不是瓶颈。
此外,Paddle提供的一些FLAGS也能很好的帮助分析性能,比如通过设置 export FLAGS_reader_queue_speed_test_mode=True
,数据队列中的训练数据在被读取之后,不会从数据队列中弹出,这样能够保证数据队列始终不为空,这样就能够很好的评估出数据读取所占的开销。注意,FLAGS_reader_queue_speed_test_mode只能在分析的时候打开,正常训练模型时需要关闭。
2.2 优化数据准备速度的方法
- 为降低训练的整体时间,建议用户使用异步数据读取的方式,并开启
use_double_buffer
。此外,用户可根据模型的实际情况设置数据队列的大小。 - 如果数据准备的时间大于模型执行的时间,或者出现了数据队列为空的情况,这时候需要考虑对Python的用户reader进行加速。常用的方法为:使用Python多进程准备数据。一个简单的使用多进程准备数据的示例,请参考 YOLOv3 。
- Python端的数据预处理,都是使用CPU完成。如果Paddle提供了相应功能的API,可将这部分预处理功能写到模型配置中,如此Paddle就可以使用GPU来完成该预处理功能,这样也可以减轻CPU预处理数据的负担,提升总体训练速度。
3. 模型训练相关优化
3.1 执行器介绍
目前Paddle中有两个执行器, Executor
和 ParallelExecutor
,这两个执行器的区别:
执行调度器
执行器 | 执行对象 | 执行策略 |
---|---|---|
Executor | Program | 根据 Program 中Operator定义的先后顺序依次运行。 |
ParallelExecutor | SSA Graph | 根据Graph中各个节点之间的依赖关系,通过多线程运行。 |
为了更好的分析模型, ParallelExecutor
内部首先会将输入的 Program
转为SSA Graph,然后根据 build_strategy
中的配置,通过一系列的Pass对Graph进行优化,比如:memory optimize,operator fuse等优化。最后根据 execution_strategy
中的配置执行训练任务。
此外, ParallelExecutor
支持数据并行,即单进程多卡和多进程多卡,关于 ParallelExecutor
的具体介绍请参考 文档 。
为了统一 ParallelExecutor
接口和 Executor
接口,Paddle提供了 fluid.compiler.CompiledProgram
接口,在数据并行模式下,该接口底层调用的是 ParallelExecutor
。
3.2 BuildStrategy中参数配置说明
BuildStrategy配置选项
选项 | 类型 | 默认值 | 说明 |
---|---|---|---|
reduce_strategy | fluid.BuildStrategy.ReduceStrategy | fluid.BuildStrategy.ReduceStrategy.AllReduce | 使用数据并行训练模型时选用 AllReduce 模式训练还是 Reduce 模式训练. |
enable_backward_optimizer_op_deps | bool | FALSE | 在反向操作和参数更新操作之间添加依赖,保证在所有的反向操作都运行结束之后才开始运行参数更新操作. |
fuse_all_optimizer_ops | bool | FALSE | 对模型中的参数更新算法进行融合. |
fuse_all_reduce_ops | bool | FALSE | 多卡训练时,将all_reduce Op进行融合. |
fuse_relu_depthwise_conv | bool | FALSE | 如果模型中存在relu和depthwise_conv,并且是连接的,即relu->depthwise_conv,该选项可以将这两个操作合并为一个. |
fuse_broadcast_ops | bool | FALSE | 在 Reduce 模式下,对最后的多个Broadcast操作融合为一个. |
mkldnn_enabled_op_types | list | {} | 如果是CPU训练,可以用 mkldnn_enabled_op_types 指明模型中的那些操作可以使用MKLDNN库,如果不进行设置,模型可以使用MKLDNN库的所有操作都会使用MKLDNN库. |
- 说明:
- 关于
reduce_strategy
,在ParallelExecutor
对于数据并行支持两种参数更新模式:AllReduce
和Reduce
。在AllReduce
模式下,各个节点上计算得到梯度之后,调用AllReduce
操作,梯度在各个节点上聚合,然后各个节点分别进行参数更新。在Reduce
模式下,参数的更新操作被均匀的分配到各个节点上,即各个节点计算得到梯度之后,将梯度在指定的节点上进行Reduce
,然后在该节点上,最后将更新之后的参数Broadcast到其他节点。即:如果模型中有100个参数需要更新,训练时使用的是4个节点,在AllReduce
模式下,各个节点需要分别对这100个参数进行更新;在Reduce
模式下,各个节点需要分别对这25个参数进行更新,最后将更新的参数Broadcast到其他节点上. - 关于
enable_backward_optimizer_op_deps
,在多卡训练时,打开该选项可能会提升训练速度。 - 关于
fuse_all_optimizer_ops
,目前只支持SGD、Adam和Momentum算法。注意:目前不支持sparse参数梯度 。 - 关于
fuse_all_reduce_ops
,多GPU训练时,可以对AllReduce
操作进行融合,以减少AllReduce
的调用次数。默认情况下会将同一layer中参数的梯度的AllReduce
操作合并成一个,比如对于fluid.layers.fc
中有Weight和Bias两个参数,打开该选项之后,原本需要两次AllReduce
操作,现在只用一次AllReduce
操作。此外,为支持更大粒度的参数梯度融合,Paddle提供了FLAGS_fuse_parameter_memory_size
选项,用户可以指定融合AllReduce操作之后,每个AllReduce
操作的梯度字节数,比如希望每次AllReduce
调用传输64MB的梯度,export FLAGS_fuse_parameter_memory_size=64
。注意:目前不支持sparse参数梯度。 - 关于
mkldnn_enabled_op_types
,支持mkldnn库的Op有:transpose, sum, softmax, requantize, quantize, pool2d, lrn, gaussian_random, fc, dequantize, conv2d_transpose, conv2d, conv3d, concat, batch_norm, relu, tanh, sqrt, abs.
- 关于
3.3 ExecutionStrategy中的配置参数
ExecutionStrategy配置选项
选项 | 类型 | 默认值 | 说明 |
---|---|---|---|
num_iteration_per_drop_scope | INT | 1 | 经过多少次迭代之后清理一次local execution scope |
num_threads | INT | 对于CPU:2dev_count;对于GPU:4dev_count. (这是一个经验值) | ParallelExecutor 中执行所有Op使用的线程池大小 |
- 说明:
- 关于
num_iteration_per_drop_scope
,框架在运行过程中会产生一些临时变量,这些变量被放在local execution scope中。通常每经过一个batch就要清理一下local execution scope中的变量,但是由于GPU是异步设备,在清理local execution scope之前需要对所有的GPU调用一次同步操作,因此耗费的时间较长。为此我们在execution_strategy
中添加了num_iteration_per_drop_scope
选项。用户可以指定经过多少次迭代之后清理一次local execution scope。 - 关于
num_threads
,ParallelExecutor
根据Op之间的依赖关系确定Op的执行顺序,即:当Op的输入都已经变为ready状态之后,该Op会被放到一个队列中,等待被执行。ParallelExecutor
内部有一个任务调度线程和一个线程池,任务调度线程从队列中取出所有Ready的Op,并将其放到线程队列中。num_threads
表示线程池的大小。根据以往的经验,对于CPU任务,num_threads=2dev_count
时性能较好,对于GPU任务,num_threads=4
dev_count
时性能较好。注意:线程池不是越大越好。
- 关于
执行策略配置推荐
- 在显存足够的前提下,建议将
exec_strategy.num_iteration_per_drop_scope
设置成一个较大的值,比如设置exec_strategy.num_iteration_per_drop_scope=100
,这样可以避免反复地申请和释放内存。该配置对于一些模型的优化效果较为明显。 - 对于一些较小的模型,比如mnist、language_model等,多个线程乱序调度op的开销大于其收益,因此推荐设置
exec_strategy.num_threads=1
。
CPU训练设置
- 如果使用CPU做数据并行训练,需要指定环境变量CPU_NUM,这个环境变量指定程序运行过程中使用的
CPUPlace
的个数。 - 如果使用CPU进行数据并行训练,并且
build_strategy.reduce_strategy
=fluid.BuildStrategy.ReduceStrategy.Reduce
,所有CPUPlace
上的参数是共享的,因此对于一些使用CPU进行数据并行训练的模型,选用Reduce
模式可能会更快一些。
4. 运行时FLAGS设置
Fluid中有一些FLAGS可以有助于性能优化:
- FLAGS_fraction_of_gpu_memory_to_use表示每次分配GPU显存的最小单位,取值范围为[0, 1)。由于CUDA原生的显存分配cuMalloc和释放cuFree操作均是同步操作,非常耗时,因此将FLAGS_fraction_of_gpu_memory_to_use设置成一个较大的值,比如0.92(默认值),可以显著地加速训练的速度。
- FLAGS_cudnn_exhaustive_search表示cuDNN在选取conv实现算法时采取穷举搜索策略,因此往往能选取到一个更快的conv实现算法,这对于CNN网络通常都是有加速的。但穷举搜索往往也会增加cuDNN的显存需求,因此用户可根据模型的实际情况选择是否设置该变量。
- FLAGS_enable_cublas_tensor_op_math表示是否使用TensorCore加速计算cuBLAS。这个环境变量只在Tesla V100以及更新的GPU上适用,且可能会带来一定的精度损失。
5. 使用Profile工具进行性能分析
为方便用户更好的发现程序中的性能瓶颈,Paddle提供了多种Profile工具,这些工具的详细介绍和使用说明请参考 性能调优 。