原創 霞影 淘系技術 4月28日

簡介
2020年5月,MNN釋出了1.0.0版本,作為移動端/服務端/PC均适用的推理引擎,在通用性與高性能方面處于業界領先水準。
這之後,我們并沒有停止腳步,而是繼續在 CPU/GPU/NPU 持續發力。其中,CPU 作為最普适易用的計算資源,我們在多種架構,多種精度模式下繼續進行優化,使 MNN 整體的CPU性能進一步提升:AVX2 架構下性能提升 70%-100%,ARM / ARMv8.2 架構下提升約 10-20%,新增了對 Intel Xeon 處理器的 AVX512 指令集的支援,建立對中低端移動手機的 BF16 優化,新增低比特量化計算支援(Int7 / OAQ / Winograd-Aware)。
▐ 成果概述如下
【注:GPU 部分優化成果參見
《談談MNN GPU性能優化政策》一文】
架構改造
所謂磨刀不誤砍柴工,為了支援更多指令集架構與更多精度選擇,我們先進行了架構改造,以便于減少後續優化成本。
▐ 幾何計算
幾何計算是我們為了适配各類硬體所提出一種簡化算子的方案,其核心思想是從原始算子裡面分離坐标映射部分,降解為更基礎的算子。
通過幾何計算機制,一些原本在CPU上優化得不充分的算子,如 Slice-TF / Pack / Unpack / StridedSlice 等自然消除,相應的業務模型也得到了加速。
但由于各硬體的記憶體設計不一樣,目前幾何計算在處理坐标映射時隻能比較保守地生成記憶體拷貝算子而不是直接引用,在處理複雜算子如卷積、反卷積等會因引入額外的性能損耗而無法拆解。
對于CPU,不論是各架構還是各類精度(以下簡稱分支),記憶體設計是完全一樣的,我們可以在幾何計算的基礎上,對各分支做複雜算子實作的統一。
▐ 算子統一
卷積、反卷積、矩陣乘等算子會有多種實作,以卷積為例有如下實作:
- Slide-Window
- Im2Col+GEMM
- Winograd-GEMM
它們在不同的輸入條件下各有優勢,MNN 會通過 cost 計算,選擇最優實作,保證各類模型都能達到最優性能。
如果針對各分支分别寫這些實作,工作量無疑是巨大的。
是以,基于CPU記憶體同構的特性,我們把繁雜而并不耗時的記憶體管理、位址計算、預處理、分塊并行等複雜邏輯統一實作,每個分支實作核心計算函數。
經過這番改造後,我們算子實作上的優化,可以較低成本同步到各個分支上。
算子優化
▐ 矩陣乘實作改進
MNN卷積最初的實作為滑窗算法,為了友善進行SIMD優化,采用NC4HW4 的記憶體布局,即:
次元上c上對齊到4然後拆分,c的坐标j變換為
Im2Col-GEMM / Winograd-GEMM 算法的實作中,我們會根據這個NC4HW4布局,實作
的矩陣乘彙編,其中X為分塊大小,受寄存器數N的限制2X+4≤N,在ARMv8 上為 14 ,然後每次拷貝/計算出
大小的計算,經過矩陣乘計算後填到對應的輸出位置。
這樣實作預設在C方向上強制做了4對齊,兩個輸入每次都需要以SIMD機關去讀,理論讀取記憶體量為:
可并發的FMLA指令數為X,有如下缺陷:
- 可并行的FMLA指令不足,在中低端CPU上不能完全隐藏掉計算延遲
- 記憶體讀取量不夠小
- 不利于其他SIMD架構實作(比如AVX2并行機關為8)
為此,我們修正了矩陣乘算法,采用ep,1,hp的分塊模式,ep,hp由各架構的核心函數确定,并增加一個把 NC4HW4 轉成ep,1的Pack過程。這樣理論讀取記憶體量為:
,可并發的FMLA指令數為(n為SIMD一次計算數)
具體到 ARMv8 架構-浮點分支上,ep=12,hp=8,n=4 ,讀取記憶體量
,并發數為12∗8/4=24>14
雖然額外增加了Pack過程,但總體而言減少了記憶體讀寫量,增加了并發數,能達到理論極限性能。
▐ 卷積實作改進
在卷積算子的實作中,上一節增加的 Pack 過程可以與 Im2Col 過程合并,僅在Im2Col過程中增加一些轉置指令,不影響記憶體讀寫的瓶頸耗時。
首先我們看原始的實作,以 w=4,h=2,c=4,n=1,kx=3,ky=1 為例,我們需要先基于NC4HW4布局進行Im2Col,再把Im2Col的結果轉換到c,ep的記憶體布局。
為了對Im2Col和Pack進行合并,我們注意到在w和c的方向上,記憶體的搬運是有連續性的,是以可以先計算出一個包含一系列矩形框的“骨架”(最多含有
),每個矩陣框包含源位址、目标偏移、矩形大小等資訊,然後再根據這些矩形框,逐個連續從輸入源取值并轉置到目标位置上。
示例流程如下圖:
經過這個優化之後,我們和Google 提出的 的 Indirect Convolution Algorithm (內建在 XNNPack)進行了對比測試,由下表資料可見,在彙編都進行了深度優化的基礎上,MNN 的 Im2Col-Pack Fuse - GEMM 卷積算法比XNNPack略優,此外,在非 1x1 卷積(矩陣乘即可,不需要額外處理)較多的模型,綜合了 Winograd 算法的 MNN 則明顯快于 Tflite-XNNPack 。
A76 上測試資料:
引擎 | Mobilenet v1/ms | Squeezenet v1.0 /ms | InceptionV3 / ms |
Tflite-XNNPack | 33.696 | 54.563 | 342.066 |
MNN-(限定Im2Col-GEMM) | 31.618 | 49.406 | 325.549 |
MNN | 31.997 | 35.463 | 240.169 |
硬體相關優化
▐ ARM 調優
在算子優化的基礎上,我們對ARM彙編做了進一步的精細調優,包括但不限于:
- 矩陣乘/packing等廣泛存在的存儲指令ld* 優化為stp,讀取指令選取合适的優化為ldp。
- 部分合适的代碼優化為讀寫雙pipeline并行,
- 部分簡單指令例如 sub add指令hide到耗時長指令之内
▐ x64-AVX2
主流PC/伺服器均支援AVX2的SIMD計算,它有16個256位的向量寄存器,一次可計算8個浮點數。
基于之前的算子優化,我們将 AVX2 的矩陣乘定為24x1x4,并編寫彙編深度調優,提升了70%-100%的性能,使AVX2上MNN多數模型運作性能接近或超過理論浮點峰值(注:部分模型用Winograd算法加速,為了和訓練架構的統計保證一緻,模型計算量仍按滑窗計算)。
▐ x64-AVX512
Intel出的Xeon處理器增加了AVX512指令集,支援512位的向量寄存器,一次可計算16個浮點數,更新的AVX512-VNNI指令集追加了uint8-int8的dot指令,可以一次性計算32個整型乘加。
dot指令-vpdpbusd
FOR j := 0 to 7
tmp1.word := Signed(ZeroExtend16(a.byte[4*j]) * SignExtend16(b.byte[4*j]))
tmp2.word := Signed(ZeroExtend16(a.byte[4*j+1]) * SignExtend16(b.byte[4*j+1]))
tmp3.word := Signed(ZeroExtend16(a.byte[4*j+2]) * SignExtend16(b.byte[4*j+2]))
tmp4.word := Signed(ZeroExtend16(a.byte[4*j+3]) * SignExtend16(b.byte[4*j+3]))
dst.dword[j] := src.dword[j] + tmp1 + tmp2 + tmp3 + tmp4
ENDFOR
dst[MAX:256] := 0
在Intel同僚的支援下,我們借助AVX512的16機關浮點乘加指令與dot量化計算指令,在AVX2優化的基礎上,使浮點矩陣乘加速 60%(1024x1024x1024矩陣由24ms下降到15ms),量化計算加速200%(單測用例31.53ms下降到10.15ms)。
低精度計算
▐ 半精度浮點
采用16位浮點替代浮點計算,一般對精度影響不大,不但能加速,還可以減少一半的記憶體占用。
2018年之後的新手機大都支援 ARMv8.2 指令集,它支援了FP16的SIMD計算,可以達到浮點的兩倍性能。
MNN在2020年即已支援這一功能,在算子統一的基礎之上,我們重新設計并實作了ARMv8.2的矩陣分塊方案與相應彙編代碼,取得了進一步的性能提升。此外,為了滿足部分應用使用32位架構部署的需求(減少包大小),我們也把FP16計算相關彙編同步編寫了一份32位的彙編,支援了32位架構的FP16加速。
對于老的機器,不支援FP16計算的情況,我們實作了介于 Int8 - FP32 的一種優化方案 —— BF16,以減少記憶體,提升性能。這個方案的原理是以少量的轉換指令(BF16與FP32可以簡單的移位指令互轉),減少讀寫記憶體的損耗。
實測在低端機紅米4上,BF16 有 10%-20% 的加速效果,在中端機榮耀10上,大部分模型也有一定性能優勢。
紅米4 單核性能
模型 | FP32 / ms | BF16 /ms |
Moblienet v2 | 140.801 | 131.472 |
Squeezenet v1.0 | 246.344 | 212.08 |
Mobilenet v1 | 219.629 | 203.085 |
Inception v3 | 1737.672 | 1516.181 |
Resnet v2-50 | 1044.753 | 779.895 |
榮耀10 單核性能
120.563 | 96.106 | |
90.800 | 92.711 | |
710.116 | 669.424 | |
470.768 | 435.611 |
打開 MNN_SUPPORT_BF16 編譯宏,設定 precision = Precision_Low 即可使用。
▐ 低比特量化計算
為了進一步提升量化模型計算的性能,我們需要在訓練時加一些限定條件,使在特定架構下能用相關指令加速。
MNN支援了OAQ量化計算,7Bit下的Winograd量化計算與AVX2優化,在在PAI平台相應的模型壓縮能力支援下,可以在原有量化模型的基礎上提升 30% 左右的性能。
簡述如下表:
低比特量化計算類型 | 限定條件 | 适配架構 | 加速原理 |
Overflow-Aware | 矩陣乘數值範圍小于等于 16bit | ARMv7a/ARMv8 | vmlal.s8 替代 vmull.s8+vpadal.s16 |
Winograd-Aware | Winograd特征變換數值範圍小于等于 8bit 或 特征與權重數值範圍均小于等于7bit | Winograd 算法 F(2, 3)減少運算量 | |
7Bit | 特征或權重數值範圍小于等于7bit | AVX2 | vpmaddubsw + vpmaddwd 替代 vpmovsxbw *2 + vpmaddwd + vpaddd |
小結
▐ 縱向對比
經過這一年優化之後,MNN在ARM/ARMv8.2上有10%-20%左右性能提升,而在 AVX2 上有70%-100%的提升。
Mate30 單核耗時/ms
Mac Retina, 15-inch, Mid 2015 單核耗時/ms
▐ 橫向對比
過去一年業界推理架構發展很快,有不少的優秀開源架構湧現,TNN / Mindspore-lite / Bolt 都宣稱自己的性能業界第一,老将Tflite在內建 XNNPack 後性能也有了質的飛躍。MNN經過這一年優化,在通用性(算子數、訓練架構支援,異構計算支援)有明顯優勢的基礎上,性能保持業界領先,如下是和業界開源架構TNN/Bolt/Tengine/Tflite/Mindspore-lite 對比,可見 MNN 整體CPU性能最優:
Mate30-A76-FP32單核耗時/ms
Mate30-A76-FP16單核耗時/ms
小米6-A73-FP32單核耗時/ms
相關開源架構版本選取如下:
- Bolt : 測試時使用master分支, 版本号>r1.1, commit-id: e951118fb28f5fead39c6e56cba16caef4583a99
- TNN : 測試時使用master分支: commit-id: 98003d6a9ce3c917ccf7522bf24de3a4b43e3824
- Mindspore-lite: 取release 1.1版本. git clone [https://github.com/mindspore-ai/mindspore](https://github.com/mindspore-ai/mindspore) -b r1.1
- TFlite : 測試時使用master分支 commit-id:ee6fa8155e966654f158dad7da06f0b708e9a650
随着新架構、新指令、新算法的湧現,性能優化之路永無止境。MNN将緊随時代潮流,以通用性為基礎,适配新硬體特性、新的壓縮算法,将推理性能優化到極緻,持續幫算法同學解決推理部署的問題。