Korábban említésre került, hogy a GPU-n található osztott és globális memória is. Ezek a CUDA modell szerint a következőképp oszlanak meg:
Ebből látható, hogy az egy blokkba tartozó szálak a blokk osztott memóriáján keresztül kommunikálhatnak egymással, míg a blokkok a globális memórián keresztül tehetik meg ugyanezt.
Egyéb modellek
A CUDA modellen kívül egyéb környezetek is rendelkezésre állnak, melyek a GPU programozására lettek megalkotva. Az egyik ilyen az OpenCL, mely teljes mértékben nyílt forráskodú eszközt ad azok kezébe, akik alkalmazni szeretnék a GPU-k nagy számítási kapacitását.
A másik platform a Microsoft DirectX 11-es verziójában megtalálható DirectCompute, melynek szerepe szintén a GPU-k erejének alkalmazása az általános feladatok körében. A leglényegesebb különbség ezen két eszköz és a CUDA modell között, hogy csak ezzel a kettővel lehet elérni az egyéb gyártótól származó GPU-kat is.
Nyelvi elemek
A CUDA nyelv lényegében a C/C++ nyelv kiegészítése és bizonyos szempontból szűkítése is, bár a kártyák és a technológia fejlődésével mára a C++ nyelv legnagyobb része használható CUDA kódban is. A kiegészítés lényegében a minősítőkből és jó pár saját típusból áll.
<<< , >>> operátor
A kernel függvényeket
függvénynév <<< gridDim, blockDim >>> (fügvény_paraméterek)
alakban kell meghívni
Függvény minősítők
- __global__ : a kernel függvény, vagyis a GPU-n futó kód, a gazda kódból hívható
- __device__ : GPU-n futó függvény kódja, csak GPU kódból hívható
- __host__ : gazda kód, csak gazda kódból hívható, melyre csak akkor van szükség, ha gazda kódot írnánk CUDA forrásfájlba
- __noinline__, __forceinline__ : jelzés a fordítónak, hogy az adott függvényt inline vagy nem inline módon szeretnénk fordítani
Változó minősítők
- __device__ : a változó az eszköz memóriában jön létre (eszköz kódban minden változó az eszköz memóriában jön létre)
- __constant__ : konstans memóriában foglalt változóinak
- __restrict__ : pointerekre vonatkozó megkötés, mely jelzi a fordító számára, hogy nincs kereszthivatkozás az egyes pointerek között, ezért nyugodtan betölthetőek regiszterekbe
- __shared__ : ez jelzi a GPU kódban ha valamelyen adatnak a blokkon belüli osztott memóriában a helye
Típusok
- beépített vektor típusok: charN, shortN, intN, longN, longlongN, floatN, doubleN, ahol N lehet 2, 3 és 4 eleműek és elemeiket elemeiket az x,y,z,w tagokkal érhetjük el
- dim3: három elemű uint típus, mely a nem megadott elemeit 1-re inicializálja
Beépített változók
- gridDim: dim3 típusú, a grid dimenzióját adja meg
- blockDim: dim3 típusú, a blokk dimenzióját adja meg
- blockIdx: uint3 típusú, a futó blokk számát adja meg
- threadIdx: uint3 típusú, a futó szál számát adja meg
- warpSize: int típusú és a warp méretét adja meg
Szinkronizációs függvények
- __syncthreads() : biztosítja, hogy az elvégzett műveletek eredményei láthatóvá válnak a többi szál számára is, valamit megvárja, hogy a többi szál is eljusson erre a pontra, ha a végrehajtás nem minden szálon volt egyforma hosszú
- __syncthreads_count(int predicate), __syncthreads_and(int predicate), __syncthreads_or(int predicate) : ugyanaz mint fent, de a predicate alapján kiértékeli a szálakat és megadja a számukat, vagy pedig össze-éseli vagy össze-vagyolja a kiértékelés eredményét
Atomikus függvények
Olyan függvények melyek egy utásítás alatt olvasnak ki egy adatot, végeznek rajta egy műveletet majd teszik vissza a kiolvasott elem helyére. Az elérhető műveleteket: összeadás, kivonás, csere, minimum, maximum, inkrementálás, dekrementálás, összehasonlítás-és-csere, és, vagy, kizáró-vagy. Ezen műveletek nagy része csak egész típusokra elérhető. Lebegőpontos típusra csak a csere és az összeadás működik.
Más nyelvek
A CUDA platformhoz számos programozási nyelven léteznek kötések, melyek legtöbbje továbbra is a CUDA fordítót használja a tényleges GPU kód fordításához.
Fordítás
A CUDA SDK az nvcc saját fordítóval érkezik, mind Windows mind Linux operációs rendszerekre. Ez az egy fordító végzi a gazda kódok fordítását is. Működése a következő: lefordítja a GPU kódot majd a CPU kódba a <<< , >>> helyére behelyettesíti a megfelelő CUDA hívásokat. A CPU kódot a továbbiakban pedig átadja az adott platformon alapértelmezett fordítónak. Ez Windows rendszereken a Visual C++ fordító, míg Linux rendszereken a GCC.
A linkelés során két lehetőség is van. A GPU kód vagy egy könyvtárként valósul meg és bekerül az alkalmazásba, vagy pedig úgynevezett PTX kód keletkezik belőle, melyet a GPU meghajtó képes fordít le gépi kódra futás előtt. Utóbbi előnye, hogy a kód újrafordítás nélkül, még nem létező eszközökön is futhasson a jövőben.
A fordítóból létezik 32bit-es és 64bit-es változat is. Mindkettő képes 32bit-es és 64bit-es kódot is fordítani a GPU, de 32bit-es GPU kód csak 32bit-es CPU kóddal együtt, 64bit-es GPU kód pedig csak 64bit-es CPU kóddal futtatható együtt.
Kompatibilitás
CUDA programozásnál többféle kompatibilitást is figyelembe kell venni. A leglényegesebb a hardver számítási kompatibilitása (compute compatibility). Ez a grafikus hardver verziószáma, mely azt írja le, hogy a C/C++ mely nyelvi elemeit ismeri fel és a hardver mely utasítás készlet futtatására képes. A kártyák fejlődése során számos kezdetben nem létező funkcionalitás került a nyelvbe és a fordítóba is, hogy a hardver növekedő teljesítményét és kapacitását minél inkább ki lehessen használni.
A teljesség igénye nélkül néhány példa:
- atomikus egész műveletek, később lebegőpontos műveletek
- dupla pontosságú lebegőpontos számítás
- C++ osztályok kezelése, template-ek kezelése
- indítható szálak és blokkok mennyiségének növelése
A C++ támogatás kérdése természetesen csak a GPU-s kódrészletekre vonatkozik, a CPU-n futó kódot ugyanis az adott platformhoz tartozó C++ fordító kezeli. A számítási kompatibilitást a fordítás során meg lehet adni, mely így a nem támogatott elemekre vagy hibát jelez, vagy pedig kevésbé optimalizáltabb gépi kódot fordít, hogy a régebbi eszközön is lefusson a program.
Memória
A CUDA rendszer több memóriát különböztet meg, melyek kapacitás szerint csökkenő, de gyorsaság szerint növekvő sorrendben a következők:
- rendszer memória: az alaplapon található a teljes rendszer számára elérhető memória
- eszköz memória: a videokártyán található memória, elsősorban az GPU kód számára elérhető memória
- osztott memória: a GPU-n indított szálblokkok számára elérhető közös memória
- regiszterek: szálak változóinak fenntartott memória
A rendszer ezen kívül támogat még különböző címzési eljárásokat is melyek heterogenizálják a kódot, és gyorsíthatják a memória műveleteket egyes esetekben:
Lap-zárolt rendszermemória
Ez a rendszer memória olyan elérését teszi lehetővé a program számára, melyet az operációs rendszer nem helyezhet át lapozással. Ez lehetővé teszi, hogy a program GPU-n való futtatással párhuzamosan is tudjon másolni az eszköz memóriájába. Lehetővé teszi, hogy a rendszer memóriájának címtere az eszközről is elérhető legyen, ezáltal elkerülve a másolást.
A GPU-k teljesítménye
A GPU-k maximális teljesítményüket a parallel alkalmazások futtatásakor képesek elérni, ezért is fontos kérdés, hogy mennyire lehet egy algoritmust párhuzamosítani.
Amdahl törvénye
Meg kell tudnunk mondani, hogy érdemes-e egy probléma párhuzamosításával foglalkozni. Erre a kérdésre ad választ Amdahl törvénye, mely egy becslést ad arra vonatkozóan, hogy maximum mekkora sebességnövekedésre számíthatunk. Amdahl törvénye:
S=1/((1-P)+(P/N)), ahol:
- S a várható sebességnövekedés,
- P a párhuzamosítható kód teljes szekvenciális végrehajtási idejének tört része,
- N a processzorok száma, amelyen a párhuzamos kód fut.
Például elég nagy N-et választva az egyenlet a következőképpen alakul:
S=1/(1-P)
Ha a program 3/4 része párhuzamosítható, akkor az iménti egyenletből azt kapjuk, hogy 4-szeres teljesítménynövekedés érhető el.
Multi-GPU rendszerek
További teljesítménynövekedés érhető el, ha az NVIDIA SLI (Scalable Link Interface) technológiának köszönhetően több GPU-t is összekötünk egyetlen rendszeren belül, jelenleg maximálisan négy darabot.
Fejlesztési követelmények
A fejlesztés során fontos, hogy vegyük figyelembe a GPU architektúra főbb jellemzőit. A kártya globális memóriája akár 100-szor is lassabb lehet bármely más, a GPU-n található memóriánál, ami nagy mértékben lecsökkenti a műveletvégzés sebességét. Ennek érdekében fontos, hogy kerüljük a sűrű másolási műveleteket a host (CPU) és a device (GPU) memóriája között. Hatékony adatmozgatáshoz törekedjünk egyszerre nagyobb méretű blokkok áthelyezésével csökkenteni a szükséges adatküldések számát. A GPU-n átlagosan nagyobb sebességgel értékelődnek ki a függvények, mint amilyen gyorsan meg tudnánk határozni, hogy pontosan hány darab folyamatra van ténylegesen szükség az adott probléma kiszámításához. Pontosan ezért a fel nem használt folyamatoktól eltekintünk.
Hardveres megszorítások
Figyelnünk kell arra, hogy az architektúra fejlődésével újabb és újabb lehetőségek nyílnak meg a fejlesztők előtt, hogy összetettebb és hatékonyabb programot készíthessenek. Az architektúra specifikus tényezők azonban mindig az újabb GPU-khoz lesznek kötve. Így bizonyos funkciók elérhetetlenek lesznek a régebbi GPU-k számára. A újabb típusok ismerete, mint például a dupla precíziós számok ábrázolása olyan dolog, amit manuálisan a programozó nehezen pótolhat, ám bizonyos függvények és eljárások kisebb-nagyobb munkával, de reprodukálhatóak a régebbi architektúra megkötései ellenére is. Figyelembe kell venni, hogy milyen felhasználói bázisba szánjuk az alkalmazást, hogy tudjuk milyen hangsúlyt kell fektetnünk ezekre a megkötésekre.
A számítási kompatibilitás mellett érdemes figyelni az adott hardver kapacitására is. Különböző memória méretek miatt, indítható blokkok és szálak száma. Az egy időben végrehajtott warp-ok száma. Ezeket nagyon könnyen megtudhatjuk a beépített eszköz lekérdező függvények segítségével. Ha hordozható kódot szeretnénk akkor érdemes ezeket az adatokat még a szálak méretezése előtt lekérni és ez alapján beosztani őket.
A VRAM mennyisége pedig akkor jelent korlátot, hogy nagy mennyiségű adatot szeretnénk feldogozni, ugyanis a bemeneti értékeknek és az eredménynek is el kell férnie a memóriában, ezért kevés memória esetén sok másolgatás miatt veszthetünk a teljesítményből. Szerencsére a technológia fejlődésével már aszinkron memóriamásolásra is van lehetőség, mely jelentősen gyorsíthat az ilyen eseteken.
Aszinkron futtatási lehetőségek
Lényegében minden hívás amit a rendszer intéz a GPU felé (memória másolás, kernel hívás) aszinkron módon fut le a rendszer számára, vagyis a meghívott függvény azonnal visszatér. Ám a grafikus hardveren természetesen ezek az utasítások várnak egymásra. Ez alól kivétel a debug és profiling eszközök használata, mely során minden kernel hívások szinkronban futnak. A eszközök fejlődése tette azt lehetővé, hogy egyes esetekben a memória műveletek és a kernel futások valóban párhuzamosan vagy egymáshoz képest aszinkron módon fussanak. Egyes eszközöknél arra is lehetőség van, hogy a párhuzamos működést stream-eken keresztül felügyeljük a CPU kódból. Ezeket a lehetőségeket szintén a számítás kompatibilitás dönti el, és minden ilyen tulajdonság lekérdezhető a kódban az eszközről.