吳 煥,吳俊敏
(中國科學技術大學 計算機科學與技術學院,安徽 合肥 230000)
過去幾年,ILSVRC(ImageNet large scale visual re-cognition competition)[1]中陸續涌現出一批經典的網絡結構[2-7]。這些模型往往都具備預測精度高、計算量大以及內存占用率高等特點。為解決卷積神經網絡的硬件需求和計算資源不匹配的問題,本文基于深度學習框架Caffe[8],對卷積操作提出了一種新的加速策略。目前,一種主流的加速方法是使用GPU通用計算。GPU憑借其高效的并行計算能力,能夠快速處理大量數據。這在很大程度上緩解了神經網絡計算量過大的問題,也促使了單機多卡、多機多卡的技術革新。然而,并不是所有硬件都支持GPU通用計算,比如arm等嵌入式設備以及Macbook等個人電腦。因此,如何在不支持GPU通用計算的硬件設備上加速卷積神經網絡的前向推理是目前急需解決的問題。有研究結果表明[9],卷積層是卷積神經網絡中耗時比例最高的部分,優化卷積操作可以在很大程度上提升神經網絡的整體性能。Caffe卷積包含兩個主要操作,一是im2col,二是gemm。本文通過增強這兩個操作的訪存連續性,從而加速卷積神經網絡的前向推理速度。
隨著深度學習技術不斷發展,加速卷積神經網絡的需求與日俱增。過去幾年陸續出現各種加速策略,總的來說有如下幾種。根據卷積定律,時域中的卷積操作等效于頻域中逐點相乘的復數乘法。該定律不僅適用于一維信號,對二維圖像同樣有效。為此,研究人員[10,11]提出用快速傅立葉變換(FFT)實現二維圖像的卷積操作。理論上,卷積核尺寸越大,加速比就越加明顯。然而,目前主流的卷積神經網絡均采用小尺寸卷積核,因此無法保證該方法總是優于傳統的實現方式。此外,在轉換到頻域之前,FFT卷積還要求對輸入圖像以及卷積核同時補零,進一步增大了內存開銷。
卷積神經網絡存在大量冗余[12]。不僅在層與層之間存在冗余連接[13],模型參數的比特位也存在冗余[14]。出于訓練目的,傳統實現通常采用單精度浮點數。而在推理階段,可以將其替換成低精度定點數。有研究結果表明,在前向推理階段將浮點數量化成定點數不會影響模型的預測準確率。結合單指令多數據指令集(X86上SSE4或者ARM上Neon),還可以同時處理多個8位定點。研究人員在此基礎上進一步提出了二元權重模型[15-17]。在專用硬件的支持下,卷積中的乘加運算全都可以用加法實現。
在大型網絡模型中,卷積層通常有上百個卷積核。雖然權值共享的特性已經在很大程度上減少了模型參數,但是網絡尺寸依然很大。有實驗結果表明[18,19],在不影響模型準確率的前提下,全秩卷積核可以分解成一系列基本的卷積核。模型參數不僅更少,卷積速度也更快。在此基礎上,還有針對非線性單元和深層網絡模型的相關工作[20]。
上述工作從各個角度提出了不同的加速策略。比如,快速傅立葉變換卷積、定點模型參數以及卷積核的低秩分解。與之不同的是,本文從訪存連續性來加速卷積操作。
卷積操作本質上是求輸入和卷積核的點積。簡單來說,就是將卷積核從左到右,從上到下以一定的步幅滑動。每滑動一次,卷積核與所在位置的圖像塊做點對點的乘加運算。由于就地實現卷積的速度很慢,Caffe卷積以矩陣乘法的形式實現。結合MKL、OpenBLAS等高效的基本線性代數庫,運算速度能夠大幅提升。Caffe卷積包括兩個主要的操作,一是im2col,二是gemm。Im2col全稱為image to columns,負責將圖像塊展開成列向量。Gemm(general matrix-matrix multiplication)負責矩陣之間的乘法運算。
為了更直觀地介紹Caffe卷積的工作原理,圖1展示了一個簡單示例。Memory一行展示了輸入圖像和卷積核在內存中的存儲情況,Convolution一行是卷積操作的宏觀表示,CaffeConvolution一行則是卷積在Caffe中的具體實現。數據塊尺寸通常表示成[n,c,h,w]的四維形式,n表示輸入或卷積核的數量,c表示通道數,h表示高度,w表示寬度。圖1所涉及的數據塊尺寸如下:輸入(input): [1, 2, 3, 3];卷積核(kernel): [2, 2, 2, 2];輸出(result): [1, 2, 2, 2];補零(pad): [0, 0];步幅(stride): [1, 1]。在im2col的作用下,輸入(input)展開成矩陣data_col。每個圖像塊對應一個列向量,其中第一列(i1,i2,i4,i5,i10,i11,i13,i14)是第一個圖像塊,第二列(i2,i3,i5,i6,i11,i12,i14,i15)是第二個圖像塊,并以此類推。接著,以kernel和data_col為實參,調用矩陣乘法函數gemm,得到卷積結果(result)。至此,一次卷積操作完成。
通過轉置操作改變輸入圖像的數據排列,可以同時提高im2col和gemm的訪存效率。如圖2所示,在輸入圖像(input)中,每個通道既可以表示成二維形式,也可以表示成一維的行向量。若以二維形式表示,數據按照寬度、高度、通道的順序存儲,(i1,i2, …,i9)是第一個通道,(i10,i11, …,i18)是第二個通道。Im2col操作負責將所有圖像塊展開,其中(i1,i2,i4,i5,i10,i11,i13,i14)是第一個圖像塊。在轉置之前,要將第一個圖像塊展開,每次只能連續拷貝兩個元素,即(i1,i2), (i4,i5), (i10,i11), (i13,i14)。而轉置之后,每次就可以連續拷貝4個元素,即(i1,i10,i2,i11),(i4,i13,i5,i14)。輸入圖像往往有幾十甚至上百個通道,因此,對于轉置后的輸入(transposedinput)而言,每次就可以連續拷貝上百個數據,即(i1,i10, …,i2,i11, …),(i4,i13, …,i5,i14, …),而轉置前的輸入(input)每次依然只能連續拷貝兩個元素。除此之外,轉置輸入圖像還順帶提升了gemm的訪存效率。由圖1可知,im2col后是卷積核矩陣(kernel)和展開結果(data_col)的矩陣乘法,也就是每個卷積核分別與每個圖像塊做點對點的乘加運算。在圖1的data_col中,讀取一個圖像塊相當于讀取一個列向量。而在圖2的data_col中,讀取一個圖像塊相當于讀取一個行向量。在以行優先存儲的體系結構中,行向量的讀取效率更高,因此矩陣乘法的執行速度也更快。
算法1是Caffe im2col的具體實現,下面結合圖1進行解釋。第1行對應data_col的行數,第2行和第3行負責處理data_col的一行。根據for循環提供的索引,可以計算data_col目前所在位置的偏移dst_offset,并反推出input的偏移src_offset。若dst_address落在補零區(某些卷積層要求在輸入圖像周圍補零,本文將這塊區域稱為補零區),給目標地址dst_address賦0。否則,將源地址src_address的數據拷貝到目標地址dst_address。最終,輸入(input)被展開成矩陣data_col,每個圖像塊對應data_col中的一個列向量。
算法1:Caffe im2col
輸入:輸入圖像(input)以及卷積操作需要的各種超

