GPU計算優化的原理是什麼?
比如http://homepages.inf.ed.ac.uk/msteuwer/papers/GPGPU2016.pdf里,貼了矩陣乘法的naive實現和,優化版本的代碼。不能理解優化的版本到底幹了啥。而我又看了下ppcg生成出來的GPU代碼也是長這個樣子的,我自己卻搞了個naive的 ...
naive
kernel mm( global float * A, B, C, int N, K, M) {
int gid0 = global_id (0);
int gid1 = global_id (1);
float acc = 0.0f;
for (int i=0; i&opt
kernel mm_amd_opt ( global float * A, B, C,
int K, M, N) {
local float tileA [512]; tileB [512];
private float acc_0 ; ...; acc_31 ;
private float blockOfB_0 ; ...; blockOfB_3 ;
private float blockOfA_0 ; ...; blockOfA_7 ;
int lid0 = local_id (0); lid1 = local_id (1);
int wid0 = group_id (0); wid1 = group_id (1);
for (int w1=wid1; w1 &
GPU與CPU相比,強在內存訪問帶寬和並行單元運算能力。常規優化思路就是儘可能多的優化資源使用,最小化內存訪問,最大化運算並行,用並行運算的吞吐量彌補訪存延時方面的劣勢。
對於矩陣相乘,最核心運算就是大量的乘加操作,要克服的問題就是讀顯存的延遲。不好好做優化很容易碰到顯存讀取瓶頸。
題主的naive版本是最直接最容易實現的版本。這種代碼一般能達到GPU硬體運算能力的5%-10%。每次乘加運算都依賴一組新的A/B輸入。如果MNK比較小,可能沒有足夠的線程補償訪存延遲。但如果MNK比較大,線程雖多但是訪存量也同比變大,容易把cache用爆,大量的訪存請求需要到DRAM處理,使延遲問題更加惡化。
再來看這個優化版本,幾個比較明顯的優化手段:
1. local memorylocal memory訪存延遲遠小於顯存。如果先批量將數據預讀到local memory再做常規計算,降低平均訪存延遲,補償延遲的門檻會降低2. 循環展開雖然優化代碼並沒有顯式要求循環展開,但我相信編譯器應該會自動做,通過展開調換不同迭代之間的訪存、運算指令順序,降低平均訪存開銷
3. 向量化存取GPU的存儲單元的訪存粒度一般都比較大,即使運算線程只要求讀取4byte, 底層存儲也許一次返回幾十byte數據。只有被要求的4byte被送給運算線程,其他數據被中途丟棄,這實質上降低了有效訪存效率。如果用vstore4、vload4指令,一個線程要求16byte數據,在一次內存返回中有更大機會獲得更多有效數據。具體用多大的向量化存取合適,可以查NVIDIA,AMD的編程指南,都是公開數據。4. 單線程算多數據點最小化顯存訪問的另一個思路是一個線程多算幾個點。反正我已經從顯存、local memory讀到了輸入數據,只算一個數豈不是太浪費了。不如多算幾個點,把這個輸入能貢獻到的輸出點都算一下吧...本例的優化代碼一個線程算32個點acc_0 ~acc_31。好好研讀一下programming guide,相信以上信息都會被講到。在nvidia/AMD的BLAS庫里也有矩陣乘的API,他們有對體系結構更深的了解,可能從底層做更「黑科技」級別的優化。想了解那部分的話,加入他們吧...
希望對題主有幫助GPU優化的核心是並行化、矢量化,對內存的訪問要盡量localize,提高cache的命中率。
題主給的優化例子正是體現了以上幾點。
1. 循環展開
這是並行化的一個很重要的手段,關鍵是每次的循環任務可以單獨執行,不存在迭代。
2. 使用local memory
這其實是一把雙刃劍,有些GPU廠家使用register實現local memory,這使得其訪問效率極高,但帶來的問題就是其大小非常有限。如果,實際使用的size超過了其大小,driver可能會使用系統memory進行補充,這就引入了額外的load/store過程,反而得不償失。
3. 矢量化運算及存取
其實就是盡量使用矢量運算。因為GPU中大多集成了矢量加法和乘法運算器,一次計算4個肯定比計算1個數效率高。不過現在像ARM最新的GPU使用了wrap manager,會集合4個同組標量運算組成一個矢量運算,這也大大提高了標量運算的效率。至於矢量化存取,要考慮匯流排一次burst的帶寬,目的就是填滿每次burst。
4. 要盡量避免分支
GPU運算中最忌諱的事就是分支,如果一個kernel中出現了分支,那麼在運行時就會由並行退化為串列,每個分支都要單獨走完,而其他線程都會等在那裡。
5. 選擇合適的workgroup size
這個約束條件就比較多了。要考慮到GPU所能接收的最大local memory的大小;考慮演算法的設計,比如有些圖形處理演算法要求一個workgroup size必須是128x128;還要考慮L/S latency的問題,合理選擇workgroup size可以有有效的cover L/S的延遲。
綜上所述,GPU的優化和具體硬體情況息息相關,這就是為什麼現在Cuda做的很好,而OpenCL一直不溫不火的原因。NV肯花力氣在自己的硬體設備上建立優化的演算法庫,而其他廠商很難花力氣做OpenCL的演算法庫,這就使別人二次開發很困難。
感謝邀請,雖然我在NVIDIA但是其實我不太熟悉GPU編程。
如果沒看錯,應該是通過將代碼的loop unrolling/vectorization,然後利用GPU的並發能力來提高速度。
當然,數據在內存和cache的分布也是要優化的。
Loop unrollinghttps://en.wikipedia.org/wiki/Automatic_vectorization謝邀。
說實話我沒搞過GPU優化,我搞的是x86的運算優化,後來是搞可編程晶元的運算優化,基本上把GPU給跳過了。
看到這裡還在繼續的也是真愛了,那我拿點真材實料給你們吧,就是你們平時說的乾貨。這些東西需要基本的線性代數知識。
首先計算機目前的很大一部分計算最後都可以歸為數學上的矩陣運算,而矩陣運算的核心是向量運算,或者說是點積/叉積。比如圖形學裡面的坐標變換,以及現在炒的燙手的深度學習,核心都是大量的向量運算。優化了向量運算,就優化了整個演算法。
這裡用點積作為例子來說。點積就是把兩個向量:
a1 a2 a3...an
b1 b2 b3...bn的各個標量相乘,a1xb1=c1 a2xb2=c2 ... anxbn=cn
最後把 c1+c2+...+cn = sum 這樣
CPU的做法是,
先從內存裡面拿個a1,等著數據來再從內存里拿個b1,等著數據來算個c1,放內存里,等著存好從內存里拿個a2,等著數據來內存比CPU慢幾千倍,你自己想想有多慢。有了cache幫忙也就是稍微好一點
後面x86的CPU是一次算4個,每次拿a1 a2 a3 a4這樣,是原來的4倍。
GPU的代碼從樓主貼的來看,也是盡量並行處理,但是要看硬體有多少個乘法器了。乘法器多,循環就能少幾次。
我當年搞的可編程計算,是有好多乘法器,所以一個點積,基本上一個時鐘周期就把乘法算好了,然後下一個時鐘周期能算3個加法,所以一個8維向量的點積,兩個周期就算好了,而且每個周期能retire一個N維向量點積。
說到底,還是要把並行化做到極致。看起來像是通過循環展開,提高內存訪問的局部性。另外,外層循環之間都是並行處理的,因為GPU內存比cache大的多,這樣做各塊只要使用自己的local內存就好了,會減少對cache的爭奪。
這個提問者莫非是?
其實很簡單啦, cpu是大哥, 一個人要完成每個像素的遍歷。 如果是1000*1000的圖片, 那麼cpu要執行1000000次循環,所以大哥也就力不從心了。 gpu是小弟, 不是一個, 使成千上萬個... 大哥做不動的任務, 分散成一個個小任務(kernal), 加入我們給一個小弟5*5的圖像碎片處理, 那麼40000個小弟同時幹活 可以在很短的時間完成大哥幹得費力的任務。cuda有點像shader, 是寫一個小程序,這個小程序不是在一個地方跑,而是利用分而治之的思路,代理給顯卡中千萬個小渲染核心去完成。
我看了一眼,應該就是普通的、通過分塊提高Cache命中率的矩陣乘法優化。
題主的naive版本和gpu的並行版本均可從以下視頻得到一個清晰的感性認識。
NVIDIA GPU 80毫秒繪製蒙娜麗莎!
推薦閱讀: