Les cartes graphiques/Les processeurs de shader VLIW et DirectX 9
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. 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.
Les processeurs de shader VLIW
[modifier | modifier le wikicode]Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire ou l'exécution dans le désordre. Les processeurs de shader de type SIMD le permettent avec les instructions en co-issue, mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la co-issue à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Généralités sur les architectures VLIW
[modifier | modifier le wikicode]Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des faisceaux d'instruction (aussi appelés bundle). 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. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.

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 d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. 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.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
L'utilisation des architectures VLIW pour les shaders
[modifier | modifier le wikicode]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 bien plus complexes, avec un scoreboard et autres joyeusetés.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW : 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. Les anciennes cartes graphiques préféraient se passer de scoreboard, afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de co-issue.
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. Avec l'évolution de la technologie, il est devenu plus rentable de remplacer le VLIW par du SIMD avec un scoreboard matériel. Le gain en performance valait plus que le budget en transistor pour le scoreboard.
Avant les pixel shaders : les register combiners
[modifier | modifier le wikicode]La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des register combiners. Pour rappel, les register combiners sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de multitexturing, les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
- un registre zéro, contenant toujours 0 ;
- un registre fog contenant la couleur du brouillard ;
- deux registres de couleur configurables par l'utilisateur.
Les registres modifiables sont les suivants :
- Des registres de texel, un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
- Des registres généraux qui n'ont pas de fonction prédéterminée ;
- Deux registres d'éclairage par sommet qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :

L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.

Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
| Multiplication | Biais facultatif |
|---|---|
| 0.5 | X |
| 1 | - 0.5 facultatif |
| 2 | -0.5 facultatif |
| 4 | X |
Les microarchitectures Terascale d'AMD
[modifier | modifier le wikicode]Voyons maintenant l'exemple des GPU AMD de microarchitecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en lockstep : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
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). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [1].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
Les GPU NVIDIA 6800
[modifier | modifier le wikicode]D'autres processeurs de shaders pouvaient exécuter une opération SIMD en co-issue avec une opération transcendantale. Un exemple est le processeur de vertex shader de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de vertex shader pouvaient faire une opération SIMD sur des flottants de 32 bits, en co-issue avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de co-issue plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de vertex shader de la Geforce 6800 disposait : d'une unité d'accès mémoire/textures, d'une unité de calcul transcendantale, et d'une unité de calcul SIMD. L'unité SIMD permettait de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. La présence d'une unité d'accès aux textures implique que le vertex shader peut lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de vertex shader.

Le processeur de pixel shader, quant à lui, avait globalement les mêmes capacités. Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, histoire de suivre la documentation NVIDIA, mais le terme est quelque peu trompeur, car elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. Chaque unité SIMD pouvait soit une opération SIMD sur un vecteur, soit faire une opération sur 3 pixels et une opération scalaire sur le quatrième, soit deux opérations vectoriels chacune sur deux pixels.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
