Skoči na vsebino

Ustvarjanje OpenCL okolja na gostitelju1

Spoznali smo že, da se vsak program, ki ga želimo zagnati na heterogenem sistemu z GPE, sestoji iz dveh delov:

  • programske kode, ki se izvaja na gostitelju in
  • ščepcev, ki se vzporedno izvajajo na napravi GPE.

Sedaj bomo najprej spoznali, kaj je naloga programa, ki se izvaja na gostitelju. Program na gostitelju se kliče iz funkcije main() in je zadolžen za naslednje korake:

  1. ustvarjanje okolja OpenCL,
  2. prevajanje ščepcev OpenCL,
  3. deklaracijo in inicializacijo vseh spremenljivk,
  4. prenos podatkov na napravo,
  5. zaganjanje ščepcev in
  6. prenos podatkov iz naprave.

Program na gostitelju torej zagotovi vse kar je potrebno, da se ščepec zažene na napravi. Program na gostitelju bomo pisali v jeziku C in pri tem uporabljali določene funkcije OpenCL API (ang. Application Programming Interface). Ta program se izvaja zaporedno na centralni procesni enoti gostitelja in v sebi ne vsebuje nobenega paralelizma. Podroben opis za OpenCL API najdemo na spletni strani The OpenCL Specification.

Okolje OpenCL

Prva naloga, ki jo mora opraviti program, ki se bo izvajall na gostitelju, je ustvarjanje OpenCL okolja. Okolje OpenCL je abstrakcija platforme, naprav, konteksta ter ukaznih vrst. Vse te pojme bomo opisali in definirali v nadaljevanju. Tukaj le na kratko vpeljimo:

  • Platforma je v bistvu programski gonilnik za konkretne naprave, ki jih bomo uproabljali (na primer Nvidia).
  • Naprava je konkretna nparava znotrja platforme (na primer Nvidia Tesla K40).
  • Kontekst bo vseboval program za posamezno napravo ter njene pomnilniške objekte (prostor v globalnem pomnilniiku v katerem bomo hranili podatke) in ukazne vrste.
  • Ukazna vrsta je programski vmesnik za prenos ukazov med gostiteljem in napravo. Primeri ukazov so zahteve za prenos podatkov v/iz naprave ali zahteve za zagon ščepcev na napravi.

Okolje OpenCL je potrebno ustvariti za vsako heterogeno platformo, na kateri bomo kasneje poganjali ščepce. Ustvarjanje okolja OpenCL poteka v naslednjih korakih:

  1. izbiranje platforme, ki vsebuje eno ali več naprav,
  2. izbiranje naprav znotraj platforme, na katerih bomo zaganjali ščepce,
  3. ustvarjanje konteksta, ki vključuje naprave ter ukazne in pomnilniške vrste,
  4. ustvarjanje ukaznih vrst za prenašanje ukazov iz gostitelja na napravo.

Poglejmo si v nadaljevanju vsakega izmed teh korakov pobliže.

Lista platform

Vsak ščepec se izvaja na nekakšni platformi (heterogenem računalniškem sistemu), ki vsebuje eno ali več naprav. Platformo si lahko predstavljamo kot gonilnik OpenCL za določen tip naprav. Da bi sploh znali prilagoditi ščepce napravam, moramo najpej ugotoviti na kakšni platformi se bo naš program izvajal in ali platforma sploh vsebuje kakšno napravo. V ta namem pokličemo funkcijo clGetPlatformIDs():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    //***************************************************
    // STEP 1: Discover and initialize the platforms
    //***************************************************
    cl_uint numPlatforms;
    cl_platform_id *platforms = NULL;

    // Use clGetPlatformIDs() to retrieve the number of platforms present
    status = clGetPlatformIDs(
        0, 
        NULL, 
        &numPlatforms);
    clerr_chk(status);

    // Allocate enough space for each platform
    platforms = (cl_platform_id *)malloc(sizeof(cl_platform_id)*numPlatforms);

    // // Fill in available platforms with clGetPlatformIDs()
    status = clGetPlatformIDs (
        numPlatforms, 
        platforms, 
        NULL);
    clerr_chk(status);

Funkcijo clGetPlatformIDs() moramo poklicati dvakrat. Prvič, da ugotovimo, koliko platform imamo v sistemu, ter drugič, da za vse platforme inicializiramo listo platform.

Lista naprav

Sedaj moramo ugotoviti koliko in katere naprave imamo v vsaki platformi. V ta namem pokličemo funkcijo clGetDeviceIDs() :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    //***************************************************
    // STEP 2: Discover and initialize the devices
    //***************************************************

    cl_uint numDevices;
    cl_device_id *devices = NULL;

    // Use clGetDeviceIDs() to retrieve the number of devices present
    status = clGetDeviceIDs(
                            platforms[0],
                            CL_DEVICE_TYPE_GPU,
                            0,
                            NULL,
                            &numDevices);
    clerr_chk(status);


    // Allocate enough space for each device
    devices = (cl_device_id*) malloc(numDevices*sizeof(cl_device_id));

    // Fill in devices with clGetDeviceIDs()
    status = clGetDeviceIDs(
                            platforms[0],
                            CL_DEVICE_TYPE_GPU,
                            numDevices,
                            devices,
                            NULL);
    clerr_chk(status);

Funkcijo clGetDeviceIDs() moramo spet poklicati dvakrat. Prvič, da ugotovimo, koliko naprav imamo v platformi, ter drugič, da vse naprave prenesemo v prej pripravljeno listo naprav.

Praviloma vse funkcije OpenCL vračajo celoštevilsko vrednost (v našem primeru se imenuje status), ki vsebuje kodo napake oziroma uspešne izvedbe. Vrednost status moramo vedno preverjati in v primeru napake ustaviti izvajanje programa.

