Parallel Programming in C with MPI and Open

  • Slides: 77
Download presentation
Parallel Programming in C with MPI and Open. MP Michael J. Quinn Chapitre 17

Parallel Programming in C with MPI and Open. MP Michael J. Quinn Chapitre 17 Open MP

Open. MP n Open. MP: Interface de programmation (API) pour le calcul parallèle sur

Open. MP n Open. MP: Interface de programmation (API) pour le calcul parallèle sur architecture à mémoire partagée. u Directives pour le compilateur u Bibliothèque logicielle u Variables de l’environnement n Open. MP fonctionne avec Fortran, C, ou C++

Modèle à mémoire partagée Les processeurs interagissent et se synchronisent à l’aide de variables

Modèle à mémoire partagée Les processeurs interagissent et se synchronisent à l’aide de variables partagées.

Parallélisme avec Fork et Join n Initialement un seul thread est actif (maître) n

Parallélisme avec Fork et Join n Initialement un seul thread est actif (maître) n Le maître exécute le code séquentiel. n Fork: Le maître crée ou active des threads additionnels afin d’exécuter du code en parallèle. n Join: À la fin du code parallèle, les threads sont éliminés ou suspendus et le flot de contrôle retourne à l’unique thread maître.

Parallélisme avec Fork et Join

Parallélisme avec Fork et Join

Parallélisation incrémentielle n Programme séquentiel: Cas particulier d’un programme parallèle à mémoire partagée. n

Parallélisation incrémentielle n Programme séquentiel: Cas particulier d’un programme parallèle à mémoire partagée. n Parallélisation incrémentielle: On transforme un programe séquentiel en programme parallèle de façon graduelle. n Le parallélisme incrémentiel est un avantage important de la programmation parallèle à mémoire partagée.

Boucle for parallèle n En C le parallélisme de données est souvent exprimé à

Boucle for parallèle n En C le parallélisme de données est souvent exprimé à l’aide de boucles for: for (i = first; i < size; i += prime) marked[i] = 1; n Avec Open. MP il est facile d’indiquer quand une boucle doit être exécuté en parallèle. n Le compilateur se charge de transformer le code séquentiel en code parallèle: n création des threads n affectation des itérations aux threads.

Pragmas n Pragma: Directive au compilateur C ou C++ n Signifie “pragmatic information” n

Pragmas n Pragma: Directive au compilateur C ou C++ n Signifie “pragmatic information” n Permet au programmeur de communiquer avec le compilateur n Le compilateur est libre d’ignorer les directives n Syntaxe: #pragma omp <reste du pragma>

Parallel for #pragma omp parallel for [clause [[, ] clause …] for (i =

Parallel for #pragma omp parallel for [clause [[, ] clause …] for (i = 0; i < N; i++) a[i] = b[i] + c[i]; n Le compilateur doit être en mesure de vérifier si le système d’exécution aura l’information nécessaire à l’ordonnancement des itérations de la boucle. n n Indépendance des itérations Nombre d’itérations

Forme canonique d’une boucle parallel for

Forme canonique d’une boucle parallel for

Variables privées et partagées n Variable partagée: Même adresse mémoire pour tous les threads

Variables privées et partagées n Variable partagée: Même adresse mémoire pour tous les threads n Variable privée: Différentes adresses mémoire pour différents threads. n Un thread ne peut pas accéder à une variable privée appartenant à un autre thread. n Par défaut, dans un “parallel for”, les variables sont partagées sauf l’indice de boucle.

Variables privées et partagées … }

Variables privées et partagées … }

Comment le système sait-il combien de threads il faut créer? Variable de l’environnement: OMP_NUM_THREADS

Comment le système sait-il combien de threads il faut créer? Variable de l’environnement: OMP_NUM_THREADS 4 fonctions utiles: n omp_get_num_procs n omp_set_num_threads n omp_get_thread_num

Fonction omp_get_num_procs n Retourne le nombre de processeurs (physique ou virtuels) disponibles par le

Fonction omp_get_num_procs n Retourne le nombre de processeurs (physique ou virtuels) disponibles par le programme parallèle. int omp_get_num_procs (void)

Fonction omp_set_num_threads n Le nombre de threads actifs dans les section de code parallèle

Fonction omp_set_num_threads n Le nombre de threads actifs dans les section de code parallèle sera égal au paramètre de la fonction n Peut être appelé à plusieurs endroits dans le programme. void omp_set_num_threads (int t)

Fonction omp_get_num_threads n Retourne le nombre de threads actifs. int omp_get_num_threads (void)

Fonction omp_get_num_threads n Retourne le nombre de threads actifs. int omp_get_num_threads (void)

Fonction omp_get_thread_num n Retourne le numéro du thread. int omp_get_thread_num(void)

Fonction omp_get_thread_num n Retourne le numéro du thread. int omp_get_thread_num(void)

Déclarer des variables privées Exemple: Algorithme de Floyd for (i = 0; i <

Déclarer des variables privées Exemple: Algorithme de Floyd for (i = 0; i < n; i++) for (j = 0; j < n; j++) a[i][j] = MIN(a[i][j], a[i][k]+a[k][j]); n N’importe laquelle des deux boucles peut être exécutée en parallèle (exécuter les deux en parallèle nécessite trop de threads) n On préfère paralléliser la boucle extérieure pour minimiser le nombre de fork/join n Chaque thread doit alors posséder sa propre copie de la variable j

Clause “private” n Clause: Composante optionnelle à un pragma n Clause “Private”: indique au

Clause “private” n Clause: Composante optionnelle à un pragma n Clause “Private”: indique au compilateur de créer une ou plusieurs variables privées. private ( <variable list> )

Exemple #pragma omp parallel for private(j) for (i = 0; i < n; i++)

Exemple #pragma omp parallel for private(j) for (i = 0; i < n; i++) for (j = 0; j < n; j++) a[i][j] = MIN(a[i][j], a[i][k]+a[k][j]);

Clause “firstprivate” n Pour créer une variable privée dont la valeur initiale est identique

Clause “firstprivate” n Pour créer une variable privée dont la valeur initiale est identique à celle du thread maître avant d’entrée dans la boucle. n Les variable sont initialisées une seule fois pour chaque thread et non pas à chaque itération n La modification d’une valeur est effective aussi pour les autres itérations exécutées par un thread donné.

Sections critiques Exemple: Approximation de double area, pi, x; int i, n; . .

Sections critiques Exemple: Approximation de double area, pi, x; int i, n; . . . area = 0. 0; for (i = 0; i < n; i++) { x += (i+0. 5)/n; area += 4. 0/(1. 0 + x*x); } pi = area / n;

Condition de concurrence n Si on ne fait que paralléliser la boucle. . .

Condition de concurrence n Si on ne fait que paralléliser la boucle. . . double area, pi, x; int i, n; . . . area = 0. 0; #pragma omp parallel for private(x) for (i = 0; i < n; i++) { x = (i+0. 5)/n; area += 4. 0/(1. 0 + x*x); } pi = area / n;

Condition de concurrence n. . . On obtient une condition de concurrence pour modifier

Condition de concurrence n. . . On obtient une condition de concurrence pour modifier la variable area

Pragma “critical” n Section critique: portion de code qui ne peut être exécuté que

Pragma “critical” n Section critique: portion de code qui ne peut être exécuté que par un seul thread à la fois. n On met #pragma omp critical devant le bloc de code C.

Exemple double area, pi, x; int i, n; . . . area = 0.

Exemple double area, pi, x; int i, n; . . . area = 0. 0; #pragma omp parallel for private(x) for (i = 0; i < n; i++) { x = (i+0. 5)/n; #pragma omp critical area += 4. 0/(1. 0 + x*x); } pi = area / n; Correct mais inefficace!

Réductions n Une réduction est l’application d’une opération associative sur les éléments d’un vecteur

Réductions n Une réduction est l’application d’une opération associative sur les éléments d’un vecteur n Les réductions sont si courantes que Open. MP fourni un mécanisme facilitant son application. n On peut ajouter une clause de réduction au pragma parallel for n On doit spécifier l’opération de réduction et la variable sur laquelle s’applique la réduction n Open. MP s’occupe de stocker les résultats partiels dans des variables privées.

Clause “Reduction” n La clause réduction a la syntaxe suivante: reduction (<op> : <variable>)

Clause “Reduction” n La clause réduction a la syntaxe suivante: reduction (<op> : <variable>) Opérateur Valeur initiale + 0 - 0 * 1 & Tous les bits à 1 | Tous les bits à 0 ^ Tous les bits à 0 && 1 || 0 max Minimum possible min Maximum possible

Exemple 1 double area, pi, x; int i, n; . . . area =

Exemple 1 double area, pi, x; int i, n; . . . area = 0. 0; #pragma omp parallel for private(x) reduction(+: area) for (i = 0; i < n; i++) { x = (i + 0. 5)/n; area += 4. 0/(1. 0 + x*x); } pi = area / n;

Exemple 2 #include <math. h> void reduction 1(float *x, int *y, int n) {

Exemple 2 #include <math. h> void reduction 1(float *x, int *y, int n) { int i, b, c; float a, d; a = 0. 0; b = 0; c = y[0]; d = x[0]; #pragma omp parallel for private(i) shared(x, y, n) reduction(+: a) reduction(^: b) reduction(min: c) reduction(max: d) for (i=0; i<n; i++) { a += x[i]; b ^= y[i]; if(c>y[i])c=y[i]; d = fmaxf(d, x[i]); } }

Amélioration de la performance #1 n Quelques fois, transformer une boucle for séquentielle en

Amélioration de la performance #1 n Quelques fois, transformer une boucle for séquentielle en boucle for parallèle peut dégrader les performances n Le problème est que la transformation peut ajouter trop de “fork” et “join” par rapport au reste du calcul. n Quelques fois, inverser deux boucles inbriquées peut aider si: u Le parallélisme est dans la boucle interne u Après l’inversion, la boucle extérieure peut être parallélisée u L’inversion n’augmente pas trop les défauts de

Exemple for (i=1; i<m; i++) for (j=0; j<n; j++) a[i][j]= 2*a[i-1][j]; for (i=1; i<m;

Exemple for (i=1; i<m; i++) for (j=0; j<n; j++) a[i][j]= 2*a[i-1][j]; for (i=1; i<m; i++) #pragma omp parallel for (j=0; j<n; j++) a[i][j]= 2*a[i-1][j]; Plusieurs fork/join #pragma omp parallel for (j=0; j<n; j++) for (j=1; j<m; i++) a[i][j]= 2*a[i-1][j]; Plus de défauts de cache

Amélioration de la performance #2 n Lorsqu’une boucle a peu d’itérations, le temps supplémentaire

Amélioration de la performance #2 n Lorsqu’une boucle a peu d’itérations, le temps supplémentaire des fork/join devient plus grand que le temps que l’on veut sauver par le parallélisme n La clause if indique au compilateur d’utiliser le parallélisme sous certaines conditions #pragma omp parallel for if(n > 5000)

Amélioration de la performance #3 n Il est possible de choisir de quelle façon

Amélioration de la performance #3 n Il est possible de choisir de quelle façon les itérations d’une boucle for seront affectées aux threads à l’aide de la clause schedule n On parlera d’ordonnancement des itérations n Il y a deux principaux types d’ordonnancement: n Statique: L’ordonnancement est déterminé avant l’exécution n Dynamique: L’ordonnancement est faite en cours d’exécution

Ordonnancement statique ou dynamique n Ordonnancement statique u Pas de charge de travail supplémentaire

Ordonnancement statique ou dynamique n Ordonnancement statique u Pas de charge de travail supplémentaire u La charge de travail peut être mal équilibrée n Ordonnancement dynamique u Charge de travail supplémentaire u Peut équilibrer la charge de travail

Segments (chunks) n Un segment est une suite d’itérations contiguës n Augmenter la taille

Segments (chunks) n Un segment est une suite d’itérations contiguës n Augmenter la taille des segments réduit la charge supplémentaire de travail n Décroitre la taille des segments permet de mieux équilibrer la charge de travail entre les threads.

Clause “schedule” n Syntaxe: schedule (<type>[, <segment> ]) n Types permis: u static: ordonnancement

Clause “schedule” n Syntaxe: schedule (<type>[, <segment> ]) n Types permis: u static: ordonnancement statique u dynamic: ordonnancement dynamique u guided: La taille des segments décroit graduellement u runtime: Le type est choisit à l’exécution en fonction de la variable de l’environnement OMP_SCHEDULE

Options n schedule(static): La taille des segments est environ n/t n schedule(static, C): La

Options n schedule(static): La taille des segments est environ n/t n schedule(static, C): La taille des segments est C n schedule(dynamic): Une itération à la fois n schedule(dynamic, C): C itérations à la fois

Options (suite) n schedule(guided, C): Ordonnancement dynamique, la taille des segments diminue graduellement jusqu’à

Options (suite) n schedule(guided, C): Ordonnancement dynamique, la taille des segments diminue graduellement jusqu’à C n schedule(guided): C=1 n schedule(runtime): Dépend de la variable OMP_SCHEDULE; Exemple en Unix: setenv OMP_SCHEDULE “static, 1” ou export OMP_SCHEDULE=“static, 1”

Autres formes de parallélisme n Jusqu’à maintenant, l’emphase a été mise sur la parallélisation

Autres formes de parallélisme n Jusqu’à maintenant, l’emphase a été mise sur la parallélisation des boucles for. n Parallélisme de données n Nous allons voir d’autres situations favorables au parallélisme de données:

Traitement d’une liste de tâches

Traitement d’une liste de tâches

Code séquentiel (1/2) int main (int argc, char *argv[]) { struct job_struct *job_ptr; struct

Code séquentiel (1/2) int main (int argc, char *argv[]) { struct job_struct *job_ptr; struct task_struct *task_ptr; . . . task_ptr = get_next_task (&job_ptr); while (task_ptr != NULL) { complete_task (task_ptr); task_ptr = get_next_task (&job_ptr); }. . . }

Code séquentiel (2/2) struct task_struct* get_next_task( struct job_struct **job_ptr ) { struct task_struct *answer;

Code séquentiel (2/2) struct task_struct* get_next_task( struct job_struct **job_ptr ) { struct task_struct *answer; if (*job_ptr == NULL) answer = NULL; else { answer = (*job_ptr)->task; *job_ptr = (*job_ptr)->next; } return answer; }

Stratégie de parallélisation n Chaque thread prend la prochaine tâche dans la liste et

Stratégie de parallélisation n Chaque thread prend la prochaine tâche dans la liste et la complète. Cela est répété jusqu’à ce qu’il n’y ait plus de tâche. n On doit s’assurer que deux threads ne prennent pas la même tâche. n On doit donc définir une section critique.

Le pragma “parallel” n Précède un bloc de code devant être exécuté par tous

Le pragma “parallel” n Précède un bloc de code devant être exécuté par tous les threads. #pragma omp parallel n Note: Tous les threads exécutent le même code

Code parallel (1/2) int main (int argc, char *argv[]) { struct job_struct *job_ptr; struct

Code parallel (1/2) int main (int argc, char *argv[]) { struct job_struct *job_ptr; struct task_struct *task_ptr; . . . #pragma omp parallel private(task_ptr) { task_ptr = get_next_task (&job_ptr); while (task_ptr != NULL) { complete_task (task_ptr); task_ptr = get_next_task (&job_ptr); } }. . . }

Code parallel (2/2) char *get_next_task(struct job_struct **job_ptr) { struct task_struct *answer; #pragma omp critical

Code parallel (2/2) char *get_next_task(struct job_struct **job_ptr) { struct task_struct *answer; #pragma omp critical { if (*job_ptr == NULL) answer = NULL; else { answer = (*job_ptr)->task; *job_ptr = (*job_ptr)->next; } } return answer; }

Le pragma “for” n Le pragma “parallel” demande à tous les threads d’exécuter tout

Le pragma “for” n Le pragma “parallel” demande à tous les threads d’exécuter tout le code dans le bloc. n Si le bloc contient une boucle for que l’on voudrait diviser entre les threads alors on peut utiliser le pragma “for” #pragma omp for

Exemple for (i = 0; i < m; i++) { low = a[i]; high

Exemple for (i = 0; i < m; i++) { low = a[i]; high = b[i]; if (low > high) { printf ("Exiting (%d)n", i); break; } for (j = low; j < high; j++) c[j] = (c[j] - a[i])/b[i]; } • La première boucle for ne peut pas être parallélisée • Paralléliser la seconde boucle est inefficace • Le pragma « parallel » seul est insuffisant

Exemple #pragma omp parallel private(i, j){ for (i = 0; i < m; i++)

Exemple #pragma omp parallel private(i, j){ for (i = 0; i < m; i++) { low = a[i]; high = b[i]; if (low > high) { printf ("Exiting (%d)n", i); break; } #pragma omp for (j = low; j < high; j++) c[j] = (c[j] - a[i])/b[i]; }

Le pragma “single” n Dans certaines situation on veut qu’un seul thread exécute une

Le pragma “single” n Dans certaines situation on veut qu’un seul thread exécute une certaine instruction n Par exemple, un message en sortie n C’est le rôle du pragma “single” n Syntaxe: #pragma omp single

Exemple #pragma omp parallel private(i, j, low, high) for (i = 0; i <

Exemple #pragma omp parallel private(i, j, low, high) for (i = 0; i < m; i++) { low = a[i]; high = b[i]; if (low > high) { #pragma omp single printf ("Exiting (%d)n", i); break; } #pragma omp for (j = low; j < high; j++) c[j] = (c[j] - a[i])/b[i]; }

Clause “nowait” n Le compilateur place une barrière de synchronisation à la fin de

Clause “nowait” n Le compilateur place une barrière de synchronisation à la fin de chaque bloc couvert par un pragma de type single, parallel ou for. n La plupart du temps cela est nécessaire n On peut enlever la barrière à l’aide de la clause nowait

Exemple #pragma omp parallel private(i, j, low, high){ for (i = 0; i <

Exemple #pragma omp parallel private(i, j, low, high){ for (i = 0; i < m; i++) { low = a[i]; high = b[i]; #pragma omp single if (low > high) { printf ("Exiting (%d)n", i); break; } #pragma omp for nowait for (j = low; j < high; j++) c[j] = (c[j] - a[i])/b[i]; }

Parallélisme de contrôle n Nous n’avons vu jusqu’à maintenant que le parallélisme de données

Parallélisme de contrôle n Nous n’avons vu jusqu’à maintenant que le parallélisme de données n Open. MP permet ausi d’affecter différentes portions du code à différents threads

Exemple v = alpha(); w = beta(); x = gamma(v, w); y = delta();

Exemple v = alpha(); w = beta(); x = gamma(v, w); y = delta(); printf ("%6. 2 fn", epsilon(x, y));

Le pragma “parallel sections” n Précède un bloc contenant k blocs devant être exécutés

Le pragma “parallel sections” n Précède un bloc contenant k blocs devant être exécutés en parallèle par k threads (1 thread par section) n Syntaxe: #pragma omp parallel sections

Le pragma “section” n Précède chacun des blocs à l’intérieur d’un bloc couvert par

Le pragma “section” n Précède chacun des blocs à l’intérieur d’un bloc couvert par le pragma “parallel sections” n Peut être omis pour le premier bloc n Syntaxe: #pragma omp section

Exemple #pragma omp parallel sections { #pragma omp section /* Optionnel */ v =

Exemple #pragma omp parallel sections { #pragma omp section /* Optionnel */ v = alpha(); #pragma omp section w = beta(); #pragma omp section y = delta(); } x = gamma(v, w); printf ("%6. 2 fn", epsilon(x, y));

Autre approche Deux blocs: • alpha et beta • gamma et delta

Autre approche Deux blocs: • alpha et beta • gamma et delta

Le pragma “sections” n Apparaît à l’intérieur d’un bloc de couvert par le pragma

Le pragma “sections” n Apparaît à l’intérieur d’un bloc de couvert par le pragma “parallel” n Possède la même signification que le pragma “parallel sections” n Ajoute de la flexibilité à la façon d’organiser le parallélisme

Exemple #pragma omp parallel { #pragma omp sections { v = alpha(); #pragma omp

Exemple #pragma omp parallel { #pragma omp sections { v = alpha(); #pragma omp section w = beta(); } #pragma omp sections { x = gamma(v, w); #pragma omp section y = delta(); } } printf ("%6. 2 fn", epsilon(x, y));

Quelques limitations avant Open. MP 3. 0 2 exemples: 1. Parcours d’une liste chaînée

Quelques limitations avant Open. MP 3. 0 2 exemples: 1. Parcours d’une liste chaînée 2. Appels récursifs

Parcours d’une liste chaînée Une solution serait de transformer la liste en tableau: (perte

Parcours d’une liste chaînée Une solution serait de transformer la liste en tableau: (perte de temps)

Parcours d’une liste chaînée Autre solution: (les threads boucles inutilement)

Parcours d’une liste chaînée Autre solution: (les threads boucles inutilement)

Appels récursifs • La création récursive des threads requiert trop de ressources • Comment

Appels récursifs • La création récursive des threads requiert trop de ressources • Comment obtenir une charge de travail équilibrée ?

La directive « task » #pragma omp task bloc d’instructions Une tâche est une

La directive « task » #pragma omp task bloc d’instructions Une tâche est une instance d’une partie de code exécutable et de la mémoire associée. Une tâche est générée lorsqu’un thread rencontre une directive task ou parallel

La directive « task » #pragma omp task bloc d’instructions Lorsqu’un thread rencontre une

La directive « task » #pragma omp task bloc d’instructions Lorsqu’un thread rencontre une directive task il créé une tâche avec le bloc d’instructions suivant qu’il peut exécuter immédiatement ou reporter à plus tard. Une tâche reportée peut être exécutée par n’importe quel thread du groupe actifs

Exemple 1 #pragma omp parallel { #pragma omp sections { #pragma omp section printf("1

Exemple 1 #pragma omp parallel { #pragma omp sections { #pragma omp section printf("1 "); #pragma omp section printf("2 "); #pragma omp section printf("3 "); #pragma omp section printf("4 "); } } bash$. /a. out 3124

Exemple 2 #pragma omp parallel { #pragma omp task printf("1 %d: ", omp_get_thread_num()); #pragma

Exemple 2 #pragma omp parallel { #pragma omp task printf("1 %d: ", omp_get_thread_num()); #pragma omp task printf("2 %d: ", omp_get_thread_num()); #pragma omp task printf("3 %d: ", omp_get_thread_num()); #pragma omp task printf("4 %d: ", omp_get_thread_num()); } bash$. /a. out 1 0: 2 0: 3 0: 4 0: 1 0: 2 0: 3 0: 4 6: 1 6: 2 0: 3 6: 2 7: 3 7: 4 2: 4 6: 1 2: 1 6: 4 7: 3 3: 2 2: 2 6: 3 0: 4 2: 2 4: 4 5: 4 7: 3 6:

Exemple 3 #pragma omp parallel { #pragma omp single { #pragma omp task printf("1

Exemple 3 #pragma omp parallel { #pragma omp single { #pragma omp task printf("1 %d: ", omp_get_thread_num()); #pragma omp task printf("2 %d: ", omp_get_thread_num()); #pragma omp task printf("3 %d: ", omp_get_thread_num()); #pragma omp task printf("4 %d: ", omp_get_thread_num()); } } bash$. /a. out 1 6: 2 7: 3 5: 4 1:

Ex. Parcours d’une liste chaînée

Ex. Parcours d’une liste chaînée

Ex. Parcours de plusieurs listes

Ex. Parcours de plusieurs listes

Ex. Fonction récursive int fib(int n) { int a, b; if (n<2) return n;

Ex. Fonction récursive int fib(int n) { int a, b; if (n<2) return n; #pragma omp task shared(a) a=fib(n-1); #pragma omp task shared(b) b=fib(n-2); #pragma omp taskwait return a+b; } Comment doit-on appeler fib la première fois?

Est-ce que le code suivant est correct? #include <math. h> void nowait_example 2(int n,

Est-ce que le code suivant est correct? #include <math. h> void nowait_example 2(int n, float *a, float *b, float *c, float *y, float *z) { int i; #pragma omp parallel { #pragma omp for schedule(static) nowait for (i=0; i<n; i++) c[i] = (a[i] + b[i]) / 2. 0 f; #pragma omp for schedule(static) nowait for (i=0; i<n; i++) z[i] = sqrtf(c[i]); #pragma omp for schedule(static) nowait for (i=1; i<=n; i++) y[i] = z[i-1] + a[i]; } }