• Aucun résultat trouvé

Pour savoir quel processus est en cours d’exécution et quel est le nombre de processus exécutant le programme, les fonctions this_image et num_images peuvent être utilisées. Par défaut, l’indice des images commence à 1.

1.5

Les GPGPU et les accélérateurs

Enfin, un dernier modèle de programmation parallèle consiste à déporter les calculs sur des accélérateurs matériels tels que notamment les GPGPU. Pour ce faire, il est souvent nécessaire (1) d’écrire les noyaux de calculs dans un langage spécifique aux GPGPU, tel que CUDA, décrit en section 1.5.1, ou OpenCL, décrit ensection 1.5.2, et (2) de gérer les données à envoyer pour effectuer les calculs et les résultats à recevoir. Certaines bibliothèques permettent de générer automatiquement ces noyaux de calcul et de communications, tel que OpenACC présentée ensection 1.5.3, ou OpenMP présentée ensection 1.5.4.

De large quantité de données doivent être traitées de façon similaire pour que ce modèle fonctionne et apporte des performances.

1.5.1

CUDA

Le langage CUDA, Compute Unified Device Architecture [77], est développé par NVidia. Il est actuellement le leader pour la programmation sur GPGPU. Mais, il est propriétaire et ne peut s’exécuter que sur des GPU de NVidia.

CUDA possède un compilateur spécifique pour ces programmes, nvcc. De plus, de nombreuses bibliothèques de programmation très performantes existent dont notamment cuBLAS [79] pour l’algèbre linéaire et cuSPARSE [80] pour les opérations sur les vecteurs et matrices creuses. De même, plusieurs outils de débogage et de profilage sont fournis par NVidia pour aider les programmeurs à optimiser leur code [78]. CUDA utilise la terminologie de grille pour parler d’une exécution sur le GPGPU. Une grille est composée d’une mémoire qui est appelée mémoire globale et d’un ensemble de blocs. Un bloc est lui-même composé d’une mémoire appelée mémoire partagée et d’un ensemble de threads.

Les fonctions CUDA s’écrivent comme des fonctions C classiques. Pour dif- férentier l’architecture sur laquelle s’exécutent les fonctions, des qualificatifs peuvent être ajoutés à leur déclaration. __global__ indique que la fonction est exécutée sur le GPGPU, le device, mais doit être appelée depuis l’hôte, l’host. Il correspond au noyau, kernel, a exécuté sur le GPGPU. __device__ est utilisé quand la fonction est exécutée et appelée depuis le device. __host__, pour une exécution et appel depuis l’host, est le qualificatif par défaut. De plus, les appels de noyaux depuis l’hôte doivent être accompagnés d’une configuration définie avec des triples chevrons≪Dg, Db≫. Dg décrit la grille de calcul sur laquelle le noyaux va s’exécuter en indiquant le nombre de blocs présents sur la grille en 1D, 2D voire 3D. Db décrit chaque bloc présent dans la grille en indiquant le nombre de fils d’exécution, thread, à exécuter en 1D, 2D voire 3D.

Les variables présentes sur le GPGPU peuvent se trouver sur différents types de mémoire. Les variables en paramètre peuvent être sur la mémoire globale ou constante du GPGPU avec les qualificatifs respectifs __device__ et __constant__. Les variables déclarées au sein d’une fonction sont par défaut mises en registre si possible, sinon elles se retrouvent dans la mémoire globale du GPGPU, le temps de son exécution. Le qualificatif __shared__ peut être utilisé

pour utiliser la mémoire au niveau des blocs qui est beaucoup plus rapide que la mémoire globale du GPGPU. Dépendantes du placement en mémoire choisi, les performances ne sont pas les mêmes. Un accès à une variable en __shared__ est plus rapide qu’un accès pour une variable en __device__. La gestion de la mé- moire du GPGPU s’effectue à l’aide des fonctions cudaMalloc, cudaFree. Les transferts de données entre CPU et GPGPU se font avec la fonction cudaMemcpy.

1.5.2

OpenCL

