Hogyan Tervezzünk GPU Chipet
Chapter 2 Gpu Rogramming Models

2. fejezet: GPU programozási modellek

A Grafikus Feldolgozó Egységek (GPU-k) fejlődése során a fix funkcionalitású grafikus gyorsítóktól eljutottak a nagyon párhuzamos, programozható számítási motorokig, melyek képesek a különböző alkalmazások széles körének gyorsítására. A GPU-k masszív párhuzamosságának hatékony kiaknázása érdekében számos párhuzamos programozási modell és API fejlődött ki, mint például az NVIDIA CUDA, az OpenCL és a DirectCompute. Ezek a programozási modellek absztrakciós rétegeket biztosítanak, amelyek lehetővé teszik a programozók számára, hogy kifejezhessék az alkalmazásaikban rejlő párhuzamosságot, miközben elrejtik a GPU hardver alacsony szintű részleteit.

Ebben a fejezetben megismerkedünk a GPU-k számára készült párhuzamos programozási modellek mögötti kulcsfontosságú koncepciókkal és elvekkel, a végrehajtási modellre, a GPU utasításkészlet architektúrákra (ISA-k), az NVIDIA GPU ISA-ra és az AMD Graphics Core Next (GCN) ISA-ra összpontosítva. Példákat is bemutatunk, amelyek illusztrálják, hogyan alkalmazzák ezeket a koncepciókat a gyakorlatban.

Végrehajtási modell

A modern GPU programozási modellek végrehajtási modellje a kernelekre épül, amelyek olyan függvények, amelyeket párhuzamosan hajt végre a GPU-n lévő nagyszámú szál. Egy kernel indításakor a programozó megadja a létrehozandó szálak számát és azok hierarchikus elrendezését rácsokba, tömbökbe (vagy kooperatív szálcsoportokba - CTA) és egyedi szálakba.

  • Egy rács képviseli a teljes problémateret és egy vagy több tömbből áll.
  • Egy tömb a szálak egy csoportja, amelyek együttműködhetnek és szinkronizálhatják magukat megosztott memória és korlátok segítségével. A tömbön belüli szálakat ugyanazon a GPU magban (streaming multiprocessor vagy compute unit) hajtják végre.

A szálaknak egyedi azonosítójuk van a tömbön és a rácsön belül, amely felhasználható a memóriacímek kiszámításához és az irányítási folyamat döntéseihez.

Ez a hierarchikus szervezés lehetővé teszi a programozók számára, hogy mind az adatszintű párhuzamosságot (amikor ugyanazt a műveletet több adatelemre alkalmazzák), mind a feladatszintű párhuzamosságot (amikor különböző feladatok hajtódnak végre párhuzamosan) kifejezhessék.

ÁbraKérem, itt van a fájl magyar fordítása. A kódnál nem fordítottam le a kódot, csak a megjegyzéseket.

            Rács
       ________________
      /   /   /   /   /
     /   /   /   /   / 
    /   /   /   /   /
   /   /   /   /   /
  /__/__/__/__/__/
    |   |   |   |
    |   | Blokk |
    |   |   |   |
  Szál Szál ...

2.1 ábra: Szálhierarchia a GPU végrehajtási modellben.

SIMT végrehajtás

A CUDA és OpenCL GPU programozási modellek Egyetlen Utasítás, Több Szál (SIMT) végrehajtási modellt követnek. A SIMT modellben a szálak csoportokban, ún. warp-okban (NVIDIA terminológia) vagy wavefront-okban (AMD terminológia) hajtódnak végre. A warp minden szála ugyanazt az utasítást hajtja végre egyszerre, de mindegyik szál más adaton dolgozik.

Azonban, eltérően a hagyományos Egyetlen Utasítás, Több Adat (SIMD) modeltől, ahol minden feldolgozási elem zárt sorban működik, a SIMT lehetővé teszi, hogy a szálak független végrehajtási útvonalakkal rendelkezzenek és elágazásoknál eltérő utakat válasszanak. Amikor egy warp elágazási utasításba ütközik, a GPU hardver kiértékeli az elágazási feltételt minden warp-ban lévő szálra. Ha minden szál ugyanazt az utat választja (konvergált), a warp normálisan folytatja a végrehajtást. Ha néhány szál más utat választ (divergált), a warp két vagy több alwarp-ra oszlik, mindegyik egy másik útvonalat követve. A GPU hardver szekvenciálisan hajtja végre a divergens utakat, kikapcsolva a nem aktív szálakat mindegyik alwarp-ban. Amikor minden út befejeződik, az alwarp-ok újra konvergálnak és zárt sorban folytatják a végrehajtást.

A 2.2 ábra bemutatja a SIMT végrehajtást divergens vezérlési folyammal.

         Warp
   ________________
  /   /   /   /   /
 /   /   /   /   / 
/   /   /   /   /
   |   |   |
   | Elágazás |
   |   |   |
  / \ / \ / \
 /   X   \   \
/   / \   \   \
   /   \   \
  /     \   \
 /       \   \
/         \   \
           \
            \
             \
   Újrakonvergencia

2.2 ábra: SIMT végrehajtás divergens vezérlési folyammal.

Ez a divergencia kezelési mechanizmus lehetővé teszi, hogy a SIMT rugalmasabban támogassa a vezérlési folyamatokat, mint a hagyományos SIMD modell.Kérjük, itt van a fájl magyar fordítása. A kódnál ne fordítsd le a kódot, csak a megjegyzéseket fordítsd le.

HAN SIMD, de ez a SIMD hatékonyság csökkenésének költségével jár, amikor divergencia történik. A programozóknak törekedniük kell a divergencia minimalizálására egy warp-on belül az optimális teljesítmény elérése érdekében.

Memória-hierarchia

A GPU-k egy összetett memória-hierarchiával rendelkeznek, hogy támogassák a párhuzamos munkafeladatok magas sávszélességű és alacsony késleltetésű követelményeit. A memória-hierarchia általában a következőket tartalmazza:

  • Globális memória: A legnagyobb, de leglassúbb memóriatér, amely minden szál számára elérhető egy kernelben. A globális memória általában nagy sávszélességű GDDR vagy HBM memóriával van megvalósítva.
  • Megosztott memória: Egy gyors, lapkára integrált memóriatér, amelyet egy blokk összes szála megoszt. A megosztott memóriát a szálak közötti kommunikációra és az adatok megosztására használják egy blokkon belül.
  • Állandó memória: Egy csak olvasható memóriatér, amelyet az összes szál számára sugárzott, csak olvasható adatok tárolására használnak.
  • Textúra memória: Egy csak olvasható memóriatér, amely a térbeli lokalitásra van optimalizálva, és textúra gyorsítótárakon keresztül érhető el. A textúra memóriát általában grafikai munkafeladatokban használják.
  • Helyi memória: Egy privát memóriatér minden szál számára, amely a regiszter kihelyezésére és a nagy adatszerkezetekre szolgál. A helyi memória általában a globális memóriára van leképezve.

A memória-hierarchia hatékony kihasználása kulcsfontosságú a GPU-k magas teljesítményének eléréséhez. A programozóknak arra kell törekedniük, hogy maximalizálják a megosztott memória használatát, és minimalizálják a globális memóriához való hozzáféréseket a memória-késleltetés és sávszélesség-szűk keresztmetszetek csökkentése érdekében.

A 2.3. ábra a GPU memória-hierarchiáját mutatja.

      ____________
     |            |
     |   Globális  |
     |   Memória   |
      ____________
           |
      ____________
     |            |
     |  Állandó    |
     |   Memória   |
      ____________
           |
      ____________
     |            |
     |   Textúra   |
     |   Memória   |
      ____________
           |
           |
      ____________
     |            |
     |  Megosztott |
     |   Memória   |
      ____________
           |
      ____________ 
     |            |
     |   Helyi     |
     |   Memória   |
      ____________

ÁbraKérjük, adja meg a következő markdown fájl magyar fordítását. A kód esetében ne fordítsa le a kódot, csak a megjegyzéseket fordítsa le. Itt van a fájl: ure 2.3: GPU memória hierarchia.

GPU utasításkészlet-architektúrák

A GPU utasításkészlet-architektúrák (ISA-k) meghatározzák a szoftver és a hardver közötti alacsony szintű interfészt. Meghatározzák a GPU által támogatott utasításokat, regisztereket és memória-címzési módokat. A GPU ISA-k megértése létfontosságú a hatékony GPU-kód fejlesztéséhez és a teljesítmény optimalizálásához.

