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