Le langage OpenCL, Open Compute Language [59][60], est un langage per- mettant l’exécution d’un programme sur tout type de matériel, aussi bien des processeurs que des cartes graphiques ou divers accélérateurs. Il a l’avantage d’être libre et est porté par le consortium Khronos [58] regroupant un grand nombre d’acteurs aussi bien académiques qu’industriels.

OpenCL décrit l’architecture sur laquelle il intervient avec les termes sui- vants :

— un NDRange fait référence à une exécution sur le matériel désiré, il est composé d’une mémoire appelée mémoire globale et d’un ensemble de

work group, il correspond à une grille sur CUDA ;

— un work group est composé d’une mémoire appelée mémoire locale et d’un ensemble de work item, il correspond à un bloc CUDA, sa mémoire locale correspond à la mémoire partagée en CUDA ;

— un work item possède une mémoire appelée mémoire privée1correspond

à un thread CUDA.

Du fait de sa portabilité sur tous types de plateforme, un plus grand nombre de fonctions de configuration sont nécessaires. Ainsi, la création d’un contexte définissant le matériel sur lequel va s’exécuter le noyau est donné avec la fonction clCreateContext ou clCreateContextFromType. De plus, une queue de com- mandes, permettant d’envoyer et recevoir les données de même que de lancer le noyau, est créée avec la fonction clCreateCommandQueue et nécessite le contexte précédemment défini. De même, la compilation du noyau se fait dynamiquement au moyen des fonctions successives suivantes clCreateProgramWithSource qui a besoin du contexte précédent et du code source du programme à exécuter sur le matériel distant ; clBuildProgram qui compile celui-ci ; et clCreateKernel qui définit le noyau. Le code écrit en OpenCL contenant la fonction qui correspond au noyau devra avoir le qualificatif __kernel.

Pour définir sur quelle mémoire les données doivent être allouées, les quali- ficatifs __global, __constant, __local et __private correspondant aux dif- férents niveaux de mémoire sont disponibles. Les variables présentes dans la mémoire globale et constante de l’accélérateur matériel doivent être déclarées et allouées depuis l’hôte au moyen des fonctions clCreateBuffer en utilisant le contexte et clEnqueueWriteBuffer en utilisant une queue de commande. Tous les arguments à envoyer à l’accélérateur et servant à faire les calculs du noyau sont définis à l’aide de clSetKernelArg qui nécessite le noyau, le rang de l’argument, sa taille et sa valeur. La demande d’exécution du noyau s’ef- fectue en ajoutant le noyau à la queue de commande au moyen de la fonction clEnqueueNDRangeKernel où la dimension et le découpage des work group et

1. lorsque l’architecture réelle ne possède pas de mémoire privée au niveau du work item celle-ci peut prendre place dans la mémoire globale, dépendant de l’implémentation d’openCL sous-jacente

1.5. LES GPGPU ET LES ACCÉLÉRATEURS

work item sont faits. clFinish permet d’attendre la fin de l’exécution du noyau

et clEnqueueReadBuffer de récupérer les résultats voulus.

1.5.3

OpenACC

OpenACC, Open Accelerator [82][83], est une bibliothèque de parallélisation en C, C++et Fortran. Elle regroupe un ensemble de directives permettant à du code de s’exécuter sur des GPGPU sans avoir à écrire dans un langage spécifique tel que CUDA ou OpenCL. Elle est née de la volonté d’offrir un accès plus simple à la programmation sur GPGPU tout en ayant des performances comparables à des programmes écrits dans des langages spécifiques. Cela ne l’empêche pas de pouvoir interagir avec d’autres bibliothèques ou langages orientés GPGPU. Ainsi toutes les bibliothèques citées ensection 1.2et les langages cités précédemment dans cette section peuvent interagir.

OpenACC repose sur un ensemble très restreint de directives qui peuvent être appliquées. La plus importante est #pragma acc kernels qui permet de demander au compilateur de générer des noyaux pour le GPGPU lorsque c’est possible. Si le compilateur ne juge pas possible de générer des noyaux, à cause de dépendances dans une boucle par exemple, il ne le fera pas.