Lastnosti platform in naprav

Ko imamo seznam platform in naprav v njih, lahko za vsako platformo ali napravo ugotovimo njihove lasnosti s klicem funkcij clGetPlatformInfo() in clGetDeviceInfo():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    printf("=== OpenCL platforms: ===\n");    
    for (int i=0; i<numPlatforms; i++)
    {
        printf("  -- The platform with the index %d --\n", i);
        clGetPlatformInfo(platforms[i],
                        CL_PLATFORM_NAME,
                        sizeof(buffer),
                        buffer,
                        NULL);
        printf("  PLATFORM_NAME = %s\n", buffer);

        clGetPlatformInfo(platforms[i],
                        CL_PLATFORM_VENDOR,
                        sizeof(buffer),
                        buffer,
                        NULL);
        printf("  PLATFORM_VENDOR = %s\n", buffer);

    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    printf("=== OpenCL devices: ===\n");
    for (int i=0; i<numDevices; i++)
    {
        printf("  -- The device with the index %d --\n", i);
        clGetDeviceInfo(devices[i],
                        CL_DEVICE_NAME,
                        sizeof(buffer),
                        buffer,
                        NULL);
        printf("  CL_DEVICE_NAME = %s\n", buffer);

        clGetDeviceInfo(devices[i],
                        CL_DEVICE_VENDOR,
                        sizeof(buffer),
                        buffer,
                        NULL);
        printf("  CL_DEVICE_VENDOR = %s\n", buffer);

        clGetDeviceInfo(devices[i],
                        CL_DEVICE_MAX_CLOCK_FREQUENCY,
                        sizeof(buf_uint),
                        &buf_uint,
                        NULL);
        printf("  CL_DEVICE_MAX_CLOCK_FREQUENCY = %u\n",
               (unsigned int)buf_uint);

        clGetDeviceInfo(devices[i],
                        CL_DEVICE_MAX_COMPUTE_UNITS,
                        sizeof(buf_uint),
                        &buf_uint,
                        NULL);
        printf("  CL_DEVICE_MAX_COMPUTE_UNITS = %u\n",
               (unsigned int)buf_uint);

        clGetDeviceInfo(devices[i],
                        CL_DEVICE_MAX_WORK_GROUP_SIZE,
                        sizeof(buf_sizet),
                        &buf_sizet,
                        NULL);
        printf("  CL_DEVICE_MAX_WORK_GROUP_SIZE = %u\n",
               (unsigned int)buf_sizet);

        clGetDeviceInfo(devices[i],
                        CL_DEVICE_LOCAL_MEM_SIZE,
                        sizeof(buf_ulong),
                        &buf_ulong,
                        NULL);
        printf("  CL_DEVICE_LOCAL_MEM_SIZE = %u\n",
               (unsigned int)buf_ulong);   
    }

Rezultat zgornje kode za platformo Nvidia Cuda in napravo Tesla K40 je sledeč:

=== OpenCL platforms: ===
  -- The platform with the index 0 --
  PLATFORM_NAME = NVIDIA CUDA
  PLATFORM_VENDOR = NVIDIA Corporation
=== OpenCL devices: ===
  -- The device with the index 0 --
  CL_DEVICE_NAME = Tesla K40m
  CL_DEVICE_VENDOR = NVIDIA Corporation
  CL_DEVICE_MAX_CLOCK_FREQUENCY = 745
  CL_DEVICE_MAX_COMPUTE_UNITS = 15
  CL_DEVICE_MAX_WORK_GROUP_SIZE = 1024
  CL_DEVICE_LOCAL_MEM_SIZE = 49152

Ustvarjanje OpenCL konteksta

Potem, ko smo izbrali platformo in naprave, moramo za naprave ustvariti kontekst. V kontekst lahko vključimo samo naprave iz iste platforme. Kontekst je nekakšen abstraktni vsebovalnik, ki vključuje naprave, njihove ukazne vrste, pomnilniške objekte ter programe, ki so namenjeni posamezni napravi. Kontekst ustvarimo z OpenCL funkcijo clCreateContext():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    //***************************************************
    // STEP 3: Create a context
    //***************************************************

    cl_context context = NULL;
    // Create a context using clCreateContext() and
    // associate it with the devices
    context = clCreateContext(
                              NULL,
                              numDevices,
                              devices,
                              NULL,
                              NULL,
                              &status);
    clerr_chk(status);

Ustvarjanje ukaznih vrst

Sedaj moramo za vsako napravo posebej ustvariti ukazno vrsto. Ukazna vrsta je namenjena prenašanju ukazov iz gostitelja v napravo. Primeri ukazov so pisanje in branje iz pomnilnika naprave ali prenos in zaganjanje ščepca. Ukazno vrsto ustvarimo s funkcijo clCreateCommandQueue() :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    //***************************************************
    // STEP 4: Create a command queue
    //***************************************************
    cl_command_queue cmdQueue;
    // Create a command queue using clCreateCommandQueue(),
    // and associate it with the device you want to execute
    // on
    cmdQueue = clCreateCommandQueue(
                                    context,
                                    devices[0],
                                    CL_QUEUE_PROFILING_ENABLE,
                                    &status);
    clerr_chk(status);

Celotno kodo iz tega poglavja najdete v mapi 01-discover-devices tukaj.




  1. © Patricio Bulić, Univerza v Ljubljani, Fakulteta za računalništvo in informatiko. Gradivo je objavljeno pod licenco Creative Commons Priznanje avtorstva-Nekomercialno-Deljenje pod enakimi pogoji 4.0 Mednarodna