Sunteți pe pagina 1din 13

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Lucrarea 3 - APLICATII OpenMP

Se va studia implementarea OpenMP a algoritmilor paraleli în multiprocesoare cu memorie partajată. Programele se pot dezvolta atât pe calculatoarele desktop din laboratoarele B125a și B138 (ITEMS), cât și în nodurile sistemului HPC Dell PowerEdge, programele sunt compatibile datorită bibliotecii OpenMP care se poate instala pe oricare dintre sisteme. Calculatoarele desktop din laboratorul B125a au procesoare cu 2 core, cele din B138 au câte 4 cores, iar fiecare nod din HPC conține câte 2 procesoare cu câte 8 cores, în total 16 cores in fiecare nod. Biblioteca OpenMP tratează fiecare nucleu (core) dintr-un sistem ca un procesor separat pe care poate lansa un thread de execuție.

3.1 Biblioteca OpenMP

Biblioteca OpenMP este instalată implicit (la instalarea sistemului de operare) atât în sistemele Linux Ubuntu 12.04 (din laboratoarele B125a, B138), cât și în fiecare nod al sistemului HPC Dell PowerEdge. Biblioteca OpenMP asigură interfaţa (API-Application Program Interface) pentru exprimarea paralelismului într-un limbaj de bază (Fortan, C, C++) folosind thread-uri multiple care comunică prin variabile partajate. Există implementări OpenMP atât pentru Linux cât și pentru Windows [Gra03],

[OMP08].

OpenMP foloseşte modelul fork-join de programare paralelă: programul este compus dintr-o succesiune de regiuni secvenţiale şi regiuni paralele; în regiunile paralele thread-urile multiple execută task-uri create implicit sau explicit care comunică între ele prin variabile partajate. Biblioteca OpenMP permite paralelizarea semi-automată a programelor:

Programatorul studiază dependențele între task-uri și definește regiunile secvențiale și paralele (compuse din thread-uri paralele) și modul de comunicație și sincronizare între thread-uri prin intermediul unor directive de compilare.

Compilatoarele compatibile OpenMP interpretează directivele de compilare şi generează codul paralel care conține thread-urile și mecanismele de comunicaţie şi sincronizare între thread-uri. Sintaxa unei directive OpenMP pentru limbajele C, C ++ este:

#pragma omp nume_directiva [clauze]

{

Bloc structurat

}

OpenMP admite numai blocuri structurate: un bloc structurat are un singur punct de intrare şi un singur punct de ieşire și nu se admit salturi din bloc în afara lui sau din afară în interiorul blocului, cu excepţia funcţiei exit() (pentru C, C++) sau a instrucţiunii STOP (pentru Fortran). În continuare vor fi prezentate cele mai importante directive OpenMP, împreună cu clauzele acestora şi cu funcţiile de bibliotecă necesare pentru limbajul C.

3.1.1 Crearea regiunilor paralele în OpenMP

Regiunile paralele se creează cu directiva #pragma omp parallel. Atunci când un thread (în general thread-ul master) execută directiva parallel, se crează o regiune paralelă compusă dintr-un grup de thread-uri care se execută în paralel; această directivă are următoarea sintaxă:

1

Lucrarea 3 – Aplicații OpenMP

#pragma omp parallel \ num_threads(nr_intreg)\ shared (var1, var2, …) \ private (var1, var2, …) \ default(shared|none)\

reduction(operator:var1, var2, …)

{

bloc structurat de cod

}

Numărul de thread-uri ale regiunii paralele create este controlat de valoarea variabilei de mediu OMP_NUM_THREADS, poate fi setată prin comanda sh/bash:

$ export OMP_NUM_THREADS=nr_thread-uri

În program se poate seta numărul de thread-uri prin apelul funcţiei:

void omp_set_num_threads (int num_threads);

sau poate fi setat ca argument al clauzei num_threads.

Dacă nu a fost setat numărul de thread-uri prin program și OMP_NUM_THREADS nu este definită, biblioteca OpenMP creează un număr de thread-uri egal cu numărul de procesoare (cores) ale nodului de calcul. Fiecare thread execută blocul structurat inclus în regiunea paralelă. În general, nu există nici-o sincronizare între thread-urile unei regiuni paralele, adică fiecare thread execută o anumită instrucţiune din bloc într-un moment de timp independent de momentul în care acea instrucţiune este executată de alte thread-uri. Fiecare thread are un număr (identificator), care este un întreg de la 0 până la numărul de thread-uri minus 1. Acest identificator poate fi aflat prin apelul funcţiei:

