Upload
others
View
2
Download
0
Embed Size (px)
Citation preview
Základní paralelní operace
Tomáš [email protected]
25. února 2021
Videa na Youtube:
Redukce
Prefix sum
https://www.youtube.com/watch?v=p0X2YKbtLs4https://www.youtube.com/watch?v=tAuYQwg4-Hw
Co je paralelní redukce?
DefinitionMějme pole prvků a1, . . . ,an určitého typu a asociativní operaci⊕. Paralelní redukce je paralelní aplikace asociativní operace⊕ na všechny prvky vstupního pole tak, že výsledkem je jedenprvek a stejného typu, pro který platí
a = a1 ⊕ a2 . . . an.
Příklad: Pro jednoduchost budeme uvažovat operaci sčítaní.Pak jde o paralelní provedení tohoto kódu:a = a[ 1 ];for( int i = 2; i
Paralelní provedení
p1 p2 p3 p4 p5 p6 p7 p8
a1 a2 a3 a4 a5 a6 a7 a8
a1 + a2 a3 + a4 a5 + a6 a7 + a8
∑4i=1 ai
∑8i=5 ai
∑8i=1 ai
Paralelní provedení
p1 p2 p3 p4 p5 p6 p7 p8
a1 a2 a3 a4 a5 a6 a7 a8
a1 + a2 a3 + a4 a5 + a6 a7 + a8
∑4i=1 ai
∑8i=5 ai
∑8i=1 ai
Paralelní provedení
p1 p2 p3 p4 p5 p6 p7 p8
a1 a2 a3 a4 a5 a6 a7 a8
a1 + a2 a3 + a4 a5 + a6 a7 + a8
∑4i=1 ai
∑8i=5 ai
∑8i=1 ai
Paralelní provedení
p1 p2 p3 p4 p5 p6 p7 p8
a1 a2 a3 a4 a5 a6 a7 a8
a1 + a2 a3 + a4 a5 + a6 a7 + a8
∑4i=1 ai
∑8i=5 ai
∑8i=1 ai
Analýza paralelní provedení
Za předpokladu n = p, snadno vidíme, že platí:I TS(n) = θ(n)I TP(n) = θ(log n)
I S(n) = θ(
nlog n
)= O(n) = O(p)
I E(n) = θ(
1log n
)
I pro velká n efektivita klesá k nuleI C(n) = θ (n log n) = ω (TS(n))
I algoritmus není nákladově optimální
Provedeme úpravu algoritmu tak, aby byl nákladově optimální.
Nákladově optimální redukce
p1 p2 p3 p4
a1, a2 a3, a4 a5, a6 a7, a8
a1 + a2 a3 + a4 a5 + a6 a7 + a8
∑4i=1 ai
∑8i=5 ai
∑8i=1 ai
Nákladově optimální redukce
p1 p2 p3 p4
a1, a2 a3, a4 a5, a6 a7, a8
a1 + a2 a3 + a4 a5 + a6 a7 + a8
∑4i=1 ai
∑8i=5 ai
∑8i=1 ai
Nákladově optimální redukce
p1 p2 p3 p4
a1, a2 a3, a4 a5, a6 a7, a8
a1 + a2 a3 + a4 a5 + a6 a7 + a8
∑4i=1 ai
∑8i=5 ai
∑8i=1 ai
Nákladově optimální redukce
p1 p2 p3 p4
a1, a2 a3, a4 a5, a6 a7, a8
a1 + a2 a3 + a4 a5 + a6 a7 + a8
∑4i=1 ai
∑8i=5 ai
∑8i=1 ai
Nákladově optimální redukce
I nyní máme méně procesorů než prvků, tj. p < n.I nejprve se provede sekvenční redukce np prvků na každém
procesoru→ θ(
np
)
I a pak paralelní redukce na p procesorech→ θ (log p)I celkem tedy TP (n,p) = θ
(np + log p
)
I S(n,p) = TS(n)TP(n,p) = θ(
nnp+log p
)= θ
(p
1+ p log pn
)
I E(n,p) = S(n,p)p = θ(
11+ p log pn
)
Nákladově optimální redukce
Pak platíI C(n,p) = pTP(n,p) = θ (n + p log p)I protože je n = Ω (p log p), máme
C(n,p) = θ(n) = θ(TS(n)).
Algoritmus je tedy nákladově optimální.I toho jsme dosáhli na úkor počtu procesorůI některé algoritmy nedokáží optimálně využít velký počet
procesorů
Paralelní redukce na architekturách se sdílenoupamětí
I OpenMP má podporu pro redukci s základními operacemi– reduction(sum:+)
I pokud chceme dělat redukci s jinou operací (min, max)nebo jiným, než základním typem (matice), je nutné jiprovést explicitně
I tyto architektury mají většinou maximálně několik desítekprocesorů
I redukci lze často provádět sekvenčněI mezivýsledky se ukládají do nesdílených proměnných
I pozor na cache coherence a false sharing - tyto proměnnéby neměly být alokované v jednom bloku
I nakonec se zapíší do sdíleného pole, na kterém seprovede redukce třeba i sekvenčně nultým procesem
Paralelní redukce na architekturách s distribuovanoupamětí
I standard MPI má velmi dobrou podporu pro redukciI podporuje více operací a to i pro odvozené typy a struktury
Paralelní redukce na GPU v CUDA
Mějme pole o N prvcích. Paralelní redukce na GPU probíhánásledujícím způsobem:I redukci bude provádět N/blkSize bloků
I pro jednoduchost předpokládáme, že blkSize dělí NI výsledkem bude pole o N/blkSize prvcíchI na toto pole opět provedeme stejným způsobem paralelní
redukci, až dojdeme k poli o velikosti 1I ve skutečnosti se může vyplatit provést poslední redukci o
pár prvcích na CPUI úlohu jsme tak převedli na redukci dat v rámci jednoho
bloku
Paralelní redukce na GPU v CUDA1 __global__ void reduc t ionKerne l1 ( i n t * dOutput , i n t * d Input )2 {3 extern __shared__ v o l a t i l e i n t sdata [ ] ;45 / / Nacteme data do sd i l ene pameti6 unsigned i n t t i d = th read Idx . x ;7 unsigned i n t gid = b lock Idx . x * blockDim . x + th read Idx . x ;8 sdata [ t i d ] = dInput [ g id ] ;9 __syncthreads ( ) ;
1011 / / Provedeme p a r a l e l n i redukc i12 for ( unsigned i n t s =1; s
Paralelní redukce na GPU v CUDA
I dInput je vstupní pole pro redukciI dOutput je výstupní pole pro redukciI řádek č. 3 provádí dynamickou alokaci paměti ve sdílené
paměti multiprocesoru do pole sdataI proměnná tid udává číslo vlákna v rámci blokuI proměnná gid udává číslo vlákna v rámci gridu a tím i
prvek globálního pole uInput, který bude vlákno načítatI na řádku 8 se načítají prvky do sdílené paměti
I z globální paměti čteme sekvenčně, tedy pomocísloučených přístupů (coalesced accesses)
I náslědne se na řádku 9 synchronizují, nelze redukovat,dokud nejsou načtena všechna data
Paralelní redukce na GPU v CUDA
I for cyklus na řádku 12 udává pomocí proměnné s, s jakýmkrokem redukujemeI začínáme s krokem jedna a krok se pokaždé zdvojnásobuje
I řádek 14 říká, že redukci provádějí vlákna, jejichž ID jedělitelné dvojnásobkem momentálního redukčního kroku
I vlastní redukce se provádí na řádku 15I než pokračujeme dalším kolem redukce, synchronizujeme
všechna vlákna na řádku 16I nakonec, když je vše zredukované, nulté vlákno zapíše
výsledek na správné místo do výstupního pole
Paralelní redukce na GPU v CUDA
Tento postup ovšem není ideální. Dává následující výsledky(výkon měříme v GB dat zredukovaných za 1 sec.)I GeForce GTX 8800 - 2.16 GB/secI GeForce GTX 460 - 4.26 GB/sec
Kde je problém?
Paralelní redukce na GPU v CUDA
tid:0 1 2 3 4 5 6 7
a0 a1 a2 a3 a4 a5 a6 a7
a0 + a1 a2 + a3 a4 + a5 a6 + a7
∑3i=0 ai
∑7i=4 ai
∑7i=0 ai
Paralelní redukce na GPU v CUDA
tid:0 1 2 3 4 5 6 7
a0 a1 a2 a3 a4 a5 a6 a7
a0 + a1 a2 + a3 a4 + a5 a6 + a7
∑3i=0 ai
∑7i=4 ai
∑7i=0 ai
Paralelní redukce na GPU v CUDA
tid:0 1 2 3 4 5 6 7
a0 a1 a2 a3 a4 a5 a6 a7
a0 + a1 a2 + a3 a4 + a5 a6 + a7
∑3i=0 ai
∑7i=4 ai
∑7i=0 ai
Paralelní redukce na GPU v CUDA
I používáme silně divergentní vláknaI pokaždé je více než jedna polovina warpu nečinná
I používáme pomalou funkci modulo
Paralelní redukce na GPU v CUDA
Nahradíme tento kód1 / / Provedeme p a r a l e l n i redukc i2 for ( unsigned i n t s =1; s < blockDim . x ; s*=2 )3 {4 i f ( ( t i d % ( 2 * s ) ) == 0 )5 sdata [ t i d ] += sdata [ t i d + s ] ;6 __syncthreads ( ) ;7 }
tímto1 / / Provedeme p a r a l e l n i redukc i2 for ( unsigned i n t s =1; s
Paralelní redukce na GPU v CUDA
tid:0 1 2 3
a0 a1 a2 a3 a4 a5 a6 a7
a0 + a1 a2 + a3 a4 + a5 a6 + a7
∑3i=0 ai
∑7i=4 ai
∑7i=0 ai
Paralelní redukce na GPU v CUDA
tid:0 1
a0 a1 a2 a3 a4 a5 a6 a7
a0 + a1 a2 + a3 a4 + a5 a6 + a7
∑3i=0 ai
∑7i=4 ai
∑7i=0 ai
Paralelní redukce na GPU v CUDA
tid:0
a0 a1 a2 a3 a4 a5 a6 a7
a0 + a1 a2 + a3 a4 + a5 a6 + a7
∑3i=0 ai
∑7i=4 ai
∑7i=0 ai
Paralelní redukce na GPU v CUDA
Nyní nemapujeme vlákna podle indexu v původním poli, aletak, že redukce vždy provádí prvních blockDim.x/s vláken.I pokud redukujeme jedním blokem 256 prvků, je v prvním
kroku činných prvních 128 vláken a zbylých 128 neI první 4 warpy jsou plně vytížené, zbylé 4 warpy vůbecI v žádném warpu ale nejsou divergentní vlákna
Jak se změnil výkon?I GeForce GTX 8800 - 4.28 GB/secI GeForce GTX 460 - 7.81 GB/sec
Výkon se zvýšil na dvojnásobek, což dobře koresponduje s tím,že jsme zabránili nečinnosti poloviny vláken ve warpu.Kde je problém ted’?I v přístupech do sdílené paměti
Paralelní redukce na GPU v CUDA
Nyní nemapujeme vlákna podle indexu v původním poli, aletak, že redukce vždy provádí prvních blockDim.x/s vláken.I pokud redukujeme jedním blokem 256 prvků, je v prvním
kroku činných prvních 128 vláken a zbylých 128 neI první 4 warpy jsou plně vytížené, zbylé 4 warpy vůbecI v žádném warpu ale nejsou divergentní vlákna
Jak se změnil výkon?I GeForce GTX 8800 - 4.28 GB/secI GeForce GTX 460 - 7.81 GB/sec
Výkon se zvýšil na dvojnásobek, což dobře koresponduje s tím,že jsme zabránili nečinnosti poloviny vláken ve warpu.Kde je problém ted’?I v přístupech do sdílené paměti
Paralelní redukce na GPU v CUDA
I víme, že sdílená pamět’ se skládá z 32 pamět’ových bankI pole wordů (4 bajty) se ukládá postupně do jednotlivých po
sobě jdoucích bankI pokud je s = 32 a redukujeme typ int, pak všechna
vlákna warpu čtou z jedné banky, ale každé z jiné adresyI to je pochopitelně velmi pomalé
Paralelní redukce na GPU v CUDA
Nahradíme tento kód1 / / Provedeme p a r a l e l n i redukc i2 for ( unsigned i n t s =1; s0; s>>=1 )3 {4 i f ( t i d < s )5 sdata [ t i d ] += sdata [ t i d + s ] ;6 __syncthreads ( ) ;7 }
Paralelní redukce na GPU v CUDA
tid:0 1 2 3
a0 a1 a2 a3 a4 a5 a6 a7
a0 + a4 a1 + a5 a2 + a6 a3 + a7
∑3i=0 ai
∑7i=4 ai
∑7i=0 ai
Paralelní redukce na GPU v CUDA
tid:0 1
a0 a1 a2 a3 a4 a5 a6 a7
a0 + a4 a1 + a5 a2 + a6 a3 + a7
∑3i=0 a2i
∑3i=0 a2i+1
∑7i=0 ai
Paralelní redukce na GPU v CUDA
tid:0
a0 a1 a2 a3 a4 a5 a6 a7
a0 + a4 a1 + a5 a2 + a6 a3 + a7
∑3i=0 a2i
∑3i=0 a2i+1
∑7i=0 ai
Paralelní redukce na GPU v CUDA
Redukce nyní probíhá tak, že slučujeme jeden prvek z prvnípoloviny redukovaného pole a jeden prvek z druhé poloviny.Tím pádem vlákna přistupují i do sdílené paměti sekvenčně.
Jak se změnil výkon?I GeForce 8800 GTX - 9.73 GB/sI GeForce 460 GTX - 11.71 GB/s
Co zlepšit dále?I stále platí, že polovina vláken nedělá vůbec nic kromě
načítání dat z globální pamětiI zmenšíme proto rozměr gridu na polovinu a necháme
každé vlákno provést jednu redukci už při načítání dat zglobální paměti
Paralelní redukce na GPU v CUDA
Redukce nyní probíhá tak, že slučujeme jeden prvek z prvnípoloviny redukovaného pole a jeden prvek z druhé poloviny.Tím pádem vlákna přistupují i do sdílené paměti sekvenčně.Jak se změnil výkon?I GeForce 8800 GTX - 9.73 GB/sI GeForce 460 GTX - 11.71 GB/s
Co zlepšit dále?I stále platí, že polovina vláken nedělá vůbec nic kromě
načítání dat z globální pamětiI zmenšíme proto rozměr gridu na polovinu a necháme
každé vlákno provést jednu redukci už při načítání dat zglobální paměti
Paralelní redukce na GPU v CUDA
Nahradíme tento kód1 / / Nacteme data do sd i l ene pameti2 unsigned i n t t i d = th read Idx . x ;3 unsigned i n t gid = b lock Idx . x * blockDim . x + th read Idx . x ;4 sdata [ t i d ] = dInput [ g id ] ;5 __syncthreads ( ) ;
tímto1 unsigned i n t t i d = th read Idx . x ;2 unsigned i n t gid = b lock Idx . x * blockDim . x *2 + th read Idx . x ;3 sdata [ t i d ] = dInput [ g id ] + dInput [ g id+blockDim . x ] ;4 __syncthreads ( ) ;
Paralelní redukce na GPU v CUDA
Poznámka: Pozor! Používáme poloviční grid. V kódu to neníexplicitně vidět. Kernel se ale musí volat s polovičním počtembloků!!!Jak se změnil výkon?I GeForce 8800 GTX - 17.6 GB/sI GeForce 460 GTX - 23.43 GB/s
Paralelní redukce na GPU v CUDA
V dalším kroku odstraníme for cyklus:1 for ( unsigned i n t s=blockDim . x / 2 ; s >0; s>>=1 )
Jelikož maximální velikost bloku je 1024, proměnná s můženabývat jen hodnotI 512, 256, 128, 64, 32, 16, 8, 4, 2 a 1.
Kernel nyní přepíšeme s pomocí šablon C++.
Paralelní redukce na GPU v CUDA1 template 2 __global__ void reductKern5 ( i n t * dOutput , i n t * d Input )3 {4 extern __shared__ v o l a t i l e i n t sdata [ ] ;56 / / Nacteme data do sd i l ene pameti7 unsigned i n t t i d = th read Idx . x ;8 unsigned i n t gid = b lock Idx . x * blockDim . x *2 + th read Idx . x ;9 sdata [ t i d ] = dInput [ g id ] + dInput [ g id+blockDim . x ] ;
10 __syncthreads ( ) ;1112 / / Provedeme p a r a l e l n i py ramida ln i redukc i13 i f ( b lockSize == 1024) {14 i f ( t i d = 512) {16 i f ( t i d = 256) {18 i f ( t i d = 128) {20 i f ( t i d < 64 ) { sdata [ t i d ] += sdata [ t i d + 64 ] ; } __syncthreads ( ) ; }21 i f ( t i d < 32) {22 i f ( b lockSize >= 64) sdata [ t i d ] += sdata [ t i d + 3 2 ] ;23 i f ( b lockSize >= 32) sdata [ t i d ] += sdata [ t i d + 1 6 ] ;24 i f ( b lockSize >= 16) sdata [ t i d ] += sdata [ t i d + 8 ] ;25 i f ( b lockSize >= 8) sdata [ t i d ] += sdata [ t i d + 4 ] ;26 i f ( b lockSize >= 4) sdata [ t i d ] += sdata [ t i d + 2 ] ;27 i f ( b lockSize >= 2) sdata [ t i d ] += sdata [ t i d + 1 ] ;28 }2930 / / Vysledek zapiseme zpet do g l o b a l n i pameti z a r i z e n i31 i f ( t i d == 0) dOutput [ b lock Idx . x ] = sdata [ 0 ] ;32 }
Paralelní redukce na GPU v CUDA
I podle toho, jak je blok velký, se provede potřebný početredukcí
I ve chvíli, kdy redukci již provádí jen 32 nebo méně vláken,výpočet probíhá v rámci jednoho warpu
I vlákna v jednom warpu jsou synchronizována implicitněnebot’ jde čistě o SIMD zpracování, proto již nemusímevolat __syncthreads()
Jak se změnil výkon?I GeForce 8800 GTX - 36.7 GB/sI GeForce 460 GTX - 46.8 GB/s
Co lze zlepšit dále?
Paralelní redukce na GPU v CUDA
I nyní pomocí p vláken redukujeme 2p prvkůI ukázali jsme si ale, že v tom případě není výpočet
nákladově optimálníI to znamená, že používáme příliš velký počet procesorů,
které ale nejsou efektivně využityI v případě GPU to znamená, že multiprocesory nejsou stále
plně využityI když budeme určitý úsek redukovaného pole zpracovávat
menším počtem multiprocesorů, bude jejich využitíefektivnější, a zbylé multiprocesory se uvolní prozpracovávání zbytku redukovaných dat
I pro docílení nákladové optimality je potřeba volit p = nlog nI pro pole o velikosti 2048 prvků dostáváme p = 204811 = 341I výsledek zaokrouhlíme na 256
Výsledný kernel vypadá takto:
Paralelní redukce na GPU v CUDA1 template 2 __global__ void reductKern6 ( i n t * dOutput , i n t * dInput , u i n t s i ze )3 {4 extern __shared__ v o l a t i l e i n t sdata [ ] ;5 / / Nacteme data do sd i l ene pameti6 unsigned i n t t i d = th read Idx . x ;7 unsigned i n t gid = b lock Idx . x * b lockSize *2 + th read Idx . x ;8 unsigned i n t gr idS ize = blockSize *2* gridDim . x ;9 sdata [ t i d ] = 0 ;
10 while ( g id < s ize )11 {12 sdata [ t i d ] += dInput [ g id ] + dInput [ g id+blockSize ] ;13 g id += gr idS ize ;14 }15 __syncthreads ( ) ;1617 / / Provedeme p a r a l e l n i py ramida ln i redukc i18 i f ( b lockSize == 1024) {19 i f ( t i d = 512) {21 i f ( t i d = 256) {23 i f ( t i d = 128) {25 i f ( t i d < 64 ) { sdata [ t i d ] += sdata [ t i d + 64 ] ; } __syncthreads ( ) ; }26 i f ( t i d < 32) {27 i f ( b lockSize >= 64) sdata [ t i d ] += sdata [ t i d + 3 2 ] ;28 i f ( b lockSize >= 32) sdata [ t i d ] += sdata [ t i d + 1 6 ] ;29 i f ( b lockSize >= 16) sdata [ t i d ] += sdata [ t i d + 8 ] ;30 i f ( b lockSize >= 8) sdata [ t i d ] += sdata [ t i d + 4 ] ;31 i f ( b lockSize >= 4) sdata [ t i d ] += sdata [ t i d + 2 ] ;32 i f ( b lockSize >= 2) sdata [ t i d ] += sdata [ t i d + 1 ] ;33 }3435 / / Vysledek zapiseme zpet do g l o b a l n i pameti z a r i z e n i36 i f ( t i d == 0) dOutput [ b lock Idx . x ] = sdata [ 0 ] ;37 }
Paralelní redukce na GPU v CUDA
Jak se změnil výkon?I GeForce 8800 GTX - 57.2 GB/sI GeForce 460 GTX - ??? GB/s
Další optimalizace pro architekturu Kepler:http://devblogs.nvidia.com/parallelforall/faster-parallel-reductions-kepler/
http://devblogs.nvidia.com/parallelforall/faster-parallel-reductions-kepler/http://devblogs.nvidia.com/parallelforall/faster-parallel-reductions-kepler/
Paralelní redukce na GPU v CUDAKernel GTX 8800 GTX 460
výkon GB/s urychlení výkon GB/s urychleníKernel 1 2.16 – 4.3 –divergentní vláknaKernel 2 4.28 1.98 7.8 1.8přístupy do sdílené pamětiKernel 3 9.73 2.3/4.5 11.71 1.5/2.7nečinná vláknaKernel 4 17.6 1.8/8.1 23.43 2/5.4for cyklusKernel 5 36.7 2.1/16.9 46.8 2/10.8není nákladově optimálníKernel 6 57.2 1.6/26.5
diverg. vl. L1 konflikty aktivní warpy spuštěné warpyKernel 1 112,344 0 90,435,920 18,728divergentní vláknaKernel 2 4,689 211,095 48,277,598 18,764přístupy do sdílené pamětiKernel 3 4,687 0 37,313,361 18,736nečinná vláknaKernel 4 2,343 0 19,501,609 9,376for cyklusKernel 5 294 0 5,948,100 1,180není nákladově optimálníKernel 6 ??? ??? ??? ???
Prefix sum
DefinitionMějme pole prvků a1, . . . ,an určitého typu a asociativní operaci⊕. Inkluzivní prefix sum je aplikace asociativní operace ⊕ navšechny prvky vstupního pole tak, že výsledkem je poles1, . . . , sn stejného typu, pro kterou platí
si = ⊕ij=1ai .
Exkluzivní prefix sum je definován vztahy σ1 = 0 a
σi = ⊕i−1j=1ai ,
pro i > 0.
Prefix sum
Poznámka: σi = si − ai pro i = 1, . . . ,n.Příklad: Pro jednoduchost budeme uvažovat operaci sčítaní.Pak jde o paralelní provedení tohoto kódu:s[ 1 ] = a[ 1 ];for( int i = 2; i
Paralelní prefix sum
p1 p2 p3 p4 p5 p6 p7 p8
a1 a2 a3 a4 a5 a6 a7 a8
a1 + a2 a3 + a4 a5 + a6 a7 + a8
∑4i=1 ai
∑8i=5 ai
∑8i=1 ai
Paralelní prefix sum
p1 p2 p3 p4 p5 p6 p7 p8
a1 a2 a3 a4 a5 a6 a7 a8
a1 a1 + a2 a2 + a3 a3 + a4 a4 + a5 a5 + a6 a6 + a7 a7 + a8
∑4i=1 ai
∑8i=5 ai
∑8i=1 ai
Paralelní prefix sum
p1 p2 p3 p4 p5 p6 p7 p8
a1 a2 a3 a4 a5 a6 a7 a8
a1 a1 + a2 a2 + a3 a3 + a4 a4 + a5 a5 + a6 a6 + a7 a7 + a8
a1 a1 + a2∑3
1 ai∑4
1 ai∑5
2 ai∑6
3 ai∑7
4 ai∑8
5 ai
∑8i=1 ai
Paralelní prefix sum
p1 p2 p3 p4 p5 p6 p7 p8
a1 a2 a3 a4 a5 a6 a7 a8
a1 a1 + a2 a2 + a3 a3 + a4 a4 + a5 a5 + a6 a6 + a7 a7 + a8
a1 a1 + a2∑3
1 ai∑4
1 ai∑5
2 ai∑6
3 ai∑7
4 ai∑8
5 ai
a1 a1 + a2∑3
1 ai∑4
1 ai∑5
1 ai∑6
1 ai∑7
1 ai∑8
1 ai
Paralelní prefix sum
I TS(n) = θ(n)I TP(n) = θ(log n)
I S(n) = θ(
nlog n
)= O(n) = O(p)
I E(n) = θ(
1log n
)
Paralelní prefix sum
I vidíme, že prefix sum má stejnou složitost jako redukceI abysme získali nákladově optimální algoritmus, potřebovali
bysme na jeden procesor mapovat celý blok číselI nyní je to o trochu složitější
Nákladově optimální paralelní prefix sum
I volíme p < n, pro jednoduchost tak, aby p dělilo nI necht’ každý procesor má blok Ak pro k = 1, . . . ,pI v něm se sekvenčně napočítá částečný prefix sum
s∗i =∑
j∈Ai∧j≤iai .
I definujeme posloupnost Sk pro k = 1, . . . ,p tak, žeSk = sik , kde ik je index posledního prvku v bloku Ak .
I napočítáme exkluzivní prefix sum z posloupnosti Sk → ΣkI každý procesor nakonec přičte Σk ke svým siI potom je TP(n,p) = θ
(np + log p
)stejně jako u redukce
Paralelní prefix sum na GPU
Prefix sum v rámci jednoho bloku:1 template < typename Real >2 __global__ Real prefixSum ( Real * values )3 {4 i n t i = th read Idx . x ;5 i n t n = blockDim . x ;67 for ( i n t o f f s e t =1; o f f s e t = o f f s e t ) values [ i ] += t ;14 __syncthreads ( ) ;15 }16 }
Paralelní prefix sum na GPU
I tato implementace opět není příliš efektivní, budemeoptimalizovat
I na GPU jsme omezeni hierarchickou strukturou pamětíI nejprve ukážeme prefix sum v rámci jednoho warpu ...I ... dál v rámci bloku ...I ... a nakonec v rámci griduI využijeme k tomu variantu pro nákladově optimální prefix
sum
Paralelní prefix sum na GPU pro jeden warp1 template < ScanKind Kind , typename Real >2 __device__ Real prefixSumWarp ( v o l a t i l e Real * values , i n t i dx= th read Idx . x )3 {4 i n t lane = idx & 31;5 / / index o f thread i n warp67 i f ( lane >= 1 ) values [ i dx ] = values [ i dx − 1 ] + values [ i dx ] ;8 i f ( lane >= 2 ) values [ i dx ] = values [ i dx − 2 ] + values [ i dx ] ;9 i f ( lane >= 4 ) values [ i dx ] = values [ i dx − 4 ] + values [ i dx ] ;
10 i f ( lane >= 8 ) values [ i dx ] = values [ i dx − 8 ] + values [ i dx ] ;11 i f ( lane >= 16 ) values [ i dx ] = values [ i dx − 16 ] + values [ i dx ] ;1213 i f ( Kind == i n c l u s i v e ) return values [ i dx ] ;14 else return ( lane >0 ) ? values [ idx−1 ] : 0 ;15 }
Paralelní prefix sum na GPU pro jeden blok
I pro jednoduchost předpokládejme, že maximální velikostbloku je druhá mocnina velikosti warpu (322 = 1024)
I nejprve se provede prefix sum v rámci jednotlivých warpůbloku
I uložíme si poslední prvek z každého warpuI tím dostaneme pole o velikosti max. jednoho warpuI provedeme opět prefix sum (exkluzivní) ve warpuI každý warp si pak přičte svoji hodnotu z výsledku z
předchozího kroku ke všem svým hodnotámI pracujeme zde v rámci jednoho bloku, tedy v jednom
kernelu a se sdílenou pamětí
Paralelní prefix sum na GPU pro jeden blok1 template < ScanKind Kind , class T >2 __device__ Real scanBlock ( v o l a t i l e Real * values , i n t i dx= th read Idx . x )3 {4 extern __shared__ v o l a t i l e i n t sdata [ ] ;56 const i n t lane = idx & 31;7 const i n t warpid = idx >> 5;89 / / Step 1 : I n t r a−warp scan i n each warp
10 Ral va l = scanWarp< Kind >( values , i dx ) ;11 __syncthreads ( ) ;1213 / / Step 2 : C o l l e c t per−warp p a r t i a l r e s u l t s14 i f ( lane == 31 ) sdata [ warpid ] = values [ i dx ] ;15 __syncthreads ( ) ;1617 / / Step 3 : Use 1 s t warp to scan per−warp r e s u l t s18 i f ( warpid == 0 ) scanWarp< exc lus i ve >( sdata , i dx ) ;19 __syncthreads ( ) ;2021 / / Step 4 : Accumulate r e s u l t s from Steps 1 and 322 i f ( waprpid > 0 ) va l += sdata [ warpid ] ;23 __syncthreads ( ) ;2425 / / Step 5 : Wr i te and r e t u r n the f i n a l r e s u l t26 values [ i dx ] = va l ;27 __syncthreads ( ) ;2829 return va l ;30 }
Paralelní prefix sum na GPU pro jeden grid
I provedeme prefix sum pro každý blokI poslední prvek každého bloku uložíme do pole
blockResults[]
I na tomto poli se provede exkluzivní prefix sumI pak si přičte i-tý blok ke všem svým prvkům hodnotu z
i-tého prvku tohoto poleI pokud výpočet probíhá ve více gridech, je potřeba toto
zopakovat i v rámci gridů
Segmentovaný prefix-sum
I jde o napočítání několik prefix-sum najednouI například
[1,2,3], [4,5,6,7,8]
dá výsledek[1,3,6], [4,9,15,22,30]
I úlohu lze zakódovat jako jednu dlouhou řadu srestartovacími značkami
I sekvenční implementace pak může vypadat takto
Segmentovaný prefix-sum
1 void sequentialSegmentedPrefixSum ( const i n t * a ,2 const i n t * segments ,3 const i n t n ,4 i n t * segmentedPrefixSum )5 {6 segmetedPrefixSum [ 0 ] = a [ 0 ] ;7 for ( i n t i = 1 ; i < n ; i ++ )8 i f ( segments [ i ] == 0 )9 segmentedPrefixSum [ i ] =
10 segmentedPrefixSum [ i − 1] + a [ i ] ;11 else12 segmentedPrefixSum [ i ] = a [ i ] ;13 }
Segmentovaný prefix-sum
I teoreticky lze segmentovaný prefix-sum implementovatstejně jako normální jen s jinak definovanou operací"sčítání"
(si , fi)⊕ (sj , fj) = (fj == 0 ? si + sj : sj , fi |fj),
kde i < j a fi je posloupnost jedniček na začátcíchsegmentů, jinak samé nuly
Paralelní redukceParalelní redukce na architekturách se sdílenou pametíParalelní redukce na architekturách s distribuovanou pametíParalelní redukce na GPU v CUDA
Prefix sumSegmentovaný prefix-sum