1. 程式人生 > >深度學習的分散式訓練--資料並行和模型並行

深度學習的分散式訓練--資料並行和模型並行

在深度學習這一領域經常涉及到模型的分散式訓練(包括一機多GPU的情況)。我自己在剛剛接觸到一機多卡,或者分散式訓練時曾對其中到底發生了什麼有過很多疑問,後來查看了很多資料,在這篇部落格裡對分散式的深度學習模型訓練做一個總結。

由於是我自己的總結,所以如果有說的不對的地方,還望指正。

當然我的總結也是基於一些文獻參考來的,所以如有雷同,純屬我借鑑別的大神的文章。

如果去查閱與分散式深度學習相關的資料,一般會看到兩個詞,模型並行和資料並行。

模型並行是指把模型的不同部分放置在各個裝置上,這樣整個叢集就像一個生產流水線一樣,對這樣的分散式設定我還沒有使用過,所以不在這裡細說。

資料並行應該是大多數人都會遇到的場景。資料並行分為同步更新和非同步更新,下面一一說明。

深度學習訓練的資料並行化,說白了就是梯度下降的並行化,所以想要理解你在並行化訓練一個深度學習的模型時到底發生了說明,你必須明白梯度下降時到底發生了什麼。

我們先從單機的梯度下降說起。

所有介紹梯度下降的參考書都會說梯度下降分為整體梯度下降(每次更新引數使用所有資料)和隨機梯度下降(每次更新引數使用一條資料),而在深度學習裡我們一般都會結合這兩種思想使用批梯度下降,每次更新引數使用一個batch的資料來更新引數。

一般我們可以把隨機梯度下降的公式寫成下面的形式:

這裡我沒有在θ上加任何標記,代指神經網路中隨意的一個引數,這個公式搞神經網路的應該很熟悉,這裡α是學習率,J(θ)

是損失函式,整個式子的意義就是引數θ的更新是用上一次迭代的θ減去學習率乘以損失函式對該θ的偏導,也就是梯度。計算該偏導數的時候,就會用到我們的樣本資料,不然無法計算J(θ)θ的偏導數

而深度學習常用的批梯度下降,是指每次更新時使用一個batch的資料量去更新引數,這句話的意思是說上面的公式中:


這個偏導數的部分,也就是梯度部分,是用這一個batch每一條資料計算出的梯度取平均數後再帶入公式計算。所以說這是一種結合了隨機梯度下降和整體梯度下降的方法。這麼做的想法來源其實就是用抽樣出來的樣本的平均數來代替整體的平均數。

理解上面這段話很重要,如果不理解這句話就沒辦法理解分散式梯度下降到底發生了什麼。

下面來說明分散式訓練時發生的事,這得分為同步更新和非同步更新,假設我們擁有這樣一個叢集:

引數伺服器(PS)1臺,記為ps,worker10臺,每臺上面都有一塊GPU,記為w1,w2……w10,而我們的資料自然也被分成了10份,每臺worker上一份,每一份都是不重複的,假設我們把batchsize設為128,並假設我們的模型一共有n個引數,再假設我們是在tensorflow上做模型的訓練。

那麼引數更新的過程大體如下:

引數伺服器收集每臺worker計算的梯度,更新引數,然後把更新後的引數再分發到各臺worker,接著各臺worker再根據新的引數計算梯度。

同步更新

同步更新的每一次迭代,每臺worker計算一個batchsize的梯度,在這裡對每一個引數就會生成一個128維的列向量,每一維代表根據一條資料計算出的梯度,所以最終會生成一個128xn的矩陣(或者是nx128,這得看怎麼實現的)。接著worker把這個矩陣傳遞給引數伺服器,引數伺服器等待所有的10臺worker把梯度矩陣都傳過來後(此時引數伺服器上對每一個引數,都收到了1280個梯度值),便會對每個引數的1280個梯度值做一個平均,做完平均就可以代入上面的梯度更新的公式更新引數。更新完引數再把新的引數值傳輸回每臺worker,每臺worker再根據新的引數值繼續計算梯度。

根據以上描述,我們可以得到關於同步更新的下面幾個事實:

  1. 雖然我們在tensorflow的程式碼裡設定了batchsize為128,但其實模型在真正更新的時候是用了一個1280的大batch在更新引數
  2. 同步更新的資訊傳輸開銷很大,而且有短板效應,如果其中一臺worker的GPU比其他幾臺的差,那麼每次更新時都需要等待這臺worker完成計算
  3. 同步更新並沒有加快每次迭代的速度,相反,由於巨大的通訊開銷,每次迭代的時間可能比單機模型下還要慢
  4. 同步更新時,整個模型更新的global_step和每臺worker的local_step是相等的,如果我們設定最大步數為20000步,那麼每臺worker都會跑20000步。

同步更新是一種很容易想到的資料並行分散式梯度下降的過程,優點是模型的收斂會比較平穩,因為這種方式用了一個很大的batch,batch越大那麼這個批梯度下降的效果就會越接近整體梯度下降。但同步更新的缺點就是資訊傳輸開銷很大,並且有短板效應。

為了避免短板效應,tensorflow中做了這樣的設定,我們現在有10臺worker,你可以指定ps只要接收到8份梯度,就開始做平均和引數更新,那麼這樣就可以避免每次更新都等待最慢的那臺worker。

