• Aucun résultat trouvé

7.2 Modèles de programmation pour accélérateurs graphiques

7.2.3 OpenCL, l’alternative mal-aimée

OpenCL [60], abréviation de « Open Compute Language », est une spécification logicielle destinée à faciliter les calculs sur tout type d’accélérateurs et, notamment, des processeurs 104

7.2. Modèles de programmation pour accélérateurs graphiques

centraux ou des processeurs graphiques. Il se démarque ainsi de CUDA, dont il reprend le modèle d’exécution, en étant moins limité quant aux cibles matérielles visées. OpenCL a fait son apparition en 2008 [59], soit un an après CUDA, et est depuis également activement soutenu et développé. La version courante d’OpenCL, dévoilée en avril 2016, est numérotée 2.2 [57].

Le projet OpenCL est géré par un consortium regroupant plusieurs industriels du monde des accélérateurs matériels. Ce consortium se nomme le Khronos Group et est, en outre, en charge d’un ensemble de technologies spécialisées ou non, comme les interfaces pour graphisme 3D OpenGL [61] et Vulkan [65]. Les spécifications développées par le Khronos Group sont libres d’accès et peuvent être implémentées sans contraintes par les fabricants désirant rendre leur matériel compatible.

OpenCL n’est pas limité qu’aux processeurs graphiques. Plusieurs implémentations pour FPGAs existent [3, 118]. En outre, OpenCL peut également cibler les processeurs multi- cœurs [73], en l’absence d’accélérateur graphique, ce qui permet à OpenCL de concurrencer le modèle de programmation OpenMP.

Architecture et modèle de programmation

OpenCL n’est, à proprement parler, pas un logiciel, mais une spécification : charge aux constructeurs souhaitant la respecter d’en fournir une implémentation viable. La spécification OpenCL peut se diviser en deux composantes :

deux langages de programmation appelés OpenCL C et OpenCL C++, respectivement

sous-ensemble de C et de C++, afin d’écrire des noyaux de calcul ;

une interface de programmation pour les langages C et C++ servant à préciser le contexte

d’exécution des noyaux de calcul.

... ... ... ... ... ... ... ... ... ... ... ... Host Compute Device Compute Unit Processing Element

Figure 7.2 – Modèle d’architecture OpenCL

Le modèle de programmation d’OpenCL est similaire à celui de CUDA : un processeur hôte associé à une mémoire hôte déporte des calculs sur un ou plusieurs accélérateurs matériels, ainsi qu’illustré figure 7.2. Un accélérateur, aussi nommé Compute Device dans la terminologie OpenCL, se compose de plusieurs Compute Units (CU), elles-mêmes composées de Processing Elements (PE), ces derniers réalisant effectivement les calculs.

7. L’approche GPGPU : le framework OpenCL

Les applications OpenCL ont une structure similaire à celle des applications CUDA : d’une part, des noyaux de calculs ou « kernels » exécutés par un accélérateur matériel et écrits en OpenCL C ou en OpenCL C++, d’autre part, un code application exécuté par l’hôte, faisant appel à l’interface de programmation C ou C++ et servant à allouer et libérer la mémoire sur l’accélérateur, transférer les données entre la mémoire de l’hôte et celle de l’accélérateur, ainsi que lancer les kernels sur les données ainsi transférées. Cependant, et au contraire de CUDA, les kernels OpenCL sont écrits dans un langage de programmation à part et doivent, par conséquent, être compilés séparément, par défaut au début de l’exécution du programme, par un compilateur spécifique à l’accélérateur cible, ceci assurant la portabilité du code sur différentes architectures. L’exécution d’une application OpenCL revient donc à effectuer la séquence d’actions suivantes :

1. compilation des noyaux de calcul vers l’accélérateur ;

2. transfert des données d’entrée depuis l’hôte vers l’accélérateur ; 3. lancement des calculs sur l’accélérateur ;

4. transfert des données résultat depuis l’accélérateur vers l’hôte.

Exemple : addition de deux vecteurs

1 __kernel void vecAdd(__global float *a,

2 __global float *b,

3 __global float *c, 4 const unsigned int n) { 5

6 unsigned int id = get_global_id(0);

7

8 if (id < n)

9 c[id] = b[id] + a[id];

10 }

Extrait 7.3 – Noyau de calcul OpenCL minimal

Un exemple de kernel OpenCL C réalisant une addition vectorielle est représenté en extrait 7.3. Comme l’exemple présenté en sous-section 7.2.2, celui-ci consiste à additionner la composante id de deux vecteurs a et b et à stocker le résultat dans un troisième vecteur c. Ce kernel, instancié sur tous les PEs d’un accélérateur, additionnera toutes les composantes des deux vecteurs a et b dans la limite de leur taille n.

Une portion de code utilisant l’interface de programmation C pour OpenCL destinée à lancer le kernel précédent est représentée extrait 7.4. La mémoire sur l’accélérateur pour les vecteurs a, b et c est allouée en utilisant la fonction

1 cl_int clCreateBuffer();

puis les opérandes a et b sont envoyés vers l’accélérateur dans les buffers alloués à l’étape précédente. Le kernel est compilé grâce à la fonction

1 cl_int clBuildProgram();

et ses arguments sont passés à l’aide de la fonction 106

7.2. Modèles de programmation pour accélérateurs graphiques

69 // Build the program executable

70 clBuildProgram(prgm, 0, NULL, NULL, NULL, NULL); 71

72 // Create the compute kernel in the program we wish to run

