CUDA haladó ismeretek CUDA környezet részletei Többdimenziós indextér használata Megosztott memória használata Atomi műveletek használata
Optimalizálás Hatékonyság mérése Megfelelő blokkméret kiválasztása
© Szénási Sándor, Óbudai Egyetem, 2010
[email protected]
CUDA haladó ismeretek CUDA környezet részletei Többdimenziós indextér használata Megosztott memória használata Atomi műveletek használata
Optimalizálás Hatékonyság és mérése Megfelelő blokkméret kiválasztása
© Szénási Sándor, Óbudai Egyetem, 2010
[email protected]
Mátrix szorzó alkalmazás elkészítése • Feladat 3.1
– – – – – – – – –
verzió
Készítsünk CUDA alkalmazást, amelyik kétdimenziós (NxN méretű, egyszeres lebegőpontos számokat tartalmazó) mátrixok szorzását tudja elvégezni a GPU segítségével: A kód tartalmazzon egy N konstanst, ami a mátrixok méretét tartalmazza Foglaljon le 3 darab NxN méretű mátrixot (A, B, C) Töltse fel az A mátrixot tetszőleges adatokkal (pl. ai,j = i + j) Töltse fel a B mátrixot tetszőleges adatokkal (pl. bi,j = i - j) Foglaljon le 3 darab NxN méretű memóriaterületet a grafikus kártya memóriájában (devA, devB, devC) Végezze el a bemenő adatok másolását: A → devA, B → devB Indítson el egy kernelt, ami kiszámítja a devC = devA * devB eredményt Végezze el a kimenő adatok másolását: devC → C Írja ki az így kapott C vektor elemeinek értékét a képernyőre
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010
[email protected]
3
Többdimenziós mátrix a memóriában • Többdimenziós mátrix – lineáris memória – Bár a C programunkban tudunk többdimenziós tömböket lefoglalni, ezek elemei a memóriában értelemszerűen lineárisan helyezkednek el – Egy egyszerű 4x4-es mátrix elhelyezkedése: A mátrix A kétdimenziós tömb a0,0 a0,1 a0,2 a0,3 a1,0 a1,1 a1,2 a1,3
A = a0,0
...
...
...
a0,0 a0,1 a0,2 a0,3 a1,0 a1,1
a2,0 a2,1 a2,2 a2,3
a1,2 a1,3 a2,0 a2,1 a2,2 a2,3 a3,0 a3,1 a3,2
a3,0 a3,1 a3,2 a3,3
a3,3
...
...
...
...
...
...
...
...
• Többdimenziós mátrix elemeihez való hozzáférés – Amennyiben tehát ismert a mátrix kezdőcíme, a dimenziói, illetve az elemek méretei, a mátrix bármelyik elemének címe egyszerűen kiszámítható: asor,oszlop = a0,0 + (sor * oszlopszám + oszlop) * elemméret – Mivel a CUDA kernelnek csak a mátrixokat tartalmazó memóriaterületek kezdőcímét adjuk át, a fenti képlettel érhetjük el az egyes elemeket verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010
[email protected]
4
Többdimenziós mátrix a memóriában • Blokkok használata – Többdimenziós mátrixok esetén már egy 30x30-as méret is 900 darab szálat igényelne, amit nem tudunk egy blokkban elhelyezni – Emiatt célszerű a programot tetszőleges méretű blokkok kezelésére felkészíteni, ennek megfelelően a blokk azonosítót is kezelni kell
• Egy lehetséges kernel devC = devA * devB mátrixszorzás kódja: 1 2 3 4 5 6 7 8 9 10 11 12 verzió
2010.07.20.
__global__ static void MatrixMul(float *devA, float *devB, float *devC) { int indx = blockIdx.x * blockDim.x + threadIdx.x; int indy = blockIdx.y * blockDim.y + threadIdx.y;
}
if (indx < N && indy < N) { float sum = 0; for(int i = 0; i < N; i++) { sum += devA[indy * N + i] * devB[i * N + indx]; } devC[indy * N + indx] = sum; } © Szénási Sándor, Óbudai Egyetem, 2010
[email protected]
5
Többdimenziós mátrix a memóriában • Hoszt oldali kód – Inicializálás, memóriaterületek lefoglalása 1 2 3 4 5
cudaSetDevice(0); float A[N][N], B[N][N], C[N][N]; float *devA, *devB, *devC; cudaMalloc((void**) &devA, sizeof(float) * N * N); cudaMalloc((void**) &devB, sizeof(float) * N * N); cudaMalloc((void**) &devC, sizeof(float) * N * N);
– Bemenő adatok átmásolása 6 7
cudaMemcpy(devA, A, sizeof(float) * N * N, cudaMemcpyHostToDevice); cudaMemcpy(devB, B, sizeof(float) * N * N, cudaMemcpyHostToDevice);
– Kernel indítása 8 9 10 11
dim3 grid((N - 1) / BlockN + 1, (N - 1) / BlockN + 1); dim3 block(BlockN, BlockN); MatrixMul<<
>>(devA, devB, devC); cudaThreadSynchronize();
– Eredmények másolása, memória felszabadítása 12 13 verzió
2010.07.20.
cudaMemcpy(C, devC, sizeof(float) * N * N, cudaMemcpyDeviceToHost); cudaFree(devA); cudaFree(devB); cudaFree(devC); © Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
6
Igazított elhelyezés • Többdimenziós mátrix igazított tárolása – Néha célszerű elválasztani egymástól a mátrix egy sorában található oszlopok számát és a memóriában tárolt tömb sorainak tényleges méretét (pl. kettes számrendszerbeli számokkal gyorsabban számítható a szorzása, illetve a CUDA végrehajtó egységek memóriakezelése is gyorsítható így) – Egy egyszerű 5x5-ös mátrix elhelyezkedése, 8 elemes igazítással: A mátrix A kétdimenziós tömb a0,0 a0,1 a0,2 a0,3 a0,4 a1,0 a1,1 a1,2 a1,3 a1,4 a2,0 a2,1 a2,2 a2,3 a2,4 a3,0 a3,1 a3,2 a3,3 a3,4 a4,0 a4,1 a4,2 a4,3 a4,4
A = a0,0
...
...
...
a0,0 a0,1 a0,2 a0,3 a0,4
a1,1 a1,2 a1,3 a1,4 ...
...
...
...
...
...
...
...
...
...
...
a1,0
a2,0 a2,1 a2,2 a2,3 a2,4
a3,0 a3,1 a3,2 a3,3 a3,4
a4,1 a4,2 a4,3 a4,4
...
...
...
...
...
a4,0
...
...
...
...
• Igazított tárolás esetén a hozzáférés – A képlet hasonló, csak nem a tényleges oszlopszámmal szorzunk: asor,oszlop = a0,0 + (sor * igazított_oszlopszám + oszlop) * elemméret verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
7
Igazított memóriakezelés • Memória lefoglalása – A CUDA osztálykönyvtár rendelkezik egy memóriafoglaló rutinnal, ami automatikusan a legoptimálisabb igazítással foglalja le a megadott kétdimenziós memóriaterületet: cudaMallocPitch(void** devPtr, size_t *pitch, size_t width, size_t height) • • • •
devPtr – lefoglalt memóriára mutató pointer (címszerinti paraméter) pitch – igazítás mértéke (címszerinti paraméter) width – mátrix egy sorának a mérete height – mátrix sorainak száma
– A lineáris foglaláshoz hasonlóan a devPtr tartalmazza majd a memóriacímet – Az igazítás méretét tehát nem a függvény hívásakor kell megadni, hanem azt az osztálykönyvtár fogja megállapítani, és az így kiszámított értéket fogja visszaadni a pitch paraméterként átadott változóban – A mátrix egy sorának méreténél (width) ügyeljünk rá, hogy bájtban kell megadni a méretet, hiszen a függvény nem tudhatja, hogy mekkora elemeket akarunk majd eltárolni
verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
8
Igazított memóriakezelés • Memória másolás – Az igazított memóriakezelés során nem használható az egyszerű lineáris memória másolás, hiszen a cél és a forrás igazítása különböző is lehet – A másolásra jól használható az alábbi függvény, ami kezeli ezt a problémát: cudaMemcpy2D(void* dst, size_t dpitch, const void* src, size_t spitch, size_t width, size_t height, enum cudaMemcpyKing kind) • • • • • • •
dst – másolás célját mutató pointer dpitch – a cél adatszerkezet által használt igazítás mérete src – másolás forrását mutató pointer spitch – a forrás adatszerkezet által használt igazítás mérete width – a 2 dimenziós adatszerkezet egy sorának a mérete (bájtban) heigh – a 2 dimenziós adatszerkezet sorainak a száma Kind – másolás iránya (értékei azonosak a lineáris másolásnál látottakkal) – – – –
hoszt → hoszt (cudaMemcpyHostToHost) hoszt → eszköz (cudaMemcpyHostToDevice) eszköz → hoszt (cudaMemcpyDeviceToHost) eszköz → eszköz (cudaMemcpyDeviceToDevice)
– Amennyiben egy egyszerű igazítás nélküli tömböt használunk, akkor ne felejtsük el, hogy az igazítás mérete = tömb sorának mérete (és nem 0) verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
9
Mátrix szorzó alkalmazás elkészítése • Feladat 3.2
– – – – – – – – –
verzió
Készítsünk CUDA alkalmazást, amelyik kétdimenziós (NxN méretű, egyszeres lebegőpontos számokat tartalmazó) mátrixok szorzását tudja elvégezni a GPU segítségével: A kód tartalmazzon egy N konstanst, ami a mátrixok méretét tartalmazza Foglaljon le 3 darab NxN méretű mátrixot (A, B, C) Töltse fel az A mátrixot tetszőleges adatokkal (pl. ai,j = i + j) Töltse fel a B mátrixot tetszőleges adatokkal (pl. bi,j = i - j) Foglaljon le 3 darab NxN méretű memóriaterületet a grafikus kártya memóriájában a kártya által választott igazítással (devA, devB, devC) Végezze el a bemenő adatok másolását: A → devA, B → devB Indítson el egy kernelt, ami kiszámítja a devC = devA * devB eredményt Végezze el a kimenő adatok másolását: devC → C Írja ki az így kapott C vektor elemeinek értékét a képernyőre
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
10
Kernel igazított memóriakezeléssel • Módosítások – A kernel a paraméterként átadott igazítás mérettel végzi a szorzást N helyett – Az igazítás mérete bájtban van megadva, így típusos mutatók esetén a értékét osztani kell az elem méretével (a példában feltételeztük hogy ez megtehető, amennyiben a igazítás nem lenne osztható az elemmérettel, akkor pl. típus nélküli mutatókkal tudjuk az elemek helyeit kiszámítani)
• Egy lehetséges kernel devC = devA *static devBvoid mátrixszorzás MatrixMul(floatkódja: *devA, float *devB, float *devC, size_t pitch) { 1 __global__ 2 3 4 5 6 7 8 9 10 11 12 }
verzió
2010.07.20.
int indx = blockIdx.x * blockDim.x + threadIdx.x; int indy = blockIdx.y * blockDim.y + threadIdx.y;
if (indx < N && indy < N) { float sum = 0; for(int i = 0; i < N; i++) { sum += devA[indy * pitch/sizeof(float) + i] * devB[i * pitch/sizeof(float) + indx]; } devC[indy * pitch/sizeof(float) + indx] = sum; } © Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
11
Kernel igazított memóriakezeléssel • Hoszt oldali kód – Inicializálás, memóriaterületek lefoglalása 1 2 3 4 5
cudaSetDevice(0); float A[N][N], B[N][N], C[N][N]; float *devA, *devB, *devC; size_t pitch; cudaMallocPitch((void**) &devA, &pitch, sizeof(float) * N, N); cudaMallocPitch((void**) &devB, &pitch, sizeof(float) * N, N); cudaMallocPitch((void**) &devC, &pitch, sizeof(float) * N, N);
– Bemenő adatok átmásolása (feltételezzük, hogy a pitch azonos lett) 6 7
cudaMemcpy2D(devA, pitch, A, sizeof(float) * N, sizeof(float) * N, N, cudaMemcpyHostToDevice); cudaMemcpy2D(devB, pitch, B, sizeof(float) * N, sizeof(float) * N, N, cudaMemcpyHostToDevice);
– Kernel indítása 8 9 10 11
dim3 grid((N - 1) / BlockN + 1, (N - 1) / BlockN + 1); dim3 block(BlockN, BlockN); MatrixMul<<>>(devA, devB, devC, pitch); cudaThreadSynchronize();
– Eredmények másolása, memória felszabadítása 12 13 verzió
cudaMemcpy2D(C, sizeof(float) * N, devC, pitch, sizeof(float) * N, N, cudaMemcpyDeviceToHost); cudaFree(devA); cudaFree(devB); cudaFree(devC); 2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
12
CUDA haladó ismeretek CUDA környezet részletei Többdimenziós indextér használata Megosztott memória használata Atomi műveletek használata
Optimalizálás Hatékonyság és mérése Megfelelő blokkméret kiválasztása
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
Megosztott memória kezelése • Előző megoldások gyengesége – A mátrix szorzás meglehetősen kevés aritmetikai műveletet tartalmaz a memóriamozgatások számához képest – A GPU a memóriakezelésből származó várakozási időket nem gyorsítótárral, hanem a végrehajtóegységek átütemezésével próbálja elrejteni. Ha azonban nincs végrehajtandó művelet, akkor ezt nem tudja megoldani – A CUDA környezetben megvalósított algoritmusok jóságának összehasonlítására emiatt gyakran használatos az aritmetikai műveletek száma/memóriahozzáférések száma hányados
• Megoldási lehetőségek – Párhuzamosság növelése, amennyiben a feladat ezt lehetővé teszi (jelen esetben erre nem látunk lehetőséget) – Memóriahozzáférések számának csökkentése (tulajdonképpen programozott gyorsítótár kezelés) • minél több változó regiszterekben tartása • megosztott memóriaterület használata
– Egyéb megoldások keresése, esetenként az algoritmusok áttervezése verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
14
Ötlet a mátrix szorzás gyorsítására • Csempe technika – Mivel a bemenő mátrixok egyes elemei több kimenő elem számításához is szükségesek, így a nem optimalizált változatban több szál is betölti őket egymástól függetlenül – Célszerű lenne ezen szálak működését összehangolni: • a teljes kimenő mátrixot régiókra bontjuk (csempe) • a régióknak megfelelő méretű megosztott memóriaterületet lefoglalunk a két bemenő mátrix elemeinek ideiglenes betöltéséhez • a régiókban található minden szál betölti az ő poziciójának megfelelő elemet a két bemenő mátrixból a megosztott memóriába • a szál a saját és a többi szál által betöltött adatok segítségével kiszámítja az eredmény mátrix rá eső értékét
– A megosztott memória mérete meglehetősen korlátos, emiatt elképzelhető, hogy a fenti műveletet csak több lépésben, a bemenő mátrixokat több csempével lefedve, majd ezek értékét összeadva lehet végrehajtani – Ez utóbbi esetben lényeges a szinkronizálás is, hogy egyik szál se kezdje el betölteni a következő csempe adatait, miközben a vele egy régióban lévő szálak még dolgoznak a részeredményeken verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
15
Mátrix szorzó alkalmazás elkészítése • Feladat 3.3
– – – – – – – – –
verzió
Készítsünk CUDA alkalmazást, amelyik kétdimenziós (NxN méretű, egyszeres lebegőpontos számokat tartalmazó) mátrixok szorzását tudja elvégezni a GPU segítségével: A kód tartalmazzon egy N konstanst, ami a mátrixok méretét tartalmazza Foglaljon le 3 darab NxN méretű mátrixot (A, B, C) Töltse fel az A mátrixot tetszőleges adatokkal (pl. ai,j = i + j) Töltse fel a B mátrixot tetszőleges adatokkal (pl. bi,j = i - j) Foglaljon le 3 darab NxN méretű memóriaterületet a grafikus kártya memóriájában (devA, devB, devC) Végezze el a bemenő adatok másolását: A → devA, B → devB Indítson el egy kernelt, ami kiszámítja a devC = devA * devB eredményt, az előzőleg megismert csempe technika segítségével Végezze el a kimenő adatok másolását: devC → C Írja ki az így kapott C vektor elemeinek értékét a képernyőre
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
16
Mátrix szorzás gyorsítása As Bs
1.
2.
3. verzió
c
B
Csempékre bontás 3x3 db régió, ezekben 3x3 db szál Minden szál átmásol A egy-egy elemet a megosztott memóriába Szinkronizáció
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
C
Globális memória 17
Mátrix szorzás gyorsítása As Szál0,0
Bs
4.
5.
verzió
c
B
Minden szál a saját részeredményét kiszámolja a megosztott memória A tartalma alapján Szinkronizáció
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
C
Globális memória 18
Mátrix szorzás gyorsítása As Szál0,0
Bs
6. 7. 8.
9. verzió
+
c
B
Következő csempék adatainak betöltése Szinkronizálás Szálak újra elvégzik A a szorzást. A szorzatot hozzáadják az előző részeredményhez Szinkronizálás
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
C
Globális memória 19
Mátrix szorzás gyorsítása As Szál0,0
Bs
6. 7. 8.
9. verzió
+
c
B
Következő csempék adatainak betöltése Szinkronizálás Szálak újra elvégzik A a szorzást. A szorzatot hozzáadják az előző részeredményhez Szinkronizálás
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
C
Globális memória 20
Mátrix szorzás gyorsítása As Bs
c
B
10. Minden szál az általa kiszámított elemet bemásolja a C-be 11. Az összes blokk és A szál lefutása után az eredmény mátrix minden eleme a helyére kerül verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
C
Globális memória 21
Optimalizált mátrix szorzás kernel • A kernel meghívása és az adatok másolása megegyezik az első mátrix szorzásnál megismerttel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 verzió
__global__ static void MatrixMul(float *devA, float *devB, float *devC) { __shared__ float Ashared[BlockN][BlockN]; __shared__ float Bshared[BlockN][BlockN]; int indx = blockIdx.x * blockDim.x + threadIdx.x; int indy = blockIdx.y * blockDim.y + threadIdx.y; float c = 0; for(int k = 0; k < N / BlockN; k++) { Ashared[threadIdx.y][threadIdx.x] = devA[k * BlockN + threadIdx.x + indy * N]; Bshared[threadIdx.y][threadIdx.x] = devB[indx + (k * BlockN + threadIdx.y) * N]; __syncthreads(); for(int i = 0; i < BlockN; i++) { c += Ashared[threadIdx.y][i] * Bshared[i][threadIdx.x]; } __syncthreads();
}
2010.07.20.
} devC[indx + indy * N] = c;
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
22
Összehasonlító vizsgálat • Eredeti és optimalizált kód összehasonlítása – Vízszintes tengely: mátrix mérete (N) – Függőleges tengely: futási idő (másodperc) 1 0,9 0,8 0,7
0,6 0,5
Eredeti Optimalizált
0,4 0,3 0,2 0,1 0
40
verzió
2010.07.20.
80
120
160
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
200
23
CUDA haladó ismeretek CUDA környezet részletei Többdimenziós indextér használata Megosztott memória használata Atomi műveletek használata
Optimalizálás Hatékonyság és mérése Megfelelő blokkméret kiválasztása
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
Atomi műveletek szükségessége • Atomiság igénye – Az adatpárhuzamos feladatok készítésekor ideális esetben a végeredmény kiszámításához nincs szükség az indextér egymástól független pontjain végzett műveletekre (pl. két mátrix összeadása), esetleg csak az olvasásra van szükség (pl. két mátrix szorzása) – Bizonyos feladatoknál azonban ez nem kerülhető el, mint például a gyakori aggregációs műveleteknél: • • • •
adatszerkezet elemeinek az összege/átlaga adatszerkezet legkisebb/legnagyobb eleme adatszerkezetben megadott tulajdonságú elemek száma stb.
• Lehetséges megoldások – Ezen feladatok egy része egyszerűen megoldható adatpárhuzamos környezetben is (pl. van-e T tulajdonságú elem) – A feladatok egy része azonban csak (részben) szekvenciális megoldással oldható meg (pl. legkisebb elem megkeresése) verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
25
CUDA atomi műveletek • CUDA lehetőségek – A CUDA által nyújtott atomi műveletek segítenek kiküszöbölni a versenyhelyzet okozta problémákat. A hardver garantálja, hogy az így elvégzett műveletek a szálak számától függetlenül szekvenciálisan fognak végrehajtódni (vagy legalábbis úgy tűnnek) – Operandusok lehetnek • Változó a globális memóriában • Változó a megosztott memóriában
– Operandusok mérete • 32 bites egész (1.1 számítási képesség) • 64 bites egész (1.2 számítási képesség)
• Teljesítmény megfontolások – Az atomi műveletek jellegüknél fogva nem párhuzamosíthatók, ennek megfelelően jelentősen rontják minden kernel teljesítményét – Bizonyos feladatoknál azonban nem lehet őket teljesen kiküszöbölni, azonban ilyenkor is célszerű esetleg előzetes párhuzamos feldolgozásokkal csökkenteni a szükséges atomi műveletek számát verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
26
CUDA atomi műveletek • Aritmetikai függvények
–
–
–
–
verzió
Az atomi műveletek egyik paramétere általában egy memóriacím (globális vagy megosztott memóriában), második paramétere pedig egy egész szám int atomicAdd(int* address, int val) Az első paraméterként címszerint átadott változó értékéhez hozzáadja a második paraméterben átadott értéket. Visszatérési értéke az eredeti szám int atomicSub(int* address, int val) Az első paraméterként címszerint átadott változó értékéből kivonja a második paraméterben átadott értéket. Visszatérési értéke az eredeti szám int atomicExch(int* address, int val); Az első paraméterként címszerint átadott változónak értékül adja a második paraméterként átadott értéket. Visszatérési értéke az eredeti szám int atomicMin(int* address, int val); Ha a második paraméterként átadott szám értéke kisebb mint az első címszerinti paraméterként átadott változó értéke, akkor ez utóbbit felülírja az előbbivel. Visszatérési értéke az eredeti szám
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
27
CUDA atomi műveletek • Aritmetikai függvények folyt. – int atomicMax(int* address, int val); Ha a második paraméterként átadott szám értéke nagyobb, mint az első címszerinti paraméterként átadott változó értéke, akkor ez utóbbit felülírja az előbbivel. Visszatérési értéke az eredeti szám – unsigned int atomicInc(unsigned int* address, unsigned int val) Ha az első paraméterként címszerint átadott változó értéke kisebb, mint a második paraméter, akkor az előbbi értékét növeli 1-el, különben 0-t ad neki értékül. Visszatérési értéke az eredeti szám – unsigned int atomicDec(unsigned int* address, unsigned int val) Ha az első paraméterként címszerint átadott változó értéke nagyobb, mint 0, akkor az előbbi értékét növeli 1-el, különben a második paraméterben átadott értéket másolja bele. Visszatérési értéke az eredeti szám – int atomicCAS(int* address, int compare, int val) Ha az elő paraméterként címszerint átadott változó értéke egyenlő a második paraméterrel, akkor az előbbinek értékül adja a harmadik paraméter értékét. Visszatérési értéke az eredeti érték verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
28
CUDA atomi műveletek • Logikai függvények – int atomicAnd(int* address, int val) Az első paraméterként címszerint átadott változó értékéül adja a második változóval vett ÉS művelet eredményét. Visszatérési értéke az eredeti szám – int atomicOr(int* address, int val) Az első paraméterként címszerint átadott változó értékéül adja a második változóval vett VAGY művelet eredményét. Visszatérési értéke az eredeti szám – int atomicXor(int* address, int val) Az első paraméterként címszerint átadott változó értékéül adja a második változóval vett KIZÁRÓVAGY művelet eredményét. Visszatérési értéke az eredeti szám
• Feladat 3.4 Készítsünk CUDA alkalmazást, amelyik egy (hoszt oldalon) véletlenszerű számokkal feltöltött vektorból kiválasztja a minimális értéket. A feladat megoldása során használja a megismert atomi műveleteket
verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
29
Vektor legkisebb elemének értéke • Megoldás a globális memória használatával – Az alap megoldás meglehetősen egyszerű, minden szál az általa indexelt elemre és a vektor első elemére meghívja a minimumot értékül adó függvényt – A kernel lefutását követően tehát a vektor legelső eleme tartalmazza a legkisebb elem értékét 1 2 3 4
__global__ static void MinSearch(float *devA) { int indx = blockIdx.x * blockDim.x + threadIdx.x; atomicMin(devA, devA[indx]); }
– Érdemes megfigyelni, hogy a kernel eleve több blokkos indításra lett tervezve, tehát az atomi műveletek nem csak blokkon belül, hanem blokkok között is biztosítják a versenyhelyzetek megoldását
• Feladat 3.5 Sejthető, hogy a globális memóriában végzett atomi műveletek meglehetősen lassúak, ezért próbáljunk egy olyan változatot, amelyik először a blokk minimumát keresi meg, majd ezzel számol globális minimumot verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
30
Memóriahozzáférések csökkentése • Megosztott memória használata – A blokkon belül az első szál feladata, hogy beállítsa a lokális minimumot tároló változó kezdőértékét – Ezt követően az összes szál elvégzi a hozzá rendelt értékkel a minimum ellenőrzést – Végül az első szál az így kiszámított lokális minimummal elvégzi a globális minimum ellenőrzést is – Szintén ügyeljünk a szinkronizációs pontokra, nehogy rossz sorrendben fussanak le az utasítások az egyes szálakban 1 2 3 4 5 6 7 8 9 verzió
2010.07.20.
__global__ static void MinSearch(float *devA) { __shared__ int localMin; int indx = blockIdx.x * blockDim.x + threadIdx.x; if (threadIdx.x == 0) localMin = devA[blockIdx.x * blockDim.x]; __syncthreads(); atomicMin(&localMin, devA[indx]); __syncthreads(); if (threadIdx.x == 0) atomicMin(devA, localMin); } © Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
31
Összehasonlító vizsgálat • Eredeti és optimalizált kód összehasonlítása – Vízszintes tengely: vektor mérete (N) – Függőleges tengely: futási idő (másodperc) 10 9 8 7
6 5
Eredeti Optimalizált
4 3 2 1 0
5000
verzió
2010.07.20.
10000
20000
30000
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
40000
32
Blokkon belüli párhuzamosítás • Atomi műveletek teljesítménye – Célszerű lenne a megoldást olyan formában keresni, hogy kihasználhassuk a párhuzamosságból eredő előnyöket is: blokkon belül is kisebb részfeladatokra kell bontani a minimum kiválasztást
• Párhuzamosított változat – A megosztott memóriába betöltjük a globális memória egy szeletét: minden szál beolvassa a globális memória két elemét, majd ezek közül a kisebbiket eltárolja a megosztott memóriában – Blokkon belül minden szál összehasonlít két elemet a megosztott memóriában, majd a kisebbiket eltárolja az alacsonyabb indexű helyen – Következő iterációban már csak az előzőleg kisebbnek talált elemeket hasonlítják össze, így a vizsgálandó elemek száma mindig feleződik – Miután megvan a blokk legkisebb eleme, az előzőleg megismert módon eldöntjük, hogy ezek közül melyik a globális minimum
• Feladat 3.6 Készítsük el a minimum kiválasztás fent leírt algoritmussal működő változatát verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
33
Párhuzamos minimum - betöltés
1 2
Min( A2 , A3 ) – Sz1
Blokk 0
Min( A4 , A5 ) – Sz2 Min( A6 , A7 ) – Sz3 Min( A8 , A9 ) – Sz0 Min( A10 , A11 ) – Sz1 Min( A12 , A13 ) – Sz2 Min( A14 , A15 ) – Sz3
4 5 6 7 8
9 10 11 12
Min( A16 , A17 ) – Sz0 Min( A18 , A19 ) – Sz1
Blokk 1
Min( A20 , A21 ) – Sz2 Min( A22 , A23 ) – Sz3 A24 – Sz0 A0 – Sz1 A0 – Sz2
• páratlanul maradt elem • vektor bármelyik eleme
3
A0 – Sz3
13
14 15 16 17
Globális memória
Min( A0 , A1 ) – Sz0
Megosztott memória
– Egy példa N = 24 BlokkN = 4 (szálak száma) – Minden blokk lefoglal BlokkN*2 méretű megosztott memóriát – Minden blokk szálai BlokkN*4 méretű globális memóriából töltik be páronként a kisebb értékeket – A blokkméret legyen 2 hatványa, így az üres helyeket feltöltjük:
0
18 19 20 21
– Betöltés után szinkronizáció
22 23 24
verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
34
Párhuzamos minimum – blokk minimuma S0
S1
S2
S3
S4
S5
S6
S7
Min(S0,S1)
Min(S2,S3)
Min(S4,S5)
Min(S6,S7)
S4
S5
S6
S7
Min(S0,S1, S2, S3)
Min(S4,S5, S6, S7)
Min(S4,S5)
Min(S6,S7)
S4
S5
S6
S7
Blokk minimum
Min(S4,S5, S6, S7)
Min(S4,S5)
Min(S6,S7)
S4
S5
S6
S7
– Minden szál log2BlokkN darab iterációt hajt végre, amely során az x indexű szál elvégzi az alábbi műveletet: Sx = Min(S2x , S2x+1) – A műveletek végén a vektor legelső eleme tartalmazza a blokk által feldolgozott elemek közüli legkisebb értékét – Ezek közül kiválaszhatjuk a globális legkisebb elemet: • atomi műveletek használatával • a blokkok által kigyűjtött minimális elemeket egy másik vektorban gyűjtjük, majd ezekre ismét lefuttatjuk a fenti minimumkiválasztást (nagy elemszám esetén már érdemes lehet ezt a változatot használni) verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
35
Párhuzamos minimum - kernel • Adatok betöltése globális memóriából (túlcímzés figyeléssel) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 verzió
__global__ static void MinSearch(int *devA) { __shared__ int localMin[BlockN*2]; int blockSize = BlockN; int itemc1 = threadIdx.x * 2; int itemc2 = threadIdx.x * 2 + 1;
2010.07.20.
for(int k = 0; k <= 1; k++) { int blockStart = blockIdx.x * blockDim.x * 4 + k * blockDim.x * 2; int loadIndx = threadIdx.x + blockDim.x * k; if (blockStart + itemc2 < N) { int value1 = devA[blockStart + itemc1]; int value2 = devA[blockStart + itemc2]; localMin[loadIndx] = value1 < value2 ? value1 : value2; } else if (blockStart + itemc1 < N) localMin[loadIndx] = devA[blockStart + itemc1]; else localMin[loadIndx] = devA[0]; } __syncthreads(); © Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
36
Párhuzamos minimum - kernel • Lokális, majd globális minimum kiválasztása 21 22 23 24 25 26 27 28 29 }
while (blockSize > 0) { int locMin = localMin[itemc1] < localMin[itemc2] ? localMin[itemc1] : localMin[itemc2]; __syncthreads(); localMin[threadIdx.x] = locMin; __syncthreads(); blockSize = blockSize / 2; } if (threadIdx.x == 0) atomicMin(devA, localMin[0]);
• Megjegyzések – A blokkméret mindenképpen 2 hatványa legyen – A k ciklus szerepe, hogy minden blokk kétszer futtassa le a globális memóriából megosztott memóriába való másolást. Ennek következtében a BlokkN darab szálat tartalmazó blokk 4*BlokkN elemet tölt be a globális memóriából, ebből 2*BlokkN elemet (páronként a kisebbet) ment el a megosztott memóriába, így a fenti ciklus első iterációjában mind a BlokkN darab szál össze tud hasonlítani egy párt verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
37
Összehasonlító vizsgálat • Első és második optimalizált kód összehasonlítása – Vízszintes tengely: vektor mérete (N) – Függőleges tengely: futási idő (másodperc) 0,3
0,25
0,2
0,15
Optimalizált 1 Optimalizált 2
0,1
0,05
0
5000
verzió
2010.07.20.
10000
20000
30000
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
40000
38
Összehasonlító vizsgálat • Egyszerű CPU – GPU kód összehasonlítása – Vízszintes tengely: vektor mérete (N) – Függőleges tengely: futási idő (másodperc) 0,6
0,5
0,4
0,3
CPU Optimalizált 2
0,2
0,1
0 10000
50000
100000
150000
200000
a mérés során nem vettük figyelembe a bemenő adatok átmásolását a grafikus kártya memóriájába! verzió
2010.07.20.
© Szénási Sándor, Óbudai Egyetem, 2010 [email protected]
39
Irodalomjegyzék címsora • [1] David B. Kirk, Wen-mei W. Hwu: Programming Massively Parallel Processors Elsevier 978-0-12-381472-2 http://www.elsevierdirect.com/morgan_kaufmann/kirk/ Angol 258 o.
• [2] NVIDIA CUDA Programming Guide 3.0 CUDA környezet teljes leírása (referencia, mintapéldák) http://developer.download.nvidia.com/compute/cuda/3_0/toolkit/docs/N VIDIA_CUDA_ProgrammingGuide.pdf
verzió
dátum/idő
© szerző, intézmény, évszám em@il
40