Programmation GPGPU

Introduction

RETOUR

Introduction

GPGPU

General Purpose Graphic Processor Unit

Programmation GPGPU

Technique de programmation visant à l'implantation des applications en exécution sur les cartes graphiques (GPU) au lieu des processeurs centraux (CPU).

Technique développée depuis le début des années 2000, moment où les fabriquants de cartes graphiques (NVidia, ATI, 3D Labs, ...) ont rendu possible l'implantation de programmes tiers sur leurs adaptateurs graphiques (fonctionnalité obligatoire pour l'implantation de Microsoft DirectX 9).

Historiquement il n'existait pas d'API spécifique.
-> Les développements se faisaient par détournement des langages de programmation de shaders graphiques à des fins autres que la programmation d'algorithmes de l'infographie:
- GLSL pour OpenGL,
- HLSL pour DirectX,
- CG pour les cartes graphiques NVidia.

Apparition d'API spécifiques indépendantes des API graphiques:
- CUDA pour les cartes graphiques NVidia
- Streams pour les cartes graphiques ATI
- OpenCL, API générique spécifiée par khronos

Avantages

  • La puissance de calcul théoriquement disponible (puissance de crête) sur les GPU est très largement supérieure à celle proposée par les CPU les plus rapides.
CPU Puissance de crête TDP
Intel Pentium 4 - 2.8 Ghz - 1 cœur - x86 2.7 GFlops 70 W
Intel Centrino Duo - 1.66 Ghz - 2 cœurs - x86 3.1 GFlops 30 W
Intel Xeon 5410 - 2.33 Ghz - 4 cœurs - x64 9,5 GFlops 100 W
Intel Core I7 980x - 3.33 Ghz - 6 coeurs - x64 20 GFlops 130 W
AMD Athlon 64 x2 - 3.0 Ghz - 2 cœurs - x64 5,9 GFlops 65 W
AMD Phenom II x6 - 3.0 Ghz - 6 cœurs - x64 17,8 GFlops 100 W
GPU Puissance de crête TDP
NVidia 9800 GT - 112 cœurs - 600 Mhz 370 GFlops 230 W
NVidia GT240 - 96 coeurs - 550 Mhz 260 GFlops 180 W
NVidia GTX 285 - 240 cœurs - 648 Mhz 700 GFlops 370 W
NVidia GTX 480 - 480 cœurs - 700 Mhz 1350 GFlops 410 W
ATI HD 3870 - 320 cœurs - 775 Mhz 490 GFlops 240 W
ATI HD 4890 - 800 cœurs - 850 Mhz 1360 GFlops 290 W
ATI HD 5970 - 3200 cœurs - 725 Mhz 4640 GFlops 430 W
  • Les adaptateurs graphiques sont peu couteux.

  • L'efficacité énergétique est meilleure sur les GPU que sur les CPU:
    - de 5 à 10 Watt / GFlops pour les CPU,
    - de 0.1 à 1 Watt / GFlops pour les GPU.

Inconvénients

  • Les GPU ayant prioritairement été conçus pour une utilisation dans le cadre d'algorithmes de l'infographie, ils peuvent présenter certaines limites techniques (abscence de certaines opérations mathématiques, entiers et réels simple précision, ...).

  • Les API actuelles ne font pas appel aux concepts de la programmation orientée objet mais seulement à la programmation structurée.

  • La programmation parallèle est obligatoire.

  • Les modèles de programmation parallèle utilisables sont limités.
    -> Il faut que l'application développée se prête bien à ce type de développement.

  • L'exploitation pleine et entière de la puissance de calcul disponible n'est pas simple à obtenir.

Les langages de développement de shader

Les API 3D modernes permettent de développer de petits programmes, les "shaders", destinés à être exécutés directement sur les GPU de la carte graphique.
C'est le cas sous DirectX depuis la version 9.0 de cette bibliothèque. Sous OpenGL, c'est aussi le cas depuis la version 2.0.

