• Nem Talált Eredményt

2. Az OpenCL API eszközei

7.9. példa - context.c

2.4.2. Futtató réteg

A futtató réteg célja egy adott környezet objektumhoz kapcsolódóan az OpenCL eszközön történő végrehajtások szervezése.

Parancssor. A környezet objektum létrehozását követően minden OpenCL-t használó programban létre kell hozni a végrehajtás ütemezéséhez szükséges parancssor objektumot. A parancssor lényegében egy sor adatszerkezet, amelyen keresztül feladatokat adhatunk egy OpenCL eszköznek, létrehozására a clCreateCommandQueue függvényt használhatjuk.

Specifikáció:

cl_command_queue clCreateCommandQueue(

cl_context context,

cl_device_id device,

cl_command_queue_properties properties, cl_int* errcode_ret);

Paraméterek: context - Egy környezet objektum.

device - Egy olyan eszköz azonosítója, amelyet a context környezet létrehozásakor megadtunk.

properties - Egy bitmező, amellyel a parancssor tulajdonságait adhatjuk meg, az egyik legfontosabb ezek közül a

CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE: a parancssorba bekerülő parancsok tetszőleges sorrendben ütemezhetők.

errcode_ret - Hiba esetén ezen címre kerül a hibakód.

Visszatérési érték: Sikeres végrehajtás esetén egy új parancssor objektum, ellenkező esetben beállításra kerül a hibakód.

A létrehozott parancssor objektumok tulajdonságait a clGetCommandQueueInfo függvénnyel kérdezhetjük le, ennek paraméterezése és használatának módja teljesen megegyezik a clGetPlatformInfo és clGetDeviceInfo függvényekével, azonban használata a gyakorlatban kevésbé fontos, ezért nem is tárgyaljuk bővebben, a lekérdezhető tulajdonságok típusait és a kapcsolódó nevesített konstansokat az OpenCL specifikációban találja az olvasó. Parancssor objektumok által lefoglalt erőforrásokat a clReleaseCommandQueue függvénnyel szabadíthatjuk fel, használata és visszatérési értéke megegyezik a korábban bemutatott clReleaseContext függvényével.

Egy környezethez tetszőleges számú parancssort létrehozhatunk, és ezekkel egyszerre ütemezhetünk egymástól független feladatokat. Ekkor fontos, hogy a feladatok függetlenek legyenek egymástól, ha ez nem teljesül, a megfelelő szinkronizációról a programozónak kell gondoskodnia.

Memóriaműveletek és esemény objektumok. A legtöbb programban elkerülhetetlen, hogy memóriaműveleteket végezzünk az OpenCL eszközön, mivel a párhuzamos végrehajtás során többnyire adatokon szeretnénk dolgozni, és az OpenCL eszközök számítási egységei direkt módon nem érik el a gazdagép memóriáját. Ha memóriaterületeket szeretnénk lefoglalni az OpenCL eszközök globális vagy konstans memóriájában, azt mindig egy környezetben tehetjük meg, nem pedig a platformok vagy eszközök direkt megadásával. Ennek okaira és részleteire hamarosan visszatérünk. Az alábbiakban négy memóriaművelet (foglalás, felszabadítás, írás, olvasás) függvényeit tekintjük át, majd egy példaprogramon keresztül szemléltetjük azok használatát.

Az OpenCL eszközökön lefoglalt memóriaterületeket un. puffer objektumok segítségével kezeljük. Puffer objektumokat a clCreateBuffer függvény segítségével hozhatunk létre, ezen függvény végzi a megfelelő méretű és tulajdonságokkal rendelkező memóriaterületek lefoglalását a globális memóriában.

7.4. táblázat - A puffer objektumok fontosabb tulajdonságai.

Konstans neve Leírás

CL_MEM_READ_WRITE A memória objektum minden kernel által írható és olvasható (alapértelmezés).

CL_MEM_WRITE_ONLY A kernelek a memóriaobjektumot írhatják, de nem olvashatják.

CL_MEM_READ_ONLY A kernelek a memóriaobjektumot olvashatják, de nem írhatják.

Specifikáció:

cl_mem clCreateBuffer( cl_context context, cl_mem_flags flags, size_t size,

void* host_ptr, cl_int*

errcode_ret);

Paraméterek: context - Egy környezet objektum azonosítója.

flags - A lefoglalandó memóriaterület tulajdonságai (bitmező), lehetséges értékeit a 7.4. táblázatban foglaljuk össze.

size - A lefoglalandó memóriaterület mérete bájtokban.

host_ptr - A bufferbe feltöltendő adatok mutatója a gazdagép címterében.

errcode_ret - Ezen címre kerül beállításra a hibakód, ha a végrehajtás sikertelen, NULL érték esetén nem kerül hibakód visszaadásra.

