效能問題無處不在,但是很多同學都可能知道一句話:“過早優化乃萬惡之源”,在日常工作中並不太關注效能,然而在機器學習應用裡面,效能卻關乎應用的生死。這一波人工智慧熱潮的源起之一就是深度學習的計算效能獲重大突破,從而使問題做到工程可解,達到實用的程度。
在實際工作中,我們發現演算法人員的知識領域往往偏應用,更熟悉諸如推薦、物體識別、自然語音處理等業務相關的演算法與模型的設計上,而對計算環境,尤其是異構計算環境的龐雜細節知識不夠了解,從而導致這些演算法應用的效能不高,資源利用效率偏低,有的時候還會因為無法滿足響應時間的要求而無法上線。同時計算平臺的工程師對機器學習的業務流程、應用軟體棧構成、模型、效能相關的可調引數和工具、方法等方面瞭解不多,感覺無處下手。
這篇文章希望能大致梳理一下機器學習應用調優的原則、方法及工具,給相關人員做個參考。我們將自頂而下介紹機器學習效能調優遇到的技術和工具,希望讀者能有一個全域性的認識。調優既需要全域性縱覽,又需要深入毫末,限於篇幅,很多細節點到即止,沒有展開,但是讀者可以根據本文提到的資訊,利用搜尋引擎和文末的參考資料部分深入學習下去。
原則
效能問題是系統性工程問題,不應該使用兵來將擋、水來土掩的救火式的方法去解決問題,而要統籌安排、遵循一定的方法論系統性地揭示和解決。
發現與解決效能問題,如同偵探辦案,需要大膽假設、小心論證。既要有全域性觀,能找到主要矛盾,又要能夠細緻入微,深入技術細節,找到問題的根本原因與解決方案。
在調查效能問題的過程中,需要隨時記錄調查過程,方便隨時覆盤,調整調查方向和手段。
在調查過程中,如果有多個影響因素,應該一次改變一個因素,觀察結果,方便區分不同影響,而不應該一次改變多個因素。
方法論
調優迴圈
效能調優是過程是一個持續改進過程,是一個動態革新的過程,可以抽象成一個優化迴圈。這個迴圈從開發效能基準測試開始,效能基準測試集需要具有典型性,也就是能反映業務的典型場景,要麼從最重要的業務簡化出來,要麼按照典型業務的效能特徵構造出來。而且這個效能基準要具有穩定性,也就是在同樣條件下可以重複得出統計意義上一致的效能結果。效能基準測試最好是做成完全自動化的,即配置、執行與輸入均不須人工干預。一旦確定好效能基準測試集,就可以執行測試,獲取和記錄效能基準資料。重複執行測試,使用效能探測、分析工具收集、分析應用的效能瓶頸,然後針對瓶頸,對系統配置、應用配置或者程式碼作出相應改動,經過正確性驗證後,再次執行效能基準測試,得到新的效能資料,比較效能,如有增強,則確認這次改動,使用版本管理系統記錄改動,這樣就完成了一次效能調優迭代,循此方法,直至效能達到要求或者接近理論預估值。
基礎堅實
在調優開始前,需要確認測試工作環境功能、效能正常,確保工作在正確的基礎上進行。這一步非常重要,但通常都被省略掉。不在正常的環境裡工作均為浪費,南轅北轍。
對於單機環境,主要是 CPU、記憶體、磁碟、網路、GPU 等部件的基礎效能。這會涉及到諸如 BIOS 裡效能偏好的設定、C-State 的設定、NUMA 設定,Hyper-Threading 的設定,記憶體條在插槽的分佈方式、GPGPU 的 PCIe 插槽的分佈方式以及作業系統核心相關調優引數的設定等。執行相關單項基礎效能測試,對照各物理部件的理論效能,如果偏差太大,需要找到相應原因解決。對於多機的叢集環境,在單機效能驗證完以後,主要考慮互聯的吞吐量和延遲效能是否達標,這會涉及到介面卡相關的物理和作業系統配置的修改,RDMA 的配置以及交換機的配置等等。
自上而下
在大規模分散式應用,比如分散式訓練中,首先要優化的是加速比,也就是每增加一臺計算裝置(伺服器或者 GPGPU 卡)的效能增長與單機效能的比值,比如單機效能為 100,而加入叢集后,叢集效能增加了 60,則加速比為 60%。提高加速比可以從並行演算法設計、實現架構以及互聯裝置層面上著手,減少計算節點間的資料依賴,減少資料交換延遲,提高並行度。加速比是提高大作業吞吐量效能的關鍵。
單計算裝置或者稱為單機的效能,一般來講改變演算法和資料結構收益最大,在機器學習應用裡對應的就是選擇合適的模型。而在實踐中,調優階段很難做這麼大改動,通常從資料輸入優化、框架配置等方面入手。然後才是執行庫調優,作業系統相關效能引數調整,諸如 IO 排程策略,大頁記憶體,CPU 核繫結等等。一步一步挖掘,根據發現的效能瓶頸逐漸深入優化。
自上而下的優化順序,有助於首先解決主要問題,獲取最大收益,快速實現調優目標。
效能指標
對於應用效能的衡量,通常有兩個指標 - 吞吐量和延遲。吞吐量指的是單位時間內完成的處理事務總數。延遲指的是一個請求發出到完成的耗時。為了達到這兩個目標的技術實現方式是不一樣的,調優方法也有所區別。提高吞吐量一般是通過提高並行度、使用並行流水線、增大快取、非同步等方法充分發揮資源使用率的方式。減低延遲通常是縮短關鍵路徑,減少同步等待,提高快取命中率,空間換時間等方式實現。提高計算資源的使用率通常對這兩種效能均有好處,但是提高 IO 資源的使用率則未必。
應用效能特徵
應用的效能特徵指的是應用在資源使用上的偏好和模式,比如常見的計算密集型應用,IO 密集型應用等說法就是一種粗略的效能特徵描述。比較精確的描述是首先確定資源維度,也就是哪些資源型別,可以包含硬體資源與軟體資源,比如 CPU、記憶體、磁碟、網路、GPU、資料庫服務等等,然後針對這些資源維度,給出量化的使用率指標,比如 CPU 利用率,記憶體使用量,IO 利用率,IOPS,網路流量, QPS 等,使用效能監控,在一個典型業務場景下,收集各指標,按資源維度消耗就可以描述出這個應用的效能特徵。
應用效能特徵可以用來指導效能優化的大方向,根據效能木桶理論,通常來講主要的優化方向就是利用率最高的那個維度。
機器學習效能相關技術構成
如上文所說,調優是一個逐層深入的過程,必須對整個技術構成有比較深入的瞭解才能知道調查的方向和每層需要關注的問題。我們先由遠而近地瞭解一下機器學習應用效能的相關技術構成。在現階段,網際網路應用的機器學習主要有兩個應用:訓練與推理。訓練是一個大規模資料處理的過程,通常比較關注吞吐量,推理則又分為線上推理業務和離線推理業務,對於線上推理,通常延遲是最主要的效能目標,而離線推理則比較關注吞吐量。主要的應用領域以圖形影象的處理、語音處理、自然語言處理、推薦等,對應的 CNN,RNN,BERT,Wide & Deep 模型與演算法。
機器學習的目的是得到一個模型,可以把模型看成一個函式,訓練就是程式讀入大量資料,經過不斷迭代計算,得出模型的引數,從而使這個函式比較好的模擬真實的規律,在讀入新資料時,得出與事實相近的推斷結果,這稱為推理。
下面我們以比較典型的深度學習的卷積神經網路的訓練過程為例,一層層地深入瞭解它的技術構成與效能調優相關技術。
IO
訓練資料輸入可以有多種來源 - 本地磁碟,網路檔案系統,分散式檔案系統服務等,訓練資料的格式與儲存方式也各異,效能關心的是資料輸入的吞吐量頻寬,IOPS (每秒 IO 操作量),IO 佇列深度等。一般來說,大量小檔案輸入的情況下,IOPS 對效能的影響更大。除了更換更強大的硬體以外,軟體層面上能做的是使用快取、預取以及將資料的 ETL(Extract, Transform, Load)各部分和訓練部分組成 pipeline 以掩蓋操作延遲、增加資源使用率,整合資料以減少 IO 操作等,如果使用網路儲存或者 SSD 等能支援更高 IOPS 或併發訪問的資源上,開闢更多 IO 執行緒也會帶來好處。在 TensorFlow 框架上,使用 tf.data API 尤其是 TFRecord 是投入產出比最好的方式。
在實際的程式碼中經常見到單執行緒的順序式的資料讀取、解碼、預處理、洗牌、組織 batch 的程式碼,這個過程涉及大量 IO 操作和 CPU 計算操作、記憶體複製操作,如果使用 GPU 還會有 PCIe 的資料傳輸操作。這些操作簡單的順序執行,過程緩慢而冗長,導致 CPU 和 GPU 強大的計算能力基本閒置等待的狀態。這個使用 htop 命令和 NVIDIA-smi dmon 命令可以清楚的看到 CPU 與 GPU 的忙閒程度。
還有一個小技巧可以判斷是不是 IO 導致訓練效能低下的元凶。將被訓練的模型計算換成最簡單的計算,然後再測試訓練效能,如果變化不大,做說明了計算速度對效能影響不大,基本可以定位 IO 是效能瓶頸。
計算
有了合適的資料輸入後,就正式進入模型訓練階段了。對於 TensorFlow 1.x 版本框架下構建的程式而言,通常會構建 tf.Graph 計算圖,計算圖描述了資料的計算流程,圖的節點是運算操作(Ops),運算操作代表了特定的抽象運算,會根據具體硬體裝置(device)有具體的運算核(kernel)實現,運算核會利用不同的運算庫,比如 Eigen,NVIDIA CuDNN,Intel的 MKL DNN 等,不同的庫通常都會有相應的效能調節方式,可以在需要的時候使用。
在計算圖節點間流動的是資料,稱為張量(Tensor),如下圖所示。計算圖可以通過 TensorBoard 檢視。
雙擊 namespase 方框,可以展開和收攏細節。直至用橢圓形表示的 Ops,單擊 Ops 圖示,可以看到 Ops 的 Operation,屬性、輸入、輸出等細節。這些細節資訊會在後面的精細化調優的時候用到。
TensorFlow 程式的客戶端,會通過 Session 介面與 Master 進行互動,Master 負責管理一個或多個 worker, worker 與一個或多個硬體裝置(device)相連。Session 的 run() 方法會啟動 worker 執行計算圖,按照事先的實現進行計算。如下圖所示。你會發現 worker 之間(worker 到 parameter server,worker 到 worker)會有資料傳輸,這些資料傳輸會跨越不同裝置,瞭解傳輸的路徑有助於發現瓶頸和優化。
在模型訓練時,尤其是神經網路訓練,如果能夠使用預先訓練好的相關模型,固化靠前的部分層(所謂通用概念層),實現增量式訓練,將大幅降低訓練的計算量,提高訓練效率。
在實際工作中,我們發現精細調優過的機器學習程式碼的效能遠好於簡單直接執行,有的甚至達到10倍以上。下面讓我們來看一下,如何充分發揮計算能力,優化計算效能。
在模型訓練中,batch size 對效能的影響較大,所謂 batch size 就是處理完這些數目的樣本後,才去更新模型的引數,這樣可以大幅度減少模型引數更新的計算數量和相關的資料交換開銷。同時在一定範圍內改變 batch size 的值,並不會對模型的精確度有影響,所以理論上來講,在可能範圍內 batch size 越大則效能越好,但是受限於計算裝置的記憶體大小。通常來講 CPU 上訓練的 batch size 上限大於 GPU 的。此外還有 learning rate 等引數,詳情請參考相關文件。
另外一個對效能有顯著影響的就是計算用的浮點數表示格式,浮點數表示方式有雙精度/DP64(64位)、單精度/FP32(32位),半精度/FP16(16位)等, 通常來講機器學習用的是 FP32 格式,但是有證據表明在神經網路中降低計算數的精度,對最終的模型精度影響可以很小。使用 FP16,由於下面會說明的向量化資料並行運算,效能比 FP32 理論上高了 1 倍,但是在不調整模型的情況下精度損失在 3% 以下甚至更小,因此可以代價很小的使用 FP16 代替 FP32,但是需要監控模型的精度。更近一步還可以使用 int8 甚至更小的資料型別,但是更小意味著更容易溢位和更多精度損失,在實際執行中計算梯度變化值的時候還是使用FP32,但在每層更新引數時轉換為 int8,因此會有大量隱式資料轉換過程,所以使用 XLA、nGraph 或者其他計算圖編譯的融合優化,能減少這種資料轉換和複製操作,從而帶來比較大的改進。
有一種重要的優化可以提高推理的效能和縮小模型的大小,就是模型量化(Quantization),所謂模型量化就是將模型的權值限制為有限個取值,類似於量子學的電子能級,這樣就可以用很少的 bit 數表示了,極端的情況包括二值化和三值化。模型量化可以在訓練好的模型上進行優化,也可以在訓練的時候使用量化感知訓練方式,將相關資訊保留下來供模型量化使用,確保精度,起到更好效果。感興趣的同學可以參考相關論文。
還有一些優化需要對底層的硬體平臺瞭解透徹。我們以一個典型的機器學習伺服器為例,一般來講是一個 2U 或者 4U 的 X86 伺服器,有兩塊 CPU,PCIe 插槽上插著 4 塊或者 8 塊 NVIDIA 的 GPU,比如 V100。
我們先來看看 CPU,伺服器上的這兩塊 CPU 放在兩個插槽(socket)裡,中間有 QPI/UPI 連線。QPI/UPI 的數目根據 CPU 的檔次不同有 1 根到 3 根,頻寬也有不同,這個會影響兩個 CPU 之間的資料交換的效能。每塊 CPU 都有自己的記憶體控制器,記憶體控制器有不同的通道數目,每個通道又支援一定數目和頻率的記憶體條,通道數目、記憶體條的頻率以及記憶體條的插法會影響記憶體的頻寬效能。使用 dmidecode 命令可以檢視記憶體的頻率、型號、插槽位置等資訊。CPU 訪問自己記憶體控制器下的記憶體和訪問另外一個 CPU 下的記憶體速度顯然是不一樣的,這個稱之為非一致性記憶體訪問,簡稱為 NUMA 架構,在 BIOS 裡可以設定為 NUMA 方式或者交叉混合(interleave)方式,現代的 Linux 作業系統都是所謂 NUMA aware 的,在程式排程以及記憶體管理上,會按距離區別對待不同區域的記憶體,因此效能表現會更好,所以建議開啟 NUMA 設定。同時 CPU 功耗偏好也可以設定為效能模式,而不是省電模式。
每塊 CPU 又有多個物理核(core),每個核根據 CPU 型號(有的型號不支援超執行緒)和 BIOS 設定,又有可能有 2 個超執行緒(Hyper-Threading),稱為邏輯核。這些資訊可以通過 lscpu 命令看到。NUMA 資訊可以通過 numactl -H 命令檢視。
在每個核裡面又有多個算術邏輯運算單元,其中對機器學習最重要的為乘加器(FMA),根據 CPU 型號,有 1 到 2 個乘加器,乘加器支援單指令多資料運算(SIMD/AVX),也就是執行一條指令就可以完成多個資料的相同計算操作,比如一條指令完成8 個浮點型數和另外 8 個浮點型數的分別相乘計算。這樣的資料並行可以數倍提高計算效能,但是需要編譯器和程式碼中做稱為向量化的支援,好訊息就是 Intel 的 MKL 數學庫已經實現了常用的計算,比如矩陣乘,使用的時候直接呼叫即可。對於 TensorFlow 而言,預設下載的官方執行程式或者 docker image 沒有針對 AVX 編譯,不能充分發揮 CPU 的效能,建議使用 Intel 發行的 docker image 或者 conda channel 獲取,還有自己從 TensorFlow 原始碼自行編譯,提速效果非常明顯。
使用 MKL enabled Tensorflow,會有一些環境變數影響效能,比如 KMP_AFFINITY, OMP_NUM_THREADS,KMP_BLOCKTIME,以及 TensorFlow 的兩個執行引數intra_op_parallelism_threads,inter_op_parallelism_threads。詳細請參見 TensorFlow Performance Overview,這裡就不展開了。更多 Session 執行引數參見: tf.ConfigProto,根據不同的執行環境和模型情況,裡面有相當多選項對效能有顯著影響。
因為伺服器有兩個物理 CPU,在通常開了 Hyper-Threading 的情況下每個物理核展現為兩個邏輯核,這樣就事實將一臺伺服器的全部邏輯核分成了 4 個不同的組,因此在 TensorFlow 裡面將這些核當成 4 個不同的計算裝置在實踐中也會部分提高效能,具體可以使用 numactl
命令將 4 個TensorFlowworker 與特定核組繫結,同時相應調整 intra_op_parallelism_threads,inter_op_parallelism_threads 引數與實際一致。
讓我們來看看 GPU。在大規模或者複雜模型的訓練中,通常會使用多個卡。如前面的圖所示,運算裝置上的 worker 會跟 parameter server (PS)有大量資料交換,PS 可以放在CPU 上,也可以放在 GPU 上,在所有 GPU 都插在同一個 PCIe Root Complex 或者使用 NVLink 的情況下,NVIDIA 的 NCCL 會利用硬體的P2P 協議,大幅度減少資料通訊的延遲。如果這些條件都不滿足,則 PS 就應該指定放在 CPU 上執行,以獲取最佳效能。
在效能調優中,經常需要揭開效能的黑盒子,常用工具就是各種效能剖析工具。常見的TensorFlow 程式開發語言是 Python,因此可以使用 Python profile、cProfile 模組進行整體性的剖析。
深入到 TensorFlow 執行層面,就可以使用 TFProf 或者 Timeline 深入瞭解各 Ops 的時間佔比資訊,以方便進一步優化主要的效能拖慢操作。TFProf 詳情請見文末的參考資料。
Timeline 用法如下:
1.在程式碼中增加 trace 資料輸出部分
sess.run(...
options=tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE),
run_metadata=run_metadata)
trace = timeline.Timeline(step_stats=run_metadata.step_stats)
with open('timeline.ctf.json', 'w') as trace_file:
trace_file.write(trace.generate_chrome_trace_format())
2.執行下後就會產生一個名為 timeline.ctf.json 的檔案。
3.然後在 Chrome 瀏覽器位址列中輸入 chrome://tracing/,然後點選 Load 按鈕,載入剛才的 timeline.ctf.json 的檔案,就可以看到執行時間線,可以放大、縮小檢視,也可以點選相關 Ops 檢視詳情。
在這個時間追蹤的剖析圖上,很容易發現最耗時的操作,或是並行度高低的情況,XLA 可以用來幫助優化。XLA 是一個編譯器,使用 JIT 即時編譯技術分析計算圖,找出可以融合的操作,將多個操作合併,並生成對應計算裝置的原生程式碼。也可以使用編譯器的優化演算法進行資料流、控制流分析,減少計算。英特爾有類似的工具稱為 nGraph,也可以試一試。
如果上述工具都沒有能幫你達到效能目標,你還可以自己實現 Ops,然後加入 TensorFlow 中,並呼叫它,這在許多需要低延遲的演算法中有較多應用,但難度最大,所以請確認別無他法再用。具體可以參見 TensorFlow 的 Adding a New Op 文件。
參考資料