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 komponensekHoszt komponensekEszkö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
  • 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)

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

  1. Kritikus pontnál kettébontjuk a kernelt
  2. A kettő új kernelt külön kell meghívni
  3. 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