# Les cartes graphiques/La microarchitecture des processeurs de shaders

La conception interne (aussi appelée microarchitecture) des processeurs de shaders possède quelques particularités idiosyncratiques. La microarchitecture des processeurs de shaders est particulièrement simple. On n'y retrouve pas les fioritures des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, les unités de décodage et/ou de contrôle sont relativement simples, peu complexes. La majeure partie du processeur est dédié aux unités de calcul.

## Les unités de calcul d'un processeur de shader SIMD

Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des instructions SIMD. Mais il peut aussi gérer des instructions scalaires, à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.

### Les unités de calcul SIMD

Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.

### Plusieurs unités SIMD, liées au format des données

Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs white papers, avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.

### Les unités de calcul scalaires

Les GPU modernes incorporent une unité de calcul entière scalaire, séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une unité de calcul flottante scalaire, utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération Multiply-And-Add (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d'unité de calcul spéciale (Special Function Unit), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.

### L'unité de texture/lecture/écriture

L'unité d'accès mémoire s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.

## La double émission et l'exécution avec un scoreboard

Un processeur de shader SIMD contient donc beaucoup d'unité de calcul, qui peuvent en théorie fonctionner en parallèle. Il est en théorie possible d'exécuter des instructions séparés dans des unités de calcul séparées. Cette possibilité est appelée l'émission multiple, car on "émet" plusieurs instructions à la fois dans des unités de calcul séparées.
Son support sur les cartes graphique a beaucoup varié dans le temps, mais les anciennes cartes graphiques comme les nouvelles en dispose. La différence est que l'émission multiple est gérée de manière différente entre les anciennes cartes graphiques SIMD et VLIW. Le VLIW est naturellement à émission multiple, pas le SIMD. Mais il est possible d'ajouter de l'émission multiple sur des processeurs SIMD.

### Les lectures non-bloquantes

Toutes les cartes graphiques gèrent une technique d'émission multiple appelée les lectures non-bloquantes. L'idée est simple : elle permet d'utiliser l'unité de texture en parallèle des autres unités de calcul. En clair, pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Vu que les lectures de texture prennent énormément de temps, cette optimisation permet de faire des calculs en avance, en attendant que le texel arrive. La technique s'applique aussi aux lectures normales, à savoir quand on lit autre chose qu'une texture.
Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et le texel en cours de lecture, voire un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. L'implémentation demande de rajouter un circuit d'émission entre le décodeur d'instruction et les unités de calcul, qui vérifie si l'instruction décodée peut s'exécuter ou non, en comparant les registres utilisés par l'instruction avec le registre de destination. Si ce n'est pas le cas, elle est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.

### La double émission (dual issue)

Mais l'émission multiple ne se limite pas aux lectures non-bloquantes. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire, en même temps, un calcul dans chaque ALU. Pendant que l'unité transcendantale fait un calcul trigonométrique quelconque, l'autre ALU effectue des calculs SIMD sur d'autres données.
La seule contrainte est que les deux opérations doivent pouvoir s'exécuter en parallèle. Par exemple, il ne faut pas que le résultat de la première dépende de la seconde. De même, il ne faut pas qu'elles écrivent dans un même registre, sous peine de perdre le résultat d'une des opération. Les deux cas précédents sont regroupés sous le terme de dépendances de données. S'il y a une dépendance de données entre deux instruction, on ne peut pas les exécuter en parallèle. Et c'est encore une fois le rôle de l'unité d'émission que de détecter ces dépendances. Pour cela, elle vérifie si deux instructions utilisent les mêmes registres.
De plus, elle vérifie d'autres formes de dépendances qui empêchent une exécution simultanée de deux opérations/instructions. Elle vérifie si les deux instructions sont du bon type. Elle vérifie aussi que les unités de calcul sont libres, qu'elles n'exécutent pas déjà une instruction. C'est possible si l'instruction en question est multicycle, qu'elle met plusieurs cycles pour calculer le résultat. Une unité d'émission de ce type est appelée un scoreboard.

## Les processeurs de shaders VLIW

Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en co-issue, abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en co-issue. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
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.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de scoreboard. Mieux valait 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. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la co-issue, ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un scoreboard matériel.

### Les architectures VLIW pures, sans unité SIMD

Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
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 hybrides SIMD/VLIW et les instructions à co-issue

La gestion des instructions en co-issue peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en co-issue regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la co-issue aisni, avec la possibilité de co-issue une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la co-issue fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de co-issue était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de vertex shader de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet 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. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des quads. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec co-issue à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un quad.
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, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
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).

## Le multithreading matériel

Les processeurs de shader sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de shaders en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de shaders disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue. A la place, ils utilisent du multithreading matériel.
Le multithreading matériel vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'hyperthreading d'Intel ? C'est une version du multithreading matériel. L'idée était d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. L'idée est que si un thread est bloqué par un accès mémoire, d'autres threads exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Les programmes en question étaient appelés des threads, mais c'était des threads d'instructions non-SIMD. Or, un thread d'instructions SIMD est un warp, ni plus ni moins, les mêmes techniques peuvent s'appliquer. L'idée est de permettre à un processeur de shader d'exécuter plusieurs warps sur le même processeur, mais pas exactement en même temps, le processeur commutant régulièrement d'un warp à l'autre. L'idée est encore une fois que si un thread lit une texture, d'autres threads exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.

### Le Fine Grained Multithreading pur