int omp_get_thread_num()

Atunci când toate thread-urile ating sfârşitul regiunii paralele, ele sunt distruse (sau puse în aşteptare) şi numai thread-ul master continuă execuţia regiunii secvenţiale următoare. Se poate considera că la terminarea unei regiuni paralele există o bariera de sincronizare implicită.

Variabile folosite în thread-uri pot fi de tip private sau partajat (shared). Dacă o variabilă este de tip private, atunci fiecare thread primeşte o copie locală neiniţializată a acestei variabile. Dacă o variabilă este de tip partajat (shared), atunci există o singură copie a variabilei și toate thread-urile care se execută în paralel au acces la acea copie a variabilei. In mod implicit, variabilele globale sunt de tip shared, iar cele locale sunt de tip private. Clauza default poate schimba aceasta, specificând că tipul implicit al variabilelor este partajat (shared), sau nici un tip (none) nu este implicit, deci trebuie declarat explicit tipul fiecarei variabile. Tipul variabilelor se poate specifica prin clauza shared (care specifică lista variabilor partajate de toate thread-urile) sau prin clauza private (care specifică lista variabilelor locale ale thread-urilor). Clauza firstprivate conține lista variabilelor private care se inițializează cu valorile pe care le au variabilele cu același nume înainte de începerea regiunii paralele.

Clauza reduction definește o operatie de reducere paralelă, pentru care se specifică un operator asociativ şi o listă de variabile.

2

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Exemplu – Programul Hello_OpenMP.c

/* Hello_OpenMP.c Compilare:

gcc Hello_OpenMP.c -o Hello_OpenMP -fopenmp -lgomp Se executa cu argument p - nr de thread-uri, implicit p = 4 ./Hello_OpenMP p

*/ #include <stdio.h> #include <omp.h> int main(int argc, char* argv[]) { int id, p = 4; if (argc >= 2) p = atoi(argv[1]);

#pragma omp parallel num_threads(p) private(id)

{

id = omp_get_thread_num(); printf("Hello OpenMP thread %d !\n", id);

}

return 0;

}

Execuția cu 4 thread-uri:

$ ./Hello_OpenMP 4 Hello OpenMP thread 0 ! Hello OpenMP thread 1 ! Hello OpenMP thread 3 ! Hello OpenMP thread 2 !

Programele OpenMP trebuie să includă header-ul bibliotecii (#include <omp.h>). Comanda de compilare trebuie să conțină opțiunea –fopenmp (pentru interpretarea directivelor OpenMP) și să link-eze programul rezultat cu biblioteca OpenMP (opțiunea –lgomp din comanda de compilare gcc).

Exercițiul E3.1. Compilați și executați programul Hello_OpenMP.c, variind modul de setare a numărului de thread-uri (folosind variabila de mediu OMP_NUM_THREAD, sau prin program cu funcția

omp_set_num_threads())

3.1.2 Directive OpenMP de execuție paralelă

În interiorul unei regiuni paralele se pot introduce directive de paralelizare a buclelor de calcul (directiva for) şi directive de sincronizare între thread-uri (directivele single, barrier şi critical).

Directiva #pragma omp for specifică faptul că bucla for imediat următoare va fi executată în paralel, prin distribuirea iteraţiilor buclei între thread-urile regiunii paralele în care a fost introdusă. Bucla trebuie să fie paralelizabilă, adică fiecare iteraţie a acesteia să poată fi executată independent de alte iteraţii. Sfârsitul buclei paralele for este un punct de sincronizare implicit (barieră) între thread-uri: nici un thread nu continuă execuția după bucla for până ce nu au terminat toate thread-urile de executat bucla respectivă. Există o exceptie, dacă se introduce clauza nowait in directiva #pragma omp for; în această situație fiecare thread continuă operațiile după bucla for, fără să aștepte ca și celelalte thread-uri să termine bucla. Directiva #pragma omp for introduce o singură buclă paralelă; dacă într-o regiune paralelă sunt mai multe bucle paralele, pentru fiecare dintre ele se foloseşte o astfel de directivă. Se pot combina directivele parallel şi for:

3

Lucrarea 3 – Aplicații OpenMP

#pragma omp parallel for [clauze] for(…){ }

OpenMP permite setarea modului de distribuire și planificare a iterațiilor buclelor paralelizate cu directiva for, prin clauza schedule:

#pragma omp for schedule (kind [,chunk_size])

Planificare statică: schedule (static [,chunk_size]): iterațiile buclei sunt împărțite

static în grupuri de câte chunk_size iterații, care se atribuie circular (robin-round), în ordine thread-

urilor 0, 1,

cele n iterații se împart echilibrat în p partiții de iterații consecutive: fie s = (int) n / p; primele k = n%p partiții au (s+1) iterații, iar ultimele (p-k) partiții au s iterații; se obține o partiționare în partiții continue, așa cum a fost definită în programele Pthread Dacă chunk_size = 1, se obține o partiționare întrețesută așa cum a fost folosită în programele Pthread.

până se epuizează toate grupurile; dacă nu este dat parameetrul chunk_size, atunci

(p-1),

Planificare dinamică: schedule (dynamic [,chunk_size]): iterațiile buclei sunt împărțite în grupe de câte chunk_size iterații care sunt distribuite dinamic thread-urilor din regiunea paralelă, pe măsură ce acestea cer un nou grup de iterații.

Planificare automată: schedule (auto): distribuirea și planificarea iterațiilor este delegată compilatorului sau sistemului executiv (runtime), în funcție de implementarea bibliotecii

Directiva #pragma omp barrier introduce un punct de sincronizare explicit în interiorul unei regiuni paralele: nici un thread din regiunea paralelă nu poate continua execuţia instrucţiunii următoare directivei barrier decât după ce toate thread-urile au ajuns la barieră.

#pragma omp parallel {

#pragma omp barrier

}