Ebben a részben két vezető GPU-gyártó, az NVIDIA és az AMD ISA-it fogjuk feltárni. Az NVIDIA Parallel Thread Execution (PTX) és SASS ISA-ira, valamint az AMD Graphics Core Next (GCN) ISA-jára fogunk összpontosítani.

NVIDIA GPU ISA-k

Az NVIDIA GPU-k két szintű ISA-t támogatnak: PTX (Parallel Thread Execution) és SASS (Streaming ASSembler). A PTX egy virtuális ISA, amely stabil célpontot biztosít a CUDA fordítók számára, míg a SASS az NVIDIA GPU-k natív ISA-ja.

PTX (Parallel Thread Execution)

A PTX egy alacsony szintű, virtuális ISA, amelyet az NVIDIA GPU-khoz terveztek. Hasonló az LLVM IR-hez vagy a Java byte-kódhoz abban, hogy egy stabil, architektúra-független célpontot biztosít a fordítók számára. A CUDA programokat általában PTX kódba fordítják, amelyet aztán az NVIDIA GPU-illesztőprogram natív SASS utasításokká alakít.

A PTX széles körű aritmetikai, memória- és vezérlési utasításokat támogat. Korlátlan számú virtuális regisztert és predikációt támogat, ami lehetővé teszi a vezérlési folyamatok hatékony végrehajtását. A PTX speciális utasításokat is biztosít a szál-szinkronizációhoz, az atomos műveletekhez és a textúra mintavételezéshez.

Itt egy példa egy egyszerű vektor-összeadó kernel PTX kódjára:

.version 7.0
.target sm_70
.address_size 64

