【翻譯】藉助 NeoCPU 在 CPU 上進行 CNN 模型推理優化
本文翻譯自 Yizhi Liu, Yao Wang, Ruofei Yu.. 的 "Optimizing CNN Model Inference on CPUs"
原文連結: https://arxiv.org/abs/1809.02697
翻譯:coneypo
這篇文章介紹了基於 TVM 改進的 NeoCPU 方案,在 CPU 上進行 CNN 模型推理優化;
與之對比是 Intel 的 OpenVINO 版本(2018.5 ,最新的是 2020.2),也是做模型推理優化工作;
TVM 深度學習編譯棧希望針對不同的深度學習框架和硬體平臺如 CPUs, GPUs 和 專用加速器 提供一個通用的軟體棧(OpenVINO
針對於 Intel CPU,TensorRT 針對於 NV GPU)儘可能高效率的將不同深度學習框架可以輕鬆的部署到不同的硬體平臺上:
通過下面這張圖,我們可以更好的看到 TVM Stack 做了什麼工作:
Amazon 基於 TVM stack 中提出的一些計算圖優化方式,加上一些自己提出的卷積過程優化方式,來對 CPU 上推理過程進行加速,提出了 NeoCPU ,並且在主流的 CNN 模型上獲得了不錯的加速效果;
摘要
CNN 的流行普及和 CPU 的大規模使用部署,使得如果我們能夠提高 CPU 上進行 CNN 模型推演的效能,這將意義重大;
為了提高 CPU 上面的 CNN 推理效能,現有的方法比如: 在 MXNet 和 Intel OpenVINO,通常把模型視為一個計算圖,然後使用高效能的庫比如 Intel MKL-DNN 來實現圖的優化操作;
儘管通過這些現有的庫可以提高效能,但是由於 Local operation-level / 區域性操作級別 的優化已經被預先定義,所以很難在圖級別進行優化;
因此,整體上進行端對端的推理優化很受限制;
這篇文章介紹了 NeoCPU, 一種對 CPU 進行 CNN 模型推斷的綜合方法,採用了全棧式的系統優化方案;
NeoCPU 無需藉助於第三方的庫將操作優化為模板,通過操作和影象級別的聯合進一步進行效能優化;
實驗表明針對於 CNN 模型推理,相比於其他實現方法, NeoCPU 能夠達到 3.45x 更低延遲;
1 介紹
CNN 模型在計算機視覺領域大規模使用,使得模型架構優化成為關鍵;
相似的,大規模在伺服器端,客戶端,邊緣端部署 CPU,也使得 CPU 上進行優化意義重大;
所以如何在 CPU 上進行 CNN 模型推演的優化成為很多使用者研究的重點;
CPU 上 CNN 模型效能的推演還有很大提升空間;
CNN 模型推演本質上就是進行執行 a computation graph consisting of operations / 一張由一系列操作構成的計算圖;
在實際應用時,大家經常高效能的 kernel 庫(比如 Intel MKL-DNN 和 OpenBlas)來提高 CNN 操作效能;
這些庫一般輸入目標資料形狀(比如 2D 卷積),然後進行常規呼叫操作,但是這些庫大多數情況下只關注於(大多數情況下是卷積)操作,而錯過了在圖級別進行進一步端對端模型推理的優化機會;
圖級別的優化往往是交給深度學習框架,比如 TensorFlow 和 MXNet;
然而,圖級別的優化,比如 operation fusion / 操作融合 和 data layout planning / 資料佈局規劃,往往因為已經在第三方庫中被預先定義而被限制;
因此框架中的優化工作和 kernel 庫中的優化相沖突,這使得有效能提升空間但是沒有被髮掘;
此外,不同的 CPU 架構會依賴不同的高效能庫,把庫和深度學習框架整合很容易出錯,而且很耗費開發時間;
而且儘管這些庫被高度優化過,它們是作為第三方的 plug-ins,這使得可能會和框架中其他的庫引起衝突;
比如說 TensorFlow 原本使用 Eigen 庫來處理 CPU 的計算,後來引入 MKL-DNN,所以執行 MKL-DNN 執行緒會和 Eigen 的執行緒導致資源爭用,引起衝突;
所以這種 framwork-specific / 依賴框架 的方法,用於 CPU 上進行 CNN 模型推算是不靈活,麻煩而且效果不好的;
由於框架的限制,如何不引入框架(比如 framework-agnostic / 框架無關 method),來進行 CNN 模型推理效能的優化成為了很多人想要解決的問題;
最近,Intel 釋出了一款通用的 CNN 模型推理引擎,稱之為 OpenVINO 開發套件;
這款開發套件在 x86 平臺的 CPU 進行計算機視覺任務的 CNN 模型優化,而且相比於單獨使用深度學習框架,能夠獲得更好的效能;
由於 OpenVINO 也是基於 MKL-DNN 來進行呼叫操作,所以只能提供有限的 Graph-level / 圖級別 的優化(比如 ngraph 中的操作融合);
因此 OpenVINO 進行優化對於大多數 CNN 模型意義不大;
基於之前的研究觀察,我們得出這樣的結論——“flexible end-to-end optimization / 靈活的端對端優化” 是進一步提高 CNN 模型推理能力的關鍵;
這篇文章中,我們建議使用 NeoCPU 方式進行 CPU 上 CNN 模型的優化;
NeoCPU 是全棧的和系統性的,其中包括操作界別和影象級別的聯合優化,而不是依靠第三方高效能庫;
在操作級別,我們利用成熟的技術來優化計算量最大的操作,比如模板中的卷積操作,適用於在不同 CPU 架構上跑不同負載,而且讓我們可以在圖級別靈活操作;
在圖級別,除了常規的比如操作融合和推理簡化,我們通過操縱資料佈局流程來協調各個操作的優化,貫穿整個模型以獲得最佳的效能表現;
總而言之,NeoCPU 通過一種靈活和高效的方式,進行端到端的優化,而現有的其他方式往往依賴於第三方庫,需要進行效能調優;
NeoCPU 基於深度學習編譯棧 TVM 進行一系列改進;TVM 讓我們可以進行操作級別的優化,而不是依賴於第三方庫,這使得我們很靈活的可以進行 operation-level / 操作級別 和 graph-level / 圖級別 的整合;
然而,在 ARM CPU 上,只有一種對於特定型別資料,進行定製化的 operation-level / 操作級別 的優化;
在此之前,TVM 沒有提供 operation-level / 操作級別 和 graph-level / 圖級別 的聯合優化功能;
除此之外,一些其他深度學習編譯器比如 Tensor Comprehensions 和 Glow,它們都不是專注於在 CPU 上進行優化,或者對於 CPU 上優化的效能提升沒有那麼顯著;
比如基於文章描述以及我們自己的實驗,Glow 僅僅優化 CPU 中的單核效能,因此我們不建議採用這種方式;
表 1 總結了 NeoCPU 和其他優化方式的對比,在幾種主流的 CPU 上優化效能結果表現不錯;
Op-level opt | Graph-level opt | Joint opt | Open-source | |
NeoCPU | 支援 | 支援 | 支援 | 支援 |
MXNet / TensorFLow | 第三方 | 有限的 | 不支援 | 支援 |
OpenVINO | 第三方 | 有限的 | 未知 | 不支援 |
Original TVM | 不完善 | 支援 | 不支援 | 支援 |
Glow | 支援單核 | 支援 | 不支援 | 支援 |
這篇文章會介紹以下幾點:
- 提供一種在不同主流 CPU (Intel, AMD 和 ARM)上的 operation- and graph-level joint optimization scheme / 操作級別和圖級別的聯合優化方案 來獲取高效能的 CNN 模型推演效能,表現要好過目前的其他方案;
- 構建一種模板可以進行高效率的卷積,通過這種方式,可以靈活的在不同架構 CPU (x86 和 ARM)上進行卷積操作的優化,而不需要依賴於第三方高效能核心庫;
- 設計一種全域性的方案,在一個 CNN 模型中的不同操作組合中,尋找最優佈局方式,在保證高效能的同時,減少操作之間資料佈局轉換帶來的開銷;
值得注意的是,本文主要考慮 direct convolution computation / 直接卷積運算,NeoCPU 也相容在其他計算密集型核心上的優化工作,比如通過 Winograd 和 FFT 進行卷積;
用 15 種主流的神經網路進行測試,我們在 x86 和 ARM 架構的 CPU 上進行了 NeoCPU 的評估,NeoCPU 的表現出色:
- 在 Intel Skylake CPU 上,15 種中 13 種最優;
- 在 AMD EYPC CPU 上,15 種中 14 種最優;
- 在 ARM Cortex A72 CPU 上,15 種中 15 種最優;
值得注意的是,在 x86 CPU 上,Intel 利用 Intel MKL-DNN 進行調優,而對於 AMD 的 CPU 優化程度很低;
選擇 framework-specific / 指定框架(比如 MXNet 和 TensorFlow)和 framework-agnostic / 框架無關(OpenVINO)的解決方案,往往在某一種情況下表現突出,而在另一種情況下表現較差;
而 NeoCPU 在不同架構的 CPU 上的表現十分的均衡高效;
除此之外,NeoCPU 提供一個小尺寸的獨立模組,不依賴於框架或者高效能核心庫,可以在不同平臺上輕鬆部署;
NeoCPU 在 Amazon 的 SageMaker Neo Service 上部署使用,使得模型開發者可以在基於 CPU 的雲端伺服器和邊緣端裝置進行推算優化;
已經有很多應用開發者在藉助 NeoCPU 在不同平臺上進行 CNN 模型的部署推算優化;
所有的原始碼都在 TVM 的開源專案中進行釋出;
這篇文章剩下部分介紹如下內容:
- 第二章介紹了現代 CPU 的背景和典型的 CNN 模型;
- 第三章介紹了我們提出的優化思路以及如何實施;
- 第四章介紹了對於該方案的評估;
- 第五章介紹了相關工作;
- 第六章總結;
2 背景
2.1 現代 CPU
儘管加速器比如 GPU 和 TPU 在深度學習中表示出色,但是很多深度學習的計算工作,尤其是 model Inference / 模型推理,是在 CPU 上進行的;
如今,大多數 CPU 都是 Intel 或者 AMD 的 x86 架構,與此同時 ARM 的 ARM CPU 佔據了嵌入式和移動裝置市場;
製程工藝的提升,電晶體尺寸不斷變小,使得我們可以製造出更大規模和更復雜的處理器,藉此 CPU 通過增加核心數來實現和提高平行計算能力;
在一個多核處理器上,要避免不同執行緒之間的干擾至關重要,最小化執行緒間的 synchronization cost / 同步損耗;
在處理器內部,一個單個物理核通過 SIMD (single-instruction-multiple-data,單指令多資料流) 技術來達到最高效能;
SIMD 將多個值載入到 wide vector registers / 寬向量暫存器,然後一起處理;
(* SIMD 是一種採用一個控制器來控制多個處理器,同時對一組資料(資料向量)中的每一個分別執行相同的操作,從而實現空間上的並行性的技術;)
比如 Intel 提出了 512-bit Advanced Vector Extension instrcution set (AVX-512),在每個 CPU 迴圈週期,處理 16 個 32 位單精度浮點數(總共 512 位);
AVX2 在 256 位的暫存器中處理資料;
除此之外,這些指令集利用 Fused-Multiply-Add (FMA) 技術來執行向量化的乘法,然後在同一個 CPU 迴圈週期中,將累加結果儲存到另一個向量暫存器中;
類似於 SIMD 的技術也被整合在 ARM CPU 和 NEON 上;
我們希望能夠找到一種在 x86 和 ARM 的架構 CPU 上通用的優化方式;
除此之外,值得注意的是,如今大多數伺服器端的 CPU 通過 simultaneous multi-threading (SMT) 技術,支援 hyper-threading / 超執行緒 技術;
這樣的話在一個物理核上可以有兩個虛擬核,用來提高系統吞吐量;
然而超執行緒對於效能的提升取決於應用程式;
在我們的案例中,我們不使用超執行緒,因為一個執行緒會佔用對應物理核心的資源,如果在同一個物理核上再開一個執行緒,會造成效能下降;
我們還會通過共享記憶體模式 (典型 CNN 模型推理中的系統設定)來限制我們的優化在處理器內;
Non-Uniformed Memory Access (NUMA) / 非統一記憶體訪問 不在本文討論範圍之內;
2.2 Convolutional neural networks / 卷積神經網路
Convolutional neural networks (CNNs)/ 卷積神經網路 在計算機視覺任務中大規模使用;一個 CNN 模型經常被抽形成一個 computation graph / 計算圖;
本質上計算圖就是 Directed Acyclic Graph (DAG) / 有向無環圖 ,一個節點代表一個操作,一個從 X 連到 Y 的線表示操作 X 輸出,然後輸入到操作 Y);
執行一個模型推理實際上就是在計算圖中輸入資料,然後得到輸出;
進行圖的優化(比如 prune unnecessary nodes and edges / 刪除多餘節點,pre-compute values independent to input data / 預計算值獨立於輸入資料)可以提高模型推算效能;
CNN 模型推理中的中的絕大多數計算工作,是在 convolutions (CONVs) / 卷積;
這些操作本質上完全可以利用 CPU 中的並行化,向量化和 FMA 特性;
已有的研究表明,通過對資料佈局的優化調整,完全可以在 CPU 上進行卷積操作的優化;
剩下的挑戰就是如何有效的管理資料流程,來讓 CNN 模型推理獲的高效能;
CNN 其餘工作大多數都是卷積中和記憶體相關的操作(比如 batch normalization / 批量歸一化,pooling / 池化,activation / 啟用,element-wise addition / 逐元素新增 等等);
常規做法是將它們融入卷積操作,提高整體的演算法複雜度,來提高效能;
CNN 模型的計算圖訓練本質上和推理沒有區別,僅僅更大規模(加入了 backwards 運算)和一些計算上的瑣碎運算(比如損失函式);
因此針對於 CNN 模型推理時的優化工作也可以用於訓練;
3 Optimizations / 優化
這章我們會介紹我們的優化思路以及如何實現;
這篇文章中介紹的 CNN 模型推算優化方法是針對於 end-to-end / 端對端 情況;
我們的給出的方案,適用於大多數常見的 CNN 模型;
基本思路是把優化視為一個端對端的問題,然後尋找全域性的最佳優化,也就是說,我們不關注於對於單個操作的優化;
為此,我們首先介紹如何利用可配置的模板,來進行 low-level computationally intensive convolution operations / 低層密集型卷積運算 的優化;
通過選擇運算間適當的資料佈局,來減少不必要開銷,使得在一個特定 CPU 架構上,針對一個特定的卷積任務,找到最優實現方式更加靈活;
我們基於 TVM stack,在 compiling pass / 編譯過程, operation scheduling / 操作排程 和 runtime components / 執行元件 加入了一些新特性來實現優化;
原生的 TVM stack 已經實現了一些圖級別的優化(包括 operation fusion / 運算融合,pre-computing / 預運算 和 simplifying interfence for batch-norm and dropout / 歸一化和丟棄的簡化),這些在我們的專案中也進行了採用,但是在此不會去介紹;
3.1 Operation optimization / 運算優化
卷積運算的優化對於整個 CNN 任務效能的提升至關重要,因為卷積運算佔據了整個運算過程中的大多數;
這是一個已經深入研究過的問題,但是以往的解決方法往往是在彙編程式碼層面去研究;
在這一節,我們會利用利用 CPU 的特性(SIMD,FMA,並行化等等)來針對單個 CONV 進行優化,而無需考慮繁瑣的彙編程式碼和 C++ 程式碼;
通過全域性的管理實現,會很容易的把我們的優化方式從單個運算拓展到整個計算圖;
3.1.1 Single thread optimization / 單個執行緒優化
我們首先在一個執行緒中進行 CONV 優化;
CONV 操作計算量大,需要多次遍歷運算元來進行運算; 因此管理輸入到 CONV 的資料佈局至關重要,是減少記憶體訪問開銷的關鍵;
我們首先回歸到 CONV 運算本身來說明記憶體管理機制;
CNN 中一個 2D 的 CONV 輸入 一個 3D 特徵(高度 x 寬度 x 通道數)和 多個 3D 卷積核(通常比高度和寬度小,但是和通道數一樣),輸出另一個 3D 的 tensor / 張量;
計算過程在圖 1 中進行說明(六個引數:in_channel, kernel_height, kernel_width, out_channel, out_height 和 out_width):
圖 1 CONV 和 AVX-512 指令集中高效實現的例子;有三種分別塗成深藍,綠色和粉色的核;為了能夠高效的 FMA(Fused multiply-add),不同核的值被打包成一個 ZMM 暫存器,和不同輸入值相乘,然後累加到不同的 ZMM 暫存器的輸出值中;
卷積核在輸入特徵圖上滑動,對應位置相乘求和,產生輸出特徵圖中相應的元素,可以利用到 FMA;
卷積核的數目構成了 out_channel;
注意 in_channel,kernel_height 和 kernel_width 相互約束,不能被 embarrassingly parallelized / 高度並行化處理 ;
*補充圖 NCHW 介紹:https://oneapi-src.github.io/oneDNN/understanding_memory_formats.html
我們使用傳統的輸入方式 NCHW (輸入和輸出是 4D 的張量,N:批次大小, C:通道數,H:特徵圖高度,W:特徵圖寬度)來描述我們預設的資料佈局;
相關的卷積核是 KCRS(K:輸出通道,C:輸入通道,R:核高度,S:核寬度);
根據經驗,我們將特徵圖的格式設定為 NHCW[x]c (c 是通道數 C 拆分出來的子維度,x 是子維度的分割大小);
比如 sizeof(c) = x,通道數 C = sizeof(C)x sizeof(c) 大小;
輸出和輸入一樣格式: NCHW[y]c,這裡分割因子可以不同;
對應地,卷積核是 KCRS[x]c[y]k,分割尺寸為 x 的 c 和分割尺寸為 y 的 k,是輸入通道 C 和 輸出通道 K 的子維度;
值得注意的是為了得到理想的佈局,需要有大量資料轉換的資源開銷;
除了尺寸地重新排序,為了更好利用最新的向量指令集(比如 AVX-512,AVX2,NEON 等等),我們藉助算數因子 reg_n 把 out_width 分成了 ow_outer 和 ow_inner,然後把 ow_inner 的迴圈移動到 register blocking 內部;
比如在一塊支援 AVX-512 的 CPU 上,我們可以利用 32 x 512 位寬度的暫存器 ZMM0 - ZMM31 ;
我們保持這樣的迴圈機制:一個 ZMM 暫存器儲存 kernel 資料的同時,其他的暫存器儲存特徵圖;
通過 AVX-512F 指令集,一個 ZMM 暫存器 中儲存的 kernel 值(最高 512 bits,float32 x 16 個輸出通道)被用來和 多個 DRAM 中連續不斷的輸入特徵圖 相乘,這些結果之後又會被累加儲存到別的 ZMM 暫存器中;
圖 1 說明了這種方法思路;
針對於其他向量化的指令,我們也可以用這種思路,但是需要改變 out_width (比如 reg_n)的 split factor / 分割因子;
演算法 1 總結了我們在單執行緒中 CONV 的優化方式,本質上是:
- Dimension ordering for friendly memory locality / 優化佈局格式來優化記憶體訪問
- Register blocking for good vectorization instruction utilization / 暫存器阻塞以實現良好的向量化指令利用率
然而不同於其他方式,我們在高階程式語言中,我們定義了一個 template,其中 block 尺寸(x,y),使用暫存器的數目(reg_n),和 loop-unroll strategy(unroll_key)很容易就可以配置;
所以根據不同的 CPU 架構(快取大小,向量寬度等等)或者不同的任務(特徵圖大小,卷積核大小等等),我們可以進行計算邏輯的調整;
這樣的話很靈活,也使得我們下一步進行圖級別的優化成為可能;
演算法1 :通過 FMA 實現 CONV 操作演算法
PARAM: x > 0 s.t. in_channel mod x = 0
PARAM: y > 0 s.t. out_channel mod y = 0
PARAM: reg_n > 0 s.t. out_width mod reg_n = 0
PARAM: unroll_ker from {True, False}
INPUT: IFMAP in NCHW[x]c
INPUT: KERNEL in KCRS[x]c[y]k
OUTPUT: OFMAP in NCHW[y]c
for each disjoint chunk of OFMAP do > parallel
for ow.outer:=0 -> out_width/reg_n do
Initialize V_REG1 to V_REGreg_n by 0
for ic.outer:=0 -> in_channel / x do
for each entry of KERNEL do > (opt) unroll
for ic.inner:=0 -> x do
vload(KERNEL, V_REG0) > y floats
for i:=1 -> reg_n + 1 do > unroll
vfmadd(IFMAP, V_REG0, V_REGi)
end for
end for
end for
end for
for i:=1 -> reg_n + 1 do
vstrore(V_REGi, OFMAP)
end for
end for
end for
3.1.2 Thread-level parallelization / 執行緒級別並行化
通常我們把 CONV 任務分割成幾塊,然後在 CPU 不同核上進行並行執行;
核心庫比如 Intel 的 MKL-DNN 經常使用現成的多執行緒方案,比如 OpenMP;
然而我們發現利用這種現成並行方案的可拓展性並不理想;
因此我們定製化了一個 thread pool / 執行緒池 來高效的處理這種尷尬的並行問題;
在一個有 N 個物理核心的系統中,我們將操作的的最外層迴圈分成 N 份,然後分給 N 個執行緒;
然後我們在併發期間,通過 C++ 11 atomics 來協調執行緒,然後在排程程式和每個工作執行緒之間,通過 an single-producer-single-consumer / 單生產者單消費者模式 的 lock-free queue / 無鎖佇列 ;
活躍的執行緒在不同的物理核上執行,來保證最小化的硬體衝突,正如之前所提到,我們沒有開啟超執行緒;
對於可以被多個執行緒訪問的全域性資料結構(比如 lock-free queues / 無鎖佇列),我們根據需要來插入快取進行填充,來避免執行緒之間的錯誤共享;
總而言之,這個定製化的執行緒池,通過這種機制,來減少資源爭奪衝突,並減少執行緒啟動開銷,這使得這種方式的效能要好於 OpenMP;
3.2 Layout transformation elimination / 佈局轉換(開銷)消除
在這一節,我們把 CNN 模型中的 單個操作優化 拓展到 整個計算圖的優化;
主要的思路來源於 3.1 節介紹的從圖級別減少資料佈局轉化開銷;
之前的操作關注於單步的操作優化,而沒有考慮高度優化的操作之間,資料佈局轉換要帶來的開銷;
在 CNN 模型計算中,大多數的工作量是 CONVs,而輸入一般都是 NCHW[x]c ,所以我們應該確保每個 CONV 都在佈局裡面執行;
然而,有些 CONVs 之間的操作可能只和預設佈局相容,導致每一個 CONV 在計算之前需要將輸入資料佈局(NCHW 或者 NHWC)轉換成 NCHW[x]c,並在最後將其轉換回去;
這種轉換會帶來明顯的效能開銷;
幸運的是,從圖級別去看,我們可以把 CONV 之外的佈局視為一個獨立的節點,僅在必要的時候去插入;
也就是說,我們消除了 CONV 計算時候發生的轉換,並儘可能地通過圖保持轉換後的佈局流程;
為了判斷一個數據轉換是否有必要,我們首先根據操作和資料的接觸方式來分為三類:
1. Layout-oblivious operations / 佈局無關操作:
這些操作不需要考慮 layout,可以在任意佈局中處理資料,比如 ReLU,Softmax 等等;
2. Layout-tolerant operations / 佈局半依賴操作:
這些操作需要知道處理的 data layout,比如 CONV,對於我們來說,要處理 NCHW,NHWC 和NCHW[x]c 佈局; 還有其他一些操作比如 Batch_Norm, Pooling 等等;
3. Layout-dependent operations / 佈局依賴操作:
這些操作只在特定 layout 裡面進行,它們不接受資料轉換,因此在進行這種操作之前,要事先轉換好特定格式;比如 Flatten, Reshape 等等;
典型 CNN 模型中 CONVs 之間操作是佈局無關的(比如 ReLU,SoftMax, Concat 和 ElemwiseAdd)或者 layout-tolerant (比如 Batch_Norm 和 Pooling)型別的,使得資料格式可以保持 NCHW[x]c 來跨越卷積層;
從 NCHW 到 NCHW[x]c 的格式轉換髮生在第一次 CONV 之前;CONVs 之間的的資料佈局可以維持相同格式而不進行轉換(比如 NCHW[x]c 的 x 值相同);
只有依賴佈局的操作,比如 Flatten ,資料佈局要從 NCHW[x]c 轉換回 NCHW;
圖 2:一個簡單 CNN 模型的佈局優化;左邊的流程是預設的資料佈局,每一個粉色的 CONV 節點需要額外的開銷來進行資料轉換,以獲得好的效能表現,然後再轉換成預設佈局;
右邊的流程在圖級別進行優化,最小化資料佈局轉化開銷;綠色的 CONV 節點在計算前後不需要進行任何資料轉換;
實際操作中,我們首先遍歷我們的計算圖,來推斷所有節點的資料格式,正如圖 2 左邊的流程圖所示,然後我們將 CONVs 的佈局從預設轉換為 NCHW[x]c 來獲得更好效能表現;
注意到為了避免進一步轉換,我們把 x 定義為 a constant number / 常量;
然而為了優化效能, x 的值在不同的 CONVs 層可能不一樣,所以需要進行佈局轉換;我們將會在 3.3 節進一步說明;
最後,將 LayoutTransform 節點相應地插入到計算圖中;
因此我們仍然具有網路的 NCHW 輸入輸出,但是 CONV 層之間內部佈局,是以優化過的 NCHW[x]c 格式存在的,正如圖 2 右邊所示;
值得注意的是,模型引數的佈局(比如卷積核權重 ,Batch_Norm 的均值和方差)是不變的,所以可以在編譯期間進行預先轉換;
我們通過向 TVM stack 引入多個圖級別的優化過程來實現這個方法;
通過儘可能保持 CONV 層之間,轉換後的格式佈局不變,和編譯時候對卷積核權重的預轉換,我們進一步提高了 CNN 模型推理的端到端效能;
3.3 Optimization Scheme search / 優化方案搜尋
我們提出了上述的優化方案,尤其根據硬體的特點,比如 cache-size, vectorization unit width, memory access pattern 等等,對資料進行佈局;
然而手動嘗試所有可能的優化方式既繁瑣又不切實際;
所以 3.2 節 假設通道分離出來的引數比如 NCHW[x]c 中的 x,在整個網路中不變,雖然在不同 CONVs 選取不同的 x 值會帶來更好的效能;
除此之外,分離出來 output width 的引數比如 reg_n,也需要針對不同的向量化指令集進行調整;
因此自動的最優方案尋找來進一步提升效能;
我們應該讓領域專家來幫忙構建一個搜尋空間(在最短的時間內,針對某種平臺裝置找到最佳方案);
搜尋分為兩步,第一步區域性搜尋,找到各個計算密集型操作的優化方案,然後是全域性搜尋,選取組合各個方案以獲得最佳的端到端效能;
在 3.1 節中提出的優化模板,證明了這種方式是可行的;
3.3.1 Local Search / 區域性搜尋
第一步為每個 computationally-intensive operations / 計算密集型操作(比如 CNN 模型中的 CONVs)找出優化方式;
我們用一個 tuple / 組 :(ic_bn, oc_bn, reg_n, unroll_ker)來代表一個卷積過程,這些引數來代表在不同架構 CPU上進行不同卷積任務;
前兩個引數 ic_bn 和 oc_bn 代表輸入和輸出通道分離出來的引數(比如 NCHW[x]c 中的 x),針對於某種 CPU,和 cache size / 快取大小 有關;
第三個引數 reg_n 代表 Innder Loop 中要使用的 SIMD 暫存器數目,和 CPU 架構和代數有關;我們也觀察到,在一個執行緒中使用所有的 SIMD 暫存器往往並不能帶來最佳效能表現;
最後一個引數 unroll_ker 是一個布林值 ,用來來決定是否展開對卷積核計算的迴圈(演算法 1 中 12 行),因為有時候展開迴圈會通過減少 branch penaltiles / 分支轉移損失 來提高效能;
區域性搜尋使用 3.1.1 節提到的 template 來找出這些值的最佳組合方式,來最小化 CONV 執行時間;
按照以下步驟進行區域性搜尋:
- 定義 ic_bn 和 oc_bn 的候選列表;為了嘗試出所有的可能,我們列出通道數的所有引數;比如,如果通道數是 64,我們選取 [32, 16, 8, 4, 2, 1] 作為備選;
- 定義 reg_n 的候選列表,實際操作中,我們從 [32, 16, 8, 4, 2] 選取 reg_n 的值;
- 定義 unroll_ker 的候選列表:[True, False];
- 遍歷定義的空間來獲得所有組合的執行時間,每個組合執行多次以獲取平均時間;最終會生成一個按照執行時間升序排列的列表;
值得注意的是,我們通過這種配置的方法來設計這樣一個 tuple,意味著我們可以根據需要去修改這個 tuple(比如加減引數,修改值);
根據經驗,在一臺機器上進行一次 CNN 模型的區域性搜尋,需要花費幾個小時,這是可以接收的;
比如在一臺 18 核 Intel Skylake 處理器機器上,需要花費 6 個小時來進行 ResNet-50 中 20 個不同 CONV 任務搜尋;
除此之外,我們維護了一個數據庫,裡面儲存著每種 CPU 上每種卷積工作量(由特徵圖核卷積核尺寸定義)的結果,以防止在不同模型中重複搜尋;
區域性搜尋針對於每個單獨的操作的優化效果都很好,而且確實是比手動搜尋更高效的方法;
然而對每個操作進行區域性最優搜尋,可能導致並不是全域性最優;
比如兩個連續 CONV 操作 conv_0 和 conv_1,如果 conv_0 的 輸出分割因子(oc_bn)和 conv_1 的 輸入分割因子(ic_bn)不同,我們需要進行額外的佈局轉換工作;
這個額外的轉換帶來的開銷要大於區域性搜尋所帶來的效能提高,尤其網路很大的時候;
換句話說,如果我們在整個網路中選取一個常量作為分割因子(正如 3.2 節所述),我們會在有些 CONVs 沒有進行優化;
因此,我們接下來會用全域性搜尋來做權衡;
3.3.2 Global search / 全域性搜尋
在這一節,我們會將優化搜尋拓展到整個計算圖中;
想法是允許每個 CONV 自由的選擇分割因子 x (即 ic_bn 和 oc_bn),並考慮相應的資料佈局轉換所帶來的時間開銷;
根據 3.2 節所述,CONVs 之間的操作要麼是 layout-oblivious 要麼是 layout-tolerant,所以它們可以使用 CONV 操作所決定的 x 值;
圖 3:CNN 模型推理全域性搜尋:LayoutTransform 可選,如果,加入 LayoutTransform,帶來的資料轉換額外開銷如黃色塊所示;
我們在以圖 3 中模型舉例來說明我們的想法;從圖中可以看到每個 CONV 有一些候選的方案(由不同的 ic_bn 和 oc_bn 組合指定);
通過區域性搜尋可以得到每個組合的最短執行時間;
由於 ic_bn 和 oc_bn 的選擇經常小於 10,所以組合總數一般小於 100;
選擇不同的方案會帶來不同的資料轉化開銷(CONVs 之間虛線框表示)或者不需要轉換(如果某個 CONV 的 oc_bn 等於後續 CONV 的 ic_bn);
為了簡化起見,我們在圖中省略了一些不影響全域性搜尋的操作(比如兩個 CONVs 之間的 ReLu,Batch_Norm);
但是,例如 Elementwis_Add 這種操作不能被省略,因為它需要它的兩個輸入運算元(CONVj 和 CONVk 的 輸出)的格式是一樣的;
也就是說,一個 有著 n 個 CONVs (每個 CONV 由 ki 個可選方案,總數是 k1 x k2 x .. x kn)的 CNN 模型,隨著層數 n 增大,很容易變得很難處理;
幸運的是實際上我們可以使用一個 dynamic programming(DP)algorithm / 動態規劃演算法 來有效的解決這個問題;
為一個 CONV 選擇方案的時候,只要記住目前的全域性最優方案,考慮它自己和它的直接前向連線的 data layout / 資料佈局 ,而不需要任何其他前向的 CONV;
演算法 2 中介紹了這種方法;
實際上許多 CNN 模型結構很簡單,可以簡化成一個列表(列表中每個 CONV 只有一個前向處理);
這種情況下,一個 CONV 完成之後,可以安全的刪除掉前面處理所產生的中間狀態;
對於更復雜點的結構,比如使用 Elementwise_Add 來將兩個 CONV 的輸出輸入到下一個 CONV 就會很棘手,因為一個 CONV 的 schemes 可能需要儲存下來,以後還要使用(比如圖 3 中通過 Elementwise_Add 方式,CONVl 需要 CONVj 的 schemes)
演算法 2 全域性搜尋演算法
以拓撲結構將計算圖中節點進行排序;
使用候選方案的執行時間
for CONVi in topological order do
for each canidate scheme CSIj of CONi do > j is the jth scheme of CONVi
t = execution_time(CSIj)
GSIj = MAX // 在方案 j 下初始化 CONVj 的全域性優化方案
for each so-far globally optimal scheme GSXk of predecessor x do // k 是 CONVx 的 kth 方案
cur_opt = t + transform_time(k,j) + GSXk
if cur_opt < GSIj then
GSIj = cur_opt
end if
end for
end for
end for
return 最後節點的最短方案
然而,如果模型結構過於複雜,CONV 之間存在很多資料依賴關係,那麼 DP 演算法也會變得不好用;
比如,由於很多 concatenation blocks / 級聯塊 的出現,SSD 中目標檢測模型的狀態數可以達到萬億數量級;
這種情況下,我們介紹相似的解決方法來加速搜尋;
我們將全域性搜尋問題,簡化成編譯器領域中,稍加修改的暫存器分配問題;
將暫存器分配問題模型化,每一個 node 有一個候選列表(包含所有可能暫存器選項),每個 edge 和一個 cost matrix / 開銷矩陣 關聯,這個矩陣描述了兩個 node 之間暫存器的可用情況;
和我們全域性搜尋中類似,每個 CONV 有一系列的備選方案,每個 edge 和 兩個 CONVs 的方案列表,所生成的 layout transformation cost matrix / 佈局轉換開銷矩陣 相關聯;
對於別的 non-CONV 的節點,比如 Elementwise_Add,會要求所有的輸入必須是相同格式,我們需要把一個輸入格式進行修改,然後其他的輸入格式都轉換過去;
因此,我們定義 non-CONV 的節點的候選列表定義為和第一個 CONV 的輸入相同,並且將這兩個節點之間的 cost martix 定義為相同,因為對角元素都為0,所以其他元素都無窮大;
由於我們本質上沒有對網路進行更改,所以模型輸出結果不變;
為了驗證,我們將 NeoCPU 結果和其他結果(影象分類模型預測精度和目標檢測模型預測準確度)進行比較;
4.1 Overall Performance / 整體效能表現
表 2 中,我們在不同的 CPU 平臺上,測試不同優化方式對 15 種主流的 CNN 模型的效能提升影響;
1000 次取樣來獲取平均執行時間,每次進行一張影象推理(batch_size=1);
總體來說,在不同的 CPU 平臺使用不同的模型,NeoCPU 方法的效能表現要比其他方法好(忽略 OpenVINO 中的一些異常結果,NeoCPU 最高可以帶來 11x 效能提升);
和每個模型的最佳基準結果比較,NeoCPU的表現如下:
- 在 Intel Skylake CPU 上得到 0.94-1.15x 效能提升;
- 在 AMD EYPC CPU 上得到 0.92-1.72 效能提升;
- 在 ARM Cortex A72 CPU 上得到 2.05-3.45 效能提升;
對於 Framework-specific / 依賴框架 的方案,MXNet 和 TensorFlow 並不是在 CPU 上進行 CNN 模型推理的最佳選擇;
因為缺少在 graph-level / 圖級別 進行優化(比如靈活資料佈局管理)的靈活性;
MXNet 支援 Intel MKL-DNN,所以在 x86 CPU 上面效能不錯;
但是 MXNet 在 ARM 上比 TensorFlow 效能差,因為 Scalability Issue / 擴充套件性問題(圖 4c 所示);
TensorFlow 在 SSD 模型表現明顯不行,因為 SSD 進行推理的時候要進行 Dynamic Decisions / 動態決策;
相比之下,OpenVINO 中框架無關的方案希望通過移除框架限制來加速效能,然而 OpenVINO 在各個模型中的效能測試結果都不穩定;
儘管一些場景下效能不錯,但是有時候在一些特定的模型很慢(比 NeoCPU 在 AMD CPU 上優化 ResNet-152 慢了 45 倍);
在進行結果分析的時候,我們沒有考慮這些異常情況;
值得注意的是 OpenVINO 測量 SSD 的執行時間時候,沒有把很多操作(比如 multibox detection)時間算進去;
由於 OpenVINO 不是開源的,所以無法進行內部修改來獲取 SSD 模型的真實執行時間;
因為 OpenVINO 依賴於 MKL-DNN(針對於 x86 架構),所以不適用於 ARM CPU;
NeoCPU 方案的效能表現突出,因為基於我們第三章所提出的高效能優化技術;
除此之外,所有的基準優化方式很大程度依賴於第三方庫(MKL-DNN,OpenBlas,Eigen);
NeoCPU 不依賴於這些庫,所以有很大的效能提升空間;
表 2 :NeoCPU 和其他基準方案的對比;每個結果是進行了 1000 次測試的平均執行時間;每種模型的最優方案被 加粗顯示;(OpenVINO 的 SSD 執行時間是不精確的)
4.2 Optimization Implications / 優化意義
這一節我們會將詳細介紹第三節描述的優化方案;
為了方便起見,我們在每一個 network family 分別只選取一個網路作比較;
相同 network family 的其他網路優化思路類似;
在 4.2.1 - 4.2.3 節,我們只討論在 Intel CPU 的效能表現(優化效果也適用於 AMD 和 ARM CPU 上);
4.2.1 節介紹 operation-level 優化,4.2.2 和 4.2.3 節介紹了 operation-level and graph-level joint optimization / 聯合優化;
4.2.1 Layout optimization of CONV /
首先,我們比較了表 3 第二行中的 CONV 操作,在有無 organizing the data in a memory access / 記憶體訪問 和 vectorized instruction utilization 向量化指令利用佈局組織資料;
這是 4.1 節中大量使用的 Operation-level 優化,
我們將其複製到一個模板,然後在不更改彙編程式碼或者內部程式碼的前提下,使用 TVM 排程機制來在不同 CPU 平臺上進行 CNN 模型優化;
從表 3 第二行我們可以看到,與預設資料佈局(NCHW)比較有著顯著提升;
兩種實現方式都配置正確的向量化,和執行緒級別的並行化,
也有 TVM stack 中介紹的基本圖級別優化方法,比如 operation fusion / 操作融合, pre-computing / 預計算,inference simplification / 推理簡化 等等;
表3:與 NCHW 基準相比我們的優化方式帶來的效能提升;
4.2.2 Layout transformation elimination / 佈局轉換評估
其次,我們評估了 3.2 節介紹的,通過消除資料佈局轉換開銷帶來的效能提升;
結果如表 3 第三行所示,可以看到減少了佈局轉換的開銷,效能提升了 1.1-1.5x;
NeoCPU 使用系統的方法來消除不必要的資料佈局轉換,通過推斷全域性的資料佈局,並僅在需要的時候插入格式轉換節點;
4.2.3 Optimization scheme search / 優化機制搜尋
接下來,我們比較我們搜尋演算法產生的優化機制,和手動篩選結果的效能表現;
根據表 3 中的第三和第四行,我們可以看到 3.3 節中介紹的演算法能夠找到資料佈局的接近最優組合;
比我們手動找出的結果效能要好 1.1-1.5 倍;
全域性搜尋加速了 ResNet-50(和它的 variants)獲得了加速,因為網路結構更加複雜所以有更多優化空間;
作為對比,VGG-19(和它的 variants)加速效果沒那麼好,因為結果比較簡單;
SSD 利用相似演算法,獲得了顯著的加速效果;
結果驗證了自動搜尋,可以讓我們不需要手動進行調參,還可以獲得更好的效能;
據我們所知,NeoCPU 是唯一種目前可以達到這種程度優化的方案;
4.2.4 Multi-thread parallelization / 多執行緒並行化
最後,我們用 3.1.2 節提到的執行緒池(通常在 GCC 編譯器中通過 OpenMP API 實現)實現的多執行緒,來進行拓展性的測試;
我們對使用了 Intel MKL-DNN,OpenBlas 和 Eigen(所有通過 OpenMP 實現多執行緒)的 MXNet,TensorFlow 和 OpenVINO 的結果進行對比;
我們通過環境變數來配置 OpenMP,來確保執行緒的分配(每個執行緒會在一個互不相干的核心上執行),類似於執行緒池的操作;
按照一張接一張的順序(batch size=1)處理,一秒內一個模型能夠處理的數目在圖 4 中給出;
為了方便展示,圖 4 按照 CPU 平臺劃分成 3 張圖(Intel / AMD / ARM);
圖 4 表明我們的執行緒池的效能,要比 OpenMP 在 NeoCPU 或者其他方案更好;
OpenMP 啟動關閉執行緒的開銷要大於我們的執行緒池,而且拓展性也不好;
此外,我們觀察到,有時候在新增執行緒時,效能表現會有波動甚至下降;
OpenMP 的效能也有可能根據實現方式不同而不同;
總結來說,我們的評估方法適用於我們的場景,但是針對於不同場景最好有自己定製化的執行緒池;
圖 4:不同執行緒數下不同優化方案的處理速度;Standard errors / 標準誤(< 0.4)太小在圖中看不到
5 Related Works / 相關工作
深度學習在我們日常生活中應用越來越廣泛,但是仍然還有很多的工作要做(在不同的硬體平臺上,CPU / GPU / FPGA / 加速器 上去進行加速深度學習過程);
如今深度學習框架經常要在不同硬體平臺上,利用這些優化的實現方式來執行深度學習訓練和推理;
對於一些對於推理效能有特殊要求(比如要求 低延遲 / low- latency 或者 small-binary-size )的硬體平臺,我們也需要對其進行優化工作;
NeoCPU 更加的靈活,高效的把 operation-level 和 graph-level 優化相結合;
儘管本文專注於如何在 CPU 平臺上進行優化,但是這些思路也可以應用到其他硬體平臺上;
NeoCPU 基於 TVM stack( 啟發於 Halide 的一個端到端的框架),TVM stack 中介紹瞭如何將一個深度學習網路轉為 Intermediate Representations (IRs) / 中間檔案 ;
也有幾種其他類似的深度學習編譯器比如 TensorFLow XLA,Tensor Comprehensions,Glow 和 DLVM;
然而,這幾種編譯器都沒有類似於我們這種,在 CPU 推理優化過程的研究結果(比如 Glow 僅僅在 CPU 進行單核優化);
我們相信我們提出的這種方案可以整合到這些框架中;
我們利用其他高效能庫中,成熟的方法來優化計算密集型的 CONV 操作;
除了這些庫,對於 convolutions / 卷積操作 和 matrix multiplications / 矩陣乘法 ,在 Intel CPU 上也有一些高度定製的優化;
這些工作大多數關注於單個 operation-level 的優化,根據卷積過程和 CPU 資源來進行微調,而不考慮整個網路;
這種優化可以在目標 CPU 上最大化卷積的效能,但是拓展到其他平臺上做聯合優化就很不方便;
和其他優化不同,我們有一個可以配置的模板來進行優化配置,這樣的話對於不同架構 CPU 就可以進行很靈活的配置,在 operation-level 和 graph-level 進行聯合優化也會變得很容易;
我們利用自動搜尋來尋找最佳優化方式;
相似的 auto-tuning 思路在其他地方也被介紹過;
然而他們都關注於對於單個操作,進行效能調整,而我們是對於整個 CNN 模型進行全域性優化考慮;
最近我們也在關注在 graph-level 進行 DNN 任務的優化,優化任務犧牲一些區域性的優化效能來提高整體的優化效能;
這種非貪婪的想法和我們的思路很相似,也運用在我們的方案中;
我們受啟發於 Register Allocation Problem 中的 PBQP,利用相似演算法來對複雜結構模型(比如 SSD)進行全域性搜尋;
這篇文章利用已有的方案思路,稍加修改然後運用到新的領域;
6 Conlusion / 總結
這篇文章中,我們提出了一種端到端的解決方案,在 CPU 上用來編譯和優化 CNN,來進行高效的模型推理;
實驗表明,在不同種類的 CPU 上(Intel Skylake,AMD EPYC 和 ARM Cortex A72),針對 15 種主流的 CNN 模型中,和其他最先進的方案相比,我們能夠達到 3.45X 效能提升;
未來我們會關注於:
- 拓展其他卷積計算演算法,比如 Winograd 和 FFT;
- 支援處理量化(比如 INT8)模型推理;
- 在別的硬體平臺(比如在 Nvidia 的 GPU 上和 TensorRT 比較)拓展我們 operation-level 和 graph-level 聯合優化方案;
&n