Visszatérési érték: Sikeres végrehajtás esetén egy memóriaobjektum,

ellenkező esetben beállításra kerül a hibakód.

A clCreateBuffer függvény paraméterezése magától értetődő, a host_ptr paraméter és a nem ismertetett flag-ek a puffer objektumok speciális használatához kapcsolódnak, ezek tárgyalásától azonban eltekintünk, így a továbbiakban a NULL mutatót adjuk át negyedik paraméterként. Memóriaobjektumok létrehozása során fontos, hogy megfelelő módon állítsuk be a memóriaterület tulajdonságait (írási és olvasási jogokat), mert így a fordító és futtató rendszer optimalizálhatja azok elérését. Kiemeljük azonban, hogy a memória modell tárgyalásánál említett konstans memória nem azonos a CL_MEM_READ_ONLY tulajdonsággal lefoglalt memóriaterületekkel. A konstans memória használatára az OpenCL C nyelv tárgyalásánál térünk visszatérünk.

Pufferek írását és olvasását a clEnqueueReadBuffer és clEnqueueWriteBuffer függvényekkel kezdeményezhetjük. Ahogy a nevükből is kitalálható, mindkét függvény egy adott parancssorban helyezi el (,,besorozza'', enqueue) az olvasási vagy írási feladatot. Az olvasás természetesen arra utal, hogy az OpenCL eszköz memóriájában lévő adatokat másoljuk a gazdagép memóriájába, míg az írási feladat során a gazdagép memóriájából másolunk az OpenCL eszköz memóriájába. A függvények paraméterezése teljesen megegyezik, ezért csak a clEnqueueReadBuffer függvényét mutatjuk be, természetesen ahol ,,olvasási feladat''-ról beszélünk a specifikációban, ott clEnqueueWriteBuffer esetén ,,írási feladat'' értendő.

Specifikáció:

cl_int clEnqueueReadBuffer(

cl_command_queue command_queue,

cl_mem buffer, cl_bool

blocking_read,

size_t offset, size_t size, void* ptr, cl_uint num_events_in_wait_list,

const cl_event* event_wait_list,

cl_event*

event);

Paraméterek: command_queue - Ezen parancssorba kerül az olvasási feladat.

buffer - Az olvasandó memória objektum.

blocking_read - Logikai paraméter: igaz értéke esetén az olvasási feladat befejeződéséig a függvény blokkolja a program futását.

offset - Az olvasás a buffer objektum kezdetétől számított offset bájttól kezdődik.

size - Az olvasott memóriaterület mérete.

ptr - A gazdagép egy memóriaterületének címe, az olvasás eredménye itt kerül tárolásra.

num_events_in_wait_list - Az event_wait_list tömbben átadott esemény objektumok száma.

event_wait_list - Esemény objektumok tömbje num_events_in_wait_list méretű tömbje.

event - Ezen címre kerül beírásra az olvasási feladat befejeződését jelző esemény objektum.

Visszatérési érték: Sikeres végrehajtás esetén CL_SUCCESS, ellenkező esetben hibakód.

A clCreateBuffer és clEnqueueReadBuffer függvények paraméterezésének vizsgálatakor az olvasó érdekes dolgot vehet észre: a clCreateBuffer egy környezetet vár paraméterként, míg a clEnqueueReadBuffer egy parancssort, amely egy környezeten belüli eszköz feladatainak ütemezéséhez használható. Joggal merülhet fel a kérdés, hogy ha több OpenCL eszközt tartalmaz a környezet, melyik fizikai eszközön jön létre a clCreateBuffer függvény által létrehozott memóriaobjektum? Honnan tudjuk, hogy melyik eszközhöz tartozó

parancssort használjuk az írási vagy olvasási feladatok kiadásakor? A válasz az OpenCL magas absztrakciós szintjében rejlik. A gyakorlatban nincs szükségünk rá, hogy tudjuk, melyik fizikai eszközön jött létre a memóriaobjektum, egészen addig, amíg ugyanazon környezethez létrehozott parancssorokon keresztül kezeljük azt. Egy környezeten belül több különböző eszközhöz létrehozott parancssorral is használhatjuk ugyanazt a memóriaobjektumot: az OpenCL könyvtár nyilvántartja, hogy melyik eszközön találhatóak az adataink, így azok mozgatását szükség esetén optimalizáltan valósítja meg. A háttérben egy puffer létrehozásakor abban sem lehetünk biztosak, hogy a megfelelő memóriaterület egyáltalán lefoglalásra került a függvény visszatérésének pillanatáig, de ezzel valójában nem is kell foglalkoznunk, az OpenCL elfedi előlünk. A lefoglalt memória objektumokat a korábbiakhoz hasonló clReleaseMemObject függvénnyel szabadíthatjuk fel, mely paraméterként a puffer objektum azonosítóját várja.

