Présentation d’un GPU

1.1.3        Prise en compte des CPU multi-coeurs

En 1947, Von Neumann décrit une architecture de programmes linéaires, appelé aujourd’hui architecture de programmes « séquentiels ». Dans ce modèle, le programme est une séquence d’instructions exécutées les unes après les autres. Ce type de programmation représente la majorité des applications.

Cette architecture logicielle est malheureusement inadaptée aux processeurs multi-cœurs, le programme séquentiel ne pouvant tirer parti que d’un seul cœur. Ainsi, dans un environnement multi-cœurs, le programme ne gagnera pas en performance. Et sans amélioration de performances, il devient difficile d’ajouter de nouvelles fonctionnalités aux applications, ce qui pourrait conduire à réduction la croissance de l’industrie informatique.

Architecture d’un programme séquentiel
Architecture d’un programme séquentiel

Pour pouvoir tirer profit de l’augmentation de la puissance de calcul apportée par les nouvelles générations de processeurs multi-coeurs, les programmes doivent être écrits suivant une architecture de « programmation parallèle ». Dans ce type d’architecture, un programme est découpé en plusieurs flux d’actions séquentiels qui s’exécutent simultanément et coopèrent pour réaliser une tâche le plus rapidement possible. Cette nouvelle façon de programmer permet de réduire radicalement les temps d’exécution en distribuant la charge sur les différents coeurs. Elle induit néanmoins une complexité accrue dans les logiciels car la synchronisation entre les flux d’actions doit être gérée explicitement par le programmeur.

La programmation parallèle n’est pas une nouveauté : celle-ci est utilisée depuis des décennies par des communautés faisant appel à l’usage de calculs intensifs, notamment la communauté scientifique. Ces programmes spécifiques sont écrits pour tirer parti de très puissants supercalculateurs sur lesquels ils s’exécutent. Ces supercalculateurs regroupent, dans une même machine, de nombreuses unités de calcul, et montre donc des ressemblances avec les processeurs multi-coeurs. Toutefois, seule une minorité d’application peut justifier le coût d’utilisation de supercalculateurs, ce qui a limité la pratique de la programmation parallèle à un petit groupe de développeurs.

Maintenant que les nouvelles générations de processeurs sont conçues sur une architecture parallèle, le nombre de programmes développés pour tirer partie de ce modèle est en augmentation croissante. Il y a actuellement un grand besoin sur le marché et les développeurs doivent se former à ce type de programmation.

1.1.4        Naissance des GPU

L’explosion des applications multimédia grand public, et notamment des jeux vidéo, a montré les limites des CPU classiques. Les attentes en terme de réalisme graphique et les spécificités algorithmiques de ces applications ont entrainé l’apparition de microprocesseurs spécialisés dans le traitement de l’image, les GPU (Graphic Process Unit).

Les GPU (Graphic Process Unit) sont des processeurs situés sur une carte annexe dédiés au traitement des images, des données 3D et de l’affichage. Ceux-ci, spécialisés dans les calculs à virgule flottante, ont permis d’ouvrir une nouvelle voie pour l’amélioration des performances des applications multimédia.

Le graphique ci-dessous- montre très nettement la stagnation de l’évolution des CPU en terme de performances en calcul à virgule flottante. Celle-ci a commencé à progresser de nouveau depuis l’apparition de l’architecture multi-cœurs.
Au contraire, l’évolution des GPU, utilisant massivement l’architecture multi-cœur, n’a cessé de progresser. En termes de calculs purs, sur des opérations à virgules flottantes, un GPU est actuellement 100 fois plus rapide qu’un CPU.
En juin 2008, NVIDIA a présenté la puce GT200, qui livre presque 1 TFLOP (1,000 GFLOPS) calcul en simple précision et presque 100 GFLOPS en double précision.

Figure 2 - Ecarts de performance entre CPU et GPU
Ecarts de performance entre CPU et GPU

1.2         GPU et Rendu 3D temps-réel

1.2.1        Introduction

A l’origine des GPU est le besoin d’afficher en temps-réel une projection en deux dimensions (sur l’écran) d’un modèle en trois dimensions. Ce besoin apparaît dans les applications de conception assistée par ordinateur (CAO) pour lesquelles on souhaite pouvoir visualiser en 3D les objets conçus, et dans les jeux vidéo pour la représentation d’un monde virtuel.

