Aller au contenu

Les cartes graphiques/Les processeurs de shaders

Un livre de Wikilivres.

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.

Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.

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.

Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé Thread Execution Control Unit, qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.

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 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 plus intuitifs sont les registres généraux, aussi appelés registres temporaires, qui servent à mémoriser des résultats temporaires. Les registres temporaires sont les registres du processeur proprement dit, ceux qu'il peut manipuler à loisir. Tout processeur digne de ce nom en possède. Mais un processeur de shader dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de shaders, qui servent à l'interfacer avec le reste du pipeline graphique.

Les registres d'interface avec le pipeline graphique

[modifier | modifier le wikicode]

Un processeur de shader reçoit des données provenant de l'unité de rastérisation, et envoie son résultat final aux ROPs. Il y a donc des registres d'entrée et de sortie spécialisés pour faire l'interface entre les deux. Ils servent d'interface avec le reste du pipeline graphique, notamment le rastérizeur et les ROPs, mais aussi avec les unités de texture.

Architecture carte graphique vertex avec texture

Les registres d'entrée réceptionnent les vertices/pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, seule l'unité de rastérisation peut écrire dedans. Ils sont initialisés avant l'exécution du shader.

Les registres de sortie sont là où le processeur stocke les résultats à envoyer aux ROP. Les registres de sorties sont en écriture seule. Avant l'apparition des shaders unifiés de DIrect X 10, les registres de sortie étaient différents entre les vertex shaders et les pixel shaders. Les pixel shaders n'avaient que deux registres de sorties : un pour la couleur à envoyer aux ROP, un autre pour la profondeur du pixel. Les vertex shaders avaient eu beaucoup plus de registres de sorties, vu que l'unité de rastérisation avait besoin de beaucoup d'information. Il y avait au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.

Registres de sortie des pixel/vertex shaders
Vertex shader Pixel shader
Couleur du pixel Couleur du sommet
Profondeur du pixel Position du sommet
Coordonnées de texture du sommet
Couleur de brouillard.

Il y a aussi des registres de texture , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec multitexturing, et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du shader, mais la plupart sont initialisé quand le shader termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.

Les registres de constante et d'adresse de constantes

[modifier | modifier le wikicode]

Les registres de constantes servent pour stocker des constantes utiles pour le shader. Par exemple, pour les vertex shaders, ils stockent 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.

Les pixel/vertex shaders 1.0 ne géraient que des constantes flottantes pour les vertex shaders, entières pour les pixel shaders. Mais les pixel/vertex shaders 2.0 et 3.0 avaient des registres de constantes séparés pour les nombres entiers, les nombres flottants, et même les nombres booléens. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Aussi, il y en avait 16, comparé aux centaines de registres de constantes flottants. Mais avec les pixel/vertex shaders 4.0 et plus, les registres de constante ont été fusionnés et n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.

L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de shaders modernes, au point qu'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.

Les registres de contrôle de flot

[modifier | modifier le wikicode]

Depuis les pixel/vertex shaders 3.0, les shaders sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un registre compteur de boucle, qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.

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 :

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.

Contenu d'un vecteur en fonction du type de données utilisé.

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. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.

Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
CPU Non-SIMD
CPU SIMD

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.

Instructions SIMD

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 le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.

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.

Un exemple : le jeu d’instruction du GPU de la Geforce 3

[modifier | modifier le wikicode]

La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article "A user programmable vertex engine", disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.

Les processeurs de vertices de la Geforce 3 disposent de registres 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".

Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :

OpCode Nom Description
Opérations mémoire
MOV Move vector -> vector
ARL Address register load miscellaneous
Opérations arithmétiques
ADD Add vector -> vector
MUL Multiply vector -> vector
MAD Multiply and add vector -> vector
MIN Minimum vector -> vector
MAX Maximum vector -> vector
SLT Set on less than vector -> vector
SGE Set on greater or equal vector -> vector
LOG Log base 2 miscellaneous
EXP Exp base 2 miscellaneous
RCP Reciprocal scalar-> replicated scalar
RSQ Reciprocal square root scalar-> replicated scalar
Opérations trigonométriques
DP3 3 term dot product vector-> replicated scalar
DP4 4 term dot product vector-> replicated scalar
DST Distance vector -> vector
Opérations d'éclairage géométrique
LIT Phong lighting miscellaneous

L'instruction la plus intéressante est clairement la dernière : elle applique l'algorithme d'illumination de Phong sur un sommet. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais l'algo de Phong est déjà là à la base.

Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de vertex shader ont acquis la possibilité de lire des données dans une texture.

On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.

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.

Vector mask register

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.

Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées 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 pour les shaders proprement dit

[modifier | modifier le wikicode]

Les processeurs VLIW plus évolués étaient des hybrides 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. Un 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 une 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 possibilités de co-issue plus développées.

Un exemple est celui du processeur de pixel shader de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le vertex shader a la possibilité de 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.

Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permttait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants co-issue avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.

GeForce 6800 Vertex processor.

Comme on peut le voir, le processeur dispose de beaucoup de registres : des registres d'entrée, qui réceptionnent les sommets lus par l'input assembler, des registres de sortie, dans lesquelles le processeur stocke les sommets transformés et éclairés, des registres de constantes qui servent à stocker des constantes, mais aussi des registres généraux/temporaires pour les résultats intermédiaires des calculs. Le shader à exécuter est mémorisé dans une instruction RAM, une sorte de mémoire locale spécialisée dans le stockage du shader proprement dit, du stockage des instructions à exécuter.

Les processeurs VLIW sur les cartes graphiques AMD

[modifier | modifier le wikicode]

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.

Un cas particulier : 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 vus dans le chapitre précédent. 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 :

Implementation de l'unité alpha des registers combiners

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.

Implementation de l'unité RGB des registers combiners

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

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.