A clEnqueueReadBuffer függvény paraméterei között találunk egy un. eseménylistát, valamint egy cl_event típusú mutatót, amely címre egy esemény objektumot ír a függvény. A clEnqueueReadBuffer esemény paraméterek nélkül is használható, az eseményeket azonban felhasználhatjuk szinkronizációs célra: a tényleges írási/olvasási művelet nem fog megtörténni egészen addig, amíg az események paraméterként kapott tömbjében minden esemény objektumhoz kapcsolódó feladat be nem fejeződött. A függvény által visszaadott esemény objektumot hasonlóan használhatjuk fel más műveletek esetén annak ellenőrzésére, hogy a clEnqueueReadBuffer függvénnyel kiadott olvasási feladat befejeződött-e6 már.

Az esemény objektumok létrehozására és kezelésére számos függvényt specifikál az OpenCL szabvány, ezek közül csak a clWaitForEvents függvénnyel ismerkedünk meg, amely blokkolja a gazdagépen futó program futását, amíg a paraméterként kapott események mindegyike be nem fejeződik.

Specifikáció:

cl_int clWaitForEvents( cl_uint num_events, const cl_event*

event_list);

Paraméterek: num_events - A event_list tömb elemeinek száma.

event_list - Események num_events méretű tömbje.

Visszatérési érték: Sikeres végrehajtás esetén CL_SUCCESS, ellenkező esetben beállításra kerül a hibakód.

Az OpenCL szabvány függvényeinek áttekintését két olyan függvénnyel folytatjuk, amelyek az egyes parancssorokba helyezett feladatok végrehajtását kényszerítik ki, ezzel egyfajta szinkronizációs lehetőséget teremtve a gazdaprogramban. A clFlush függvény a paraméterként kapott parancssorhoz rendelt minden feladatot elküld a megfelelő eszköznek. Ekkor a feladatok már az eszközön és nem a gazdaprogramban létrehozott parancssorban várakoznak, ha az OpenCL eszköz éppen foglalt, vagy a feladatok megkezdését más feladatok befejeződéséhez kötöttük. Minden blokkoló függvény (pl. clEnqueueReadBuffer, stb.

automatikusan meghívja a clFlush függvényt a megfelelő parancssorra.

Abban az esetben, ha egy A és B parancssor ugyanazon OpenCL eszközt vezérli és feladatokat helyezünk az A parancssorba, úgy, hogy azok szinkronizációs célból egy B parancssorba helyezett feladat végrehajtásától függenek, biztosítanunk kell, hogy a B parancssorba helyezett feladatok már az OpenCL eszközön várakozzanak, azaz vagy meg kell hívnunk explicit módon a clFlush függvényt, vagy a B parancssoron valamilyen blokkoló clEnqueue* függvényt kell használnunk. Ezen ,,kötelezettség'' első olvasásra furcsának tűnhet, azonban a következő, holtponthoz vezető szituációt kerülhetjük el vele: tegyük fel, hogy elhelyezünk egy T1 feladatot az A parancssorban, majd egy ennek végrehajtásától függő T2 feladatot a B parancssorban. Az ütemezés vagy esetleg párhuzamos végrehajtás miatt a B parancssor hamarabb éri el az eszközt, mint az A parancssor, és elküldi a T2 feladatot, majd csak ezt követően küldi az A parancssor a T1 feladatot az eszközre.

Ekkor a T2 feladat végrehajtása nem indulhat meg, amíg a T1 feladat be nem fejeződött, azonban a T1 az OpenCL eszközön csak a T2 után szerepel, így annak végrehajtása nem kezdődik meg, holtpont alakul ki.

Specifikáció:

cl_int clFlush( cl_command_queue command_queue);

cl_int clFinish( cl_command_queue

6Mivel az esemény objektumok egy-egy feladathoz (például memória írás, olvasás, a későbbiekben kernel végrehajtás) kapcsolódnak, beszélhetünk arról, hogy egy esemény ,,befejeződik'', s ez alatt az eseményhez kapcsolódó feladat befejeződését értjük.

command_queue);

Paraméterek: command_queue - Egy parancssor objektum.

Visszatérési érték: Sikeres végrehajtás esetén CL_SUCCESS, ellenkező esetben beállításra kerül a hibakód.

A clFlush függvényhez hasonlóan működik a clFinish függvény, azonban a clFinish blokkolja a program futását, amíg a paraméterként kapott parancssorhoz rendelt összes feladat be nem fejeződik.

A parancssorokat és memória objektumokat kezelő függvények használatát az alábbi példaprogrammal szemléltetjük.