22. tétel - Végrehajtás a grafikus kártyán
Miben különbözik a CPU és GPU futtatási rendszere, hogyan lehet a GPU-n szálakat indítani és paraméterezni? Ismertesse a blokk fogalmát (használata, szinkronizáció, shared memory)!
- Alapvetően a CPU-k soros feldolgozásra összpontosítanak, összetett utasításkészlettel rendelkeznek
- A GPU-k párhuzamos feldolgozásra vannak optimalizálva, utasításkészletük specifikusabb, szűkebb
Maga a CUDA Runtime 3 komponenscsoportot biztosít az alap C nyelvi elemeken felül:
| Általános komponensek | Hoszt komponensek | Eszköz komponensek |
|---|---|---|
| A GPU és CPU kódban is használhatóak. | Csak a CPU kódban használhatóak (memóriafoglalás, GPU-ra másolás, kernel irányítás, stb.). | Csak a GPU kernel kódjában használhatóak. |
- A GPU-n futó kód a számítógép legtöbb komponenséhez nem fér hozzá (pl. háttértár, fájlkezelés.)
- A kivételkezelés is más, hiszen a hoszt oldali kivételektől eltérően semmiféle visszajelzés nem jön ezekről.
- Fontos a függvények visszatérési értékét ellenőrizni, hogy nem történt-e hiba a GPU-s végrehajtás során
Kernel
- A szálak indításához kernelre van szükség
- A kernel lényegében egy C függvény, meghívásakor a GPU szálain végrehajtásra kerül
- Szintaktikailag azonos, de el kell látni speciális kulcsszavakkal
- pl:
__global__: a belépési pontot jelöli, meghívható a hosztról
- pl:
- A kernelen belül bizonyos hoszt funkciók nem érhetőek el, mint pl. fájlkezelés
- A kernel indításához speciális szintaktikával kell meghívni a kernel függvényt
- blokkok és szálak számát meg kell adni
- lehet átadni paramétereket, de minden szál ugyanazokat kapja meg
- A kernelen belül lekérhető az adot szál indexe
- vagy indexei, ha több dimenziós blokkban van**
- Van szinkronizáló eljárás
- amikor egy szál eljut egy ilyen híváshoz, várni fog addig, míg azt a többi szál is eléri pontosan ugyanazt az eljárást
- ezzel könnyű holtpontot okozni (pl. elágazásnál)
- amikor egy szál eljut egy ilyen híváshoz, várni fog addig, míg azt a többi szál is eléri pontosan ugyanazt az eljárást
Blokkok
- a szálak blokkokba vannak rendezve
- a GPU a blokkokat függetlenül kezeli
- nem lehet köztük szinkronizálni, hatékonyan adatot megosztani
- néhány nagyon modern GPU-nál lehetséges a blokkok közötti szinkronizáció, de csak erős fizikai korlátok között (pl. egyszerre kell futniuk)
- az egy blokkon belül futtatott szálak száma korlátozott (ma tipikusan 1024)
- ha ennél több szál kell, akkor több blokk is kell
- egy GPU több streaming multiprocesszort tartalmaz
- egy blokk csak egy ilyenen futhat
- könnyen lehet, hogy a blokkok nem egyszerre futnak le hardveres korlátok miatt
A blokkok összessége a grid: ezen belül a blokkoknak szintén van indexe.
- többdimenzióban is lehet a blokkokat is indítani, mint a szálakat
- így egy szálnak van globális + lokális azonosítója is
A szükséges blokkok száma (\(BM\) = blokkméret, pl. 1024):
\[\Bigl\lfloor\frac{szálak-1}{BM}\Bigr\rfloor+1\]
Blokkok közötti szinkronizáció alternatív megoldása
- Kritikus pontnál kettébontjuk a kernelt
- A kettő új kernelt külön kell meghívni
- A szinkronizáció a CPU-nál történik
- lehet addig blokkolni a CPU-t, ameddig minden munka befejeződik a GPU-n
- a CPU blokkolása nem hasznos, de ez a megoldás, ha nagy szükség van a szinkronizációra
Shared memória
- A GPU 3 féle memóriájának (device/global, shared, szálak saját változói) egyike
- Hardver oldalról: a streaming multiprocesszor belső memóriája
- Programozói oldalról: minden blokknak saját shared memóriája van
- Adatok beolvasása innen sokkal gyorsabb, mint a globális memóriából
- A mérete azonban sokkal kisebb is, kb 48kB
- Az egy blokkon belüli szálak látják, ezen keresztül tudnak hatékonyan kommunikálni
- speciális kulcsszó kell hozzá (
__shared__) - Előnyös használni:
- Memóriahozzáférések csökkentésére (ha minden szálnak ugyanaz az adat kell, érdemes ide másolni)
- Szálak egymás közti kommunikációjára
- Nem előnyös: ha minden szál más adathoz akar hozzáférni, azokhoz is kevésszer