Directiva #pragma omp single, introdusă într-o regiune paralelă, are ca efect execuţia instrucţiunii (sau a blocului de instrucţiuni) care urmează de către un singur thread. Thread-ul care ajunge primul la această directivă este “câştigătorul” pentru execuţia blocului ei, iar toate celelalte thread-uri care ajung la ea aşteaptă până când blocul este executat, după care o depăşeşte şi continuă execuţia. Directiva single introduce o barieră de sincronizare implicită între thread-uri dacă nu conține clauza nowait.

#pragma omp parallel {

#pragma omp single

{

bloc single

}

}

Directiva #pragma omp master, introdusă într-o regiune paralelă, are ca efect execuţia instrucţiunii (sau a blocului de instrucţiuni) care urmează doar de către thread-ul master, fără barieră de sincronizare între thread-uri.

Directiva #pragma omp critical este utilizată pentru serializarea acceselor mai multor thread- uri la o variabilă partajată. Instrucţiunea (sau blocul de instrucţiuni) care urmează unei astfel de directive este o secţiune critică şi poate fi executată numai de un singur thread la un moment dat:

4

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

#pragma omp critical (nume_sect_critica)

{

bloc sectiune critica

}

Argumentul acestei directive este opţional dacă toate secţiunile critice dintr-o regiune paralelă sunt independente unele de altele, sau trebuie introdusă explicit, cu un anumit nume pentru toate directivele critical care se referă la controlul accesului la aceeaşi variabilă partajată.

Directiva critical protejează, aşadar, accesul mai multor thread-uri la o secţiune critică (blocul directivei) şi este echivalentă cu utilizarea unui mecanism de sincronizare de tip mutex (lock).

3.2. Implementarea OpenMP a înmulțirii matricelor

În OpenMP se pot implementa algoritmi paraleli folosind thread-uri care comunică prin variabile partajate. Spre deosebire de biblioteca Pthread, în care variabilele partajate trebuie să fie definite obligatoriu în segmentul de date (ca variabile globale, statice sau alocate dinamic în heap, nu ca variabile locale alocate în stivă), în OpenMP programatorul poate defini variabilele ca în orice program secvențial (de regulă locale, în funcția main() sau în alte funcții) și compilatorul OpenMP (gcc cu opțiunea –fopenmp) schimbă categoria de memorare a variabilelor în conformitate cu clauzele

de partajare din directive (shared, private, firstprivate, default).

Se implementează același algoritm de înmulțire a două matrice care a fost implementat și în aplicațiile Pthread:

for (i = 0; i < n; i++) for (j = 0; j < n; j++) { c[i][j] = 0; for (k = 0; k < n; k++) c[i][j] += a[i][k]*b[k][j];

}

In fisierul Matrix_Mult_OpenMP.c sunt date mai multe versiuni de implementare OpenMP a algoritmului de înmulțire a două matrice, cu diferite moduri de partiționare. Partea de citire a parametrilor din linia de comandă, de alocare și inițializare a datelor este la fel ca în programul Pthread de înmulțire a două matrice, cu deosebirea că toate variabilele (inclusiv int n; int p; float **a, **b, **c;) sunt definite local în funcția main(), iar categoria de memorare este setată prin clauzele shared și private ale directivei #pragma omp parallel.

// Matrix_Mult_OpenMP.c #include <stdio.h> #include <stdlib.h> #include <sys/time.h>

#include <omp.h> int main(int argc, char *argv[]){

int n = 1024; int p = 2; int mod = 0;

int max_rep = 1; // Numar de repetari ale executiei

float **a, **b, **c; int i, j, k, rep; char *text_mod;

// dimensiunea implicita a matricelor

// numarul implicit de thread-uri // Mod 0: partitionare pe linii

5

Lucrarea 3 – Aplicații OpenMP

// Alocarea dinamica a matricelor

a = (float**)malloc(sizeof(float*)*n);

b = (float**)malloc(sizeof(float*)*n);

c = (float**)malloc(sizeof(float*)*n);

for (i = 0; i < n; i++){ a[i] = (float*)malloc(sizeof(float)*n); b[i] = (float*)malloc(sizeof(float)*n); c[i] = (float*)malloc(sizeof(float)*n);

}

// Initializarea matricelor a si b for (rep = 0; rep < max_rep; rep++){ if (p == 1) { // Inmultirea secventiala a doua matrice

}

// Inmultirea paralela a doua matrice else if (mod == 0) { text_mod = "mod 0: Inmultire paralela cu partitionare pe linii";

#pragma omp parallel num_threads(p) shared(a,b,c,n) private(i,j,k)

{

#pragma omp for for (i = 0; i < n; i++) for (j = 0; j < n; j++){ c[i][j] = 0; for (k = 0; k < n; k++) c[i][j] += a[i][k] * b[k][j];

}

}

}

else if (mod == 1) {

text_mod = "mod 1: Inmultire paralela cu partitionare pe linii \ intretesute"; #pragma omp parallel num_threads(p) shared(a,b,c,n) private(i,j,k)

{

#pragma omp for schedule (static, 1) for (i = 0; i < n; i++) for (j = 0; j < n; j++){ c[i][j] = 0; for (k = 0; k < n; k++) c[i][j] += a[i][k] * b[k][j];

}

}

}

else if else { printf("Eroare: mod %d - inexistent\n", mod);

exit (0);

}

} // end for (rep)

// Masurare timp, afisare rezultate

return 0;

}

Dimensiunea matricelor (n) și numărul de thread-uri (p) se pot introduce ca parametri la lansarea în execuție (argv[1], argv[2]), iar implicit n = 1024, p = 2. Matricele a, b, c se alocă dinamic.

6

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Modul de partiționare pe linii a matricelor. Pentru partiționarea pe linii (continue sau întrețesute) se paralelizează bucla exterioară (care parcurge liniile) prin aplicarea construcției #pragma omp for acestei bucle. Variabilele i, j, k sunt private: variabile j, k parcurg integral buclele interne neparalelizate; variabila i este contorul buclei paralelizate: fiecare thread are propria variabilă i, care nu parcurge toate iterațiile buclei de la 0 la (n-1), așa cum pare din sintaxa directivei, ci numai iterațiile care îi revin, conform clauzei schedule:

partiții continue: #pragma omp for sau #pragma omp for schedule (static)

partiții întrețesute: #pragma omp for schedule (static, 1)

Modul de partiționare pe coloane după înterschimbarea buclelor. Dacă se interschimbă buclele i și j între ele, distribuirea iterațiilor buclei externe (de contor j) înseamnă distribuirea coloanelor matricelor. Se obține un program cu o singură regiune paralelă, asemănător cu cel cu partiționarea pe linii. Variabilele i, j, k sunt private: variabilele i, k parcurg integral buclele interne neparalelizate; variabila j este contorul buclei paralelizate: fiecare thread parcurge numai iterațiile care îi revin din cele n iterații (conform clauzei schedule)

#pragma omp parallel num_threads(p) shared(a,b,c,n) private(i,j,k)

{

#pragma omp for for (j = 0; j < n; j++) for (i = 0; i < n; i++){ c[i][j] = 0; for (k = 0; k < n; k++) c[i][j] += a[i][k] * b[k][j];

}

}

