Les cartes graphiques/Les processeurs de shaders
Les shaders sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de shaders. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
- L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.
En conséquence, il est possible de traiter chaque instance d'un shader en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes warps processor, ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le Thread Execution Control Unit en jaune, qui répartit les différentes instances du shader sur les différents processeurs. Elle est aussi appelée le processeur de commandes, comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.
Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de shaders, la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
Les processeurs de shaders modernes : les processeurs SIMD
[modifier | modifier le wikicode]Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
- Graphics Core Next 1 instruction set ;
- Graphics Core Next 2 instruction set ;
- Graphics Core Next 3 and 4 instruction sets ;
- Graphics Core Next 5 instruction set ;
- "Vega" 7nm instruction set architecture (also referred to as Graphics Core Next 5.1) ;
- Jeu d'instruction des GPU de type RDNA3 d'AMD.
Les processeurs de shaders peuvent effectuer le même calcul sur plusieurs vertices ou plusieurs pixels à la fois. On dit que ce sont des processeurs parallèles, à savoir qu'ils peuvent faire plusieurs calculs en parallèle dans des unités de calcul séparées. Suivant la carte graphique, on peut les classer en deux types, suivant la manière dont ils exécutent des instructions en parallèle : les processeurs SIMD et les processeurs VLIW. Dans cette section, nous allons voir les processeurs SIMD.
Avant d'expliquer à quoi correspondent ces deux termes, sachez juste que l'usage de processeurs VLIW dans les cartes graphiques n'est plus très courant de nos jours. Il a existé des cartes graphiques assez anciennes qui utilisaient des processeurs de type VLIW, mais ce n'est plus en odeur de sainteté de nos jours. De nos jours, les processeurs de shaders sont tous des processeurs SIMD ou des dérivés (la technique dite du SIMT est une sorte de SIMD amélioré). Cependant, il arrive que même en étant des processeurs SIMD, certaines de leurs instructions soient inspirées des instructions VLIW.
Les instructions SIMD
[modifier | modifier le wikicode]Les instructions SIMD manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des vecteurs, des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
Les vecteurs sont stockés dans des registres vectoriels, aussi appelés registres SIMD. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU.
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits, avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
Les instruction scalaires entières, typiques des CPU
[modifier | modifier le wikicode]Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des instruction scalaires. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d'instructions entières, agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des instructions flottantes scalaires, à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais pour les processeurs de shaders, ce n'est pas le cas. Le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Aussi, les processeurs de shaders disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des instructions transcendantales, car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
Les instructions en co-issue
[modifier | modifier le wikicode]Beaucoup de cartes graphiques récentes comme anciennes incorporent des instructions de co-issue qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de co-issue regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à co-issue en plus des instructions normales. Les instructions à co-issue sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à co-issue : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de co-issue est la co-issue entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
La prédication et le SIMT
[modifier | modifier le wikicode]Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de prédication. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
- une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
- suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le Vector Mask Register. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le Vector Mask Register stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
La prédication avec une pile SIMT
[modifier | modifier le wikicode]Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La pile de masques remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
- Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
- Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
Les compteurs d'activité
[modifier | modifier le wikicode]Une variante de la technique précédente remplace la pile de masques par des compteurs d'activité. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
masque 1 | 1 | 1 | 1 | 1 |
---|---|---|---|---|
masque 2 | 0 | 1 | 1 | 1 |
masque 3 | 0 | 1 | 1 | 1 |
masque 4 | 0 | 0 | 0 | 1 |
masque 1 | vide |
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
masque 1 | 3 | 1 | 1 | 0 |
---|
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
A chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
Les processeurs de shaders anciens : des processeurs VLIW
[modifier | modifier le wikicode]Après avoir vu les processeurs de shaders de type SIMD, nous allons voir les processeurs de shaders de type VLIW. Les cartes graphiques AMD assez anciennes utilisaient des processeurs de type VLIW, sur la microarchitecture Terascale, avant le passage à l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi la technique sur les Geforce 6 et 7, et même auparavant sur les Geforce 3/4 et FX. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11. Aucune carte graphique DirextX 12 n'utilise de processeurs VLIW.
Avant d'expliquer ce qu'est un processeur VLIW, il faut faire un petit interlude sur l'intérieur d'un processeur, quelques rappels. Un processeur moderne contient plusieurs circuits de calcul, chacun étant relativement spécialisé. Par exemple, un processeur moderne peut incorporer une dizaine de circuits capables de faire des additions/soustractions, 3 circuits pour faire des multiplications, un circuit pour faire des divisions, une dizaine de circuits pour les opérations logiques et bit à bit, etc. De tels circuits sont appelés des unités de calcul.
Il est possible de lancer plusieurs opérations, une par unité de calcul. C'est possible sur les processeurs dits superscalaires, ceux à exécution dans le désordre, mais aussi sur des processeurs plus simples qui ont juste un pipeline (ils sont dits à émission dans l'ordre). En général, les processeurs disposent de circuits pour répartir les opérations/instructions sur les unités de calcul adéquates. Les circuits en question portent des noms à coucher dehors : unité d'émission, scoreboard, fenêtre d'instruction, et j'en passe. Mais les processeurs VLIW arrivent à répartir les instructions sur plusieurs unités de calcul sans utiliser le moindre matériel : tout est réalisé en logiciel. Un indice pour comprendre comment : les instructions en co-issue le font nativement, comme on l'a vu plus haut.
Les processeurs VLIW : généralités
[modifier | modifier le wikicode]Les processeurs VLIW peuvent être vus comme des processeurs dont toutes les instructions sont des instructions à co-issue sous stéroïdes. Le terme VLIW, terme qui désigne tous les processeurs qui regroupent plusieurs opérations en une seule instruction. La différence est que sur ces processeurs, toutes les instructions sont des instructions à co-issue, sans exception.
Les processeurs VLIW regroupent plusieurs instructions/opérations dans des sortes de super-instructions appelées des faisceaux d'instruction (aussi appelés bundle). Le faisceau est chargé en une seule fois et est encodé comme une instruction unique. En clair, les processeurs VLIW chargent "plusieurs instructions à la fois" et les exécutent sur des unités de calcul séparées (les guillemets sont là pour vous faire comprendre que c'est en réalité plus compliqué).
Une autre manière de voir les choses est que les faisceaux d'instruction regroupent plusieurs opérations en une seule super-instruction machine. Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées.
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant de deux circuits d'addition et d'un circuit pour les multiplications : il sera possible de regrouper deux additions avec une multiplication, mais pas deux multiplications ou trois additions. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Sur les processeurs de shaders anciens, on pouvait que regrouper jusqu’à 5/6 opérations. Mais la plupart du temps, le regroupement était de 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence.
Les processeurs VLIW de chez NVIDIA et AMD/ATI
[modifier | modifier le wikicode]Les processeurs VLIW les plus simples étaient des hubrides SIMD/VLIW qui pouvaient exécuter une opération SIMD en co-issue avec une opération arithmétique flottante très complexe, à savoir une opération transcendantale. Le processeur de vertices de la Geforce 3, le tout premier processeur de shader au monde, était l'un d’entre eux. Un autre exemple est le processeur de vertices de la Geforce 6880, qui lui aussi pouvait faire une opération SIMD sur des flottants de 32 bits, en co-issue avec uen opération transcendantale sur des flottants de 32 bits. Les processeurs de vertices simples étaient souvent de ce type.
Par contre, les processeurs de pixel shader avaient des possiiblités de co-issue plus développées. Un exemple est celui du processeur de pixel shader de la Geforce 6800, qui regroupait 4 opérations scalaires, une opération d'accès aux textures, et une opération transcendantale. En tout, 6 opérations pouvaient s'exécuter simultanément. Il était possible de simuler une opération SIMD en utilisant plusieurs opérations scalaires. Avec les 4 opérations scalaires, on pouvait émuler : une opération SIMD agissant sur des vecteurs de 4 éléments, une opération vectorielle sur 3 flottants, regroupé avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune. Mais il s'agissait d'un jeu d'instruction VLIW proprement dit.
Les cartes graphiques AMD et ATI assez anciennes, d'architectures R300, de la série des Radeon 9700, étaient aussi des processeurs VLIW. Elles incorporaient ces instructions pour faciliter les calculs de produits vectoriels combinés avec de l'éclairage. Elles combinaient trois opérations : une opération SIMD sur des vecteurs de 4 flottants, avec une opération scalaire. Les contraintes de combinaisons des instructions sont assez complexes.
- Certaines opérations sont disponibles à la fois pour l'opération scalaire et vectorielle. C'est le cas pour les opérations entières suivante : les comparaisons, les additions, les soustractions, les opérations logiques, les décalages, les opérations bit à bit, les instructions CMOV. Les mêmes opérations, mais sur des opérandes flottants, sont disponibles aussi bien pour l’opération scalaire que vectorielle.
- L'opération vectorielle pouvait être une des opérations précédente, mais gérait aussi des multiplications et additions flottante, une opération MAD, des produits vectoriels ou scalaires, et diverses opérations d'arrondis ou de conversion entre flottants.
- L'opération scalaire était : soit une opération de conversion entier-flottant, soit une opération transcendantale (entière ou flottante), soit une multiplication entière 32 bits, soit une multiplication flottante 32 bits.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [1]
Par la suite, les cartes graphiques AMD ont changé les possibilités de combinaisons entre opérations. Le changement a réduit le nombre d'opérations simultanées à deux. La seconde opération est une opération scalaire flottante, la possibilité de faire une opération entière a été retirée. La première opération est soit une opération transcendantale, soit une opération vectorielle sur trois flottants. L'origine de ce changement, peu intuitif, sera expliqué dans le chapitre sur la microarchitecture des processeurs de shaders. Pour résumer, le processeur peut faire au choix :
- 4 opérations flottantes en parallèle : 3 calculs flottants via SIMD, plus un par l’opération scalaire.
- une opération transcendantale couplée à une opération flottante.
Le tout donna une architecture appelée par AMD : VLIW-4. 4, car le processeur peut faire au grand max 4 opérations flottantes en parallèle.
L'abandon des architectures VLIW
[modifier | modifier le wikicode]Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un bundle. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
Les registres des processeurs de shaders
[modifier | modifier le wikicode]Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Les anciennes cartes graphiques avaient beaucoup de registres spécialisés, c'est à dire que chaque registre avait une fonction bien définie et ne pouvait stocker qu'un type de donnée bien précise. Mais avec l'évolution de Direct X et d'Open GL, les registres sont devenus de moins en moins spécialisés et sont devenues des registres généraux, interchangeables, capables de stocker des données arbitraires. De nos jours, les registres spécialisés sont devenus anecdotiques, car leur a perdu de son intérêt avec l'unification des shaders : la plupart des registres spécialisés n'avaient de sens que pour les vertices et rien d'autre.
Les anciennes cartes graphiques : beaucoup de registres spécialisés
[modifier | modifier le wikicode]Sur les processeurs de vertices des anciennes cartes 3D, il y avait bien quelques registres généraux, sans fonction préétablie, mais ils étaient secondés par un grand nombre de registres spécialisés assez nombreux. Les registres des processeurs de vertices peuvent se classer en plusieurs types.
Les registres d'entrée réceptionnent les vertices ou pixels. Les registres de sortie sont là où le processeur stocke ses résultats finaux. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, les registres de sorties sont en écriture seule. Ils servent d'interface avec le reste du pipeline graphique, notamment le rastérizeur et les ROPs.
Viennent ensuite les registres généraux, aussi appelés registres temporaires, qui peuvent mémoriser tout type de données. Les registres temporaires sont les registres du processeur proprement dit, ceux qu'il peut manipuler à loisir. Il faut noter que la lecture des textures passe souvent par ces registres : la lecture d'un texel charge celui-ci dans un registre temporaire. A la rigueur, un texel lu peut passer directement dans un registre de sortie, mais c'est signe que le pixel shader ne fait aucun traitement dessus.
Les registres de constantes servent pour stocker les matrices servant aux différentes étapes de transformation ou d'éclairage. Ces constantes sont placées dans ces registres peu après le chargement du vertex shader dans la mémoire vidéo. Toutefois, le vertex shader peut écrire dans ces registres, au prix d'une perte de performance particulièrement violente.
L'adressage des registres de constante est quelque peu particulier. Dans les faits, il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un local store un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce local store en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce local store est dans un registre, séparé du reste, appelé le registre d'adresse de constante.
Un exemple classique est celui des processeurs de vertices de la Geforce 3. Ses registres sont des registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Les cartes graphiques modernes : un grand nombre de registres vectoriels
[modifier | modifier le wikicode]Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD. Un point important est que les registres SIMD sont séparés des registres entiers. Les registres scalaires stockent chacun un seul entier, éventuellement un seul flottant. Ils sont manipulés par les instructions scalaires, que ce soit des calculs simples, des comparaisons, des branchements, des opérations logiques ou autres. A l'opposé, les registres SIMD sont manipulés par des instructions SIMD et seulement celles-ci. La séparation entre registres SIMD/scalaires va de pair avec la séparation entre instructions scalaires/SIMD.