Luiz Angelo Steffenel Université de Reims Champagne-Ardenne Vendredi 29 mai 2009 Plan Pourquoi faire du parallélisme ? C’est quoi une GPGPU ? Programmation GPU Hiérarchie de Threads et de Mémoire Le GPU pour les problèmes combinatoires 2 Pourquoi faire du parallélisme ? Pourquoi utiliser le parallélisme ? Pour dépasser la loi de Moore Pour les différents besoins du calcul intensif : large volumes de données problèmes de simulation 4 Pourquoi utiliser le parallélisme ? 5 Évaluation Accélération Efficacité Échange de données et collaboration Scalabilité Plus de processeurs plus rapide ? Loi d'Amdhal Coût des parties séquentielles 6 C’est quoi une GPGPU ? Accélérateurs Non, pas ce type … 8 Accélérateurs Dans le calcul à haute performance (HPC), un accélérateur est un matériel dont le rôle est d’augmenter la performance d’un type d’opération. Dans les années 80, les supercalculateurs (mainframes) avaient parfois des processeurs vectoriels, qui faisaient des opérations parallèles sur un vecteur de données (SIMD) Les PCs avaient des co-processeurs mathématiques : des processeurs dédiés pour le calcul à point flottant. Plus récemment, Field Programmable Gate Arrays (FPGAs) permettent la reprogrammation des fonctions du chip. 9 Avantages et Désavantages des Accélérateurs Les accélérateurs sont bons car : Ils rendent votre application plus rapide Les accélérateurs sont mauvais car : Coûtent cher ; Ne sont pas faciles à programmer ; Rarement un code fait pour un accélérateur est portable : on risque de perdre tout l’investissement lorsque la technologie/mode change. 10 Le Roi des Accélérateurs Parmi tous les options, le champion est sans doute le processeur graphique (GPU). 11 Pourquoi les GPUs ? Les GPUs ont été créées afin d’accélérer les tâches liées au rendu d’images. Ont gagné une immense popularité initialement parmi les joueurs, car une GPU peut rendre des images plus définies et avec un taux de rafraîchissement bien plus élevé. Premiers GPUs vers 1998-1999. Les prix sont devenus très abordables Intégrés dans la plupart des ordinateurs moyenne gamme Quelques centaines d’euros pour un modèle haut de gamme 12 Les GPUs sont populaires Le projet d’un processeur est très cher Une usine de fabrication coûte vraiment cher Centaines de millions de dollars Milliards de dollars Mais pas chers à produire En 2006 – 2007, autour de 80 millions de cartes graphiques ont été vendues par an, avec un profit autour de $20 milliards de dollars par an. Ça veut dire que les fabricants de GPU ont largement pu payer les coûts de développement. 13 Qu’est-ce que qu’une GPU fait ? Les GPUs ont été développées pour des applications en imagerie telles que l’application d’ombres et de textures. Ces opérations sont normalement faites avec des opérations arithmétiques de point flotant – le même type d’opération que le calcul haute performance utilise. 14 15 Puissance des GPUs ROMEO II Cluster constitué de 11 noeuds et d'un total de 108 cœurs Machines avec 8, 16 et 32 cœurs (Itanium II). Puissance de pic : 614 giga flops Calculateur NVIDIA® Tesla™ S1070 4 GPUs de 240 processeurs chacune Puissance de pic : 4,14 tera flops 16 Programmation GPU C’est difficile de programmer une GPU? Jusqu’à très récemment, la programmation GPU était faite : Pour programmer une GPU à d’autres fins, on était obligés à faire passer les données pour des éléments graphiques Soit à travers un langage pour le rendu graphique comme OpenGL, ou Programmation de bas niveau (langage d’assemblage). charger une matrice comme si c’était une texture, par exemple Dur, dur… Alors presque personne n’essayait. Maintenant, les fabricants ont lancé des APIs destinées au calcul général sur GPU (GPGPU) Encore plus de $$ pour les fabricants ;) 18 Comment programmer une GPU ? APIs ou langages propriétaires NVIDIA : CUDA (C/C++) AMD/ATI : StreamSDK/Brook+ (C/C++) OpenCL (Open Computing Language) : un nouveau standard industriel pour le calcul GPGPU. Compilateurs Fortran et C (Portland Group) avec directives de programmation directement reconnues par le compilateur. 19 NVIDIA CUDA Approche propriétaire de NVIDIA Issue de la “Compute Unified Device Architecture” de NVIDIA Principe : des extensions au langage C pour mieux contrôler les ressources des GPUs Nombre limité d’instructions Oblige une importante réécriture du code source N’est pas disponible pour le langage Fortran Les physiques/chimistes/etc normalement ne font que ça 20 Exemple CUDA Partie 1 // example1.cpp : Defines the entry point for the console applicati on. // #include "stdafx.h" #include <stdio.h> #include <cuda.h> // Kernel that executes on the CUDA device __global__ void square_array(float *a, int N) { int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx<N) a[idx] = a[idx] * a[idx]; } 21 Exemple CUDA Partie 2 // main routine that executes on the host int main(void) { float *a_h, *a_d; // Pointer to host & device arrays const int N = 10; // Number of elements in arrays size_t size = N * sizeof(float); a_h = (float *)malloc(size); // Allocate array on host cudaMalloc((void **) &a_d, size); // Allocate array on device // Initialize host array and copy it to CUDA device for (int i=0; i<N; i++) a_h[i] = (float)i; cudaMemcpy(a_d, a_h, size, cudaMemcpyHostToDevice); // Do calculation on device: int block_size = 4; int n_blocks = N/block_size + (N%block_size == 0 ? 0:1); square_array <<< n_blocks, block_size >>> (a_d, N); // Retrieve result from device and store it in host array cudaMemcpy(a_h, a_d, sizeof(float)*N, cudaMemcpyDeviceToHost); // Print results for (int i=0; i<N; i++) printf("%d %f\n", i, a_h[i]); // Cleanup free(a_h); cudaFree(a_d); } 22 AMD/ATI Brook+ Propriétaire de AMD/ATI Connu auparavant comme “Close to Metal” (CTM) Extension du langage C pour mieux contrôler les ressources des GPUs Comme CUDA, n’est pas disponible pour Fortran 23 Exemple Brook+ Partie 1 float4 matmult_kernel (int y, int x, int k, float4 M0[], float4 M1[]) { float4 total = 0; for (int c = 0; c < k / 4; c++) { total += M0[y][c] * M1[x][c]; } return total; } 24 Exemple Brook+ Partie 2 void matmult (float4 A[], float4 B’[], float4 C[]) { for (int i = 0; i < n; i++) { for (j = 0; j < m / 4; j+) { launch_thread{ C[i][j] = matmult_kernel(j, i, k, A, B’);} } } sync_threads{} } 25 OpenCL Open Computing Language Standard ouvert développé par le Khronos Group, qui est un consortium regroupant plusieurs constructeur (dont NVIDIA, AMD et Intel) Une version initial du standard OpenCL a été lancée en Décembre 2008. On attend que plusieurs compagnies lancent leurs implémentations Très probablement, la première version à sortir sera celle d’Apple, qui inclut OpenCL dans sont système Mac OS X v10.6 “Snow Leopard”, prévu cette année. 26 Exemple OpenCL Partie 1 // This kernel computes FFT of length 1024. The 1024 length FFT // is decomposed into calls to a radix 16 function, another // radix 16 function and then a radix 4 function kernel void fft1D_1024 ( global float2 *in, __global float2 *out, local float *sMemx, __local float *sMemy) { int tid = get_local_id(0); int blockIdx = get_group_id(0) * 1024 + tid; float2 data[16]; // starting index of data to/from global memory in = in + blockIdx; out = out + blockIdx; globalLoads(data, in, 64); // coalesced global reads 27 Exemple OpenCL Partie 2 fftRadix16Pass(data); // in-place radix-16 pass twiddleFactorMul(data, tid, 1024, 0); // local shuffle using local memory localShuffle(data, sMemx, sMemy, tid, (((tid & 15) * 65) + (tid >> 4))); fftRadix16Pass(data); // in-place radix-16 pass twiddleFactorMul(data, tid, 64, 4); // twiddle factor multiplication localShuffle(data, sMemx, sMemy, tid, (((tid >> 4) * 64) + (tid & 15))); // four radix-4 function calls fftRadix4Pass(data); fftRadix4Pass(data + 4); fftRadix4Pass(data + 8); fftRadix4Pass(data + 12); // coalesced global writes globalStores(data, out, 64); } 28 Exemple OpenCL Partie 3 // create a compute context with GPU device context = clCreateContextFromType(0, CL_DEVICE_TYPE_GPU, NULL, NULL, NULL); // create a work-queue queue = clCreateWorkQueue(context, NULL, NULL, 0); // allocate the buffer memory objects memobjs[0] = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(float)*2*num_entries, srcA); memobjs[1] = clCreateBuffer(context, CL_MEM_READ_WRITE, sizeof(float)*2*num_entries, NULL); // create the compute program program = clCreateProgramFromSource(context, 1, &fft1D_1024_kernel_src, NULL); // build the compute program executable clBuildProgramExecutable(program, false, NULL, NULL); // create the compute kernel kernel = clCreateKernel(program, "fft1D_1024"); 29 Exemple OpenCL Partie 4 // create N-D range object with work-item dimensions global_work_size[0] = n; local_work_size[0] = 64; range = clCreateNDRangeContainer(context, 0, 1, global_work_size, local_work_size); // set the args values clSetKernelArg(kernel, 0, (void *)&memobjs[0], sizeof(cl_mem), NULL); clSetKernelArg(kernel, 1, (void *)&memobjs[1], sizeof(cl_mem), NULL); clSetKernelArg(kernel, 2, NULL, sizeof(float)*(local_work_size[0]+1)*16, NULL); clSetKernelArg(kernel, 3, NULL, sizeof(float)*(local_work_size[0]+1)*16, NULL); // execute kernel clExecuteKernel(queue, kernel, NULL, range, NULL, 0, NULL); 30 Directives Portland Group Directives propriétaires en Fortran et C (Intel, etc) Similaire à OpenMP dans son structure Actuellement en version beta Si le compilateur ne comprends pas les directives, il les ignore A priori, cela peut marcher sur plusieurs modèles Le même code peut être exécuté sur GPU ou CPU, dans un compilateur Portland ou pas Pour l’instant, on n’a que l’annonce d’une version NVIDIA Un accord entre PGI et AMD/ATI a été annoncé Les directives indiquent au compilateur quelles parties du code doivent utiliser la GPU ; le reste du code s’exécute sur la CPU. 31 Exemple des directives PGI !$acc region do k = 1,n1 do i = 1,n3 c(i,k) = 0.0 do j = 1,n2 c(i,k) = c(i,k) + & a(i,j) * b(j,k) enddo enddo enddo !$acc end region 32 Quels sont les inconvénients ? Dans tous les cas (CUDA, Brook+, OpenCL, PGI) le code doit être réécrit CUDA/Brook+ : Propriétaires, C/C++ uniquement OpenCL : portable mais pas vraiment facile à comprendre Directives PGI : pour l’instant il n’est pas clair comment les blocs de code sont attribués aux GPUs. 33 Hiérarchie de Threads et Mémoire La programmation GPU reste complexe Une GPU est une unité de calcul créée pour le traitement d’images Opérations limitées Répartition des données Peu d’interaction avec la CPU et la mémoire principale 34 Structure CPU vs GPU Source : Nvidia CUDA Programming Guide Mot clé : Kernel Dans la programmation GPU, un kernel est un code (normalement une fonction) qui peut tourner sur la GPU. De manière générale, le code kernel est exécuté de manière synchrone par les processeurs dans le GPU. 36 Mot clé : Thread Une thread est une instance d’exécution du kernel avec un indice. Chaque thread use son indice pour accéder une partie spécifique des éléments d’un vecteur cible Ceci permet que l’ensemble de threads travaille sur la totalité des données en parallèle. Les threads peuvent avoir des variables partagées et des variables privées 37 Mot clé : Block Un block est un groupe de threads. Isolées, les threads peuvent être exécutées de manière concurrent ou indépendante, sans un ordre précis. Un block permet de coordonner les threads, utilisant des fonctions telles que _syncthreads() comme une barrière Ceci permet de resynchroniser les threads avant de passer à la prochaine étape de l’application 38 Mot clé : Grid Un grid est un groupe de blocks. Les grids sont totalement indépendants les uns des autres 39 Hiérarchie des GPUs NVIDIA Grids représentent les GPUs Blocks représentent les unités de calcul ( MultiProcesseurs) Un block est restreint à un seul MP, mais un MP peut avoir plusieurs blocks Threads sont les Stream Processors (SP) Warps sont groupes de (32) threads qui exécutent simultanement Hiérarchie de la mémoire Mémoire de la machine (serveur x86) Énorme et leeeeeeente 400/600 cycles pour y accéder Mémoire du dispositif (GPU) Global: visible par toutes les threads dans tous les blocks – grande et +-lente Shared: visible par toutes les threads dans un block – taille et vitesse moyennes (4 cycles d’horloge) Local: visible uniquement par la thread - rapide et petite 41 Hiérarchie de la mémoire Et encore (GPU) Constant: mémoire à lecture-seule, visible par toutes les threads Texture: mémoire à lectureseule, visible par toutes les threads 42 Exemple : GPU Nvidia Geforce 8 et 9 Nombre de multi-processeurs : 2 à 16 Nombre de processeurs / multi-processeurs : 8 processeurs de flux Taille : 32 threads, max : 512 threads Recommandation : 64 threads (optimiser les coûts d'accès mémoire) Nombre de registres / multi-processeurs : 8192 Mémoire partagé : 16 Ko, dans 16 banks 43 Programmation Performante Aujourd’hui, le plus important goulot d’étranglement pour les cartes GPU est l’interface PCIe : PCIe 2.0 x16: 8 GB/sec 1600 MHz Front Side Bus: 25 GB/sec GDDR3 GPU RAM: 102 GB/sec per carte Et il faut : Au début, déplacer les données de la mémoire RAM du serveur vers la RAM de la GPU. Faire la plupart du travail sur la GPU. Utiliser le serveur x86 uniquement pour les entrées/sorties et passage de message (réseau), ce qui réduit le transfert de données à travers l’interface PCIe. 44 Des applications qui ont réussi http://www.nvidia.com/object/cuda_home.html#state=home 45 Le GPU pour les problèmes combinatoires Pourquoi ? Faible coût du matériel Grand nombre de processeurs SAT est un prototype de problème combinatoire Père de tout les problèmes NP-complet (Cook, 71) Au cœur de la conjecture P ≠ NP 46 Le Problème SAT SAT est un problème de décision oui (s'il y a une solution) ou non Une formule SAT est une conjonction de disjonction (X1 ou X2 ou -X3) ET (-X1 ou X4 ou X3) 47 SAT en pratique Conception de processeurs Validation des bases de connaissances Physique statique Robotique Reconstruction d'images ... 48 Définition de SAT Une formule logique est représentée sous la forme normale conjonctive (CNF) Soit X={x1, ..., xn} un ensemble de n variables booléennes. Soit C={c1, ..., cm} un ensemble de m clauses. CNF : une conjonction de clauses Une clause c est une disjonction de littéraux un littéral l est une variable x signée, l=x ou l=-x Exemple de clause : ci=-x1∨x2∨x3 49 Définition de SAT Un littéral x est satisfait ssi x=true Un littéral -x est satisfait ssi x=false Une clause c=l1∨...∨lj , est satisfaite ssi ∃ li satisfait avec i ∈ [1,j] Un problème est satisfiable ssi chaque clause de C l'est 50 Solveurs Méthode complète (basée sur une recherche arborescente) : DPLL KCNFS PSATO ... Méthode incomplète (recherche local) : Walksat (B. Selman, H. Kautz) GSAT (B. Selman, H. Levesque, D. Mitchell) 51 SAT sous forme matricielle Pourquoi ? Utiliser la puissance calculatoire du GPU. Principe : Une matrice représente l'ensemble des clauses Chaque ligne représente une clause Une affectation est un vecteur Un vecteur satisfait la formule, ssi le produit matriciel ne contient pas de zéro 52 Exemple 53 Produit matriciel sur GPU Multiplication de A x B=C Chaque bloc évalue une sous-matrice de C Chaque thread d'un bloc évalue un élément de la sous-matrice de C La grille est confiée aux multi-processeurs 54 SAT sur GPU Une approche complète Générer les affectations dans une matrice (2n) Faire le produit matriciel avec la formule SAT Évaluer le nombre de conflits pour chaque affectation Si une solution sans conflit existe : la formule est satisfiable Limites : taille de la mémoire, traitement de petite formule (n<16) 55 SAT sur GPU : approche parallèle Un ensemble de threads évalue différentes affectations Une itération de l'algorithme : Choisir aléatoirement une clause c insatisfaite par l'affectation Calculer l'amélioration des variables de c si max f(x) < 0 f(x) = #clauses_0_x - #clauses_1_x choisir aléatoirement une variable de la clause c sinon (avec une probabilité p) choisir une variable de c telle que max f(x) 56