郭曉龍,牛晉宇,杜永萍
(北京工業(yè)大學(xué) 信息學(xué)部,北京 100124)
隨著深度學(xué)習(xí)的快速普及,卷積神經(jīng)網(wǎng)絡(luò)在圖像識別、目標(biāo)跟蹤等視覺領(lǐng)域作為其算法的核心技術(shù)得到了廣泛應(yīng)用。通常卷積操作無論在參數(shù)量大小還是運算復(fù)雜度上已經(jīng)占據(jù)了整個模型的80%以上。尤其針對低功耗等邊緣設(shè)備,受其有限的算力和內(nèi)存寬帶資源的限制,能否在此類設(shè)備上落地應(yīng)用成為亟需解決的優(yōu)化問題。雖然各大廠商都推出了自己的推理框架,如Caffe、TensorFlow Lite、ncnn、mnn等,但是這些框架通常作為通用型推理框架,往往在某一特定平臺(比如樹莓派)下未必能夠達(dá)到速度最優(yōu)。比如Caffe使用Im2col技術(shù)做卷積優(yōu)化,其核心思想是將卷積操作轉(zhuǎn)化為兩個矩陣相乘,為實現(xiàn)直接矩陣乘法,先要將輸入數(shù)據(jù)和卷積核按照卷積規(guī)則在內(nèi)存中重新排布變成二維矩陣。但在樹莓派上其內(nèi)存重排的耗時往往要大于直接卷積的耗時,還未包含后面的矩陣乘法運算耗時。此外還有國內(nèi)的開源推理框架ncnn,針對卷積的padding操作也需要提前做內(nèi)存重排,以及卷積的bias和relu操作也需要重新遍歷一遍內(nèi)存??梢娖浒嘿F的額外訪存開銷變成了資源的額外浪費。
計算圖優(yōu)化[1]的作用是在不影響模型數(shù)值精度的基礎(chǔ)上,通過拓?fù)鋱D變化達(dá)到減化計算、減少訪問等系統(tǒng)開銷的目的,有助于特定設(shè)備推理加速。在推理過程中,對于不斷的圖片檢測輸入,計算圖優(yōu)化部分只需要在前期做一次即可,后期的每一次推理都可以直接使用優(yōu)化結(jié)果。因此,圖優(yōu)化是整個推理框架[2]的首要準(zhǔn)備及重要工作,也是收益最大的一部分。
該文優(yōu)化的模型為Retinaface[3],該模型基于MobileNet網(wǎng)絡(luò)架構(gòu)[4]。其中類似Conv+BN+ReLU的經(jīng)典組合占據(jù)了模型的80%以上。為提升推理速度,可以采用算子融合的方法將3個算子變?yōu)?個算子,從而減少計算量和訪存量。首先,Conv和BN進(jìn)行合并的推導(dǎo)公式可以參考文獻(xiàn)[5],提前把卷積新的Weight和Bias計算好,替換原卷積對應(yīng)的Weight和Bias值,同時刪除BN算子。其次,對于Relu算子,可以采用本地合并計算(inplace relu)方法,就是在卷積運算的類中設(shè)置標(biāo)志bool relu,在卷積計算之后結(jié)果寫到內(nèi)存之前,根據(jù)保存的relu標(biāo)志位來決定是否執(zhí)行std::max(x,0)。由此優(yōu)化掉了一次全局內(nèi)存遍歷。最終如圖1所示。
圖1 Conv+BN+ReLU算子融合
為了充分利用CPU計算硬件特性,該文采用NEON SIMD方式進(jìn)行計算加速。樹莓派4B采用的ARM A72 64位CPU,NEON指令寬度是128 bit,即16字節(jié),而float占4個字節(jié),因此一條NEON指令可以處理4個float數(shù)據(jù)。又根據(jù)卷積的運算公式是卷積核與圖像數(shù)據(jù)的對應(yīng)位置相乘再相加到結(jié)果中,因此與NEON中的FMLA指令的處理方式相同?;诖?該文采用一種名為NHW4C的存儲形式,將同一像素的連續(xù)的4通道像素連續(xù)存儲在一起。每4通道所有W*H數(shù)據(jù)組成一組,一共分成C/4組。
經(jīng)過上一步的計算圖優(yōu)化之后,已經(jīng)合并了所有的BN+ReLU算子,拓?fù)鋱D已經(jīng)變成連續(xù)相鄰的卷積算子,且所有卷積算子的Weight和Bias參數(shù)格式已轉(zhuǎn)換為NHW4C。為了評估卷積的優(yōu)化方法[6],該文采用Roofline模型分析方法。
Roofline模型[7]分析方法使用計算強(qiáng)度(Operational Intensity)進(jìn)行定量分析,并給出了模型在計算平臺上所能達(dá)到理論計算性能上限的公式。公式表達(dá)為算力上限π除以帶寬上限β,兩個指標(biāo)相除即可得到計算平臺的計算強(qiáng)度上限Imax,單位是FLOPs/Byte。樹莓派4B的峰值算力π=1.5 GHz*1(FMLA指令為單發(fā)射)*(4+4)(FMLA指令一次可以完成4次加法和4次乘法)=12G Flops/s。內(nèi)存帶寬β=4G Bytes/s。因此樹莓派4B的理論計算強(qiáng)度Imax上限=12/4=3 FLOPs/Byte(這里計算的值均為理論值,實測可能要稍低一點)。若計算出的強(qiáng)度低于3 FLOPs/Byte為帶寬瓶頸區(qū)域,說明因為訪存過大無法完全發(fā)揮平臺算力。高于3 FLOPs/Byte為計算瓶頸區(qū)域,說明此時算法已經(jīng)100%地利用了CPU的全部算力。該文的優(yōu)化目標(biāo)就是盡量接近Imax理論上限。樹莓派4B的理論計算強(qiáng)度如圖2所示。
圖2 樹莓派4B Roofline模型
Retinaface模型的第一個算子就是3×3普通卷積,輸入tensor為(1,IC,IW,IH),輸出tensor為(1,OC,OW,OH),卷積核tensor為(OC,IC,3,3),輸入通道IC=3,輸出通道OC=8,步長stride =2。算法1給出了一個普通卷積的常規(guī)計算方法。
算法1:3×3普通卷積原始算法。
輸入:input tensorI,kernel filterF,paddingP,strideS;
輸出:output tensorO。
1 Mat IP=enlarge inputIwith paddingP
2O=add(O,bias)
3 forc=0 toCo-1 do
4 forw=0 toWo-1 do
5 forh=0 toHo-1 do
6 fork=0 toCi-1 do
7 fori=0 toWf-1 do
8 forj=0 toHf-1 do
9Oc,w,h+=IPk,w×s+i,h×s+j×Fc,k,i,j
10O=max(0.f,O)
算法1流程與ncnn中卷積算法基本一致,除了卷積計算部分(第3~9行)采用SIMD外。算法1對內(nèi)存的所有操作包括:
(1)對輸入數(shù)據(jù)進(jìn)行padding擴(kuò)張。讀取輸入數(shù)據(jù)后寫入到擴(kuò)張的新內(nèi)存。共需要IC*IW*IH+IC*(IW+2*padding)*(IH+2*padding)內(nèi)存讀寫??墒÷詾?*IC*IW*IH。
(2)將bias值提前寫到輸出數(shù)據(jù)中(第2行)。共需要OC*OW*OH內(nèi)存讀寫。最后對輸出數(shù)據(jù)進(jìn)行Relu操作(第10行),先讀取后寫入,共2*OC*OW*OH。
(3)4層for循環(huán)中(第3~6行)輸入數(shù)據(jù)共讀取了OC*IC*IW*IH,輸出數(shù)據(jù)先讀取后寫入總量2*OC*IC*OW*OH。
(4)卷積核共需OC*IC*3*3內(nèi)存讀取。
算法1的計算訪存比為:
[OC*IC*OW*OH*3(KW)*3(KH)*2]/
[((2+OC)*IC*IW*IH+5*OC*IC*OW*OH+OC*IC*3*3)*sizeof(float)]
由 IC=3,OC=8,stride=2,可知OW=IW/2,OH=IH/2。因此可以進(jìn)一步簡化為:
可見算法1的計算強(qiáng)度僅為樹莓派4B的理論計算強(qiáng)度的15%。已經(jīng)明顯屬于嚴(yán)重訪存瓶頸區(qū)域了,嚴(yán)重受限于訪存瓶頸的限制,無法發(fā)揮硬件的算力。
2.1.1 訪存優(yōu)化
由于第一個算子的輸入數(shù)據(jù)(Input Tensor)是每一次推理過程的外部傳入的(通常為圖像的RGB輸入格式),其格式為NCHW,這里也可以將NCHW轉(zhuǎn)為NHW4C再進(jìn)行推理(如ppl.nn框架就是這么做的)。但即使輸入數(shù)據(jù)尺寸為320*320,3通道的float類型圖片數(shù)據(jù),占用內(nèi)存空間也達(dá)到了1 200 KB,在樹莓派4B上一次Reorder轉(zhuǎn)換耗時就要3 ms,一次padding擴(kuò)張需要2 ms。尺寸為800*800的Reorder轉(zhuǎn)換耗時高達(dá)18 ms,padding擴(kuò)張也高達(dá)12 ms。所以進(jìn)行一次內(nèi)存格式轉(zhuǎn)換是非常昂貴的。因此,算法2采用直接卷積方法,意味著輸入數(shù)據(jù)為NCHW,卷積核和輸出數(shù)據(jù)為NHW4C。同時對于算法1中第1行代碼的padding操作,也采用9宮格分塊的策略進(jìn)行優(yōu)化。該文提出的算法2總體的優(yōu)化思路是減少所有不必要的訪存開銷。
算法2:3×3普通卷積優(yōu)化算法。
輸入:input tensorI,kernel filterF,biasB,paddingP,strideS;
輸出:output tensorO。
1 out_neon_w_start=(left_padding+s-1)/s;
2 out_neon_h_start=(top_padding+s-1)/s;
3 out_neon_h_end=Ho-bottom_padding/s;
4 out_neon_w=(out_neon_w_end - out_neon_w_start)>>2
5 out_left_start=out_neon_w_start+out_neon_w<<2
6 out_left_w=Wo-out_left_start
7 for oc=0 toCo/4-1 in parallel do
8 for ic=0 toCi-1 do
9 fori=0 toWfdo
10 forj=0 toHfdo
11Ki,j=SIMD_Load(Foc,ic,i,j)
12 out_bias=(ic==0) ? Boc:0
13 relu0=((ic==Ci-1) &&relu_flag) ? 0:-max_float
14 for top=0 to out_neon_h_start-1 do
15 direct_padding(Iic, -left_padding, top×s-top_padding,Ooc,0,top,K,Wo,out_bias,relu0)
16 for top=out_neon_h_start to out_neon_h_end-1 do
17 direct_padding(Iic, -left_padding, top×s-top_padding,Ooc,0,top,K,output_neon_w_start,out_bias,relu0)
18 direct_padding(Iic,out_left_start×s-left_padding, top×s-top_padding, Ooc,out_left_start,top,K,out_left_w,out_bias,relu0)
19 for top=out_neon_h_start to (out_neon_h_end-1)/2 step 2 do
20 direct_simd(Iic,out_neon_w_start×s-left_padding, top×s-top_padding,Ooc,out_neon_w_start,top,K,out_neon_w, out_bias, relu0)
21 for top=out_neon_h_end toHo-1 do
22 direct_padding(Iic, -left_padding, top×s-top_padding,Ooc,0,top,K,Wo, out_bias, relu0)
23 Function direct_padding(IB,OB,K,ow,bias,relu0):
24 forw=0 to ow-1 do
25v=SIMD_fadd(SIMD_Load(OBw×4),bias)
26 fori=0 toWfdo
27 forj=0 toHfdo
28 if not in padding:
29 SIMD_fma_lane(v,Ki,j,IBw×s+i,j,i)
30v=SIMD_fmax(v,relu0)
31 SIMD_Store(OBw×4,v)
32 Function direct_simd(IB,OB,K,ow,bias, relu0):
33 forw=0 to ow -1 do
34 fork=0 to 3 do
35Vk=SIMD_fadd(SIMD_Load(OB(w×4+k)×4, 0), bias)
36Wk=SIMD_fadd(SIMD_Load(OB(w×4+k)×4, 1), bias)
37 fori=0 to Wfdo
38 forj=0 to Hfdo
39 fork=0 to 3 do
40 SIMD_fmla(Vk,Ki,j, IB(w×4+k) ×s+i, j)
41 SIMD_fmla(Wk,Ki,j, IB(w×4+k) ×s+i, j+s)
42 fork=0 to 3 do
43Vk=SIMD_fmax(Vk, relu0)
44Wk=SIMD_fmax(Wk, relu0)
45 SIMD_Store(OB(w×4+k)×4, 0,Vk)
46 SIMD_Store(OB(w×4+k)×4, 1,Wk)
9宮格分塊策略。為了避免對輸入數(shù)據(jù)進(jìn)行padding的擴(kuò)張,提出了一種9宮格的分塊策略,如圖3所示。9宮格的思想是建立一個虛擬的padding區(qū)域(虛線框),而實際的輸入數(shù)據(jù)尺度是不變的(實線框)。之所以這樣建立,是因為padding區(qū)域填充的元素都為0,而輸入數(shù)據(jù)的0乘以任何卷積核元素都為0。例如圖3中白底的卷積框所對應(yīng)的卷積結(jié)果=k1*i1+k2*i2+k3*i3+k4*i4+k5*i5+k6*i6+k7*i7+k8*i8+k9*i9。其中k1/k2/k3/k4/k7所在區(qū)域為padding,所有值全為0。因此只需要計算非padding區(qū)域即可,最后簡化為:k5*i5+k6*i6+k8*i8+k9*i9。這就是算法2中direct_padding函數(shù)所實現(xiàn)的功能。同理,算法只需對所有卷積核落在虛線框的padding區(qū)域中(即9宮格的外圈8格)統(tǒng)一調(diào)用direct_padding函數(shù)做特殊處理。那么剩下的中間input區(qū)域都是有效區(qū)域,卷積核的所有元素都可參與運算(如黑底的卷積框),就可以利用SIMD進(jìn)行加速處理。9宮格的分塊策略雖然增加了一些算法復(fù)雜度,但是節(jié)省了一次全量內(nèi)存拷貝,相比于昂貴的內(nèi)存開銷,所增加的邏輯處理耗時要小的多。但是這里有一點需要注意的地方是,對于padding區(qū)域地址的計算會產(chǎn)生內(nèi)存溢出,如算法2第15行對輸入數(shù)據(jù)的地址計算,它指向了外側(cè)虛線框的起始位置。這樣使用主要是為了地址計算的統(tǒng)一性,只要提前判斷,溢出的地址不訪問,就不會報錯(即便是內(nèi)存泄露工具檢測也不會報錯)。
圖3 卷積計算的padding策略
訪存合并。此外Add bias的處理也可以優(yōu)化到算法邏輯中,因為每一層的輸出數(shù)據(jù)都是所有輸入數(shù)據(jù)的全部channel層的卷積求和。因此,該文的做法是在每一次對輸入數(shù)據(jù)channel的遍歷都強(qiáng)制加上out_bias,只不過這里的bias只有在input channel第一層的時候才是真的bias參數(shù),其他層為0。這么做的目的是為了指令流水線不被if指令中斷,因為CPU的分支預(yù)測如果發(fā)生錯誤,就需要清空流水線,重新加載正確的分支,會產(chǎn)生Pipeline Bubbles,相對成本較高。而多加的一條SIMD_fadd指令最多只消耗4個時鐘周期,而且吞吐量為2條,即4個時鐘周期可以同時處理兩條fadd相加指令。最后Relu的處理也類似,是在input channel最后一層,因為最后一層的卷積結(jié)果與前面所有層相加之后才是最終輸出結(jié)果。因此,只有最后一層,relu0的值才為0,其他情況是float負(fù)數(shù)較大值,故SIMD_fmax只有relu=0時才會生效。SIMD_fmax指令延遲3個周期,吞吐量2條。
算法2中對內(nèi)存的所有操作包括:
(1)由于輸出數(shù)據(jù)和卷積核都采用了NHWC4格式(輸入數(shù)據(jù)仍為NCHW),每次可以利用SIMD同時處理4個channel,因此最外層循環(huán)次數(shù)降為OC/4,所以輸入數(shù)據(jù)讀取降到了OC/4*IC*IW*IH。
(2)9宮格分塊策略只是分開讀取,但讀取總量不變,因此輸出數(shù)據(jù)先讀取后寫入總量為2*OC/4*IC*OW*OH*4。
(3)卷積核共需OC/4*IC*3*3*4內(nèi)存讀取。
算法2的計算訪存比為:
[OC*IC*OW*OH*3(KW)*3(KH)*2]/
[(OC/4*IC*IW*IH+2*OC*IC*OW*OH+OC*IC*3*3)*sizeof(float)]
進(jìn)一步簡化為:
最終算法2的計算強(qiáng)度相當(dāng)于算法1提升了3倍,相當(dāng)于樹莓派4B的理論一半。加上NEON指令的加持,推理速度得到大幅改進(jìn)。
2.1.2 SIMD指令優(yōu)化
使用SIMD指令計算量不變,只是利用ARM NEON指令一次可以處理4個float的乘加操作[8],提升計算速度,所以Roofline模型中算力上限π是不變的。SIMD的優(yōu)化方法分為二部分:其一是padding區(qū)域的處理,這里使用了NEON Intrinsics函數(shù),每次處理1個元素的4個通道卷積結(jié)果(如direct_padding函數(shù))。其二是非padding區(qū)域的處理,這里使用了Asm Volatile內(nèi)聯(lián)匯編方式,每次處理8個元素的4個通道卷積結(jié)果(如direct_simd函數(shù))。
根據(jù)Cortex A72優(yōu)化手冊,LD1指令,一次可以讀取4個float,指令延時(Exec Latency)為5個時鐘周期,吞吐量(Execution Throughput)為1,每時鐘只能發(fā)射1條LD1指令。FMLA指令,一次針對4個float(Q-form)進(jìn)行乘加操作,指令延時為7個時鐘周期,每時鐘只能發(fā)射一條FMLA指令。但LD1和FMLA所使用的執(zhí)行端口(Utilized Pipelines)不同,這意味著對于數(shù)據(jù)無關(guān)的二條指令是可以并行執(zhí)行的,如圖4所示。
圖4 Cortex A72優(yōu)化手冊
根據(jù)A72優(yōu)化手冊[9],一條讀取指令LD1的耗時比FMLA少了2個時鐘周期,意味著一條FMLA指令就可以遮蓋LD1取數(shù)據(jù)的耗時,因此只要流水線設(shè)計合理,就不會使得LD指令變成瓶頸,運算器才不會因等待而浪費。鑒于FMLA指令的延遲為7周期,每周期吞吐量為1,因此最少需要7條無數(shù)據(jù)寫依賴關(guān)系(讀依賴不影響)的FMLA指令才能填滿流水線。根據(jù)上述結(jié)論,算法設(shè)計為每次迭代輸出2行,每行4個像素,共需要8條FMLA指令。一條FMLA指令可處理4個float,因此每次迭代共輸出32個卷積中間結(jié)果。圖5所示為stride=2的單次迭代卷積所有參數(shù)個數(shù)。
圖5 3×3普通卷積單次迭代示意圖
指令流水線設(shè)計。圖5所示的輸入數(shù)據(jù)格式是NCHW,卷積核Weight、Bias以及輸出數(shù)據(jù)格式為NHW4C。為了避免Reorder操作,一條FMLA指令計算方法是將輸入數(shù)據(jù)的1個元素分別與卷積核的1個元素4通道(同色塊)相乘,結(jié)果與輸出數(shù)據(jù)的1個元素4通道(同色塊)相加。同理分別對卷積核的9個元素遍歷運算,將每次運算結(jié)果產(chǎn)生的8*4個float輸出數(shù)據(jù)求和累加,就得到了單通道輸入圖像的卷積結(jié)果(算法2第37~41行)。其中8*4個float輸出數(shù)據(jù)是存放在neon寄存器中的,所以累加求和的操作并不會每次都要訪存。故將算法分成了9組,每組有連續(xù)的8條不存在寫依賴關(guān)系的FMLA指令,對輸入數(shù)據(jù)的LOAD指令可以穿插在每組中間,而卷積核的9條LD指令在調(diào)用函數(shù)前就加載到neon寄存器中了。按此設(shè)計就可以充分發(fā)揮處理器流水線作業(yè)。同理,遍歷輸入數(shù)據(jù)的所有3個通道,將所有輸出通道的卷積結(jié)果累加就完成了3×3普通卷積最終結(jié)果。這就是算法2中,函數(shù)direct_simd所實現(xiàn)的功能。
循環(huán)展開優(yōu)化。實際編寫代碼除函數(shù)內(nèi)第1行循環(huán)(第33行代碼)外,direct_simd函數(shù)中的其他for循環(huán)都是不存在的。算法2中這樣寫是為了避免占用過長的篇幅。實際上for循環(huán)屬于分支預(yù)測指令,如果預(yù)測失敗,同樣會導(dǎo)致清空流水線,重新加載正確的分支。因此,為了保證流水線不會產(chǎn)生氣泡,direct_simd里第34行到第46行代碼都是展開的,這種方法叫作循環(huán)展開優(yōu)化[10]。
Mobilenet中逐通道卷積尺寸通常為:輸入tensor為(1,C,IW,IH),輸出tensor為(1,C,OW,OH),卷積核tensor為(1,C,3,3)。所有tensor的通道數(shù)是一致的。因為卷積運算只關(guān)心當(dāng)前通道,因此不需要遍歷輸入層和卷積核的所有通道[11],所以相比于3×3普通卷積和逐點卷積訪存效率要高得多。未優(yōu)化版本如算法3所示。
算法3:逐通道卷積原始算法。
輸入:input tensorI,kernel filterF,paddingP;
輸出:output tensorO。
1 Mat IP=enlarge inputIwith paddingP
2O=add(O,bias)
3 forc=0 toC-1 do
4 forw=0 toWo-1 do
5 forh=0 toHo-1 do
6 fori=0 toWf-1 do
7 forj=0 toHf-1 do
8 Oc,w,h+= IPc,w+i,h+j×Fc,i,j
9O=max(0,f,O)
整體流程與算法1基本一致,只是中間少了一層對輸入層通道的遍歷。算法3中對內(nèi)存的所有操作包括:
(1)對輸入數(shù)據(jù)進(jìn)行padding擴(kuò)張約為2*C*IW*IH。
(2)Addbias:C*OW*OH。Relu操作:2*C*OW*OH。
(3)輸入數(shù)據(jù)讀取C*IW*IH,輸出數(shù)據(jù)讀寫2*C*OW*OH。
(4)卷積核共需C*3*3內(nèi)存讀取。
算法3的計算訪存比為:
[C*OW*OH*3(KW)*3(KH)*2]/[(3*C*
IW*IH+5*C*OW*OH+C*3*3)*sizeof (float)]
由stride =1,OW=IW,OH=IH,則進(jìn)一步簡化為:
同樣算法3,也是嚴(yán)重訪存瓶頸區(qū)域了,嚴(yán)重受限于訪存瓶頸的限制。遠(yuǎn)遠(yuǎn)無法達(dá)到樹莓派4B的理論上限3 FLOPs/Byte。
2.2.1 訪存優(yōu)化
與3×3普通卷積算法的優(yōu)化方法相同,減少所有不必要的訪存開銷。同樣采用9宮格分塊策略和訪存合并。因此,該文提出的逐通道卷積優(yōu)化方法,如算法4所示。
算法4:逐通道卷積優(yōu)化算法。
輸入:input tensorI,kernel filterF,biasB;
輸出:output tensorO。
1 Function depthwise_simd(IB,OB,K,ow,bias,relu0):
2 forw=0 to ow-1 do
3 fork=0 to 3 do
4Vk=SIMD_fadd(SIMD_Load(OB(w×4+k)×4,0), bias)
5Wk= SIMD_fadd(SIMD_Load(OB(w×4+k)×4, 1), bias)
6 fori=0 toWfdo
7 forj=0 toHfdo
8 fork=0 to 3 do
9 SIMD_fmla(Vk,Ki,j,SIMD_Load(IB(w×4+k)×4+i, j))
10 SIMD_fmla(Wk,Ki,j,SIMD_Load(IB(w×4+k)×4+i, j+1))
11 fork=0 to 3 do
12Vk=SIMD_fmax(Vk,relu0)
13Wk=SIMD_fmax(Wk,relu0)
14 SIMD_Store(OB(w×4+k)×4, 0,Vk)
15 SIMD_Store(OB(w×4+k)×4, 1,Wk)
Depthwise卷積對padding的處理同樣采用9宮格分塊策略,與算法2相同,這里就不重新列出了。
算法4中對內(nèi)存的所有操作包括:
(1)輸入數(shù)據(jù)讀取為C/4*IW*IH。
(2)輸出數(shù)據(jù)讀寫總量為2*C/4*OW*OH*4。
(3)卷積核共需C/4*3*3*4內(nèi)存讀取。
算法4的計算訪存比為:
[C*OW*OH*3(KW)*3(KH)*2]/[(C/4*
IW*IH+2*C*OW*OH+C*3*3)* sizeof(float)]
由stride =1,OW=IW,OH=IH,則進(jìn)一步簡化為:
最終算法4的強(qiáng)度接近于樹莓派4B的理論2/3。比優(yōu)化前提升了3.5倍。
2.2.2 SIMD指令優(yōu)化
指令流水線設(shè)計。與3×3普通卷積需要遍歷二層(OC/4、IC)通道相比,逐通道卷積只需遍歷一層(C/4)通道。另外,由于3×3普通卷積每次只能處理1個輸入元素的1通道,而NHW4C格式的輸入數(shù)據(jù)每次卷積運算就可以處理1個輸入元素的4通道??梢妰H靠NHW4C格式,就能將處理能力提升4倍。首先,為保證最少7條無數(shù)據(jù)寫依賴關(guān)系的FMLA指令來填滿流水線,算法設(shè)計為每次迭代輸出2行,每行4個像素,共需要8條FMLA指令。其次,因卷積核大小為3*3,需要分別對卷積核的9元素運算,故分成了9組,每組有連續(xù)的8條FMLA執(zhí)令,LD指令可以穿插在每組中間。每次迭代共輸出32個卷積最終結(jié)果,如圖6所示。
圖6 逐通道卷積單次迭代示意圖
循環(huán)展開優(yōu)化。同樣為了避免占用過長的篇幅,實際編寫代碼除函數(shù)內(nèi)第1行循環(huán)(第3行代碼)外,depthwise_simd函數(shù)中的其他for循環(huán)都是不存在的。采用循環(huán)展開優(yōu)化技術(shù),從而避免了分支預(yù)測指令和流水線產(chǎn)生氣泡。
Mobilenet中逐點卷積尺寸通常為:輸入tensor為(1,IC,IW,IH),輸出tensor為(1,OC,OW,OH),卷積核tensor為(OC,IC,1,1)。未優(yōu)化版本如算法5所示。
算法5:逐點卷積原始算法。
輸入:input tensorI,kernel filterF;
輸出:output tensorO。
1O=add(O,bias)
2 forc=0 toCo-1 do
3 forw=0 toWo-1 do
4 forh=0 toHo-1 do
5 fork=0 toCi-1 do
6Oc,w,h+=IPk,w,h×Fc,k
7O=max(0.f,O)
算法5中對內(nèi)存的所有操作包括:
(1)Add bias:OC*OW*OH。Relu操作:2*OC*OW*OH。
(2)輸入讀取OC*IC*IW*IH,輸出讀寫2*OC*IC*OW*OH。
(3)卷積核共需OC*IC*1*1內(nèi)存讀取。
算法5的計算訪存比為:
[OC*IC*OW*OH*1(KW)*1(KH)*2]/
[(OC*IC*IW*IH+2*(1+IC)*OC*OW*OH+OC*IC*1*1)*sizeof(float)]
由padding=0,stride =1,OW=IW,OH=IH,IC通常在8~256之間,則進(jìn)一步簡化為:
逐點卷積的計算強(qiáng)度為樹莓派4B的理論的5.6%。
2.3.1 訪存優(yōu)化
對于1×1卷積,TensorFlow Lite[12]的做法是轉(zhuǎn)換成二個矩陣的乘法[13-14],然后使用OpenBLAS高性能矩陣乘法庫[15]來完成卷積運算。而該文由于采用NHW4C格式,轉(zhuǎn)換成兩個矩陣乘法的格式內(nèi)存開銷較大。因此,逐點卷積優(yōu)化方法與之前相同,減少所有不必要的訪存開銷。優(yōu)化如算法6所示。
算法6:逐點卷積優(yōu)化算法。
輸入:input tensorI,kernel filterF,biasB;
輸出:output tensorO。
1 for oc=0 toCo/4-1 in parallel do
2 for ic=0 toCi/4-1 do
3 out_bias=(ic==0) ?Boc: 0
4 relu0=((ic==Ci-1) &&relu_flag) ? 0:-max_float
5 forn=0 to 3 do
6Kn=SIMD_Load(Foc,ic,n*4)
7 forw=0 to (Wo*Ho)/8-1 do
8 fork=0 to 7 do
9Vk=SIMD_fadd(SIMD_Load(Ooc,(w×8+k)×4), bias)
10Xk=SIMD_Load(Iic,(w×8+k)×4)
11 forn=0 to 3 do
12 fork=0 to 7 do
13 SIMD_fmla(Vk,Kn,Xk[n])
14 fork=0 to 7 do
15Vk=SIMD_fmax(Vk, relu0)
16 SIMD_Store(Ooc,(w×8+k)×4,Vk)
逐點卷積沒有padding參數(shù),因此不需要9宮格分塊策略。算法6中對內(nèi)存的所有操作包括:
(1)輸入數(shù)據(jù)讀取為OC/4*IC/4*IW*IH*4。
(2)輸出數(shù)據(jù)讀寫總量為2* OC/4*IC/4*OW*OH*4。
(3)卷積核共需OC/4*IC/4*1*1*4*4內(nèi)存讀取。
算法6的計算訪存比為:
[OC*IC*OW*OH*1(KW)*1(KH)*2]/
[(OC/4*IC*IW*IH+OC/2*IC*OW*OH+OC*IC*1*1)*sizeof(float)]
由stride =1,OW=IW,OH=IH則進(jìn)一步簡化為:
優(yōu)化后的計算強(qiáng)度也僅為理論的22%,與逐通道卷積相差很大。從最終實驗也知,逐點卷積效率是三者中最差的。
2.3.2 SIMD指令優(yōu)化
由于逐點卷積的卷積核大小是1*1的,卷積操作每次只需要一個輸入元素。所以輸入/輸出Tensor由寬和高組成的二維數(shù)組可以看成連續(xù)排列的一維數(shù)組,且輸入和輸出數(shù)據(jù)的一維數(shù)組長度相等,同時輸入/輸出的格式都是NHW4C。逐點卷積的設(shè)計原則依然是組成8條FMLA指令流水線,因此算法6中單次迭代(第7~16行)可以對8個輸出變量進(jìn)行1×1卷積運算,如圖7所示。
圖7 逐點卷積單次迭代示意圖
指令流水線設(shè)計。圖7所示逐點卷積所有參數(shù)的例子,圖的左側(cè)是輸入Tensor(1,8,W,H),中間卷積核Tensor (16,8,1,1),右側(cè)輸出Tensor(1,16,W,H)。其中輸入數(shù)據(jù)和輸出數(shù)據(jù)可以看成一維數(shù)組,長度為W*H,NHW4C格式由4個連續(xù)通道數(shù)據(jù)排列在一起,因此輸入Tensor分成了上下兩組(8/4),輸出Tensor分成了四組(16/4)。卷積核大小為16*8個,同樣分成了4組,每組中又分成2組(與輸入數(shù)據(jù)通道數(shù)相對應(yīng))。算法的設(shè)計思想是,對于每一組的輸出數(shù)據(jù)是所有組的輸入數(shù)據(jù)卷積后累加求和而得(第2~16行代碼)。為求得一個輸出元素的4通道數(shù)據(jù)U0,需要對每組的輸入數(shù)據(jù)分別用每一通道與1個卷積核4通道相乘,產(chǎn)生的4條FMLA指令結(jié)果累加而成。如:①U0=k1*V0[0]②U0+=k2*V0[1]③U0+=k3*V0[2]④U0+=k4*V0[3]。由于這4條FMLA指令存在寫依賴關(guān)系,因此同時處理8個輸出元素U0~U7,將上述4步的每一步分散在8個輸出元素中,從而組成8條無寫依賴的FMLA指令(第11~13行)。對于不滿8條的剩余輸入/輸出數(shù)據(jù),可以利用NEON Intrinsics函數(shù),采用同樣方法,每次可處理1個元素的4通道卷積。
循環(huán)展開優(yōu)化。同樣代碼中第5~14行的所有for循環(huán)需要展開處理,與前面方法相同不再重復(fù)。
測試平臺為:樹莓派4B 4 GB內(nèi)存,Raspberry Pi OS 64位系統(tǒng),Retinaface mnet 0.25模型。該文提出的三種算法分別與國產(chǎn)開源框架騰訊ncnn、商湯ppl.nn及阿里mnn做對比。分別測試三種不同分辨率圖片在不同平臺耗時。測試方法使用源碼Release編譯,開啟OpenMP及O2編譯優(yōu)化選項,并在源碼中添加耗時打印語句。注:這里統(tǒng)計的耗時不包含圖優(yōu)化部分,只有每一層的算子推理耗時。具體如表1~表3所示。
表1 3×3普通卷積耗時分析 ms
表2 逐通道卷積耗時分析 ms
表3 逐點卷積耗時分析 ms
實驗結(jié)果表明,針對普通卷積和逐通道卷積,由于ncnn框架需要提前做padding操作和單獨的Add bias及relu操作,而該文提出的9宮格分塊策略和訪存合并優(yōu)化避免了無畏的浪費,從而大大減少了訪存時間,從一定程度上減少了卷積的推理耗時。在普通卷積實驗中,ppl.nn之所以是幾種平臺中最差的,是因為需要提前將NCHW轉(zhuǎn)為NHW4C之后才做卷積運算,而ncnn雖然不用提前做reorder轉(zhuǎn)換,但多余的3次全量內(nèi)存訪問也嚴(yán)重影響耗時。根據(jù)2.1節(jié)分析,算法受限于訪存瓶頸,因此文中算法采用直接計算(同時計算1通道輸入數(shù)據(jù)和4通道輸出數(shù)據(jù)),雖然無法發(fā)揮全部算力,但相比一次昂貴的reorder開銷,實驗表明推理耗時可以減少50%以上。
由于逐通道卷積的計算量較小,因此訪存的增加反而變成最大的瓶頸,所以對于ncnn和mnn沒有做訪存優(yōu)化(采用了單獨的padding、Add bias、relu操作)。mnn雖然采用三分塊(上中下)策略,對于padding策略依然使用了額外的內(nèi)存布局轉(zhuǎn)換,同時代碼處理邏輯也是過于復(fù)雜,影響指令流水線,最后由于mnn采用NEON Intrinsics指令,相比Asm Volatile內(nèi)聯(lián)匯編方式要慢10%左右,而導(dǎo)致結(jié)果為所有平臺中測試結(jié)果最差。而該文提出的9宮格算法和訪存計算合并方法,對于輸入數(shù)據(jù)、卷積核和輸出數(shù)據(jù),只需要從頭遍歷一遍即可完成卷積計算。相比于其他平臺減少了至少2次以上的全量訪存操作。同時又能完全發(fā)揮neon指令算力(同時計算4通道輸入數(shù)據(jù)和4通道輸出數(shù)據(jù)),最終算法的計算訪存比達(dá)到為理論峰值的2/3。多種因素疊加使得優(yōu)化高于其他平臺1倍以上。且ppl.nn和mnn的代碼邏輯過于復(fù)雜,過多的if語句也會導(dǎo)致流水線得不到充分發(fā)揮,指令預(yù)處理失敗率也要高出很多。而該文從ARM A72 CPU指令性能進(jìn)行分析,設(shè)計了一組能充分利用CPU流水線的指令序列,可以在同等計算量下最大化發(fā)揮處理器的算力,同時在減少分支預(yù)測和循環(huán)展開技術(shù)的加持下,最終提出的普通卷積和逐通道卷積算法耗時相比其他平臺提升幅度高達(dá)75%~367%。
逐點卷積由于不需要padding操作,但ncnn依然存在多余的Add bias和relu單獨操作。mnn采用了與該文類似的計算訪存合并算法,因此也證實了計算訪存合并方法有利地加速推理。由表3可見,與同分辨率下的逐通道卷積耗時相比也大了5~6倍。原因有幾方面,首先,逐點卷積的計算訪存比非常低,優(yōu)化后的算法也只能達(dá)到理論的22%,相比于逐通道卷積達(dá)到理論的67%,大部分都浪費在訪存上,算力得不到充分利用。其次,在相同分辨率下逐點卷積數(shù)據(jù)量要比逐通道卷積數(shù)據(jù)量多了50%,導(dǎo)致計算量和訪存量都相應(yīng)增加。最終相比其他平臺提升幅度僅為9%~59%。
由于篇幅限制,僅對Retinaface典型的3種卷積算法進(jìn)行針對性優(yōu)化。至少在樹莓派4B這一設(shè)備上,相比于國內(nèi)成熟的開源框架ncnn提升了24%~367%。即使與商湯最新開源的ppl.nn框架相比,也有9%到143%不同幅度的領(lǐng)先。
針對樹莓派4B設(shè)備,基于Roofline模型進(jìn)行定量分析,提出了針對性的優(yōu)化方法。從計算圖優(yōu)化,合并算子,NHW4C格式轉(zhuǎn)換,到9宮格分塊策略,SIMD加速方法等所有策略都是為了優(yōu)化推理耗時。實驗結(jié)果表明,在推理耗時上比國內(nèi)大廠的開源框架ncnn和ppl.nn都有較大幅度的提升。限于篇幅原因,并未對卷積的其他類型進(jìn)行分析,如dilation>1的空洞卷積。另外對于通道數(shù)比較深的卷積方法使用winograd方法[16]速度會更有優(yōu)勢。