73 knl = clCreateKernel(prgm, "vecAdd", &err); 74

75 // Create the input and output arrays in device memory

76 d_a = clCreateBuffer(ctx, CL_MEM_READ_ONLY, bytes, NULL, NULL);

77 d_b = clCreateBuffer(ctx, CL_MEM_READ_ONLY, bytes, NULL, NULL); 78 d_c = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, bytes, NULL, NULL); 79

80 // Write our data set into the input array in device memory

81 err = clEnqueueWriteBuffer(q, d_a, CL_TRUE, 0, bytes, 82 h_a, 0, NULL, NULL); 83 err |= clEnqueueWriteBuffer(q, d_b, CL_TRUE, 0, bytes,

84 h_b, 0, NULL, NULL); 85

86 // Set the arguments to our compute kernel

87 err |= clSetKernelArg(knl, 0, sizeof(cl_mem), &d_a);

88 err |= clSetKernelArg(knl, 1, sizeof(cl_mem), &d_b); 89 err |= clSetKernelArg(knl, 2, sizeof(cl_mem), &d_c); 90 err |= clSetKernelArg(knl, 3, sizeof(unsigned int), &n);

91

92 // Execute the kernel over the entire range of the data set 93 err = clEnqueueNDRangeKernel(q, knl, 1, NULL, &globalSize, 94 &localSize, 0, NULL, NULL);

95

96 // Wait for the command queue 97 clFinish(q);

98

99 // Read the results from the device

100 clEnqueueReadBuffer(q, d_c, CL_TRUE, 0, bytes, h_c, 0, NULL, NULL);

Extrait 7.4 – Code C minimal pour exécuter le kernel extrait 7.3

1 cl_int clSetKernelArg(cl_kernel kernel, cl_uint arg_index, 2 size_t arg_size, const void *arg_value);

appelée une fois pour chaque argument. Enfin, la fonction

1 cl_int clEnqueueNDRangeKernel();

lance l’exécution du kernel sur l’accélérateur selon une topologie définie par les variables globalSize et localSize, avant le retour du vecteur résultat via l’appel à

1 cl_int clEnqueueReadBuffer();

En sus de ce mode de fonctionnement séquentiel, OpenCL permet l’exécution asynchrone des kernels au travers d’un système d’événements permettant de définir des dépendances entre les exécutions des kernels.

7. L’approche GPGPU : le framework OpenCL

Comparaison avec CUDA

Déporter des calculs sur accélérateur graphique revient aujourd’hui à choisir entre CUDA et OpenCL. Le premier, réservé aux produits de la marque Nvidia, dispose d’une interface plus agréable pour les développeurs et de performances meilleures sur le matériel concerné. Quant à OpenCL, sa généricité rend son interface plus verbeuse : les portions de code présentées en extrait 7.2 et extrait 7.4 réalisent toutes deux le transfert de trois tableaux et l’exécution d’un kernel, mais la version OpenCL comporte plus de deux fois plus de lignes. L’utilisation de l’interface Python au travers du module PyOpenCL [81] peut, là, faciliter le travail des développeurs sans perdre en performance.

OpenCL étant un standard supporté par un nombre important de constructeurs, les ap- plications l’utilisant sont portables : elles peuvent s’exécuter sans modifications sur tous les accélérateurs compatibles. Néanmoins, la spécification OpenCL autorise l’utilisation d’exten- sions propriétaires qui peuvent faciliter la programmabilité sur certaines plateformes aux dépends de la portabilité. En outre, puisque les cibles matérielles d’OpenCL sont parfois très différentes, il est difficile de conserver les performances en changeant de type d’accélérateur, obligeant parfois à réécrire certains kernels. Au contraire, les performances des applications CUDA sont relativement stables sur les GPUs de la marque Nvidia.

Enfin, CUDA dispose d’un vaste écosystème d’outils et de bibliothèques, telles cuBLAS [34], cuFFT [36] et cuDNN [35], qui favorisent le développement d’applications complexes. Les outils de développement CUDA simplifient ainsi le débogage d’applications, tâche d’autant plus complexe que les calculs sont déportés. OpenCL ne fournit en comparaison pas d’outils de ce type.

Évolutions futures

Le standard OpenCL, sous l’impulsion du Khronos Group, est en évolution constante. La prochaine version 2.2 voit ainsi l’arrivée du nouveau langage OpenCL-C++ [58] pour écrire les kernels, auparavant limités à OpenCL-C. Ce nouveau langage apporte des fonctionnalités de haut niveau issues du langage C++, comme les fonctions lambda, les templates ou la surcharge d’opérateurs.

Pour pallier le manque de performance entre les différentes plateformes cibles d’OpenCL, Khronos a développé un langage intermédiaire appelé SPIR-V [63] (pour « Standard Por-

table Intermediate Representation »). Celui-ci vise à devenir la représentation intermédiaire

commune aux kernels OpenCL ainsi qu’aux shaders OpenGL et Vulkan. En outre, il profite des transformations et optimisations du compilateur LLVM. De cette façon, les pilotes des accélérateurs matériels n’auront, en entrée, qu’à supporter ce langage de bas niveau.

OpenCL évolue ainsi vers plus de simplicité pour les constructeurs voulant implémenter la spécification ou bien les développeurs voulant l’utiliser. Cependant, des interfaces de programmation de plus haut niveau, en chantier depuis quelques années, promettent de s’affranchir de la séparation entre code hôte et kernels déportés.