Horovod是一套面向TensorFlow的分佈式訓練框架,由Uber構建並開源,目前已經運行於Uber的Michelangelo機器學習即服務平臺上。Horovod 能夠簡化並加速分佈式深度學習項目的啓動與運行。通過利用消息傳遞接口(簡稱 MPI)實現應用環狀規約,顯著提升 TensorFlow 模型的實用性與性能表現。
本文作者在實踐中發現,TensorFlow集羣存在諸多缺點,如概念太多、學習曲線陡峭、修改的代碼量大、性能損失較大等,而Horovod則讓深度學習變得更加美好,隨着規模增大,Horovod性能基本是線性增加的,損失遠小於TensorFlow。
當數據較多或者模型較大時,爲提高機器學習模型訓練效率,一般採用多GPU的分佈式訓練。
按照並行方式,分佈式訓練一般分爲數據並行和模型並行兩種:
-
模型並行:分佈式系統中的不同GPU負責網絡模型的不同部分。例如,神經網絡模型的不同網絡層被分配到不同的GPU,或者同一層內部的不同參數被分配到不同GPU;
-
數據並行:不同的GPU有同一個模型的多個副本,每個GPU分配到不同的數據,然後將所有GPU的計算結果按照某種方式合併。
注意,上述中的不用GPU可以是同一臺機上的多個GPU,也可以是不用機上的GPU。
當然也有數據並行和模型並行的混合模式。
因爲模型並行各個部分存在一定的依賴,規模伸縮性差(意思是不能隨意增加GPU的數量),在實際訓練中用的不多。而數據並行,則各部分獨立,規模伸縮性好,實際訓練中更爲常用,提速效果也更好。
數據並行會涉及到各個GPU之間同步模型參數,一般分爲同步更新和異步更新。同步更新要等到所有GPU的梯度計算完成,再統一計算新權值,然後所有GPU同步新值後,才進行下一輪計算。異步更新,每個GPU梯度計算完後,無需等待其他GPU的梯度計算(有時可以設置需要等待的梯度個數),可立即更新整體權值,然後同步此權值,即可進行下一輪計算。同步更新有等待,異步更新基本沒有等待,但異步更新涉及到梯度過時等更復雜問題。
在實際應用中,單機多卡的同步式數據並行是最常用的,在論文中最常見的訓練方式是單機八卡。數據再多時,一般就需要多機多卡了。
無論是單機多卡,還是多機多卡,均是分佈式訓練,在Horovod出現之前,使用TensorFlow,一般只有官方推薦的集羣訓練方式。
可是TensorFlow的集羣訓練,用起來並不輕鬆。
TensorFlow集羣的缺點
1. 概念多,學習曲線陡峭
TensorFlow的集羣採用的是Parameter Server架構,因此引入了比較多複雜概念,羅列如下
server
client
master
cluster
parameter server
worker
job
task
replica_device_setter
master service
worker service
clone
複製代碼涉及到的函數
tf.train.Server
tf.train.Supervisor
tf.train.SessionManager
tf.train.ClusterSpec
tf.train.replica_device_setter
tf.train.MonitoredTrainingSession
tf.train.MonitoredSession
tf.train.SingularMonitoredSession
tf.train.Scaffold
tf.train.SessionCreator
tf.train.ChiefSessionCreator
tf.train.WorkerSessionCreator
複製代碼我反覆研究過多次,還是沒有徹底弄清楚server、client、master、master service、worker service、clone、session之間的關係。
大致是,在client中創建server實例,session與server一一對應,server內含master service和worker service兩個服務,master service負責與外界通訊,比如sess.run一般都是告訴server的master service要開始工作了,server的master service通知同一個server的worker service去幹活,worker service調動GPU運算,完成後,返回結果給master service,做權值更新,如果是多機多卡的分佈式,Parameter Server與master service之間做梯度傳遞和權值同步。(參考https://stackoverflow.com/questions/38732502/tensorflow-master-and-worker-service)
2. 修改的代碼量大
如果想把單機單卡的模型,移植到多機多卡,涉及的代碼量是以天記的,慢的話甚至需要一週。
3. 需要多臺機子跑不同的腳本
TensorFlow集羣是採用Parameter Server架構的,要想跑多機多卡的集羣,每個機子都要啓動一個client,即跑一個腳本,來啓動訓練,100個機子,人就要崩潰了。
4. PS和Worker的比例不好選取
TensorFlow集羣要將服務器分爲PS和Worker兩種job類型,PS設置多少性能最近並沒有確定的計算公式。
5. 性能損失較大
TensorFlow的集羣性能並不好,當超過一定規模時,性能甚至會掉到理想性能的一半以下。
Horovod
由於TensorFlow集羣太不友好,業內也一直在嘗試新的集羣方案。
2017年Facebook發佈了《Accurate, large minibatch SGD: Training ImageNet in 1 hour 》驗證了大數據並行的高效性,同年百度發表了《Bringing HPC techniques to deep learning 》,驗證了全新的梯度同步和權值更新算法的可行性。受這兩篇論文的啓發,Uber開發了Horovod集羣方案。
約定如下:
網絡帶寬記爲:B(單位Mb/s),
模型總參數數據量記爲:D(單位Mb),
總服務器數量記爲:n,
參數服務器數量記爲:n_p(其中有n= n_p+ n_w),
worker服務器數量記爲:n_w(其中有n= n_p+ n_w),
單服務器計算一次耗時記爲:T_0
梯度同步和權值更新算法
1) Parameter Server架構
TensorFlow的集羣架構是Parameter Server架構,數據的傳導模型如下圖。
則可以計算出,Parameter Server架構的集羣方案,總耗時:
可以看出T與總節點數n基本成線性關係,但不同的參數服務器和woker服務器分配方案,總性能也將不同。
假設,e表示worker服務器佔比,即e=n_w/n,則可以計算出最優的e值爲:
可以看出,最優worker服務器佔比與模型大小、網絡帶寬、單機運行時長都有關係,並不是一個一眼能最優值的超參數。
2)Horovod的ring-allreduce算法
百度2017年發表的《Bringing HPC techniques to deep learning 》中,採用了全新的梯度同步和權值同步算法,叫做ring-allreduce。此種算法各個節點之間只與相鄰的兩個節點通信,並不需要參數服務器。因此,所有節點都參與計算也參與存儲。
一次權重更新,主要包含兩個過程:
1)累計梯度
將所有梯度分爲n個片段,每次只與相鄰節點傳遞1個片段的梯度,n-1次後,每一片段的梯度都完成了所有節點這一片段梯度的累計,但不用片段的累計值分佈在不同節點上。如下圖的第2、第3步;
2)將累計後的梯度分發到所有節點
將第一步累計的梯度再次通過n-1次的相互交換後,所有節點的梯度完成同步。如下圖的第4、第5步。再平均後,更新權重,就完成了所有節點權重的更新。
可以計算出ring-allreduce算法的總耗時爲:
可以看出,總耗時基本與總節點數n成線性關係(n較大時,1/n基本爲0)。
Horovod的梯度同步和權值同步就採用了ring-allreduce算法。
概念
Horovod的數據傳遞是基於MPI,因此其涉及的概念也是MPI中的概念。以4個服務器,每個服務器4個GPU爲例,
-
size進程數量,也即所有GPU數量,爲16
-
rank 進程的唯一ID,0-15
-
local rank,每一個server中的進程的本地唯一ID,0-3
-
allreduce 累加所有數據,並同步到所有節點的操作,如下圖
- allgather 收集所有數據,並同步到所有節點的操作,完成後每個節點都包含所有節點的數據,並且這些數據單獨存在,如下圖。
- broadcast 將數據(需要由根節點確認)從一個節點傳播到其他所有節點的操作
大概就這麼多概念,簡單清晰。
將單機單卡改爲多機多卡
將一個只支持單機單卡的訓練腳本修改爲支持多機多卡的訓練腳本,以TensorFlow爲例,只需要做如下改動:
import tensorflow as tf
import horovod.tensorflow as hvd
# Initialize Horovod
hvd.init()
# Pin GPU to be used to process local rank (one GPU per process)
config = tf.ConfigProto()
config.gpu_options.visible_device_list = str(hvd.local_rank())
# Build model...
loss = ...
opt = tf.train.AdagradOptimizer(0.01 * hvd.size())
# Add Horovod Distributed Optimizer
opt = hvd.DistributedOptimizer(opt)
# Add hook to broadcast variables from rank 0 to all other processes during
# initialization.
hooks = [hvd.BroadcastGlobalVariablesHook(0)]
# Make training operation
train_op = opt.minimize(loss)
# Save checkpoints only on worker 0 to prevent other workers from corrupting them.
checkpoint_dir = '/tmp/train_logs' if hvd.rank() == 0 else None
# The MonitoredTrainingSession takes care of session initialization,
# restoring from a checkpoint, saving to a checkpoint, and closing when done
# or an error occurs.
with tf.train.MonitoredTrainingSession(checkpoint_dir=checkpoint_dir,
config=config,
hooks=hooks) as mon_sess:
while not mon_sess.should_stop():
# Perform synchronous training.
mon_sess.run(train_op)
可以看出,改動不大,只需添加10行左右的代碼,主要分爲6步:
1)初始化Horovod
hvd.init()
2)一個GPU與一個進程綁定
config = tf.ConfigProto()
config.gpu_options.visible_device_list = str(hvd.local_rank())
3)根據總GPU數量放大學習率
opt = tf.train.AdagradOptimizer(0.01 * hvd.size())
因爲BatchSize會根據GPU數量放大,所以學習率也應該放大。
4)使用hvd.DistributedOptimizer封裝原有的optimizer
opt = hvd.DistributedOptimizer(opt)
分佈式訓練涉及到梯度同步,每一個GPU的梯度計算仍然由原有的optimizer 計算,只是梯度同步由hvd.DistributedOptimizer負責。
5)廣播初始變量值到所有進程
hooks = [hvd.BroadcastGlobalVariablesHook(0)]
主要爲了確保所有進程變量初始值相同
6)只在worker 0上保存checkpoint
checkpoint_dir = '/tmp/train_logs' if hvd.rank() == 0 else None
防止checkpoint保存錯亂
Horovod只是需要改動必要改動的,不涉及Parameter Server架構的device設置等繁瑣的操作。
起訓練
在單機4卡的機上起訓練,只需執行以下命令:
horovodrun -np 4 -H localhost:4 python train.py
在4機,每機4卡的機子上起訓練,只需在一個機子上執行以下命令即可:
horovodrun -np 16 -H server1:4,server2:4,server3:4,server4:4 python train.py
注意無論是單機多卡,還是多機多卡,都只需在一個機子上執行一次命令即可,其他機Horovod會用MPI啓動進程和傳遞數據。
性能對比
Horovod隨着規模增大,性能損失遠小於TensorFlow,基本是線性增加的。
結論
用過TensorFlow集羣的人,會深刻體會到Horovod有多好用,感謝百度、Facebook和Uber讓深度學習更美好。
不過,也要注意到,Horovod的分佈式貌似只支持同步更新式的數據並行,模型並行和異步更新式的數據並行,我沒有嘗試過,根據ring-allreduce算法可知,應該是不支持的。