圖1 Caffe卷積的實現細節

圖2 改進后的im2col
參,比如補零(pad)、步幅(stride)等
輸出:輸入圖像的展開結果(data_col)
(1)for卷積核的長度do
(2)for卷積輸出中單個通道的高度do
(3)for卷積輸出中單個通道的寬度do
(4) 根據索引計算dst_offset和src_offset
(5) src_address = base_address(input) + src_offset
(6) dst_address = base_address(data_col) + dst_offset
(7)if((不需要為輸入圖像補零)or(需要補零and當前位置在非補零區))then
(8) data_col[dst_address]=input[src_address]
(9)else
(10) data_col [dst_address]=0
算法2是算法1的改進版本,下面結合圖2進行解釋。與算法1相比,算法2的輸入多了一塊事先開辟的空間transposedinput,這塊空間用來存儲轉置后的輸入。因此,空間開銷由原先的n*c*h*w變為2*n*c*h*w。值得注意的是,這塊空間一旦分配完畢就可以一直使用,因此其開銷不計入測量時間內。根據3.1小節,首先轉置input并存入transposedinput。接著將data_col中的所有元素置0,這么做可以去除循環中的條件分支語句。最外面兩層循環對應data_col的行數,最內層循環負責處理data_col的一行。根據for循環提供的索引,可以計算data_col的偏移dst_offset,并反推transposed_input中的偏移src_offset。之后,便如圖2所示,批量拷貝源地址src_address處的數據到dst_address中。每次批量拷貝的數據量為kernel.cols*kernel.channels,即(i1,i10, …,i2,i11, …),(i4,i13, …,i5,i14, …)。算法2不僅能去除for循環中的條件分支語句,更重要的是通過轉置提升了im2col和gemm的訪存效率。
算法2: Optimized im2col
輸入: 輸入圖像(input)以及卷積操作需要的各種超參,比如補零(pad)、步幅(stride)等。并事先開辟一塊空間(transposed input),用以存儲轉置后的輸入
輸出:輸入圖像的展開結果(data_col)
(1) transposed_input = transpose (input)
(2) memset (data_col, 0)
(3)for卷積輸出中單個通道的高度do
(4)for卷積輸出中單個通道的寬度do
(5)for卷積核的高度do
(6) 根據索引計算dst_offset和src_offset
(7) src_address = base_address (transposed_input) + src_offset
(8) dst_address = base_address (data_col) + dst_offset
(9) memcpy (dst_address, src_address, kernel.cols * kernel.channels)
本節總共設置了3個實驗,測試環境是Intel Core i5,OS X EI Capitan 10.11.4,BLAS庫是Intel的MKL。默認情況下,Caffe采用gcc的-o2編譯開關。為了更充分地利用編譯器,改進前后的對比實驗均采用-o3選項。第一個實驗測試im2col、gemm以及總的卷積耗時。相比于im2col,gemm的計算時間更長,優化效果也更大。第二個實驗測試通道數和輸入圖像尺寸對性能的影響。實驗結果表明,卷積加速比最高可超過100%,平均加速比在40%左右。最后一個實驗測試卷積核尺寸對性能的影響。
本實驗總共測試7種不同的通道數,測試內容包括im2col,gemm以及卷積各自的計算時長。出于制表目的,所有結果均四舍五入到最近的整數。如表1所示,相比于im2col,gemm的計算時間更長,優化效果也更加明顯。容易看出,矩陣乘是Caffe卷積中非常耗時的一個操作,可以從兩個方向進行優化。一是具體實現,二是訪存效率。優化矩陣乘法的實現可以考慮使用x86上的SSE4或者ARM上的NEON。這些指令又稱為SIMD指令,即一條指令可以同時處理多個數據。由于Caffe直接調用BLAS庫,因此這部分優化工作由BLAS庫的提供商負責。再者就是提升矩陣乘的訪存效率,也正是本文所做的工作。若圖像塊以列向量的方式存儲(如圖1所示),每讀取一個圖像塊都需要跨行讀取,訪存效率很低。而若以行向量的方式存儲(如圖2所示),就能連續讀取所有圖像塊,大大提升了矩陣乘法的執行效率。
卷積層之間的輸入尺寸和通道數往往不盡相同,為測試優化方法在不同卷積層上的加速效果,本實驗測試了4種輸入尺寸以及7種通道數。實驗所用的配置如下:輸入(input): [10,c,h,w];卷積核(kernel): [64,c, 3, 3];補零(pad): [1,1];步幅:(stride): [1,1]。沒有固定的字母代表實驗變量,其中c為通道數,h為高度,w為寬度。如圖3和表2所示,128×128的加速效果最為明顯,平均加速比高達93.18%。對于其它3種尺寸,加速比均在40%上下浮動。
除了輸入圖像,卷積層中的卷積核也不大相同,最常用的是1×1和3×3兩種卷積核。出于完整性考慮,本實驗加入了其它幾種不常用的尺寸,總共測試6種卷積核。

