作者簡介:Vlad Shimanskiy是Qualcomm公司GPU計算解決方案團隊的高級工程師。他一直致力于開發(fā)和原型設(shè)計Snapdragon上OpenCL 2.x新的標準特性,改進Adreno GPU架構(gòu),用于計算和加速重要線性代數(shù)算法,包括GPU上的矩陣乘法。
由于近來依賴于卷積的深度學(xué)習(xí)引起廣泛關(guān)注,矩陣乘法(MM)運算也在GPU上變得流行起來。我們也收到開發(fā)人員的反饋,希望利用配備Adreno?GPU的Qualcomm?Snapdragon?處理器加速深度學(xué)習(xí)(DL)應(yīng)用。
本文由我們Adreno工程師Vladislav Shimanskiy撰寫,分為兩個部分。本篇文章中的概念和下一篇文章中的OpenCL代碼清單,表示Adreno 4xx和5xx GPU系列設(shè)備端矩陣乘法內(nèi)核函數(shù)和主機端參考代碼的優(yōu)化實現(xiàn)。我們希望本系列文章將幫助和鼓勵您使用這些想法和代碼示例寫出自己的OpenCL代碼。
像Adreno GPU這樣的并行計算處理器是加速線性代數(shù)運算的理想選擇。然而,MM算法在密集并行問題中具有其獨特性,因為它需要在各個計算工作項之間共享大量的數(shù)據(jù)。在要相乘的矩陣中,例如A和B,每個元素對結(jié)果矩陣C的不同分量貢獻多次。因此,為Adreno優(yōu)化MM算法需要我們利用GPU內(nèi)存子系統(tǒng)。
關(guān)于GPU 上的矩陣乘法存在哪些困難?
當(dāng)我們嘗試在GPU上加速MM時,上面提到的數(shù)據(jù)共享問題又可以拆分為幾個相關(guān)問題:
- MM對相同的值進行重復(fù)運算,但是矩陣越大,越有可能必須到內(nèi)存中讀?。ň徛┮延兄堤鎿Q緩存中的值,這樣做效率低下。
- 在MM的簡單實現(xiàn)中,很自然的將標量矩陣元素映射到單獨的工作項。但是,讀寫標量的效率很低,因為GPU上的存儲器子系統(tǒng)和算術(shù)邏輯單元(ALU)被優(yōu)化用于向量運算。
- 同時加載大矩陣A和B的元素有可能導(dǎo)致緩存沖突和存儲器總線爭用的風(fēng)險。
- 內(nèi)存復(fù)制很慢,因此我們需要找到一個更好的方法,使數(shù)據(jù)對CPU和GPU同時可見。
這些問題使MM的主要任務(wù)復(fù)雜化,即多次讀取相同的值并共享數(shù)據(jù)。
矩陣乘法的OpenCL 優(yōu)化技術(shù)
我們詳細說明了一個OpenCL實現(xiàn),其中包括解決每個問題的技術(shù)。
1. 平鋪(Tiling)
第一個眾所周知的問題是將從內(nèi)存(比如高級緩層或DDR)中重復(fù)緩慢讀取相同矩陣元素的次數(shù)降到最低。我們必須嘗試對內(nèi)存訪問(讀取和寫入)進行分組,以使它們在地址空間彼此接近。
我們改進數(shù)據(jù)重用的技術(shù)是將輸入和輸出矩陣拆分為稱為tile的子矩陣。然后,我們強制執(zhí)行內(nèi)存運算指令,使得矩陣乘法得到的點積在整個tile中部分完成,之后我們將讀取指針移動到tile邊界之外。
我們的算法確認兩個層次的平鋪:micro-tile和macro-tile。下圖表示如何映射矩陣,使矩陣A中的分量乘以矩陣B中的分量,得到矩陣C中的單點積:
圖1:平鋪
micro-tile——{dx,dy}是矩陣內(nèi)的矩形區(qū)域,由內(nèi)核函數(shù)單個工作項處理。每個工作項是SIMD子組中的單線程,反過來又形成OpenCL工作組。通常,micro-tile擁有4×8 = 32個分量,稱之為像素(pixel)。
macro-tile——{wg_size_x,wg_size_y},通常是由一個或多個micro-tile組成并且對應(yīng)于工作組的更大矩形區(qū)域。在工作組中,我們完全在macro-tile范圍內(nèi)運算。
要計算矩陣C中的4×8micro-tile,我們將重點放在矩陣A和B中分別擁有4×8和4×4大小的區(qū)域。我們從pos = 0開始,計算部分結(jié)果或點積,并將其存儲在該micro-tile臨時緩沖區(qū)。同時,相同macro-tile中的其他工作項使用從矩陣A或矩陣B加載的相同數(shù)據(jù)并行計算部分結(jié)果。矩陣A行中所有數(shù)據(jù)被共享。同樣,矩陣B的列中所有數(shù)據(jù)在同一列的工作項之間共享。
我們計算macro-tile中的所有micro-tile的部分結(jié)果,然后在A中水平地增加pos,同時在B中垂直地增加pos。通過進行針對tile的計算并使pos逐漸遞增,我們可以最大程度地重復(fù)利用緩存中的已有數(shù)據(jù)。micro-tile繼續(xù)積累或卷積部分結(jié)果,將其增加到點積。
所以,在macro-tile內(nèi)的所有位置完成所有的部分計算后,我們才移動位置。我們可以完成整個micro-tile,從左到右和從上到下移動pos,然后前進,但是這樣做效率不高,因為我們需要的相同數(shù)據(jù)已經(jīng)被緩存清除。關(guān)鍵是我們在一個由工作組限制的區(qū)域工作,有若干工作項目在同時運行。此方法保證來自并行工作項的所有內(nèi)存請求均在有邊界的地址區(qū)域內(nèi)發(fā)出。
平鋪(Tiling)通過專注于內(nèi)存中的特定區(qū)域(工作組)來優(yōu)化運算,這樣,我們可以以緩存友好的方式進行工作。與跨越大塊內(nèi)存、必須到DDR中讀取不再存于緩存中的值相比,效率得到了極大的提升。
2. 矢量化
由于內(nèi)存子系統(tǒng)在硬件層面為矢量運算進行過優(yōu)化,所以最好使用數(shù)據(jù)向量而不是標量來運算,并且使每個工作項處理一個micro-tile和一個全矢量。因此,我們可以使用每次向量讀取操作時獲得的所有值。
例如,在32位浮點矩陣的情況下,我們的內(nèi)核函數(shù)使用float4類型的矢量,而不僅僅是一個浮點類型。這樣,如果我們想從矩陣中讀取一些東西,我們不僅讀取矩陣的單個浮點分量,而且讀取整個數(shù)據(jù)塊。這一點很重要,因為它同總線設(shè)計方式是一致的。因此我們從矩陣中讀取4個元素的分量,并使內(nèi)存帶寬飽和。相應(yīng)地,micro-tile 的大小均為4的倍數(shù)。
如果我們在CPU上工作,我們可能一次讀取一個2-D數(shù)組一個標量元素,但GPU上的OpenCL提供了更好的方法。為使讀寫更加高效,我們使用數(shù)據(jù)類型float4或float4的倍數(shù)變量進行操作。
3. 紋理管道( Texture Pipe)
兩個矩陣使用獨立緩存(L2 direct和Texture Pipe / L1),如下圖所示,允許我們避免大多數(shù)爭用和并行讀取操作,以便矩陣A和矩陣B的數(shù)據(jù)在同一時間得到加載。涉及L1有助于大大減少到L2的讀取流量。
圖2:紋理管道(Texture Pipe)
Adreno和許多其他GPU一樣,每個計算單元具??有到紋理管道(TP)單元的獨立連接。TP具有其自己的L1緩存,并獨立連接到L2緩存。
我們增加帶寬的技巧是通過TP加載一個矩陣,通過直接加載/存儲管道加載另一個矩陣。因為我們在矩陣乘法中重用了這么多的分量,所以我們還獲得了L1緩存的優(yōu)勢。最終,從TP/L1到計算單元的流量遠高于從L2到L1的流量。該區(qū)塊顯著降低了流量。如果不利用TP,只是連接到L2,就不會有太大幫助,因為在兩個總線之間有很多爭用和仲裁。
結(jié)果導(dǎo)致直接連接上產(chǎn)生大量流量,而從TP/L1到L2流量卻很少。這有助于我們增加總內(nèi)存帶寬,平衡ALU運算,實現(xiàn)更高的性能。我們等待數(shù)據(jù)從緩存返回的時間幾乎和ALU運算相同,我們可以對其采用管道化方式,使它們不致成為瓶頸。
4. 內(nèi)存復(fù)制預(yù)防
我們的OpenCL實現(xiàn)有兩個部分:運行在GPU上的內(nèi)核函數(shù)和運行在CPU上的主機代碼,并由主機代碼控制內(nèi)核函數(shù)的執(zhí)行。如果我們實現(xiàn)一個GPU加速庫(如BLAS)來做矩陣乘法,那么輸入矩陣將在CPU虛擬內(nèi)存空間,并且乘法結(jié)果也必須在CPU內(nèi)存中可用。為了加速GPU上的矩陣乘法,矩陣必須首先被傳輸?shù)紾PU內(nèi)存。
傳統(tǒng)方法是將矩陣復(fù)制到GPU地址空間,讓GPU執(zhí)行其計算,然后再將結(jié)果復(fù)制回CPU。但是,復(fù)制大矩陣所需的時間可能抵得上在GPU上總的計算時間,因此,我們希望避免使用低效率的CPU內(nèi)存復(fù)制。Adreno GPU具有共享Snapdragon處理器內(nèi)存硬件的優(yōu)勢,我們可以加以利用,而不是顯式復(fù)制內(nèi)存。
那么,為什么不簡單地分配在CPU和GPU之間自動共享的內(nèi)存?可惜,這樣并不可行,因為我們需要解決諸如對齊等等限制。只有使用OpenCL驅(qū)動程序例程正確完成分配,才能使用共享內(nèi)存。
結(jié)果
下圖顯示了Adreno各版本單精度一般矩陣乘法(SGEMM)的性能提升:
圖3:Adreno GPU 4xx和530的性能數(shù)據(jù)
該圖基于常用浮點運算數(shù)據(jù)。使用不同數(shù)據(jù)類型(8位、16位、固定點等)的其他MM內(nèi)核函數(shù)可以根據(jù)我們在SGEMM采用的相同原理進行有效實現(xiàn)。
一般來說,我們對Adreno GPU優(yōu)化的MM實現(xiàn)比簡單實現(xiàn)至少快兩個數(shù)量級。
接下來?
在下一篇文章中,我將給出這些概念背后的OpenCL代碼清單。
矩陣乘法是卷積神經(jīng)網(wǎng)絡(luò)中一個重要的基本線性代數(shù)運算。尤其是DL算法性能與MM相關(guān),因為DL卷積的所有變化均可以簡化為乘法矩陣。
上面描述的概念和您在下一篇文章中看到的代碼并不是計算卷積的唯一方法。但事實上,很多流行的DL框架,比如Caffe,Theano和谷歌的TensorFlow往往將卷積運算分解為MM,因此沿著這個方向思考不失為一個好辦法。敬請關(guān)注第2部分中的代碼示例。
相關(guān)閱讀:
Qualcomm Adreno GPU 如何獲得更好的OpenCL性能——內(nèi)存優(yōu)化篇
經(jīng)驗分享:Silk Labs 如何以極低的成本,獲得軟硬件開發(fā)資源
如何開始使用Adreno SDK for Vulkan
Vulkan開發(fā)系列視頻教程
更多Qualcomm開發(fā)內(nèi)容請詳見: Qualcomm開發(fā)者社區(qū)?。
電子發(fā)燒友App


















評論