Académique Documents
Professionnel Documents
Culture Documents
MÉMOIRE
pour l’obtention du
par
Thomas Jost
Composition du jury
Olivier Perrin Maı̂tre de Conférences à l’Université Nancy 2
Bernard Girau Professeur à l’Université Henri Poincaré – Nancy 1
Encadrants : Sylvain Contassot-Vivier Professeur à l’Université Henri Poincaré – Nancy 1
Stéphane Vialle Enseignant-chercheur à Supélec
Remerciements 4
Introduction 5
1 Présentation du problème 7
1.1 Problème étudié . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.1.1 Description mathématique du modèle de transport . . . . . . . . . . . . . 7
1.1.2 Modèle de calcul . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2 Algorithmes synchrones et asynchrones . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3 Intérêt des GPU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.3.1 Architecture matérielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.2 Aspects logiciels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Conclusion 33
Références 34
4 Remerciements
Remerciements
Je tiens tout d’abord à remercier Stéphane Vialle et Sylvain Contassot-Vivier pour leur aide,
leurs conseils et leur soutien dans ce projet. Merci également à Patrick Mercier, qui fait beau-
coup d’efforts pour que l’on puisse travailler dans de bonnes conditions sur le cluster de Supé-
lec. Un grand merci à Philippe Dessante pour ses conseils éclairés sur les méthodes numériques
utilisables dans ce projet. Merci à tous mes camarades de l’option IIC ainsi qu’à l’ensemble de
mes professeurs pour leur sympathie et la bonne humeur dans laquelle s’est passée toute l’an-
née 2008-2009. Merci également à tous les membres d’AlGorille pour l’accueil chaleureux qu’ils
m’ont réservé au sein de leur équipe.
Merci enfin à ma famille et tout spécialement à Katia pour son soutien et ses encourage-
ments de tout instant.
Introduction 5
Introduction
On cherche tout d’abord à déterminer s’il peut être intéressant d’utiliser des GPU pour ce
genre de calculs. En effet, bien que les GPU soient plus puissants que les CPU conventionnels
en termes de puissance brute, leur utilisation est plus délicate car elle nécessite une program-
mation très spécifique, en prenant en compte un parallélisme massif, et avec des contraintes
très fortes sur la mémoire et les transferts de données. On s’intéresse donc à la faisabilité de
l’écriture sur GPU d’un simulateur, ainsi qu’à ses performances tant en temps qu’en précision
de calcul.
Dans un second temps, l’objectif de ce stage est d’utiliser les GPU avec des algorithmes
asynchrones. Cette classe d’algorithmes est relativement nouvelle, et à ce jour il n’y a virtuel-
lement aucune étude concernant l’utilisation conjointe de ces algorithmes asynchrones et de
GPU. L’idée est donc de réaliser un simulateur de transport 3D d’espèces chimiques ; ce simula-
teur utilisera des algorithmes asynchrones pour les itérations et communications entre chacun
des nœuds du cluster sur lequel il fonctionnera, et chacun des nœuds de ce cluster utilisera une
carte GPU pour accélérer autant que possible les calculs qui lui sont attribués.
Au final, on aura donc deux niveaux d’asynchronisme, à des échelles temporelles très diffé-
rentes : au niveau des transferts réseau entre les différentes machines, et au niveau des trans-
ferts mémoire entre la RAM globale et la RAM du GPU.
Si l’on sait aujourd’hui que les algorithmes asynchrones sont plus efficaces que leurs équi-
valents synchrones dans le cadre des traitements itératifs parallèles, on ne sait pas encore com-
ment ils interagissent avec les GPU ou dans le cadre d’une architecture hybride CPU/GPU.
L’objet de ce stage est donc d’évaluer l’intérêt de cette approche, tant en termes d’accélération
brute qu’en termes de précision de calcul. Ce stage de recherche a commencé en mars 2009 et
se déroulera jusqu’à la fin du mois d’août 2009 au sein de l’équipe AlGorille du LORIA.
Dans ce rapport, nous détaillerons tout d’abord la nature du problème étudié, ainsi que les
algorithmes asynchrones et les GPU. Dans une seconde partie, nous présenterons ce que nous
avons accompli dans le cadre du calcul sur GPU, en décrivant les solutions déjà existantes, les
choix que nous avons faits et leurs motivations, les algorithmes utilisés, leur implémentation,
et les performances obtenues. Enfin, nous décrirons ce qu’il reste à faire jusqu’à la fin du stage.
6 Introduction
1. Présentation du problème 7
Chapitre 1
Présentation du problème
L’idée générale est de modéliser le déplacement de polluants en eaux peu profondes. L’ob-
jectif de cette simulation est de pouvoir observer les effets à long terme d’une pollution sur un
écosystème, ce qui est d’un grand intérêt pour la gestion de l’environnement. Le résultat de la
simulation est la concentration des différentes espèces chimiques considérées dans l’espace et
dans le temps.
Idéalement, une simulation réaliste du modèle de transport devrait être associée à un mo-
dèle hydrodynamique des courants marins : le modèle prend en entrée un champ de vélocité
des courants, et celui-ci devrait être calculé par un solveur hydrodynamique. Dans notre cas,
on supposera que ce champ de vélocité est constant ; on parlera ainsi de cours d’eau plutôt que
de mers ou d’océans.
Si l’on considère un problème à trois dimensions spatiales et réduit à deux espèces chi-
miques, on montre que l’équation (1.1) est équivalente au système de deux EDP suivant :
∂c 1
à ! µ ¶ µ ¶ µ ¶
∂t + ∇c 1 × a = ∇ · ((∇c 1 ) × d ) + R 1 (c, t ) (1.2)
∂c 2 ∇c 2 × a ∇ · ((∇c 2 ) × d ) R 2 (c, t )
∂t
8 1. Présentation du problème
Dans un premier temps, l’équation (1.2) est discrétisée dans le domaine temporel (méthode
d’Euler) et dans le domaine spatial (différences finies du second ordre). On obtient ainsi un
système d’équations aux dérivées ordinaires :
dU (t )
= f (U (t ), t ) (1.3)
dt
où U est le vecteur contenant le maillage de points utilisé pour le calcul et f une fonction non
linéaire.
Comme on utilise une intégration temporelle implicite, le système aux EDO précédent de-
vient :
U (t ) −U (t − h)
= f (U (t ), t ) (1.4)
h
où h est un pas de temps fixé. On peut alors réécrire cette équation :
g (U (t ),U (t − h), t ) = U (t − h) −U (t ) + h f (U (t ), t ) (1.5)
et le problème consiste maintenant à résoudre g (U (t ),U (t − h), t ) = 0, ou, en simplifiant l’écri-
ture, g (U (t ),C (t − h)) = 0.
En utilisant la méthode de Newton, on aboutit à un schéma de calcul itératif :
³ ´ ³ ´
U k+1 (t ) = U k (t ) − J −1 U k (t ) g U k (t ),C (t − h) (1.6)
Résoudre cette équation revient à résoudre un système linéaire à chaque itération. On peut
de plus noter que la matrice jacobienne est creuse, et que ses termes non-nuls sont répartis sur
plusieurs diagonales.
Néanmoins, le calcul de la matrice jacobienne elle-même est très complexe. On préférera
donc utiliser la méthode quasi-Newton : la matrice jacobienne n’est calculée qu’à la première
itération, et est réutilisée telle quelle ensuite. Ceci permet de réduire le temps de calcul, mais en
contrepartie le nombre d’itérations risque d’être beaucoup plus important qu’avec la méthode
de Newton classique.
Pour paralléliser le calcul, on utilisera la méthode multisplitting Newton. Cette méthode est
assez similaire aux techniques de décomposition par blocs : le domaine initial est découpé en
plusieurs sous-domaines, et chacun est assigné à un processeur différent.
Dans notre cas, il faut donc résourdre l’équation (1.7) à chaque itération. Pour simplifier les
notations, on la notera ainsi :
− J × δU = g (1.9)
Dans le cadre de la méthode de multisplitting, on découpe la matrice jacobienne en blocs,
et δU et g sont décomposés de manière similaire et compatible. Chaque processeur travaille
alors sur un système plus petit, comme représenté sur la figure 1.1.
Cette méthode, et tout ce problème en général, sont décrits plus en détail dans [BCMS06]
et [BCVC07].
1. Présentation du problème 9
Dans les algorithmes itératifs parallèles, les données et routines de calcul sont réparties sur
plusieurs processeurs. Ces algorithmes peuvent être divisés en deux grandes catégories : les
algorithmes synchrones et les algorithmes asynchrones.
Les algorithmes synchrones sont les plus connus, et actuellement quasiment les seuls à
être utilisés dans les milieux scientifiques. Le principe est de synchroniser chaque itération
en échangeant des données à la fin des itérations. Si les communications sont synchrones,
on parle d’algorithme SISC (Synchronous Iterations with Synchronous Communications). Si les
communications sont asynchrones, on parlera de la même manière d’algorithmes SIAC (Syn-
chronous Iterations with Asynchronous Communications).
Le déroulement d’un algorithme SISC est représenté sur la figure 1.2. On voit qu’il y a des
périodes d’inactivité dues aux échanges de données et aux calculs liés à la détections de conver-
gence (les communications sont représentées par des flèches). Les algorithmes SIAC sont un
peu plus rapides car les communications tendent à être masquées par les calculs, mais les pé-
riodes d’inactivité ne sont pas totalement supprimées pour autant.
L’avantage de ces algorithmes synchrones parallèles est que la convergence est exactement
la même que celle de leurs pendants séquentiels.
La classe d’algorithmes itératifs parallèles la plus générale est la classe de ceux dont les
10 1. Présentation du problème
itérations et les communications sont asynchrones. On parle alors d’algorithmes AIAC (Asyn-
chronous Iterations with Asynchronous Communications). Les algorithmes SISC et SIAC sont
alors des cas particuliers d’algorithmes AIAC.
Le déroulement d’un algorithme AIAC est représenté sur la figure 1.3. On constate que les
temps d’attente sont complètement supprimés : les processeurs envoient leurs données aux
autres dès qu’ils ont fini une itération et si, lorsqu’une itération est terminée, un processeur
n’a pas encore reçu de nouvelles données, il recommence avec les mêmes données plutôt que
d’attendre que les autres processeurs soient prêts. Avec cette méthode, le nombre d’itérations
est généralement plus important, mais comme on évite les pertes de temps dues aux synchro-
nisations, ces algorithmes sont souvent plus rapides que leurs équivalents synchrones (mais ce
n’est pas toujours le cas). On peut encore distinguer plusieurs sous-catégories d’algorithmes
AIAC (selon la manière dont on effectue les communications), mais ceci dépasse le cadre de ce
rapport.
La convergence des algorithmes AIAC est différente de celle des algorithmes séquentiels
équivalents : les opérateurs appliqués pendant les itérations doivent être contractants pour
être certain que le processus converge bien vers la solution du système.
Les algorithmes asynchrones, leur formalisme et leur implémentation sont décrits plus en
détails dans [BCMS06] et [BCVC07].
Sur le cluster de GPU de Supélec, chaque carte (de type GeForce 8800 GT) compte 14 multi-
processeurs (MP), et chaque multi-processeur compte 16 ko de mémoire partagée et 8 192 re-
gistres.
Les threads sont regroupés en blocs (auquel on peut donner une topologie 1D, 2D ou 3D).
Chaque bloc compte au plus 512 threads, et chaque bloc s’exécute sur un et un seul MP. Ainsi,
il est conseillé d’utiliser des blocs dont le nombre de threads :
– est multiple de 32, afin d’utiliser des warps complets ;
– divise 768, afin de pouvoir utiliser 100% d’un MP.
Dans la pratique, on utilise souvent des blocs de 256 threads. Chaque MP peut alors exécuter 3
blocs en parallèle, ce qui permet (si les threads n’utilisent pas trop de registres et de mémoire
partagée) d’utiliser pleinement ses capacités de calcul.
Les blocs sont ensuite regroupés dans des grilles, auquel on peut aussi donner une topolo-
gie 1D, 2D ou 3D. Les tailles limites des grilles sont beaucoup plus élevées (65 536 dans chaque
dimension).
Depuis tout thread, on peut accéder à la dimension de la grille et du bloc parent, ainsi qu’à
l’indice du bloc dans la grille et à l’indice du thread dans le bloc. On peut ainsi construire pour
chaque thread un identifiant unique, qui sert souvent à calculer l’emplacement mémoire où le
thread va travailler.
Cette hiérarchie thread-bloc-grille est représentée sur la Figure 1.5.
L’API CUDA permet de gérer tous les aspects de la programmation sur GPU :
– détection et initialisation des cartes GPU ;
– allocation de mémoire dans la RAM GPU ;
– transferts de données bidirectionnels entre RAM globale et RAM GPU ;
– lancement de kernels et attente de terminaison des exécutions asynchrones ;
– gestion des erreurs ;
– compatibilité avec les API graphiques OpenGL et Direct3D...
Il existe en fait 2 API, l’une de bas niveau (driver API) et l’autre de haut niveau (runtime API).
Elles sont mutuellement exclusives. Sauf cas particuliers, il vaut mieux utiliser l’API de haut
niveau, plus simple et fiable que l’autre.
Les API CUDA sont constituées de fonctions C/C++ standard, et peuvent être compilées
avec n’importe quel compilateur C/C++. Par contre, les kernels sont très différents : ils ne peu-
vent contenir qu’un sous-ensemble du C auquel s’ajoute un nombre fini de fonctions mathé-
matiques. Ce code doit de plus être compilé dans un format spécifique aux GPU. Il doit donc
être écrit dans des fichiers .cu et compilé avec nvcc, le compilateur dédié fourni par NVIDIA.
Normalement, les fichiers objets générés par nvcc peuvent être liés à ceux de gcc (par
exemple), de sorte que l’on peut appeler des fonctions C définies dans un fichier .cu depuis
n’importe quelle partie d’un programme C/C++. En pratique il y a cependant des problèmes :
nvcc est un « driver de compilateur » qui s’appuie sur le compilateur natif du système (gcc,
msvc...), mais avec des limitations assez fortes, notamment au niveau de C++ et des templates.
L’optimisation d’un programme CUDA peut être assez compliquée. En effet, contrairement
aux CPU, les GPU n’incluent pas de mécanisme de cache mémoire. Il faut donc être particu-
lièrement attentif aux accès mémoire, et notamment aux accès à la RAM qui est beaucoup
1. Présentation du problème 13
plus lente que la mémoire partagée ou que les registres d’un MP (d’un facteur variant pou-
vant atteindre 100). Il faut en particulier veiller à faire des accès alignés à la mémoire, à éviter
les conflits de banque, à ne faire que des accès ordonnés, etc.
Pour plus de détails, il est plus que recommandé de consulter la documentation officielle
de CUDA [NVI08], très complète et très bien conçue.
2. Solveur linéaire sur GPU 15
Chapitre 2
La première étape du stage a été l’élaboration d’un solveur linéaire sur GPU, qui constitue
l’élément central du problème traité. Dans cette partie, nous décrivons ce qui existait déjà dans
ce domaine ; puis nous explicitons les choix qui ont été faits lors de la conception du solveur,
ainsi que leurs motivations ; enfin, nous évaluons les performances du solveur afin de pouvoir
conclure sur son efficacité.
Au niveau des performances, il y a quelques soucis avec CUBLAS. Les opérations les plus
utilisées (produit de matrices denses...) ou les plus simples (produit scalaire...) sont très ra-
pides, d’autres le sont beaucoup moins. Il semble que la dimension des données est également
très importante : un padding pour atteindre un multiple de 32 peut être très intéressant [QO08].
Néanmoins, au fur et à mesure de la publication par NVIDIA de nouvelles versions de CUDA
et de CUBLAS, les choses vont en s’améliorant : les performances sont régulièrement revues à
la hausse. CUBLAS reste donc une solution intéressante pour faire rapidement des choses qui
fonctionnent ; l’optimisation manuelle du code peut être une étape ultérieure, à n’effectuer que
quand on est sûr de disposer d’un algorithme qui fonctionne.
16 2. Solveur linéaire sur GPU
MAGMA
MAGMA est un projet mené par l’université du Tennessee en collaboration avec les univer-
sités de Berkeley, du Colorado, et de Coimbra (Portugal) 1 .
Selon leur site : The MAGMA project aims to develop a dense linear algebra library similar
to LAPACK but for heterogeneous/hybrid architectures, starting with current "Multicore+GPU"
systems. Il s’agit donc d’utiliser à la fois le CPU et le GPU pour améliorer les performances
[DMP+ 08]. Cependant le code n’est pas disponible publiquement.
cudaztec
cudaztec est un projet mené par David Neckels, ingénieur chez Beckam Coulter, Colorado.
Le projet est décrit ainsi sur sa page d’accueil : Iterative solvers and preconditioners for the
GPU 2 . A priori c’est donc très intéressant... Mais dans la pratique, il n’y a pas encore grand
chose d’utilisable.
Pour le moment, un solveur utilisant l’algorithme GMRES est implémenté en utilisant CU-
BLAS et une implémentation externe de SPMV (Sparse Matrix-Vector Multiplication). L’auteur
note cependant que le calcul est plus lent sur le GPU que sur le CPU ; selon lui c’est parce que
CUBLAS est plus lent, mais les explications qu’il donne sont un peu douteuses : CUBLAS ferait
des transferts CPU/GPU à chaque opération. Dans la mesure où il y a des méthodes spécifiques
pour les transferts (cublasGetMatrix, cublasSetMatrix, cublasGetVector,
cublasSetVector), ce n’est pas très crédible : il semble plus probable que les méthodes utili-
sées par l’auteur ne soient pas très adaptées au GPU.
La compilation de l’exemple proposé est également problématique (sous Linux avec le SDK
CUDA 2.1 ; l’auteur travaille sous Windows, visiblement avec une version plus ancienne du
SDK).
GPUmatrix
GPUmatrix 3 est un projet visant à implémenter LAPACK sur GPU mené par Nicolas Bon-
neel, doctorant dans l’équipe REVES de l’INRIA Sophia-Antipolis.
Dans son état actuel, cette bibliothèque implémente les décompositions LU, QR, et de Cho-
lesky. Le code est assez fruste, peu commenté, visiblement non maintenu ; là encore il est prévu
pour compiler avec msvc mais ne compile pas avec gcc.
FLAME
FLAME (Formal Linear Algebra Method Environment) 4 est un projet très ambitieux visant
à créer un système permettant, à partir d’une description d’algorithme d’algèbre linéaire, de
générer un code optimal implémentant cet algorithme. Il s’agit notamment de déduire auto-
matiquement les dépendances entre les différentes opérations (basées sur BLAS) à effectuer
1. http://icl.cs.utk.edu/magma/index.html
2. http://code.google.com/p/cudaztec/
3. http://sourceforge.net/projects/gpumatrix
4. http://www.cs.utexas.edu/users/flame/index.html
2. Solveur linéaire sur GPU 17
CNC
CNC (Conccurent Number Cruncher) 5 est une implémentation sur GPU d’un general sparse
linear solver, développé par l’équipe ALICE du LORIA [BCLar]. Le code de cette librairie est
disponible sur le site du LORIA sous licence GPL.
Ses performances sont apparemment très bonnes : l’article le présentant indique que les
opérations de BLAS sur GPU sont entre 4 et 18 fois plus rapides que leurs équivalents sur CPU
(avec la librairie MKL d’Intel, très optimisée pour les processeurs récents avec SSE3). Mais s’il
est bien adapté aux matrices creuses peu ou pas structurées, il faudrait voir ce qu’il en est pour
des matrices structurées (diagonales) comme celle que l’on aura besoin de traiter. En effet,
Bruno Lévy, directeur de l’équipe ALICE, nous a fortement conseillé d’exploiter la structure des
matrices que nous utilisons, et donc d’utiliser autre chose que ce solveur.
Le code source disponible est prévu pour MS Visual Studio, il faudrait donc faire quelques
petites adaptations (peut-être simplement un Makefile ?) pour arriver à le compiler sous Linux.
Il est à noter que CNC est l’un des solveurs creux les plus connus dans la communauté des
développeurs CUDA, car il est l’un des premiers à bien fonctionner, et NVIDIA l’indique dans
sa liste des meilleurs projets utilisant CUDA. Il serait donc très intéressant pour nous d’envi-
sager d’intégrer dans CNC le solveur linéaire adapté aux matrices diagonales que nous avons
développé ; ceci n’est encore pour le moment qu’à l’état de projet.
Pour réaliser un solveur, on a le choix entre deux classes d’algorithmes : les méthodes di-
rectes et les méthodes itératives. Les premières sont potentiellement plus précises (car plus
exactes), mais plus lentes ; les secondes ont l’avantage d’être nettement plus rapides et généra-
lement moins coûteuses en mémoire. Il s’agit donc principalement d’un compromis vitesse/-
précision.
Cependant les GPU présentent une contrainte forte au niveau de la précision : les cartes à
notre disposition ne gèrent que le calcul sur des nombres flottants en simple précision (type
float, codé sur 32 bits). Alors que les flottants double précision (64 bits) sont parfaitement gé-
néralisés sur CPU et que beaucoup de monde envisage de passer à 128 bits pour encore gagner
en précision, les GPU « bas de gamme » sont donc nettement désavantagés.
De plus, l’implémentation des nombres flottants n’est pas totalement conforme à la norme
IEEE : si l’addition et la multiplication se font sans erreur, il y a une erreur d’arrondi lors des
divisions et des autres opérations (racine carrée, fonctions trigonométriques, etc.). Si l’on uti-
lise un algorithme utilisant la division, il y a alors un risque élevé de propagation de l’erreur
d’arrondi : des essais effectués par Stéphane Vialle indiquent que l’erreur relative par rapport
5. http://alice.loria.fr/index.php/publications.html?redirect=1&Paper=CNC@2008
18 2. Solveur linéaire sur GPU
aux calculs sur CPU est couramment de 10−6 mais peut facilement monter jusqu’à 10−4 voire
10−3 , ce qui est énorme.
La sensibilité aux erreurs d’arrondis est donc essentielle dans le choix de l’algorithme à
utiliser.
Souvent ces méthodes nécessitent plus de mémoire que les méthodes itératives. Il convient
donc de les utiliser avec prudence.
Décomposition QR
Ax = QR x = b ⇐⇒ R x = Q −1 b = Q T b
R étant triangulaire, Rx = Q T b se calcule très facilement (méthode BLAS appropriée par exem-
ple).
On voit cependant qu’il faut stocker deux matrices à la place d’une seule : cette méthode
est donc très gourmande en mémoire. Pour des systèmes de taille importante, ceci peut rendre
la méthode difficilement utilisable, et donner un avantage à des méthodes itératives.
La partie difficile de cette méthode est le calcul de Q et R. Il existe au moins trois méthodes
permettant de les trouver [Wik09] [Des06] :
– méthode de Gram-Schmidt : relativement sensible aux erreurs d’arrondis ;
– rotations de Givens : facilement parallélisable, mais nécessite beaucoup de produits de
matrices, d’accès mémoire et de calculs trigonométriques (peu précis) ;
– méthode de Householder : la plus stable numériquement.
De manière générale, la méthode de Householder est la plus utilisée. Elle a une complexité
en O (n 3 ), mais est connue pour être celle qui résiste le mieux aux matrices mal conditionnées,
car elle limite le nombre de divisions par des petits nombres.
Sur le principe, toutes les méthodes itératives fonctionnent de la même manière : pour ré-
soudre un système Ax = b, on construit une suite de vecteur (x k )k∈N qui converge vers x. À par-
tir d’une hypothèse initiale x 0 , on calcule donc successivement les différents termes de la suite,
données par une fonction de type x k+1 = f (x k , A, b), et on s’arrête lorsque l’erreur kb − Axk est
suffisament faible (ou que l’on a atteint un nombre maximum d’itérations, ou que l’erreur de-
vient stable et ne diminue plus...).
A priori, les méthodes de Jacobi et de Gauss-Seidel semblent être bien adaptées. Elles sont
bien connues, très documentées, et efficaces ; elles sont de plus relativement simples à mettre
en œuvre, aussi bien sur CPU que sur GPU.
Pour chercher à avoir de meilleures conditions de convergence, nous nous sommes égale-
ment intéressés à l’algorithme du gradient biconjugué. Celui-ci reste également relativement
simple à mettre en œuvre, mais l’on dispose de moins de résultats théoriques quant à ses per-
formances ou ses conditions de convergence.
En pratique, on prend pour M la diagonale de A. On montre alors que x k+1 peut se calculer
composante par composante :
à !
1
x ik+1 = a i j x kj
X
bi −
ai i j 6=i
De manière théorique, on sait que cet algorithme converge si et seulement si le rayon spectral
de la matrice M −1 N est strictement inférieur à 1. En pratique, il converge pour toute valeur
initiale si la matrice A est à diagonale strictement dominante [BCVC07].
L’algorithme de Gauss-Seidel est une variante de la méthode de Jacobi (qui améliore la vi-
tesse et les conditions de convergence). Pour un système Ax = b, on construit une suite de
vecteurs (x k ) telle que :
(D − L)x k+1 = Ux k + b
20 2. Solveur linéaire sur GPU
Ces méthodes présentent néanmoins un défaut : à chaque itération, on effectue une divi-
sion par un terme diagonal (terme en a1i i ). Sur GPU, il y a donc un risque de perte de précision
à cet endroit-là.
Cependant les termes diagonaux de la matrice A n’interviennent que dans le cadre de ces
divisions, et nulle part ailleurs. On peut donc envisager de prétraiter la matrice sur le CPU en
remplaçant tous les termes diagonaux par leurs inverses, de transférer cette matrice modifiée
sur le GPU, et d’effectuer ensuite des produits en lieu et place des divisions : les multiplications
sur GPU étant conformes à la norme IEEE, on élimine ainsi la perte de précision.
Aδx = b − A x̂
Ainsi, avec le même algorithme, on peut calculer le résidu δx et l’utiliser pour améliorer la pré-
cision obtenue sur x̂.
2. Solveur linéaire sur GPU 21
11: p i ← r i −1 + βp i −1
12: p̃ i ← r˜i −1 + βp̃ i −1
13: end if
14: q i ← Ap i
15: q̃ i ← A T p̃ i
ρ −1
16: α ← p̃ ii·q i
17: x i ← x i −1 + αp i
18: r i ← r i −1 − αq i
19: r˜i ← r˜i −1 − αq̃ i
20: détection de convergence
21: end for
Cette méthode simple n’est pas à négliger lors de la finalisation du solveur. La méthode QR
se prête de plus particulièrement bien à ce calcul de résidu : comme A ne change pas, il n’est
pas nécessaire de recalculer Q et R ; le calcul de δx peut alors être très rapide.
On peut aussi utiliser des méthodes relevant plutôt d’astuces de programmation pour amé-
liorer la précision des calculs. Ainsi, lorsque l’on effectue des calculs sur des nombres flottants
sur 32 bits, on sait que l’on risque de perdre les bits de poids faible, et donc de perdre de la
précision. On peut alors envisager d’utiliser un second nombre flottant servant à compenser
cette perte de précision.
Cette approche a été utilisée il y a plusieurs années pour apporter une précision proche de
64 bits aux machines 32 bits uniquement grâce à un module Fortran nommé DSFUN90 [Bai05].
Ce module a récemment été partiellement porté en CUDA par un chercheur de l’université de
Delft (Pays-Bas) qui a publié son code sur le forum de discussion de NVIDIA 6 . Il n’y a cependant
pas d’évaluation rigoureuse et complète de l’efficacité réelle de cette méthode sur les perfor-
mances et la précision du calcul ; elle peut cependant être intéressante à utiliser dans le cas où
les méthodes de calcul « simples » ne permettraient pas d’atteindre une précision suffisante.
Il faut enfin noter qu’il y a un biais naturel entre les calculs en flottants 32 bits sur CPU par
rapport à ceux sur GPU : les floating point units (FPU) intégrés aux CPU modernes travaillent
sur des registres de 80 bits. Ainsi, si l’on applique différentes opérations à un flottant 32 bits,
celui-ci sera traité en interne sur 80 bits, avec donc une très grande précision, avant d’être en-
suite tronqué à 32 bits pour le stockage en mémoire.
Cette précision plus importantes dans les calculs peut faire une différence non négligeable
sur les résultats finaux des différents algorithmes, aussi nous avons décidé de désactiver ces
capacités de calcul sur 80 bits (à l’aide des méthodes de l’en-tête C standard fpu_control.h)
afin de tout ramener à 32 bits.
6. http://forums.nvidia.com/index.php?showtopic=73067&mode=linear
22 2. Solveur linéaire sur GPU
Le format que nous avons retenu est similaire au format DIA tel que décrit dans [BG08].
Considérons par exemple une matrice creuse 10×10 (A = (a i j )), composée de 4 diagonales
non-vides (cf. Figure 2.1). Chaque diagonale est composée d’une famille d’éléments (a i j ) telle
2. Solveur linéaire sur GPU 23
que k = j − i est une constante. Cette constante, l’indice de la diagonale, permet donc de re-
trouver j si l’on connaît i . Par convention, on la donnera toujours modulo n (avec n la taille du
système).
Si l’on étend cette définition à tous les a i j vérifiant j − i = k et k 6= 0, la diagonale s’étend
alors à des valeurs situées au-dessus et en-dessous de la diagonale principale : on identifiera
alors tous les termes de cette « diagonale étendue » grâce à i et k.
01 4 8
On peut alors proposer un mode de stockage très efficace pour cette matrice :
– un tableau n × d pour stocker les d diagonales en colonne ;
– une liste de d entiers indiquant à quelle indice k correspond chaque colonne du tableau.
La matrice A donnée précédemment sera ainsi stockée comme représenté sur la Figure 2.2.
01 48
Nous avons surtout constaté que le format de stockage de données utilisé est très adapté
à ces algorithmes sur GPU : il est possible de faire des accès mémoires séquentiels entre les
différents threads d’un même bloc, ce qui est un facteur extrêmement important pour les per-
formances du calcul sur GPU.
Pour encore améliorer les choses, nous avons modifié quelques détails sur le schéma de
stockage de la matrice :
– la matrice est stockée en mémoire sous forme transposée (une « colonne » en entier, puis
la seconde, etc.) ;
– les dimensions des colonnes sont alignées pour que l’adresse de début de chaque co-
lonne soit un multiple de 16. Ceci est nécessaire pour que les accès mémoire séquentiels
24 2. Solveur linéaire sur GPU
sur GPU n’aboutissent qu’à une seule transaction entre la mémoire et les multiproces-
seurs ; ainsi lire (ou écrire) 16 éléments mémoire séquentiellement est aussi rapide que
d’en lire (ou écrire) un seul.
Les performances sont relativement bonnes par rapport au CPU, mais dans certains cas la
version GPU ne converge pas alors que la version CPU converge.
Sur GPU, nous avons décidé pour simplifier l’écriture du programme d’utiliser la biblio-
thèque CUBLAS pour les opérations vectorielles simples : produit scalaire (avec l’opération
BLAS SDOT) et mise à jour du type y ← y +αx (opération SAXPY). Il reste alors à écrire 3 kernels :
– calcul de b − Ax : utilisé pour calculer r 0 , et pour calculer l’erreur après quelques itéra-
tions. Dans notre code, ce kernel s’appelle delta() ;
– mise à jour de p et p̃ : kernel update_p(), sans difficulté particulière ;
– mise à jour de q et q̃ : ce kernel-ci s’est avéré beaucoup plus délicat à écrire du fait que q̃
nécessite un produit par la transposée de A 7 (cf. ligne 15 de l’Algorithme 1). Le format de
stockage choisi n’étant pas particulièrement adapté, ceci a posé de nombreux problèmes
liés à des accès mémoires inefficaces, voire incorrects (écriture simultanée depuis plu-
sieurs threads différents, donnant des valeurs aléatoires). Ces problèmes ont fini par être
résolus, et le kernel update_q(), bien qu’étant encore le goulot d’étranglement de cet
algorithme, est maintenant stable et efficace.
2.3.5 Performances
Nous avons effectué de nombreux tests de performances pour les différents solveurs im-
plémentés, tant en temps qu’en précision sur la solution trouvée.
Pour produire ces graphes, nous avons fait tourner chaque test 10 fois avec des tailles de
problèmes variables (seulement 2 fois pour les grands problèmes sur CPU pour que ce soit un
peu plus rapide) puis avons calculé la moyenne arithmétique des valeurs obtenues lors de ces
différentes exécutions. Les données mesurées sont les suivantes :
– temps d’exécution total de l’algorithme, y compris les transferts (wall time) ;
– précision, en calculant la norme infinie (maximum en valeur absolue) du vecteur d’er-
reur (kb − Axk∞ ).
Nous avons fait ces tests avec plusieurs jeux de données différentes. Nous présentons ici
deux cas différents : un premier qui est plutôt favorable, et un second qui l’est moins. Dans
tous les cas, il s’agit de problèmes construits algorithmiquement, et donc reproductibles d’une
exécution à l’autre (aucune valeur aléatoire) ; pour tracer les graphes nous avons fait varier la
taille des problèmes entre n = 100 et n = 3 000 000.
Cas favorable
Dans ce premier cas, nous avons obtenu les résultats présentés sur les figures 2.3 et 2.4.
7. En fait, sa matrice adjointe, mais dans le cas d’une matrice réelle c’est effectivement la transposée.
2. Solveur linéaire sur GPU 25
200 000
20 000
Temps de convergence (ms)
2 000
GS CPU
Jacobi GPU
Jacobi+ GPU
200 BiCG CPU
BiCG GPU
20
2
100 1 000 10 000 100 000 1 000 000
Taille du problème
F IGURE 2.3 – Performances en temps des différents solveurs dans le cas favorable
1E+1
1E+0
1E-1
1E-2
GS CPU
Erreur
1E-3
Jacobi+ GPU
Jacobi GPU
BiCG CPU
1E-4 BiCG GPU
1E-5
1E-6
1E-7
100 1 000 10 000 100 000 1 000 000
Taille du problème
F IGURE 2.4 – Performances en précision des différents solveurs dans le cas favorable
26 2. Solveur linéaire sur GPU
– les solveurs sur CPU sont toujours plus lents, mais très précis (erreur de l’ordre de 10−6 ) ;
– les solveurs sur GPU sont plus rapides, mais largement moins précis ;
– BiCG sur GPU devient aussi précis que BiCG sur CPU pour des systèmes de taille supé-
rieure à 10 000 ;
– la précision augmente avec la taille du problème, ce qui est dû à un phénomène de com-
pensation statistique des erreurs d’arrondi ;
– la version de Jacobi utilisant l’émulation de la double précision (méthode désignée par
« Jacobi+ » sur les graphes) est trois à quatre fois plus lente que le Jacobi standard sans
rien apporter en précision ;
– le temps de convergence des solveurs sur GPU est constant pour les systèmes de taille
inférieure à 10 000, puis augmente linéairement ensuite. Ceci est dû à la latence intro-
duite par les accès mémoire : ceux-ci sont beaucoup plus lents que les calculs, et pour
les « petits » systèmes le scheduler intégré à CUDA masque cette latence par d’autres cal-
culs. Ainsi la vitesse réellement observée pour ces premières valeurs est celle des accès
mémoire. Pour des systèmes plus grands, il y a « trop » de threads : il n’y a plus aucun ins-
tant où les multiprocesseurs sont inactifs, donc augmenter la taille du système augmente
d’autant la durée d’exécution de l’algorithme ;
– pour les problèmes de très grande taille, la RAM disponible sur le GPU est un facteur
limitant, et on ne peut pas traiter d’aussi grands problèmes sur GPU que sur CPU.
Cas défavorable
Les résultats de ce second cas sont présentés sur les figures 2.5 et 2.6.
200 000
20 000
Temps de convergence (ms)
2 000
GS CPU
Jacobi GPU
Jacobi+ GPU
200 BiCG CPU
BiCG GPU
20
2
100 1 000 10 000 100 000 1 000 000
Taille du problème
F IGURE 2.5 – Performances en temps des différents solveurs dans le cas défavorable
1E+0
1E-1
1E-2
1E-3
GS CPU
Erreur
Jacobi+ GPU
1E-4 Jacobi GPU
BiCG CPU
BiCG GPU
1E-5
1E-6
1E-7
100 1 000 10 000 100 000 1 000 000
Taille du problème
F IGURE 2.6 – Performances en précision des différents solveurs dans le cas défavorable
Les solveurs GPU que nous avons écrits sont donc assez intéressants. En particulier, le
solveur utilisant la méthode du gradient biconjugué permet d’avoir une précision meilleure
qu’avec la méthode de Jacobi (ou au pire aussi bonne) tout en étant plus rapide dans les petits
problèmes et à peu près aussi rapide pour les systèmes plus grands. Dans certains cas, cette
méthode devient même aussi précise que son équivalent sur CPU.
Il conviendra néanmoins d’être très attentif, lors du traitement du problème réel, aux pro-
blèmes de précision induits par le GPU : dans certains cas (par exemple si le solveur sur GPU
ne converge pas), se rabattre sur un solveur CPU peut être une solution de secours acceptable.
Si l’on voulait encore améliorer les performances de ces solveurs, il faudrait probablement
utiliser des cartes plus puissantes. En particulier, il faudrait des bus mémoires plus rapides : ici,
le facteur limitant est clairement la bande passante mémoire.
Enfin, pour améliorer les performances en précision, il sera probablement nécessaire de
passer à une carte GPU plus récente, qui supporte le calcul sur des flottants en double préci-
sion ; mais ceci risque de se faire au détriment des performances brutes de calcul.
28 2. Solveur linéaire sur GPU
3. Pistes pour la suite du stage 29
Chapitre 3
Ce stage est encore loin d’être fini. Ayant cette année suivi le Master de Recherche en In-
formatique en parallèle de ma troisième année d’École d’Ingénieurs à Supélec, ce stage doit en
effet correspondre aux critères du Master et à ceux de Supélec. À ce titre, le stage a commencé
mi-mars (à la fin des cours de Supélec), et se prolonge jusqu’à fin août 2009, soit deux mois
après la soutenance de ce stage – et presque 3 mois après la remise de ce rapport.
Dans cette partie, nous présentons ainsi succintement le travail qui reste à faire sur ce projet
au cours des prochains mois, tant au niveau du solveur linéaire sur GPU que du projet global,
à savoir la simulation du transport d’espèces chimiques.
Pour tenter d’améliorer la précision atteinte, plusieurs pistes sont envisageables. La plus
simple est probablement d’utiliser un préconditionneur : ceci est censé permettre de « sta-
biliser » le problème, et ainsi d’obtenir à la fois une meilleure convergence et une meilleure
précision.
Dans les prochaines semaines, nous allons donc faire un état de l’art des techniques de
préconditionnement existantes, en insistant en particulier sur ce qui s’est déjà fait sur GPU.
Dans un second temps, nous implémenterons la ou les techniques les plus intéressantes et
évaluerons son impact à la fois sur la précision, la vitesse, et la qualité de la convergence.
Au cours des prochaines semaines, nous allons donc concevoir de manière théorique un
système « idéal », où l’on déporte le plus possible de calculs sur le GPU, et où le CPU ne sert
qu’à commander ces calculs et à effectuer les transferts réseaux.
A priori, il est certain qu’il faudra par exemple gérer le GPU depuis un seul et même thread
CPU, et les communications asynchrones (entrantes et sortantes) depuis un ou plusieurs autres
threads CPU. Pour gérer la synchronisation des données entre le CPU et le GPU, il faudra né-
cessairement utiliser des mécanismes de synchronisation classiques (exclusion mutuelle ou
sémaphores). L’architecture précise de ce système reste cependant à élaborer.
Il est particulièrement intéressant de noter que ce système comptera deux niveaux d’asyn-
chronismes différents, à deux échelles de temps totalement différentes :
– les communications entre CPU et GPU : elles seront asynchrones pour recouvrir le calcul
par des transferts, et aux débits autorisés par le bus PCI-Express reliant la carte graphique
à la carte mère, soit plusieurs gigaoctets par seconde ;
– les communications réseaux : elles seront asynchrones également, mais à des débits bien
plus faibles, le débit maximum théorique du cluster de GPU de Supélec étant d’un giga-
bits par seconde (Ethernet 1 Gbps).
La conception (et encore plus l’implémentation) d’un tel système devra donc respecter de
nombreuses contraintes temporelles pour assurer un fonctionnement correct.
Comme nous l’avons fait pour le solveur GPU, l’implémentation de ce simulateur se fera
par étapes, avec à chaque fois une validation sur CPU :
3. Pistes pour la suite du stage 31
Conclusion
Une partie importante du travail prévu a déjà été accomplie. Entre la mi-mars et début juin,
nous nous sommes appliqués à concevoir et implémenter un solveur linéaire sur GPU, ce qui
n’est pas une tâche simple.
En premier lieu, il a été nécessaire de choisir un format de stockage des matrices creuses.
Il fallait que celui-ci soit à la fois efficace et adapté à la programmation sur GPU. Le format de
stockage par diagonales proposé ici répond à ces contraintes, et possède également d’autres
vertus qui n’ont été découvertes que plus tard, comme la simplicité d’accès à la matrice trans-
posée par quelques manipulations d’indices simples.
Ensuite, il existe de nombreuses méthodes qui permettent de résoudre des systèmes li-
néaires. Mais relativement peu d’entre elles sont adaptées à un calcul massivement parallèle
comme celui offert par les GPU actuels. Parmi toutes les méthodes disponibles, nous avons re-
tenu deux méthodes itératives : la méthode de Jacobi (et Gauss-Seidel sur CPU), et la méthode
du gradient biconjugué.
Une fois ces méthodes choisies, il a fallu les implémenter sur GPU. Ceci a été fait, avec
des résultats très encourageant au niveau des performances. Les mêmes algorithmes ont été
implémentés sur CPU, afin de valider les résultats obtenus ; ceci a permis de confirmer que les
CPU sont largement plus lents que les GPU, mais aussi beaucoup plus précis.
En effet, les GPU, plate-formes destinées en priorité au rendu graphique pour les jeux vi-
déos, ne sont pas parfaitement adaptés au calcul intensif, en particulier au niveau de la préci-
sion des opérations en virgule flottante. Non seulement les types de données disponibles sont
d’une précision plus faibles que ceux disponibles sur CPU, mais en plus certaines opérations
essentielles, comme la division, sont implémentées matériellement d’une manière non stan-
dard qui risque de causer des erreurs d’arrondis importantes. Il convient donc de choisir des
méthodes résistantes faces à ces imprécisions, ou d’ajouter des contre-mesures visant à ré-
duire au maximum leur influence sur le résultat final. Ceci n’a été que partiellement fait, mais
d’autres améliorations sont prévues.
Le solveur linéaire résultant de ces travaux, bien qu’encore incomplet, a d’ores et déjà été
présenté à la communauté scientifique lors de la Journée jeunes chercheurs sur les Multipro-
cesseurs et Multicœurs (le 4 juin 2009) ; les réactions ont été plutôt positives, et plusieurs per-
sonnes se sont montrées très intéressées.
Bibliographie
[Bai05] D. Bailey. DSFUN90 : Fortran-90 double-single package. World-Wide Web site with
software archives., March 2005.
[BBC+ 94] R. Barrett, M. Berry, T. F. Chan, J. Demmel, J. Donato, J. Dongarra, V. Eijkhout,
R. Pozo, C. Romine, and H. Van der Vorst. Templates for the Solution of Linear Sys-
tems : Building Blocks for Iterative Methods, 2nd Edition. SIAM, Philadelphia, PA,
1994.
[BCLar] L. Buatois, G. Caumon, and B. Lévy. Concurrent number cruncher - a GPU im-
plementation of a general sparse linear solver. International Journal of Parallel,
Emergent and Distributed Systems, to appear.
[BCMS06] J. Bahi, R. Couturier, K. Mazouzi, and M. Salomon. Synchronous and asynchronous
solution of a 3D transport model in a grid computing environment. Applied Mathe-
matical Modelling, 30(7) :616–628, 2006.
[BCVC07] J. Bahi, S. Contassot-Vivier, and R. Couturier. Parallel Iterative Algorithms : from
sequential to grid computing, volume 1 of Numerical Analysis & Scientific Compu-
tating. Chapman & Hall/CRC, 2007.
[BG08] Nathan Bell and Michael Garland. Efficient sparse matrix-vector multiplication on
CUDA. NVIDIA Technical Report NVR-2008-004, NVIDIA Corporation, December
2008.
[Des06] P. Dessante. Analyse numérique et optimisation. Supélec, 2006.
[DMP+ 08] J. Dongarra, S. Moore, G. Peterson, S. Tomov, J. Allred, V. Natoli, and D. Richie. Explo-
ring new architectures in accelerating CFD for air force applications. In Proceedings
of the DoD HPCMP User Group Conference, July 2008.
[NVI08] NVIDIA. NVIDIA CUDA 2.1 Programming Guide, December 2008.
[QO08] E. Quintana-Ortí. Use of GPUs in dense linear algebra. Seminar at National Instru-
ments, Austin, April 2008.
[TDB08] Stanimire Tomov, Jack Dongarra, , and Marc Baboulin. Towards dense linear alge-
bra for hybrid gpu accelerated manycore systems. Technical Report 210, LAPACK
Working Note, October 2008.
[VD08] V. Volkov and J. Demmel. Benchmarking GPUs to tune dense linear algebra. In SC,
page 31, 2008.
[Wik09] Wikipedia. QR decomposition, 2009. [Online ; accessed 23-March-2009].