Il existe plusieurs techniques pour calculer la projection d’un modèle 3D sur une surface 2D. Certaines, qui ne seront pas détaillées en détails ici, nécessitent une algorithmique complexe : par exemple, le lancé de rayons (Raytracing), consistant à déterminer le mouvement de la lumière sur une scène en 3D.

Le Raytracing consiste à réaliser le parcours inverse de la lumière (pour des raisons de performances) et donc à « lancer » des rayons depuis la caméra (l’œil de l’utilisateur) vers chaque point de la scène afin de déterminer les points d’impacts avec les objets. L’intensité lumineuse de ce pixel est alors calculée en lançant de nouveaux rayons depuis cet impact, en direction de toutes les sources lumineuses de la scène.

D’autres techniques sont beaucoup plus linéaires et vectorielles par nature, comme la rastérisation.
Cette dernière consiste à convertir une image vectorielle en une image matricielle destinée à être affichée sur un écran ou imprimée par un matériel d’impression.
C’est cette technique qui permettra la création de processeurs spécialisés.

1.2.1        Composants matériels

La carte graphique est un des organes de l’ordinateur, chargé du traitement graphique et de l’affichage. Elle se compose d’une zone mémoire et de processeurs, auxquels viennent s’ajouter divers registres et chipsets de communication.

Ce qu’on appelle GPU, au sens strict, est l’ensemble des processeurs graphiques contenus sur cette carte, c’est-à-dire les unités opérant les calculs. Par abus de langage, on nommera également GPU la carte entière.

La mémoire disponible varie aujourd’hui jusqu’à 768Mo, cadencée à 1080MHz, accessible en lecture et en écriture par les processeurs de la carte mais aussi par le CPU.

Les GPU embarquent jusqu’à 128 (G80) ou 240 processeurs de flux chacun (G200), cadencés jusqu’à 1500MHz, et répartis en différentes catégories. Ces processeurs sont nommés processeurs de flux en raison de leurs natures : ils sont en effet capables de traiter, de façon parallèle, un ensemble de données élémentaires présenté sous la forme d’un flux.

Chacun de ces processeurs fonctionne en mode SIMD parallèle / CREW, il reçoit donc un ensemble d’instructions qui sera exécuté sur la totalité des données du flux. Ils sont de plus tous capables de gather, mais pas de scatter : ils peuvent accéder à n’importe quelle adresse mémoire en lecture, mais pas en écriture (voir sections 1.3.2 et 1.3.4).

Les processeurs d’un GPU sont organisés en pipeline que nous nous proposons de décrire.

1.2.1        Vocabulaire de base

Avant d’aller plus loin, nous devons connaitre le vocabulaire suivant :

Vertex
Tous les objets d’une image 3D sont constitués de vertex. Un vertex est un point dans un espace à trois dimensions avec des coordonnées X, Y, Z, sommet d’un ou plusieurs polygones.
Plusieurs vertex réunions (au moins trois) forment un polygone qui peut n’être qu’un simple triangle, un cube ou bien une forme plus complexe.
Le cube ci-contre est composé de huit vertex.

Vertex
Vertex

Texture
Une texture est une image 2D, dont la taille varie et que l’on applique à un objet 3D pour simuler sa surface.
Notre cube ci-contre est constitué de huit vertex. Il ressemble à une simple boîte avant qu’on ne lui applique une texture. Une fois la texture appliquée à l’objet 3D, on a l’impression que la texture a été peinte dessus.

Texture
Texture

Fragment
Un fragment est une partie élémentaire d’une scène en cours de rendu qui pourrait occuper l’espace d’un pixel dans l’image finale. La différence entre pixel et fragment est une vue d’esprit : alors que le pixel est un « point » de couleur visible par l’utilisateur, le fragment est le « point » de couleur que le programme manipule. Le fragment est donc constituée d’une position (selon des coordonnées X, Y, Z), d’une couleur, de coordonnées au sein d’une textures, d’une profondeur de visibilité. Une fois tous ces traitements appliqués, nous obtiendrons un pixel visible à l’écran.

Shader
Un shader est un programme à vocation graphique, généralement court, que le développeur peut écrire en différents langages, maniant des vertex ou des fragments et permettant de contrôler un sous-ensemble des processeurs d’un GPU.

