飛槳PaddlePaddle單機訓練速度優化最佳實踐

PaddlePaddle發表於2019-07-29

導讀:飛槳(PaddlePaddle)致力於讓深度學習技術的創新與應用更簡單。在單機訓練速度方面,通過高並行、低開銷的非同步執行策略和高效率的核心運算元,優化靜態圖訓練效能,在Paddle Fluid v1.5.0的基準測試中,在7個典型模型上進行了測試(影像領域5個,NLP領域2個),其中5個模型的速度顯著優於對標框架(大於15%),2個模型與對標框架持平(5%之內)。如果想讓單機訓練速度更快,可以根據這篇文件的建議從網路構建、資料準備、模型訓練三個方向瞭解飛槳單機訓練中常用的優化方法。來一組測試資料先睹為快。

飛槳PaddlePaddle單機訓練速度優化最佳實踐

測試環境如下:

•   PaddlePaddle version:1.5.0

•   Tensorflow version:1.12.0

•   PyTorch version:1.1.0

•   MXNet version:1.4.1

•   GPU:Tesla V100-SXM2

•   CPU:Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz,38核

•   Nvida driver: 418.39

•   CUDNN VERSION:7.4.2.24

•   CUDA VERSION:9.0.176,單卡模式

1. 網路構建過程中的配置優化

減少模型中Layer的個數

為方便使用者使用,飛槳提供一些不同粒度的Layer,其中有些Layer的組合可以通過單個Layer完成。比如:

(1)fluid.layers.softmax_with_cross_entropy,該操作其實是fluid.layers.softmax和fluid.layers.cross_entropy的組合,因此如果模型中有出現fluid.layers.softmax和fluid.layers.cross_entropy的組合,可以直接用fluid.layers.softmax_with_cross_entropy替換。

(2)如果模型中需要對資料進行標準化,可以直接使用fluid.layers.data_norm,而不用通過一系列layer組合出資料的標準化操作。

因此,建議在構建模型時優先使用飛槳提供的單個Layer完成所需操作,這樣減少模型中Layer的個數,並因此加速模型訓練。

2. 資料準備優化

資料準備通常分為兩部分:第一部分是資料載入,即程式從磁碟中載入訓練/預測資料;第二部分是資料預處理,程式對載入的資料進行預處理,比如影像任務通常需要進行資料增強、Shuffle等。這兩部分需要使用者根據自己的模型需要進行設定,只需要最後得到Data Reader介面即可。Data Reader返回iterable物件,可以每次返回一條樣本或者一組樣本。程式碼示例如下:

def data_reader (width, height):    defreader():     
  while True:yield np.random.uniform(-1, 1,size=width*height), \      
 np.random.randint(0,10)return readertrain_data_reader = data_reader(32, 32)

飛槳提供了兩種方式從Data Reader中讀取資料:同步資料讀取和非同步資料讀取。

2.1 同步資料讀取

同步資料讀取是一種簡單並且直觀的資料準備方式,程式碼示例如下:

Image = paddle.layer.data("image",...)
label = paddle.layer.data("label",...)
# 模型定義
# ……
prediction = fluid.layers.fc(input= image,size=10)
loss=fluid.layers.cross_entropy(input=prediction, label= label)
avg_loss = fluid.layers.mean(loss)
# ……
# 讀取資料
# paddle.dataset.mnist.train()返回資料讀取的Reader,每次可以從Reader中讀取一條樣本,batch_size為128
train_reader =paddle.batch(paddle.dataset.mnist.train(), 128)
end = time.time()
for batch_id, batch in enumerate(train_reader):   data_time = time.time() - end  
  # 訓練網路 
  executor.run(feed=[...], fetch_list=[...])   batch_time = time.time() - end    
end= time.time()

使用者首先需要通過fluid.layers.data定義模型的輸入,然後根據輸入構建模型,最後從事先自定義的Reader函式中獲取一個batch的資料,並將資料傳遞給執行器。

 

可以看出,採用同步資料讀取方式時,使用者可通過加入計時函式來統計資料準備部分和執行部分所佔用的時間。由於資料準備和執行是順序進行的,所以程式的執行速度可能較慢。如果使用者想進行模型除錯的話,同步資料讀取是一個不錯的選擇。

 

更多同步資料讀取的介紹請參考:

https://www.paddlepaddle.org.cn/documentation/docs/en/1.5/user_guides/howto/prepare_data/reader.html


2.2 非同步資料讀取


飛槳裡面使用py_reader介面來實現非同步資料讀取,程式碼示例如下:

