1. 程式人生 > 其它 >實戰Google深度學習框架:TensorFlow計算加速

實戰Google深度學習框架:TensorFlow計算加速

要將深度學習應用到實際問題中,一個非常大的問題在於訓練深度學習模型需要的計算量太大。比如Inception-v3模型在單機上訓練到78%的正確率需要將近半年的時間 ,這樣的訓練速度是完全無法應用到實際生產中的。為了加速訓練過程,本章將介紹如何通過TensorFlow利用GPU或/和分散式計算進行模型訓練。本文節選自《TensorFlow:實戰Google深度學習框架》第十章。

本文將介紹如何在TensorFlow中使用單個GPU進行計算加速,也將介紹生成TensorFlow會話(tf.Session)時的一些常用引數。通過這些引數可以使除錯更加方便而且程式的可擴充套件性更好。然而,在很多情況下,單個GPU的加速效率無法滿足訓練大型深度學習模型的計算量需求,這時將需要利用更多的計算資源。為了同時利用多個GPU或者多臺機器,10.2節中將介紹訓練深度學習模型的並行方式。然後,10.3節將介紹如何在一臺機器的多個GPU上並行化地訓練深度學習模型。在這一節中也將給出具體的TensorFlow樣例程式來使用多GPU訓練模型,並比較並行化效率提升的比率。最後在10.4節中將介紹分散式TensorFlow,以及如何通過分散式TensorFlow訓練深度學習模型。在這一節中將給出具體的TensorFlow樣例程式來實現不同的分散式深度學習訓練模式。雖然TensorFlow可以支援分散式深度學習模型訓練,但是它並不提供叢集建立、管理等功能。為了更方便地使用分散式TensorFlow,10.4節中將介紹才雲科技基於Kubernetes容器雲平臺搭建的分散式TensorFlow系統。

1. TensorFlow使用GPU

TensorFlow程式可以通過tf.device函式來指定執行每一個操作的裝置,這個裝置可以是本地的CPU或者GPU,也可以是某一臺遠端的伺服器。但在本節中只關心本地的裝置。TensorFlow會給每一個可用的裝置一個名稱,tf.device函式可以通過裝置的名稱來指定執行運算的裝置。比如CPU在TensorFlow中的名稱為/cpu:0。在預設情況下,即使機器有多個CPU,TensorFlow也不會區分它們,所有的CPU都使用/cpu:0作為名稱。而一臺機器上不同GPU的名稱是不同的,第n個GPU在TensorFlow中的名稱為/gpu:n。比如第一個GPU的名稱為/gpu:0,第二個GPU名稱為/gpu:1,以此類推。

TensorFlow提供了一個快捷的方式來檢視執行每一個運算的裝置。在生成會話時,可以通過設定log_device_placement引數來列印執行每一個運算的裝置。下面的程式展示瞭如何使用log_device_placement這個引數。

'a'

在以上程式碼中,TensorFlow程式生成會話時加入了引數log_device_placement=True,所以程式會將執行每一個操作的裝置輸出到螢幕。於是除了可以看到最後的計算結果之外,還可以看到類似“add: /job:localhost/replica:0/task:0/cpu:0”這樣的輸出。這些輸出顯示了執行每一個運算的裝置。比如加法操作add是通過CPU來執行的,因為它的裝置名稱中包含了/cpu:0。

在配置好GPU環境的TensorFlow中 ,如果操作沒有明確地指定執行裝置,那麼TensorFlow會優先選擇GPU。比如將以上程式碼在亞馬遜(Amazon Web Services, AWS)的 g2.8xlarge例項上執行時,會得到以下執行結果。

name

從上面的輸出可以看到在配置好GPU環境的TensorFlow中,TensorFlow會自動優先將運算放置在GPU上。不過,儘管g2.8xlarge例項有4個GPU,在預設情況下,TensorFlow只會將運算優先放到/gpu:0上。於是可以看見在上面的程式中,所有的運算都被放在了/gpu:0上。如果需要將某些運算放到不同的GPU或者CPU上,就需要通過tf.device來手工指定。下面的程式給出了一個通過tf.device手工指定執行裝置的樣例。

import tensorflow as tf# 通過tf.device將運算指定到特定的裝置上。with tf.device('/cpu:0'):
   a = tf.constant([1.0, 2.0, 3.0], shape=[3], name='a')
   b = tf.constant([1.0, 2.0, 3.0], shape=[3], name='b')with tf.device('/gpu:1'):
    c = a + b


sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))print sess.run(c)'''在AWS g2.8xlarge例項上執行上述程式碼可以得到以下結果:
Device mapping:
/job:localhost/replica:0/task:0/gpu:0 -> device: 0, name: GRID K520, pci bus id: 0000:00:03.0
/job:localhost/replica:0/task:0/gpu:1 -> device: 1, name: GRID K520, pci bus id: 0000:00:04.0
/job:localhost/replica:0/task:0/gpu:2 -> device: 2, name: GRID K520, pci bus id: 0000:00:05.0
/job:localhost/replica:0/task:0/gpu:3 -> device: 3, name: GRID K520, pci bus id: 0000:00:06.0


add: /job:localhost/replica:0/task:0/gpu:1
b: /job:localhost/replica:0/task:0/cpu:0
a: /job:localhost/replica:0/task:0/cpu:0
[ 2.  4.  6.]'''

在以上程式碼中可以看到生成常量a和b的操作被載入到了CPU上,而加法操作被放到了第二個GPU“/gpu:1”上。在TensorFlow中,不是所有的操作都可以被放在GPU上,如果強行將無法放在GPU上的操作指定到GPU上,那麼程式將會報錯。以下程式碼給出了一個報錯的樣例。

_cpu = tf.Variable(0, name="a_

不同版本的TensorFlow對GPU的支援不一樣,如果程式中全部使用強制指定裝置的方式會降低程式的可移植性。在TensorFlow的kernel 中定義了哪些操作可以跑在GPU上。比如可以在variable_ops.cc程式中找到以下定義。

REGISTER_GPU_KERNELS(type)

在這段定義中可以看到GPU只在部分資料型別上支援tf.Variable操作。如果在TensorFlow程式碼庫中搜索呼叫這段程式碼的巨集TF_CALL_GPU_NUMBER_TYPES,可以發現在GPU上,tf.Variable操作只支援實數型(float16、float32和double)的引數。而在報錯的樣例程式碼中給定的引數是整數型的,所以不支援在GPU上執行。為避免這個問題,TensorFlow在生成會話時可以指定allow_soft_placement引數。當allow_soft_placement引數設定為True時,如果運算無法由GPU執行,那麼TensorFlow會自動將它放到CPU上執行。以下程式碼給出了一個使用allow_soft_placement引數的樣例。

import tensorflow as tf


a_cpu = tf.Variable(0, name="a_cpu")
with tf.device('/gpu:0'):    a_gpu = tf.Variable(0, name="a_gpu")# 通過allow_soft_placement引數自動將無法放在GPU上的操作放回CPU上。
sess = tf.Session(config=tf.ConfigProto(    allow_soft_placement=True, log_device_ placement=True))sess.run(tf.initialize_all_variables())'''執行上面這段程式可以得到下面的結果:
Device mapping:
/job:localhost/replica:0/task:0/gpu:0 -> device: 0, name: GRID K520, pci bus id: 0000:00:03.0
/job:localhost/replica:0/task:0/gpu:1 -> device: 1, name: GRID K520, pci bus id: 0000:00:04.0
/job:localhost/replica:0/task:0/gpu:2 -> device: 2, name: GRID K520, pci bus id: 0000:00:05.0
/job:localhost/replica:0/task:0/gpu:3 -> device: 3, name: GRID K520, pci bus id: 0000:00:06.0
a_gpu: /job:localhost/replica:0/task:0/cpu:0
a_gpu/read: /job:localhost/replica:0/task:0/cpu:0
a_gpu/Assign: /job:localhost/replica:0/task:0/cpu:0
init/NoOp_1: /job:localhost/replica:0/task:0/gpu:0
a_cpu: /job:localhost/replica:0/task:0/cpu:0
a_cpu/read: /job:localhost/replica:0/task:0/cpu:0
a_cpu/Assign: /job:localhost/replica:0/task:0/cpu:0
init/NoOp: /job:localhost/replica:0/task:0/gpu:0
init: /job:localhost/replica:0/task:0/gpu:0
a_gpu/initial_value: /job:localhost/replica:0/task:0/gpu:0
a_cpu/initial_value: /job:localhost/replica:0/task:0/cpu:0

從輸出的日誌中可以看到在生成變數a_gpu時,無法放到GPU上的運算被自動調整到了CPU上(比如a_gpu和a_gpu/read),而可以被GPU執行的命令(比如a_gpu/initial_value)依舊由GPU執行。'''

雖然GPU可以加速TensorFlow的計算,但一般來說不會把所有的操作全部放在GPU上。一個比較好的實踐是將計算密集型的運算放在GPU上,而把其他操作放到CPU上。GPU是機器中相對獨立的資源,將計算放入或者轉出GPU都需要額外的時間。而且GPU需要將計算時用到的資料從記憶體複製到GPU裝置上,這也需要額外的時間。TensorFlow可以自動完成這些操作而不需要使用者特別處理,但為了提高程式執行的速度,使用者也需要儘量將相關的運算放在同一個裝置上。

2. 深度學習訓練並行模式

TensorFlow可以很容易地利用單個GPU加速深度學習模型的訓練過程,但要利用更多的GPU或者機器,需要了解如何並行化地訓練深度學習模型。常用的並行化深度學習模型訓練方式有兩種,同步模式和非同步模式。本節中將介紹這兩種模式的工作方式及其優劣。

為幫助讀者理解這兩種訓練模式,本節首先簡單回顧一下如何訓練深度學習模型。圖10-1展示了深度學習模型的訓練流程圖。深度學習模型的訓練是一個迭代的過程。在每一輪迭代中,前向傳播演算法會根據當前引數的取值計算出在一小部分訓練資料上的預測值,然後反向傳播演算法再根據損失函式計算引數的梯度並更新引數。在並行化地訓練深度學習模型時,不同裝置(GPU或CPU)可以在不同訓練資料上執行這個迭代的過程,而不同並行模式的區別在於不同的引數更新方式。

圖10-2展示了非同步模式的訓練流程圖。從圖10-2中可以看到,在每一輪迭代時,不同裝置會讀取引數最新的取值,但因為不同裝置讀取引數取值的時間不一樣,所以得到的值也有可能不一樣。根據當前引數的取值和隨機獲取的一小部分訓練資料,不同裝置各自執行反向傳播的過程並獨立地更新引數。可以簡單地認為非同步模式就是單機模式複製了多份,每一份使用不同的訓練資料進行訓練。在非同步模式下,不同裝置之間是完全獨立的。

圖10-1 深度學習模型訓練流程圖

圖10-2 非同步模式深度學習模型訓練流程圖

然而使用非同步模式訓練的深度學習模型有可能無法達到較優的訓練結果。圖10-3中給出了一個具體的樣例來說明非同步模式的問題。其中黑色曲線展示了模型的損失函式,黑色小球表示了在t0時刻引數所對應的損失函式的大小。假設兩個裝置d0和d1在時間t0同時讀取了引數的取值,那麼裝置d0和d1計算出來的梯度都會將小黑球向左移動。假設在時間t1裝置d0已經完成了反向傳播的計算並更新了引數,修改後的引數處於圖10-3中小灰球的位置。然而這時的裝置d1並不知道引數已經被更新了,所以在時間t2時,裝置d1會繼續將小球向左移動,使得小球的位置達到圖10-3中小白球的地方。從圖10-3中可以看到,當引數被調整到小白球的位置時,將無法達到最優點。

圖10-3 非同步模式訓練深度學習模型存在的問題示意圖

圖10-4 同步模式深度學習模型訓練流程圖

為了避免更新不同步的問題,可以使用同步模式。在同步模式下,所有的裝置同時讀取引數的取值,並且當反向傳播演算法完成之後同步更新引數的取值。單個裝置不會單獨對引數進行更新,而會等待所有裝置都完成反向傳播之後再統一更新引數 。圖10-4展示了同步模式的訓練過程。從圖10-4中可以看到,在每一輪迭代時,不同裝置首先統一讀取當前引數的取值,並隨機獲取一小部分資料。然後在不同裝置上執行反向傳播過程得到在各自訓練資料上引數的梯度。注意雖然所有裝置使用的引數是一致的,但是因為訓練資料不同,所以得到引數的梯度就可能不一樣。當所有裝置完成反向傳播的計算之後,需要計算出不同裝置上引數梯度的平均值,最後再根據平均值對引數進行更新。