Modul de partiționare pe coloane cu n regiuni paralele. Pentru partiționarea pe coloane fără interschimbarea buclelor se paralelizează bucla internă (de contor j): se definește o regiune paralelă în fiecare iterație a buclei exterioare (de contor i), iar directiva #pragma omp for distribuie iterațiile buclei interne (de contor j) între p thread-uri. Variabila i este partajată: toate thread-urile folosesc același contor de linii; variabilele j, k sunt private; variabila j este contorul buclei paralelizate, fiecare thread parcurge numai iterațiile care îi revin conform clauzei schedule; variabila k parcurge integral bucla internă neparalelizată. S-a obținut varianta cu n regiuni paralele a algoritmului, care are timpul de execuție paralelă estimat și măsurat mai mare decât cel al variantei cu o regiune paralelă.

for (i = 0; i < n; i++)

#pragma omp parallel num_threads(p) shared(a,b,c,n,i) private(j,k)

{

#pragma omp for for (j = 0; j < n; j++){ c[i][j] = 0; for (k = 0; k < n; k++) c[i][j] += a[i][k] * b[k][j];

}

}

Se poate introduce clauza nowait in directiva for deoarece iterațiile sunt independente, dar nu are nici un efect deoarece terminarea buclei coincide cu terminarea regiunii paralele care introduce o bariera de sincronizare implicită.

7

Lucrarea 3 – Aplicații OpenMP

Modul de partiționare pe coloane fără interschimbarea buclelor. Se transformă programul de partiționare pe coloane cu n regiuni paralele în program cu 1 regiune paralelă prin reunirea task- urilor asignate fiecărei partiții în cele n iterații ale buclei i. Variabilele i, j, k sunt private; variabila j este este contorul buclei paralelizate: fiecare thread parcurge numai iterațiile care îi revin conform clauzei schedule; variabilele i și k parcurg integral buclele interne neparalelizate Se introduce clauza nowait deoarece iterațiile sunt independente și thread-urile nu trebuie să se sincronizeze la fiecare linie.

#pragma omp parallel num_threads(p) shared(a,b,c,n) private(i,j,k)

{

for (i = 0; i < n; i++) { #pragma omp for nowait for (j = 0; j < n; j++){ c[i][j] = 0; for (k = 0; k < n; k++) c[i][j] += a[i][k] * b[k][j];

}

}

}

Compilarea programului se face pe calculatorul pe care se va executa (în clusterul B125 sau HPC) și se link-ează cu bibliotecile corespunzătoare:

$ gcc Matrix_Mult_OpenMP.c –o Matrix_Mult -fopenmp -lgomp

La execuția programului rezultat (Matrix_Mult_OpenMP) pentru dimensiuni ale matricelor n = 1024 cu p = 16 thread-uri, cu partiționare pe linii (mod 0, implicit) în nodul hpc.intern al clusterului HPC Dell PowerEdge se obține:

$ ./Matrix_Mult_OpenMP 1024 16

n = 1024, p =16, t = 1.337 sec

Pentru măsurarea performanțelor se lansează fișierul de execuție repetată (Exec_Matrix_Mult_OpenMP), cu diferiți parametri n și p a programului Matrix_Mult_OpenMP executabil pe multiprocesorul dorit.

La fiecare execuție, programul Matrix_Mult_OpenMP înscrie în fișierul de rezultate (Res_Matrix_Mult_OpenMP.txt) valoarea măsurată a timpului de execuție. În final, în fisierul de rezultate se obține o matrice de valori ale timpului de execuție: pe o linie valorile pentru n dat (32, 512, 1024, 2048), pe o coloană valorile pentru un anumit număr de procesoare (p = 1, 2, 4, 8, 16).

Pentru reprezentarea grafică se lansează programulGrafice.R și se setează numele fișierului rn<-”Res_Matrix_Mult_OpenMP.txt”. Se obțin graficele TP(p), S(p), E(p) pentru diferite valori ale parametrului n. Graficele de mai jos reprezintă performanțele de execuție paralelă ale algoritmului de înmulțire a două matrice în nodul multiprocesor hpc.intern din sistemul HPC Dell PowerEdge, folosind între 1 (execuție secvențială) și 16 procesoare (cores) ale multiprocesorului.

8

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

9
9

Lucrarea 3 – Aplicații OpenMP

