Sziasztok!
Leírok pár gondolatot - mert nagyon bugyognak bennem - és hátha van valami jó ötletetek hozzájuk.
Szóval azon gondolkozok, hogy hogyan kéne a SIMD-s vektor utasításokra és a GPUs nagyon mély hierarchiájú párhuzamos végrehajtókra jól kódot generálni, és nem akar összeállni a dolog (nyilván nem triviális).
Illetve az a rész triviális, ha teljesen független feladatok vannak, amin ugyan azt a műveletsort kell végigcsinálni, és a szálak között nincs adatfüggőség / kommunikáció, akkor az triviálisan párhuzamosítható (vagy szekvencializálható, ha visszafelé kell menni). De a többi eset nagyon nehéz.
Az egyik ötlet: hasonlít-e ez a dolog a regiszter allokációra? Pl. az ember azt mondhatja, hogy tegyük fel, hogy van végtelen sok szálunk, és megpróbáljuk azokra szétosztani a feladatot (először lebontjuk skalárokra), majd ezeket megpróbáljuk egymásmellé gyűjteni, hogy vektorutasítások legyenek belőlük, illetve ha kevesebb szál van, mint ami kell, akkor vissza-szekvencializálunk. Működhet-e ez? (elvileg a Csaba által említett ISPC v mi, Inteles compiler hasonlókat csinál messziről nézve, asszem).
Viszont az adatkommunikációknál és a többszintű esetben nem tudom mi van. A GPU-s tájkép alulról nézve a következő durván nézve:
- alul vannak a 128 bit-es SIMD regiszteres műveletek, ez kb. ua mint a CPU-kon.
- ezeken felül vannak a szálak 32 vagy 64-es csoportokban (warp NVIDIA-n, wavefront AMD-n, etc). Itt vannak olyan műveletek, amelyek a warp szálak között skalár regiszter méretű (32-64 bit) adatokat tudnak átvenni (shuffle szerű), ami nagyon gyors és hatékony. Időnként még a divergens esetek is kezelhetőek + van maszkolás, ha nem kell minden szál az adott művelethez. Ezekből úgy fest egyre több lesz.
- a warpok/szálak felett van a rendes "nagy" szálcsoport, ami kb. 256-1024 szálat tartalmaz, tehát jónéhány warpot ölel fel. Ez a legnagyobb egység, amin belül a szálak még szinkronizálhatóak az élettartamuk alatt, illetve van egy dedikált gyors memóriája (shared/local memory), ami néhány 10k byte méretű és csak ez a csoport látja.
- a szálcsoportok felett van a teljes számítási rács, ami ki lett küldve a kártyára, amelyből azonban nem biztos h egyszerre mindegyik fut, így azok között nincs szinkronizáció, csak a kernelhívás vége, és csak a globális, lassú memórián (VRAM)
keresztül kommunikálnak.
- aztán ugye lehet még több kártya is egy gépben, ahol az eszközök között gyorsabb lehet a kommunikáció, mint a RAM felé.
Szóval erre elég macerás átképezni számításokat, kérdés, hogy mit érdemes csinálni. Néhány elképzelés:
- mivel a fordító mindig limitált lesz, a programozónak kell tudnia expliciten leírnia ilyesmiket, viszont ha mindenhova át kell adogatni ilyen szálcsoportokkal kapcsolatos paraméterket, az elcsúfítja a kódokat (pl. ha user defined operátort akar az ember, akkor nem is "fér el" az ilyen info).
- node vannak ezek az implicit argumentumok, és lehetne ezt kicsit abuzálni, és lehetne absztraktul azt mondani, hogy egy adott függvényt a programozó megírhat néhány különböző granularitásban, pl. 4-32-256-os szálmérettel, és ezek közül választhat a fordító, amikor oda jut, hogy na ezt az adott függvényt éppen milyen környezetbe kell beágyazni és ott valójában mennyi szál van. Mert párhuzamos kódból visszatérni szekvencializált kódra valszeg könnyebb, mint fordítva. És ez a paraméter olyasmi lenne, ami fordítási időben eldől, így az ehhez kapcsolódó esetszétválasztás teljesen eltűnhet fordításkor. Cserébe viszont lehet, hogy sok kód duplikációval jár, amit nem akarok de még nem látom át ez hogy nézne ki jobban.
- lehetne a szálcsoportosdinak egy absztrakt modellje, ahol alapvetően az absztrakt szálak között scatter, gather és shuffle műveletek vannak, meg szinkronizáció, meg ilyesmik, mert úgyfest az minden ilyen környezetben van nagyjából.
- nem tudom minek kéne előbb eldöntődnie: a párhuzamosításnak, vagy a memória layoutnak, valószínűleg mindkét irányban kéne tudnia működni a megszorításnak és alkalmazkodásnak, mert mindkettőre vannak use-casek.
Egy másik érdekes aspektus: mennyire lehet tudni, hogy honnan jön a párhuzamosítási lehetőség? Nekem valahogy azaz intuícióm, hogy mindig abból indulunk ki, hogy valamiből sok van, és azok ugyan azt akarjuk csinálni, és ennek magas szintről kéne látszódnia, tehát felülről lefelé.
Ugyanakkor meg az clang/llvm vektorizációjánál olvastam, hogy abban van olyan pass, ami nézegeti előre hátra N utasításig, hogy hol vannak összevonható skalár műveletek, amikből vektor műveletet lehet csinálni. Ez meg alulról felfelé megy. Szerintetek ez is olyan, hogy mindkettőre szükség lehet, vagy a clangos csak kényszermegoldás, mert nincs elég felülről jövő információja?
Egyelőre kb. itt tartok ezügyben...
üdv,
D.