65
Základní paralelní operace Tomáš Oberhuber [email protected] 25. února 2021

Základní paralelní operace - cvut.czgeraldine.fjfi.cvut.cz/~oberhuber/data/hpc/paa/prezentace/13-zaklad… · Co je paralelní redukce? Definition Mejme pole prvku˚ˇ a1;:::;an

  • 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