Il existe différents types de shaders. Les types les plus courants sont les "vertex shaders" et les "fragment shaders" aussi appelés "pixel shaders".

Le vertex shader configuré pour une opération d'affichage sera appelé pour traiter chacun des vertices (sommets) de définition des primitives graphiques de modélisation géométrique de la scène.
Le fragment shader configuré sera appelé pour traiter chacun des pixels issus de la rasterisation des primitives graphiques de modélisation.

Un premier type de parallèlisation pourra être obtenu en utilisant les vertex shaders:
En effet, les sommets d'une primitive graphique seront gérés en parallèle par traitement au moyen du vertex shader simultanément sur plusieurs GPU. Plusieurs primitives graphiques pourront même voir leurs sommets traités en parallèle sur plusieurs GPU.

Un deuxième type de parallèlisation pourra être obtenu en utilisant les fragment shaders:
Les pixels de rasterisation d'une primitive seront gérés en parallèle par traitement au moyen du fragment shader exécuté simultanément sur plusieurs GPU.

Les shaders sont de "relativement petits" programmes destinés à être exécutés un très très grand nombre de fois. Les problèmes solvables au moins d'une tâche unique exécutée de multiple fois sur des données différentes de type identique sont succeptibles de bien être adaptés à une programmation au moyen d'un langage de shader.

Contrainte: La communication des données et la récupération des résultats doivent s'intégrer dans le cadre d'un fonctionnement de type "programme avec affichage graphique 3D".

Données:
  - positions de sommets
  - normales
  - couleur de tracé
  - valeur de matériel
  - texture
  - variable d'environnement
  - ...
Résultats:
  - une image

OpenGL GLSL

GLSL est le langage de développement de shaders introduit dans OpenGL avec la version 2.0 de l'API. Il permet de créer des vertex shaders et des fragment shaders.

Si ni vertex shader ni fragment shader n'est spécifié, OpenGL utilise ses propres shaders "fixes". Si un vertex shader est employé, celui-ci sera utilisé en lieu et place du vertex shader d'OpenGL. Si un fragment shader est employé, celui-ci sera utilisé en lieu et place du fragment shader d'OpenGL.
-> Pour obtenir le même résultat écran qu'avec les shaders de base OpenGL, tout shader installé devra réaliser l'ensemble des opérations réalisées par le shader de base qu'il remplace.

Rôles des vertex shaders GLSL

Le but du vertex shader est de traiter le sommet qu'il reçoit en gestion pour préparer les informations qui seront transmises au vertex shader exécuté pour chacun des pixels de rastérisation de la primitive auquel appartient le sommet.

  • Réalisation de la transformation géométrique de passage des coordonnées de modélisation spécifiées par glVertex en coordonnées écran.
  • Réalisation des calculs d'éclairage si l'illumination "flat" ou Gouraud est utilisée.
  • Calcul de toute autre information à transmettre au fragment shader.
  • ...

Rôles des fragment shaders GLSL

Le but du fragment shader est de calculer la couleur du pixel pour lequel il est lancé. Toute information reçue du vertex shader est automatiquement interpolée entre les valeurs calculées par le vertex shader et en fonction de la position du pixel au sein de la primitive à laquelle il appartient.

  • Réalisation des calculs d'éclairage si l'illumination Phong est utilisée.
  • Réalisation du plaçage de texture.
  • ...

Syntaxe

Syntaxe proche de celle du langage C.

Exemple de vertex shader

uniform float RotationY;
varying vec3 MCposition;
varying vec3 Direction;
void main() {   
  Direction = vec3(0.0,0.0,-1.0);
  mat3 ry = mat3( cos(RotationY), 0.0, sin(RotationY),
                             0.0, 1.0,            0.0,
                 -sin(RotationY), 0.0, cos(RotationY));
  MCposition = ry*vec3(gl_Vertex);
  Direction = ry*Direction;
  gl_Position = ftransform();
}

