熱點新聞網 匯聚海量最新國內、國際資訊

5大典型模型測試單機訓練速度超對標框架,飛槳如何做到?

2019-08-08已圍觀 來源:互聯網編輯:熱點新聞網

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

模型名稱

對標開源框架

飛槳

對標開源框架

吞吐量對比(%)

飛槳VS對標開源框架

1

DeepLab V3+

TensorFlow

13.70 examples/s

6.40 examples/s

+ 113.98%

2

YOLOv3

MXNet

29.90 examples/s

18.58 examples/s

+ 60.95%

3

BERT

TensorFlow

4.04 steps/s

3.42 steps/s

+ 18.23%

4

Mask-RCNN

PyTorch

3.81 examples/s

3.24 examples/s

+ 17.62%

5

CycleGAN

TensorFlow

7.51 examples/s

6.45 examples/s

+ 16.44%

6

SE-ResNeXt50

PyTorch

168.33 examples/s

163.13 examples/s

+ 3.19%

7

Transformer

TensorFlow

4.87 examples/s

4.75 examples/s

+ 2.42%

測試環境如下:

  • 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. 網絡構建過程中的配置優化1.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對象,可以每次返回一條樣本或者一組樣本。代碼示例如下:

defdata_reader(width, height):

defreader:

whileTrue:

yieldnp.random.uniform( -1, 1, size=width*height),

np.random.randint( 0, 10)

returnreader

train_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 += 1

except 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。

這兩個執行器的差別:

執行器

執行對象

執行策略

Executor

Program

根據 ProgramOperator定義的先后順序依次運行

ParallelExecutor

SSA Graph

根據Graph中各個節點之間的依賴關系,通過多線程運行

可以看出,Executor的內部邏輯非常簡單,但性能可能會弱一些,因為Executor對于program中的操作是串行執行的。而Parallel Executor首先會將program轉變為計算圖,并分析計算圖中節點間的連接關系,對圖中沒有相互依賴的節點(OP),通過多線程并行執行。

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

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

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

build_strategy = fluid.BuildStrategy

build_strategy.enable_inplace = True

build_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讀取數據,因此執行時不需要feed

fetch_outs = exe.run(train_program, fetch_list=[loss.name],)

更多關于Parallel Executor的介紹請參考:

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梯度,我們正在積極的完善這些策略,并在下一個版本將這些策略默認打開。

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

選項

類型

默認值

說明

reduce_strategy

fluid.BuildStrategy.ReduceStrategy

fluid.BuildStrategy.ReduceStrategy.AllReduce

使用數據并行訓練模型時選用 AllReduce模式訓練還是 Reduce模式訓練

enable_backward_optimizer_op_deps

bool

FALSE

在反向操作和參數更新操作之間添加依賴,保證在所有的反向操作都運行結束之后才開始運行參數更新操作

fuse_all_optimizer_ops

bool

FALSE

對模型中的參數更新算法進行融合

fuse_all_reduce_ops

bool

FALSE

多卡訓練時,將all_reduce 操作進行融合

fuse_relu_depthwise_conv

bool

FALSE

如果模型中存在relu和depthwise_conv操作,并且是連接的,即relu->depthwise_conv,將這兩個操作合并為一個

fuse_broadcast_ops

bool

FALSE

Reduce模式下,對最后的多個Broadcast操作融合為一個

mkldnn_enabled_op_types

list

{}

如果是CPU訓練,可以用 mkldnn_enabled_op_types指明模型中的些操作可以使用mkldnn庫,默認情況下,模型中用到的操作如果在飛槳目前支持的可以使用mkldnn庫計算的列表中,這些操作都會調用mkldnn庫的接口進行計算

debug_graphviz_path

str

“”

將Graph以graphviz格式輸出到debug_graphviz_path所指定的文件

參數說明:

(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配置選項說明:

選項

類型

默認值

說明

num_iteration_per_drop_scope

INT

1

經過多少次迭代之后清理一次local execution scope

num_threads

INT

經驗值

對于CPU:2*dev_count;對于GPU:4*dev_count.

ParallelExecutor中執行所有Op使用的線程池大小

參數說明:

(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設置優化

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和last cell會作為下一個batch的init hidden和init cell。

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

優化前:

forindexinrange(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)

forindex inrange( len):

input = sliced_inputs[index]

...

- 減少reshape的次數

優化前:

forindex inrange( 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])

經過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

本文提到的項目地址:

模型名稱

項目地址

1

DeepLab V3+

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

2

YOLOv3

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

3

BERT

https://github.com/PaddlePaddle/ERNIE

4

Mask-RCNN

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

5

CycleGAN

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

6

SE-ResNeXt50

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

7

Transformer

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

特码一码公式规律