Les microprocesseurs superscalaires se sont progressivements imposés depuis les années 1990 dans la quasi-totalité des segments de marché liés au calcul : ordinateurs personnels, serveurs, supercalculateurs, voire informatique embarquée [HPAD07]. Ils ont été suivis par les processeurs multi-cœurs au cours des années 2000. Leur production de masse permet des économies d’échelle qui rendent leur emploi plus économique que celui d’alternatives spécialisées.
L’évolution des performances de ces architectures se heurte cependant à plusieurs limites. D’une part, les gains permis par le parallélisme d’instructions s’amenuisent à mesure de son exploitation. Le parallélisme de tâches que les multi-cœurs exploitent en complément présente quant-à-lui des difficultés de programmation. D’autre part, le fossé de performance entre les unités de calcul et la mémoire s’accroît de manière continuelle, et devient de plus en plus difficile à combler tout en présentant au logiciel l’illusion d’une unique mémoire cohérente. Enfin, l’énergie consommée et dissipée par les processeurs représente le nouveau facteur limitant la performance, devant la surface de silicium et la vitesse de commutation des transistors [ABC+06].
En parallèle au développement des multi-cœurs, on constate l’émergence des processeurs graphiques (GPU) dédiés au rendu d’images de synthèse. Conçus à l’origine comme des accélérateurs spécialisés, ils sont devenus des architectures parallèles à grain fin entièrement programmables. Leur rôle est complémentaire à celui des processeurs généralistes. Alors que les superscalaires multi-cœur sont optimisés pour minimiser la latence de traitement d’un faible nombre de tâches séquentielles, les GPU sont conçus pour maximiser le débit d’exécution d’applications présentant une grande quantité de parallélisme.
Le marché de masse que constitue le jeu vidéo a permis de concevoir et de produire en volume des GPU dont la puissance de calcul est nettement supérieure à celle des processeurs multi-cœurs [MM05].
Cette puissance disponible à faible coût a suscité l’intérêt de la communauté scientifique qui y a vu l’occasion d’exploiter le potentiel des GPU pour d’autres tâches que le rendu graphique. Ainsi, les GPU ont été proposés pour accélérer des applications de calcul scientifique haute performance, telles que des simulations physiques, ou des applications multimédia, telles que du traitement d’image et de vidéo [OLG+07, GLGN+08].
Les constructeurs de GPU ont vu dans ces travaux académiques une opportunité de s’ouvrir au marché du calcul scientifique, et ont commencé à intégrer des fonctionnalités matérielles non liées au rendu graphique dans leurs processeurs respectifs [LNOM08, NVI09b]. Ce double engouement pour le calcul généraliste sur processeur graphique (GPGPU) soulève plusieurs questions
Les GPU sont décrits dans la littérature tantôt comme des architectures totalement nouvelles [LNOM08], tantôt comme des architectures parallèles classiques qui n’auraient de nouveau que le vocabulaire qui leur est associé [VD08, Kan08]. Ces deux visions extrêmes ne représentent chacune qu’une part de la réalité. En effet, de nombreux aspects des GPU actuels sont hérités des architectures SIMD [HPAD07]. On peut notamment retrouver des similarités dans la façon dont est conçue l’architecture, dans les langages et environnements de programmation utilisés, ainsi que dans les algorithmes parallèles suivis.
Le GPU est conçu à l’origine comme un coprocesseur spécialisé dédié à accélérer les calculs intervenant dans le rendu interactif d’images de synthèse. Il prend place au sein d’un système hôte, constitué d’un ou plusieurs CPU, d’un espace mémoire partagé et d’autres périphériques. En tant que périphérique de calcul, le GPU utilise un environnement d’exécution et un pilote de périphérique. Un programme utilisant le GPU sera divisé en deux parties : un programme principal exécuté par le processeur hôte, dont une des tâches consistera à configurer le GPU, et éventuellement un ou plusieurs noyaux de calcul exécutés par le GPU. Les modèles de programmation associés aux noyaux de calcul GPU sont tous dérivés du modèle SPMD (Single Program, Multiple Data). Le code du noyau est exécuté de manière concurrente par un grand nombre de threads, de l’ordre de la dizaine de milliers. Tous les threads exécutent le code du même noyau. Afin de distinguer les threads entre eux, chacun dispose d’un identifiant unique. Ainsi, le noyau correspond au corps d’une boucle parallèle, tandis que l’identifiant du thread correspond au compteur de la boucle parallèle.
La manière traditionnelle de configurer et programmer un GPU consiste passer par l’intermédiaire d’environnements de programmation dédiés au rendu graphique. Les environnements de programmation graphique actuellement utilisés sont Direct3D de Microsoft [Mica] et les implémentations de la norme OpenGL du consortium Khronos [OGL].
Les données en entrée provenant du CPU sont des listes de sommets composant des primitives géométriques, telles que des triangles ou des lignes. Des attributs sont associés à chaque sommet pour représenter ses coordonnées dans l’espace, sa couleur, ses coordonnées de texture ou son vecteur normal.
Un premier programme nommé vertex shader est appliqué de manière indépendante à chaque sommet. Il se charge de modifier les attributs, typiquement en appliquant des transformations géométriques, afin de calculer les coordonnées du sommet dans l’espace de l’écran. Les primitives sont ensuite assemblées à partir des sommets les composant. L’unité de rastérisation détermine les pixels (ou fragments) couverts par chaque primitive. Des attributs de fragments correspondant aux attributs de sortie du vertex shader sont calculés par interpolation entre les valeurs des attributs des sommets de la primitive. Les coordonnées de chaque fragment sont fixées définitivement à cette étape. La seconde étape programmable, le fragment shader ou pixel shader consiste à calculer la couleur résultante de chaque fragment. Ces deux types de shader peuvent lire depuis des images rangées en mémoire, nommées textures. Le processus d’accès à la texture est dit échantillonnage. Enfin, le fragment doit rejoindre l’image de destination. Une étape d’arbitrage et de fusion est réalisée par les unités ROP (Raster Operations). Il permet de gérer les situations d’occlusion et de transparence qui se présentent lorsque plusieurs fragments se trouvent aux mêmes coordonnées.
1 État de l’art du calcul sur GPU |