train_py_reader = fluid.layers.py_reader(       capacity=10,      
 shapes=((-1, 784), (-1, 1)),    
   dtypes=('float32', 'int64'),       name="train_reader",       use_double_buffer=True)
# 使用 read_file() 方法從py_reader中獲取模型的輸入
image, label = fluid.layers.read_file(reader)
# 模型定義
# ……
prediction = fluid.layers.fc(input= image,size=10)loss = fluid.layers.cross_entropy(input=prediction, label= label)avg_loss = fluid.layers.mean(loss)
# ……
# 讀取資料
train_reader =paddle.batch(paddle.dataset.mnist.train(), 128)train_py_reader.decorate_paddle_reader(train_reader)
# 啟動py_reader
train_py_reader.start()
try:   
 end= time.time()  
 while True:   
    print("queue size: ", train_py_reader.queue.size())     
  loss, = executor.run(fetch_list=[...])       # ...       
 batch_time = time.time() - end    
   end = time.time()    
   batch_id += 1except fluid.core.EOFException:  
 train_py_reader.reset()

使用者首先需要通過fluid.layers.py_reader定義py_reader物件,並使用 read_file() 方法從py_reader中獲取模型的輸入,然後根據輸入構建模型,再然後用decorate_paddle_reader將自定義的Reader與py_reader繫結。在訓練開始之前,通過呼叫start()方法來啟動資料讀取。在資料讀取結束之後,executor.run會丟擲fluid.core.EOFException,表示訓練已經遍歷完Reader中的所有資料。

採用非同步資料讀取時,Python端和C++端共同維護一個資料佇列,Python端啟動一個執行緒,負責向佇列中插入資料,C++端在訓練/預測過程中,從資料佇列中獲取資料,並將該資料從對佇列中移除。使用者可以在程式執行過程中,監測資料佇列是否為空,如果佇列始終不為空,表明資料準備的速度比模型執行的速度快,這種情況下資料讀取可能不是瓶頸。

另外,飛槳提供的一些FLAGS也能很好的幫助分析效能。如果使用者希望評估一下在完全沒有資料讀取開銷情況下模型的效能,可以設定一下環境變數:FLAGS_reader_queue_speed_test_mode,在該變數為True情況下,C++端從資料佇列中獲取資料之後,不會從資料佇列中移除,這樣能夠保證資料佇列始終不為空,從而避免了C++端讀取資料時的等待開銷。

需要特別注意的是,FLAGS_reader_queue_speed_test_mode只能在效能分析時開啟,正常訓練/預測模型時需要關閉。

為降低訓練的整體時間,建議使用者使用非同步資料讀取的方式,並開啟 use_double_buffer=True 。使用者可根據模型的實際情況設定資料佇列的大小。如果資料準備的時間大於模型執行的時間,或者出現了資料佇列為空的情況,就需要考慮對資料讀取Reader進行加速。常用的方法是使用多程式準備資料,

可以參考:

https://github.com/PaddlePaddle/models/blob/develop/PaddleCV/yolov3/reader.py

 

更多非同步資料讀取的介紹請參考:

https://www.paddlepaddle.org.cn/documentation/docs/en/1.5/user_guides/howto/prepare_data/use_py_reader_en.html

3. 模型訓練相關優化

3.1 飛槳的執行器介紹

目前Python API中,飛槳提供了fluid.compiler.CompiledProgram的概念,使用者可以通過CompiledProgram將傳入的program(飛槳中的網路模型)進行編譯,如果希望採用資料並行模式訓練,只需要將CompiledProgram返回的物件呼叫一下with_data_parallel即可,最後統一通過executor.run(…)執行compiled_program。

 

雖然統一通過executor.run(…)介面來執行,實際底層的執行策略有兩種,對應C++部分的兩個執行器,即Executor和ParallelExecutor,如果使用者採用資料並行模式,C++部分使用的是ParallelExecutor,除此之外都是使用Executor。

這兩個執行器的差別:

飛槳PaddlePaddle單機訓練速度優化最佳實踐

可以看出,Executor的內部邏輯非常簡單,但效能可能會弱一些,因為Executor對於program中的操作是序列執行的。而ParallelExecutor首先會將program轉變為計算圖,並分析計算圖中節點間的連線關係,對圖中沒有相互依賴的節點(OP),通過多執行緒並行執行。

因此,Executor是一個輕量級的執行器,目前主要用於引數初始化、模型儲存、模型載入。ParallelExecutor是Executor的升級版本,目前ParallelExecutor主要用於模型訓練,包括單機單卡、單機多卡以及多機多卡訓練。