Pe grafice se observă diferite aspecte ale comportării algoritmului: scăderea timpului de execuție (T P ) atunci când crește numărul de procesoare, creșterea accelerării (S) și a eficienței (E) pentru un p dat, atunci când crește dimensiunea n; scăderea eficienței atunci când crește numărul de procesoare, dacă dimensiunea n rămâne constantă, etc. Modul de încărcare a procesoarelor în cursul execuției unui algoritm paralel se poate urmări cu toolset-ul System Monitor. De exemplu, dacă lansăm în execuție mai multe thread-uri în nodul hpc.intern din clusterul HPC se poate vedea cum sunt atribuite procesoarele.

. (a) (b) (c) (c)
.
(a)
(b)
(c)
(c)

10

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

În screenshot-ul (a) sunt lansate în execuție 4 thread-uri care sunt atribuite la 4 procesoare, care sunt încărcate 100%, restul procesoarele având o încărcare mult mai mică (execuția altor procese, de sistem sau utilizator). În screenshot-ul (b) sunt lansate 8 thread-uri, atribuite la 8 procesoare care sunt încărcate 100%. În screenshot-ul (c) sunt lansate 16 thread-uri, toate cele 16 procesoare ale nodului hpc.intern sunt ocupate 100%.

Exercițiul E3.2. Studiați programul folosind textul sursă Matrix_Mult_OpenMP.c, executați pentru fiecare mod de partiționare pe stația Linux și pe HPC și comparați rezultatele. În scriptul de execuție Exec_Matrix_Mult_OpenMP numărul de repetări ale execuției (parametrul max_rep transmis la comanda ./Matrix_Mult_Pthread n p mod max_rep) este mai mare pentru n mic: max_rep = 4096 / n.

3.3. Program OpenMP de adunare a doi vectori

Se implementează același algoritm de adunare a doi vectori x și y de dimensiune n, cu rezultat în y folosit și în aplicațiile Pthread:

for (i = 0; i < n; i++) y[i] = y[i] + x[i];

Partea de calcul din programul OpenMP de adunare a doi vectori:

// Adunare paralela cu partitii continue (cu elemente adiacente) #pragma omp parallel num_threads(p) shared (x, y, n) private (i)

{

#pragma omp for schedule (static) for (i = 0; i < n; i++) y[i] = y[i] + x[i];

}

// Adunare paralela cu partitii intretesute (cu elemente intretesute) #pragma omp parallel num_threads(p) shared (x, y, n) private (i)

{

#pragma omp for schedule (static,1) for (i = 0; i < n; i++) y[i] = y[i] + x[i];

}

Exercițiul E3.3. Dezvoltați programul Vector_Add_OpenMP.c, executați pe stația Linux și pe HPC. Generați matricea de valori ale timpului de execuție folosind un script Exec_Vector_Add_OpenMP asemănător cu cel pentru înmulție, pentru n = (4096, 32768, 262144, 16777216), p = (1, 2, 4, 8, 12, 16). Reprezentați graficele performanțelor TP (p), S(p), E(p) cu parametru n, folosind programul R dat.

3.4. Program OpenMP de adunare a două matrice

Se implementează același algoritm de adunare a două matrice pătrate a și b de dimensiuni n x n folosit și în aplicațiile Pthread:

for (i = 0; i < n; i++) for (j = 0; j < n; j++) c[i][j] = a[i][j] + b[i][j];

11

Lucrarea 3 – Aplicații OpenMP

Programul Matrix_Add_OpenMP.c poate fi identic cu cel de înmulțire a două matrice, se înlocuiește numai partea de calcul a elementului c[i][j] al matricei de ieșire; de exemplu, partea de program care realizează înmulțirea a două matrice cu partiționare orientată pe linii continue:

#pragma omp parallel num_threads(p) shared(a,b,c,n) private(i,j,k)

{

#pragma omp for for (i = 0; i < n; i++) for (j = 0; j < n; j++) c[i][j] = a[i][j] + b[i][j];

}

Exercițiul E3.4. Implementați algoritmul de adunare a două matrice Matrix_Add_OpenMP.c, în mod asemănător inmulțirii a două matrice. Executați programul pe stația Linux și pe HPC. Generați matricea de valori ale timpului de execuție folosind un script Exec_Matrix_Add_OpenMP pentru aceleași valori ale lui p (1, 2, 4, 8, 12, 16) și n = (64, 256, 512, 4096, 32768). Reprezentați graficele performanțelor TP (p), S(p), E(p) folosind programul Grafice.R