Il existe deux sortes de shaders : les vertex shaders et les pixels shaders.
Les vertex shaders déforment ou transforment des éléments 3D tandis que les pixel shaders peuvent changer la couleur des pixels en fonction de données complexes (source de lumière, effets d’ombre, etc.).
Les programmes de pixel shaders permettent des effets spectaculaires tels que cette eau miroitante.

Shader
Shader

Pipeline
Un pipeline est une séquence ordonnée de différents étages. Chaque étage récupère ses données de l’étage précédent, effectue une opération propre et renvoie ses résultats à l’étage suivant.

Un pipeline est dit rempli lorsque tous ses étages sont mis à contribution simultanément, c’est son utilisation optimale, diminuant le nombre d’étapes globales d’exécution d’un algorithme par rapport à une version séquentielle classique.

1.2.4        Pipeline graphique actuel

Le pipeline des GPU actuels est représenté ci-dessous. Il est composé de trois étages programmables, pilotés par les shaders, et d’un étage non programmable, le rasterizer.

Pipeline GPU
Pipeline GPU

Pour traiter la géométrie d’une image, les données sont premièrement envoyées au GPU. Ces données sont les primitives géométriques de la scène à rendre, décrites via les commandes d’une API graphique, et représentés sous la forme d’un flux de vertex.

Ces vertex sont les données d’entrée du premier étage du pipeline, le Vertex Shader. Celui-ci a pour vocation de modifier les différents attributs des vertex : il peut les changer et leur en donner de nouveaux, comme couleur et normale. Le flux sortant est de même taille et est composé des mêmes vertex modifiés. Il a accès à la mémoire de la carte en lecture et peut y écrire ses résultats. La principale utilité du Vertex Shader est de déplacer les objets pour donner l’impression de mouvement d’une frame à une autre.

Le flux sortant du précédent shader est le flux d’entrée pour l’étage suivant du pipeline, le Geometry Shader. Ce shader permet de modifier la géométrie de la scène, ici le maillage géométrique passé à la carte. Il peut en effet ajouter ou supprimer des vertex et en modifier les attributs. Le flux de sortie contient donc un ensemble de vertex de taille potentiellement différente à celle du flux d’entrée. Il a également accès à la mémoire en lecture pour pouvoir y écrire pour ses résultats. Ce shader est en particulier utilisé pour les méthodes de raffinement de maillage ou d’augmentation de détails sur les modèles géométriques. Par exemple, une sphère composée de 20 triangles sera re-décomposée en 100 triangles plus petits. Il permet également la mise en place d’autre technique comme le Displacement Mapping permettant de créer le relief en surface d’un objet en déplaçant certains points de cette surface en fonction de valeurs contenues dans une texture de hauteur (displacement map).

L’étage suivant dans le pipeline est le Rasterizer. A partir du maillage composé des vertex sortant du shader précédent (modèle 3D), la géométrie finale est projetée sur une grille de la taille de l’image de sortie (image 2D), et les primitives géométriques sont divisées en fragments. C’est à cet étage du pipeline qu’on réalise également quelques opérations supplémentaires améliorant la vitesse d’exécution des calculs suivants, telles que clipping (suppression des objets extérieurs au cône de vision donc non visibles) ou back-face culling (suppression de polygones suivant leur orientation par rapport à la caméra : un polygone présentant son « dos » à la caméra n’est pas censé être visible). Les fragments résultants possèdent des coordonnées, correspondant à la position finale dans l’image. Le flux de fragment sortant est ensuite dirigé vers le dernier étage du pipeline.

Le Fragment Shader est le troisième et dernier étage programmable du pipeline. C’est le shader le plus important car il attribue sa couleur finale à chaque fragment de son flux d’entrée,, en fonction de l’éclairage, des réflexions et réfractions lumineuses et de diverses techniques de rendu pouvant interroger des textures tierces. Comme les shaders précédents, il peut consulter la mémoire de la carte mais ne peut y écrire n’importe où. Il a seulement l’autorisation d’écrire aux coordonnées du fragment qu’il est en train de traiter, coordonnées qu’il ne peut donc modifier.