Le Fine Grained Multithreading (FGMT) change de programme/thread/warp à chaque cycle d'horloge. L'avantage principal est que l'implémentation matérielle est très simple.
De tels processeurs permettent de masquer la latence des accès mémoire, sous conditions. Par exemple, imaginons que les lectures prennent 8 cycles. Si le processeur gère 8 threads et en change à chaque cycle, alors deux instructions d'un même programme seront espacées de 8 cycles de distance. Le résultat de la lecture est disponible immédiatement pour l'instruction suivante, sans avoir à bloquer le pipeline.
Un défaut est que le FGMT demande d'utiliser énormément de registres. En effet, prenons le cas d'un processeur sans FGMT avec 8 registres (ce qui est peu). Maintenant, ajoutons un support du FGTM : les registres devront être dupliqués pour que chaque thread ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un thread à l'autre à chaque cycle. Un processeur capable de gérer 128 threads devra multiplier ses registres par 128 ! Ca vous parait beaucoup ? Sachez qu'un processeur de shader peut exécuter entre 16 et 32 thrads/warps, ce qui multiplie le nombre de registres par 16/32.

### Le Fine Grained Multithreading à émission dans l'ordre

Après, impossible de gérer des accès mémoire d'une centaine de cycles avec cette méthode : il faudrait plusieurs centaines de threads. Du FGMT capable de gérer 16/32 warps simultannés ne permet que de gérer des accès mémoires de 16/32 cycles, ce qui est bien en-deça des 150 à 200 cycles d'un accès mémoire sur un GPU.
Une autre forme de Fine Grained Multithreading permet au processeur s'exécuter un même thread durant plusieurs cycles avant d'en changer. La techniques est beaucoup utilisée sur les cartes graphiques modernes, et sur certains processeurs spécialisés. Elle permet de mieux masquer les accès mémoire.
Un exemple historique assez ancien est le processeur Tera MTA (MultiThreaded Architecture), qui introduit la technique de l'anticipation de dépendances explicite (Explicit-Dependence lookahead). L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. Et elle est utilisée sur les cartes graphiques depuis la sortie des cartes graphiques NVIDIA de microarchitecture Kepler.
Une autre méthode, elle aussi utilisée sur les cartes graphiques, est de séparer le processeur de shader en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une file d'instruction. Là, elles attendent leur tour, le temps que leurs opérandes soient disponibles, que les données à manipuler soient enfin disponibles. Une unité d'émission vérifie à chaque cycle si chaque instruction est prête à s'exécuter. Les instructions prêtes sont soit envoyées aux unités de calcul, soit attendent leur tour. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
Il faut noter que le chargement des instructions depuis la mémoire et leur exécution dans les unités de calcul se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Ce fonctionnement dit en pipeline a de nombreuses conséquences peu intuitives. Et le pipeline des processeurs de shaders est en réalité encore plus compliqué que ça. Le chargement d'une instruction et son décodage prend facilement entre 2 et 20 d'étapes d'un cycle chacune, pareil pour son exécution, et j'en passe.
Si un thread accède à la mémoire, il est mis en pause et le processeur ne l'exécute plus tant que l'accès mémoire n'est pas terminé. Mais les autres threads continuent de s'exécuter sur le processeur. Et si plusieurs threads sont en pause, ils sont retirés du pool de thread chargés à chaque cycle. L'avantage est que si un thread est mis en pause par un accès mémoire, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter.
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction, sans quoi la technique marche nettement moins bien. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque thread ait assez de réserve pour exécuter plusieurs instructions consécutives. En soi, le découplage du chargement et de l'exécution est nécessaire pour cela. L'unité de chargement lit des instructions depuis le cache d'instruction, elle change de thread à chaque cycle ou presque. Il arrive qu'elle charge plusieurs instructions consécutives d'un même thread, mais cela ne dure que quelques cycles d'horloge. Pareil pour l'unité d'émission : soit elle change de thread à chaque cycle, soit elle peut envoyer aux ALU plusieurs instructions consécutives d'un même thread, tout dépend du GPU considéré.
De plus, il faut ajouter au processeur une unité d'émission pour vérifier si deux instructions consécutives peuvent s'exécuter de manière consécutive, pour détecter les dépendances entre instructions. Pour les connaisseurs, l'unité d'émission est généralement très simple, basée sur un scoreboard simplifié (un vecteur de registre), qui ne gère que l’exécution dans l'ordre. Mais le grand nombre de registres et de threads fait que le scoreboard devient rapidement impraticable, même s'il est censé être très simple sur le principe. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé "Register File Allocation", déposé par NVIDIA durant décembre 2009.

## Le banc de registres d'un processeur de shader

Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de threads hardware simultanés. En effet, prenons le cas d'un processeur sans FGMT avec 8 registres (ce qui est peu). Avec le FGMT, les registres devront être dupliqués pour que chaque thread ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un thread à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de shader peut exécuter entre 16 et 32 thrads/warps, ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de coeurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de shaders ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !

### Le banc de registre est multiport de type externe

Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le banc de registre. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Ajoutons aussi que le banc de registre doit permettre de lire deux opérandes par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre.
Les processeurs font face aux mêmes problèmes, surtout celui du nombre de ports, mais ils peuvent se permettre des solutions assez simples. Ils utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient pus lent. Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de multiport externe.
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un conflit d'accès aux banques.

### L'Operand Collector et les caches de register reuse

Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le collecteur d'opérandes (operand collector). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du thread/warp pour ne pas confondre des opérandes entre threads, et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'operand reuse cache, aussi appelés register reuse cache. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un register reuse cache, pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même slot d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les register reuse cache et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* Operand collector architecture
* Methods and apparatus for source operand collector caching