實現更好的 TensorFlow 效能
高階 API 本身可實現更好的 TensorFlow 效能。以下各部分詳細介紹了要使用的高階 API、幾點與除錯相關的提示、發展歷史以及建議使用手動調整的一些例項。此外,還介紹了與各種硬體和模型相關的最佳做法。
輸入流水線
輸入流水線會從一個位置提取資料,對資料進行轉換,然後將資料載入到加速器上進行處理。隨著加速器速度的提升,確保輸入流水線與需求保持同步非常重要。tf.data API 在設計時考慮了靈活性、易用性和效能。要使用 tf.data API 並最大限度地提高其效能,請參閱資料輸入流水線指南 (https://tensorflow.google.cn/guide/performance/datasets?hl=zh-CN) 。
讀取大量小檔案會顯著影響 I/O 效能。實現最大 I/O 吞吐量的一種方法是將輸入資料預處理為更大(約 100MB)的 TFRecord 檔案。對於較小的資料集 (200MB-1GB),最好的方法通常是將整個資料集載入到記憶體中。下載並轉換為 TFRecord 格式文件包含用於建立 TFRecord 的資訊和指令碼,此指令碼 會將 CIFAR-10 資料集轉換為 TFRecord(https://github.com/tensorflow/models/blob/master/tutorials/image/cifar10_estimator/generate_cifar10_tfrecords.py) 。
雖然使用 feed_dict 提供資料可實現高度靈活性,但 feed_dict 通常無法提供可擴縮的解決方案。請儘量將 feed_dict 用於簡單樣本,避免全部應用。
# feed_dict often results in suboptimal performance.
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
RNN 效能
您可以通過多種方法在 TensorFlow 中指定 RNN 計算,這些方法可在模型靈活性和效能方面進行權衡。tf.nn.rnn_cell.BasicLSTMCell 應被視為參考實現,並且只能在沒有其他選項可用時用作最後的補救手段。
在使用其中一個單元(而不是完全混合的 RNN 層)時,您可以選擇使用 tf.nn.static_rnn 或 tf.nn.dynamic_rnn。在執行時通常不應存在效能差異,但展開次數較多會增加 tf.nn.static_rnn 的圖大小並導致編譯時間變長。tf.nn.dynamic_rnn 的另一個優勢是可以選擇將記憶體從 GPU 切換到 CPU,從而能夠訓練很長的序列。這可能會降低效能,具體取決於模型和硬體配置。您也可以並行執行 tf.nn.dynamic_rnn 和底層 tf.while_loop 構造的多次迭代,儘管這對於 RNN 模型用處不大(因為這些模型本身是序列模型)。
在 NVIDIA GPU 上,除非您想要實現不受支援的層歸一化,否則應始終首選使用 tf.contrib.cudnn_rnn。它通常比 tf.contrib.rnn.BasicLSTMCell 和 tf.contrib.rnn.LSTMBlockCell 快至少一個數量級,並且 tf.contrib.rnn.BasicLSTMCell 佔用的記憶體比它多 3 - 4 倍。
如果您需要一次執行 RNN 的一步(使用遞迴政策的強化學習中可能會出現這種情況),則應該將 tf.contrib.rnn.LSTMBlockCell 與您自己的環境互動迴圈(在 tf.while_loop 構造內部)結合使用。可以一次執行 RNN 的一個步驟並返回到 Python,但速度會慢一些。
在 CPU 和移動裝置上,如果 GPU 不支援 tf.contrib.cudnn_rnn,那麼最快且最節省記憶體的選項是 tf.contrib.rnn.LSTMBlockFusedCell。
對於所有不常見的單元型別,如 tf.contrib.rnn.NASCell、tf.contrib.rnn.PhasedLSTMCell、tf.contrib.rnn.UGRNNCell、tf.contrib.rnn.GLSTMCell、tf.contrib.rnn.Conv1DLSTMCell、tf.contrib.rnn.Conv2DLSTMCell、tf.contrib.rnn.LayerNormBasicLSTMCell 等,請注意它們是在圖(如 tf.contrib.rnn.BasicLSTMCell)中實現的,因此會出現同樣的效能不佳和記憶體使用率較高的情況。在使用這些單元之前,請考慮是否值得進行此類權衡。例如,雖然層歸一化可以加速收斂(因為 cuDNN 的速度要快 20 倍),但最快的收斂速度通常是在不使用層歸一化的情況下實現的。
手動調整
針對 CPU 進行優化
在使用目標 CPU 支援的所有指令從原始碼構建 TensorFlow 時,CPU(包括 Intel® Xeon Phi™)可實現最佳效能。
除了使用最新的指令集,Intel® 還在 TensorFlow 中增加了對 Intel® Math Kernel Library for Deep Neural Networks (Intel® MKL-DNN) 的支援。雖然名稱不完全準確,但這些優化通常簡稱為 MKL 或包含 MKL 的 TensorFlow。包含 Intel® MKL-DNN 的 TensorFlow 包含有關 MKL 優化的詳細資訊。
下面列出的兩種配置用於通過調整執行緒池來優化 CPU 效能。
-
intra_op_parallelism_threads:可以使用多個執行緒並行處理其執行的節點會將各個部分安排到此池中
-
inter_op_parallelism_threads:所有就緒節點均已在此池中安排完畢
這些配置是使用 tf.ConfigProto 進行設定的,並傳遞給 tf.Session(在 config 屬性中),如以下程式碼段中所示。對於這兩個配置選項,如果它們未設定或設定為 0,則將預設為邏輯 CPU 核心數。測試表明,預設設定對於配備一個 CPU(具有 4 個核心)和多個 CPU(具有 70 個以上組合邏輯核心)的系統有效。常見的替代優化是將兩個池中的執行緒數設定為等於物理核心數(而不是邏輯核心數)。
config = tf.ConfigProto()
config.intra_op_parallelism_threads = 44
config.inter_op_parallelism_threads = 44
tf.session(config=config)
比較編譯器優化部分包含使用不同編譯器優化的測試結果。
包含 Intel® MKL DNN 的 TensorFlow
通過使用 Intel® Math Kernel Library for Deep Neural Networks (Intel® MKL-DNN) 優化的基本功能,Intel® 為 Intel® Xeon® 和 Intel® Xeon Phi™ 版 TensorFlow 添加了優化。這些優化還加快了使用消費級處理器(例如 i5 和 i7 Intel 處理器)的速度。Intel 釋出的 TensorFlow* Optimizations on Modern Intel® Architecture(針對現代 Intel® 架構的 TensorFlow* 優化)論文包含有關實現的其他詳情。
注意:MKL 是從 TensorFlow 1.2 開始新增的,目前僅適用於 Linux。同時使用 --config=cuda 時它也無法工作。
除了在訓練基於 CNN 的模型時能顯著提升效能之外,使用 MKL 進行編譯還可以建立針對 AVX 和 AVX2 進行優化的二進位制檔案,從而得到一個經過優化且與大多數現代(2011 年後)處理器相容的二進位制檔案。
可以使用以下命令通過 MKL 優化對 TensorFlow 進行編譯,具體命令取決於所用 TensorFlow 原始碼的版本。
對於 1.3.0 之後的 TensorFlow 原始碼版本:
./configure
# Pick the desired options
bazel build --config=mkl --config=opt //tensorflow/tools/pip_package:build_pip_package
對於 1.2.0 到 1.3.0 的 TensorFlow 版本:
./configure
Do you wish to build TensorFlow with MKL support? [y/N] Y
Do you wish to download MKL LIB from the web? [Y/n] Y
# Select the defaults for the rest of the options.
bazel build --config=mkl --copt="-DEIGEN_USE_VML" -c opt //tensorflow/tools/pip_package:build_pip_package
調整 MKL 以獲得最佳效能
本部分詳細介紹了可用於調整 MKL 以獲得最佳效能的不同配置和環境變數。在調整各種環境變數之前,請確保模型使用的是 NCHW (channels_first) 資料格式。MKL 針對 NCHW 進行了優化,並且 Intel 正努力確保在使用 NHWC 時獲得幾乎一樣的效能。
MKL 使用以下環境變數調整效能:
-
KMP_BLOCKTIME - 設定執行緒在執行完並行區域之後,在休眠之前應該等待的時間(以毫秒為單位)
-
KMP_AFFINITY - 使執行時庫能夠將執行緒繫結到物理處理單元
-
KMP_SETTINGS - 允許 (true) 或禁止 (false) 在程式執行期間輸出 OpenMP* 執行時庫環境變數
-
OMP_NUM_THREADS - 指定要使用的執行緒數
如需詳細瞭解 KMP 變數,請訪問 Intel 網站;如需詳細瞭解 OMP 變數,請訪問https://gcc.gnu.org/onlinedocs/libgomp/Environment-Variables.html
雖然調整環境變數可為您帶來很多好處(如下文所述),但簡單的做法是將 inter_op_parallelism_threads 設定為與物理 CPU 的數量相等並設定以下環境變數:
-
KMP_BLOCKTIME=0
-
KMP_AFFINITY=granularity=fine,verbose,compact,1,0
使用命令列引數設定 MKL 變數的示例:
KMP_BLOCKTIME=0 KMP_AFFINITY=granularity=fine,verbose,compact,1,0 \
KMP_SETTINGS=1 python your_python_script.py
使用 python os.environ 設定 MKL 變數的示例:
os.environ["KMP_BLOCKTIME"] = str(FLAGS.kmp_blocktime)
os.environ["KMP_SETTINGS"] = str(FLAGS.kmp_settings)
os.environ["KMP_AFFINITY"]= FLAGS.kmp_affinity
if FLAGS.num_intra_threads > 0:
os.environ["OMP_NUM_THREADS"]= str(FLAGS.num_intra_threads)
有些模型和硬體平臺可以從不同的設定中受益。下面介紹了會影響效能的各個變數。
-
KMP_BLOCKTIME:MKL 預設值為 200 毫秒,這在我們的測試中並不是最佳值。對於經過測試的 CNN 類模型,0(0 毫秒)是一個不錯的預設值。AlexNex 可在預設值設定為 30 毫秒時實現最佳效能,而 GoogleNet 和 VGG11 可在預設值設定為 1 毫秒時實現最佳效能
-
KMP_AFFINITY:建議的設定是 granularity=fine,verbose,compact,1,0
-
OMP_NUM_THREADS:預設為物理核心數。針對某些模型使用 Intel® Xeon Phi™ (Knights Landing) 時,調整此引數並使其與核心數量不匹配會產生影響。要了解最佳設定,請參閱針對現代 Intel® 架構的 TensorFlow* 優化 (https://software.intel.com/en-us/articles/tensorflow-optimizations-on-modern-intel-architecture)
-
intra_op_parallelism_threads:建議將此值設定為與物理核心數相等。將值設定為 0(預設值)會將此值設定為邏輯核心數,這是某些架構可以嘗試的備用選項。此值應與 OMP_NUM_THREADS 相等
-
inter_op_parallelism_threads:建議將此值設定為等於套接字數。將值設定為 0(預設值)會將此值設定為邏輯核心數
從原始碼構建和安裝
預設的 TensorFlow 二進位制檔案面向最廣泛的硬體,以便讓所有人都可以訪問 TensorFlow。如果使用 CPU 進行訓練或推斷,建議您使用可用於所用 CPU 的所有優化來編譯 TensorFlow。下面的比較編譯器優化中介紹瞭如何加快在 CPU 上進行訓練和推斷的速度。
要安裝最優化的 TensorFlow 版本,請從原始碼構建和安裝。如果需要在具有與目標硬體不同的硬體的平臺上構建 TensorFlow,則使用針對目標平臺的最佳優化進行交叉編譯。以下命令展示瞭如何使用 bazel 針對特定平臺進行編譯:
# This command optimizes for Intel’s Broadwell processor
bazel build -c opt --copt=-march="broadwell" --config=cuda //tensorflow/tools/pip_package:build_pip_package
環境、構建和安裝提示
-
./configure 會詢問您要在構建中包含哪個計算功能。這不會影響整體效能,但會影響初始啟動。執行一次 TensorFlow 後,編譯的核將被 CUDA 快取。如果使用 Docker 容器,則不會快取資料,並且每次 TensorFlow 啟動時都會受到影響。最佳做法是將系統會使用的 GPU 計算能力包括在內,例如 P100:6.0、Titan X (Pascal):6.1、Titan X (Maxwell):5.2 和 K80:3.7
-
使用支援目標 CPU 的所有優化的 gcc 版本。建議的最低 gcc 版本是 4.8.3。在 OS X 上,升級到最新的 Xcode 版本並使用 Xcode 隨附的 Clang 版本
-
安裝 TensorFlow 支援的最新穩定 CUDA 平臺和 cuDNN 庫
發展歷史
資料格式
資料格式是指傳遞到給定操作的張量的結構。下面專門討論了代表影象的四維張量。在 TensorFlow 中,四維張量的各部分通常用以下字母表示:
-
N 是指批次中的影象數量
-
H 是指垂直(高度)維度中的畫素數
-
W 是指水平(寬度)維度中的畫素數
-
C 是指通道。例如,1 代表黑白或灰度,3 代表 RGB
在 TensorFlow 中,有兩種命名約定,它們分別代表兩種最常見的資料格式:
-
NCHW 或 channels_first
-
NHWC 或 channels_last
NHWC 是 TensorFlow 的預設值,NCHW 是使用 cuDNN 在 NVIDIA GPU 上訓練時使用的最佳格式。
最佳做法是構建同時支援這兩種資料格式的模型。這樣可以簡化在 GPU 上進行訓練、然後在 CPU 上進行推斷的過程。如果使用 Intel MKL 優化編譯 TensorFlow,許多操作(尤其是與 CNN 類模型相關的操作)都將得到優化,並且支援 NCHW。如果不使用 MKL,則在使用 NCHW 時,CPU 不支援某些操作。
這兩種格式的簡短歷史是 TensorFlow 首先使用 NHWC,因為它在 CPU 上的速度要快一些。從長遠來看,我們正在研究可用於自動重寫圖的工具,以使格式之間的切換變得透明,並利用微型優化(在這種優化中,GPU 操作使用 NHWC 可能要比通常最有效的 NCHW 速度更快)。
除錯
除錯輸入流水線優化
典型模型會從磁碟檢索資料,並在將資料傳入網路之前對這些資料進行預處理。例如,處理 JPEG 影象的模型將遵循以下流程:從磁碟載入影象,將 JPEG 解碼為張量,裁剪並填充,可能會翻轉和失真,然後進行批處理。該流程稱為輸入流水線。隨著 GPU 和其他硬體加速器的速度的提升,資料預處理可能會成為瓶頸。
確定輸入流水線是否是瓶頸可能很複雜。最簡單的一種方法是在完成輸入流水線之後將模型簡化為單個操作(小型模型),並測量每秒樣本數。如果完整模型和小型模型的每秒樣本數差異很小,那麼瓶頸可能在於輸入流水線。以下是發現問題的一些其他方法:
-
通過執行 nvidia-smi -l 2 檢查 GPU 是否未充分利用。如果 GPU 利用率未達到 80-100%,那麼輸入流水線可能是瓶頸所在
-
生成時間軸並查詢大塊空白區域(等待)。XLA jit 教程中包含生成時間軸的示例
-
檢視 CPU 使用情況。輸入流水線可能已經經過優化,但沒有 CPU 週期來處理該流水線
-
估算所需的吞吐量,並驗證所用磁碟是否能夠達到該吞吐量級別。某些雲端解決方案具有網路掛接磁碟,這些磁碟的啟動速度的速度較慢,僅有 50 MB / 秒,比旋轉磁碟(150 MB / 秒)、SATA SSD(500 MB / 秒)和 PCIe SSD(2000+ MB / 秒)要慢
在 CPU 上進行預處理
將輸入流水線操作放在 CPU 上可以顯著提高效能。利用 CPU 執行輸入流水線可以騰出 GPU 專門用於訓練。要確保在 CPU 上進行預處理,請按如下所示封裝預處理操作:
with tf.device('/cpu:0'):
# function to get and process images or data.
distorted_inputs = load_and_distort_images()
如果使用 tf.estimator.Estimator,輸入函式將自動放置在 CPU 上。