Le flux de fragments calculés, ou shadés, est écrit en mémoire. Classiquement, la structure accueillant ces données est le framebuffer, directement affiché à l’écran, image ne nécessitant pas un transit supplémentaire par le CPU. Néanmoins, il est possible d’écrire cette image dans une texture, pouvant être utilisée par un autre shader ou récupérée sur CPU.

Jusqu’à la cinquième génération de cartes graphiques, les processeurs embarqués étaient catégorisés, certains servant au traitement de vertex, les Vertex Units, d’autres à celui des fragments, les Fragment Units. Cela a pour inconvénient de pouvoir créer un goulot d’étranglement : lorsque les Vertex Units sont surchargés, l’utilisation des Fragment Unit n’est pas optimale et peut même être extrêmement diminuée, et réciproquement. Avec la mise à disposition de la sixième génération de cartes graphiques (GeForce 8), les processeurs ne sont plus spécifiques, ils peuvent maintenant servir aussi bien à la gestion des vertex qu’à celle des fragments. Cela permet de remplir au mieux le pipeline et d’éviter le goulot d’étranglement précédent. L’architecture de ces cartes est dite unifiée.

1.2.5        Evolution des GPU au cours des differentes generations

Depuis l’apparition des accélérateurs graphiques matériels, dans les années 80, leur évolution n’a cessé de s’accélérer. Les premières cartes graphiques accessibles au grand publiques sont apparues dans le milieu des années 90, avec en particulier les Voodoo Graphics.

Le terme de GPU a été introduit par NVidia, pour remplacer le terme VGA Controller, trop réducteur pour désigner l’ensemble des possibilités offertes par ces cartes.

L’industrie du jeu vidéo, en particulier, n’a depuis cessé de motiver le développement de nouveaux processeurs dédiés au traitement graphiques. Cette évolution peut être catégorisée en différentes générations, selon leurs capacités, sur lesquelles nous nous proposons de dévoiler quelques aspects. Une description plus détaillée de l’architecture des dernières générations, permettant de comprendre leur utilisation dans nos applications, est ensuite proposée.

Les forces ayant poussé les industriels à améliorer sans cesse ces cartes graphiques sont dues principalement à un aspect économique de compétitivité, pour faire face à un appétit de plus en plus grand de complexité visuelle ou de réalisme dans les représentations cinématographiques ou les simulations vidéo-ludiques, poussé par une volonté assurément humaine de divertissement.

Nous nous proposons ici de classer ces architectures en différentes générations.

Avant les GPU
Dans les années 80, des sociétés comme Silicon Graphics proposèrent des solutions matérielles extrêmement couteuses, réservé à quelques professionnels spécialisés, et ne pouvant effectuer que quelques opérations graphiques très simples comme des transformations de vertex ou des applications de textures. De façon générale, le reste des opérations graphiques étaient accomplies par le CPU.

Première génération
La première génération de GPU à proprement parler a débuté avec l’arrivée des Voodoo Graphics de 3dfx Interactive en 1996, et dura jusqu’en 1999. Les principales cartes de cette génération sont les TNT2 de NVidia (architecture NV5), les Rage d’ATI et les Voodoo3 de 3dfx. Les premières opérations graphiques disponibles sont la rasterization de triangles et l’application de texture. Ces cartes implémentent également le jeu d’instruction de DirectX 6, API (Application Programming Interface) alors standard. Cependant, elles souffrent de certaines limitations, principalement la grande faiblesse du jeu d’instructions mathématiques et l’impossibilité de transformer matériellement des vertex, opération lourde encore à la charge du CPU.

Deuxième génération
Les premières GeForce 256 de NVidia (NV10) font leur apparition en 1999, à peu près en même temps que les Radeon 7500 d’ATI (architecture RV200) et Savage 3D de S3. Ces cartes permettent maintenant une prise en charge complète de la transformation des vertex et du calcul des pixels (Transform and Lightning, T&L). Les deux API principales, DirectX 7  et OpenGL, sont maintenant supportées par ces cartes. Les améliorations apportées au jeu d’instructions rendent ces cartes plus facilement configurables, mais pas encore programmable : les opérations sur les vertex et les pixels ne peuvent être modifiés par le développeur.