Tous les éléments des noyaux seront transférés, entre l’hôte et l’accélérateur, à chaque entrée et sortie d’un noyau, ce qui affecte souvent les performances. Pour régler ce problème la directive #pragma acc data peut être utilisée. Les clauses copy, copyin et copyout sont à utiliser pour indiquer si les variables doivent être envoyées à l’accélérateur au début de la clause et/ou reçues de l’accélérateur à la fin de la clause.

Si le programmeur est certain que les itérations d’une boucle sont indépen- dantes les une des autres, la directive #pragma acc loop accompagnée de la clause independent peut être utilisée. Elle permet au compilateur de s’affran- chir du calcul de dépendances sur cette boucle. De plus, les clauses private ou reduction permettent d’indiquer si des variables sont privées à une itération de boucle ou qu’une réduction s’applique sur certaines d’entre elles. Contrairement à OpenMP, elles sont optionnelles et sont souvent automatiquement déduites par le compilateur. Les clauses collapse ou tile permettent quant à elles de regrouper plusieurs nids de boucles en un seul ou de les redécouper selon les paramètres indiqués. Enfin, la possibilité de répartir les différentes itérations dans différents groupes pour effectuer les calculs est possible avec les clauses gang, worker et vector. Ces dernières servent aussi bien pour obtenir de la localité sur l’accélérateur qu’à utiliser au maximum toutes les unités de calculs disponibles.

1.5.4

OpenMP 4.x

Une introduction d’OpenMP a déjà été faite ensection 1.2.2 présentant no- tamment son utilisation historique en mémoire partagée. Mais depuis la version 4.0 sortie en 2013 et 4.5 en 2015, de nouvelles directives ont été ajoutées pour profiter de la présence d’un accélérateur tel qu’un GPGPU.

La philosophie d’OpenMP par rapport à OpenACC est relativement diffé- rente, bien qu’elles utilisent toutes deux des directives pour effectuer la parallé- lisation.

OpenMP requiert que le programmeur garantisse la correction de son code lorsqu’il utilise des directives OpenMP. Par exemple, c’est au programmeur d’assurer la présence ou l’absence de dépendances de données. Le programmeur indique ce qui doit ou ne doit pas être parallélisé et le compilateur génère le code correspondant sans nécessairement vérifier la correction du code demandé. OpenACC permet au programmeur d’indiquer explicitement ce qui doit être parallélisé, mais aussi de faire des suggestions de parallélisation au compilateur. Dans ce dernier cas, le compilateur devra réaliser plusieurs analyses pour dé- terminer si un code parallèle peut être généré et avec quelles caractéristiques : données à communiquer, répartition des données sur l’accélérateur, etc.

Chacune de ces deux approches a ses avantages et inconvénients. En OpenMP, le compilateur est relativement simple mais beaucoup d’efforts sont déportés sur le programmeur. Alors qu’en OpenACC, le programmeur a juste à indiquer ce qu’il aimerait paralléliser et le compilateur doit se charger de vérifier leur faisa- bilité. Malgré ces différences, OpenMP et OpenACC tentent de converger vers une norme commune. Plusieurs membres participent aux deux commissions de spécification d’OpenMP et OpenACC.

La directive OpenMP permettant d’indiquer quelle partie du code doit être exécutée sur un accélérateur est #pragma omp target. Elle est l’équivalente de la directive OpenACC #pragma acc kernels. La directive OpenMP peut avoir comme argument map(...) permettant de lister explicitement les variables qui doivent être envoyées et/ou reçues de l’accélérateur. Cette distinction se fait avec les indications to, from et tofrom.

Si plusieurs noyaux sont présents successivement, pour limier les commu- nications effectuées, la directive #pragma omp target data permet de définir l’environnement que l’accélérateur utilisera. Elle s’accompagne de l’argument map présenté précédemment. Elle correspond à la directive OpenACC #pragma acc data.

Les autres directives classiques de OpenMP présentées dans lasection 1.2.2

peuvent être utilisées au sein des instructions exécutées par l’accélérateur.

Documents relatifs