循环删除unordered_map:https://stackoverflow.com/questions/15662412/how-to-remove-multiple-items-from-unordered-map-while-iterating-over-it
g++ -std=c++11 Vocabulary.cpp -o Vocabulary
./Vocabulary -train demo.txt
2015《Scalable Distributed DNN Training Using Commodity GPU Cloud Computing》
作者:Strom
Abstract
1.对象:DNN GPU Gloud
2.方法:独立控制每一个权重的更新频率。
3.效果:总的通信量降低了三个数量级,收敛率(convergence rate)和准确率(accuracy)没有降低。
1.Introduction
3.并行训练也可以转化为模型并行问题。对于稀疏结构化连接层(CNN),可以有效分配每层的计算;对于全连接层,每一层可以由不同节点进行处理。由于紧密连接的网络,模型并行受到DNN层数的限制,因此本篇工作只研究数据并行的问题。(数据并行可以和模型并行进行合并)
2.Method
2.1Data-parallel distributed SGD
减少通信量的方法:1.减少同步的频率 2.压缩梯度
2.2Two observations
1.很多SGD加速方法可以看作是延迟更新的变体:minibatch、Momentum、Nesterov、double buffering、asynchronous SGD
2.对于全连接DNN来说,子梯度十分稀疏,因此对于接近于0的梯度,可以延缓更新。
2.3Compaction and dead reckoning
2.string compaction(与GPU有关的技术,好像和本文关系不大?)
3.dead reckoning:所有计算节点的更新逻辑相同(没什么,不值得再看)
2.4Gradient residual
1.由于不同权重的大小不同,因此不能简单丢弃低于阈值的梯度。
2.所有权重最后都被更新。
2.5Quantization and compression
1.索引和梯度都被pack,最终一个键值对以32 bit的int表示。
2.量化误差(error feedback)要被累加到residual
3.进一步压缩:entropy coding——不记录值的大小,记录值之间的差值。(3倍压缩率,因为需要额外的时间成本,因此本文没有使用)
2.6Pseudo code
一个minibatch的过程:
1.从其他节点接收、解压梯度,累加到本地模型参数上
2.加载一个minibatch的训练数据
3.BP计算子梯度Gs
4.累加梯度:Gr=Gr+Gs
5.重置消息Map:M
6.对于每个元素gi:
7.若gi>+阈值(t):
将{i,+t}加入到M中
gi=gi-t
否则若gi<-阈值(-t):
将{i,-t}加入到M中
gi=gi+t
8.压缩M,然后发送到其他节点
3.Experiments
3.1环境:AWS
3.2用单机结果作为baseline
3.3
1.使用了预训练;
2.另外,为了避免低性能,第一个epoch,使用了更小的minibatch size和计算节点。(为什么可以改善)
实验的其他细节省略了
4.Discuss
该方法不能无限制扩展节点:
1.随着节点数目增大,batchsize过大,容易不收敛。(1.节点数目和batch size是什么关系? 2.batchsize过大,为什么不收敛?)
2.随着节点数目增多,通信可能再次成为瓶颈。
5.Conclusion
1.该方法有效扩展了规模:80GPU
2.模型越大,方法的优越性越显著
Hello World
Welcome to Hexo! This is your first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.
Quick Start
Create a new post
1 | $ hexo new "My New Post" |
More info: Writing
Run server
1 | $ hexo server |
More info: Server
Generate static files
1 | $ hexo generate |
More info: Generating
Deploy to remote sites
1 | $ hexo deploy |
hbsun
More info: Deployment
2014《1-Bit SGD and its Application to Data_Parallel Distributed Training of Speech DNNs》
作者:Frank Seide 、 Dong Yu
摘要:
1.主要思想:只要量化损失(quantization error)被带到下一个minibatch,就可以实现1-bit的无损压缩。
2.方法:和Adagrad、自动minibatch大小调节、双缓冲、模型并行结合。
3.压缩率:没有提及。只是说了提速多少。
Introduction
4.R-prop方法好像可以直接丢弃上一个minibatch的信息(这个不确定,后续调研一下R-prop方法)
5.介绍其他加速训练的方法:低阶近似(low-rank approximations)、二阶方法(Hessian Free)、模型平均、ADMM。(这些方法都不了解,后续可以看一下,但是都好像和梯度压缩无关,所以暂时略过)。
6.第六段和DNN的关系比较大,后续问问wamingchen。
和Hogwild/ASGD不同,该方法是确定性不改变收敛性能的方法。(比较怀疑这个说辞)
2
2.1数据并行分布式SGD
讨论了通信时间和计算时间的关系,并由两者确定最佳的节点数量K。(其中的公式没有看懂,后续一定要再看一下!)
2.2利用双缓存对minibatch进行切分
将minibatch分成前一半和后一半。push前一半梯度的同时,计算后一半的梯度。(文献19、8应该是介绍了这个技术)
3. 1-bitSGD with Error Feedback
1.量化误差不应该被抛弃,否则会导致不收敛。
2.通过公式可以看出来,就是计算出量化误差,然后累加到下一个minibatch当中。(下标表示什么没有看明白)
3.将0作为量化阈值(如何量化得????)
3.1集成梯度
每一个minibatch需要经历两次quantization。(这里也没想明白,回头再看看!)
4系统描述
1.增加minibatch大小可以增加数据并行度,但是太大容易导致模型不收敛(这个还不理解为什么?)。因此这个系统前期先在小数据上确定可以最大的minibatch size。
2.训练过程中使用交叉验证动态调整学习率的衰减系数。
3.使用了Adagrad,它可以收敛得更快,并且允许更大的minibatch size。
4.可以在三个地方使用Adagrad:
4.1 量化之前
4.2 量化之后:先集成子梯度,然后进行Adagrad
4.3 进行动量平滑之后
目前看,4.2效果最好。(文章也不知道为什么这样效果最好)
5实验
5.2 1-bit量化的影响
1.前期阶段没有使用梯度量化和并行化(冷启动:冷启动和热启动的区别是啥?)
2.error feedback很关键,一定要加上。
6结论
2017《DEEP GRADUIENT COMPRESSION》
作者:Song Han
摘要:
1.DGC:DEEP GRADUIENT COMPRESSION
2.前提:99.9%的梯度都是没有用的
3.方法:动量修正、本地梯度缩放、动量因子遮蔽、热启动。
4.压缩倍数[270,600]->压缩率[0.003703704,0.001666667]。注意,是无损压缩。
5.对象:CNN、RNN。
1.介绍
1.动量修正、本地梯度缩放:保证了无损压缩。
2.动量因子遮蔽、热启动:缓解了参数陈旧的问题。
2.相关工作
1.For instance, asynchronous SGD accelerates the training by removing gradient synchronization and updating parameters immediately once a node has completed back-propagation(这个后续了解一下)
2.梯度量化:1—bit SGD、QSGD、TernGrad(3-level)、DoReFa-Net(1-bit weights with 2-bit gradients)
3.梯度稀疏化:主要思想是利用thresold进行裁剪。具体方法参加原文。
4.DGC:600倍压缩,无损,不需要额外的layer normalization,无需更改模型结构。
3.DGC
3.1梯度稀疏化
1.仅传输重要的梯度,简单认为绝对值大的梯度就重要(这个真的对吗?)。
2.要累积没有发送的梯度,在以后进行发送。保证到最后所有的梯度都被发送了。
3.使用encode方法进行发送(没有看懂encode方法:The encode() function packs the 32-bit nonzero gradient values and 16-bit run lengths of zeros.)
4.本质上就是将minibatch的大小由Nb增长为NbT。(具体见公式)
3.2增加本地累积的准确率
一旦单次传送梯度的稀疏度很高,就破坏了收敛。动量修正、本地梯度缩放可以减缓这个问题。
动量修正
当使用动量算法作为优化器时,直接传送裁剪后的梯度就忽略了discounting factor,破坏了收敛。
解决方法:由传输梯度改为传输动量:本地计算动量,裁剪动量(相应也是累积动量),传送动量。
(感觉逻辑上有问题,本地的动量和全局动量应该不是一回事儿吧)
本地梯度缩放
前人方法:梯度的L2范数总和超过阈值就进行缩放,步骤:收集各个节点梯度,求和,缩放,累加。
DGC:利用乘上sqrt(N)进行缩放;步骤:先计算本地梯度G(t),然后进行缩放,然后加上G(t-1)。(这里有一个疑问:如果先加和再缩放呢?)
3.3避免参数陈旧问题
当模型稀疏度达到99.9%时,T=[600,1000],因此参数陈旧问题很严重。
动量因子遮蔽
步骤见公式
在公式7中,动量和累积量使用相同的Mask,没有解释原因,但从上文看,好像这样就足够好了。
注意这里面的thresold应该是和“3.1梯度稀疏化”中的thresold不同。
(疑问:其实就是去除了绝对值比较大的累积量,但是绝对值大就一定是陈旧的吗?为什么不用遍历次数进行衡量?)
热启动训练
结论:训练早期不应该进行梯度稀疏化。原因:早期的梯度有多样性和攻击性,如果不让发送就会限制变化范围,从而减缓收敛;累积早期梯度可能会误导收敛方向。
采用的方法:对于神经网络,训练早期采用攻击性小的学习速率,以减缓网络的变化速度;早期减缓梯度的稀疏度;渐变过程:随着epoch的增加, 不是线性增加学习速率,而是指数增常梯度稀疏度,文章认为这样可以帮助训练更容易适应较大的梯度稀疏度(为什么?)。
4实验
5系统分析和性能
在梯度稀疏化时,确定thresold很耗时。因此采用采样方法确定阈值。
利用 Wen et al. (2017) 提出模型进行测试。
调整集群数量和网卡配置进行对比。
6结论
未来将要分层设置thresold,用来加速梯度稀疏化的速度。
2017《Sparse Communication for Distributed Gradient Descent》
作者:Aji
Abstract
1.压缩率:99%
2.结合:和梯度量化结合
3.对象:MNIST(大多数配置都表现良好)、神经网络机器翻译任务(不同配置各有好坏)
4.MNIST:49%的提速;NMT:22%的提速。注意:都没有损失准确率。
1.Introduction
2.Related Work
介绍了一些文章,我都下载了,看一下!
3.Distributed SGD
4.Sparse Gradient Exchange
2.直接将小于阈值的梯度设置为0会破坏收敛性,因此需要将它们累加到下一个minibatch。
4.取样0.1%选取thresold
5.分别是用了local thresold和global thresold(结合了layer normalization)
5.Experiment
5.1确定压缩率(这个可以动态调整吗?)
2.99.9%的压缩率导致很不好的压缩性能,99%的几乎不影响性能,
5.2局部threshold VS 全局thresold
1.只实现了thresold_push,没有实现thresold_pull(Based on the results and due to the complicated interaction with sharding, we did not implement locally thresholded pulling, so only locally thresholded pushing is shown.)
2.layer normalization只对NMT有效,对MNIST几乎没什么影响。
5.3收敛速度
通过缩小minibatch大小,强行增加通信时间,然后说自己的梯度压缩增快了收敛速度,这个实验稍显牵强。
5.4 1-bit quantization
1.列举了一些梯度量化的方法
2.三种层次的量化方法:min thresold、column、_wise average thresold、global average。(实验图看 应该是global average thresold效果最好 感觉这个依赖具体场景)
3.1-bit可能影响收敛速度,2-bit一般是足够的,因为它可以将小梯度和大梯度区分开了。
Conclusion
2017《AdaComp:Adaptive Residual Gradient Compression for Data-Parallel Distributed Training》
作者:Chia-Yu Chen
写在前面:
这片论文的引用量目前只有被Hansong引用过,感觉算不上是经典论文,但提出了自适应的思路,因此看了一下。
Abstract
1.压缩率:对于全连接层:200倍压缩;对于CNN,40倍压缩。
2.优势:适用于各种领域(vision、speech、language),各种数据集,各种优化器。
3.方法:稀疏化和量化
4.总结:AdaComp是基于本地的梯度累积进行选择,并且可以本地情况自动调节压缩率。
1.Introduction
3.之前的工作都是对全连接层进行压缩,本文提出的工作还适用于卷积层等多种复合结构,并且准确率和收敛率的损失可以忽略不计。
2.Residual gradient compression
Background
Strom 2015:只有梯度值超过指定阈值,才会被量化为1bit进行传输。
之前的工作都没有讨论如何选择阈值。
Dryden:不是选择固定阈值,而是固定每次的传输百分比,但需要预先对所有梯度进行排序,这是一个代价比较高的计算任务。
所有的工作都基于一个准则:未被发送的梯度都累积了下来。
Observations
第一个发现:
对于裁剪来说,不应该只考虑梯度的大小,还应该考虑数据和数据对特征活跃度的影响。因为可能有的梯度虽然值很小,但它们可能连接的高活跃度的输入特征。
累积梯度对特征活跃度几乎没有什么影响。
第二个发现:
对于神经网络来说,网络结构、层的类型、minibatch的大小、其他参数都会影响压缩率,因此需要动态地、自动地调整压缩率。
最后,为了减小成本,本文避免了排序,并且基于本地数据进行压缩。
Adaptive Residual Gradient Compression(AdaComp) Technique
1.其实核心思路就是将所有参数进行了分桶(第一段记录了策略:根据网络结构进行分桶),先找到每一个bin中的累积量的最大值,然后一旦(累积量+factor*新的梯度)超过了这个最大值,就把(累积量+新的梯度)发送出去。
伪代码
residue:之前的累积量
dW:本轮计算的梯度
1 G=residue+dW
2 H=G+dW(其实就是H= residue+2*dW,2就是factor)
3 对于所有的桶:
计算该桶中的最大值,记为gmax
4 对于每一个桶:
对于桶中的每一个值:
若 Hi>gmax:
就将G进行量化、发送
residual记录了量化误差
否则:
residual记录了G,保存在本地。
3.实验
这个没来得及看,后续看一下。
4.讨论
这个没来得及看,后续看一下。
优化方法:梯度下降 总结
1.按照批次大小分类
可以分为随机梯度下降、批量梯度下降,二者的原理和性能有很多相关资料,不再赘述。其中,批量梯度下降方法性能更好,使用更为普遍,wuliang系统使用的就是批量梯度下降方法,目前在LR中batch size设置为1000,以后可以更改一下,对比一下实验效果。
2.按照优化方法进行分类
参照《深度学习》一书,目前主流的优化方法有分为基本优化算法、自适应学习率算法、二阶近似方法。wuliang系统原本支持传统的sgd,我此次共实现了六个算法,包括两个基本算法(Momentum、Nesterov)和四个自适应学习率算法(AdaGrad、RMSProp、Adam、AMSGrad)。
其中Adam在深度学习当中应用十分广泛,AMSGrad是ICLR 2018论文刚刚提出的新算法,受到了广泛的关注。目前集群资源比较紧张,方便起见我此次主要对比Sgd、Adam、AMSGrad的效果。
3.实现思想
基于wuliang原本的lr和sgd设计思想,我既可以将优化方法放到worker端(KVWorkerTableMultiThread.h),又可以放到server 端(AdamServerTable.h),但是最后决定放到server端,主要基于以下几种考虑:
- server端目前只负责存储,没有大量的计算任务,CPU很空闲,优化方法放到server端实现可以利用这些空闲的CPU,减少worekr端额外的计算负载。
- 如果放到worker端,需要本地另申请空间存放一阶矩和二阶矩,并且需要在所有epoch当中都需要一直存储,直观上感觉会占有比较大的空间。
- Adam中有一个变量是时间步t,t随着计算梯度的次数而增加,单机实现的话属于全局参数,现在在分布式环境中,如果放到worker端就会变成局部参数(以worker为单位),放到server端就还是全局参数。
- 放到worker端,一阶矩和二阶矩需要用unordered_map存储;放到server端可以使用vector存储,索引时间有优势。
前五种算法实现参照《深度学习》一书,AMSGrad参照其论文和网上资料实现。
4.实验
数据:来源于Tencent业务侧数据,现在选取的是前一天的数据(2018.4.1)做train(53022663行),后一天的数据做test(53022665行),特征维度是百亿,数据稀疏度是(0.001 即1000个空间中只有一个有值)。
场景:LR算法用于点击率预估
超参数:
每个worker只开了两个task(wahmingchen说开两个线程基本就CPU跑满了)
minibatch=1000
epoch=50
reg=3
reg_value=0.1
consistency=bsp
1. sgd: learningrate=0.1(wahmingchen说这个最好)
2. Adam(实验效果最好,也和书上的建议值完全一致):learningrate=0.001 rho1=0.9 rho2=0.999
3. Amsgrad:learningrate=0.001(虽然论文建议学习率对比Adam要小一点,但是公平起见,还是和Adam一样了) rho1=0.9 rho2=0.999
对比train_logloss
对比train_auc
对比test_logloss
对比test_auc
对比train_test_logloss
对比train_test_auc
分析:
auc和logloss变化基本一致
train中AMSGrad拟合最好(与其他两个有明显差别,甚至auc到达了0.77,这个想想为什么?),但是test中Adam效果最好(比sgd的auc提高了2%)
train_test_auc中,sgd的train和test基本一致,但是Adam和Amsgrad中train和test相差比较大。(也不知道为什么)
其他
另外:
- AMSGrad论文中没有采用偏差修正,因此上述实验我也没有偏差修正,实验了一下有偏差修正的AMSGrad,无偏差修正效果更好一点,但是只跑了三轮迭代,可能没有什么说服力。
Tencent无量 多线程push&pull实现
实现的初衷
对于参数服务器来说,影响速度性能的主要有两方面的因素:
第一是worker的计算能力,所以可以利用Eigen库对worker上的计算过程进行优化、减少计算的时间;
其次就是网络通信,worker和server都需要对消息进行收发,并且经常要等到对方的消息才能进行后续操作。这方面我能想到的有两点可以优化的方向。
2.1 第一就是尽量减少网络带宽,以梯度g的push和pull为例进行说明。首先,我们可以牺牲梯度的精度用来减少网络带宽,之前g的精度是64位的double,如果可以用32位的float进行代替,就显著减少了网络通信量,这只是很初级的优化,百度的PaddlePaddle有了进阶的优化–梯度量化。另外,也可以进行User-defined Filters——支持用户自定义过滤器来过滤掉那些比较小的被push的entry。需要说明的是,这只是我知道的两种方法,由于此篇的重点不在此,就不详细展开了。
2.2 第二就是缩短对于通信消息的处理时间,对于无量系统来说,之前是单线程对消息进行处理,以server为例进行说明,PSServer拥有一个Customer对象负责消息的收发,通过std::bind对PSServer::process函数进行绑定,消息的处理流程都是在process里面进行的,对于Customer的实现机制我还不太了解,但是通过process的函数参数可以看到每次只能处理一条消息,具体流程是收到消息,对消息进行拆解,然后调用相关函数对消息的请求进行处理,最后发回反馈信息,对于server来说,每一时刻只能处理一条消息,这样即使此时还收到了其他消息,这些消息的处理也需要被阻塞,进而消息的发送者也需要等待,这就增加了网路通信的时间成本。
如何优化
通过2.2的瓶颈分析,我们发现server端“每次只能处理单条消息”大大增加了网络通信的延时,另外目前server主要的功能就是参数的存储,也就是说对Memory的利用比较充分,但是由于server没有什么复杂的计算,所以对CPU的利用率很低。自然,我们可以想到的是在server端通过多线程对消息进行处理,这样既可以减少网络的通信时间,也可以增加CPU的利用率,何乐而不为呢?
第一个想法:是对于push和pull各开一个线程池,分别有自己的消息队列。process收到消息后,先判断是push还是pull,meta.push==1就塞进push的消息队列,meta.push==0就塞进pull的消息队列(注意这里还需要仔细区分clock或者predict等操作,我还没仔细看,就不详细展开了,后续代码上也需要做进一步处理)。线程池中的线程直接从各自的消息队列中拿到消息,然后独自进行处理。这样,server端就可以实现同时对多个消息进行处理。进一步思考,我们发现push消息和push消息同时进行处理,pull消息和pull消息同时进行处理,push消息和pull消息同时进行处理。这三种并行机制有没有什么问题呢,对于pull来说,是从server读消息,所以同时pull没有问题;但是push是写参数,如果同时写到同一个参数,或者同时读写一个参数,由于对server_table没有加锁,就会出现逻辑问题(什么问题??)(也可能core掉)。另外,由于push和pull是独立进行处理,所以在pull一批参数的时候,worker可能获得不同迭代次数的参数,记得这样会导致收敛变慢或者不收敛的问题(这个具体记不清楚了,需要后面查一下资料),但好像单线程处理也会出现这样的问题啊?或者说这个问题是问题吗?
第二个想法:第一个想法的数据一致性问题比较严重,因此做一下改进。在多个server存在的情况下,我们是将一个大的模型表分布式存储在各个server中,每个server独自负责表的一部分——也就说按照server为单位对模型表进行划分,依此类推,我们更加细化——按照线程为单位对模型表进行划分。在“server独自负责表的一部分”基础上,一个server中的每个线程独自负责“一部分”的一部分。定性分析一下如此实现和之前的不同:
- 对于worker端来说,之前每次发送请求的时候是需要计算每个数据应发送到哪个server就行,即按照server为单位对数据进行拆分(partition)和打包发送(send),现在在WorkerImp中请求的数据key是按照线程为单位进行划分,即首先按照server为单位进行划分,然后再按照线程为单位进行划分,因此需要对partition函数进行重写,即增加了计算量和处理流程。
- WorkerImp原来针对每个server发送一次请求——数据包比较大,现在WorkerImp针对每个线程发送一次请求,即把原来的大数据包拆分成了小数据包,这样每次的数据包比较小,但是发送消息的频率显著增加了,倍数略等于server端开的线程数目。把大包拆解成小包,是能加快通信还是延缓通信,这个应该到生产环境中进行评测的。
- 考虑极限情况,频繁进行push和pull操作的话,消息队列的长度将会非常大。
实现细节
worker端:
WorkerImp增加两个函数multi_pull和multi_push,和之前pull、push的不同在于分线程对key进行处理,分别发送消息。这里有一个小坑:在worker端msg.meta.threadindex要被赋值,这样server端才能根据threadindex将不同的消息分发给不同的线程,但是仅仅在这里设置是无效的,在van.cc里面,函数Van::PackMeta还需要进行pb.set_threadid(meta.threadindex)设置,对应Van::UnpackMeta还需要进行meta->threadindex = pb.threadid()设置,因此需要对meta.proto也增加threadindex字段。
目前只是在multi_pull和multi_push里面对msg.meta.threadindex进行了赋值,对于clock操作没有实现multi_clock功能,因此在server端都是threadindex=0的线程执行clock操作,之所以没有实现主要是考虑到clock操作本身就很复杂,并且调用的频率应该不是很频繁,因此直观上感觉指定threadindex=0的线程进行处理也没有什么问题。
新增文件kv_worker_table_multi_thread.h,和kv_worker_table.h对比,其实就是重新实现了partition函数,其中函数参数out的类型发生了改变,比原来的out内部多了一层vector,用来存储不同的线程应该接收到的消息。目前只实现了hash_partition,没有实现range_partition。
在lr_minibatch.cc里面需要在void StartServer()和void RunWorker()分别设置
‘’’GlobalContext::Get()->set_server_thread_num(server_thread_num);’’’
因为是两个进程嘛,在全局参数的设置和读取其实都是独立的,需要各自去读配置文件。
ps端:
- 增加了solve_thread.h,它内部维护一个消息队列,可以进行push、pull、clock、predict操作。
- ps_server.cc改动比较大,首先构造函数里初始化线程池,注册receive函数,receive函数可以针对不同的请求调用相关的函数。process函数和之前的功能不同了,现在只是将消息insert到不同的线程的消息队列中。
- (之前有一个问题没有发现,就是clock比较特殊。在server端处理worker端的clock请求的时候,所有的其他线程都不可以再进行其他原子请求的处理了(push、pull),因为)——这个仔细想了一下,应该是不用加锁,后续再想想实验一下。
其他说明:
- 注意和实现之前的优化算法不同,对于lr_minibatch.cc等这些有main函数的文件来说,我们只需要更改worker注册的模型表(因为需要更改消息分发的机制),比如之前用的是KVWorkerTable,现在用的是KVWorkerTableMultiThread,对于server端来说没有任何变化(优化算法仅需要更改server端的注册模型表)。
性能评测
评测主要在传统sgd和adam上进行,这两个算法在server端的逻辑有不同。
先看server端单线程的情况:对于sgd来说,单纯看pull和push的耗时,是相同的,都是0.001秒;而对于Adam来说,push操作具有比较繁琐的计算过程,因此push的操作是0.01秒,pull的耗时不变——还是0.001秒。我打印了server端的消息队列长度,对于sgd来说,队列的长度一般是0、1、2,即基本没有阻塞,server端的push和pull操作的for循环很快,不是server和worker交互时的瓶颈;但对于adam来说,push操作很耗时,因此server端复杂的计算过程成为了交互时的瓶颈,消息队列的长度是180、190这么长。因此,adam一个minibatch时间都很长(3.5s左右),一个epoch耗时208min。sgd一个minibatch时间约为0.5s(0.3s——0.6s之间),一个epoch耗时34min。
现在在server端多线程push&&pull(线程池大小为40,因为server申请了40个CPU),对于adam来说,每个线程的消息队列长度基本就是0了,峰值的时候会到达18、19(极少情况会出现,但是没有想明白为什么会出现峰值情况)。每个线程的消息队列长度基本就是0,是因为每个push和pull操作相对于单线程时需要处理的量减少了很多(40倍),因此for循环的时间也急剧减少(pull的耗时为0,push的耗时一般也为0,有时会为0.001),这样就会和单线程sgd的情况一致了,server端的计算过程不会是两者交互的瓶颈,因此一个minibatch的时间基本和单线程sgd的时间差不多——0.6左右(0.6-0.8区间,除掉WorkerImp::process一些无用日志后,基本也就是0.5了),一个epoch耗时41min(除掉WorkerImp::process一些无用日志后,耗时缩减为33min),因此提升了6倍的速度。对于sgd来说,一个epoch耗时为32min,性能提升不多的主要原因是因为server端的for循环基本不是两者交互的瓶颈(从消息队列长度一般是0、1、2就可以看出来),有一点点提升是因为单线程有时消息队列还是被阻塞的(长度为1、2的时候)。
总结
之前其实比较纠结,相对于单线程来说,多线程处理到底好在哪里。现在知道了,通过多线程其实减少了server端for循环的耗时,只要for循环是server-worker交互的瓶颈,那么通过多线程可以将for循环的长度减少了,极大消除了瓶颈问题。