ParallelExecutor執行計算圖之前,可以對計算圖進行一些優化,比如使計算圖中的一些操作是In-place的、將計算圖中的引數更新操作進行融合等。使用者還可以調整Parallel Executor執行過程中的一些配置,比如執行計算圖的執行緒數等。這些配置分別是構建策略(BuildStrategy)和執行策略(ExecutionStrategy)引數來設定的。

一個簡單的使用示例如下:

build_strategy = fluid.BuildStrategy()build_strategy.enable_inplace = Truebuild_strategy.fuse_all_optimizer_ops=True exec_strategy = fluid.ExecutionStrategy()exec_strategy.num_threads = 4 train_program = fluid.compiler.CompiledProgram(main_program).with_data_parallel(                loss_name=loss.name,                build_strategy=build_strategy,                exec_strategy=exec_strategy) place = fluid.CUDAPlace(0)exe = Executor(place)# 使用py_reader讀取資料,因此執行時不需要feedfetch_outs = exe.run(train_program, fetch_list=[loss.name],)

更多關於ParallelExecutor的介紹請參考:

https://www.paddlepaddle.org.cn/documentation/docs/zh/1.5/api_guides/low_level/parallel_executor.html

更多關於CompiledProgram的介紹請參考:

https://www.paddlepaddle.org.cn/documentation/docs/zh/1.5/api_guides/low_level/compiled_program.html

3.2 構建策略(BuildStrategy)配置引數介紹

BuildStrategy中提供了一些關於計算圖優化的策略,這些策略可以在不同程度上提升模型的訓練速度,但是其中一些策略與模型的結構有關,比如fuse_all_optimizer_ops不支援sparse梯度,我們正在積極的完善這些策略,並在下一個版本將這些策略預設開啟。

構建策略的詳細介紹如下:

飛槳PaddlePaddle單機訓練速度優化最佳實踐

引數說明:

(1)關於 reduce_strategy , Parallel Executor 對於資料並行支援兩種引數更新模式:AllReduce 和 Reduce 。在 AllReduce 模式下,各個節點上計算得到梯度之後,呼叫 AllReduce 操作,梯度在各個節點上聚合,然後各個節點分別進行引數更新。在 Reduce 模式下,引數的更新操作被均勻的分配到各個節點上,即各個節點計算得到梯度之後,將梯度在指定的節點上進行 Reduce ,然後在該節點上進行引數的更新,最後將更新之後的引數Broadcast到其他節點。

即:如果模型中有100個引數需要更新,訓練使用的節點數為4,在 AllReduce 模式下,各個節點需要分別對這100個引數進行更新;在 Reduce 模式下,各個節點需要分別對這25個引數進行更新,最後將更新的引數Broadcast到其他節點。注意:如果是使用CPU進行資料並行訓練,在Reduce模式下,不同CPUPlace 上的引數是共享的,所以在各個CPUPlace 上完成引數更新之後不用將更新後的引數Broadcast到其他CPUPlace。

(2)關於 enable_backward_optimizer_op_deps ,在多卡訓練時,開啟該選項可能會提升訓練速度。

(3)關於 fuse_all_optimizer_ops ,目前只支援SGD、Adam和Momentum演算法。注意:目前不支援sparse引數梯度。

(4)關於 fuse_all_reduce_ops ,多GPU訓練時,可以對 AllReduce 操作進行融合,以減少 AllReduce 的呼叫次數。預設情況下會將同一layer中引數的梯度的 AllReduce 操作合併成一個,比如對於 fluid.layers.fc 中有Weight和Bias兩個引數,開啟該選項之後,原本需要兩次 AllReduce 操作,現在只用一次 AllReduce 操作。此外,為支援更大粒度的引數梯度融合,飛槳提供了 FLAGS_fuse_parameter_memory_size 選項,使用者可以指定融合AllReduce操作之後,每個 AllReduce 操作的梯度位元組數,比如希望每次 AllReduce 呼叫傳輸64MB的梯度,export FLAGS_fuse_parameter_memory_size=64 。注意:目前不支援sparse引數梯度。

(5)關於 mkldnn_enabled_op_types ,目前飛槳的Op中可以使用mkldnn庫計算的操作包括:transpose, sum, softmax,requantize, quantize, pool2d, lrn, gaussian_random, fc, dequantize,conv2d_transpose, conv2d, conv3d, concat, batch_norm, relu, tanh, sqrt, abs.

3.3 執行策略(ExecutionStrategy)配置引數介紹

ExecutionStrategy中提供了關於計算圖執行時的一些配置,這些配置可能會影響模型的訓練速度。同時,這些配置與模型的結構有關,如果使用者希望模型訓練速度更快,可以調整一下這些配置。在後續的優化中,我們會對這部分進行優化,根據輸入模型結構動態調整這些設定。

