• Aucun résultat trouvé

S OLVEURS LIN EAIRES CREUX SUR UN ´ GPU

LIN EAIRES CREUX ´

Algorithme 7 : Multiplication matrice-vecteur avec le format HYB

3.2/ S OLVEURS LIN EAIRES CREUX SUR UN ´ GPU

Dans cette section, nous pr ´esentons les principaux points cl ´es de la mise œuvre des solveurs CG et GMRES sur un seul processeur graphique GPU pour r ´esoudre des syst `emes lin ´eaires creux. Puis, nous effectuons une comparaison de leurs performances obtenues sur un GPU avec celles obtenues sur un processeur traditionnel CPU. Nous utilisons l’environnement de programmation CUDA pour le codage des kernels GPU.

3.2.1/ MISE EN ŒUVRE SUR UN GPU

Une mise en œuvre efficace des algorithmes CG et GMRES sur un GPU n ´ecessite, tout d’abord et avant tout, de d ´eterminer toutes les parties de leurs codes qui peuvent ˆetre ex ´ecut ´ees en parall `ele et ainsi, b ´en ´eficier d’une acc ´el ´eration sur le GPU. Comme la plupart des m ´ethodes des sous-espaces de Krylov, les it ´erations de CG et de GMRES sont, principalement, bas ´ees sur des op ´erations arithm ´etiques vectorielles : multiplica-tions matrice-vecteur, multiplicamultiplica-tions scalaire-vecteur, produits scalaires, normes eucli-diennes, op ´erations AXPY (y ← ax + y o `u x et y sont des vecteurs et a un scalaire ), etc. Ces op ´erations sont souvent faciles `a parall ´eliser et plus efficaces sur des architectures parall `eles lorsqu’elles op `erent sur de grands vecteurs. Donc, toutes les op ´erations vec-torielles des algorithmes CG et GMRES doivent ˆetre ex ´ecut ´ees par le GPU sous formes de kernels.

Dans les codes CG et GMRES ´ecrits en CUDA, la d ´eclaration de chaque kernel est pr ´ec ´ed ´ee par une ent ˆete __global__ ou __device__ qui permet de le diff ´erencier des fonctions s ´equentielles `a ex ´ecuter par le CPU. En fait, CUDA utilise les qualificatifs __global__ou __device__ pour d ´efinir les fonctions `a ex ´ecuter par le GPU et appel ´ees par le CPU ou par le GPU, respectivement. Tout appel d’un CPU `a un kernel __global__ doit sp ´ecifier la configuration de la grille de threads `a ex ´ecuter par le GPU en utilisant la syntaxe suivante :

≪nbBlocs, nbT hreads ≫

o `u nbBlocs est le nombre de blocs de threads dans la grille et nbT hreads est le nombre de threads par bloc. Le nombre de blocs de threads dans la grille d’un kernel est, g ´en ´eralement, dict ´e par le nombre de donn ´ees `a traiter. Dans nos mises en œuvre GPU, nous avons utilis ´e la formule (3.17) pour calculer ce nombre, nbBlocs, de fac¸on `a ce que nous ayons un thread par ligne de matrice ou un thread par ´el ´ement de vecteur :

nbBlocs = n + nbT hreads − 1

nbT hreads , (3.17)

o `u n d ´efinit la taille du syst `eme lin ´eaire `a r ´esoudre.

Le code pr ´esent ´e dans la figure 3.1 illustre un exemple de programme ´ecrit en CUDA permettant de calculer un produit scalaire-vecteur. La premi `ere ligne du kernel Produit_Scalaire_Vecteur()permet de calculer un indice unique pour chaque thread en fonction de ses coordonn ´ees dans la grille de threads. Dans cet exemple, les co-ordonn ´ees des threads et celles des blocs de threads sont d ´efinies sur une seule di-mension (axe de coordonn ´ees X) : threadIdx.x et blockIdx.x, respectivement. La va-riable blockDim.x d ´efinit la taille de chaque bloc de threads sur une dimension et elle est ´equivalente `a nbThreads dans cet exemple. Par ailleurs, le nombre de blocs de threads dans la grille est d ´efini par le CPU en utilisant la formule (3.17) (ligne 3 du programme

/* Kernel `a ex´ecuter par le GPU */ __global__

void Produit_Scalaire_Vecteur(int n, double a, double *x, double *y) {

1: int id = blockIdx.x * blockDim.x + threadIdx.x; //ID du thread dans la grille 2: if(id < n)

3: y[id] = a * x[id]; }

/* Programme principal `a ex´ecuter par le CPU */ void main(int argc, char** argv)

{

1: int n = 256 * 256 * 256; //Taille du vecteur

2: int nbThreads = 512; //Nombre de threads par bloc 3: int nbBlocs = (n + nbThreads - 1) / nbThreads; //Nombre de blocs de threads 4: double *x, *y, alpha;

5: //Initialiser le scalaire alpha

6: //Allouer les espaces m´emoires GPU pour les vecteurs x et y 7: //Initialiser les ´el´ements du vecteur x dans la m´emoire GPU

...

8: Produit_Scalaire_Vecteur<<<nbBlocs,nbThreads>>>(n, alpha, x, y); ...

}

FIGURE3.1 – Exemple de programme CUDA pour le calcul d’un produit scalaire-vecteur

principal). A la ligne 8, le CPU fait appel au kernel Produit_Scalaire_Vecteur() qui permet de multiplier le scalaire alpha par le vecteur x et de mettre le r ´esultat dans le vec-teur y. Enfin, la troisi `eme ligne du kernel montre que le calcul du produit scalaire-vecvec-teur est effectu ´e en parall `ele par n threads CUDA tel que chaque thread soit en charge d’un ´el ´ement de chacun des vecteurs x et y.

Pour la mise en œuvre GPU de certaines op ´erations vectorielles des solveurs CG et GMRES, nous avons utilis ´e les kernels standardis ´es de la biblioth `eque CUBLAS (CUDA Basic Linear Algebra Subroutines) d ´evelopp ´ee par nVIDIA [27]. Cette biblioth `eque poss `ede un ensemble de fonctions, mises en œuvre en CUDA, permettant de r ´ealiser des op ´erations de base de l’alg `ebre lin ´eaire. Donc, nous avons utilis ´e de cette biblioth `eque les kernels suivants : cublasDdot() pour les produits scalaires, cublasDnrm2() pour les normes euclidiennes et cublasDaxpy() pour les op ´erations AXPY. Pour le reste des op ´erations parall ´elisables, nous avons cod ´e leurs kernels en CUDA. Pour le solveur CG, nous avons d ´evelopp ´e un kernel pour l’op ´eration XPAY (y ← x+ay) utilis ´ee `a la ligne 11 de l’algorithme 8. Pour le solveur GMRES, nous avons programm ´e un kernel pour les multi-plications scalaire-vecteur (lignes 7 et 15 de l’algorithme 9), un kernel pour la r ´esolution du probl `eme de moindres carr ´es (linge 18) et un kernel pour la mise `a jour des valeurs du vecteur solution x (ligne 19).

La r ´esolution du probl `eme de moindres carr ´es dans la m ´ethode GMRES est bas ´ee sur :

– une factorisation QR de la matrice de Hessenberg ¯Hm en utilisant des rotations planes,

– une r ´esolution par des substitutions arri `ere pour calculer y qui minimise au mieux le r ´esidu.

donn ´ees entre threads CUDA. Ceci signifie que son ex ´ecution en parall `ele sur un GPU n’est pas int ´eressante. Cependant, le probl `eme de moindres carr ´es `a r ´esoudre dans la m ´ethode GMRES avec red ´emarrage poss `ede, g ´en ´eralement, une taille m tr `es petite. Par cons ´equent, nous lui avons d ´evelopp ´e un kernel de r ´esolution peu co ˆuteux qui doit ˆetre ex ´ecut ´e en s ´equentiel par un seul thread CUDA.

Par ailleurs, l’op ´eration la plus importante des solveurs CG et GMRES est la mul-tiplication matrice creuse-vecteur. C’est une op ´eration co ˆuteuse en termes de temps d’ex ´ecution et d’espace m ´emoire. De plus, elle effectue des acc `es irr ´eguliers `a la m ´emoire pour lire les valeurs non nulles de la matrice creuse. Ceci signifie que sa mise en œuvre sur un GPU utilisera des acc `es non coalescents `a la m ´emoire globale qui ra-lentiront encore davantage ses performances. A cet effet, nous avons utilis ´e le format compress ´e HYB (voir section 2.2.4) pour le stockage de la matrice creuse en m ´emoire globale GPU. Il donne, en g ´en ´eral, de bonnes performances pour le calcul de la multipli-cation matrice creuse-vecteur sur les GPUs [14]. Par cons ´equent, nous avons utilis ´e le kernel HYB de la biblioth `eque CUPS [1] d ´evelopp ´e par nVIDIA pour la mise en œuvre de cette multiplication sur le GPU. Par contre, pour la mise en œuvre sur un CPU, nous avons utilis ´e le format de stockage CSR (voir section 2.2.2) pour le calcul de la multipli-cation matrice creuse-vecteur.

L’ex ´ecution des solveurs CG et GMRES sur le GPU n ´ecessite un chargement de donn ´ees du syst `eme lin ´eaire creux `a r ´esoudre de la m ´emoire CPU vers la m ´emoire GPU, `a savoir : la matrice creuse A, le vecteur solution initiale x0, le vecteur second membre bet la matrice de pr ´econditionnement M. Pour cela, nous avons utilis ´e les deux routines CUBLAS cublasAlloc() et cublasSetVector(), respectivement, pour l’allocation et le chargement de donn ´ees dans la m ´emoire globale GPU. Ensuite, durant la r ´esolution du syst `eme lin ´eaire, le processus CPU agit comme un contr ˆoleur de l’ex ´ecution de la boucle principale (Tant que ... faire) du solveur lin ´eaire creux (CG ou GMRES) et le GPU ex ´ecute tous les kernels cod ´es `a l’int ´erieur de cette boucle. Enfin, la solution du syst `eme lin ´eaire creux x est copi ´ee de la m ´emoire GPU vers la m ´emoire CPU en utilisant la routine CUBLAS cublasGetVector().

La figure 3.2 montre les prototypes des routines CUBLAS que nous avons utilis ´ees pour la mise en œuvre des m ´ethodes CG et GMRES sur un GPU. Elles sont d ´efinies comme suit :

– cublasAlloc() permet de r ´eserver un espace m ´emoire GPU de n ´el ´ements, chacun ayant une taille de T ailleElement octets. Si l’allocation m ´emoire est r ´ealis ´ee avec succ `es, un pointeur vers cet espace m ´emoire r ´eserv ´e est plac ´e dans la variable VectGPU,

– cublasSetVector() permet de copier n ´el ´ements du vecteur VectCPU dans la m ´emoire CPU vers le vecteur VectGPU dans la m ´emoire GPU. Les ´el ´ements des deux vecteurs VectCPU et VectGPU ont une taille de T ailleElement octets chacun. L’ ´ecart m ´emoire espac¸ant deux ´el ´ements cons ´ecutifs est incx dans le vecteur source VectCPUet incy dans le vecteur destination VectCPU,

– cublasGetVector() a le m ˆeme r ˆole que la routine cublasSetVector(). Cependant, elle permet de copier n ´el ´ements, chacun ayant T ailleElement octets, du vecteur source VectGPU dans la m ´emoire GPU vers le vecteur destination VectCPU dans la m ´emoire CPU,

– cublasDdot() permet de calculer sur un GPU le produit scalaire de deux vecteurs xet y, de n ´el ´ements chacun en double pr ´ecision. Le r ´esultat de ce produit est plac ´e dans la variable res,

/* Allocation m´emoire GPU */

cublasAlloc(int n, int TailleElement, void **VectGPU); /* Copie de la m´emoire CPU vers la m´emoire GPU */ cublasSetVector(int n, int TailleElement,

const void *VectCPU, int incx, void *VectGPU, int incy );

/* Copie de la m´emoire GPU vers la m´emoire CPU */ cublasGetVector(int n, int TailleElement,

const void *VectGPU, int incx, void *VectCPU, int incy );

/* Produit scalaire de deux vecteurs en double pr´ecision */

double res = cublasDdot(int n, const double *x, int incx, const double *y, int incy); /* Norme euclidienne d’un vecteur en double pr´ecision */

double res = cublasDnrm2(int n, const double *x, int incx); /* Op´eration AXPY de deux vecteurs en double pr´ecision */

cublasDaxpy(int n, double alpha, const double *x, int incx, double *y, int incy); /* Lib´eration de la m´emoire GPU */

cublasFree(const void *VectGPU);

FIGURE3.2 – Routines CUBLAS utilis ´ees pour la mise en œuvre sur un GPU – cublasDnrm2() permet de calculer sur un GPU la norme euclidienne d’un vecteur

xde n ´el ´ements en double pr ´ecision. La norme calcul ´ee est plac ´ee dans la variable res,

– cublasDaxpy() permet de r ´ealiser sur un GPU une op ´eration AXPY entre deux vecteurs x et y, de n ´el ´ements chacun en double pr ´ecision. Elle multiplie le vecteur xpar le scalaire double pr ´ecision alpha puis, additionne le r ´esultat au vecteur y, – cublasFree() permet de lib ´erer l’espace m ´emoire GPU r ´ef ´erenc ´e par le pointeur

VectGPU.

3.2.2/ EXPERIMENTATIONS SUR UN´ CPU ´EQUIPE D´ ’UNGPU

Nous avons test ´e les performances des deux solveurs lin ´eaires creux CG et GMRES sur un CPU de type Xeon E5620 ´equip ´e d’une carte graphique GPU Fermi C2070. Le processeur Xeon E5620 est un Quad-Core cadenc ´e `a 2, 4 GHz, poss ´edant 12 Go de RAM avec un d ´ebit m ´emoire de 25, 6 Go/s. Le GPU Fermi C2070 poss `ede 448 cœurs cadenc ´es `a 1, 15 GHz et une m ´emoire globale de 6 Go avec un d ´ebit m ´emoire de 144 Go/s. Nous avons utilis ´e le langage de programmation C pour les mises en œuvre CPU et GPU des deux solveurs et l’environnement de programmation CUDA version 4.0 pour le codage des kernels GPU.

Tous les tests ont ´et ´e effectu ´es sur des op ´erations `a virgule flottante double pr ´ecision. Les param `etres de r ´esolution des deux solveurs ont ´et ´e initialis ´es comme suit : le seuil de tol ´erance r ´esiduelle ε = 10−12, le nombre maximum d’it ´erations max = 500, tous les ´el ´ements du vecteur solution initiale ont ´et ´e initialis ´es `a 0 et ceux du vecteur se-cond membre ont ´et ´e initialis ´es `a 1. De plus, pour le solveur GMRES, nous avons

li-thermal2

cage13 language poli_large torso3

2cubes_sphere

crashbasis FEM_3D_thermal2

ecology2 finan512 G3_circuit shallow_water2

FIGURE3.3 – Structures des matrices creuses choisies dans la collection de l’universit ´e de Floride

mit ´e le processus d’Arnoldi `a 16 it ´erations (m = 16). Pour des raisons de simplicit ´e, nous avons choisi de prendre, pour chaque syst `eme lin ´eaire `a r ´esoudre, une matrice de pr ´econditionnement M ´equivalente `a la diagonale principale de la matrice creuse A. En effet, ceci nous permet de calculer ais ´ement l’inverse de la matrice M et il permet de fournir un pr ´econditionnement relativement bon pour la plupart des matrices creuses qui ne sont pas tr `es mal conditionn ´ees. La taille des blocs de threads pour le calcul GPU est initialis ´ee `a 512 threads. Enfin, les r ´esultats de performance pr ´esent ´es dans ce chapitre sont obtenus `a partir de la valeur moyenne de dix ex ´ecutions du m ˆeme solveur pour les m ˆemes donn ´ees d’entr ´ee.

Nous avons test ´e les deux solveurs CG et GMRES sur des matrices r ´eelles choisies dans la collection de l’universit ´e de Floride [30]. En effet, cette collection contient un ensemble de matrices creuses provenant d’un large ´eventail d’applications concr `etes du monde r ´eel. Nous avons choisi de cette collection, au total, douze matrices creuses : six matrices sym ´etriques et six matrices asym ´etriques. La figure 3.3 montre la structure de ces douze matrices creuses et le tableau 3.1 pr ´esente leurs principales caract ´eristiques, `a savoir : le nombre de lignes de matrice, le nombre de valeurs non nulles et la largeur de bande maximale. La largeur de bande d’une matrice est d ´efinie comme le nombre de colonnes de matrice qui s ´eparent la premi `ere et la derni `ere valeur non nulle dans une ligne de matrice.

Les tableaux 3.2 et 3.3 montrent, respectivement, les performances des solveurs CG et GMRES pour la r ´esolution des syst `emes lin ´eaires associ ´es aux matrices creuses pr ´esent ´ees dans le tableau 3.1. Cependant, le tableau 3.2 pr ´esente uniquement les per-formances de r ´esolution des syst `emes lin ´eaires sym ´etriques, en raison de l’incapacit ´e de la m ´ethode CG de r ´esoudre les syst `emes asym ´etriques. Les deuxi `eme et troisi `eme colonnes des deux tableaux 3.2 et 3.3 donnent, respectivement, les temps d’ex ´ecution en secondes sur un cœur CPU (T empscpu) et sur un GPU (T empsgpu). Toutefois, nous prenons en consid ´eration le gain relatif, τ, d’une m ´ethode de r ´esolution mise en œuvre sur un GPU par rapport `a la m ˆeme m ´ethode mise en œuvre sur un CPU. En effet, les gains relatifs, pr ´esent ´es dans la quatri `eme colonne de chaque tableau, sont calcul ´es

Type Nom de # Lignes # Valeurs Largeur de matrice la matrice de matrice non nulles de bande

2cubes sphere 101.492 1.647.264 100.464

ecology2 999.999 4.995.991 2.001

Sym ´etrique finan512 74.752 596.992 74.725

G3 circuit 1.585.478 7.660.826 1.219.059

shallow water2 81.920 327.680 58.710

thermal2 1.228.045 8.580.313 1.226.629

cage13 445.315 7.479.343 318.788

crashbasis 160.000 1.750.416 120.202 Asym ´etrique FEM 3D thermal2 147.900 3.489.300 117.827

language 399.130 1.216.334 398.622

poli large 15.575 33.074 15.575

torso3 259.156 4.429.042 216.854

TABLE 3.1 – Principales caract ´eristiques des matrices choisies de la collection de l’uni-versit ´e de Floride

Matrice T empscpu T empsgpu τ # iter prec ∆

2cubes sphere 0, 071s 0, 011s 6, 22 12 1, 14e − 09 3, 47e − 18 ecology2 0, 324s 0, 030s 10, 76 13 5, 06e − 09 1, 11e − 16 finan512 0, 028s 0, 006s 4, 81 12 3, 52e − 09 2, 22e − 16 G3 circuit 0, 697s 0, 064s 10, 84 16 4, 16e − 10 5, 55e − 16 shallow water2 0, 009s 0, 002s 5, 23 5 2, 24e − 14 3, 88e − 26 thermal2 0, 762s 0, 081s 9, 37 15 5, 11e − 09 3, 33e − 16 TABLE3.2 – Performances du solveur CG sur un cœur CPU vs. sur un GPU

Matrice T empscpu T empsgpu τ # iter prec ∆

2cubes sphere 0, 198s 0, 028s 7, 17 21 2, 10e − 14 4, 34e − 18 ecology2 1, 316s 0, 093s 14, 18 21 4, 30e − 13 9, 28e − 14 finan512 0, 089s 0, 015s 6, 10 17 3, 21e − 12 5, 55e − 16 G3 circuit 2, 271s 0, 157s 14, 45 22 1, 04e − 12 2, 37e − 14 shallow water2 0, 083s 0, 013s 6, 36 17 5, 42e − 22 1, 68e − 25 thermal2 2, 053s 0, 165s 12, 44 21 6, 58e − 12 3, 33e − 16 cage13 1, 003s 0, 088s 11, 40 26 3, 37e − 11 1, 75e − 14 crashbasis 1, 454s 0, 150s 9, 66 121 9, 24e − 12 1, 48e − 12 FEM 3D thermal2 0, 963s 0, 098s 9, 82 64 3, 87e − 09 1, 82e − 12 language 2, 464s 0, 226s 10, 91 90 1, 18e − 10 1, 16e − 10 poli large 0, 062s 0, 035s 1, 78 69 5, 00e − 11 1, 36e − 12 torso3 4, 053s 0, 373s 10, 86 175 2, 69e − 10 1, 78e − 14 TABLE3.3 – Performances du solveur GMRES sur un cœur CPU vs. sur un GPU

sous formes de rapports entre les temps d’ex ´ecution T empscpu et T empsgpu comme suit : τ = T empscpu

T empsgpu

A partir de ces gains relatifs, nous pouvons remarquer que la r ´esolution de syst `emes lin ´eaires creux sur un GPU est plus rapide que sur un CPU. Ceci est d ˆu au fait que le GPU permet d’acc ´el ´erer le calcul des deux solveurs (CG et GMRES) en ex ´ecutant cha-cune de leurs op ´erations vectorielles en parall `ele par un grand nombre de threads. Pour l’ensemble des tests effectu ´es, les gains relatifs de CG et de GMRES sur un GPU sont en moyenne, respectivement, de 7 et 9. Cependant, la r ´esolution d’un syst `eme lin ´eaire creux sym ´etrique avec la m ´ethode CG, que ce soit sur un CPU ou sur un GPU, est plus rapide qu’avec la m ´ethode GMRES (en moyenne 4 fois plus rapide sur ces exemples). En effet, CG poss `ede une convergence meilleure (moins d’it ´erations) que celle de GMRES et, les it ´erations sont calcul ´ees plus rapidement avec CG qu’avec GMRES. De plus, nous pouvons aussi remarquer que les gains relatifs obtenus avec GMRES sont sensiblement meilleurs que ceux obtenus avec CG. Ceci revient au fait que la m ´ethode CG, par rap-port `a GMRES, est bas ´ee sur un ensemble d’op ´erations moins compliqu ´ees et faciles `a r ´ealiser sur un processeur traditionnel CPU.

En plus des temps d’ex ´ecution et des gains relatifs, nous donnons dans les ta-bleaux 3.2 et 3.3 le nombre d’it ´erations, iter, effectu ´ees pour r ´esoudre un syst `eme lin ´eaire creux, la pr ´ecision, prec, de la solution calcul ´ee sur le GPU et la diff ´erence, ∆, entre la solution calcul ´ee sur le CPU et celle calcul ´ee sur le GPU. Les deux param `etres ∆ et prec permettent de valider et de v ´erifier la pr ´ecision de la solution calcul ´ee sur le GPU. Nous les avons calcul ´es comme suit :

∆ = max|xcpu− xgpu|, (3.19)

prec = max|M−1rgpu|, (3.20)

o `u ∆ est l’ ´el ´ement maximum, en valeur absolue, de la diff ´erence entre les deux solutions xcpu et xgpu calcul ´ees, respectivement, sur un CPU et sur un GPU et prec est l’ ´el ´ement maximum, en valeur absolue, du vecteur r ´esiduel rgpu∈ Rnde la solution xgpu. Ainsi, nous pouvons constater que les solutions obtenues sur le GPU ont ´et ´e calcul ´ees avec une pr ´ecision suffisante (environ 10−10) et qu’elles sont, plus ou moins, ´equivalentes `a celles calcul ´ees sur le CPU avec une faible diff ´erence variant entre 10−10et 10−26.

Enfin, nous pouvons remarquer que le GPU est moins efficace pour la r ´esolution de syst `emes lin ´eaires creux de petites tailles. Par exemple, nous pouvons constater dans les tableaux 3.2 et 3.3 que les gains relatifs obtenus des matrices finan512, shallow -water2 et poli large sont faibles par rapport `a ceux des autres matrices. En effet, les petites tailles des syst `emes lin ´eaires creux ne permettent pas de maximiser l’utilisation des cœurs GPU (voir section 1.2.3.1) et, ainsi, d’exploiter au mieux les ressources et la puissance de calcul du GPU. Dans la section suivante, nous ´etudierons les performances des solveurs CG et GMRES pour la r ´esolution de syst `emes lin ´eaires creux de grandes tailles et ce, en exploitant plusieurs GPUs.