此系列文章为笔者学习工作中的阶段性总结,难免有纰漏,如有不正之处,欢迎指正,大家共同成长。
分布式深度学习现状
在深度学习中,随着模型和数据的不断增长,在很多情况下需要使用单机多卡或者多机多卡进行训练,即分布式训练。当前的分布式深度学习主要包含以下模块[1]。
- 数据与模型划分:模型并行 vs 数据并行
- 单机优化:优化算法(略)
- 通信机制:网络拓扑(Parameter Server vs Allreduce), 通信步调(同步更新 vs 异步更新)
- 模型聚合:对单机优化的模型参数/梯度进行汇总,更新新的参数(略)
模型并行vs数据并行
当模型过大无法在单机上存储时,会采用模型并行将模型分割到不通机器上,但多数情况下,一块常规显卡还是能承受住模型的存储开销的,目前训练数据量过大导致的模型训练速度慢才是分布式机器学习领域的主要矛盾。所以大多数情况下还是使用数据并行的方式,每个GPU平均分配一个batch的数据量。
同步更新 vs 异步更新
同步更新过程和原始的 mini batch sgd 是等价的,符合梯度下降的规则,训练收敛稳定,但是通信效率低,更新的速度取决于运算最慢的那台机器。
异步更新虽然不用等,但是会出现梯度延时的问题,导致往往收敛效果不佳。
Parameter Server vs Ring Allreduce
在分布式深度学习中,目前主流的通信拓扑结构是PS和Ring Allreduce。拿PS来说,至少经历了三次版本的迭代,这里不打算展开,主要介绍这两种结构的基本思想。
首先看到一个简化的PS的结构:
对于数据并行的同步更新训练过程,所有GPU上开始都存有模型的副本,每一轮迭代所有的worker独立完成 forward 和 backward ,并将各自backward得到的参数梯度传递给server来做平均、更新,server更新好参数后再将其复制给所有的worker。
两次传递过程的通信量分别是全量的模型梯度。从上图可以明显看到,PS的瓶颈出现在server与其余worker之间的通信,单个GPU的带宽是很有限的,随着worker数目增加,通信的开销将会越大。
举例而言,假设需要训练的模型有300w参数,每个参数是4bytes,那么参数的memory就大约1.2G,最后再假设带宽是1G/s。那么当PS中只有两个node时,每次迭代大概需要1.2s。当PS中有9个worker一个server时,这个开销就至少是10.8s。
假设节点数为N,模型参数量为P,则其通信成本为:
说明随着worker的增加,通信的开销呈线性增长。
为了解决这个问题,百度研究院将HPC中的技术引入了进来,也就是Ring Allreduce。
Ring Allreduce中每一个结点都有一个左结点和右结点,并且每一个结点只能从左节点recv,向右结点send。
Ring Allreduce包含两个步骤:先Scatter-Reduce后Allgather,最终目的是在每个结点上都得到参数梯度的平均。
Scatter Reduce 首先在每个结点上将模型参数梯度划分成N个一致的chunks,其中N为Ring上GPU的个数。这样所有结点上模型的参数梯度组合形成一个方阵。
同理,每个chunk都相应有自己的右节点和左节点,模型参数梯度划分成N个chunk会形成N个chunk ring。
然后从参数梯度方阵对角线上的chunk开始,每个chunk向其右chunk send一份自己的备份,同时右chunk需要recv这个备份并加和。之后经过N-1次迭代,矩阵的右-1对角线chunk和左下角chunk会得到各自所在的chunk ring上所有chunk之和。
All Gather 在Scatter Reduce的基础上,每个GPU上都有一个chunk是其所在chunk ring上的参数梯度之和。所以只需要将这份chunk在其所在的ring上传播N-1次,那么每个GPU上就得到了所有结点上参数梯度之和,这也是All Gather所做的工作。之后各结点对参数梯度求平均即得到整个batch的平均梯度。
假设节点数为N,模型参数量为P,则总的通信量为:
说明随着worker的增加,其通信成本是恒定的。
Horovod
对于一般场景,我们需要一个数据并行的高效通信的同步更新的深度学习分布式架构,Uber 开源的高效分布式训练通信框架horovod是个不错的选择。
Horovod 有如下主要特点[2]:
- Horovod 可以实现接近 90% 的scaling efficiency
- 支持多个主流框架( TensorFlow、Keras、PyTorch 和 MXNet),且分布式修改成本低。
架构搭建
horovod提供多种部署方式,这里讨论基于docker的部署方式。在horovod下使用多机多卡需要满足以下3个先决条件[3]:
- 不同机器可以访问相同的文件:nfs
- 不同机器使用相同的训练环境: Docker
- 不同机器可以ssh交互:ssh 免密登录
详情请参考我的另一篇文章:horovod多机多卡部署
Horovod 框架性能调优
当分布式架构部署成功并且demo可以顺利的跑起来,是否分布式训练就成功了呢,很显然不是,因为我们还没有验证过加速性能,而这也是我们的最终目的。那么如何理解分布式框架的并行性能?性能不达标如何调试?
Scaling Efficiency[4]
HPC中的一个常见任务是测量应用程序的scalability(可伸缩性)(也称为scaling efficiency伸缩效率)。这个度量表明当使用越来越多的并行处理元素(cpu/cores/processes/threads/etc)时,应用程序的效率有多高。
衡量给定应用程序的并行性能有两种基本方法,这取决于一种应用程序是否受cpu限制还是内存受限。它们分别称为Strong Scaling(强标度)和Weak Scaling(弱标度)。
Strong Scaling
在这种情况下,问题大小保持不变,但处理单元的数量增加了。
假设通过一个进程单元完成工作的时间是t1,使用N个单元完成同样工作量的时间花费是tN,则指标公式为:
Weak Scaling
在这种情况下,指定给某一个处理单元的问题量是一定的,另外的处理单元用于处理更多的问题(比如一个节点的内存容量无法容纳问题的情况)。
假设通过一个处理单元完成一份工作的时间是t1,用N个单元完成N份工作的时间是tN,则指标公式为:
综上,对于数据并行的分布式架构来说,有多少节点,就应该有大概多少倍的加速性能。比如单机跑完一个epoch需要10min,10个节点的分布式训练理论上需要1min(实际中会大于1min)
Horovod性能调优[5]
如果分布式架构的并行性能不满足上面提到的指标怎么办?可以参考一下方法:
1. 查看网络带宽
ethtool eth0
千兆带宽可以满足一般的分布式训练的需求,笔者遇到过百兆端口,分布式性能完全上不去。
2. 查看GPU显存占用及使用率
如果batchsize设置正常,各个GPU节点显存占用应该大体相同。如果datapipeline设置正常,GPU使用率应该在80-95%。data pipeline的设置,可查看官方代码。
3. 使用horovod timeline 性能分析
如果遇到GPU占用或使用率异常,可以通过horovod提供了的记录活动的时间轴工具,叫做horovod timeline分析,使用方法通过--timeline-filename开启并通过chrome://tracing查看。
4. 使用mpirun代替horovodrun
horovodrun提供了一个基于openMPI封装的可以简便控制horovod脚本的方法。而在一些场景下我们需要更细粒度控制传递给openMPI的选项,horovod支持使用Open MPI的命令直接控制horovod训练脚本。
horovodrun -np 4 python train.py
等价于
mpirun -np 4
-bind-to none -map-by slot
-x NCCL_DEBUG=INFO -x LD_LIBRARY_PATH -x PATH
-mca pml ob1 -mca btl ^openib
python train.py
注:
MPI:英文全称是Message Passing Interface,信息传递接口,是独立于语言的通信协议(标准),是一个库。
openMPI:英文全称是open Message Passing Interface。openMPI是MPI的一种实现,一种库项目。
分布式训练参数调优
horovod搭建分布式需要对源代码进行相应的侵入式修改,以pytorch为例
1.初始化
# Horovod: initialize library.
hvd.init()
2.每个GPU分配一个进程
# Horovod: pin GPU to local rank.
if torch.cuda.is_available():
torch.cuda.set_device(hvd.local_rank())
3.根据节点数线性缩放学习率
lr=arg.lr * hvd.size()
4.包装优化器
hvd.DistributedOptimizer()
5.把初始参数广播到各节点
hvd.broadcast_parameters(model.state_dict(), root_rank=0)
hvd.broadcast_optimizer_state(optimizer, root_rank=0)
6.定义分布式数据迭代器
train_sampler = torch.utils.data.distributed.DistributedSampler(trainset, num_replicas=hvd.size(), rank=hvd.rank())
dataloader = torch.utils.data.DataLoader(trainset, batch_size=arg.batch_size,num_workers=arg.workers, pin_memory=True, sampler=train_sampler)
具体可参考官方示例,这里需要强调的有两点:
1. large batch与learning rate[6]
分布式训练的学习率需要等于非分布式时的学习率×节点数,即:
在数据并行的情况下,就相当于增大了batchsize,增大的倍数等于节点数。从理论上来说,因为 batch_size 的增大会导致你 update 次数的减少,所以为了达到相同的效果,学习率应该是同比例增大的。
换句话说,通常当我们增加batchsize为原来的N倍时,要保证数据利用率相同(经过同样的样本后更新的权重相等),按照线性缩放规则[7],学习率应该增加为原来的N倍。
但是如果要保证权重的方差不变,则学习率应该增加为原来的sqrt(N)倍,目前这两种策略都被研究过,使用前者的明显居多。
直观理解:
假设蓝色和橘色块分别为同一训练数据的不同batchsize划分,显然橘色batchsize是蓝色的两倍。采用蓝色划分的数据进行4次迭代到达一个点,采用橘色划分的数据进行只能进行2次迭代,此时若学习率相同,则橘色数据只能达到一半的优化效率。所以,一个直观有效的方式就是增加学习率为蓝色的2倍。
2. Learning Rate Warm up
在分布式训练中,需要对学习率进行预热。
当使用线性缩放规则放大lr,可能会在训练开始导致收敛的不够好,训练可能会直接爆炸,所以可能会需要一些 warmup 来逐步的把 lr 提高到你想设定的 lr。学习率预热主要有以下方式:
constant warmup
在开始的几个epoch中使用小的学习率,然后增大到大学习率
gradual warmup
在开始的几个epoch中线性的增加学习率到大学习率
总结
深度学习分布式分数据并行和模型并行,而不同的并行方式又对应着不同的通信拓扑结构及参数更新方式。本文重点讨论了数据并行下基于ring allreduce 的同步更新的分布式架构。
深度学习的分布式中还有很多细节,比如horovod中高级特性的应用,mpirun的细粒度控制等等都需要深入探索。但如果我们需要在当前的项目中短时间内低成本的引入一个高效的深度学习分布式架构,推荐按照如下思路:
- 基于docker快速部署horovod框架
- 运行benchmark或者demo,确认框架是否部署成功及其加速性能
- 性能调优(如第2步性能未满足要求)
- 侵入式修改训练脚本,运行并再次确认加速性能
- 性能调优(如第3步性能未满足要求)
- 分布式训练并调试参数,确认训练效果
本文从分布式深度学习现状出发,介绍了分布式中的一些基本概念及笔者自己的一些思考,最后引出了基于docker部署horovod框架的实践。
参考
[1] Distributed Deep Learning with Horovod
http://yuchaoyuan.com/2019/12/09/Horovod/
[2] horovod官方文档
https://horovod.readthedocs.io/en/latest/summary_include.html#why-horovod
[3] horovod多机多卡启动指南
http://chaopeng.name/2020/01/03/horovod%E5%A4%9A%E6%9C%BA%E5%A4%9A%E5%8D%A1%E5%90%AF%E5%8A%A8%E6%8C%87%E5%8D%97/
[4] Measuring Parallel Scaling Performance
https://www.sharcnet.ca/help/index.php/Measuring_Parallel_Scaling_Performance
[5] Why is your Horovod slower than the usual?
https://towardsdatascience.com/why-is-your-horovod-slower-than-the-usual-201b4b8574d5
[6]如何理解深度学习分布式训练中的large batch size与learning rate的关系?
https://www.zhihu.com/question/64134994
[7]Accurate, Large Minibatch SGD:Training ImageNet in 1 Hour
https://arxiv.org/abs/1706.02677v1