ExecutionStrategy配置選項說明:

飛槳PaddlePaddle單機訓練速度優化最佳實踐

引數說明:

(1)關於 num_iteration_per_drop_scope ,框架在執行過程中會產生一些臨時變數,通常每經過一個batch就要清理一下臨時變數,但是由於GPU是非同步裝置,在清理之前需要對所有的GPU呼叫一次同步操作,因此耗費的時間較長。為此我們在 execution_strategy 中新增了 num_iteration_per_drop_scope 選項。使用者可以指定經過多少次迭代之後清理一次。

(2)關於 num_threads ,ParallelExecutor 根據OP之間的依賴關係確定OP的執行順序,即:當OP的輸入都已經變為ready狀態之後,該OP會被放到一個佇列中,等待被執行。ParallelExecutor 內部有一個任務排程執行緒和一個執行緒池,任務排程執行緒從佇列中取出所有Ready的OP,並將其放到執行緒佇列中。num_threads 表示執行緒池的大小。根據以往的經驗,對於CPU任務,num_threads=2*dev_count 時效能較好,對於GPU任務,num_threads=4*dev_count 時效能較好。注意:執行緒池不是越大越好。

4. 執行時FLAGS設定優化

Paddle Fluid中有一些FLAGS可以有助於效能優化:

(1)FLAGS_cudnn_exhaustive_search表示在呼叫cuDNN中的卷積操作時,根據輸入資料的shape等資訊,採取窮舉搜尋的策略從演算法庫中選取到更快的卷積演算法,進而實現對模型中卷積操作的加速。需要注意的是:

a. 在搜尋演算法過程中需要使用較多的視訊記憶體,如果使用者的模型中卷積操作較多,或者GPU卡視訊記憶體較小,可能會出現視訊記憶體不足問題。

b. 通過窮舉搜尋選擇好演算法之後,該演算法會進入Cache,以便下次執行時,如果輸入資料的shape等資訊不變,直接使用Cache中演算法。

(2)FLAGS_enable_cublas_tensor_op_math表示是否使用TensorCore加速cuBLAS等NV提供的庫中的操作。需要注意的是,這個環境變數只在Tesla V100以及更新的GPU上適用,且可能會帶來一定的精度損失,通常該損失不會影響模型的收斂性。

5.最佳實踐(Best Practise)

(1)儘可能的使用飛槳提供的單個layer實現所需操作。

(2)採用非同步資料讀取。

(3)模型訓練相關優化:

 a. 使用ParallelExecutor作為底層執行器,程式碼示例:

compiled_prog = compiler.CompiledProgram(        fluid.default_main_program()).with_data_parallel(                  loss_name=loss.name)

如果是單卡訓練,也可以呼叫with_data_parallel方法。

b. 如果模型中引數的梯度都是非sparse的,可以開啟fuse_all_optimizer_ops選項,將多個引數更新操作融合為一個。

c. 如果是多卡訓練,可以開啟enable_backward_optimizer_op_deps、fuse_all_reduce_ops選項。如果想指定每次每次AllReduce操作的資料大小,可以設定FLAGS_fuse_parameter_memory_size,比如 export FLAGS_fuse_parameter_memory_size=1,表示每次 AllReduce 呼叫傳輸1MB的梯度。

d. 使用CPU做資料並行訓練時,推薦使用Reduce模型,因為在使用CPU進行資料並行訓練時,在Reduce模式下,不同CPUPlace 上的引數是共享的,所以在各個CPUPlace 上完成引數更新之後不用將更新後的引數Broadcast到其他CPUPlace上,這對提升速度也有很大幫助。

e. 如果是Reduce模式,可開啟fuse_broadcast_ops選項。

f. 如果使用者的模型較小,比如mnist、language_model等,可以將num_threads設為1。

g. 在視訊記憶體足夠的前提下,建議將 exec_strategy.num_iteration_per_drop_scope 設定成一個較大的值,比如設定為100 ,這樣可以避免反覆地申請和釋放記憶體。

目前我們正在推進這些配置自動化的工作:即根據輸入的模型結構自動配置這些選項,爭取在下一個版本中實現,敬請期待。

(4)FLAGS設定

 FLAGS_cudnn_exhaustive_search = True FLAGS_enable_cublas_tensor_op_math = True 

6.典型案例