對於每次迭代的時間可能比單機還要慢這種說法,這是我在使用tensorflow時的切身感受,當然這慢多少和網路頻寬是有關係的。但每次迭代慢並不意味著最終的收斂時間慢,因為你使用了更大的batch,所以模型可能會更快的收斂。也就是說本來你達到99%的準確率需要訓練20000步,現在可能只需要10000步,並且有可能會繼續收斂。

對於我個人的使用而言,我覺得同步更新解決了我這樣的一個痛點:我可以設定更大的batchsize,因為batchsize越大,每次引數更新時儲存的梯度矩陣就會越大,有可能會超出GPU的視訊記憶體,這樣如果你用的GPU視訊記憶體不大,那麼一塊GPU能承受的batchsize就是有限的,而多GPU並行後,使用同步更新時batchsize就會自然而然的擴大(實際程式碼裡的batchsize沒變,但計算時其實變大了)。

非同步更新

非同步更新的每一次迭代,每臺worker計算好每個引數的128個梯度後,便會把梯度矩陣傳輸到ps上,ps在接受到任意一臺worker的梯度矩陣後,就會立即更新引數,並把更新後的引數分發回worker,接著worker再根據新的引數抽取128條資料計算梯度。

根據上面的描述,我們可以得到關於非同步更新的下面幾個事實:

  1. 非同步更新時ps不需要再等待全部的10臺worker都計算好梯度後再更新
  2. 非同步更新時,我們在程式碼裡寫的batchsize為128,那麼每次更新時確確實實是用的128條資料的平均梯度在更新引數,而不是像同步更新時是1280條資料
  3. 非同步更新時,整體模型更新的global_step等於各臺worker的local_step之和。比如我們設定總的迭代次數,也就是global_stpe為20000步,那麼每臺worker只要跑2000步迭代就會停止,因為每臺worker的每一步local_step都會讓引數更新一次,10臺worker的話每臺worker只要跑2000步。這也是和同步更新不一樣的地方。

乍一看,非同步更新好像是就是單機版的更新方式,batchsize和單機時一樣,全部的步數也和設定的一樣,但非同步更新時會遇到過期梯度的問題。

過期梯度可以這麼理解,比如說現在模型的global_step為100,記這時的引數為100版本的引數,worker1拿到第100版本的引數後開始計算梯度,而這時worker2剛剛把自己計算的梯度傳到ps,那麼ps就會立即更新引數到101版本,過了一會worker1還在計算梯度,這時worker3也計算好梯度了,於是ps立馬又把引數更新到102版本,這樣幾個過程後,假設等worker1根據100版本的引數計算好梯度後,ps上的引數版本已經是102版本了,那麼ps就會在102引數的版本基礎上,根據worker1傳來的梯度進行更新,也就是利用worker1根據100版本的引數計算出的梯度,把引數從102版本更新到103版本,就像下面的公式這樣:

上面更新的公式裡,θ的上標表示引數的版本(global_step),上文說的worker1的情況就和展示出來的這三道公式類似,前面兩道更新對應worker2,worker3更新,最後一道對應worker1的更新。

對於出現這種過期梯度的情況,會導致梯度下降的過程變得不穩定,因為模型在更新引數時使用的梯度不是它這一步實際算出來的梯度,但梯度下降本身就是一個求近似解的過程,所以一般來說最終都還是會收斂。對於過期梯度的問題,目前還沒有很好的解決,但可以設定一些類似過期梯度的閾值,比如一個梯度如果使用的引數版本小於當前ps上梯度的引數版本一定數量時,就放棄使用該梯度等等手段。

非同步更新每次更新引數的速度都要比同步更新快得多,因為非同步更新時不需要等待最慢的那個,沒有短板效應。同樣的,更新速度越快並不意味著收斂的快,也不意味著收斂時的精確度越高。

以上就是關於資料並行時的梯度下降。

至於該選擇同步更新還是非同步更新,這可以根據實際情況來調節,對於一般的小任務,比如單塊GPU在幾個小時內就可以跑完的任務,乾脆就直接單GPU跑就可以了,這樣就不會有很大的通訊開銷,也不會有過期梯度的問題。一般新手跑的模型基本上一塊顯示卡足矣。

如果資料量很大,模型也很大,一定要使用分散式,那麼該如何選擇呢?對於這個問題,我不會很裝X的回答,這得好好思考,因為沒啥好思考的,同步還是非同步我在上面都說明了,各有各的好處,同步每一次更新都會變慢,但總的更新次數可能會少,非同步每次更新快,但總的更新次數可能會比較多,所以最終哪個收斂更快,這都不好說。也有種選擇方式是,比如你有10臺worker,每臺worker上有4個GPU,那麼你可以設定成每臺worker內部的4塊顯示卡同步,而worker與worker之間非同步。

要相信不管你是用同步還是非同步最後應該都會收斂,至於哪種方式更快,收斂時的準確率更高,這得去做嘗試,不去做一遍誰都不知道(好像說的是廢話)。

參考文獻:

http://engineering.skymind.io/distributed-deep-learning-part-1-an-introduction-to-distributed-training-of-neural-networks