Halide应用开发
Halide應用開發
- 基本原理
1.1、介紹
隨著人工智能的普及,深度學習網絡的不斷涌現,為了讓各硬件(CPU, GPU, NPU,…)能夠支持深度學習應用,各硬件芯片需要軟件庫去支持高性能的深度學習張量運算。目前,這些高性能計算庫主要由資深HPC工程師(高性能計算優化工程師)進行開發,為了加快開發進程,縮短深度學習應用落地周期,自動化算子優化是一個趨勢。
AutoKernel是由OPEN AI LAB提出的高性能算子自動優化工具,可以自動優化調度策略、生成底層優化代碼,大幅減少各硬件芯片算子開發成本,提升算子優化效率,讓工程師更快實現深度學習算法在各硬件芯片上的高性能部署。
1.2、 AutoKernel架構
AutoKernel分為3個模塊:
算子生成器
該模塊使用了開源項目Halide;Halide是業界廣泛使用的自動代碼生成項目,它首次提出將計算和調度分離。該模塊的輸入是和硬件無關的算子計算描述,輸出是相應后端的優化匯編代碼/目標文件;
自動搜索模塊
該模塊可以通過最優化算法/搜索算法/機器學習/強化學習搜索出相應后端的最優算子的調度策略參數(該模塊仍在開發中);
算子部署插件(AutoKernel Plugin)
Tengine是OPEN AILAB開源的深度學習推理框架,實現了AI算法在不同硬件的快速高效部署。該模塊實現了將自動生成的優化算子代碼以plugin的形式一鍵集成到Tengine中,實現自動優化算子的一鍵部署;
1.3、 AutoKernel特點
? 低門檻: 無需底層優化匯編的知識門檻
? 簡單易用: 提供docker環境,無需安裝環境,plugin一鍵集成到推理框架Tengine
? 高效率: 無需手寫優化匯編,一鍵生成優化代碼,一鍵部署
2 AutoKernel優化GEMM實踐
本部分將帶領大家一步步優化矩陣乘法GEMM。無需手工擼代碼,編寫繁雜冗長的底層匯編代碼,只需十幾行簡潔的調度代碼。
2.1、優化的本質
在詳細講解優化步驟前,先談談優化的本質。在談”優化“的時候:
? 計算機底層做了什么?
? 優化的”瓶頸“是什么?
? 為什么通過一波"優化操作",性能就能提升呢?
? AutoKernel使用的Halide是如何實現自動優化的呢?
上圖是典型的存儲理器層次結構:主存容量大,訪問速度慢,寄存器和緩存讀取速度快,但容量有限。在寄存器的層級上,CPU可以在一個時鐘周期內訪問它們,如果CPU去訪問外部的DDR的話,延遲是非常大的,大概是200個時鐘周期左右。如果CPU去訪問cache的話,一般需要6到12個cycle就夠了。所以,第1個優化宗旨是:優化內存訪問,充分利用寄存器和高速緩存去存數據。
第2個優化宗旨則是提高并行性:充分利用SIMD進行指令向量化和多核心并行。大部分現代CPU支持SIMD(Single Instruction Multiple Data,單指令流多數據流)。在同一個CPU循環中,SIMD可在多個值上同時執行相同的運算/指令。如果在4個數據點上進行向量化,一次計算四個數據,理論上就可以實現4倍的加速。
2.2、運行環境搭建
AutoKernel提供了docker鏡像,docker里已經配置好運行環境,進入docker即可直接運行demo代碼:
拉取鏡像
docker pull openailab/autokernel
啟動容器,進入開發環境
docker run -it openailab/autokernel /bin/bash
獲取代碼
git clone https://github.com/OAID/AutoKernel.git
cd AutoKernel/doc/tutorials/data/
目錄下的build.sh是demo的執行腳本,運行需要指定優化步驟step,可選的step是從1 到7,其中step=1是默認不優化的,step=7是最極致優化的。
執行指令:
執行demo
./build.sh 1 ==> 默認不優化
./build.sh 7 ==> 最極致優化
下圖展示了在Intel? Core? i9-9900K CPU @ 3.60GHz的電腦上的優化效果,無需手工擼代碼,無需編寫繁雜冗長的底層匯編代碼,只需十幾行簡潔的調度代碼, 就能性能優化200+倍~
2.3、詳細的優化步驟:
? STEP-1
第1個步驟是不帶任何優化的。用Halide語言直接描述GEMM的計算過程。
Var x,y;
RDom k(0, K);
Func gemm(“gemm”);
gemm(x, y) += A(k, y) * B(x, k);
計算M=N=K=640的矩陣乘法。運行腳本第一個參數指定step=1。耗時結果如下:
root@xxxxxxxxxx:/AutoKernel/doc/tutorials/data# ./06_build.sh 1
step = 1
M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 240.8523 ms 1.1376 ms
? STEP-2
這一步采用分塊tile。分塊的目的是為了充分利用緩存。如果原來的循環較大,tile分塊改成小塊數據去計算,可以使得每次計算的數據都比較舒適地呆在緩存里,不用經歷重復的驅逐(在緩存中重復的添加和刪除數據)。分塊后進行reorder操作,交換兩個嵌套循環的順序,目的是最內層的內存訪問友好。按照x,y維度劃分成16x8的小分塊去計算:
gemm.update()
.tile(x, y, xo, yo, xi, yi, 16, 8)
.reorder(xi, yi, k, xo, yo);
執行結果如下:
root@xxxxxxxxxx:/AutoKernel/doc/tutorials/data#./06_build.sh 2
step = 2
M N K = 640 640 640 err 0.00 [rep 50] halide | blas 81.8148 ms 1.1281 ms
性能從240ms優化到82ms,提升了近3倍。
? STEP-3
在上一步的基礎上增加向量化vectorize。向量化是把幾個標量計算(scale)轉換為一個向量計算(vector),充分利用SIMD向量指令。大部分現代CPU支持SIMD(Single Instruction Multiple Data,單指令流多數據流)。在同一個CPU循環中,SIMD可在多個值上同時執行相同的運算/指令。
gemm.update()
.tile(x, y, xo, yo, xi, yi, 16, 8)
.reorder(xi, yi, k, xo, yo)
.vectorize(xi, 8);
執行結果
root@xxxxxxxxxx:/AutoKernel/doc/tutorials/data# ./06_build.sh 3
step = 3
M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 27.5433 ms 1.1445 ms
性能從82ms優化到27ms,又加速了接近3倍。可以看到,圍繞前面提到的兩條優化宗旨:優化內存訪問和提高并行性,從step1到step3,性能已經提升了近9倍。
? STEP-4
調度策略在step3的基礎上增加并行化parallel。對一個循環并行化是把循環的每次迭代分給多個線程或者處理器去同時處理,每個線程處理通過代碼段(loop body),但是處理不同的數據。
增加并行化后,build.sh默認指定四線程,性能直接翻了近4倍,從27ms到7.3ms.
? STEP-5
調度策略在上一步的基礎上增加unroll展開。如果循環體內的語句沒有數據相關依賴,循環展開可以增加并發執行的機會,使得更充分利用寄存器,減少循環時每個操作內存加載和保存的次數。
unroll展開后,性能從7.3ms優化到4.8ms.
? STEP-6
前面的分塊成 16 x 8的小kernel, 這一步先劃分成 16 x 32的分塊,然后把每個分塊再分成 16 x 8的子分塊。把最外層的兩層循環合并到一層,并對這一層進行并行化。這一步計算描述多了一個prod函數來定義子分塊的計算,prod函數的計算公式和總的gemm是一樣的,通過 compute_at指定在 yi維度之下計算prod,則prod計算的是 16x8的小kernel, 大致邏輯如下:
這一步距離STEP1性能已經優化了近80倍了,性能越來越接近OpenBlas了。
? STEP-7
這一步添加的操作是對矩陣B進行數據重排,使得在計算小kernel 16x8時,內存讀取更順暢。因為小kernel的x維度是按照16劃分的,因此重排數據B的x維度也是按照16重排。
至此,的每一步調優策略始終都圍繞兩條優化宗旨“優化內存訪問”,“提高并行性”展開優化,到最后性能已經與OpenBlAS差不多了,距離STEP1已經加速了200+倍了。
總結
以上是生活随笔為你收集整理的Halide应用开发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Octave Convolution卷积
- 下一篇: Docker基本原理概述