.visible .entry vecAdd(
    .param .u64 vecAdd_param_0,
    .param .u64 vecAdd_param_1,
    .param .u64 vecAdd_param_2,
    .param .u32 vecAdd_param_3
)
{
    .reg .b32 %r<4>;
    .reg .b64 %rd<6>;

    ld.param.u64 %rd1, [vecAdd_param_0];
    ld.param.u64 %rd2, [vecAdd_param_1];
    ld.param.u64 %rd3, [vecAdd_param_2];
    ld.param.u32 %r1, [vecAdd_param_3];
    cvta.to.global.u64 %rd4, %rd1;
    cvta
```Itt látható a magyar fordítás a kódhoz:

.to.global.u64 %rd5, %rd2; mov.u32 %r2, %tid.x; mul.wide.u32 %rd6, %r2, 4; add.s64 %rd7, %rd4, %rd6; add.s64 %rd8, %rd5, %rd6;

ld.global.f32 %f1, [%rd7]; ld.global.f32 %f2, [%rd8]; add.f32 %f3, %f1, %f2;

cvta.to.global.u64 %rd9, %rd3; add.s64 %rd10, %rd9, %rd6; st.global.f32 [%rd10], %f3;

ret; }


Magyar fordítás a kommentárokhoz:

.to.global.u64 %rd5, %rd2; mov.u32 %r2, %tid.x; mul.wide.u32 %rd6, %r2, 4; add.s64 %rd7, %rd4, %rd6; add.s64 %rd8, %rd5, %rd6;

// Globális memóriából olvasás az f1 és f2 lebegőpontos változókba ld.global.f32 %f1, [%rd7]; ld.global.f32 %f2, [%rd8]; // Az f1 és f2 értékek összeadása, az eredmény az f3 lebegőpontos változóba add.f32 %f3, %f1, %f2;

cvta.to.global.u64 %rd9, %rd3; add.s64 %rd10, %rd9, %rd6; // A globális memóriába írás az f3 értékkel st.global.f32 [%rd10], %f3;

// Visszatérés a függvényből ret; }


A kód egy vektorösszeadó kernel függvényt definiál, amely négy paramétert kap: a bemeneti és kimeneti vektorok címeit, valamint a vektorok méretét. A kernel kiszámítja a globális szálon belüli azonosítót, betölti a megfelelő elemeket a bemeneti vektorokból, végrehajtja az összeadást, és eltárolja az eredményt a kimeneti vektorban.

A SASS (Streaming ASSembler) a NVIDIA GPU-k natív ISA-ja, amely közvetlenül leképeződik a GPU hardverre. A SASS-utasítások a NVIDIA GPU-illesztőprogram által generálódnak a PTX-kódból, és általában nem láthatók a programozók számára.

Az AMD Graphics Core Next (GCN) architektúra és ISA a AMD GPU-kat használja. A GCN egy RISC-alapú ISA, amely támogatja mind a grafikai, mind a számítási feladatokat. Magas teljesítményt, skálázhatóságot és energiahatékonyságot céloz meg.Itt a magyar fordítás a megadott markdown fájlhoz. A kódban csak a megjegyzéseket fordítottam le, a kódot nem.

alar ALU hatékony skaláris műveletek és vezérlési áramlás végrehajtásához.
- Vektor ALU párhuzamos, adatpárhuzamos műveletek végrehajtásához.
- Nagy sávszélességű memóriarendszer, amely támogatja az atomi műveleteket és az alacsony késleltetésű hozzáférést a megosztott memóriához.
- Rugalmas címzési mód a memóriaműveletekhez, amely támogatja az alap+eltolás és a skaláris+vektor címzést.

Itt egy példa a GCN ISA kódra egy vektor-összeadási kernel esetén:

```asm
.text
.globl vecAdd
.p2align 2

.type vecAdd,@function
vecAdd:
    .set DPTR, 0

    s_load_dwordx4 s[0:3], s[4:5], 0x0 # Argumentumok betöltése a memóriából
    s_load_dword s4, s[4:5], 0x10
    s_waitcnt lgkmcnt(0)

    v_lshlrev_b32 v0, 2, v0 # Indexek eltolása
    v_add_u32 v1, vcc, s1, v0 # Első vektor címének kiszámítása
    v_mov_b32 v3, s3
    v_addc_u32 v2, vcc, s2, v3, vcc # Második vektor címének kiszámítása
    flat_load_dword v1, v[1:2] # Első vektor betöltése

    v_add_u32 v3, vcc, s0, v0 # Harmadik vektor címének kiszámítása
    v_mov_b32 v5, s3
    v_addc_u32 v4, vcc, s2, v5, vcc # Negyedik vektor címének kiszámítása
    flat_load_dword v0, v[3:4] # Második vektor betöltése

    v_add_f32 v0, v0, v1 # Vektor-összeadás
    flat_store_dword v[3:4], v0 # Eredmény tárolása
    s_endpgm

Ez a GCN kód betölti a bemeneti vektorelemeket flat_load_dword használatával, végrehajtja az összeadást v_add_f32 segítségével, és eltárolja az eredményt a memóriában flat_store_dword használatával. Az s_load_dwordx4 és s_load_dword utasításokat a kernel argumentumok memóriából való betöltésére használják.

Algoritmusok leképezése GPU architektúrákra

Az algoritmusok hatékony leképezése a GPU architektúrára kulcsfontosságú a magas teljesítmény eléréséhez. Fontos szempontok:

Elegendő párhuzamosság biztosítása

Az algoritmust sok, finom szemcsés szálra kell bontani, hogy azok párhuzamosan tudjanak futni, és kihasználják a GPU párhuzamos feldolgozási képességeit. Ez gyakran azt jelenti, hogy meg kell találni az algoritmus adatpárhuzamos részeit, amelyek függetlenül, különböző adatelemeken hajthatók végre.

Elágazás-divergencia minimalizálása

A warp/wavefront-on belüli divergens vezérlési áramlás szerializációhoz és csökkent SIMD-hatékonysághoz vezethet. Az algoritmusokat úgy kell kialakítani, hogy minimalizálják az elágazás-divergenciát, amennyire lehetséges. Ezt úgy lehet elérni, hogy csökkentjük az adattól függő elágazások használatát### A memória-hierarchia kiaknázása

A globális memória elérése költséges. Az algoritmusoknak maximalizálniuk kell a megosztott memória és a regiszterek használatát, hogy csökkentsék a globális memória-hozzáféréseket. Az adatokat is úgy kell elrendezni a memóriában, hogy lehetővé tegyék a koaleszkált memória-hozzáféréseket, ahol a warp-on belüli szálak összefüggő memóriahelyekhez férnek hozzá. A memória-hierarchia hatékony használata jelentősen csökkentheti a memória-késleltetést és a sávszélesség-szűk keresztmetszeteket.

A számítás és a memória-hozzáférések kiegyensúlyozása

Az algoritmusoknak magas arányt kell mutatniuk az aritmetikai műveletek és a memória-műveletek között, hogy hatékonyan elrejthessék a memória-késleltetést, és magas számítási teljesítményt érjenek el. Ezt úgy lehet elérni, hogy maximalizáljuk az adatok újrafelhasználását, előre beolvassuk az adatokat, és átfedésbe hozzuk a számítást a memória-hozzáférésekkel.

A gazdagép-eszköz adatátvitel minimalizálása

Az adatok gazdagép (CPU) és eszköz (GPU) memóriája közötti átvitele lassú. Az algoritmusoknak minimalizálniuk kell ezeket az átviteleket, és a lehető legtöbb számítást a GPU-n kell végezniük. Az adatokat nagy adagokban kell átvinni a GPU-ra, és a lehető legtovább ott kell tartani, hogy kompenzálják az átvitel költségeit.

A GPU-kerneleket fejlesztésénél általánosan használt párhuzamos algoritmus-tervezési minták:

  • Leképezés: Minden szál ugyanazt a műveletet végzi egy különböző adatelemen, lehetővé téve a nagy adathalmazok egyszerű párhuzamos feldolgozását.

  • Csökkentés: A párhuzamos csökkentést arra használják, hogy hatékonyan kiszámítsanak egyetlen értéket (pl. összeg, maximum) egy nagy bemeneti adathalmazból. A szálak helyi csökkentéseket végeznek, amelyeket aztán egyesítenek a végső eredmény előállításához.

  • Scan: Más néven előtagösszeg, a scan arra szolgál, hogy kiszámolja az elemek futó összegét egy tömbben. A hatékony párhuzamos scan-algoritmusok kulcsfontosságú építőelemek sok GPU-gyorsított alkalmazáshoz.

  • Sablonszerűség: Minden szál egy érték kiszámítását végzi a szomszédos adatelemek alapján. A sablonszerű számítások gyakoriak a tudományos szimulációkban és a képfeldolgozási alkalmazásokban.Itt a fájl magyar fordítása:

  • Összegyűjtés/Szétszórás: A szálak tetszőleges helyekről olvasnak (összegyűjtés) vagy írnak (szétszórás) a globális memóriába. A hatékonyság érdekében gondos adatelrendezésre és hozzáférési mintákra van szükség.

A 3.20. ábra egy példát mutat a map módszerre, ahol minden szál egy függvényt (pl. négyzetgyök) alkalmaz a bemeneti tömb egy különböző elemére.

Bemeneti tömb:  
               |  |   |   |   |   |   |   |
               v  v   v   v   v   v   v   v
              ______________________________
Szálak:       |    |    |    |    |    |    |    |
             |____|____|____|____|____|____|____|
                |    |    |    |    |    |    |
                v    v    v    v    v    v    v
Kimeneti tömb: 

3.20. ábra: A map minta példája GPU programozásban.

Következtetés

A CUDA és az OpenCL, mint GPU programozási modellek, hozzáférést biztosítanak a modern GPU-k párhuzamos feldolgozási képességeihez a fejlesztők számára, lehetővé téve széles körű alkalmazások gyorsítását. Ezek a programozási modellek olyan absztrakciót nyújtanak, amely lehetővé teszi a finoman szemcsézett párhuzamos feladatok hatékony leképezését a GPU hardverre.

A végrehajtási modell, a memóriahierarchia és a szinkronizációs primitívák megértése elengedhetetlen a nagy teljesítményű GPU-kód írásához. A fejlesztőknek gondosan kell figyelembe venniük olyan tényezőket, mint a szálak szervezése, az elágazás divergenciája, a memória-hozzáférési minták és az algoritmus tervezése, hogy teljes mértékben kihasználhassák a GPU-k számításiteljesítmény-képességeit.

Ahogy a GPU-architektúrák tovább fejlődnek, a programozási modellek és eszközök is fejlődniük kell, hogy lehetővé tegyék a fejlesztők számára, hogy hatékonyan hasznosítsák az új hardveres funkciókat és képességeket. A programozási nyelvtervezés, a fordítóprogram-optimalizálás és az automatikus hangolás terén folyó kutatások elengedhetetlenek lesznek a programozói termelékenység és a teljesítményhordozhatóság javításához a heterogén számítástechnika korszakában.