3. Collective 同步训练实践¶
3.1. 同步训练简介¶
许多研究表明深度学习的预训练受益于更多的数据[1] [2] [3],但更大的数据量也意味着更长的训练耗时,数据并行同步训练是一种加速大规模数据训练的方法,有PServer和Collective两种模式。
同步训练通过数据划分,将计算工作量(前向、反向)分布到GPU 集群中的每一个worker上, 提高整体计算吞吐。但参数更新(update) 的过程在两种模式中有所不同:
在
PServer模式中,会启动多个pservers 和多个trainers,每个pserver会保存一部分模型参数,并负责接收从trainer发送的梯度并更新这些模型参数;每个trainer 会保存一份完整的模型,并使用一部分数据进行训练,然后向pserver发送梯度,最后从pserver拉取更新后的参数。 pserver进程和trainer可以在不同的计算节点上,也可以在同一公用节点。一个分布式任务所需要的pserver进程个数通常需要根据实际情况调整,以达到最佳的性能,然而通常来说pserver的进程不会比trainer更多。
在
Collective模式中,集群中只存在多个地位平等的trainers。 每个trainer进程都保存一份完整的模型参数。 前向和反向中每个 trainer 使用自己划分 (shard)的数据进行计算,得到对应的梯度;之后trainers 之间通过 allreduce 等 Collective 通信方式[4] 同步梯度到所有trainers,最后每个 trainer 使用同步后的梯度独立完成参数更新。
相交于异步训练, 同步训练的的优势在于Loss可以比较稳定的下降,缺点是整体速度的快慢取决于最慢的trainer. 因此在训练较为复杂的模型时,即模型训练过程中神经网络训练耗时远大于节点间通信耗时的场景下,推荐使用同步训练模式。
Fleet中 PServer模式使用 gRPC 通信,Collective模式使用 NCCL2 通信。
3.2. Fleet Collective 同步训练优化¶
Fleet 支持在 GPU (CUDA 版本 >= 7.5) 服务器集群上完成高性能分布式训练。
用户可以通过 fleet.DistributedStrategy
设置许多与训练性能策略相关参数。目前Fleet
为这些参数提供了一个较通用默认值,用户可以不去调整。但如果用户希望针对性调优分布式训练的性能,可以根据自身硬件和任务设置对应参数。
在进行性能优化时, 检查每项优化点并验证对应提升,最终获得最优性能。
一个简单的验证当前的训练程序是否需要进一步优化性能的方法,
是查看GPU的计算利用率,通常用 :code:nvidia-smi命令查看。
如果GPU利用率较低,则可能存在较大的优化空间。
下文对性能影响较大,设置频率比较高的几个参数,详细的参数列表放在文末的附录中。
注意: 使用NCCL2模式分布式训练时,需要确保每个节点训练等量的数据,防止在最后一轮训练中任务不退出。通常有两种方式:
随机采样一些数据,补全分配到较少数据的节点上。(推荐使用这种方法,以训练完整的数据集)。
在python代码中,每个节点每个pass只训练固定的batch数,如果这个节点数据较多,则不训练这些多出来的数据。
3.2.1. OP融合¶
将模型网络中顺序执行的多个OPs进行融合能够减少OP 调度的开销,提升训练速度。目前Fleet 中支持如下3种的OP 融合:
fuse_all_optimizer_ops:表明是否融合(fuse) 是否融合 optimizer_op,仅对部分 optimizer 可用(SGD、Adam和Momentum)。fuse_elewise_add_act_ops:表明是否融合(fuse) elementwise_add_op和activation_op。fuse_bn_act_ops:表明是否融合(fuse) batch_norm_op 和 activation_op。
通常使用这些策略都会使整体执行过程更快。
dist_strategy = fleet.DistributedStrategy()
dist_strategy.fuse_all_optimizer_ops = True
dist_strategy.fuse_bn_act_ops = True
dist_strategy.fuse_elewise_add_act_ops = True
3.2.2. AllReduce融合¶
AllReduce 融合默认情况下会将同一layer中参数的梯度的多个AllReduce操作合并成一个。 比如对于 fluid.layers.fc 中有Weight和Bias两个参数,打开该选项之前,需要两次AllReduce操作;打开该选项之后,只用一次AllReduce 操作。这样可以减少梯度同步时的通信耗时。
此外,为支持更大粒度的参数梯度融合,Fleet 提供了以下两个选项,用户可以在训练程序运行前在DistributedStrategy中设置:
fuse_grad_size_in_MB: 指定每个AllReduce操作的梯度字节数,如该参数等于16 则每次AllReduce调用传输16MB的梯度。 该参数的经验值为总通信量的十分之一。fuse_grad_size_in_TFLOPS: 指定每次AllReduce操作的最大层数,即到达该层数就进行AllReduce。如该参数等于50, 则最多每50层做一次 fused AllReduce。
注意: AllReduce融合目前不支持sparse参数梯度。
dist_strategy = fleet.DistributedStrategy()
dist_strategy.fuse_grad_size_in_MB=32
dist_strategy.fuse_grad_size_in_TFLOPS=20
dist_strategy.fuse_all_reduce_ops=True
3.2.3. 分层 AllReduce¶
对于多机模式,针对小数据量的通信,Ring AllReduce通信效率低,采用Hierarchical AllReduce可以缓解这一问题。 分层AllReduce 运行如下图所示:
dist_strategy = fleet.DistributedStrategy()
dist_strategy.use_hierarchical_allreduce = True
dist_strategy.hierarchical_allreduce_inter_nranks = 8
3.2.4. 使用同步Allreduce¶
Fleet 使用多进程+NCCL2模式(collective)以获得更好的性能。 在多进程模式下,每台服务器的每个GPU卡都会对应启动一个训练进程, 集群中的所有进程之间会互相通信完成训练。以此方式最大限度的降低进程内部资源抢占的开销。
dist_strategy.sync_nccl_allreduce=True
3.2.5. 设置合适的nccl通信器数量¶
nccl通信器数量 nccl_comm_num 可以加快GPU之间的通信效率,建议单机设置为1,多机设置为2。
dist_strategy = fleet.DistributedStrategy()
dist_strategy.nccl_comm_num = 2
3.2.6. 设置合适的CPU线程数¶
PaddlePaddle Fluid使用“线程池” [5] 模型调度并执行Op,Op在启动GPU计算之前, 通常需要CPU的协助,然而如果Op本身占用时间很小,“线程池”模型下又会带来额外的调度开销。 使用多进程模式时,如果神经网络的计算图 [6] 节点间有较高的并发度, 即使每个进程只在一个GPU上运行,使用多个线程可以更大限度的提升GPU利用率。
根据以往的经验,对于CPU任务,num_threads=2 * ev_count 时性能较好,对于GPU任务,num_threads=4 * dev_count 时性能较好。注意:线程池不是越大越好。
dist_strategy = fleet.DistributedStrategy()
dist_strategy.thread_num = 3
3.2.7. 提高网络的吞吐¶
多节点训练时网络的带宽常常成为训练的瓶颈。我们在实测中发现,当使用自动混合精度训练后,TCP
socket 的通信方式将成为训练速度的瓶颈, 使多节点训练无法充分利用 FLeet
混合精度计算带来的速度提升。 在我们实测中使用: 100Gb
网卡,RDMA[7]
和
InfiniBand[8]来提升网络带宽,使网络传输不会成为计算速度的瓶颈。
在开始训练前,需要正确设置以下 NCCL 环境变量使对应硬件设置生效:
Env Name |
Description |
|---|---|
NCCL_SOCKET_IFNAME |
The RDMA device, e.g. eth2 |
NCCL_P2P_DISABLE |
Set to 1 to disable P2P transfer between GPUs |
NCCL_IB_DISABLE |
Set to 1 to disable using RDMA |
NCCL_IB_CUDA_SUPPORT |
Set to 1 to enable GPU Direct if supported |
NCCL_DEBUG |
Set debug level: VERSION, WARN, INFO |
3.2.8. 预先分配足够的显存¶
通过设置环境变量 FLAGS_fraction_of_gpu_memory_to_use=0.7 设置预先分配的显存占比。 由于CUDA原生的显存分配cuMalloc和释放cuFree操作均是同步操作,非常耗时,因此 通过 设置 FLAGS_fraction_of_gpu_memory_to_use 成一个较大的值,比如0.7,可以显著地加速训练的速度。
0.7 是指 70%的显存会预先分配。设置的范围是0.0~1.0。注意, 设置成0.0会让每次显存分配都调用 cudaMalloc 这样会极大的降低训练性能。
os.environ['FLAGS_fraction_of_gpu_memory_to_use'] = "0.98"
3.2.9. 降低scope drop频率和fetch频率¶
减少scope drop和fetch频率,可以减少频繁的变量内存申请、释放和拷贝, 从而提升性能。
# 每 30 batch 之后清理一次临时变量
dist_strategy = fleet.DistributedStrategy()
dist_strategy.BuildStrategy = {'num_iteration_per_drop_scope': 30}
# 降低fetch频率,每 30 batch fetch 一次训练输出
for pass_id in xrange(PASS_NUM):
batch_id = 0
while True:
if batch_id % 30 == 0:
fetched = exe.run(fetch_list)
else:
exe.run([])
3.2.10. 增大batch_size或使用设置通信频率(batch merge)¶
分布式同步训练,跨节点通信或多或少会带来性能影响,增大训练的batch_size, 可以保持通信开销不变的情况下,增大计算吞吐从而降低通信在整个训练过程中的占比来提升总体的训练吞吐。
然而增大batch_size会带来同等比例的显存消耗提升,为了进一步的增大batch_size,Fluid提供“batch merge”功能, 通过在一个GPU上串行计算多个小的batch并积累梯度,然后再执行多机多卡之间的通信, 此模式同样也可以被称为“可变通信频率“。使用batch merge功能,在同样的模型, 可以极大的增加batch size,提升多机训练的总吞吐。
3.2.11. 使用 DALI reader¶
数据读取的优化在GPU训练中至关重要,尤其在不断增加batch_size提升吞吐时,数据reader 可能成为训练速度的瓶颈。 Fleet 中可以使用 Nvidia DALI6 作为数据reader. 使用DALI的有点有:
使用GPU完成部分数据预处理,加速数据读取过程,减少 CPU 负担。
DALI 提供预取队列(perfetch queue)功能,让数据预处理和模型计算可以异步进行,减少模型计算对数据读取的等待。
import fleetx as X
model = X.applications.Resnet50()
loader = model.load_imagenet_from_file("/pathto/imagenet/train.txt", use_dali=True)
3.2.12. 使用混合精度训练¶
V100 GPU提供了 Tensor Core 可以在混合精度计算 场景极大的提升性能。使用混合精度计算的例子可以参考文档 ` <https://todo/>`__
目前Paddle只提供在两个模型(ResNet, BERT)的混合精度计算实现并支持static loss scaling,其他模型使用混合精度也 可以参考以上的实现完成验证。