Exemple de fragment shader

uniform vec3 Dimensions;
uniform sampler3D Texture3D;
varying vec3 MCposition;
varying vec3 Direction;
vec4 rayTracing(float x,float y,float z,
                float incx,float incy,float incz) {
  float alpha = 1.0;
  vec4 cl = vec4(0.0,0.0,0.0,1.0);
  do {
    vec4 c = texture3D(Texture3D,vec3(x,y,z));
    float aa = c.a*alpha;
    cl.r = cl.r + c.r*aa;
    cl.g = cl.g + c.g*aa;
    cl.b = cl.b + c.b*aa;
    alpha *= (1.0-c.a);
    x += incx;
    y += incy;
    z += incz; }
  while ( ( x >= 0.0 ) && ( x < 1.0 ) &&
          ( y >= 0.0 ) && ( y < 1.0 ) &&
          ( z >= 0.0 ) && ( z < 1.0 ) );
  return(cl);
}
void main() {
    float x = MCposition.x/Dimensions.x+0.5;
    float y = MCposition.y/Dimensions.y+0.5;
    float z = MCposition.z/Dimensions.z+0.5;
    gl_FragColor = rayTracing(x,y,z,
                              Direction.x/Dimensions.x,
                              Direction.y/Dimensions.y,
                              Direction.z/Dimensions.z);
}

Les variables "uniform" sont directement reçue de l'host. Les variables "varying" permettent la transmission d'information (avec interpolation) du vertex shader vers le fragment shader.
Dans le vertex shader, la variable gl_position est la variable qui doit être affectée pour indiquer la coordonnée écran du sommet.
Dans le fragment shader, la variable gl_FragColor est la variable qui doit être affectée pour indiquer la couleur du pixel.

Les shaders sont chargés et compilés directement sur la carte graphique par le pilote logiciel de la carte au moyen d'un jeu de fonction d'extension d'OpenGL.

Limitations

  • Tous les processeurs exécutent le même vertex shader.
  • Tous les processeurs exécutent le même fragment shader.
  • Pas de réel double précision.
  • Pas d'entier long.
  • Pas de récursivité!

Les API spécialisées

Les contraintes liées à l'utilisation des API graphiques pour les détourner à une autre fin que le calcul d'images sont trop importantes. Elles ne rendent pas cette technique suffisamment souple pour qu'elle soit adaptée à un large spectre applicatif.

Des API spécialisées ont été créées pour rendre plus versatile l'emploi des GPU.

Les trois principales sont NVidia CUDA, ATI Streams et OpenCL. CUDA et Streams sont des API propriétaires. OpenCL est une API libre développée par le consortium Kronos qui est en charge d'OpenGL, OpenML, ...

CUDA

CUDA permet la création de programmes destinés à être exécutés sur les multiples processeurs présents sur une carte graphique.

Le kit de développement CUDA inclut un compilateur destiné à générer le code exécutable sur la carte graphique.
Il inclut aussi une API permettant de contrôler les interactions entre le CPU, nommé host, et la(les) carte(s) graphique(s) utilisée(s), nommé device(s). Entre autres, cette API permet d'organiser l'allocation de mémoire sur les devices, les transferts mémoire host -> devices et devices -> host et bien entendu le lancement des programmes CUDA sur les devices.

La syntaxe du langage associé à CUDA, le CUDA-C est proche de celle du langage C.

Avantages

  • Beaucoup plus souple que les langages de shading
  • Assez propre
  • Langage très proche du langage C -> pas déstabilisant
  • ...

Inconvénients

  • Allers et retours mémoire entre host et devices très néfastes en terme de performance
  • Saturation possible de la bande passante mémoire
  • Gros challenge pour obtenir les performances de crête (exploitation maximale des caches mémoire, ...).
  • ...