Troisième génération
Dès cette génération, les constructeurs NVidia et ATI se partagent la quasi-totalité du marché, NVidia ayant fait l’acquisition de 3dfx. Les Geforce 3 (NV20) en 2001 et GeForce 4 Ti (NV25) en 2002 de NVidia, la Radeon 8500 d’ATI (R200) en 2001 forment cette génération de GPU, permettant enfin au développeur de diriger la transformation des vertex par une suite d’instructions qu’il spécifie. Néanmoins, la programmabilité des opérations sur les pixels n’est toujours pas possible. DirectX 8 et quelques extensions à OpenGL permettent tout de même une plus grande souplesse de configuration dans le traitement de ces pixels.

Quatrième génération
Courant 2002, ATI propose sa Radeon 9700 (R300), et NVidia sa GeForce FX (NV30) à la fin de la même année. Ces deux cartes, en plus de supporter le jeu d’instructions DirectX 9 et de nouvelles extensions OpenGL, apportent de plus la possibilité de programmer le traitement des pixels. C’est à partir de cette quatrième génération de cartes que les premières opérations GPGPU ont pu se faire.

Cinquième génération
Les GeForce 6 (NV40, avril 2004) et GeForce 7 (G70, juin 2005) de NVidia, les Radeon X800 (R420, juin 2004) et X1800 (R520, octobre 205) et dérivées composent cette génération. Ces cartes permettent quelques fonctions intéressantes, en particulier en calcul GPGPU : l’accès aux textures lors de la transformation des vertex, le rendu dans différentes textures (Multiple Render Target, MRT) et le branchement dynamique, uniquement pour la transformation des sommets, améliorant grandement la vitesse d’exécution. Le traitement des pixels ne supporte pas ce branchement dynamique.

Sixième génération
C’est la génération en plein essor actuellement, équipant la plupart des ordinateurs sur le marché. Ses limites sont plus floues, chaque constructeur apportant ses innovations propres. Elle est composée des GeForce 8 (G80) et GeForce 9 (G92), lancées respectivement en 2006 et 2008, apportant des modifications dans la façon de concevoir le pipeline graphique. Entre la transformation des vertex et la rasterization des primitives (leur transformation en fragments, oupixels), une étape supplémentaire est insérée, permettant de modifier la géométrie du maillage des primitives : insertions et suppression de vertex sont possibles, ce qui ouvre de nouvelles voies au calcul GPGPU. L’ajout du type int utilisable en interne (registres) et surtout en externe (textures) a également contribué à élargir le nombre d’applications possibles. De plus, sur les cartes GeForce 8, il n’existe plus de différence physique entre les différents processeurs. L’architecture matérielle modifiée est dite unifiée, cette caractéristique étant exploitée par le langage de programmation CUDA, de NVidia, utilisable avec des cartes à architectures G80 ou supérieures. La série GeForce 9 ne connait pas un succès véritable, ayant moins de mémoire et ses performances étant égales voire inférieures aux cartes équivalentes de la série GeForce 8, pour un prix semblable. Les séries de cartes d’ATI de cette génération sont les Radeon HD2000 et Radeon HD3000 (R600), lancées en 2007, pour contrer la série GeForce 8. Malgré quelques améliorations intéressantes proposées par ce constructeur (support de DirectX 10.1, de la norme Shader 4.1 par exemple), ces cartes peinent à s’imposer à cause d’un prix trop élevé, de pièces bruyantes et de puces chauffant beaucoup.

Septième génération
La dernière génération en date n’est pas encore répandue à l’heure actuelle. Les GeForce 200 (G200), lancées en juin 2008, apportent des améliorations principalement techniques : augmentation de la mémoire disponible, du nombre de processeurs, des fréquences mémoire et GPU, de la bande passante par élargissement du bus de données, pour des performances annoncées jusqu’à deux fois supérieures aux séries précédentes. Pour ATI, les Radeon HD4000, en juin 2008 également, sont censées rivaliser avec la série GeForce 9, mais les performances des modèles haut de gamme les mettent au même niveau que les GeForce 200, proposant les mêmes types d’améliorations techniques, mais affichant des tarifs plus attractifs et une consommation électrique revue à la baisse.

Rétrocompatibilité
Il est important de noter que les programmes écrits pour un type ou une génération de cartes restent utilisable avec les générations futures de cartes, même si toutes les capacités ne sont plus nécessairement exploitées. C’est, par ailleurs, un problème important de développement que de devoir faire avec les différentes générations de cartes présentes sur le marché.