不同的模型計算特徵不同,最優執行時配置也就不盡相同。大體來說,主要是兩種情況,第一種情況:模型組網OP數量少、OP的計算量大,常見的如ResNet、VGG模型,通過設定合適的batch_size,這類模型很容易就可以將最大限度的利用GPU計算資源,因此設定不同的執行器引數對總體速度影響可能不是很明顯。第二種情況:模型由大量的計算量很小的OP組成,比如RNN模型,這類模型則需要使用者通過實驗來選擇執行時引數的最佳配置。因此,我們以典型的語言模型(language model)為例,瞭解一下上述優化策略的實際效果。

6.1 LSTM language model原理介紹

飛槳提供了論文《Recurrent Neural Network Regularization》中基於LSTM迴圈神經網路(RNN)的language model的開源實現。相比於傳統的語言模型方法,基於迴圈神經網路的語言模型方法能夠更好地解決稀疏詞的問題。

該模型的目的是給定一個輸入的詞序列,預測下一個詞出現的概率。

模型中採用了序列任務常用的RNN網路,實現了一個兩層的LSTM網路,然後使用LSTM的結果去預測下一個詞出現的概率。由於資料的特殊性,每一個batch的last hidden和lastcell會作為下一個batch的init hidden和init cell。

飛槳PaddlePaddle單機訓練速度優化最佳實踐


6.2 language_model單GPU訓練效能優化效果

language_model中提供了4種RNN執行模式,分別為:static、padding、cudnn和lstm_basic。本案例中測試的為static模式。language_model中同樣提供了small、medium、large三種模型配置,主要差別在於隱層的大小、RNN的步數、dropout比例上。我們對這個案例在模型配置、執行選項和資料讀取三個方面都進行了優化,我們依次測試瞭如下優化版本的結果:

(1)Baseline版本

(2)設定exec_strategy.num_threads = device_count

(3)設定exec_strategy.num_iteration_per_drop_scope = 100

(4)設定build_strategy.enable_inplace = True,build_strategy.memory_optimize = False

(5)設定build_strategy.fuse_all_optimizer_ops = True

(6)使用py_reader進行非同步資料讀取

(7)配置優化

  •  reshape中設定inplace=True

  • 使用split操作代替多次slice

優化前:

for index in range(len):input = layers.slice(input_embedding, axes=[1], starts=[index],ends=[index + 1])


優化後:

sliced_inputs = layers.split(input_embedding,num_or_sections=len, dim=1)for index in range(len):input = sliced_inputs[index]
  • 減少reshape的次數

優化前:

for index in range(len):res.append(layers.reshape(input, shape=[1, -1,hidden_size]))      real_res = layers.concat(res, 0)      real_res = layers.transpose(x=real_res, perm=[1, 0, 2])優化後:for index in range(len):res.append(input)      real_res = layers.concat(res, 0)      real_res = layers.reshape(real_res, shape=[len, -1, hidden_size],inplace=True)       real_res = layers.transpose(x=real_res,perm=[1, 0, 2])

飛槳PaddlePaddle單機訓練速度優化最佳實踐

經過7個版本的優化,small和large模型最終分別獲得了1.64x和1.35x的加速。從實驗結果可以看出,即使是類似的網路結構,調整執行引數產生加速效果也不同,如設定exec_strategy.num_threads = device_count,small模型獲得了4.9%的加速,large模型只獲得0.8%的加速。另外,非同步資料讀取對該模型總體訓練時間的減少也不明顯,主要是因為這個模型的所使用的PTB資料集很小,可以提前將所有資料讀取到記憶體裡,因此訓練時,資料準備部分對整體時延的影響較小。


如果有興趣的同學,可以加入官方QQ群,您將遇上大批志同道合的深度學習同學。官方QQ群:432676488。

如果您想詳細瞭解更多飛槳PaddlePaddle的相關內容,請參閱以下文件。

官網地址:https://www.paddlepaddle.org.cn?fr=gzh


本文提到的專案地址:

  • DeepLabV3+:

    https://github.com/PaddlePaddle/models/tree/v1.5/PaddleCV/deeplabv3%2B

  • YOLOv3:

    https://github.com/PaddlePaddle/models/tree/v1.5/PaddleCV/yolov3

  • BERT:

    https://github.com/PaddlePaddle/ERNIE

  • Mask-RCNN:

    https://github.com/PaddlePaddle/models/tree/v1.5/PaddleCV/rcnn

  • CycleGAN

    https://github.com/PaddlePaddle/models/tree/v1.5/PaddleCV/PaddleGAN/cycle_gan

  • SE-ResNeXt50:

    https://github.com/PaddlePaddle/models/tree/v1.5/PaddleCV/image_classification

  • Transformer:

    https://github.com/PaddlePaddle/models/tree/v1.5/PaddleNLP/models/neural_machine_translation/transformer

相關文章