表1 im2col,gemm以及卷積耗時

圖3 不同輸入尺寸和通道數的加速比

Input SizeAverage Speedup32×3250.00%64×6443.51%128×12893.18%224×22430.64%
需要注意兩點,一是由于Caffe在處理1×1卷積核時沒有用到im2col,因此本實驗不測試這個尺寸。二是當輸入圖像的尺寸過小時,卷積時間很短,因此在4.3節沒有測試本實驗使用的幾種輸入尺寸,而是在本節直接給出運算時間,時間單位是微秒。如表3所示,虛線兩邊分別是改進前后的卷積時長。不論是哪種尺寸的卷積核,卷積速度均有不同程度的提升。

表3 不同卷積核的加速效果
為了加速卷積神經網絡的前向推理速度,本文基于深度學習框架Caffe,對卷積操作做了兩點優化。首先是優化im2col的拷貝速度。在展開輸入圖像的過程中,Caffe im2col每次都只將輸入(input)的一個元素拷貝到data_col中。經過優化,每次就可以連續拷貝上百個元素。接著是矩陣乘法的訪存連續性。在Caffe的原始實現中,每讀取一個圖像塊都相當于讀取一個列向量,訪存效率很低。經過優化,可以以行向量的形式連續讀取所有圖像塊,這在很大程度上提升了矩陣乘法的執行效率。實驗結果表明,應用這兩個優化點,卷積操作的平均加速比在40%左右。