3.5. Program OpenMP de reducere paralelă

Implementarea OpenMP a algoritmului de reducere paralelă adaptivă (asemănătoare cu varianta Pthread) poate folosi clauza critical sau reduction pentru excluderea mutuală din etapa 2.

În varianta cu directiva critical, bucla cu n iterații de adunare a elementelor vectorului a[n] este descompusă (distribuită) de directiva #pragma omp for celor p thread-uri ale regiunii paralele și fiecare thread acumulează rezultatul reducerii parțiale din propria partiție într-o variabilă locală (private) (psum), inițializată cu 0. Bucla for poate fi fără sincronizare la sfârșit (cu nowait), dat fiind că fiecare thread își execută propria partiție apoi însumează rezultatul în rezultatul final fără să aștepte Rezultatul final se obține prin însumarea valorilor acestor variabile locale în variabila partajată sum folosind excluderea mutuală prin clauza #pragma omp critical

În varianta cu clauza reduction(op:list), operațiile de calcul a rezultatelor partiale si a rezultatului global folosind excluderea mutuală sunt introduse automat. Standardul prevede inițializarea variabilei rezultat (sum în acest exemplu) în funcție de operatorul de reducere: cu 0 pentru operația +, cu 1 pentru operația * etc.), dar nu toate implementările OpenMP respectă această recomandare. Variabilele din list trebuie să fie shared (partajate). Clauza reduction se aplică corect numai dacă operația de reducere se face în bucla for căreia îi aparține (nu într-o buclă imbricată în aceasta).

Exercițiul E3.5. Studiați și executați programul Reduction_OpenMP.c pe stația Linux și pe HPC. Generați matricea de valori ale timpului de execuție folosind scriptul Exec_Reduction_OpenMP. Reprezentați graficele performanțelor TP (p), S(p), E(p) folosind programul R dat.

3.6 Program OpenMP de înmulțire succesivă a matricelor

Se implementează același algoritm de înmulțire succesivă a matricelor : c = a x b; e = d x c ca și cel implementat Pthread. Partea de calcul paralel a înmulțirii succesive va arăta astfel:

12

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

<include <omp.h> int main(int argc, char **argv){ int i, j, k, n = 1024, p = 2; float **a, **b, **c, **d, **e; // Citire parametri n, p, alocarea si initializarea matricelor #pragma omp parallel num_threads(p) shared (a,b,c,d,e,n) \ private (i,j,k)

{

 

#pragma omp for for (i = 0; i < n; i++) for (j = 0; j < n; j++){ c[i][j] = 0; for (k = 0; k < n; k++) c[i][j] += a[i][k] * b[k][j]; }

#pragma omp for for (i = 0; i < n; i++) for (j = 0; j < n; j++){ e[i][j] = 0; for (k = 0; k < n; k++) e[i][j] += d[i][k] * c[k][j]; }

}

}

Acest program conține două bucle succesive. Fiecare buclă este o buclă imbricată pe 3 niveluri, cu bucla exterioară paralelizabilă, care se distribuie celor p thread-uri prin directiva #pragma omp for Exită dependențe dintre fiecare iterație din a doua buclă și toate iterațiile din prima buclă, de aceea este necesară o barieră de sincronizare între cele două bucle. În OpenMP, bariera de sincronizare necesară este introdusă automat la sfârșitul directivei #pragma omp for (dacă nu se adaugă clauza

nowait).

Exercițiul E3.6. Implementați algoritmul de înmulțire succesivă a matricelor folosind biblioteca OpenMP, în mod asemănător cu inmulțirea a două matrice. Executați programul pe stația Linux și pe HPC. Generați matricea de valori ale timpului de execuție folosind un script Exec_Many_Matrix_Mult_OpenMP pentru aceleași valori ale lui p (1, 2, 4, 8, 12, 16) și n = (16, 32, 64, 256, 1024, 4096). Reprezentați graficele performanțelor TP (p), S(p), E(p) folosind programul Grafice.R.

Bibliografie

1. Felicia Ionescu, Calcul paralel, 2014, slide-uri publicate pe site-ul moodle ETTI.

2. OpenMP Architecture Review Board, OpenMP Application Program Interface, 2008

3. B. Barney, OpenMP Programming, Lawrence Livermore National Laboratory, 2013, https://computing.llnl.gov/tutorials/OpenMP/

13