Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.46.0-wmf.23
first-letter
Média
Spécial
Discussion
Utilisateur
Discussion utilisateur
Wikilivres
Discussion Wikilivres
Fichier
Discussion fichier
MediaWiki
Discussion MediaWiki
Modèle
Discussion modèle
Aide
Discussion aide
Catégorie
Discussion catégorie
Transwiki
Discussion Transwiki
Wikijunior
Discussion Wikijunior
TimedText
TimedText talk
Module
Discussion module
Event
Event talk
Liste de mnémoniques
0
12821
763249
763248
2026-04-08T12:37:52Z
JackPotte
5426
Révocation d’une modification de [[Special:Contributions/~2026-21748-19|~2026-21748-19]] ([[User talk:~2026-21748-19|discussion]]) vers la dernière version de [[User:DavidL|DavidL]]
759822
wikitext
text/x-wiki
Cette page contient une '''liste de [[w:mnémotechnique|mnémotechniques]]''', c’est-à-dire différentes constructions qui facilitent la mémorisation.
Par exemple : afin de retenir beaucoup plus facilement les sept péchés capitaux, une phrase mnémotechnique possible offrant une image plus visuelle regroupant une partie ou la totalité d'un péché :
Par goût, Colette envie l'orgue luxueux d'Avarice
(Paresse, gourmandise, colère, envie, orgueil, luxure, avarice)
== Atmosphère ==
=== Structure verticale ===
L’atmosphère terrestre présente une structure verticale en couches basée sur l’évolution de la température. On distingue la Troposphère (siège des phénomènes météorologiques), la Stratosphère, la Mésosphère et la Thermosphère.
'''T'''out '''S'''ur '''M'''a '''T'''ête
== Mathématiques ==
=== Les 126 premières décimales du nombre π (pi) ===
Le nombre de lettres de chaque mot de ce poème correspond à une décimale de [[w:Pi|Pi]]. Un mot de dix lettres correspond au chiffre 0.
{|
|- align="center"
| ''Que'' || || ''j'' || ''<nowiki>’</nowiki>'' || ''aime'' || ''à'' || ''faire'' || ''apprendre'' || ''un'' || ''nombre'' || ''utile'' || ''aux'' || ''sages'' || ''!''
|- align="center"
! 3 !! , !! 1 !! !! 4 !! 1 !! 5 !! 9 !! 2 !! 6 !! 5 !! 3 !! 5
|}
{|
|- align="center"
| ''Immortel'' || ''Archimède'' || '','' || ''artiste'' || ''ingénieur'' || '',''
|- align="center"
! 8 !! 9 !! !! 7 !! 9
|}
{|
|- align="center"
| ''Qui'' || ''de'' || ''ton'' || ''jugement'' || ''peut'' || ''priser'' || ''la'' || ''valeur'' || ''?''
|- align="center"
! 3 !! 2 !! 3 !! 8 !! 4 !! 6 !! 2 !! 6
|}
{|
|- align="center"
| ''Pour'' || ''moi'' || '','' || ''ton'' || ''problème'' || ''eut'' || ''de'' || ''pareils'' || ''avantages'' || ''...''
|- align="center"
! 4 !! 3 !! !!3!! 8 !! 3 !! 2 !! 7 !! 9
|}
{|
|- align="center"
| ''Jadis'' || '','' || ''mystérieux'' || , || ''un'' || ''problème'' || ''bloquait''
|- align="center"
! 5 !! !! 0 !! !! 2 !! 8 !! 8
|}
{|
|- align="center"
| ''Tout'' || ''l'' || ''<nowiki>’</nowiki>'' || ''admirable'' || ''procédé'' || '','' || ''l'' || ''<nowiki>’</nowiki>'' || ''œuvre'' || ''grandiose''
|- align="center"
! 4 !! 1 !! !! 9 !! 7 !! !! 1 !! !! 6 !! 9
|}
{|
|- align="center"
| ''Que'' || ''Pythagore'' || ''découvrit'' || ''aux'' || ''anciens'' || ''Grecs'' ||''.''
|- align="center"
! 3 !! 9 !! 9 !! 3 !! 7 !! 5
|}
{|
|- align="center"
| ''Ô'' || ''quadrature'' || ''!'' || ''Vieux'' || ''tourment'' || ''du'' || ''philosophe'' || ''...''
|- align="center"
! 1 !! 0 !! !! 5 !! 8 !! 2 !! 0
|}
{|
|- align="center"
| ''Insoluble'' || ''rondeur'' || '','' || ''trop'' || ''longtemps'' || ''vous'' || ''avez''
|- align="center"
! 9 !! 7 !! !! 4 !! 9 !! 4 !! 4
|}
{|
|- align="center"
| ''Défié'' || ''Pythagore'' || ''et'' || ''ses'' || ''imitateurs'' || ''.''
|- align="center"
! 5 !! 9 !! 2 !! 3 !! 0
|}
{|
|- align="center"
| ''Comment'' || ''intégrer'' || ''l'' || ''<nowiki>’</nowiki>'' || ''espace'' || ''plan'' || ''circulaire'' || ''?''
|- align="center"
! 7 !! 8 !! 1 !! !! 6 !! 4 !! 0
|}
{|
|- align="center"
| ''Former'' || ''un'' || ''triangle'' || ''auquel'' || ''il'' || ''équivaudra'' || ''?''
|- align="center"
! 6 !! 2 !! 8 !! 6 !! 2 !! 0
|}
{|
|- align="center"
| ''Nouvelle'' || ''invention'' || '':'' || ''Archimède'' || ''inscrira''
|- align="center"
! 8 !! 9 !! !! 9 !! 8
|}
{|
|- align="center"
| ''Dedans'' || ''un'' || ''hexagone'' || '';'' || ''appréciera'' || ''son'' || ''aire''
|- align="center"
! 6 !! 2 !! 8 !! !! 0 !! 3 !! 4
|}
{|
|- align="center"
|''Fonction'' || ''du'' || ''rayon'' || ''.'' || ''Pas'' || ''trop'' || ''ne'' || ''s'' || ''<nowiki>’</nowiki>'' || ''y'' || ''tiendra''
|- align="center"
! 8 !! 2 !! 5 !! !! 3 !! 4 !!2 !! 1 !! !! 1 !! 7
|}
{|
|- align="center"
|''Dédoublera'' || ''chaque'' || ''élément'' || ''antérieur''
|- align="center"
! 0 !! 6 !! 7 !! 9
|}
{|
|- align="center"
|''Toujours'' || ''de'' || ''l'' || ''<nowiki>’</nowiki>'' || ''orbe'' || ''calculé'''e'''''¹ || ''approchera''
|- align="center"
! 8 !! 2 !! 1 !! !! 4 !! 8 !! 0
|}
{|
|- align="center"
|''Définira'' || ''limite'' || '';'' || ''enfin'' || '','' || ''l'' || ''<nowiki>’</nowiki>'' || ''arc'' || '','' || ''le'' || ''limiteur''
|- align="center"
! 8 !! 6 !! !! 5 !! !! 1 !! !! 3 !! !! 2 !! 8
|}
{|
|- align="center"
|''De'' || ''cet'' || ''inquiétant'' || ''cercle'' || '','' || ''ennemi'' || ''trop'' || ''rebelle''
|- align="center"
! 2 !! 3 !! 0 !! 6 !! !! 6 !! 4 !! 7
|}
{|
|- align="center"
|''Professeur'', || ''enseignez'' || ''son'' || ''problème'' || ''avec'' || ''zèle'' || ''!''
|- align="center"
! 0 !! 9 !! 3 !! 8 !! 4 !! 4
|}
¹ Le mot orbe est du masculin mais ce ne fut pas toujours le cas, ceci induit à présent une faute d’accord à « ''calculée'' » que l’on peut remplacer par « ''escompté'' » pour conserver le bon nombre de lettres.
=== Premières décimales de l’inverse du nombre π : 1/π ===
La valeur de '''1/π = 0,3183098''' se retient sous la forme d’une phrase historique faisant référence aux trois glorieuses :
{{Citation|« Les 3 journées de 1830 ont renversé 89 [la Révolution de 1789] »}}.
Le score de la finale de la coupe du monde de football 98 a fait '''un surpris''' (1 sur pi), côté Brésil : '''0''', '''3''' (c’est le feu ! = '''18''' = '''''Cher''''' payé ?) ; '''3-0''' ('''''Gard'''''ez en souvenir) '''98'''
=== Trigonométrie ===
Le principe est de ne retenir que la première lettre ou la première syllabe des mots-clés de chaque définition ou théorème :
==== Définitions ====
* « Cosinus = côté Adjacent sur l'Hypoténuse »
* « Sinus = côté Opposé sur l'Hypoténuse »
* « Tangente = côté Opposé sur côté Adjacent »
Une "phrase" permet de se rappeler ces trois définitions à la fois :
'''cah soh toa''' pour « ''casse-toi'' » : '''C'''osinus = '''A'''djacent sur '''H'''ypoténuse ; '''S'''inus = '''O'''pposé sur '''H'''ypoténuse ; '''T'''angente = '''O'''pposé sur '''A'''djacent. Certains préfèrent '''soh cah toa'''.
On peut aussi ressortir les dénominateurs de chaque fraction (afin de ne pas mélanger numérateurs et dénominateurs dans ces égalités) en apprenant les sons : '''SO-CA-TO, H-H-A''' (HHA étant les dénominateurs : '''S'''in ='''O'''pp /''H''yp , '''C'''os = '''A'''dj/''H''yp, '''T'''an='''O'''pp/''A''dj)
<br />
D'autres méthodes consistent à associer un "mot" facile à retenir à chacune des trois définitions:<br />
- Cosadi - Sinopi - Tanopad <br />
- cosadjip - sinopip - tangopaj <br />
- CAHier - SOHo - TOAst (ou COCA)
==== Théorèmes ====
* « sin (a+b) = sin a cos b + cos a sin b » devient « ''sico cosi'' »
* « cos (a+b) = cos a cos b - sin a sin b » devient « ''coco sisi'' » (ou « ''coco MOINS sisi'' ou « ''coco ISsi'' » pour retenir le signe)
* À noter que la formule « ''sico cosi / coco moins sisi'' » ou « ''Coco si méchant, si, Coco, si'' » permet également d’apprendre les formules de factorisation suivantes :
sin p + sin q = 2 sin [(p+q)/2] •cos [(p-q)/2]
sin p - sin q = 2 cos [(p+q)/2] •sin [(p-q)/2]
cos p + cos q = 2 cos [(p+q)/2] •cos [(p-q)/2]
cos p - cos q = -2 sin [(p+q)/2] •sin [(p-q)/2]
Avec p = A + B et q = A - B
'''Cosinus est menteur et raciste''' ('''CO'''s comme '''CO'''n) en effet cos (a+b) donne (cos a cos b) - (sin a sin b). Cosinus est donc menteur puisque le signe de l’addition (positive) est négatif. Cosinus est raciste puisque on obtient (cos a cos b) d’une part et (sin a sin b) d’autre part : les cosinus et les sinus ne se mélangent pas.
'''co'''sinus est un '''co'''pain '''co'''n (copain pour le sens et con pour le signe): cos(a'''+'''b)= cos(a)cos(b) '''-''' sin(a)sin(b) : les cosinus restent ensemble, mais le signe change.
'''s'''inus est une '''s'''alade '''s'''ympa (salade pour le sens et sympa pour le signe): sin (a'''+'''b)=sin(a)cos(b) '''+''' sin(b)cos(a) : sin et cos se mélangent mais le signe reste le même
On trouve également : '''opip adjip opadj''' : sinus ('''op'''posé sur '''hyp'''oténuse), cosinus ('''adj'''acent sur '''hyp'''oténuse), tangente ('''op'''posé sur '''adj'''acent). La phrase prononcée rapidement d’un seul coup est très facile à mémoriser.
De même que '''SOH CAH TOA''': '''S'''inus= '''O'''pposé sur '''H'''ypoténuse '''C'''osinus= '''A'''djacent sur '''H'''ypoténuse '''T'''angente= '''O'''pposé sur '''A'''djacent
On peut lui substituer la formule plus percutante : '''CAH SOH TOA''' (à prononcer Casse toi ! )
=== Dates et constantes ===
Le [[w:code chiffres-sons|code chiffres-sons]] est une méthode qui permet de se souvenir de dates ou de valeurs numériques en formant des phrases.
=== Formules de géométrie ===
* Circonférence d’un cercle : 2 pi R (2 pierres)
** La circonférence est toute fière d’être égale à 2 pi R
* Aire d’un disque: pi R<sup>2</sup> (« pierre carrée » ou « pierre deux »)
** Le cercle est tout joyeux d’être égal à pi R<sup>2</sup> (prononcer « pi R deux »)
* « Le volume de la sphère, est quoi qu’on y puisse faire, 4/3 pi R<sup>3</sup>, fut-elle de bois. » ([[w:fr:Marcel Pagnol|Marcel Pagnol]])
Le volume d'une sphère, qu'elle soit de pierre, qu'elle soit de bois est égal aux 4/3 de pi R3
* Le volume d'une pizza (d'un camembert, ou de n'importe quel objet semblable) de rayon 'z' et de hauteur 'a' est égale à '''Pi.(z.z).a''' (la formule correspond à son nom)
soit V = π.z<sup>2</sup>.a
=== Analyse vectorielle ===
[[File:DRG chart fr.svg|thumb|right|300px|Diagramme des principales relations entre opérateurs de calcul vectoriel.]]
* Opérateurs s'annulant: '''DiR'''i'''G'''é (décrivant les flèches centrales sur le diagramme à droite)
** '''DiR'''i: <math>\mathrm{div}(\overrightarrow{\mathrm{rot}})=0</math>
** '''R'''i'''G''': <math>\overrightarrow{\mathrm{rot}}(\overrightarrow{\mathrm{grad}})=\vec{0}</math>
* Autres formules (flèches reliant div et grad sur le diagramme à droite):
** <math>\Delta = \mathrm{div}(\overrightarrow{\mathrm{grad}})</math>
** <math>\overrightarrow{\mathrm{grad}}(\mathrm{div})= \overrightarrow{\mathrm{rot}}(\overrightarrow{\mathrm{rot}})+\vec{\Delta}</math>
=== Ordre des opérations ===
En algèbre, les opérations simples : <code>(</code> <code>)</code>, <code>+</code>, <code>-</code>, <code>×</code> et <code>÷</code>, sont évaluées selon un certain ordre : '''PEMDAS''' pour « '''p'''arenthèses, '''e'''xposant,
'''m'''ultiplication, '''d'''ivision, '''a'''ddition et '''s'''oustraction ». Pour plus de détails sur l'application de ce mnémonique, voir [[:w:fr:Ordre des opérations|Ordre des opérations]].
=== Double distributivité ===
Retenir le mot « '''PIED''' » qui donne les termes à regrouper lorsque l’on développe : '''P'''remiers, '''I'''ntérieurs, '''E'''xtérieurs, '''D'''erniers.
=== Constante e<ref name="villemin.gerard">http://villemin.gerard.free.fr/Wwwgvmm/MnemoTe/Phrase.htm</ref> ===
{| border="0" cellpadding="0" cellspacing="1"
|align="center"|Tu
|
|align="center"|aideras
|
|align="center"|à
|
|align="center"|rappeler
|
|align="center"|ta
|
|align="center"|quantité
|
|align="center"|à
|
|align="center"|beaucoup
|
|align="center"|de
|
|align="center"|docteurs
|
|align="center"|amis.
|-
|align="center"|2
| ,
|align="center"|7
|
|align="center"|1
|
|align="center"|8
|
| align="center" |2
|
|align="center"|8
|
|align="center"|1
|
|align="center"|8
|
|align="center"|2
|
|align="center"|8
|
|align="center"|4
|}
=== Nombre d'or<ref name="villemin.gerard"/> ===
{| border="0" cellpadding="0" cellspacing="1"
|align="center"|Ô
|
|align="center"|nombre
|
|align="center"|d'
|
|align="center"|élégance
|
|align="center"|!
|
|align="center"|Toi,
|
|align="center"|toi,
|
|align="center"|grandiose,
|
|align="center"|étonnant :
|
|align="center"|''le nombre d'or''.
|-
|align="center"|1
| ,
|align="center"|6
|
|align="center"|1
|
|align="center"|8
|
|align="center"|0
|
|align="center"|3
|
|align="center"|3
|
|align="center"|9
|
|align="center"|8
|
|align="center"|
|}
(! pour 0)
=== Statistiques ===
* [[w:Règle 68-95-99.7|Règle 68-95-99.7]] : la proportion des échantillons entre [-σ, +σ], [-2σ, +2σ], [-3σ, +3σ] pour une distribution gaussienne centrée.
* Erreurs de première espèce et deuxième espèce.
** Se rappeler la fable d’Ésope dans laquelle un enfant [[wikt:crier au loup|crie au loup]] (hypothèse nulle <math>H_0</math>: « il n'y a aucun loup »).
**# D'abord, les villageois pensent qu'il y a un loup alors qu'il n'y en a aucun (erreur de première espèce).
**# Puis, les villageois pensent qu'il n'y a aucun loup alors qu'il y en a un (erreur de seconde espèce).
** Il y a une barre dans '''P'''ositif (faux positif : erreur de type '''I''') et deux barres dans '''N'''égatif (faux négatif : erreur de type '''II''').
== Sciences ==
=== Astronomie ===
==== Ordre des planètes du Système solaire ====
Il existe toute une série de termes mnémotechniques pour se souvenir de l'ordre des planètes à l’intérieur du [[w:système solaire|Système solaire]]. La première lettre de chaque mot de cette phrase correspond à la première lettre de chaque [[w:planète|planète]], de la plus rapprochée à la plus éloignée du Soleil. L'[[w:apostrophe|apostrophe]] ou la [[w:virgule|virgule]] peut représenter la [[w:ceinture d'astéroïdes|ceinture d'astéroïdes]] entre [[w:Mars (planète)|Mars]] et [[w:Jupiter|Jupiter]].
Voici l’ordre des planètes du Système solaire :
Mercure, Vénus, Terre, Mars, Jupiter, Saturne, Uranus, Neptune.
''À NOTER que selon la [[w:Définition des planètes de l'UAI|nouvelle définition]] de l’[[w:Union astronomique internationale|Union astronomique internationale]] d’août [[w:2006|2006]], [[w:Pluton (planète naine)|Pluton]] n’est plus considérée comme une [[w:planète|planète]] mais comme une planète naine ([[w:(134340) Pluton|(134340) Pluton]])'' (de même que [[w:(1) Cérès|(1) Cérès]], [[w:(136199) Éris|(136199) Éris]], [[w:(136108) Haumea|(136108) Haumea]] et [[w:(136472) Makemake|(136472) Makemake]]), <br/>
'''''Ordre des planètes du Système solaire : '''Mercure, Vénus, Terre, Mars, Jupiter, Saturne, Uranus, Neptune.''
On emploie par exemple les phrases suivantes :
*'''''M'''ême '''V'''ieux '''T'''ruc '''M'''ais '''J''''en '''S'''ais '''U'''n '''N'''ouveau.''
*'''''M'''a '''V'''ieille '''T'''ante '''M'''arie a '''J'''eté '''S'''amedi '''U'''n '''N'''avet.''
*'''''M'''a '''V'''ieille '''T'''rompette '''M'''e '''J'''oue '''S'''on '''U'''ltime '''N'''octurne.''
*'''''M'''a '''V'''oiture '''T'''e '''M'''ène '''J'''oyeusement '''S'''ur '''U'''ne '''N'''ationale.''
*'''''M'''arie, '''V'''iendras-'''T'''u '''M'''anger '''J'''eudi '''S'''ur '''U'''ne '''N'''appe ?''
*'''''M'''angez '''V'''os '''T'''artes, '''M'''ais '''J'''uste '''S'''ur '''U'''ne '''N'''appe .''
*'''''M'''e '''V'''oici '''T'''oute '''M'''ignonne''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ébuleuse.''
*'''''M'''e '''V'''oici '''T'''oute '''M'''odifiée''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ouveauté.''
*'''''M'''e '''V'''oilà '''T'''out '''M'''ouillé''',''' '''J''''ai '''S'''uivi '''U'''n '''N'''uage.''
*'''''M'''e '''V'''oilà '''T'''oute '''M'''ouillée''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ymphomane.''
*'''''M'''on '''V'''ieux, '''T'''u '''M''''as '''J'''eté '''S'''ur '''U'''ne '''N'''avette.''
*'''''M'''on '''V'''ioloncelle '''T'''ombe, '''M'''ais '''J'''e '''S'''auve '''U'''ne '''N'''ote.
*'''''M'''aman '''V'''ole '''T'''ous '''M'''es '''J'''ouets, '''S'''auf '''U'''n '''N'''ounours !''
*'''''M'''e '''V'''oici, '''T'''onton '''M'''arcel, '''J'''e '''S'''uis '''U'''n '''N'''ageur.''
*'''''M'''a '''V'''ille '''T'''hionville '''M'''ontre '''J'''oyeusement '''S'''on '''U'''nivers '''N'''octurne.''
*'''''M'''on '''V'''élo '''T'''e '''M'''ènera ''' J'''usque '''S'''ur '''U'''n '''N'''uage.''
*'''''M'''on '''V'''élo '''T'''ourne '''M'''al, ''' J''''en '''S'''ouhaite '''U'''n '''N'''ouveau.''
*'''''M'''onsieur, '''V'''ous '''T'''ravaillez '''M'''al ; - ''' J'''e '''S'''uis '''U'''n '''N'''ovice.''
*'''''M'''al '''V'''êtu '''T'''oi '''M'''ême, '''J'''e '''S'''uis '''U'''n '''N'''udiste''
*'''''M'''on '''V'''ieux '''T'''outou '''M'''édor '''J'''oue '''S'''ur '''U'''n '''N'''uage''
*'''''M'''a '''V'''erge '''T'''e '''M'''ènera '''J'''usque '''S'''ur '''U'''n '''N'''uage''
*'''''M'''ais '''V'''ous '''T'''ombez '''M'''al, '''J''''ai '''S'''auté '''U'''ne '''N'''aine''
* '''''M'''essieurs!''' V'''otre '''T'''rahison '''M'''<nowiki/><nowiki>'écœure: </nowiki>'''J'''ouer''' S'''ur '''U'''ne '''N'''omenclature!'' (au sujet de la disparition de Pluton de la liste)
*'''''S'''ors (pour Soleil) -'''M'''oi '''V'''ite '''T'''a '''M'''armite '''J'''aune '''S'''ur '''U'''ne '''N'''appe.''
*'''''M'''arquez '''V'''otre '''T'''emps '''M'''esuré '''J'''uste '''S'''ous '''U'''ne '''N'''anoseconde.''
'''Et pour les nostalgiques de Pluton...'''
* '''''M'''anon '''V'''iendras '''T'''u '''M'''anger '''J'''eudi '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.''
*'''''M'''ercure '''V'''eut '''T'''aquiner '''M'''ars, '''J'''e '''S'''uis '''U'''ne '''N'''ouvelle '''P'''lanète.''
*'''''M'''ademoiselle, '''V'''ous '''T'''ravaillez '''M'''al, '''J'''e '''S'''uis '''U'''n '''N'''ouveau '''P'''rofesseur''
*''Le '''M'''onde '''V'''oit '''T'''ourner du '''M'''atin '''J'''usqu'au '''S'''oir '''U'''niquement '''N'''euf '''P'''lanètes''.
*'''''M'''on '''V'''ieux, '''T'''u '''M'''e '''J'''ettes '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.''
*'''''M'''onsieur '''V'''euillez '''T'''ournez '''M'''a '''J'''upe '''S'''ans '''U'''ne '''N'''aïve '''P'''udeur.''
*'''''M'''e '''V'''oici, '''T'''oute '''M'''ignonne, '''J'''e '''S'''uis '''U'''ne '''N'''ouvelle '''P'''lanète.
*'''''M'''e '''V'''oici, '''T'''onton '''M'''arcel, '''J'''e '''S'''uis '''U'''n '''N'''ageur '''P'''rofessionnel.
*'''''M'''e '''V'''oici, '''T'''out '''M'''ouillé, '''J'''e '''S'''uis '''U'''n '''N'''ageur '''P'''ressé.
*'''''M'''e '''V'''oici, '''T'''out '''M'''ouillé, '''J''''ai '''S'''uivi '''U'''n '''N'''uage '''P'''luvieux.
*'''''M'''ais '''V'''iendras-'''T'''u '''M'''anger, '''J'''ulie, '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.
*'''''M'''on '''V'''ieux '''T'''héâtre '''M'''e '''J'''oue '''S'''ouvent '''U'''ne '''N'''ouvelle '''P'''ièce.
*'''''M'''on '''V'''ieux, '''T'''u '''M''''as '''J'''eté '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.
*'''''S'''ors (pour Soleil) -'''M'''oi '''V'''ite '''T'''a '''M'''armite '''J'''aune '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.''
*'''''M'''onsieur '''V'''ous '''T'''irez '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''ovice '''P'''itoyable.
* '''''Mé'''lanie, '''V'''ous '''T'''ombez '''Ma'''l, '''J'''e '''S'''uis '''U'''n '''N'''avet '''P'''ourri.''
*'''''M'''on '''V'''aisseau '''T'''e '''M'''ènera '''J'''eudi '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.
*'''''M'''a '''V'''ieille '''T'''ante '''M'''arge '''J'''oue '''S'''ur '''U'''n '''N'''ouveau '''P'''iano.
*'''''M'''aman '''V'''ole '''T'''ous '''M'''es '''J'''ouets, '''S'''auf '''U'''n '''N'''ounours '''P'''ourri !''
*'''''M'''arin '''V'''aleureux, '''T'''u '''M'''ourras '''J'''eune '''S'''ur '''U'''n '''N'''avire '''P'''erdu !''
*'''''M'''on '''V'''élo '''T'''ourne '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''ouveau '''P'''iéton.
*'''''M'''erci '''V'''ous '''T'''ous '''M'''aintenant '''J'''e '''S'''ais '''U'''nir '''N'''euf '''P'''lanètes.
*'''''M'''élanie '''V'''ous '''T'''ombez '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''avet '''P'''ourri.
*'''''M'''es '''V'''ieilles '''T'''antes '''M'''angeaient '''J'''adis '''S'''ur '''U'''ne '''N'''appe '''P'''ercée.''
*'''''M'''ets '''V'''ite '''T'''on '''M'''aillot '''J'''e '''S'''uis '''U'''n '''N'''udiste '''P'''oilu.
* '''''M'''on '''V'''ieux '''T'''acot '''M'''´a '''J'''eté '''S'''ur '''U'''n '''N'''oble '''P'''''assant.
*'''''M'''ange '''V'''ite '''T'''on '''M'''ars '''J''' 'en '''S'''ors '''U'''n '''N'''ouveau '''P'''aquet.
*'''''MÈR'''E, '''V'''iens '''Ter'''miner '''M'''a '''JUP'''e, '''SA''' cout'''UR'''e '''NE''' tient '''PLU'''s.''
* '''''M'''aman, '''V'''oudrais-'''T'''u '''M''''emmener '''J'''ouer '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète ?''
Suite à un concours qui s’est déroulé au Québec, la formule suivante a été retenue :
*'''''M'''angez '''V'''os '''T'''artes, '''M'''ais '''J'''uste '''S'''ur '''U'''ne '''N'''appe !''
Il existe aussi cette formule (la plus ancienne mnémonique connue en astronomie) qui se retient facilement, grâce à ses trois mots de trois syllabes :
*''Merveter, Marjusa, Uneplu''
('''Mer'''cure, '''Vé'''nus, '''Ter'''re, '''Mar'''s, '''Ju'''piter, '''Sa'''turne, '''U'''ranus, '''Nep'''tune, '''Plu'''ton)
Une variante<ref>Formule tirée de l’un des tomes du [https://fr.m.wikipedia.org/wiki/Manuel_des_Castors_Juniors ''Manuel des Castors Juniors'']</ref> de celle-ci :
* ''Mervé'', ''Termaju'', ''Saturneplu''
('''Mer'''cure, '''Vé'''nus, '''Ter'''re, '''Ma'''rs, '''Ju'''piter, '''Sat'''urne, '''Ur'''anus, '''Ne'''ptune, '''Plu'''ton)
Qui existe aussi sous cette forme :
* ''Mervé, Termaju, Satur n'est plus''
Et celle-ci qui inclut le Soleil :
*'''''S'''alut ! '''Me''' '''v'''ois-'''t'''u ? '''M'''oi '''j'''e '''s'''uis '''u'''ne '''n'''ouvelle '''pl'''anète !''
'''Planète ayant un système d'anneaux'''
* '''J'''e '''S'''uis '''U'''ne '''N'''ouille (Jupiter, Saturne, Uranus, Neptune)
==== Ordre des quatre lunes principales de Jupiter ====
'''I'''l '''e'''st '''g'''rand, '''C'''harles !
* [[w:Io|Io]], [[w:Europe|Europe]], [[w:Ganymède_(lune)|Ganymède]], [[w:Callisto_(lune)|Callisto]]
==== Croissant de Lune ====
Le '''p'''remier croissant et le '''d'''ernier croissant peuvent être reconnus en les assimilant aux sens du p et du d. En effet, en « ajoutant » au croissant de lune un bâton, on obtient un p ou un d selon le croissant. Cette méthode marche uniquement dans l'[[w:Hémisphère (géographie)|hémisphère]] [[w:nord|nord]], dans l’hémisphère sud il faudra considérer que la Lune ment.
Une méthode plus simpliste consistait autrefois à lire le croissant de lune directement. Quand il formait un '''C''' la lune incitait à penser qu'elle était '''C'''roissante . Or dans ce cas là elle est décroissante. Et quand elle formait un '''D''' (en supposant l’ajout de la barre droite nécessaire) elle incitait à penser qu’elle était '''D'''écroissante. Or dans ce cas là elle est croissante. Il en est venu l’expression populaire : ''Il est menteur comme la lune''. Cependant, dans ce cas la Lune ne ment que dans l'hémisphère Nord : C correspond bien à la Lune croissante et D à la Lune décroissante.
Ces méthodes ne sont plus valables autour de l’[[w:Équateur (ligne équinoxiale)|équateur]], ou le sens de ''lecture'' varie selon les saisons.
==== [[w:type spectral|Types spectraux]] [[w:étoile|stellaires]] ====
Les différents [[w:type spectral|types spectraux]], du plus chaud au plus froid, sont : O, B, A, F, G, K, M.
'''''O'''h, '''b'''e '''a''' '''f'''ine '''g'''irl/'''g'''uy, '''k'''iss '''m'''e !''
'''''O'''verseas '''b'''roadcast: '''a''' '''f'''lash! [[w:Godzilla|'''G'''odzilla]] '''k'''ills [[w:Mothra|'''M'''othra]] !''
=== Physique ===
==== Électromagnétique ====
Énergie électrique stockée dans un condensateur :
<math>E= (1/2) C U^2</math>
"l'''e''' '''demi''' '''cu'''l '''carré'''"
==== Les sept unités fondamentales====
Pour: ''seconde, ampère, candela, kilogramme, mètre, kelvin, mole '':
<br> Sac km km
<br> Je <u>'''s'''ais q</u>u<u>'''a'''n</u>d <u>'''c'''a</u>c<u>'''k'''i</u> <u>'''m'''et</u> <u>'''k'''el</u> <u>'''m'''o</u>t ! : (je) ''Sec-Am-Ca-Ki-Mè-Kel-Mo''
<br> <u>Ce con</u> d'<U>Ampère</U>, <u>qu'en d</u>it <u>qui l'au</u>ra!, <u>mettr</u>a <u>quel vin</u> au <u>môl</u>e ?: ''Sec<s>onde</s>'' d' ''ampère'', ''cand<s>ela</s>'' ''kilo<s>gramme</s>''ra, ''mètr<s>e</s>''a ''kelvin'' au ''moles''
"Secondes molles, quand des lacs-îlots [[wikt:grammer|grammant]] [[wikt:pairer|pairent]], Maître Kelvin !" (qui se prononce comme : "seconde, mole, candela, kilogramme, ampère, mètre, kelvin")
*
==== Ordre des couleurs de [[w:Résistance (composant)#Repérage et valeurs normalisées|résistance électrique]] ====
('''N'''oir, '''M'''arron, '''R'''ouge, '''O'''range, '''J'''aune, '''V'''ert, '''B'''leu, '''Vio'''let, '''G'''ris, '''B'''lanc)
'''''N'''e '''M'''ange '''R'''ien '''O'''u '''J'''e '''V'''ais '''B'''leuir '''V'''iolemment (ton) '''G'''ros '''B'''laze.''
'''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''rule '''V'''otre '''G'''rosse '''B'''arbe.''
'''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''eûnez, '''V'''oilà '''B'''ien '''V'''otre '''G'''rande '''B'''êtise.''
'''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''rise '''V'''otre '''G'''rosse '''B'''outeille.''
'''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''ats '''V'''iolemment '''G'''ros '''B'''êta
'''''N'''adine '''M'''e '''R'''épondit''' O'''ui, '''J'''e '''V'''eux '''B'''ien '''V'''otre '''G'''rosse '''B'''iroute''
'''''N'''oir, '''M'''arron, les couleurs de l'arc en ciel (sauf l'indigo), '''G'''ris, '''B'''lanc.''
Version québécoise utilisant la lettre '''B''' pour Brun au lieu de Marron :
'''''N'''otre '''B'''ar '''R'''estera '''O'''uvert '''J'''eudi et '''V'''endredi. '''B'''ière et '''V'''in '''G'''ratuit, '''B'''ienvenue.''
'''''N'''otre '''B'''ar '''R'''estera '''O'''uvert '''J'''eudi et '''V'''endredi. '''B'''ien'''V'''enue '''G'''ros '''B'''uveur.''
==== Ordre des couleurs du [[w:Couleur#Le spectre lumineux|spectre visible]] ====
Les sept couleurs du spectre visible ou de l'arc-en-ciel
(dans l'ordre des fréquences croissantes : '''R'''ouge - '''O'''range - '''J'''aune - '''V'''ert - '''B'''leu - '''I'''ndigo - '''V'''iolet)
peuvent se retenir grâce à la phrase suivante :
La '''ROU'''sse '''OR'''ienta le '''J'''uge '''VER'''s le '''BL'''azer de l''''INDI'''enne '''VIOL'''ée.
Dans l'ordre inverse (soit de la plus petite à la la plus grande longueur d'onde) elles peuvent se retenir grâce au mot '''VIBUJOR''', en remplaçant le '''U''' par un '''V''' ('''vert''' comme '''Hu'''lk).
'''V'''iolet - '''I'''ndigo - '''B'''leu - '''V'''ert - '''J'''aune - '''O'''range - '''R'''ouge
Remarque : en se figurant le drapeau français '''bleu'''-'''blanc'''-'''rouge''', on peut retrouver l’ordre des longueurs d’onde, en assimilant le bleu à l’ultraviolet, le blanc au visible, et le rouge à l’infra-rouge : '''ultraviolet'''-'''visible'''-'''infra-rouge'''.
==== Longueur d'onde des couleurs ====
Le mot rouge est plus long que le mot bleu (5 vs 4), sa longueur d'onde est plus longue également.
==== Couleurs en peinture et rayonnements lumineux ====
Les peintres utilisent les trois couleurs fondamentales '''Cyan Magenta et Jaune''', chacune absorbant une seule des trois couleurs fondamentales de la lumière (Rouge Vert et Bleu). Notre œil ne reconnaît la couleur que par la lumière identifiée par chacune des 3 familles de cônes de l'œil respectivement sensibles aux rayonnements '''Rouge Vert et Bleu'''.
Cette phrase permet aux peintres et aux physiciens d'identifier un équivalent des deux couleurs de rayonnement lumineux identifiées par les cônes de l'œil pour chacune des couleurs fondamentales de la peinture.
'''C'''ette '''B'''onne '''V'''ieille<br />
'''M'''ijote des '''R'''aviolis "'''B'''uitoni"<br />
'''<nowiki>J'</nowiki>'''en '''R'''é'''V'''ais
La couleur '''C'''yan de la peinture correspond ainsi à la réception des rayonnements lumineux '''B'''leu et '''V'''ert.
Le '''M'''agenta, pour sa part correspond aux rayonnements lumineux '''R'''ouge et '''B'''leu.
Quant à la couleur '''J'''aune, elle renvoie vers l'œil les rayonnements lumineux '''R'''ouge et '''V'''ert.
==== Constantes ====
* vitesse de la lumière<ref name="villemin.gerard" /> :
:{| style="text-align: center;"
|Ah,||messagère||admirable,||lumière||éclatante,||je||sais||votre||célérité||
|-
|La||constante||lumineuse||restera||désormais||là||dans||votre||cervelle||
|-
|2||9||9||7||9||2||4||5||8||m/s
|}
* définition formelle d'une seconde (périodes de la radiation correspondant à une transition entre les deux sous-niveaux hyperfins du césium 133) :
:{| style="text-align: center;"
|« Pharaonne,||j'||affirmais||là,||honore||mal||l'||aimable||seconde||0 ! »
|-
|9||1||9||2||6||3||1||7||7||0
|}
=== Chimie ===
'''Priorité des groupes caractéristiques en nomenclature'''
Pour nommer une molécule composée de plusieurs groupes caractéristiques, on utilise l'ordre suivant :
Acide carboxylique - anhydride d'acide - ester - halogénure d'acyle - amide - nitrile - aldéhyde - cétone - alcool - amine - alcyne - alcène - éther-oxyde - dérivé halogéné - alcane
Pour se rappeler de l'ordre :
'''Ac'''e '''an'''nule '''Ester''' ! '''Halo ami'''? '''Ni'''e '''al'''ors '''Cé'''cile '''alcool'''isée, '''Am'''élie '''ascene''' à N'''eoxi''' et '''De'''lp'''h'''ine un '''alcane'''
==== Radicaux alkyles ====
Pour se rappeler l’ordre des 3 premiers groupement alcanes :
* Il ai'''mait''' '''êt'''re '''pro'''pre. (Oralement, "Il ai'''Mét Éth Prop''' ")
Pour se rappeler l’ordre des 4 premiers groupement alcanes :
* ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane)
* '''M'''aman '''Et''' '''P'''apa '''B'''ébé
* '''M'''aman '''Et''' '''P'''apa '''But'''inent.
* '''M'''alin qui '''É'''tudie '''P'''our le '''B'''ac.
* '''M'''ieux '''É'''tudier '''P'''our le '''B'''ac.
* '''M'''on '''É'''cole '''P'''eut '''B'''rûler.
* '''M'''on '''É'''lève '''P'''isse '''B'''ien
* '''M'''organe '''E'''st '''P'''as '''B'''elle.
* '''Me'''s '''é'''lèves '''p'''arlent '''b'''eaucoup.
* '''M'''ets '''t'''es '''Prop'''res '''But'''s ! (Oralement, "Mét Éth Prop But")
Pour se rappeler l’ordre des 5 premiers groupement alcanes :
* ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane, '''P'''entane)
* '''M'''aman '''E'''st '''P'''artie '''B'''ébé '''P'''leure
* '''M'''amie '''E'''st '''P'''artie '''B'''oire une '''P'''inte
Pour se rappeler l’ordre des 6 premiers groupement alcanes :
* ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane, '''P'''entane, '''H'''exane)
* '''M'''aurice '''E'''st '''P'''as '''B'''eau '''P'''our '''H'''élène.
* '''M'''amie '''E'''t '''P'''api '''B'''atifolent '''P'''endant l''''H'''iver.
* '''M'''aman '''E'''t '''P'''apa '''B'''oivent '''P'''endant '''H'''alloween
* '''M'''amie '''E'''st '''P'''artie '''B'''oire une '''P'''inte de '''H'''eineken
* '''M'''et '''E'''th '''P'''rop '''B'''ut '''P'''ent '''H'''ex
Pour les plus vulgaires :
* '''M'''audite '''É'''paisse ! '''P'''ourquoi '''B'''aiser '''P'''our l' '''H'''iver !
==== [[w:Tableau périodique des éléments|Tableau périodique des éléments]] ====
Il est à noter que la plupart des moyens mnémotechniques concernant les éléments ont été créés par des [[w:étudiant|étudiant]]s, d’où le [[w:vocabulaire|vocabulaire]] parfois amusant des maximes.
===== [[w:Éléments de la période 2|Période 2]] =====
'''Pour : Li'''thium, '''Bé'''ryllium, '''B'''ore, '''C'''arbone, '''N'''itrogène (Azote), '''O'''xygène, '''F'''luor, '''Né'''on.''
* «La '''Li''''''Bé'''llule '''B'''leue, d’une '''C'''aresse, '''N'''oit dans l’'''O'''nde la '''F'''leur de '''Né'''nuphare. »
* « '''Li'''thus et '''Be'''rénice '''B'''oivent, '''C'''haque '''N'''uit, '''O''' '''F'''rais de '''Né'''ron »
* « '''Li'''verpool, '''Be'''rceau des '''B'''eatles, '''C'''onnait '''N'''aturellement ces '''O'''librius '''F'''ous et '''Né'''vrosés »
* « '''Li'''bérez '''Be'''n '''B'''arkans, '''C'''élèbre '''N'''arrateur, '''O'''u '''F'''usillez '''Né'''ron »
* « '''Li''' '''Be''' le '''B'''on '''C'''anard du '''N'''ord '''O'''uest de la '''F'''rance '''Ne'''ogauchiste »
* « '''Li'''li '''Be'''sa '''B'''ien '''C'''ouchée '''N'''ue '''O''' '''F'''lanc de '''Né'''ron »
* « '''Li'''li '''Be'''se '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''umeur de '''Ne'''squik »
* « '''Li'''li '''Be'''se '''B'''ien '''C'''onfortablement '''N'''otre '''O'''ncle '''F'''rançois '''Ne'''stor »
* « '''Li'''mace '''Be'''te '''B'''ouffa '''C'''inq '''N'''ouveaux '''O'''ignons '''F'''raîchement '''Né'''s »
* « '''Li'''li '''Be'''rça '''B'''ébé '''C'''hez '''N'''otre '''O'''ncle '''F'''ernand '''N'''estor
* « '''Li'''li '''B'''ecta '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''erdinand '''N'''estor »
* « '''Li'''li '''Bé'''cha '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''rançois-(ou '''F'''erdinand-)'''Ne'''stor »<br />
* « '''Li'''li '''Bé'''se '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''rançois-(ou '''F'''erdinand-)'''Ne'''stor »<br />
* « '''LiBe'''rté '''B'''afouée '''C'''ontre '''N'''otre '''O'''rganisation '''F'''édérale '''Né'''ogaulliste (ou '''Né'''otrotskiste) »
* « le '''Li''' t de '''BE''' '''B''' é a '''C'''assé le '''N'''ez de l' '''O'''ncle '''F'''urieux '''Né'''on »
* « '''Li'''vrez '''Bê'''tement '''B'''ataille '''C'''ar '''N'''ous, '''O'''fficiers '''F'''rançais, '''Né'''gocions »
* « '''L'i'''magination '''Be'''lliqueuse '''B'''aissa '''C'''ar '''N'''otre '''O'''rdre '''F'''ut '''Ne'''t »
* « '''Li'''re '''Be'''aucoup '''B'''alzac '''C'''ar '''N'''otre '''O'''rthographe '''F'''ait '''Né'''gligé »
* « '''Li'''ste de '''Be'''lles '''B'''outeilles de '''C'''ognac '''N'''ous '''O'''nt '''F'''outus '''Ne'''rveux »
* « '''Li'''li '''Be'''cote '''B'''ien '''C'''omme '''Ni'''cole '''O''' '''F'''ond '''N'e'''st ce pas »
* « '''Li'''bérez '''Be'''rnard '''B'''ossu '''C'''ontre '''N'''ouvel '''O'''tage '''F'''éminin. Signé '''Ne'''on »
* « '''LiBe'''rté de '''B'''oire '''C'''ar '''N'''ous '''O'''n '''F'''oire '''N'''os '''e'''xams »
* « '''Li'''bérez '''Be'''n '''B'''arka '''C'''ar '''N'''ous, '''O'''fficiers '''F'''rançais, '''Né'''gocions »
* « '''Li'''li et '''Be'''rnard '''B'''aisent '''C'''omme '''N'''ous '''O'''n '''F'''ait '''Ne'''spa »
===== [[w:Éléments de la période 3|Période 3]] =====
''Pour : '''S'''odium, '''M'''a'''g'''nésium, '''Al'''uminium, '''Si'''licium, '''P'''hosphore, '''S'''oufre, '''C'''h'''l'''ore, '''Ar'''gon.''
* « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''russiens '''S'''ans '''Cl'''aquer '''A'''p'''r'''ès. »
* « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''aquer des '''Ar'''ticulations. »
* « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets (ou '''P'''erdrix) '''S'''ans '''Cl'''aquer d' '''Ar'''gent. »
Le sodium est représenté par '''Na'''. Alors Napoléon remplace Suzanne pour retrouver le symbole :
* « '''Na'''poléon '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''aquer d''''Ar'''gent » — ou « sans claquer '''A'''p'''r'''ès », « d''''Ar'''gon », « d''''Ar'''tère », « (d')'''Ar'''tiche » ou « sans claquer les '''Ar'''ticulations » pour éviter la confusion avec l'élément argent, noté '''Ag'''.
* « '''Na'''poléon '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''russiens '''S'''ans '''Cl'''ore l’'''Ar'''mistice. »
* « '''Na'''guère '''M'''onsei'''g'''neur '''Al'''louche '''Si''' '''P'''ervers '''S'''uça '''Cl'''aire '''Ar'''demment. »
* « '''Na'''poléon '''M'''an'''g'''eait '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''amser '''A'''p'''r'''ès. »
* « '''Na'''poléon '''M'''a'''g'''nera '''À''' '''l'''<nowiki/>'est '''Si''' '''P'''ossible '''S'''a '''C'''o'''l'''onne '''A'''rmée. »
*« '''Na'''billa '''M'''an'''g'''e (h)'''Al'''lal '''Si''' '''P'''atrick '''S'''ébastien '''Cl'''ash '''Ar'''thur. »
===== [[w:Éléments de la période 4|Période 4]] =====
''Pour : '''K'''allium (Potassium), '''Ca'''lcium, '''Sc'''andium, '''Ti'''tane, '''V'''anadium, '''C'''h'''r'''ome, '''M'''a'''n'''ganèse, '''Fe'''r, '''Co'''balt, '''Ni'''ckel, '''Cu'''ivre, '''Z'''i'''n'''c, '''Ga'''llium, '''Ge'''rmanium, '''A'''r'''s'''enic, '''Sé'''lénium, '''Br'''ome, '''Kr'''ypton.''
* « '''K'''arl '''Ca'''pitaine '''Sc'''andinave '''Ti'''ra sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule, '''Fé'''conda le '''Co'''n de '''Ni'''cole, et le '''Cu'''l de ses '''Z'''e'''n'''nemies, '''Ga'''rdant '''Ge'''néreusement '''As'''sez de '''Se'''mence pour ce '''Br'''ave '''K'''h'''r'''ouchtchev. »
* « '''K'''évin '''Ca'''pture un '''Sc'''arabée '''Ti'''mide dans le '''V'''agin '''Cr'''éatif de '''M'''o'''n'''ique, '''Fé''' (fait) '''Co'''mme '''Ni'''cole dans le '''Cu'''l en '''Zinc''' de '''Ga'''spard de '''Ge'''rmanie, puis '''As'''pire '''Sé'''bastien dans la '''Br'''aguette du '''Kr'''aken »
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''iemment '''Ti'''to. '''V'''orochev '''Cr'''ia '''M'''ag'''n'''anime : "'''Fé''' pas le '''Co'''n '''Ni'''kita, ton '''Cu'''l en '''Z'''i'''n'''c '''Ga'''lvanisé te '''Gè'''ne '''AsSe'''z pour '''Br'''anler des '''Kr'''evettes. »
* « '''K'''épler '''Ca'''lculait des '''Sc'''alaires '''Ti'''tanesques, '''V'''oyant '''Cr'''o-'''M'''ag'''n'''on '''Fé'''sant le '''Co'''n '''Ni'''ché sur le '''Cu'''l d'un '''Z'''ébulo'''n''', '''Ga'''gnant '''Gé'''néralement '''AsSe'''z de '''B'''iè'''r'''es '''Kr'''onenbourg. »
* « '''K''' '''Ca''' (cacas) '''Sc'''iés de '''Ti'''ti '''V'''olant et '''Cr'''os '''M'''i'''n'''et qui '''F'''ont ('''Fe''') des '''Co'''nneries ont '''Ni'''qué le '''Cu'''l à '''Z'''a'''n'''zibar d'un '''Ga'''rs '''Gé'''nial '''As'''sis '''Se'''rrant une '''B'''onne ('''Br''') '''Kr'''o. »
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''andaleusement '''Ti'''tov. '''Van'''ia '''Cr'''ia '''M'''ag'''n'''animement "'''F'''ais pas le '''Co'''n Nikita, la Cuisine en '''Z'''i'''n'''c de la '''Ga'''re de '''Ge'''nève '''As''' Ses '''Br'''iques '''Cr'''euses » (ou '''K'''hrouchtchev '''Ca'''ssa le '''SC'''ooter à '''TI'''tov)
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''andaleusement la '''Ti'''gnasse de '''V'''anadia, '''Cr'''oyant '''M'''a'''n'''ifestement '''Fe'''re '''Co'''cu '''Ni'''colaiev, le '''Cu'''ré de '''Z'''a'''n'''zibar '''Ga'''gna '''Ge'''nève '''As'''sez '''Se'''crètement avec Son '''Br'''éviaire '''Kr'''ipté. »
* « '''K'''arl '''Ca'''valier '''Sc'''andinave '''Ti'''ra '''V'''engeance '''C'''r'''u'''elle '''M'''a'''n'''iant le '''Fe'''r '''Co'''ntre le '''Ni'''kel. Le '''Cu'''l de '''Z'''e'''n'''obie '''Ga'''rnit de '''Ge'''ranium '''As'''pire la '''Se'''ve '''Br'''ûlante du '''Kr'''atère (cratère/Krypton). »
* « '''K'''onrad '''Ca'''pitaine '''Sc'''andinave '''Ti'''ra sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule, '''Fe'''rmant le '''Co'''n de '''Ni'''cole, le '''Cu'''l de '''Zn'''obie, et '''Ga'''rdant '''Ge'''néreusement l''''As'''permique '''Se'''mence du '''Br'''ave '''K'''e'''r'''mit. »
* « '''K'''a'''Ca''' '''Sc'''quatte avec '''Ti'''ti la '''V'''oiture de '''Cr'''os '''M'''i'''n'''et. '''Fe'''rnand '''Co'''nduit sa mi'''Ni''' '''COOPER''' en '''Z'''i'''g'''za'''Ga'''nt, '''Ge'''néralement '''As'''sis en se '''Se'''rvant une '''B'''ière '''Kr'''onembourg. »
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''rupuleusement le '''Ti'''tanesque et '''V'''elue '''Cr'''ane du '''M'''o'''n'''de avec '''Fe'''rmeté. '''Co'''ntre l'ennemie, '''Ni'''kita '''C'''a'''u'''sa la '''Z'''iza'''n'''ie, il en'''Ga-Ge'''a l''''As'''sault '''S'''talinien. '''Br'''avo '''K'''h'''r'''ouchtchev. »
* « '''K'''af'''Ca''' (Kafka) '''Sc'''ruta, '''Ti'''mide, '''V'''era '''Cr'''uz '''M'''o'''n'''trant ses '''Fe'''sses, '''Co'''mme la '''Ni'''mphe (nymphe) '''Cu'''pide '''Z'''é'''n'''a. '''Ga'''llien et '''Ge'''rard, '''As'''ssis, '''Se''' '''Br'''assaient de la '''Kr'''onenbourg. »
* « '''K'''orrigan '''Ca'''pitaine '''Sc'''andinave '''Ti'''rant sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule ('''Mn''') '''Fe'''rma le '''Co'''n de '''Ni'''cole et le '''Cu'''l de '''Z'''é'''n'''obie ('''Zn''') '''Ga'''rdant '''Gé'''néreusement l’'''As'''permatique '''Se'''mence d’un '''Br'''un '''Kr'''omatique (chromatique).»
*« '''K'''évin '''Ca'''sse '''Sc'''iemment sa '''Ti'''relire et '''V'''ient '''Cr'''ier : "'''M'''ama'''n''', fait ('''Fe''') '''Co'''mme '''Ni'''cole, '''Cu'''isine !". '''Z'''hi'''n'''g lui dit : "dé'''GaGe''', '''AsSe'''z '''Br'''aillé '''Kr'''étin".
* « '''K'''arine '''Ca'''lcula que '''Sc'''ientifiquement '''Ti'''tillé, '''V'''incent '''Cr'''ame '''M'''o'''n''' '''F'''outr'''e''' '''Co'''mme '''Ni'''colas '''Cu'''pide '''Z'''i'''n'''zin '''Ga'''lleux '''Ge'''sticulant '''As'''ymétriquement et '''Se''' '''Br'''ulant à la '''Kr'''yptonite. »
* « '''K'''ptain '''Ca'''ca '''Sc'''andinave '''Ti'''re sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule (Mn) des '''Fe'''sses de '''Co'''rine '''Ni'''çoise, '''Cu'''ltivée et '''Z'''e'''n''' (Zn), '''Ga'''lamment '''Ge'''néreuse, '''As'''sez '''Se'''xy et '''Br'''anlant '''K'''a'''r'''im (Kr). »
* « '''K'''évin '''Ca'''tapulta '''Sc'''iemment '''Ti'''bère le '''V'''erreux, le '''Cr'''étin, le '''M'''écha'''n'''t, '''Fe'''roce, '''Co'''rrompu. Ha'''Ni'''bal, '''Cu'''i'''Z'''a'''n'''t, '''Ga'''ve '''Ge'''ntillement d''''As'''pirine '''Se'''c '''Br'''utus le '''Kr'''asseux. »
* « '''K'''arine '''Ca'''ressa '''S'''e'''c''' '''Ti'''mothée '''V'''ers sa '''Cr'''oupe '''M'''ais '''n'''e '''Fé'''(fait) '''Co'''uille '''Ni''' '''Cu'''l. '''Z'''ho'''n'''g '''Ga'''gna '''Ge'''ntiment '''A s'''e '''Br'''anler '''Kr'''asseusement. »
* « '''K'''oalas de '''Ca'''nberra, '''S'''’é'''c'''ria-'''T'''-'''i'''l, je '''V'''eux '''Cr'''oire '''M'''o'''n''' '''F'''r'''è'''re '''Co'''mplètement ! Ils '''Ni'''chent, '''C'''op'''u'''lent en '''Z'''o'''n'''ages '''Ga'''lamment '''Gé'''rés, '''As'''sistés '''Se'''ulement de '''Br'''ouillons '''K(r)'''yptés. (variante : de '''Br'''aves '''K'''angou'''r'''ous)»
===== [[w:Éléments de la période 5|Période 5]] =====
''Pour : '''R'''u'''b'''idium, '''S'''t'''r'''ontium, '''Y'''ttrium, '''Z'''i'''r'''conium, '''N'''io'''b'''ium, '''Mo'''lybdène, '''T'''e'''c'''hnétium, '''Ru'''thénium, '''Rh'''odium, '''P'''alla'''d'''ium, '''A'''r'''g'''ent, '''C'''a'''d'''mium, '''In'''dium, '''S'''ta'''n'''num (Étain), '''S'''ti'''b'''ium (Antimoine), '''Te'''llure, '''I'''ode, '''Xé'''non.''
* « '''R'''o'''b'''in '''S'''u'''r''' '''Y'''vette a le '''Z'''èb'''r'''e '''N'''o'''b'''le de '''Mo'''nsieur '''T'''u'''c''' '''Ru'''. '''R'''o'''h'''an '''P'''ru'''d'''emment '''Ag'''é '''C'''é'''d'''a '''In'''évitablement '''S'''a'''n'''s '''S'''u'''b'''ir '''Te'''s '''I'''dées '''Xé'''nophobes. » (variante : '''R'''o'''b'''in '''S'''o'''r'''t '''Y'''von, le '''Z'''èb'''r'''e '''N'''o'''b'''le, dans '''Mo'''n '''T'''a'''c'''ot '''R'''o'''u'''illé)
* «'''R'''o'''b'''ert '''S'''enio'''r''' est un '''Y'''éti du '''Z'''aï'''r'''e ou un '''N'''o'''b'''lio de '''Mo'''dène. Le '''T'''e'''c'''hnicien '''Ru'''dement bourré au '''Rh'''um '''P'''é'''d'''ale '''Ag'''ilement des '''C'''ou'''d'''es, il '''I'''nsulte '''S'''ai'''n'''t '''S'''e'''b'''astien, un '''Te'''rrien '''I'''diot et '''Xé'''nophobe. »
* « '''R'''u'''b'''y, '''S'''o'''r'''te de '''Y'''éti '''Z'''aï'''r'''ois qu’un '''N'''o'''b'''liau de '''Mo'''dène, '''T'''e'''c'''hniquement '''Ru'''iné, '''R'''ac'''h'''ète au '''P'''ala'''d'''in avec l’'''A'''r'''g'''ent des '''C'''a'''d'''eaux '''In'''digènes '''S'''a'''n'''s '''S'''u'''b'''ir '''Te'''s '''I'''res '''Xé'''nophobes. »
* « Le '''R'''a'''b'''bin '''S'''o'''r'''t son '''Y'''acht, le '''Z'''èb'''r'''e, pendant que '''N'''a'''b'''il, le '''Mo'''ldave '''T'''ur'''c''' '''Ru'''dement bourré au '''Rh'''um '''P'''é'''d'''ale '''Ag'''ilement des '''C'''ou'''d'''es, '''In'''diquant à '''S'''o'''n''' ami '''S'''é'''b'''astien la '''Te'''rre '''I'''mbibée de '''Xé'''rès. »
* « '''R'''o'''b'''in '''S'''′'''r'''approche du '''Y'''éti sur un '''Z'''èb'''r'''e, '''No'''nobstant le '''Mo'''rse '''Tc'''hèque en '''Ru'''t et le '''Rh'''inocéros '''P'''é'''d'''é '''Ag'''ressif et '''C'''an'''d'''ide ; '''In'''capable de '''Sn'''iffer du '''S'''a'''b'''le et de la '''Te'''rre en '''I'''mitant '''Xé'''na. »
* «'''R'''o'''b'''ert '''S'''enio'''r''', un '''Y'''éti du '''Z'''aï'''r'''e, '''N'''o'''b'''le et '''Mo'''rose, '''T'''ri'''c'''otait '''Ru'''e du '''Rh'''um et, '''P'''en'''d'''ant l′'''Ag'''enouillement du '''C'''i'''d''', '''In'''sulta '''Sn'''obes et '''Sb'''ires '''Te'''rrorisés à l′'''I'''rruption d′un '''Xé'''nophobe. »
===== [[w:Éléments de la période 6|Période 6]] =====
''Pour : '''C'''é'''s'''ium, '''Ba'''ryum, '''La'''nthane, '''H'''a'''f'''nium, '''Ta'''ntale, '''W'''olfram (Tungstène), '''R'''h'''é'''nium, '''Os'''mium, '''Ir'''idium, '''P'''la'''t'''ine, '''Au'''rum (Or), '''H'''ydrar'''g'''irum (Mercure), '''T'''ha'''l'''lium, '''P'''lom'''b''', '''Bi'''smuth, '''Po'''lonium, '''A'''s'''t'''ate, '''R'''ado'''n'''.'' (La ou Lu selon la classification)
* « '''C'''é'''s'''ar '''Ba'''lade '''La''' '''H'''i'''f'''i de '''Ta'''ta dans le '''W'''agon et '''Re'''garde '''Os'''si '''Ir'''ma. '''P'''e'''t'''er '''Au''' '''H'''an'''g'''ar, un '''T'''e'''l''' '''P'''ro'''b'''lème '''Bi'''en '''Po'''sé '''At'''tend '''R'''épo'''n'''se. » (variante : et '''Re'''garde '''Os'''ciller '''Ir'''ma)
* « '''C'''é'''s'''ar '''Ba'''isa '''La'''ngoureusement l''''H'''orri'''f'''iante '''Ta'''ntouse dans les '''W'''C ('''Ré'''pugnants), '''Ré'''pétant les '''Os'''cillations '''Ir'''resistibles du '''P'''é'''t'''ard d''''Au'''rélien ; Mercure('''Hg''') lui '''T'''ai'''l'''la dans le '''P'''lom'''b''' une '''Bi'''te '''Po'''ilue pour lui '''A'''s'''t'''iquer les '''R'''ei'''n'''s. »
* « '''C'''a'''s'''imir et '''Ba'''stien '''La'''ncent des '''H'''yper'''f'''réquences qui '''Ta'''pent sur un '''W'''agon '''Re'''mpli d''''Os''' '''Ir'''radiés, qui '''P'''é'''t'''a à l''''Au'''be '''H'''y'''g'''iénique, '''T'''é'''l'''éportant un '''P'''lom'''b'''ier '''Bi'''zarre '''Po'''lonais '''At'''taché à la '''R'''ei'''n'''e. »
===== [[w:Éléments de la période 7|Période 7]] =====
Pour : '''Fr'''ancium, '''Ra'''dium, '''Ac'''tinium, '''R'''uther'''f'''ordium, '''D'''u'''b'''nium, '''S'''eabor'''g'''ium, '''B'''o'''h'''rium, '''H'''a'''s'''sium, '''M'''ei'''t'''nérium, '''D'''arm'''s'''tadtium, '''R'''oent'''g'''enium, '''C'''oper'''n'''icium
* « Les '''Fr'''ancais '''Ra'''lent '''Ac'''tivement depuis que '''R'''a'''f'''farin a '''D'''ou'''b'''lé '''S'''é'''g'''olène, é'''B'''a'''h'''ie par son '''H'''i'''s'''toire '''M'''ue'''t'''te sur la '''D'''i'''s'''tribution '''R'''é'''g'''ionale de la '''C'''o'''n'''nerie. »
* « '''Fr'''anck '''Ra'''te '''Ac'''tuellement le '''R'''on'''f'''lement '''D'''é'''b'''ile du '''S'''i'''g'''nal '''B'''o'''h'''émien à l''''H'''i'''s'''toire '''M'''y'''t'''hique... »
===== [[w:Lanthanides|Lanthanides]] =====
''Pour : ('''La'''nthane), '''Cé'''rium, '''Pr'''aséodyme, '''N'''éo'''d'''yme, '''P'''ro'''m'''ethium, '''S'''a'''m'''arium, '''Eu'''ropium, '''G'''a'''d'''olinium, '''T'''er'''b'''ium, '''Dy'''sprosium, '''Ho'''lmium, '''Er'''bium, '''T'''hulliu'''m''', '''Y'''tter'''b'''ium, '''Lu'''técium''
* « '''Cé'''dric, '''Pr'''ophète '''N'''éan'''d'''ertalien, '''P'''ro'''m'''et la '''S'''a'''m'''ba. '''Eu'''gène, '''G'''ran'''d''' '''T'''rou'''b'''le '''Dy'''namique des '''Ho'''mmes '''Er'''rants, '''T'''o'''m'''be sur l'h'''Yb'''ride '''Lu'''ne. »
* « La '''Ce'''llule du '''Pr'''ofesseur est à '''N'''otre-'''d'''ame de '''P'''ana'''m'''e,'''S'''o'''m'''met de l' '''Eu'''rope, '''G'''ran'''d'''e et '''T'''rès''' b'''elle, où '''Dy'''onisos et '''Ho'''mère '''Er'''raient sans '''T'''u'''m'''ulte. '''Y'''a'''b'''on '''Lu'''tèce! »
* « '''Ce'''cile, qui '''Pr'''atiquait le '''N'''u'''d'''isme, '''P'''ro'''m'''ettait à la '''S'''a'''m'''aritaine '''Eu'''phorique un '''G'''o'''d'''emiché en '''T'''u'''b'''e de '''Dy'''namite, '''Ho'''chet '''Er'''otique et '''T'''u'''m'''éfiant, s'''Y'''m'''b'''ole de '''Lu'''xure. »
* « '''Ce'''sar se '''Pr'''omène avec '''N'''a'''d'''ine et '''P'''a'''m'''ina, en '''S'''e'''m'''ant les '''Eu'''nuques qui '''G'''ar'''d'''aient le '''T'''éné'''b'''reux '''Dy'''lan dans un cac'''Ho'''t car il '''Er'''rait près de la '''T'''o'''m'''be d''''Yb'''-'''Lu'''"
* « '''Ce''' '''P'''a'''r'''adis que '''N'''ous '''d'''onna ('''Nd''') '''P'''ro'''m'''éthée, '''S'''e'''m'''blable à l''''Eu'''rope, nous '''G'''ar'''d'''e des '''T'''erri'''b'''les '''Dy'''sputes à l''''Ho'''rizon. Nous '''Er'''igerons une '''T'''o'''m'''be et '''Y''' '''b'''annirons '''Lu'''cifer »
===== [[w:Actinides|Actinides]] =====
''Pour : ('''Ac'''tinium), '''Th'''orium, '''Pr'''otactinium, '''U'''ranium, '''N'''e'''p'''tunium, '''P'''l'''u'''tonium, '''Am'''ericium, '''C'''uriu'''m''', '''B'''er'''k'''élium, '''C'''ali'''f'''ornium, '''E'''in'''s'''teinium, '''F'''er'''m'''ium, '''M'''en'''d'''élénium, '''No'''bélium, '''L'''aw'''r'''encium.''
* « '''Th'''éo '''Pa'''rle '''U'''niversellement mais ''' N' '''ex'''p'''rime '''P'''l'''u'''s l''''Am'''ertume '''C'''o'''m'''mune. '''B'''roo'''k''' '''C'''on'''f'''ie l''''Es'''poir de '''F'''or'''m'''er un '''M'''on'''d'''e '''No'''uveau et '''L'''ib'''r'''e. »
* « L''''Ac'''tivation''' Th'''ermique des '''Pa'''tates à l''''U'''ranium du '''N'''é'''p'''al en '''Pu'''rée '''Am'''ène, '''C'''o'''m'''me à '''B'''ang'''k'''ok, le '''C'''on'''f'''ort '''Es'''thétique d'une '''F'''a'''m'''ine un '''M'''i'''d'''i de '''No'''ël au '''L'''ibé'''r'''ia. (ou en bord de '''Lw'''oire selon les classifications) »
* « '''Th'''or '''Pa'''rtit '''U'''ne '''N'''uit '''p'''our '''P'''l'''u'''ton, '''Am'''oureux de '''C'''a'''m'''ille. '''B'''er'''k'''eley, '''C'''ali'''f'''ornia '''Es'''perait '''F'''u'''m'''er '''M'''a '''d'''ouce et '''No'''ble '''L'''au'''r'''a.»
===== [[w:Alcalin|Alcalin]]s Groupe 1 =====
''Pour : ('''H'''ydrogène, non alcalin), '''Li'''thium, '''Na'''trium (Sodium), '''K'''allium (Potassium), '''R'''u'''b'''idium, '''C'''é'''s'''ium, '''Fr'''ancium.''
* « ('''H'''eureux) dans le '''Li'''t de '''Na'''tacha, [[w:Khrouchtchev|'''K'''hrouchtchev]] '''R'''a'''b'''aissait '''C'''on'''s'''tamment son '''Fr'''oc. »
* « '''L'''’'''i'''nter'''Na'''tionale '''K'''ommuniste '''R'''e'''b'''ute les '''C'''apitali'''s'''tes '''Fr'''ançais. »
* « '''Li'''li '''N’a''' '''K''' '''R'''e'''b'''outonner '''C'''e'''s''' '''Fr'''ocs ('''Fr'''usques). »
===== [[w:Alcalino-terreux|Alcalino-terreux]] Groupe 2 =====
''Pour : '''Bé'''ryllium, '''M'''a'''g'''nésium, '''Ca'''lcium, '''S'''t'''r'''ontium, '''Ba'''ryum, '''Ra'''dium.''
* « '''Bé'''bel(mondo) '''M'''an'''g'''eait du '''Ca'''ssoulet '''S'''u'''r''' un '''Ba'''teau '''Ra'''pide. »
* « '''Bé'''bert '''M'''an'''g'''ea du '''Ca'''nard '''S'''u'''r''' un '''Ba'''teau-'''Ra'''dar. »
* « '''Bé'''ta '''M'''an'''g'''ea du '''Ca'''ca '''S'''u'''r''' le '''Ba'''r de '''Ra'''bat (Maroc). »
* « '''Bé'''atrice '''M'''an'''g'''ea une '''Ca'''rotte en '''S'''i'''r'''otant un '''Ba'''nana-split '''Ra'''vissant. »
===== Groupe 13 =====
''Pour '''B'''ore, '''Al'''uminium, '''Ga'''llium, '''In'''dium, '''T'''ha'''l'''lium.''
*"'''B'''oris '''Al'''lait '''Ga'''mbader '''In''' '''T'''ou'''l'''ouse"
===== [[w:Cristallogène|Cristallogène]]s Groupe 14 =====
''Pour : '''C'''arbone, '''S'''ilicium, '''Ge'''rmanium, '''S'''ta'''n'''num (Étain), '''P'''lom'''b'''.''
* « '''C'''es '''S'''imples '''Ge'''stes '''S'''eraie'''n'''t '''P'''ro'''b'''lématiques'''. »'''
* « '''C''' 'est '''Si''' '''Gê'''nant '''S'''a'''n'''s '''P'''u'''b'''is. »
===== [[w:Pnictogène|Pnictogène]]s Groupe 15 =====
''Pour : '''N'''itrogène (Azote), '''P'''hosphore, '''A'''r'''s'''enic, '''S'''ti'''b'''ium (Antimoine), '''Bi'''smuth.''
* « '''N'''e '''P'''as '''As'''tiquer '''S'''e'''b''' et sa '''Bi'''te. »
* « '''N'''e '''P'''as '''As'''tiquer '''S'''o'''b'''rement le '''Bi'''zuth. »
* « '''N'''e '''P'''as '''As'''tiquer le'''S b'''outs de '''Bi'''te. »
* « '''N'''e '''PAs''' '''S'''a'''b'''rer Byzance('''Bi'''). »
* " '''N'''e '''P'''as '''As'''soir '''S'''a'''b'''rina '''Bi'''zarrement"
===== [[w:Chalcogène|Chalcogène]]s Groupe 16 =====
''Pour : '''O'''xygène, '''S'''oufre, '''Sé'''lénium, '''Te'''llure, '''Po'''lonium.''
* « '''O'''live '''S'''uce le '''Se'''xe '''Te'''ndu de '''Po'''peye. »
* « '''O'''hh '''S'''uce moi le '''Se'''xe et les '''Te'''sticules '''Po'''ilus. »
* « '''O'''scar '''S'''uce '''Se'''s '''Te'''sticules '''Po'''ilus. »
* « '''O'''rgasme '''S'''ur le '''Se'''duisant '''Te'''odore '''Po'''ilu. »
* « '''OS''' '''Se'''dimentaire '''Te'''rriblement '''Po'''li. »
* « '''O'''h '''S'''acré '''Se'''igneur aux '''Te'''sticules '''Po'''lyèdriques. »
* « '''O'''h '''S'''eigneur '''Sé''' (c'est) '''Te'''llement '''Po'''urri. »
===== [[w:Halogène|Halogène]]s Groupe 17 =====
''Pour : '''F'''luor, '''C'''h'''l'''ore, '''Br'''ome, '''I'''ode, '''A'''s'''t'''ate.''
* « '''F'''ootball '''Cl'''ub de '''Br'''èles '''I'''ncapables d''''At'''taquer. »
* « '''F'''ranck et '''Cl'''aude '''Br'''outent '''I'''rène '''A''' '''t'''able. »
* « Une '''F'''issure '''Cl'''aviculaire '''Br'''isa tout '''I'''ntérêt d''''At'''taquer. »
* « '''F'''outez '''Cl'''aire, qui '''Br'''anle '''I'''saac, car elle '''At'''tend. »
* « '''F'''antastique, '''Cl'''aire '''Br'''anche '''I'''nstinctivement l''''At'''tache. »
* « '''F'''erdinand '''Cl'''aque '''Br'''utalement '''I'''rène '''A''' '''t'''erre. »
* « Les '''F'''ameuses '''Cl'''ochettes des '''Br'''ebis d''''I'''talie '''At'''tirent. »
* « Le '''F'''ranc '''Cl'''ovis '''Br'''oie d''''I'''nnombrables '''At'''omes. »
* « '''F'''élicie '''Cl'''aqua '''Br'''ian, '''I'''nnocent '''At'''tardé. »
* « '''F'''olle '''Cl'''ara '''Br'''ave l''''I'''nvincible '''At'''hena. »
===== [[w:Gaz noble|Gaz noble]]s Groupe 18 =====
''Pour : '''Hé'''lium, '''Né'''on, '''Ar'''gon, '''Kr'''ypton, '''Xé'''non, '''R'''ado'''n'''.''
* « '''He'''rcule '''Né'''gligea d’'''Ar'''racher le '''K'''o'''r'''sage de '''Xé'''na et '''R'''o'''n'''fla. »
* « '''Hé''','''Né'''ron, '''Ar'''rête de '''Kr'''âner, '''Xé'''nophobe '''R'''i'''n'''gard ! »
===== [[w:Métalloïde|Métalloïde]]s =====
''Pour : '''B'''ore, '''Si'''licium, '''Ge'''rmanium, Arsenic '''As''', Antimoine '''Sb''', '''Te'''llure et '''Po'''lonium''
*« '''B'''ob '''Si'''ffle son '''Ge'''t '''As'''sis avec '''S'''é'''b''' devant la '''Té'''lé '''Po'''lonaise. »
==== Couples acide/base ====
L’a'''c'''i'''d'''e '''c'''è'''d'''e un ou plusieurs protons tandis que la b'''a'''se c'''a'''pte un ou plusieurs protons.
==== Couples oxydant/réducteur ====
« Les électrons sont du côté de l'Occident. » (phonétiquement ''l’oxydant'')
On peut également retenir que :
**Ox Fixe, Red Cède (L'oxydant fixe des électrons, le réducteur en cède)
** l’oxyd'''ant''' est méch'''ant''' (il prend donc des électrons) ;
** le réduct'''eur''' a bon c'''œur''' (il donne donc des électrons).
**L'oxydANT gagnANT, réductEUR donnEUR
**Cap sur l'occident ! (L'oxydant '''cap'''te les électrons)
**Notons aussi que l'oXydant aXepte (accepte) les électrons.
Phrase qui marche à la fois pour les couples Acide/Base et Oxydant/Réducteur : L'Apéro gagne toujours ! ( A[cide] perd (des protons), O[xydant] gagne (des électrons) ).
'''<nowiki/>'''
==== Ordre de priorité des groupements radicaux dans la nomenclature====
'''<nowiki/>'''
'''A'''bruti '''H'''ans '''est''' l' '''ami de''' '''Nitr'''o! '''Al'''lez '''c'''hantons, L''''alcool''' '''am'''ené '''i'''ci '''e'''st '''t'''rès '''t'''errible
Acide carboxylique, Halogénure, Ester, Amide, Nitrile, Aldéhyde, Cétone, Alcool, Amine, Imine, Ether, Thiol, Thioléter (pour ces deux derniers se reporter à la longueur).
'''A Carbalo''' '''Ester''' '''a mit''' du '''Nitrile Aldéhyde''', '''s'étonne''' '''Alcolaminimine'''. '''Et tertio''', du '''thioleter'''.
"L'''oïc''' '''est''' '''l’ami de Dalton":''' acide carboxylique (-oïque), ester, amide, aldéhyde, cétone.
(ne pas confondre l'aldéhyde et l'alcool- voir la longueur des mots: c'est le plus long qui gagne).
'''<u>Règle de Cahn, Ingold et Prelog</u>'''
<u>''pour '''I '''> '''Br''' > '''Cl''' > '''S''' > '''F '''> '''O''' > '''N''' > '''C''' > '''H'''''</u>
** « '''Ib'''ra '''Cl'''ame '''S'''a '''F'''oi '''O''' '''N'''ouveau '''C'''avani '''H'''éroïque. »
=== Thermodynamique ===
==== Loi des gaz parfaits ====
'''pV''' = '''nRT'''
'''p''' = pression en pascals ; '''V''' = volume en mètres cubes ; '''n''' = quantité de matière en mols ; '''R''' = constante des gaz parfaits. R = 8,3 J.K-1.mol-1 ; '''T''' = température en Kelvins
'''P'''ascal '''v'''oulut '''n'''ous '''r'''endre '''t'''héiste (référence au pari de Pascal)
'''P'''a'''v'''a'''n'''e'''r'''ai'''t''' (sans les voyelles)
'''pV''' = '''nRT''' n’est pas pété, énervé (ptnrv)
'''P'''rocès-'''v'''erbal ; '''n'''ous '''r'''end '''t'''riste
'''P'''uissance de '''V'''itesse = '''n'''otion de '''R'''apidité '''T'''errestre - Pour les joueurs de jeu de rôle uniquement !
Les PV d'un Pokemon est égale au Niveau fois sa RésisTance
<br>
==== Différentielle de l’enthalpie ====
dH = TdS + VdP
'''d'''îners '''H'''onorables = '''T'''artes '''d'''e '''S'''aison + '''V'''ins '''d'''u '''P'''ays (dH=TdS + VdP)
'''d'''ouces '''H'''armonies = '''T'''oniques '''d'''e '''S'''olfège + '''V'''ibrations '''d'''e '''P'''iano (dH=TdS + VdP)
'''d'''écouvertes '''H'''éroïques = '''T'''résors '''d'''e '''S'''able + '''V'''oyages '''d'''e '''P'''irates (dH=TdS + VdP)
'''d'''anses '''H'''ispaniques = '''T'''angos '''d'''e '''S'''eville + '''V'''alses '''d'''e '''P'''ampelune (dH=TdS + VdP)
'''d'''épart '''H'''éroïque = '''T'''oujours '''d'''u '''S'''tyle + '''V'''itesse '''d'''e '''P'''ointe (dH=TdS + VdP)
'''d'''estination des '''H'''istoriens = '''T'''raversant '''d'''es '''S'''iècles + '''V'''oyageant '''d'''ans le '''P'''assé (dH=TdS + VdP)
==== Différentielle de l’enthalpie libre ====
dG = VdP-SdT
'''V'''iande '''d'''e '''P'''orc '''-''' '''S'''el '''d'''e '''T'''able (VdP-SdT)
'''V'''ends '''d'''u '''P'''ain sans (-) '''S'''ortir '''d'''e '''T'''on '''G'''îte (VdP - SdT = dG)
==== Différentielle de l’énergie interne (sans variation de quantité de matière) ====
dU=TdS-PdV
'''T'''u '''d'''ois '''S'''avoir mais '''P'''as '''d'''e'''V'''iner (TdS-PdV)
'''T'''out '''d'''e '''S'''uite '''Moins''' de '''P'''oints '''d'''e '''V'''ie
'''T'''éter '''D'''u '''S'''el '''P'''endant '''D'''eux (ou '''D'''ix) '''V'''endredis.
'''T'''rou '''d'''ans le '''S'''lip et '''P'''antalon '''d'''ans le '''V'''ent
'''<nowiki>d'</nowiki>'''après '''U'''lysse = '''T'''outes '''d'''es '''S'''irènes mais '''P'''as '''d'''es '''V'''ampires
<br />
==== Différentielle de l’énergie interne (avec variation de quantité de matière) ====
dU=TdS-PdV+µdn
'''T'''rop '''d'''e '''S'''avoir mais '''P'''as '''d'''<nowiki/>'en'''V'''ie c'est être '''nu''' '''d'''ans la '''n'''uit
=== Géologie ===
'''Les ères géologiques, du Quaternaire au Primaire (permettant de retenir les datations approximatives 7x60 + 3x40 Ma)'''
'''Cénozoïque''' 60 (Quaternaire + Tertiaire) '''Crétacé''' 120 '''Jurassique''' 180 '''Trias''' 240 '''Permien''' 300 '''Carbonifère''' 360 '''Dévonien''' 420 '''Silurien''' 460 '''Ordovicien''' 500 '''Cambrien''' 540
'''C'''ite '''C'''e '''J'''oli '''T'''ruc '''P'''our '''C'''onnaître '''D'''es '''S'''iècles '''O'''rdonnés '''C'''orrectement
==== Niveaux de l'échelle chronologique géologique ====
'''Éo'''le '''ér'''adiqua les '''pe'''upliers '''ép'''uisés par l''''âge'''.
* (éon, ère, période, époque, âge)
==== Les six périodes géologiques de l’ère primaire ====
;Cambrien, Ordovicien, Silurien, Dévonien, Carbonifère, Permien.
* '''''Cambr'''onne, l’'''ord'''urier,''' s’il eû'''t été '''dévo'''t, n’eût point '''carboni'''sé son '''pèr'''e''
* '''''Cambr'''onne '''ordo'''nna '''sil'''ence et '''dévo'''uement à ses '''car'''abiniers '''perm'''issionnaires''
* '''''Cambr'''onne '''aur'''ait, '''s'il eût''' été '''dévo'''t, '''carboni'''sé son '''pèr'''e''
* '''c-or-si-dé-ca-pé''' = Corps si décapés.
* ''Le '''ca-or-sil-dé-ca-pe''' ='' Le Cahors, il décape.
==== Les trois périodes géologiques de l’ère secondaire ====
;Trias, Jurassique, Crétacé.
* '''T'''rois '''j'''ours '''c'''hacune.
==== Les cinq périodes géologiques de l’ère tertiaire ====
;Paléocène, Éocène, Oligocène, Miocène, Pliocène.
* '''Pâl'''e '''Et o'''bscène '''Au lit''', '''Mio''' se '''Plie au'''x scènes (de l'amour)
==== Stalactites et stalagmites ====
''Les stalac'''t'''ites '''t'''ombent, les stalag'''m'''ites '''m'''ontent.''
==== Géophysique ====
Formule pour la [http://fr.wikipedia.org/wiki/Anomalie_de_Bouguer correction gravitationnelle de Bouguer]:
2*π*h*ρ*G
(G=constante gravitationnelle ρ=Masse volumique/Densité)
"deux pies hachent Roger"
2 π h ρ G
(ρ = "Rho", lettre grecque)
==== [[w:Échelle de Mohs|Échelle de Mohs]] ====
"'''T'''a '''G'''rosse '''C'''oncierge '''F'''olle d''''A'''mour '''O'''se '''Q'''uémander '''T'''es '''C'''aresses '''D'''ivines"
"'''T'''oi '''G'''rand '''C'''hevalier, '''F'''uis '''A'''vec '''O'''rdre '''Q'''uand '''T'''on '''C'''œur '''D'''éfaille"
"'''T'''rès '''G'''rand '''C'''hemin de '''F'''er '''A'''pache. '''O'''h ! '''Q'''uel '''T'''emps '''C'''e '''D'''imanche !"
"'''T'''on '''G'''igolo '''C'''onte '''F'''leurette '''A''' '''(H)O'''rtense, '''Q'''ui '''T'''e '''C'''ocufie '''D'''iablement !"
"'''T'''on '''G'''rand '''C'''ul '''F'''endu '''A''' une '''O'''uverture '''Q'''ue '''T'''u '''C'''aches '''D'''écemment"
"'''T'''on '''G'''ros '''C'''ochon '''F'''ait '''A'''ïe '''O'''uille '''Q'''uand '''T'''u '''C'''ognes '''D'''essus"
([[w:Talc|'''T'''alc]], [[w:Gypse|'''G'''ypse]], [[w:Calcite|'''C'''alcite]], [[w:Fluorite|'''F'''luorite]], [[w:Apatite|'''A'''patite]], [[w:Orthose|'''O'''rthose]], [[w:Quartz (minéral)|'''Q'''uartz]], [[w:Topaze|'''T'''opaze]], [[w:Corindon|'''C'''orindon]], [[w:Diamant|'''D'''iamant]])
=== Botanique ===
==== Distinguer les hêtres des charmes ====
Le '''charme''' d''''Adam''' est d''''être''' à '''poil'''.
ou encore: "Être à poils charme Adam"
La feuille du charme a des dents (charme d’Adam)
et la feuille du hêtre a des poils (être à poil).
==== Distinguer les principales espèces de pin ====
Les aiguilles du pin '''blanc''' sont groupées par '''5'''.
'''Blanc''' a '''5''' lettres.
Le pin '''rouge''' a des aiguilles groupées par '''2'''.
Le mot '''rouge''' a '''2''' syllabes (s'il est suivit d'un mot commençant par une consonne en versification).
Les aiguilles du pin '''noir''' sont en groupes de '''2'''.
Dans le mot '''noir''', il y a '''2''' voyelles.
==== Distinguer les sapins des épicéas ====
Les sapins ('''''A'''bies'') ont des cônes '''a'''scendants, les épicéas ('''''P'''icea'') ont des cônes '''p'''endants.
==== Distinguer les cèdres ====
Le cèdre de l’'''A'''tlas a les pointes des branches '''a'''scendantes, le cèdre de l’Himalaya (''Cedrus '''d'''eodara'') '''d'''escendantes, le cèdre du '''L'''iban horizonta'''l'''es.
==== Distinguer les platanes des érables ====
pl'''A'''t'''A'''ne : feuilles '''A'''lternes
'''É'''rable : feuilles oppos'''É'''es
==== Distinguer un Catalpa d’un Paulownia ====
P'''a'''ulowni'''a''' : deux feuilles par nœud (2 fois le a dans le nom)
C'''a'''t'''a'''lp'''a''' : trois feuilles par nœud (3 a dans son nom)
==== Distinguer les feuilles de trèfle ====
Les feuilles du trèfle blanc ont de petites dents autour, celles du trèfle rouge ont des poils autour. Dents blanches, poils rouges (et non dents rouges, poils blancs !)
==== Distinguer les feuilles de trèfles de celles des luzernes ====
Les luzernes (''Medicago'') ont des pointes (= aiguilles, les médecins font des injections) au bout des folioles.
==== Distinguer les vesces des gesses ====
gesses ('''''L'''athyrus'') : l'alignement des points d'insertion des filets des étamines forme un angle droit avec le tube des étamines → L
vesces ('''''V'''icia'') : il est oblique par rapport au tube ; on retrouve ce côté oblique dans la lettre V
==== Distinguer les knauties des scabieuses ====
'''k'''nautie : 4 ('''k'''atr’) pétales dans chaque fleur de l’inflorescence
'''s'''cabieuse : 5 ('''s'''inq) pétales par fleur de l’inflorescence et des '''s'''oies sur le réceptacle
==== Distinguer les plantules de céréales dans un champ ====
BOAS :
le '''b'''lé étant plus riche a des oreillettes, des poils et une ligule
l’'''o'''rge a des oreillettes et une ligule
l’'''a'''voine a une ligule
le '''s'''eigle étant plus pauvre, n’a plus rien
=== Zoologie ===
==== Ordre des cétacés ====
« '''C'est assez''', dit la '''baleine''', al'''ors que''' j'ai le '''dos fin''' je me '''cache à l'eau'''
**baleine, orque, dauphin, cachalot, mais la liste est très incomplète.
==== Ordre des tatous ====
Les tatous font partie de l’ordre des ''Édentés'' car : "T’as tout sauf les dents !"
=== Biologie ===
==== L'ordre hiérarchique de la classification de [[w:Taxinomie|taxinomie]] ====
Des Rats Essayent de Courir là Où Finissent les Grands Espaces (Raccourcis).<br />
DoRs EnCOre, la Famille GÈRe.<br />
Reste En Classe Ou Fais Grandes Études.<br />
Reste En Contact, Odile, Fais Gaffe, Émile ! (inspiré de la Cité de la peur, où Odile est attachée de presse et Émile tueur)<br />
Domaine, Règne, Embranchement, Classe, Ordre, Famille, Genre, Espèce, (Race).
RECOFGE: Règne, Embranchement, Classe, Ordre, Famille, Genre, Espèce
==== Les [[w:bases azotées|bases azotées]] de l’ADN ====
'''À''' '''T'''on '''G'''rand '''C'''œur.
'''A'''h '''T'''a '''G'''ueule '''C'''rétin
'''À''' '''T'''able '''G'''rand '''C'''hef
('''ATGC''' : [[w:adénine|adénine]], [[w:thymine|thymine]], [[w:guanine|guanine]], [[w:cytosine|cytosine]])
==== Intron/Exon ====
'''Int'''ron = '''Int'''rus ou '''int'''rusif, c'est la partie de nucléotide d'un gène qui est excisé de l'ARN lors de l'épissage, à l'inverse des '''exons.'''
'''<nowiki/>'''
==== La séquence nucléotidique des télomères humains ====
'''T'''ous '''t'''es '''a'''mis se '''g'''avent de '''g'''énial '''g'''uarana.
(TTAGGG)
==== Les différentes phases de la [[mitose]] ====
**le Prophète Athée (Pro Met A T)
** Je te '''ProMets''' de l''''Ana'''l au '''Telo'''
** '''ProMets''' à '''Anna''' de '''Tél'''éphoner
** '''P'''etit '''M'''ammifère '''À''' '''T'''éton
** '''P'''etit '''M'''atin '''A'''uprès de '''T'''oi
** '''P'''etit '''M'''artien '''A'''ttaque la '''T'''erre
** '''P'''etite '''M'''émé '''A''' '''T'''éléphoné
** '''P'''apa '''M'''aman '''A'''mour '''T'''oujours
** '''P'''our '''M'''on '''A'''mi '''Th'''omas
** '''P'''our '''M'''on '''A'''mour '''T'''oujours
** '''P'''our '''M'''on '''A'''nus '''T'''roué
** '''P'''etite '''M'''ademoiselle '''A'''ge '''T'''endre
** Promettante : '''Pro'''/'''met'''t/'''an'''/'''te'''
** '''PROMETANATELO'''
** ou ProMéthée est AnaTello (Se rappeler de la phrase Prométhée est un intello)
** '''P'''apa '''M'''ange '''A''' '''T'''able
** '''P'''ro des '''M''' '''A''' '''T'''hs
** '''P'''rof de '''M''' '''A''' '''T'''hs
** '''P'''ierre '''M'''angea des '''A'''nanas '''T'''ransgéniques
** '''P'''ouvoir '''M'''asculin '''A'''vant '''T'''out
** '''P'''aris-'''M'''arseille '''A''' '''T'''rotinette
** '''P'''our '''M'''émoriser : '''A'''voir '''T'''ravaillé
** '''P'''our '''M'''ieux '''A'''pprendre'''T'''out
** '''P'''ays les '''M'''oins '''avancés'''
** Le '''Pro'''f '''Met''' l' '''Âne''' devant la '''Tél'''é
('''P'''rophase, '''M'''étaphase, '''A'''naphase, '''T'''élophase)
** '''P'''auline '''M'''arche '''à''' la '''T'''équila
** TAMPI (à lire à l'envers)
** c'est PRoMEtteur An(un) inTello
** Papa Mange un Abricot Trop sucré (avec le s de sucré pour la synthèse qui suit la mitose G1 --> S --> G2)
==== Les différentes phases de la PROPHASE ====
'''Le''' '''Zi'''zi du '''Pachy'''derme a des '''Di'''mensions '''Dia'''boliques.
(Leptotène, Zygotène, Pachytène, Diplotène, et Diacinèse)
ou
Letzplin (lepto/zygo) protége (pachy) didier (diplo/diacinèse)
ou
Le Zip à Didier
ou
Le zizi n'a pas de diarrhée
ou
Le zizi poilu du doyen
ou
Le zizi du pachyderme et du diplodocus sont différents
ou
Les Zizis Peuvent Devenir Durs !
ou '''Pré'''férer '''Le''' '''Zi'''zi du '''Pachy'''derme à celui du '''Diplo'''docus '''Dia'''bétique
Pour préleptotène, leptotène, zygotène, pachytène, diplotène, diacinèse
==== Les acides aminés dits essentiels : ====
''on compte neuf acides aminés essentiels : le tryptophane, la lysine, la méthionine, la phénylalanine, la thréonine, la valine, la leucine, l'isoleucine et l'histidine''
Le (LEU) trou (THR) de l'hystérique (HIS) Lyse (LYS) fait (PHE) tripper (TRY) valentin (VAL) mais (MET) ilose (ILE) pas !
'''''Le''' '''très''' '''ly'''rique '''Tri'''stan '''fait''' '''va'''chement '''m'''archer '''Ys'''eult, quelle '''Hist'''oire !''
ou encore :
Hystérique, le très lyrique Tristan fait vachement méditer Iseult en Argentine (His)Leu-Thr-Lys-Trp-Phe-Val-Met-Iso(Arg): Histidine et Arginine seulement essentiels chez les enfants.
('''Le'''ucine, '''Thré'''onine, '''Ly'''sine, '''Try'''ptophane, '''Phé'''nylalanine, '''Va'''line, '''M'''éthionine, '''Is'''oleucine, '''Hist'''idine)
Met le dans la valise, il fait trop d'histoire avec l'argent/en argentine.
le cours d’'''hist'''oire, '''il''' '''le''' '''lit''' '''mais''' '''fait''' '''tres''' '''trivial'''
('''Hist'''idine; '''Ile''': Isoleucine, '''Leu'''cine, '''Ly'''sine, '''Mé'''thionine, '''Phé'''nylalanine '''Thré'''onine,'''Try'''ptophane,, '''Va'''line)
Dans une '''V'''(aline)'''I'''(soleucine)'''L'''(eucine), il y a des '''H'''(ystidine)'''L'''(ysine)'''M'''(ethionine) et des '''P'''(hénilalanine)'''T'''(hréonine)'''T'''(ryptophane) (Dans une ville, il y a des HLM et des PTT)
''ils le valent trop trop mes félicitations''
'''ile''' '''leu''' '''val''' '''thr'''''op'' '''tr'''''o'''''p''' '''met''' '''phe''' '''lys''' ''itations''
''Va te le mettre, Phillipe''
'''VA'''l '''TH'''r '''LE'''u '''MET''' '''TR'''p '''PH'''e '''ILE''' '''LY'''s pe
'''''Va''' '''tri'''poter '''Lys'''e mais ('''met''') fait ('''phe''') '''le''' '''tr'''ès '''iso'''lément''
''val thr lys met phe (fait) leu trp ile (iso-leucine)''
Plus simple et plus concret que tous les autres moyens mnémotechniques:
VTT MILLPH (prononcé VTT MILF) et ainsi vous obtiendrez : Valine, Thréonine, Tryptophane, Méthionine, Isoleucine, Leucine, Lysine, Phénylalanine, Histidine.
==== Les acides aminés dits apolaire (Proline polaire/apolaire comprise) ====
Valérie promet à la triste Iseult le phénix et la Glycine.
(val) (Pro/Met)(Ala)(Trp) (Ile) (Leu) (Phe) (Gly)
Glycine dévale à la pelle, il le promet trop.
==== Le [[w:cycle de Krebs|cycle de Krebs]] ====
**''Si le '''citr'''on '''iso'''<nowiki>le l'</nowiki>'''acéto'''ne, le '''succi'''nct '''succès''' '''fumera''' '''m'''oins '''haut'''''
('''citr'''ate, '''iso'''citrate, alph'''acét'''oglutarate, '''succ'''inyl CoA, '''succ'''inate, '''fumara'''te, '''ma'''late, '''o'''xaloacétate)
** Avec les initiales : "C'est ici ce samedi soir : fumette, mal-à-la-tête, oubliette."
ou encore :
** ''La '''C''' '''I''' '''A''' '''su'''specte un '''su'''spect qui '''fum'''e des '''Mal'''boros '''ox'''ydées.''
** '''O'''h '''C'''atastrophe ! '''I'''l '''Os'''e '''Ac'''tiver '''Sa''' '''Su'''per '''F'''orce '''M'''agique
==== Le [[w:Cycle de Calvin|Cycle de Calvin]] ====
**"Les '''ri'''mes '''intermédiaires''' aux '''fo'''rmes '''diffo'''rmes de '''PGAL''' '''ri'''ment."
('''ri'''bulose phosphate, '''intermédiaire''' instable qui se scinde en deux 3-'''pho'''sphoglycérate, 1,3-'''dipho'''sphoglycérate, phosphoglycéraldéhyde ('''PGAL'''), dont l'un quitte le cycle et cinq sont utilisés pour reformer le '''ri'''bulose phosphate.)
==== Les Aldohexoses ====
'''Allo'''ns, '''altr'''uiste '''gl'''acer la '''mann'''e, '''Gul'''liver '''i'''ra '''gal'''érer au '''tall'''us
('''Allo'''se; '''altr'''ose; '''gl'''ucose; '''mann'''ose, '''Gu'''lose '''i'''dose '''ga'''lactose '''ta'''lose)
==== Les protéines intervenant dans les jonctions cellulaires ====
(Attention que ces phrases ne fonctionnent pleinement que si l'on connaît <i>déjà</i> les protéines intervenant, mais que l'on a du mal à retenir lesquelles font quoi.)
- Jonction Adherens : '''Vin'''t le '''cad'''avre '''é'''quipé d''''a'''rmes '''α''' qui '''plaqu'''a le '''glo'''ussant '''ca'''valier.
-> '''Vin'''culine, '''cad'''hérine-'''E''', '''a'''ctine, '''α'''-actinine, '''plak'''o'''glo'''bine, '''ca'''ténine.
- Jonction de contact focal : '''Vin'''t la '''paix''' '''intégr'''ale; les '''a'''rmes '''α''' en '''ta'''s.
-> '''Vin'''culine, '''pax'''iline, '''intégr'''ines, '''a'''ctine, '''α'''-actinine, '''ta'''line.
- Desmosome : Tout ce qui '''colle''', plus la '''kératine'''.
-> Desmo'''coll'''ine, desmo'''glé'''ine, desmo'''plak'''ine, '''plak'''oglobine, '''plak'''ophiline, '''kératine'''.
- Jonction Gap (de communication) : Elle induit une '''connexion'''.
-> '''Connex'''ines.
- Hémidesmosomes : La '''p'''yramide de '''Khé'''ops '''d'''étruit '''intégr'''alement '''la mi'''en'''ne'''.
-> '''P'''lectine, '''ké'''ratine 5 et 14, '''d'''ystonine, '''intégr'''ine α6β4, '''laminine''' 332.
- Jonction tight, ou étanche, qui comporte des "'''kissing''' points" et dont le complexe crée une "'''zonula occludens'''" : '''Embrasser''' '''Claudine''' crée une '''occlu'''sion '''acti'''ve.
-> Claudine, occludine, actine, ZO-1.
==== Les protéines des filaments intermédiaires ====
Elles diffèrent en fonction du tissu où elles se trouvent. Attention que les moyens proposés ici servent plus à retrouver la fonction d'une protéine déjà connue qu'à retenir le nom en lui-même.
- Épithéliums : Kératines.
Facile, il suffit de réfléchir un instant pour s'apercevoir que l'épithélium est bourré de kératines (couche cornée, desmosomes, ...).
- Tissu '''C'''onjonctif : '''V'''imentines.
On retient "'''CV'''".
- Tissu '''M'''usculaire : '''D'''esmines.
On retient "'''MD'''", une abréviation fréquente en anglais pour qualifier un Docteur en Médecine (Medicinæ doctor).
- Tissu Nerveux proprement dit : Protéines des neurofilaments.
Elles n'ont donc pas de nom propre, leur nom est leur fonction : des '''protéines''' dans les '''filaments''' intermédiaires des '''neuro'''nes.
- Tissu Nerveux "de soutien" (tissu glial, donc) : Protéines fibrillaires acides gliales.
Elles n'ont pas de nom propre, le nom est la fonction : Des '''protéines''' qui génèrent des '''filaments''' ('''fibrillaires''', donc) appartenant au tissu '''glial'''. La seule chose à retenir est qu'elles sont acides.
- Noyaux : Lamines.
On peut retenir qu'elles forment la '''lamina''' nucléaire, ou encore que pour arriver au noyau d'une cellule il faut la "maltraiter", et pourquoi pas la '''laminer'''.
==Technologie==
===Électronique===
====Code couleur des résistances====
Code couleur à retenir : Noir, Marron, Rouge, Orange, Jaune, Vert, Bleu, Violet, Gris, Blanc
* Ne Mangez Rien Ou Je Vous Battrai Violemment Gros Béta.
* Ne Mangez Rien Ou Je Vous Brûle Votre Grosse Barbe.
* Ne Mangez Rien Ou Jeunez Voilà Bien Votre Grande Bêtise.
=== Informatique ===
==== RJ45 croisé ====
Broches 361782'''45'''.
==== Modèle OSI ====
Le modèle OSI divise les fonctionnalités nécessaires à la communication en sept couches :
*# '''P'''hysique,
*# '''L'''iaison,
*# '''R'''éseau,
*# '''T'''ransport,
*# '''S'''ession,
*# '''P'''résentation,
*# '''A'''pplication.
** Il faut être deux pour avoir une liaison.
** Le 4*4 est un transport.
"Félicie, OSI"
*#Séduit par son '''PHYSIQUE'''
*#et n'ayant aucune '''LIAISON''',
*#je l'ai contactée sur un '''RÉSEAU''' social.
*#Arrivé chez elle en '''TRANSPORT''' en commun,
*#suivi une '''SESSION''' de va-et-vient,
*#sans aucune forme de '''PRÉSENTATION''',
*#j'y ai mis toute mon '''APPLICATION'''.
Le lendemain, elle me recontactait...
Les mots des phrases suivantes ont des initiales identiques à celles des couches, dans l'ordre ci-dessus ( P L R T S P A ) :
*#'''P'''artout '''L'''e '''R'''oi '''T'''rouve '''S'''a '''P'''lace '''A'''ssise
*# '''P'''our '''L'''e '''R'''éseau '''T'''out '''S'''e '''P'''asse '''A'''utomatiquement
*# '''P'''ar '''L'''à, '''R'''aisonnons '''T'''ransport '''S'''ans '''P'''résenter l''''A'''pplication
*# ''Pour les amateurs du jeu d'échecs :'' '''P'''rends '''L'''a '''R'''eine '''T'''out '''S'''era '''P'''lus '''A'''gréable
*# '''P'''etit '''L'''apin '''R'''ose '''T'''rouvé à la '''S.P.A.'''
*# '''P'''our '''L'''e '''R'''epas '''T'''out '''S'''era '''P'''rêt '''À''' 7 heures (7 couches)
*# '''P'''our '''L'''e '''R'''éseau '''T'''u '''S'''eras '''P'''as '''A'''ugmenté
*# '''P'''our '''L'''a '''R'''oute, '''T'''u '''S'''uis '''P'''ierre-'''A'''lain !
*# '''P'''ourquoi '''L'''e''' R'''oux '''T'''ouche '''S'''on '''P'''énis '''A'''llongé ?
*# '''P'''our '''L'''a''' R'''etenir '''T'''oujours '''S'''e '''P'''arler '''A'''vant !
*# '''P'''ierre '''L'''ouis ''' R'''este '''T'''oujours '''S'''ans '''P'''énétration '''A'''nale !
*# '''P'''endant '''L'''es '''R'''ègles '''T'''oujours '''S'''évir '''P'''ar l''''A'''nus !
Les mots des phrases suivantes ont des initiales identiques à celles des couches, dans l'ordre inverse ( A P S T R L P ) :
*# '''A'''près '''P'''lusieurs '''S'''emaines, '''T'''out '''R'''espire '''L'''a '''P'''aix
*# '''A'''près '''P'''lusieurs '''S'''odomies, '''T'''out '''R'''ectum '''L'''âche '''u'''n '''P'''et
*# '''A'''vec '''P'''atrick '''S'''abatier, '''T'''u '''R'''amasses '''L'''e '''P'''ognon
*# L''''A'''méricain '''P'''uritain '''S'''e '''T'''itille ('''R'''arement|'''R'''égulièrement) '''L'''e '''P'''hallus
*# '''A'''pparemment '''P'''atrick '''S'''ebastien '''T'''on '''R'''ectum '''L'''aisse '''P'''erplexe
*# '''A'''h '''P'''etite '''S'''alope, '''T'''u '''R'''ecraches '''L'''a '''P'''urée
==== Classe d'adresse IP ====
En binaire, compter le nombre de 1 avant le premier 0.
* A : 0 -> 127 (+ 127) <code>0xxxxxxx</code>
* B : 128 -> 191 (+ 63) <code>10xxxxxx</code>
* C : 192 -> 223 (+ 31) <code>110xxxxx</code>
== Grammaire et orthographe ==
===<u>Les principaux mots interrogatifs</u>===
Ce moyen mnémotechnique est très utile pour les coups de téléphone où l’on doit demander des renseignements. Il faut dresser rapidement la liste des mots interrogatifs sur un papier et être sûr que l’on a des réponses à toutes les questions.
'''C’est cucu, c’est occupé !'''
*'''C'''ombien ?
*'''Q'''uoi ?
*'''Q'''ui ?
*'''C'''omment ?
*'''O'''ù ?
*'''Q'''uand ?
*'''P'''ourquoi ?
=== [[w:Conjonction de coordination|Conjonctions de coordination]] ===
''Mais où est donc Ornicar ?''
(Mais, Ou, Et, Donc, Or, Ni, Car)
Mais cette méthode est pédagogiquement discutable, car elle entretient la confusion entre ''et'' (conjonction) et ''est'' (verbe ''être'' à la troisième personne du singulier), ainsi qu'entre ''ou'' (conjonction) et ''où'' (adverbe ou pronom relatif).
Attention, ''donc'' n’est plus une conjonction de coordination, mais bien un verbe conjugué pour ''est'' et un adverbe de coordination pour ''où'' !
On peut aussi l’apprendre de cette manière afin de sortir le OU et ne pas induire de confusion dans l’esprit
Mais ! Et donc Ornicar (mais, et, donc, or, ni, car) en jouant sur la sonorité de la surprise
Au Québec, on dit aussi: ''Mais où est donc Carnior ?''
===<u>Les principales prépositions</u>===
*''Adam part pour Anvers avec cent sous sûrs, entre derrière chez Decontre''
:(À, Dans, Par, Pour, En, Vers, Avec, Sans, Sous, Sur, Entre, Derrière, Chez, De, Contre)
*''Adam part pour envers avec deux cents sous chez Parmisur. ''
:(À, Dans, Par, Pour, En, Vers, Avec, De, Sans, Sous, Chez, Parmi, Sur)
*''Adam part pour Anvers avec deux cents sous.''
*"Adam part pour Anvers avec deux cents sous chez Sur."
:( À, Dans, Par, Pour, En, Vers, Avec, De, Sans, Sous, Chez, Sur)
*''Adam part pour Anvers avec cent sous de chez surdurand.''
*"(À, Dans, Par, Pour, En, Vers, Avec, Sans, Sous, De, Chez, Sur, Durant)
*''Adam Surché part pour Anvers avec deux-cents sous''
*"(À, Dans, Sur, Chez, Par, Pour, En, Vers, Avec, Sans, Sous)
*’’ Adeudans part pour Sur sans sous chez Devant-derrière avec Avant-après-contre’’
===<u>Les pronoms relatifs</u>===
3 culs domptent ouvertement monsieur lequel,Duquel,Auquel...
''<nowiki>Qui que quoi dont où lequel duquel auquel…'</nowiki>''
===<u>Les déterminants possessifs</u>===
Au pluriel: ''Mais c'était nos voleurs !''
(mes, ses, tes, nos, vos, leurs)
===<u>Orthographe</u>===
* Mou'''r'''ir ne prend qu’un “ r ” car on ne meurt qu’une fois.
* Nou'''rr'''ir prend deux “ r ” car on se nourrit plusieurs fois.
* Cou'''r'''ir ne prend qu’un “ r ”car on manque d’air en courant,<br /> mais quand on a'''rr'''ive on prend tout l’air qu’on peut.
* L’hironde'''ll'''e prend deux “ l ” car elle vole avec ses deux ailes.
* La v'''i'''e'''i'''lle ne peut marcher qu’avec ses deux bâtons.
* A'''pp'''uyer prend deux « p » car on s’appuie mieux sur deux pattes.
* Un ba'''l'''ai prend un seul “ l ” car il n’y a qu’un manche.
* Un ba'''ll'''et prend deux “ l ” car pour danser il faut deux jambes.
* Toujour'''s''', toujours un “ s ” et à jamai'''s''', ne jamais l’oublier.
* J’a'''p'''erçois sur une jambe mais j’a'''pp'''arais sur les deux.
* Quand je mets deux "p" à apercevoir, j'aperçois une faute.
* Je n’a'''p'''erçois qu’un '''p''' à a'''p'''ercevoir (ou je m’a'''p'''erçois qu’a'''p'''ercevoir ne prend qu’un '''p''').
* Enve'''l'''oppe ne prend qu’un “ l ” car on ne met qu’une lettre dans une enveloppe. En revanche pour un vélo on a deux '''p'''neus : dé''velo'''''pp'''er, en''velo'''''pp'''er, etc.
* Cuiss'''eau''' de v'''eau'''.
* Sate'''ll'''ite prend 2 '''L''' car c’est plus pratique pour voler. (et un seul '''t''' car il ne tourne qu’autour d’une seule '''T'''erre)
* Évide'''mm'''ent prend deux '''m''' comme dans '''Papa/Maman''' (à noter : tous les adjectifs qui se terminent par "ent", comme "évident", prennent 2 "m" ensuite, comme "évidemment").
* Je me souviens d’une corde en rappel : On se souvient '''DE''' quelque chose, mais on se rappelle quelque chose.
* Un pa'''r'''esseux cou'''r'''onné ca'''r'''essait une ca'''r'''otte avec un air intéressé : liste de mots qui ne prennent qu’un '''r'''.
* Co'''ll'''ine a deux colonnes (2 ” l ”) et colo'''nn'''e a deux collines (2 ” n ”).
* Dé'''velopp'''er je fais du vélo avec mes deux pieds pour pédaler.
* Échapper prend deux "P" car on s'échappe mieux avec deux pieds.
* Culo'''tt'''e prend deux '''T''' car il y a deux jambes pour une culotte
* Un professeur a un seul '''F'''ront et deux '''S'''ourcils, donc un seul F, mais deux S
* Philippe : je marche (2p) mais ne vole pas (1l)
* On parle le flaman'''D''' dans les Flan'''D'''res. Le flaman'''T''' rose est un oiseau de grande '''T'''aille.
* L’am'''a'''nde pousse sur un '''a'''rbre ; l’am'''e'''nde sur un '''e'''ssuie-glace.
*Guè'''r'''e signifie "pas beaucoup", donc un seul r. Il faut au moins deux adversaires pour faire la gue'''rr'''e, donc 2 R.
* Une P'''ê'''che (melba...) / P'''ê'''cher (du poisson) / P'''é'''cher (commettre une offense) / un P'''é'''ché (originel...) : Dans la p'''ê'''che en rivière, le '''^''' représente l’hameçon et la p'''ê'''che (fruit) représente le flotteur de la canne à p'''ê'''che). Quand on confesse au prêtre un p'''é'''ché, on fait profil bas (accent aigu sur le '''é''').
* Tous les membres de la famille ont un accent grave, sauf pépé et mémé : père, mère, nièce…
* M devant Mbappé (M devant M, B et P)
* Reg versus Erg :
** Un Reg est un désert de Roches, de pieRRes
** L’ERg est un désERt de dunes
===<u>Mots avec accent circonflexe</u>===
* Une t'''a'''che c'est suffisamment sale pour ne pas avoir besoin d'en rajouter (d'accent circonflexe)
* Le chapeau de c'''i'''me est tombé dans l’ab'''î'''me. Et celui du bo'''i'''teux dans la bo'''î'''te !
* On dit chapeau ! pour la '''tâche''' accomplie et non pas chapeau ! pour la '''tache''' sur le vêtement.
* Un chien ou un chat marche sur deux paires de pa'''tt'''es. Par contre, on fait cuire des p'''ât'''es dans une casserole qu'on couvre avec le chapeau du â.
* "Traîner ses guêtres", c'est flâner.
<u>Pour les anglophones</u>, il suffit souvent de comparer le mot anglais de même racine que le mot français sur lequel on a un doute pour l'accent circonflexe. Si ce mot anglais contient un S, le mot français équivalent contient souvent un accent circonflexe. Exemples :
* Ancêtre / Ancestor
* Apôtre / Apostle
* Arrêt / Arrest
* Bâtard / Bastard
* Bête / Beast
* Boîte / Box
* Château / Castle
*Cloître / Cloister
* Côte (anatomie, rivage, pente, culinaire) / Coast (rivage)
* Coût / Cost
* Crête / Crest (vague, cimier, huppe…)
* Dégoût / Disgust
* Épitre / Epistle
* Fête, Festif (fr) / Feast (eng)
* Guêpe / Wasp (eng) tous deux venant de vespa (latin)
* Forêt / Forest
* Hâtif / Hasty
* Hôpital / Hospital
* Hôte, hôtesse / Host, hostess
* Hâte / Haste
* Honnête / Honest
* Huître / Oyster
* Île / Island
* Intérêt / Interest
* Maître / Master
* Mât / Mast (bateau)
* Paître / To pasture
* Pâtisserie, Pâte / Pastry / Pasta (ital.)
* Plâtre / Plaster
* Quête / Quest
* Rôtir / To roast
* Tâche (travail et non salissure) / Task
* Tempête / Tempest
===<u>Pluriel</u>===
*Pluriel en OUX au lieu de OUS
::Un '''hibou''' moche comme un '''pou'''
::Avait pour '''joujou''' sur ses '''genoux'''
::Un '''caillou''' aussi '''chou''' qu’un '''bijou'''.
Variante :
::Viens mon '''chou''', mon '''bijou'''
::Viens sur mes '''genoux'''
::Avec des '''joujoux''' et des '''cailloux'''
::Pour éloigner ces vilains '''hiboux''' pleins de '''poux'''
Variante :
::Viens mon '''chou''', mon '''joujou''', mon '''bijou'''
::Sur mes '''genoux'''
::Jeter des '''cailloux'''
::À ces vieux '''hiboux''', pleins de '''poux'''
Variante :<blockquote>Viens mon '''chou''', sur mes '''genoux''' avec tes '''joujoux''' et tes '''bijoux'''</blockquote><blockquote>Pour jeter des '''cailloux''' sur les vilains '''hiboux''' pleins de '''poux'''.</blockquote>
Variante :
Répéter plusieurs fois très vite : Hi-ge-jou-bi-ca-chou-pou.
Vous avez ainsi les premières syllabes des 7 noms qui se terminent en "oux" au pluriel.
2e variante:
'''J'''e '''P'''eux '''B'''oire '''C'''omme '''C'''es '''G'''ros '''H'''ommes.
* Les noms terminés en « -al » font leur pluriel en « -aux » (sauf ''aval, bal, cal, carnaval, chacal, choral, festival, mistral, naval, pal, récital, régal''… qui font leur pluriel en « s ») :
::Dans mon pays '''natal'''
::Où les gens sont pourtant '''joviaux'''
::Eut lieu, c’était '''fatal''', un combat '''naval''',
::Heureusement, ce fut le combat '''final'''
::Parce qu’il faisait '''glacial'''.
== [[w:Grammaire|Langues étrangères]] ==
Ces langues nous sont étrangères, d’où l’importance de trouver des moyens mnémotechniques
=== [[w:Allemand|Allemand]] ===
==== Liste des particules verbales non détachables ====
''J’ai mis Cerbère en enfer'' : ge-, miss-, zer-, be-, er-, ent-, emp-, ver-
''Cerbère gémit en enfer'' : zer-, be-, er-, ge-, miss-, ent-, emp-, ver-
''Miss Verzer bégaie en panthère'' : miss-, ver-, zer-, be-, ge-, emp-, ent-, er-
==== Genre des mots ====
Les mots (de plus d'une syllabe) se terminant en -e, -ei, -ie, -heit, -keit, -tion, -ung sont féminins. <br />
Il existe bien sûr des exceptions : der Däne, das Genie, der Ursprung, der Hochsprung...
=== [[w:Anglais|Anglais]] ===
==== Mots contraires ou confondables ====
* Left: gauche / '''R'''ight: d'''r'''oite
** avec la''' main gauche''', on peut former un '''L''' en tenant les doigts en haut et le pouce en avant. C’est le '''L''' de '''L'''eft.
** dans l’alphabet le '''L''' est à '''gauche''' ('''L'''eft) et le '''R''' est à '''droite''' ('''R'''ight) :
**:A B C D E F G H I J K '''L''' M N O P Q '''R''' S T U V W X Y Z
** Copy'''right''' veut dire '''droit''' d'auteur.
** Quand on est a'''droit''', c’est bien (= '''right''' en anglais)
* Odd (3 lettres) : impair / Even (4 lettres) : pair
** Tuesday : mardi / Thu'''r'''sday : jeudi
*** Thu'''r'''sday est le quat'''r'''ième jour de la semaine, il a donc un '''r''' ('''quatrième''' lettre)
*** En classant les mots dans l’ordre lexicographique :
****jeudi (Thursday) est avant mardi (Tuesday),
****Thursday (jeudi) est avant Tuesday (mardi).
*** Je'''u'''di et Th'''u'''rsday ont tous les deux la lettre '''u''' en troisième position.
*** étymologiquement : Thursday = jour de '''Thor''', et jeudi = jour de '''Jupiter''' (Jovis die). Thor (mythologie nordique) et Jupiter (mythologie romaine) sont tous les deux ''dieu du tonnerre''. Même chose pour l'étymologie de Tuesday (jour de '''Tyr''') et de mardi (jour de '''Mars'''), tous les deux étant ''dieu de la guerre''. Mais il est plus difficile de retrouver Odin dans Tuesday.
***TUEsday sonne comme two-sday two étant égal au nombre 2 et mardi est le deuxième jour de la semaine.
=== [[w:Espagnol|Espagnol]] ===
==== [[w:Consonne|Consonnes]] doublées ====
Les seules consonnes que l’on peut trouver à l’écrit en double sont celles du mot CaRoLiNa.
On peut remarquer que le "[[w:LL|ll]]" est une consonne à part entière.
Attention ! Ne pas confondre N et Ñ
==== <u>Subjonctif</u> ====
===== verbe Savoir (saber) =====
si tu '''sé '''ton présent mais que tu ne '''sepa '''ton subjonctif ce n'est pas grave!
=== [[w:Japonais|Japonais]] ===
{{article détaillé|Japonais/Hiragana/Leçon 1}}
=== [[w:Néerlandais|Néerlandais]] ===
==== Liste des particules verbales non détachables ====
''begeherontverer' : be-, ge-, her-, ont-, ver-, er-''
BEnoit et Gerard ONT HERité du VERgER
==== Conjugaison de l’[[w:imparfait|imparfait]] ====
On forme l’[[w:imparfait|imparfait]] avec un '''t''' si le radical (Verbe -EN) du verbe se termine par F, K, P, S, T, CH.<br>
Retenez : '''F'''ran'''K'''lin '''p'''rend '''s'''on '''t'''hé '''ch'''aud. <br>
Si le radical se termine par une autre lettre, on forme l’imparfait avec un '''d'''.
Exemples : <br>
- pakken (''prendre'') : pak'''k'''-en > hij pak'''t'''e (''il prenait'')<br>
- ruilen (''échanger'') : rui'''l'''-en > hij ruil'''d'''e (''il échangeait'')
=== [[w:Russe|Russe]]===
==== Verbes à voyelle alternante ы/о dans le thème ====
'''К'''арл '''Р'''о'''М'''а'''Н'''о'''В'''
крыть « couvrir » - рыть « creuser » - мыть « laver » - ныть « gémir; faire mal » - выть « hurler »
Se fléchissent tous sur le modèle :
* infinitif : мыть (accent stable au passé: м'ыла)
* conjugaison : мóю, мóешь... мóют
Contrairement à ст'ыть, ст'ыну « refroidir » ; слыть, слывý « être réputé... »
== Littérature ==
=== Auteurs français du {{XVIIe siècle}} ===
''Sur une racine de la bruyère, une corneille boit l’eau de la fontaine Molière''
([[w:Jean Racine|Racine]], [[w:Jean de La Bruyère|Jean de La Bruyère]], [[w:Pierre Corneille|Pierre Corneille]], [[w:Nicolas Boileau|Nicolas Boileau]], [[w:Jean de La Fontaine|Jean de La Fontaine]], [[w:Molière|Molière]])
Variante : La Corneille perchée sur la Racine de La Bruyère, Boileau de La Fontaine Molière
''(Remarque : La fontaine Molière est une fontaine à Paris)''
== Théâtre ==
Face à la scène, le côté '''j'''ardin et le côté '''c'''our sont du côté de chaque initiale de '''J'''ésus '''C'''hrist, de '''J'''ules '''C'''ésar , de '''J'''acques '''C'''artier ou de '''J'''acques '''C'''hirac (J.C. : jardin à gauche, cour à droite)
Face au public, c’est l’inverse, et le côté '''cour''' est le côté du [[w:cœur|cœur]], à gauche.
== Musique ==
'''Ah ! Lala !'''
* Se souvenir de cette interjection pour faire correspondre les notes musicales latines (do, ré, mi fa...) avec les anglo-saxonnes (C, D, E, F...) '''A''' correspond à '''la''', il n'y a plus qu'à suivre B=si, C=do, D=ré, E=mi, F=fa, G=sol.
'''TS MS DSS'''
*''Nom des degrés'': '''t'''onique, '''s'''us-tonique, '''m'''édiante, '''s'''ous-dominante, '''d'''ominante, '''s'''us-dominante, '''s'''ensible
'''Sa mère la racaille ! Saleté de fumier !'''
*''Ordre des bémols'' : '''S'''i '''m'''i '''l'''a '''r'''é '''s'''ol '''d'''o '''f'''a
'''Six mille laquais repus songent au dodo, fatigués'''
*''Ordre des bémols'' : '''Si''' '''mi''' '''la''' '''ré''' '''so'''l '''do''' '''fa'''
'''Facteur, donne au soldat réjoui la missive'''
*''Ordre des dièses'' : '''Fa''' '''do''' '''sol''' '''ré''' '''la''' '''mi''' '''si'''
'''Dommage, la mine est cassée Do Majeur --> La mineur'''
*"Gamme relative" de Do Majeur
'''Il Doit Posséder Les Modes En Lui'''
Ionien, Dorien, Phrygien, Lydien, Mixolydien, Éolien, Locrien
== Géographie ==
=== Points cardinaux ===
==== Où est l’est ? ====
*Visualiser Strasbourg et Brest. Strasbourg est à l'est, Brest est à l'ouest
* Penser au mot '''O'''rang'''E''', sur une carte, l'Ouest est à gauche et l'Est à droite.
* Écrire "'''où est''' l''''est'''" --> ouest à gauche et est à droite (en considérant le nord en haut bien entendu)
* L’ouest est à gauche, l’est à droite (si le nord est au-dessus).
* Penser que si on regarde la France, à gauche c'est l' ''eau'' comme dans '''O'''uest et à droite c'est l' ''étranger'' comme dans '''E'''st.
* Penser qu'en France, on parle des "pays de l'est" (à droite sur la carte) et en parlant de la conquête de l'ouest on pense à l'Amérique (à gauche sur la carte)
* Penser à : Ouest, le suffixe "est" se trouve à droite, tout comme l’est. Ce qui signifie "ouest" à gauche et "est" à droite.
* Écrire ONE (1 en anglais) : Ouest-Nord-Est
* Penser au mot 'OiE': l’Ouest est à gauche comme le O et l’Est est à droite comme le E (si le Nord est en haut)
* Dans le mot ouest il y a un "u" comme dans gauche. Dans le mot est il n'y a pas de "u" comme dans droite.
* Retenir le mot NESO en tournant dans le sens des aiguilles d'une montre, car l'inverse donne la nausée (NOSE).
==== Le soleil se couche à l’est ou à l’ouest ? ====
* Penser qu’en France, on voit de beaux couchers de soleil sur nos côtes atlantiques, à l’ouest.
* Penser aussi au pays du soleil levant, le Japon, qui est bien à l’est du continent
* Ou encore : Le Soleil se l'''è'''ve à l’'''e'''st et se c'''ou'''che à l’'''ou'''est
=== Pays limitrophes de la France ===
Aime '''I''S''A''B''E''L''A''' (Aimer pour la lettre '''M''')
'''M'''onaco, '''I'''talie, '''S'''uisse, '''A'''llemagne, '''B'''elgique, '''E'''spagne, '''L'''uxembourg, '''A'''ndorre.
''MAL BAISÉ''
'''M'''onaco, '''A'''ndorre, '''L'''uxembourg, '''B'''elgique, '''A'''llemagne, '''I'''talie, '''S'''uisse, '''E'''spagne.
''AIMABLES''
'''A'''llemagne, '''I'''talie, '''M'''onaco, '''A'''ndorre, '''B'''elgique, '''L'''uxembourg, '''E'''spagne, '''S'''uisse
Avec l’océan Atlantique, la Manche et la Méditerranée en plus :
'' '''O'''h '''MA''' '''MER'''veilleuse '''BALISE''' ''
'''O'''h = '''O'''céan Atlantique '''MA'''='''MA'''nche '''MER'''= '''MER'''Méditerranée '''BALISE'''= '''B'''elgique '''A'''llemagne '''L'''uxembourg '''I'''talie '''S'''uisse '''E'''spagne
PS cette liste est valable seulement pour la France Métropolitaine car le pays avec lequel la France a la plus longue frontière est le ... Brésil ! (car la Guyane est un département français)
=== Grands lacs de l'Amérique du Nord ===
''SMHEOL'' (d'ouest en est)
'''S'''upérieur, '''M'''ichigan, '''H'''uron, '''E'''rié, '''O'''ntario (et St '''L'''aurent)
''HOLMES (élémentaire !)''
'''H'''uron, '''O'''ntario, St '''L'''aurent, '''M'''ichigan, '''E'''rié, '''S'''upérieur.
Une autre méthode, souvent enseignée dans les cours de géographie, fait appel au mot anglais ''foyers'' de la manière suivante :
''HOMES''
'''H'''uron, '''O'''ntario, '''M'''ichigan, '''E'''rié, '''S'''upérieur
=== Les 5 arrondissements de New-York (Boroughs) ===
Vous n'arrivez pas à vous souvenir des 5 arrondissements de la grande ville de New-York? En sachant qu'il est incontournable d'aller se promener dans les grandes avenues et rues sans s'arrêter dans un stand BBQ et y manger les bons hot-dogs d'un sympathique New-yorkais. Il faut dire: '''Si Man BBQ''' (Staten Island, Manhattan, Brooklyn, Bronx, Queen). Bon appétit, bonne visite!.
=== Pays baltes ===
Vous confondez les pays baltes sur la carte ? C'est tout simple, ils sont placés par ordre alphabétique du nord au sud... [[w:Estonie|Estonie]], [[w:Lettonie|Lettonie]], [[w:Lituanie|Lituanie]]
* Cela fonctionne aussi avec les appellations anglo-saxonnes : Estonia, Latvia, Lithuania et aussi avec les noms locaux : Eesti, Latvija, Lietuva.
* Pour les capitales de ces pays : Estonie [[w:Tallinn|Tallinn]], Lettonie [[w:Riga|Riga]] et Lituanie [[w:Vilnius|Vilnius]]
'''T'''rafic '''R'''outier '''V'''olumineux
=== Les tropiques ===
Les tropiques du Cancer et du Capricorne sont classés du nord au sud par ordre alphabétique.
Le capricorne coule (en bas) car il a plus de lettres, il est plus lourd. L'antarctique aussi : l'arctique flotte.
ou tropique du caNcer : N represente le Nord.
ou Capricorne sonne comme "Cap Horn" donc au Sud
=== Les pays d'Amérique Centrale ===
*Du nord au sud : [[w:Bélize|Bélize]], [[w:Guatemala|Guatemala]], [[w:Honduras|Honduras]], [[w:Salvador|Salvador]], [[w:Nicaragua|Nicaragua]], [[w:Costa Rica|Costa Rica]], [[w:Panama|Panama]].
BGHSNCP soit: '''B'''eau '''G'''arçon '''H'''abitant '''S'''alvador '''N'''ettoie et '''C'''i'''r'''e les '''P'''lanchers ou Belle Guatemalaise Habitant Salvador, Nage sur la Côte du Panama.
=== Fleuves de Russie ===
D'ouest en est, les initiales des cinq principaux fleuves de [[w:Russie|Russie]] forment le mot "VOILA" : [[w:Volga|Volga]], [[w:Ob|Ob]], [[w:Ienisseï|Ienisseï]], [[w:Léna|Léna]], [[w:Amour (fleuve)|Amour]].
L'Ob, le Ienissei et la Léna sont les trois plus grands cours d'eau de Sibérie.
=== Pays du Moyen-Orient ===
Le Qatar est une presQu'île dans la péninsule arabiQue. Le Qatar peut être vu comme comme une Crête de CoQ juchée sur l'Arabie saoudite et s'ouvrant sur le golfe arabo-persiQue.
=== Pays d'Asie Centrale (en -stan) ===
* Du Nord au Sud et de l'Ouest à l'Est
'''Kaz'''akhstan - '''Ou'''zbékistan - '''Ki'''rghizistan - '''Tu'''rkménistan - '''Ta'''djikistan - '''Af'''ghanistan - '''Pa'''kistan
Kaz Ou Ki Tu Ta Af Pa
Kazouki, tu taffes pas ?
=== Principales villes traversées par la Loire ===
* De l´Atlantique au Mont Gerbier de Jonc
'''Na'''thalie '''an'''goisse '''to'''ujours les '''bl'''ondes '''or'''iginaires de '''Nevers''', elles '''ro'''ugissent '''sa'''ns '''pu'''deur.
Nantes Angers Tours Blois Orléans Nevers Roanne Saint-Étienne Le Puy en Velay
== Histoire ==
=== Préhistoire ===
{{loupe|#Les périodes géologiques de l’ère primaire|# Les périodes géologiques de l’ère secondaire}}
==== Évolution des [[w:Homininae|homininés]] ====
Les '''Austral'''iens '''habil'''es eurent une '''érec'''tion, quand ils aperçurent, dans le '''néan'''t, des '''sapins''' gigantesques, <br /> ce qui donne, par ordre d'apparition<br />
[[w:Australopithèque|Australopithèque]], [[w:Homo habilis|Homo habilis]], [[w:Homo erectus|Homo erectus]], [[w:Homme de Néanderthal|Homme de Néanderthal]] et [[w:Homo Sapiens|Homo Sapiens]].
=== Les 7 Merveilles du monde antique ===
"'''Mostapha''' ! '''J’attends''' la '''copie''' !"
Variante: "'''Mostapha''' ! '''J’attends''' ta '''coloscopie''' !"
('''Mau'''solée d’Halicarnasse, '''Sta'''tue de Zeus à Olympie, '''Pha'''re d’Alexandrie, '''Ja'''rdins suspendus de Babylone, '''Tem'''ple d'Artémis à Éphèse, '''Co'''losse de Rhodes, '''Py'''ramides d’Égypte)
=== Les 7 rois de Rome ===
''Ronutuann' tarsertar'' (qu'on retient mieux en imaginant le paresseux boucher Ronu : "Ronu, tuant tard, sert tard")
('''Ro'''mulus, '''Nu'''ma Pompilius, '''Tu'''llus Hostilius, '''An'''cus Martius, '''Tar'''quin l’Ancien, '''Ser'''vius Tullius, '''Tar'''quin le Superbe)
=== Les 11 [[w:Liste des empereurs romains|premiers empereurs romains]], dans l’ordre de leur règne ===
''AuTiCaClauNéGalOViVesTiDo''
('''Au'''guste, '''Ti'''bère, '''Ca'''ligula, '''Clau'''de, '''Né'''ron, '''Gal'''ba, '''O'''thon, '''Vi'''tellus, '''Ves'''pasien, '''Ti'''tus, '''Do'''mitien)
Et les six suivants : ''NeTraHadAnMarCo''
(Nerva, Trajan, Hadrien, Antonin, Marc-Aurèle, Commode)
Cesautica
Clonegalo
Vivestido
CESar, AUguste, TIbere, CAligula
CLAUde, NEron, GALba, Othon
VItellus, VESpasien, TItus, Domitien
=== Les traités napoléoniens, dans l’ordre de leur signature ===
'''''CAV''''' (penser à une ''cave'') : ''Cambalu, Apresti, Viparis''
('''Cam'''po Formio, '''Bâ'''le, '''Lu'''néville, '''A'''miens, '''Pres'''bourg, '''Ti'''lsit, '''Vi'''enne, '''Paris''')
=== Les présidents de la troisième République française ===
''Thimagré Carcafauloufa Poindemidoudoule''
('''Thi'''ers, '''Ma'''c-Mahon, '''Gré'''vy, '''Car'''not, '''Ca'''simir-Perier, '''Fau'''re, '''Lou'''bet, '''Fa'''llières,
'''Poin'''caré, '''De'''schanel, '''Mi'''llerand, '''Dou'''mergue, '''Dou'''mer, '''Le'''brun)
OU ce petit poème :
Tire Mon Glaive
Car Casse-Pierre Fort
Loup Faillit Point
Dèche Mille Dômes
D'où Merle Brun
=== Les présidents de la cinquième République française ===
'''D'''es '''P'''illards '''G'''ouvernent, '''M'''ais '''C'''hacun '''S'''ubit '''H'''élas la Macronie
'''D'''ur '''P'''armesan '''G'''orgonzola '''M'''ozzarella '''Ch'''auds '''S'''ervis.
'''D'''ouce '''P'''atrie '''G'''auloise où '''M'''iaulent les '''Ch'''ats '''S'''iamois '''H'''eureux.
<math>\Longrightarrow</math>De Gaulle, Pompidou, Giscard d'Estaing, Mitterrand, Chirac, Sarkozy, Hollande.
'''D'''ouce '''P'''atrie '''G'''auloise où '''M'''iaulent les '''Ch'''ats '''S'''iamois '''H'''eureux et '''M'''alades.
<math>\Longrightarrow</math>De Gaulle, Pompidou, Giscard d'Estaing, Mitterrand, Chirac, Sarkozy, Hollande, Macron.
- Le sauveur de la France, Charles '''de Gaulle''', s’orthographie avec deux L. On peut retenir que de Gaulle a deux L, comme les deux barres de la croix de Lorraine, symbole de la France libre.
On retient que la Gaule n’a qu’un L comme le L unique dans Celtes. Car "les Gaulois" est le nom que Jules César donne aux Celtes.
=== Les présidents américains à partir de Roosevelt ===
'''R'''udy '''T'''ente '''E'''n '''K'''araté '''J'''e '''N'''ique '''F'''abienne '''C'''omme '''R'''udy '''B'''ouche '''C'''ontre '''B'''ouche '''O'''utré '''T'''errifié.
'''R'''oosevelt '''T'''rouva '''E'''léonore en '''K'''imono, '''J'''ames '''N'''e '''F'''ilma '''C'''arrément '''R'''ien, '''B'''rave '''C'''améraman '''B'''ouché et '''T'''êtu !
<math>\Longrightarrow</math>Roosevelt, Truman, Eisenhower, Kennedy, Johnson, Nixon, Ford, Carter, Reagan, Bush, Clinton, Bush, Obama, Trump.
=== Les dirigeants de l'URSS et de la Russie ===
'''L'''aissant '''S'''on '''K'''imono '''B'''leu '''À''' '''T'''rois '''G'''amins, '''E'''lle '''P'''ut '''M'''aintenir '''P'''outine.
L = Lénine, S = Staline, K = Khrouchtchev B = Brejnev, À = Andropov, T = Tchernenko, G = Gorbatchev.....la "virgule" marque la chute du communisme, et E = Eltsine, P = Poutine, M = Medvedev, P = Poutine
== Médecine ==
=== Plan des muscles complexus ===
1. Muscle semi-épineux de la tête
* Sème tranquillement 156 graines dans un carré de terre pour 71 arbres épineux.
(Le semi-épineux a pour origine les processus transverses de Th1 à TH5/Th6 et C4 à C7 et se termine sur les processus épineux de C7 ) th1)
2. Muscle longicissimus du cou
*Louons tranquillement une sainte tu (la) sauteras.
(Le muscle longicissimus du cou a pour origine les processus transverses de Th1 à Th5 et se termine sur les tubercules post de C3 à C7)
=== Les os du carpe ===
*''SSPP - TTCC'' (Initiales)
* ''Sca-Lu-Py-Pi T-T-Go-Oc'' (Phonétique)
<math>\Longrightarrow</math>('''Sca'''phoïde, (Semi-'''Lu'''naire)'''lu'''natum* , '''Py'''ramidal, '''Pi'''siforme - '''T'''rapèze, '''T'''rapézoïde, '''C'''apitatum ''', '''os '''C'''rochu ''')'''
NB : dans la nouvelle nomenclature ce n'est plus le semi-lunaire mais le LUNATUM
*On peut le voir sous un autre angle :
** PI - TRI - LU - SCA
*: (pisciforme) (triquetrum) (lunatum) (scaphoide)
** HA - CA - TRI - TRA
*: (hamatum) (capitatum) (trapézoide) (trapèze)
Ou encore prendre les consonnes de ces 2 mots :
*'''P'''é'''T'''a'''L'''e'''S''' : '''P'''isiforme - '''T'''riquetrum - '''L'''unatum - '''S'''caphoïde
* a'''TT'''a'''CH'''e : '''T'''rapèze - '''T'''rapézoïde - '''C'''apitatum - '''H'''amatum
*Trouvé par un étudiant :
*
** '''S'''a'''L'''e '''T'''e'''P'''u, '''<nowiki>T'</nowiki>'''é'''T'''ais à '''CH'''ier.
** '''S'''uce '''l'''a '''t'''rique '''P'''atrick, '''t'''u '''t'''ireras '''Ch'''arlotte
** Le '''S'''carabée à '''L'''unettes '''T'''rie ses '''P'''ièces, '''T'''out '''T'''as est un '''C'''apital ('''h''')Amassé"
** '''S'''a'''L'''u'''T''' '''P'''ierre, '''T'''<nowiki/>'é'''T'''ais '''C'''haud '''H'''ier.
=== Les muscles épicondyliens médiaux (ex-épitrochléens) du membre supérieur ===
''Grand Papa cuve et ronfle''
<math>\Longrightarrow</math>('''Grand pa'''lmaire, Petit '''pa'''lmaire, '''Cu'''bitus antérieur, '''Ron'''d pronateur, '''Flé'''chisseur commun superficiel)
Une autre phrase est proposée
"Grand Papa, Petit Papa, fléchit rondement le cul en avant"
<u>Avec la nouvelle nomenclature</u> :
''Paulo Fuck Les Filles Sans Défense Faisant Un CAprice''
''==> rond '''P'''ronateur, '''F'''léchisseur radial du carpe, '''L'''ong palmaire, '''F'''léchisseur '''S'''uperficiel des '''D'''oigts, '''Fl'''échisseur '''U'''lnaire du '''Ca'''rpe''
=== Les muscles épicondyliens latéraux (ex-épicondyliens) du membre supérieur ===
<math>\Longrightarrow</math>'''2'''ème '''Ra'''dial, '''Ext'''enseur '''commun''', '''ext'''enseur '''propre''' '''du 5'''ème doigt, '''Court Su'''pinateur, '''Cu'''bital '''Post'''érieur, '''Anconé'''
Deux rats excommuniés, expropriés du 5e ont cousu (court supinateur) le cul de la postière en cône.
<u>Avec la nouvelle nomenclature</u> :
''Charlie Rêve d'Explorer Des Etoiles, 5 Etoiles Uniques Au Sanctuaire''
''==>'' Court extenseur Radial du carpe, Extenseur commun des Doigts, Extenseur du 5ème doigt, Extenseur Ulnaire du carpe, Anconé, Supinateur
=== Les 12 paires de nerfs crâniens ===
==== Ancienne nomenclature ====
*'''''O'''h '''O'''scar, '''m'''a '''p'''etite '''t'''héière '''m'''e '''f'''ait '''à''' '''g'''rand '''p'''eine '''s'''ix '''g'''rogs''
*'''''O'''h '''O'''scar, '''m'''a '''p'''etite '''t'''hérèse '''m'''e '''f'''ait '''à''' '''g'''rand '''p'''eine '''s'''ix '''g'''osses''
<math>\Longrightarrow</math>'''O'''lfactifs, '''O'''ptiques, '''M'''oteur oculaire commun, '''p'''athétiques, '''T'''rijumeau, '''M'''oteur oculaire externe, '''F'''aciaux, '''A'''uditifs, '''G'''losso-pharyngiens, '''P'''neumogastriques, '''S'''pinaux, '''G'''rand hypoglosse.
* Nouvelle nomenclature
*'''''OL'''ivia '''OPT'''<nowiki>e pour l'</nowiki>'''OC'''éan c'est '''TRO'''p '''TRI'''<nowiki>ste d'</nowiki>'''A'''ller '''FA'''ire des '''V'''isites '''G'''avantes quand les '''VAGUES''' '''A'''<nowiki>pportent l'</nowiki>'''HYP'''nose.''
*'''O'''h '''O'''h ! '''O'''scar ! '''T'''a '''T'''héière '''A''' '''F'''ait '''V'''ingt '''G'''rands '''V'''erres '''A''' '''H'''ector.
*'' '''Ol'''ivier '''Op'''oil '''Ocul''' '''Troqu'''a '''Tri'''stement '''A'''vec '''Fa'''nny '''V'''ingt '''Gloss Par'''fums '''Va'''nille '''Accessoire'''<nowiki> d'</nowiki>'''Hy'''dratation.''
*'''''Ol'''é '''O'''<nowiki>scar d'</nowiki>'''Occ'''ident ! '''Tr'''availle ton '''Tri'''ceps, tes '''Abd'''o et tes '''F'''esses au '''WC'''. '''Gl'''isse '''vague'''ment ton '''accessoire''' et '''hip''' !''
*'''''O'''yez, '''O'''yez ! '''O'''bstinée '''T'''ortue '''T'''enace '''A''' '''F'''inalement '''V'''aincu, '''G'''rand '''V'''antard '''A''' '''H'''onte.''
*'''''O'''yé! '''O'''yé! '''O'''bstinée '''T'''ortue '''T'''enace '''A''' '''F'''inalement '''V'''aincu (la) '''G'''rande '''V'''ague '''À''' '''H'''awaii''.
*'' '''Ol'''af '''Opt'''a '''Occ'''asionellement pour le '''Tro'''quet '''T'''andis qu' '''Abd'''el '''Fa'''isait '''V'''alser '''G'''rand'''-P'''ère '''Vague'''ment '''A'''utour de l' '''Hippo'''campe. ''
*'''''O'''n '''O'''ccasion '''O'''livier '''T'''ries '''T'''o '''A'''nally '''F'''uck '''V'''arious '''G'''uys, '''V'''aginas '''A'''re '''H'''istory''.
*'''''Ol'''a! '''Op'''hélie '''O cul Tro'''p '''Tri'''pant '''A Fa'''it '''Co'''quettement '''Glo'''usser '''Va'''lentin '''A''' l'<nowiki/>'''Hypo'''drome.''
<math>\Longrightarrow</math>'''O'''lfactif, '''O'''ptique, '''O'''cculomoteur, '''T'''rochléaire, '''T'''rijumeau, '''A'''bducens, '''F'''acial, '''V'''estibulo-Cochléaire ''ou'' '''C'''ochléo-vestibulaire, '''G'''losso-Pharyngien, '''V'''ague, '''A'''ccessoire, '''H'''ypoglosse.
==== Sensitif ou moteur ====
- Un dernier pour savoir la '''composante de chaque nerfs''' :
Mots commençant par un '''S''' = sensitif, '''M''' = moteur, '''B''' = les deux (both). Ensuite les noms communs et les adjectifs sont parasympathiques (Money, brother, big, boobs).
*'''''S'''ome '''S'''ay '''M'''oney '''M'''atters, '''B'''ut '''M'''y '''B'''rother '''S'''ays : '''B'''ig '''B'''oobs '''M'''atter '''M'''ost.''
ex: Boobs = 10e nerf (Vague), B = sensitif et moteur, nom commun = parasympathique. Ceci correspond aux caractéristiques du nerf vague !
- Une autre plus rigolote et moins décente :
*'''''S'''eb '''S'''uces '''M'''oi '''M'''es '''D'''eux '''M'''amelons '''D'''e '''S'''ilicone '''D'''é-'''D'''é '''M'''e '''M'''anque''
'''S'''ahara '''S'''ablonneux (et) '''M'''er '''M'''orte, '''D'''eux '''M'''ondes '''D'''e '''S'''ilence (et) '''D'''éserts '''D'''e '''M'''ouvants '''M'''irages
'''S''' = sensitif,
'''M''' = moteur,
'''D'''= les deux.
Ainsi le 1{{er}} nerf crânien est sensitif.
Le 9{{e}} et le 10{{e}} sont à la fois sensitifs et moteurs, etc.
=== Les 15 collatérales de l’artère maxillaire ===
'''''T'''on '''m'''épris '''p'''eut '''a'''mener '''m'''a '''t'''empête '''p'''etite '''b'''iche '''t'''ant aimée. '''Un''' '''p'''etit '''c'''âlin '''p'''eut '''p'''ardonner''
<math>\Longrightarrow</math>('''T'''ympanique, '''M'''éningée moyenne, '''P'''etite méningée, '''A'''lvéolaire inférieure, '''M'''asseterine, '''T'''emporale profonde postérieure, '''P'''térygoïdienne, '''B'''uccale, '''T'''emporale profonde antérieure, '''A'''lvéolaire supérieure, '''In'''fra-orbitaire, '''P'''alatine descendante, du '''C'''anal ptérygoïdien, '''P'''térygo-palatine, '''P'''haryngienne)
(N.B. Non! l'artère pharyngienne est une branche de la carotide externe!)
les 15 branches dans l'ordre:
un '''T'''ic '''MENING'''é '''PE'''ut '''DE'''venir '''MA'''léfique, '''T'''andis qu'un '''BU'''bon '''TE'''rriblement '''AL'''gique '''P'''eu'''T''' être '''SOU'''lagé '''VI'''te '''PA'''r une '''PT'''yaline '''SP'''écifique
'''( T'''ympanique, '''MENINGE'''é moyenne, '''PE'''tite méningée, '''DE'''ntaire inférieure, '''MA'''ssétérine, '''T'''emporale profonde moyenne, '''BU'''ccale, '''TE'''mporale profonde antérieure, '''AL'''véolaire, '''PT'''érygoïdienne, '''SOU'''s orbitaire, '''VI'''dienne, '''PA'''latine descendante, '''PT'''érygopalatine, '''SP'''hénopalatine.)
=== Les branches de l'artère axillaire ===
'''TH'''éodore '''a''' '''m'''angé '''s'''on '''c'''a'''c'''a
<math>\Longrightarrow</math>'''TH'''oracique supérieure, '''A'''cromiothoracique, '''M'''ammaire Interne, '''S'''capulaire et les deux '''C'''irconflexes
Cette artère se trouve dans la région de l'aisselle et pas ailleurs.
=== Branche de l'artère carotide externe : ===
'''''T'''ous '''L'''es '''F'''rançais '''O'''nt '''A'''pplaudi le '''P'''résident '''M'''onsieur '''T'''hiers''.
<math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''L'''inguale, '''F'''aciale, '''O'''ccipitale, '''A'''uriculaire postérieure, '''P'''haryngienne ascendante, '''M'''axillaire interne, '''T'''emporale superficielle):
=== Collatérales de la carotide externe ===
'''''T'''ire '''l'''a '''f'''icelle, '''p'''ortier ! '''O'''uvre '''à''' '''t'''on '''m'''aître '''r'''apidement''
<math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''L'''inguale, '''F'''aciale, '''P'''haryngienne ascendante, '''O'''ccipitale, '''A'''uriculaire postérieure, '''T'''emporale superficielle, '''M'''axillaire, '''R'''ameau parotidien)
'''T'''ou'''s''' '''l'''es '''F'''rançais '''a'''cclament '''O'''bama '''p'''résident
<math>\Longrightarrow</math>('''T'''hyroïdienne '''s'''upérieure, '''L'''inguale, '''F'''aciale, '''A'''uriculaire postérieure, '''O'''ccipitale, '''P'''haryngienne ascendante)
'''S'''ome '''a'''ngry '''l'''ady '''f'''igured '''o'''ut '''p'''ost '''m'''enopausal '''s'''yndrom.
<math>\Longrightarrow</math>('''S'''uperior thyroidal, '''A'''scending pharyngeal, '''L'''ingual, '''F'''acial, '''O'''ccipital, '''P'''osterior auricular, '''M'''axillary, '''S'''uperficial temporal)
Ce dernier, bien qu'en anglais, a l'avantage d'indiquer les artères dans l'ordre ascendant.
'''T'''u '''P'''eux '''L'''a '''F'''ourrer '''O'''u l'''A''' '''M'''anger '''T'''oute.
<math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''P'''haryngée ascendante, '''L'''inguale, '''F'''aciale, '''O'''ccipitale, '''A'''uriculaire postérieure, '''M'''axillaire, '''T'''emporale superficielle)
Ce dernier est une variante québécoise à caractère sexuel qui a également l'avantage d'indiquer les artères dans l'ordre ascendant.
=== Les collatérales de l’artère ophtalmique ===
'''''R'''emets '''l'''es '''c'''apotes '''s'''ans '''n'''ous '''f'''aire '''p'''<nowiki>erdre l'</nowiki>'''é'''rection''
<math>\Longrightarrow</math>(Centrale de la '''R'''étine, '''L'''acrymales, '''C'''iliaires, '''S'''upra-orbitaire, '''N'''asales, '''F'''rontales, '''P'''alpébrales, '''E'''htmoïdales antérieures et postérieures)
=== Les rameaux du plexus lombaire ===
'''Il''' '''hyp'''notise '''il'''lico l''''igu'''ane '''gé'''ant et '''fé'''roce et lui '''c'''oupe '''l'''ittéralement '''la''' '''cuisse''' qui '''fai'''sait '''ob'''struction '''car''' elle '''l'omb'''rageait.
<math>\Longrightarrow</math>('''il'''io-'''hyp'''ogastrique; '''il'''io-'''ingu'''inal; '''gé'''nito-'''fé'''moral; '''c'''utané '''l'''atéral de '''la''' '''cuisse'''; '''fé'''moral; '''ob'''turateur; du muscle '''car'''ré des '''lombes''')
=== Les rameaux du plexus sacré ===
'''Si''' un '''gl'''acier '''sup'''er '''inf'''idèle est '''honteux''' d’avoir '''per'''foré le '''rect'''um et '''élev'''é '''l’anus''' d’un '''cu'''isinier '''po'''urtant '''c'''onsentant, un '''curé''' '''fe'''ra un '''ju'''gement '''im'''partial en '''oub'''liant l’'''in'''acceptable.
<math>\Longrightarrow</math>('''sci'''atique; '''gl'''utéal '''sup'''érieur et '''inf'''érieur; '''honteux'''; '''pir'''iforme; '''rect'''al supérieur ; '''élév'''ateur de '''l’anus'''; '''cu'''tané '''po'''stérieur de la '''c'''uisse; '''carré''' '''fé'''moral et '''ju'''meau '''in'''férieur; '''ob'''turateur '''in'''terne)
=== Les muscles du grand trochanter ===
Mon '''g'''ars, '''troqu'''ons: Le '''Petit''' '''Pierre''' et '''les jumeaux''' '''moyen'''nant '''fesses''' à '''obtur'''er.
<math>\Longrightarrow</math>('''G'''rand '''Troch'''anter: '''petit''' fessier; '''pir'''iforme; '''jumeaux''' supérieur et inférieur; '''moyen fess'''ier; '''obtur'''ateurs internes et externes)
=== Les muscles de la patte d'oie ===
*SA GRA - Tte ("ça gratte")
*# Sartorius (ex-Couturier)
*# Gracile
*# Tendineux
**CGT
**Sartre est grat et tendre
**ça gratte
=== Les ménisques du genou ===
'''''CI'''TR'''OE'''N''
(Le ménisque en forme de '''C''' est le ménisque '''I'''nterne ; le ménisque en forme de '''O''' est le ménisque '''E'''xterne)
=== Les obturateurs pelvi-trochantériens ===
Être '''ex-empt''' d''''im-pôt'''s
(L'obturateur '''ex'''terne s'insère à la face '''ant'''érieure de l'os coxal, l'obturateur '''in'''terne s'insère à la face '''po'''stérieure de l'os coxal)
== Mythologie ==
=== Les 3 Grâces ===
'''Aglaé''' offre une u'''sine''' à Na'''thalie''' (variante : Aglaé offre Rosine à Nathalie)
** Aglaé, Euphrosine et Thalie
=== Les 9 Muses ===
Voici l'astuce des étudiants en grec à l'école pour retenir les neuf muses :
'''''Cl'''ame, '''Eu'''gène, '''ta''' '''mél'''odie, '''terr'''ible '''air''' '''pol'''onais, o'''ura'''gan '''cal'''culé''
<math>\Longrightarrow</math>('''Cl'''io, '''Eu'''terpe, '''Tha'''lie, '''Mel'''pomène, '''Ter'''psichore, '''Ér'''ato, '''Pol'''ymnie, '''Ura'''nie, '''Cal'''liope)
Calliope porte une couronne d’or, Clio une couronne de laurier, Érato une couronne de myrtes et de roses, Euterpe une couronne de fleurs, Melpomène une couronne de pampre de vigne, Polymnie une couronne de perles, Terpsichore une couronne de guirlandes, Thalie une couronne de lierre, et Uranie une couronne d'étoiles
=== Les dieux grecs ===
''Hazah Phadhadah''
<math>\Longrightarrow</math>('''H'''éra, '''A'''phrodite, '''Z'''eus, '''A'''pollon, '''H'''éphaïstos, '''P'''oséidon, '''H'''ermès, '''A'''rtémis, '''D'''ionysos, '''H'''adès, '''A'''théna, '''D'''éméter, '''A'''rès, '''H'''estia)
Notons que la plupart des dieux grecs commencent par la lettre "A" ou "H".
{| class="wikitable"
|-
| Hestia || arrêta || d'aimer || Zeus<small>:</small>
|| possédée || <small>par</small> Dionysos<small>,</small>
|| <small>elle</small> erra || <small>dans</small> Athènes</small>,</small>
|| affolée<small>,</small> || <small>et fit</small> l'apologie || <small>de l'</small>art || effarant || d'Hermes || et d'Hadès
|-
| Hestia || Arès || Demeter || Zeus || Poséidon || Dionysos
|| Héra || Athéna || Aphrodite || Apollon || Artémis || Héphaïstos || Hermes || Hades
|}
=== Les dieux romains ===
'''''J'''eune '''v'''euve '''j'''oyeuse '''c'''herche '''v'''ieux '''b'''aron '''m'''ême '''m'''alade '''a'''fin '''d'''e '''v'''ivre '''m'''ieux'' '''p'''oint
<math>\Longrightarrow</math>('''J'''unon, '''V'''énus, '''J'''upiter, '''C'''érès, '''V'''ulcain, '''B'''acchus, '''M'''ercure, '''M'''inerve, '''A'''pollon, '''D'''iane, '''V'''esta, '''M'''ars, '''P'''luton)
== Religion ==
=== Les dimanches avant Pâques ===
Les petits protestants alsaciens (et allemands aussi, sans doute) apprenaient jadis le nom des dimanches qui précédaient Pâques grâce à la phrase : « '''I'''n '''R'''echter '''O'''rdnung '''L'''ehre '''J'''esu '''P'''assion », ce qui signifie : « Apprends dans le bon ordre la passion de Jésus ». On peut aussi dire : « '''I'''n '''R'''ektors '''O'''fen '''L'''iegen '''J'''unge '''P'''almen », ce qui veut dire : « Dans le poêle du recteur se trouvent de jeunes palmes (allusion à rameaux) ». Et ils retrouvaient : '''I'''nvocavit, '''R'''eminiscere, '''O'''culi, '''L'''aetare, '''J'''udica et '''P'''almarum (Palmsonntag - dimanche des Rameaux).
=== Péchés capitaux ===
Les initiales des [[w:sept péchés capitaux|sept péchés capitaux]] sont rassemblés dans le mot « Pô glacé ». ('''p'''aresse, '''o'''rgueil, '''g'''ourmandise, '''l'''uxure, '''a'''varice, '''c'''olère et '''e'''nvie)
** Autre expression pour retenir les 7 péchés capitaux : " CE GALOP ".
(Colère, Envie, Gourmandise, Avarice, Luxure, Orgueil, Paresse)
** L' '''ENV''' ie '''EN V'''eut, lorsqu'on lui demande de travailler, la '''P'''aresse se '''P'''ousse, l' '''O'''rgueil se pense plus haut ('''O''') que les autres, la '''G'''ourmandise mange du '''G'''ateau et du '''G'''ras, la '''L'''uxure va dans un hot-'''L''', l' '''A'''varice en '''A''', la '''C'''olère '''C'''rie.
**Ou encore une phrase: '''L'''es '''G'''rands '''E'''sprits '''O'''bligent '''C'''ertainement '''A''' '''P'''enser (luxure, gourmandise, envie, orgueil, colère, avarice, paresse)
(entendu cité par Jacques Lacarrière lors d'une interview sur France-Culture)' '
**Une phrase courte et très facile à mémoriser, car elle ne fait pas appel qu'aux initiales qui demanderaient encore beaucoup d'efforts pour les identifier. Ici, l'utilisation de mots qui agissent immédiatement sur la divulgation des 7 péchés: ''''<nowiki>Par goût, Colette envie l'orgue luxueux d'Avarice'</nowiki>''' ('''Par'''esse, '''gou'''rmandise, '''colè'''re, '''envie''', '''orgue'''il, '''lux'''ure, '''avarice''')
=== Épîtres de Paul ===
Rococo Galéphicol ThèThè TimTim TiPhilHé
('''Ro'''mains, 1 '''Co'''rinthiens, 2 '''Co'''rinthiens, '''Gal'''ates, '''Éph'''esiens,
'''Phi'''lippiens, '''Col'''ossiens, 1 '''The'''ssaloniciens,
2 '''The'''ssaloniciens, 1 '''Tim'''othée, 2 '''Tim'''othée, '''Ti'''te, '''Phil'''émon, '''Hé'''breux)
=== Apôtres de Jésus ===
'''S'''ouvent '''a'''bsent, '''J'''ean '''J'''aures '''p'''erdait '''b'''êtement '''t'''oute '''m'''onnaie le '''j'''our '''J''' de '''s'''on '''j'''eûne.
'''S'''imon, '''A'''ndré, '''J'''acques, '''J'''ean, '''P'''hilippe, '''B'''arthélémy, '''T'''homas, '''M'''atthieu, '''J'''acques, '''J'''ude, '''S'''imon, '''J'''udas.
'''Si JaJa Ma embarté Juju, J'enfile To Pierre'''
'''Si'''mon, '''Ja'''cques, '''Ja'''cques, '''Ma'''tthieu, '''An'''dré, '''Barthé'''lémy, '''Ju'''de, '''Ju'''das, '''Jean''', '''Phil'''ippe, '''Tho'''mas, '''Pierre'''.
=== '''Vertus Cardinales''' ===
'''P'''rudence, '''J'''ustice, '''F'''orce et '''T'''empérance se retient avec : " '''P'''our '''J'''ésus, '''F'''ais '''T'''out ! ", les premières lettres des mots rappellent les quatre vertus cardinales.
=== Les sept sacrements catholiques ===
''BECOROM'' :
# Baptême
# Eucharistie
# Confirmation
# Onction des malades
# Réconciliation
# Ordination
# Mariage
=== Les sept dons de l'Esprit Saint ===
''P C DS FCC (paie ces déesses fichier central des chèques)''
# Sagesse
# Discernement
# Conseil
# Force
# Connaissance
# Crainte du Seigneur
# Piété
== Navigation ==
=== Marine ===
==== Bâbord/Tribord ====
'''''Bâ'''bord c’est g'''a'''uche, t'''r'''ibord, c’est d'''r'''oite.'' (si on regarde vers la partie avant du bateau)
On peut aussi le retenir avec le mot batterie (que l’on prononce généralement « BaTri »), on a Ba à gauche (comme bâbord) et Tri à droite (comme tribord) OU plus simple avec "BaTeau".
{| class="wikitable"
| Bâ
| Tri
|-
| Bâbord
| Tribord
|-
| Gauche
| Droite
|}
Variante : la seconde lettre de b'''â'''bord est la même que la seconde lettre de g'''a'''uche et la seconde lettre de t'''r'''ibord est la même que la seconde lettre de d'''r'''oite.
Variante : Babo<s>rd ga</s>uche = Babouche.
On ne retrouve qu'un A dans bAbord et gAuche et un I dans trIbord et droIte.
==== Couleur et signalisation bâbord et tribord ====
Couleur des feux de navigation d'un bateau:
{|
| Bâbord
| '''R'''ouge
|-
| Tribord
| '''V'''ert
|}
Lu de gauche à droite cela fait '''RV''' ou le prénom '''Hervé'''.
''Un marin emporte toujours Un Tricot Vert et Deux Bas Si Rouges'' = chiffres '''impairs''', '''Tri'''bord, '''Cô'''ne, '''Vert''', et chiffres '''pairs''', '''Bâ'''bord, '''Cy'''lindre, '''Rouge'''.
Ou encore : en entrant au port, on trouve les balises COniques VERTES à TRIbord, et les balise CYlindrique ROUGES à BAbord.
En [[w:aviron|aviron]], '''T.G.V''' à savoir '''T'''ribord-'''G'''auche-'''Vert''' car le rameur est dos à l'embarcation et donc tribord se situe à sa gauche.
==== Compartimentage ====
Le compartimentage est la méthode employée pour assurer la sécurité d'un navire contre les voies d'eau. Il consiste à diviser l'espace en compartiments étanches en-dessous de la ligne de flottaison. Il résulte de ces nombreux cloisonnements une difficulté à localiser, notamment sur les gros bâtiments, tel ou tel lieu précis (cabine, soute, etc.). Afin de régler cette difficulté se pratique un référencement des locaux selon une numérotation en quatres chiffres/lettres : la tranche (de la proue vers la poupe), le pont (de la surface vers le fond à partir du pont principal 0 situé immédiatement au-dessus de la ligne de flottaison), le rang (sous-section de tranche) et le bord (local dans un rang donné).
Ex. : A216 signifie que le local se trouve dans la tranche alpha, au niveau du deuxième pont inférieur (ou faux pont), au premier rang, premier local à bâbord en partant de la ligne médiane (ou de la coursive centrale) du navire.
La mémorisation de l'ordre des bords s'effctue avec la phrase mnémotechnique suivante : "j'aime les femmes en slip", le nombre de lettres de ces mots donnant la suite 1-5-3 (impairs sur tribord) 6-2-4 (pairs sur bâbord).
==== Nœuds ====
La phrase « ''Le serpent sort du trou, tourne autour de l'arbre, et rentre dans le trou'' » permet de se rappeler de la méthode pour faire un [[w:nœud de chaise|nœud de chaise]].
=== Aviation ===
Chez les pilotes d’avions utilisant le système [[w:Precision_Approach_Path_Indicator|Vasi]] pour trouver l’altitude correcte à l’atterrissage en [[w:vol à vue|vol à vue]], des panneaux rouges et blancs à côté de la piste leur fournissent de précieuses indications qui ont abouti à cette comptine :
:« ''White over white, you're high as a kite''
:''Red over white, you're right''
:''Red over red, you're dead'' »
(Blanc sur blanc : trop haut, rouge sur blanc : correct, rouge sur rouge : trop bas).
Pour récolter toutes les informations nécessaires pour compléter leur feuille de route, les pilotes aguerris utilisent la phrase :
'''''R'''''etranchez '''v'''otre '''d'''érive, '''c'''ela '''v'''ous '''d'''onne '''c'''haque '''m'''esure '''d'''u '''c'''ap '''c'''ompas.
Route vraie - X (dérive) / Cap vrai - Déclinaison / Cap magnétique - déviation / Cap compas
Pour le décollage : CC PP VV TT (ou 3T)
Compas, Conservateur de cap (ou gyro compas), Phares, Pompe, Volets, Verrière, [[w:Transpondeur|Transpondeur]], Top, (Talons au sol). Cela dépend bien sûr du type d'avion.
Pour l’observateur au sol et face à l’avion, contrôleur, mécanicien de piste, le rouge est à droite et le vert est à gauche. Très pratique la nuit, on ne voit rien d’autre que les feux.
Moyen mnémotechnique :
Quand on se sert du vin à boire, on a le "rouge" dans la main droite et le "verre" dans la main gauche.
Ou plus facile à retenir : Comme en politique, le rouge est toujours à gauche !
== Vie quotidienne ==
La vie quotidienne regorge de moyens mnémotechniques plus ou moins utiles.
=== Droite et Gauche ===
Se souvenir de sa main habile (droitier ou gaucher)
Utiliser les initiales en majuscules (G et D) : l'arrondi est du côté correspondant (à Gauche pour G), (à droite pour D).
En anglais, pour Left et Right (Gauche et Droite). On peut faire un L avec la main gauche, avec le pouce et l'index et non avec la main droite. D'où Left=gauche et right=droite.
=== Mois courts et mois longs ===
En mettant ses poings fermés côte à côte, les bosses des [[w:phalange|phalange]]s peuvent correspondre aux mois de 31 jours du [[w:calendrier|calendrier]] et les creux entre chacune aux mois de 30 jours ou moins. Et sans compter la jonction entre les mains comme un creux.
On peut aussi le faire avec une seule main (c'est mieux pour les enfants, car ça permet de suivre le décompte des mois avec un doigt de l'autre main...) : on commence sur le premier sommet pour janvier, on s'arrête sur le dernier sommet pour juillet et, pour les mois suivants, on repart en arrière en comptant de nouveau le sommet (ou bien, on recommence au sommet initial, pour bien marquer les 2 mois de 31 jours). Attention ! Juillet-Aout ont 31 Jours mais Décembre-Janvier aussi !!
=== Heure d’été, heure d’hiver ===
En été on avance d’une heure, car "'''é'''té" et "'''a'''vance" commencent par une voyelle.
En été le soleil qui passe à travers les volets nous réveille tôt car on dort une heure en moins.
En hiver on recule d’une heure, car "'''h'''iver" et "'''r'''ecule" commencent par une consonne.
En hiver on hiberne, donc on dort une heure en plus.
Le changement d’heure se faisant en avril (!) et en octobre :
* OCTOBRE finit par RE donc on REcule
* ''AVRIL commence par AV donc on AVance''
Hélas..... cette information est affichée sur de très nombreux sites.. alors que le changement annoncé en AVRIL.. est en réalité réalisé le dernier dimanche de MARS !
Comme le changement a lieu en MARS et OCTOBRE, et qu'en général on ne peut pas (ou ne doit pas) faire reculer une aiguille sur une montre analogique :
* MARS étant plus court (4 lettres), on avance de 1 heure,
* OCTOBRE étant plus long (7 lettres), on avance de 23 heures (ou 11 heures pour les montres et horloges analogiques sans date).
Pour les anglophones : Spring Forward, Fall Back
(Spring étant le Printemps et Fall l'Automne)...
=== Retrouver de tête le nom du jour de la semaine quelle que soit la date donnée ===
{{pas clair}}
Par convention, on associe les chiffres aux lettres suivantes :
<pre>
0 = S, Z
1 = T, D
2 = N, Gn
3 = M
4 = R
5 = L, Y, ill
6 = CH, J, Ge
7 = K, Qu, Gu
8 = F, Ph, V
9 = P, B
</pre>
==== N°1 - liens entre jours et lettres : ====
Lundi est le premier jour de la semaine . Donc '''Lundi = 1''' mardi = 2 ...
==== N°2 - liens entre mois et lettres ====
===== a) correspondances pour année normale =====
:
** Janvier Février Mars... deviennent
"'''S'''a'''M''' '''M'''e '''J'''e'''T'''e'''R'''a a'''G'''e'''N'''ou'''iLL'''é '''S'''on '''M'''a'''iLL'''ot"
'''S''' = Janvier ; '''M''' = Février ; '''M''' = Mars
'''J''' = Avril ; '''T''' = Mai ; '''R''' = Juin
'''g'''= Juillet ; '''n'''= Août ; '''L'''= Septembre
'''s'''= Octobre ; '''m'''= Novembre ; '''L'''= Décembre
===== b) correspondances pour année bissextile =====
"'''G'''i'''N'''o '''M'''e '''J'''e'''T'''e'''R'''a ..."
'''G''' = Janvier ; '''N''' = Février ; etc
===== N°3 - facteur en fonction du siècle =====
====== a) Avant le 4 octobre 1582, enlever 7 au siècle (''' -7''' ) ======
Année 670 : siècle 6 '''-7''' = 0
Année 1100 : siècle 11 '''-7''' = 4
Et le résultat on le transforme :
4 devient 0 , 3 devient 1 , 5 devient 6 , 2 reste 2 et inversement.
Moyen mnémotechnique (suivant la convention) :
RuSé MaTou :: 4-0 3-1
NoNNe LouChe :: 2-2 5-6
====== b) À compter du 4 Octobre 1582 ======
soit le '''4''' - '''10''' - '''1582''' , c'est à dire le jour où le '''R'''oi '''T'''hé'''S'''ée '''T'''é'''L'''é'''PH'''o'''N'''a , on enlève 4 et non 7 , autant de multiple possible :
**Année 1800 : siècle 18 - ( '''4'''x4 ) = 2
**Année 2000 : siècle 20 - ( '''4'''x5 ) = 0
La transformation donne 0-6 1-4 2-2 3-0
{| class="wikitable"
|-
| 0 || 1 || 2 || 3
|-
| '''6''' || '''4''' || '''2'''|| '''0'''
|-
| '''CH'''è... || ...'''r'''e || '''N'''iai... || '''se'''
|-
| 0-6 || 4-1 || 2-2 || 0-3
|}
==== Calcul pour le 26 septembre 1955 ====
Il y a plusieurs opérations à réaliser en se conformant aux règles citées .
**26 Sept 1955
règle 2a ::
septembre = L = ''5''
**26+''5'' = 31
règle 3b
** 31-(7x4) = '''''3'''''
**55 - (7x4) = 55-28= 27
**27 + (27/4) = 27+6 = 33
**33 - (7x4 ) = 33-28= '''''5'''''
** '''''5 + 3 ''''' =''''' 8'''''
comme il y a 7 jours dans la semaine :
*''''' 8-7 '''''= '''1'''
le 1 correspond à '''lundi''' donc le 26 septembre 1955 était un '''lundi''', toute la journée !!!
==== Sinon pour l'année cours, passée ou à venir, ====
le moyen le plus simple et le plus rapide est de diviser l'année en trimestres . Pour chaque mois, on cherche le quantième du premier dimanche par exemple .
On fait une phrase par trimestre et pour trouver la correspondance on rajoute 7 + de 1 à 6 .
Exemple, pour 2012,
{| class="wikitable"
|-
! Janvier !! Février !! Mars
|-
| '''1''' || '''5''' || '''4'''
|-
| '''D'''e || '''<nowiki>L'</nowiki>''' || ai'''R'''
|}
Donc, le premier dimanche de Janvier 2012 est le 1er janvier . Pour aller à mon Rendez vous du 3 je rajoute 2.
** Selon ma convention déjà vue , lundi=1 mardi=2 mercredi=3 jeudi=4 vendredi=5 samedi=6 et dimanche=7 .
Alors 2 correspond à mardi donc le 3 Janvier 2012 est mardi
pour aller au 18 janvier 2012 , une semaine ayant 7 jours, même sous le règne actuel, je retire autant de semaine complète que possible . Donc 18 - (2x7 ) = 18 - 14 = 4 . Surprise, le 18 janvier 2012 sera un mercredi . Soit parce que j'ai rajouté 3 jours au dimanche, soit parce que j'ai décidé une fois pour toutes que le dimanche est le premier jour de la semaine, donc le mardi le 2ème etc ...
** dans ce cas, dimanche=1 mardi=2 mercredi=4 jeudi=5 vendredi=6 samedi=7 dimanche=8-7=1
C'est selon sa préférence intellectuelle .
Je vous laisse continuer pour février et les autres trimestres.
{| class="wikitable"
|-
! janvier !! février !! mars !! avril !! mai !! juin !! juillet !! aout !! sept !! oct !! nov !! déc
|-
| 1 || 5 || 4 || 1 || 6 || 3 || 1 || 5 || 2 || 7 || 4 || 2
|-
| D || l || r || t || ch || m || d || l || n || k || r || g
|-
| de || l' || air || tu || chô || me || dans || la || nuit || qui || rè || gne
|}
=== Valeur d'un Euro en [[w:Franc français|Francs français]] ===
Selon le même principe que pour les décimales de ''π'' :
{| border="0" cellpadding="0" cellspacing="1"
|align="center"|''Chacun''
|
|align="center"|''saura''
|
|align="center"|''enfin''
|
|align="center"|''convertir''
|
|align="center"|''notre''
|
|align="center"|''monnaie''
|-
!align="center"|6
!align="center"|,
!align="center"|5
|
!align="center"|5
|
!align="center"|9
|
!align="center"|5
|
!align="center"|7
|}
=== Morse ===
Le code morse est facilement mémorisable à l’aide des codes courts et longs remplacés par des syllabes.
Le code long (-) remplacé par une syllabe en "O".
Le code court (.) remplacé par une des autres voyelles.
Ex : A = .- = Au/tO (une syllabe en A pour le . et une syllabe en O pour le -)
La liste complète est [[w:Alphabet Morse#Tableau Mn.C3.A9motechnique|Ici]]
=== Les vins ===
==== AOC de la côte de Nuits ====
Un mnémonique permettant de se rappeler des [[wikipedia:AOC|AOC]] communales de la [[wikipedia:Côte de Nuits|Côte de Nuits]], et dans l'ordre géographique en plus, par Paul Brunet, auteur du livre ''Le vin et les vins au restaurant'' : ''Messieurs, faites gaffe, mon chat vous voit noir'' (Marsannay, Fixin, Gevrey-Chambertin, Morey-Saint-Denis, Chambolle-Musigny, Vougeot, Vosne-Romanée, Nuits-Saint-Georges).
==== Nom des bouteilles de vin ====
Ce moyen mnémotechnique permet de mémoriser les principales tailles de [[w:bouteille de vin|bouteilles]] dans l'ordre croissant de contenance :
* Car de bon matin je remarquais mal sa banalité naturelle (quart, demi, bouteille, magnum, jéroboam, réhoboam, mathusalem, salmanazar, balthazar, nabuchodonosor).
Autre phrase mnémo, plus complète :
* PICARD FIT : DE BON MATIN, JE REMARQUE SA BANALITÉ, SA MATIERE SI PAUVRE.
Pour se rappeler tous les contenants de vin ou de champagne :
* PIccolo, QUARt, FIllette, DEmi-bouteille, Bouteille, MAgnum, Jeroboam, REhoboam, Mathusalem, SAlmanazar, Balthazar, Nabuchodonosor, SAlomon, Melchisédech, Souverain, Primat.
=== <u>Dresser une table</u> ===
Pour se souvenir d'où mettre la fourchette et le couteau :
Four'''<u>ch</u>'''ette à gau'''<u>ch</u>'''e, couteau à droite.
ou encore : A, B, '''C''', '''D''' ... '''C'''outeau à '''D'''roite. E, '''F''', '''G''', H ... '''F'''ourchette à '''G'''auche
== Cinéma, Bande dessinée... ==
=== Dupond et Dupont ===
Pour différencier les deux [[w:Dupond et Dupont|dupondt]] de la bande dessinée [[w:Les Aventures de Tintin et Milou|Les Aventures de Tintin et Milou]], celui à la moustache tombante comme un D est Dupond, celui à la moustache pointue comme les barres du T est Dupont.
=== Les 7 nains ===
'''A''' '''J'''ouer '''P'''resque '''S'''eul '''T'''u '''D'''eviens '''G'''rincheux
* Atchoum, Joyeux, Prof, Simplet, Timide, Dormeur, Grincheux : les 7 nains dans Blanche Neige...
=== Les Simpson ===
Pour différencier les deux sœurs de Marge, on observe les cheveux. Patty n'a pas de raie au centre et Selma a les cheveux découpés en deux blocs par une raie ou on regarde les boucles d'oreilles qui sont différentes
Sinon, regarder les boucles d'oreilles de Patty, elles sont triangulaires (P comme Pythagore).
== Sport ==
=== Escalade ===
''Pour la prochaine longueur je reste en bas, donc je me vache au plus bas.'' <br>
:En escalade, permet de savoir sur quel point d'assurage se vacher lors du relais '''réversible''' uniquement.
== Voir aussi ==
=== Article connexe ===
* [[w:Code chiffres-sons|Code chiffres-sons]]
=== Liens externes ===
* [https://jeretiens.net -JeRetiens.net- Libre recueil ayant pour objectif de rassembler tous les trucs et astuces mnémotechniques pour retenir et apprendre plus facilement.]
* [http://www.finallyover.com/categorie-1079667.html Méthodes thématiques de mémorisation]
* [http://www.echolalie.org/wiki/index.php?ListeMnemotechnique Liste mnémotechnique]
* [http://trucsmaths.free.fr/Pi.htm#poeme Le nombre pi]
* [http://www.francaisfacile.com/exercices/exercice-francais-2/exercice-francais-75628.php francaisfacile.com]
=== Références ===
<references />
[[Catégorie:minilivres]]
tsjhah5ny9fm0amm40ii63hth308q94
Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading
0
65961
763257
755756
2026-04-08T14:17:35Z
Mewtow
31375
763257
wikitext
text/x-wiki
Vous pensez surement qu'il faut obligatoirement plusieurs cœurs pour exécuter plusieurs programmes en parallèle, mais sachez que c'est faux ! Les processeurs mono-cœur en sont capables, en alternant entre les programmes à exécuter. Plusieurs programmes s’exécutent donc sur le même processeur, mais chacun à leur tour et non en même temps. D'ordinaire, cette alternance est gérée par le système d'exploitation, mais certains processeurs gèrent cette alternance eux-mêmes, directement au niveau matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, le processeur donne la main à un autre programme. Les processeurs en question sont appelés des processeurs multithreadés, ou encore des '''architectures multithreadées''', en référence au terme ''thread'', qui est plus ou moins équivalent à celui de programme dans ce cours. Ils exécutent un ''thread'' à la fois, mais changent de ''thread'' plus ou moins régulièrement.
Pour comprendre le pourquoi des architectures multithreadées, il faut rappeler qu'il arrive que l'unité de calcul d'un processeur ne fasse rien, par exemple pendant que le processeur accède à la mémoire. Les cycles d'horloge où l'unité de calcul est inutilisée sont des '''cycles gâchés'''. L’exécution dans le désordre réduit ces cycles gâchés, mais les architectures multithreadées sont une solution alternative et complémentaire. Elles visent à ce que les cycles gâchés d'un ''thread'' soient remplis par les calculs d'autre ''thread''. Pour cela, le reste du processeur subit des changements liés à la présence de plusieurs ''threads'' en cours d'exécution. Notamment, il y a deux changements qui sont systématiquement présents sur tous les processeurs avec ''multithreading'' matériel.
Premier changement : le processeur doit savoir à quel ''thread'' appartient chaque instruction dans son pipeline. Pour cela, il attribue à chaque ''thread'' un '''identifiant de ''thread''''', aussi appelé ''thread ID''. Il n'est qu'un numéro qui précise le ''thread'' en question. Il y en autant que de ''threads'' exécutables simultanément par le processeur. Par exemple, si le processeur gère au maximum 8 ''threads'' simultanés, l'identifiant de ''thread'' va de 0 à 7 et est codé sur 3 bits.
Second changement : il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement''', dont le fonctionnement dépend du processeur, comme nous le verrons plus bas.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Le ''Simultaneous MultiThreading'' est spécifique aux processeurs superscalaires, alors que les autres techniques fonctionnent sur tous les processeurs, qu'ils soient superscalaire ou simple-émission.
: Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''.
==Le multithreading temporel==
Le FGMT et le CGMT sont regroupés sous le terme de '''multithreading temporel'''. Les deux partagent en effet un même point commun : à chaque cycle, les instructions émises par l'unité d'émission proviennent d'un seul programme. La distinction n'a de sens que sur les processeurs à exécution multiple, ce sera plus clair dans la suite du chapitre quand on comparera ''multithreading'' temporel et SMT. La différence entre FGMT et CGMT est la fréquence des changements de ''thread'' : à chaque cycle ou presque pour le FGMT, lors d'un évènement bien précis pour le CGMT.
Les processeurs à ''multithreading'' temporel sont généralement des processeurs sans exécution dans le désordre, et n'ont donc pas de renommage de registres. Avec eux, il est obligatoire de dupliquer les registres pour que chaque programme ait son ensemble de registres architecturaux rien qu'à lui. Cela demande soit un banc de registre par programme, soit un banc de registre commun géré par fenêtrage de registre (chaque programme ayant sa propre fenêtre de registres).
Il faut aussi dupliquer les ''load-store queue'', pour séparer les lectures/écritures en attente de chaque ''thread''. Là encore, il est possible d'utiliser une ''load-store queue'' unique dans laquelle on ajoute des informations pour savoir à quel ''thread'' appartient telle ou telle lecture/écriture.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2.5|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''Coarse Grained Multithreading''===
[[File:Coarse Grained Multithreading.png|vignette|upright=1|Coarse Grained Multithreading.]]
Le '''Coarse Grained Multithreading''' change de programme quand un évènement bien précis a lieu. L'évènement en question fait que l'ALU restera inutilisé pendant un moment : accès à la mémoire, branchements, etc.
Sur certains processeurs CGMT, il y a une instruction précise pour changer de ''thread''. La commutation de ''thread'' est alors totalement ou partiellement décidée par le logiciel. Mais il s'agit là d'un cas particulier.
Le cas le plus fréquent est de changer de ''thread'' lors d'un défaut de cache. Vu que l'accès à la RAM est quelque chose de très lent, il est intéressant d'exécuter des instructions d'un autre ''thread'' pour recouvrir l'accès à la RAM. L'idée est similaire à ce qu'on a avec les lectures non-bloquantes et/ou l'exécution dans le désordre : pendant que l'unité d'accès mémoire gère le défaut de cache, on alimente l'unité de calcul avec des calculs indépendants. Sauf qu'avec le ''multithreading'', les calculs proviennent d'un autre ''thread''.
Un point important est que le cache doit être un cache non-bloquant, sans quoi le ''multithreading'' matériel ne fonctionne tout simplement pas. Par exemple, prenons une architecture qui change de ''thread'' à chaque défaut de cache. Si on veut supporter plus de deux ''threads'', il faut que plusieurs ''threads'' subissent un défaut de cache pour que le ''multithreading'' ait de l'intérêt, ce qui implique un cache non-bloquant.
[[File:Multithreading et mitigation de la latence mémoire.png|centre|vignette|upright=1.5|Multithreading et mitigation de la latence mémoire.]]
Avec le CGMT, on est certain que toutes les instructions en cours d’exécution appartiennent au même ''thread''. Quand il passe d'un ''thread'' à l'autre, le processeur attend naturellement que le pipeline soit complétement vidé avant de charger les instructions du ''thread'' suivant. Le processeur peut donc se débrouiller avec un simple '''registre de ''thread''''' qui mémorise le ''thread ID'' du ''thread'' en cours d'exécution. Le registre de ''thread'' est directement connecté à l'entrée d'adresse du banc de registre, pour gérer le fenêtrage de registres. Il est aussi connecté à la ''load-store queue'', et surtout au multiplexeur de choix de ''thread'', dans l'unité de chargement.
[[File:Implementation du CGMT.png|centre|vignette|upright=2|Implémentation du CGMT]]
L'unité d'ordonnancement détermine quel ''thread'' charger dans le pipeline, elle sélectionne un ''thread ID''. Pour faciliter son travail, cette unité contient un registre qui mémorise quels sont les ''threads'' actifs. Les ''threads'' bloqués par un défaut de cache sont marqués comme inactifs tant que le défaut de cache n'est pas résolu. Évidemment, dès qu'un défaut de cache est résolu, l'unité d'accès mémoire prévient l'unité de choix de ''thread'' pour que celui-ci marque le ''thread'' adéquat comme de nouveau actif. Le registre est composé de N bits sur un processeur qui gère N threads maximum : chaque bit est associé à un ''thread'' et indique s'il est actif ou non.
===Le ''Fine Grained MultiThreading''===
[[File:Fine Grained Multithreading.png|vignette|upright=2|''Barrel processor''.]]
Le '''''Fine Grained Multithreading''''' regroupe deux types de processeurs différents. Le premier type est celui des '''''barrel processors''''', qui changent de programme à chaque cycle d'horloge. Idéalement, chaque étage du pipeline est utilisé par une instruction différente. L'avantage est que, à l'intérieur d'un ''thread'', une nouvelle instruction démarre quand la précédente est terminée. Le résultat d'une instruction est déjà dans les registres quand l'instruction suivante s’exécute. Il n'y a donc pas de dépendances entre instructions successives, les circuits liés aux dépendances de données sont fortement simplifiés, le réseau de contournement de l'ALU disparait, l'unité de prédiction de branchement disparait (car le résultat d'un branchement est connu avant que le même ''thread'' exécute sa prochaine instruction).
Mais les accès mémoire sont gérés à part des autres instructions. Lorsque un ''thread'' effectue un accès mémoire, il est mis en pause et d'autres ''threads'' sont lancés à sa place. Prenons l'exemple d'un processeur qui gère 16 ''threads'' maximum : si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Mais pour que cela fonctionne sans encombre, on est obligé d'avoir un nombre assez élevé de programmes en cours d’exécution. Pour donner un exemple, le processeur MTA était capable d'exécuter un 128 ''threads'' maximum, pour un pipeline de 21 étages. La marge est élevée, mais cela compense le fait que le processeur n'a pas de cache, avec des accès mémoire prenant 150-170 cycles d'horloge ! Le désavantage est que les registres étaient dupliqués 128 fois ! Le processeur avait près d'un millier de registres, ce qui est énorme pour l'époque.
[[File:Full multithreading.png|vignette|upright=2|Processeur à ''Fine Grained Multithreading''.]]
Utiliser un ''barrel processor'' au mieux demande donc qu'il y ait un grand nombre de ''threads'' en cours d'exécution. Mais quelques processeurs FGMT font autrement, en se rapprochant du CGMT. Ils peuvent exécuter un ''thread'' durant quelques cycles successifs, plus d'une dizaine, voire plus. Cependant, il s'agit de processeurs sans exécution dans le désordre, qui sont bloqués dès qu'ils tombent sur une instruction dépendante. Et justement, ils masquent ces blocages en changeant de thread au lieu d'émettre des bulles de pipeline. La technique est beaucoup utilisée sur les cartes graphiques modernes, et sur certains processeurs spécialisés.
Le désavantage est qu'ils doivent intégrer des ''scoreboards'' pour gérer les dépendances de données, sauf sur quelques processeurs laissaient la détection des dépendances au compilateur ! 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, avant de changer de ''thread''.
Qu'il s'agisse des ''barrel processor'' ou des autres processeurs FGMT, tous ont une implémentation similaire. Contrairement aux processeurs CGMT, il n'y a pas de registre de ''thread ID'' unique. En effet, le processeur doit savoir, pour chaque instruction dans le pipeline, à quel ''thread'' elle appartient. Pour cela, les ''thread ID'' sont générés lors du chargement et propagés dans le pipeline en même temps que les instructions, où il est utilisé par le banc de registre, dans les ''load store queues'', etc. Pour résumer : propagation du ''thread ID'' dans le pipeline et disparition du registre de ''thread'' global.
Avec le FGMT, le processeur charge et décode les instructions, avant de les placer dans plusieurs files d'instruction. Il y a une file d'instruction par ''thread''. Le choix de la file d'instruction est réalisé par un multiplexeur, commandé par une super-unité d'émission qui décide quel ''thread'' émet ses instructions (''thread issue unit'').
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2.5|FGMT sur un processeur.]]
L'unité d'émission en question contient un ''scoreboard'' par ''thread'', et combine leurs résultats pour décider quel ''thread'' émet une instruction. Sur les ''barrel processor'', le ''scoreboard'' ne gère que les dépendances liées aux accès mémoire, pour gérer les ''threads'' mis en pause et les re-démarrer quand la lecture est terminée. Sur les autres processeurs, les ''scoreboard'' gèrent les dépendances entre instructions.
Les unités de choix de ''thread'' fonctionnent différemment entre les ''barrel processors'' et ceux qui peuvent exécuter plusieurs instructions successives d'un même ''thread''. Pour ces derniers, l'implémentation demande une coopération entre l'unité d'émission et l'unité de chargement. Le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Si un thread émet une instruction, ce même thread charge une instruction au même moment. Sauf si la file d'instruction du thread est déjà pleine, auquel cas un autre thread est choisi.
Il est possible de tenir compte du contenu de la fenêtre d'instruction pour décider quels ''threads'' sont éligibles au chargement. Il est possible de mettre en pause un ''thread'' si celui-ci accumule un peu trop d'instructions dans la fenêtre d'instruction. C'est en effet signe que ce ''thread'' est bloqué par un accès mémoire, une instruction multicycle ou des dépendances de données. À l'inverse, il est possible de prioriser les ''threads'' qui n'ont presque aucune instruction dans la fenêtre d'instruction. Car ce sont des ''threads'' qui s'exécutent rapidement au point de vider la fenêtre d'instruction. L'idée est de charger en priorité les instructions du ''thread'' pour lequel la file d'instruction est la moins remplie. L'unité de choix du ''thread'' utilise cette information pour déterminer quel ''thread'' charger au prochain cycle.
==Le ''Simultaneous multithreading'' : une spécificité des processeurs superscalaires==
Les deux techniques vues au-dessus peuvent s'adapter sur les processeurs à émission multiple, à savoir qui sont capables d'émettre plusieurs instructions simultanément. La seule contrainte est que l'implémentation du FGMT est compliquée sur les architectures superscalaires, ce qui fait que les ''barrel processors'' sont généralement des processeurs VLIW ou sans émission multiple. Mais il existe une technique de ''multithreading'' matériel spécialement pensée pour les processeurs à émission multiple : le ''Simultaneous multithreading''. La comprendre est assez simple si on la compare au FGMT et au CGMT.
Avec le ''multithreading'' temporel, lors d'un cycle d'horloge, les instructions émises appartiennent au même ''thread'' matériel. Il n'y a pas de situation où une instruction du ''thread'' 1 est émise en même temps qu'une instruction du ''thread'' 2.
{|
|[[File:CGMT sur processeur superscalaire.png|vignette|upright=1.5|CGMT sur processeur superscalaire]]
|[[File:FGMT sur processeur superscalaire.png|vignette|upright=1.5|FGMT sur processeur superscalaire]]
|}
Le '''Simultaneous Multi-Threading''', abrégé en SMT, permet d'émettre simultanément des instructions provenant de ''threads'' séparés. Elle fonctionne sur les processeurs superscalaires, mais n'est pas possible sur les processeurs VLIW.
[[File:Simultaneous Multi-Threading.png|centre|vignette|upright=2|Simultaneous Multi-Threading]]
===L'implémentation matérielle du SMT===
Dans les faits, tous les processeurs SMT sont des processeurs à exécution dans le désordre. Non pas que ce soit obligatoire, juste que le cout d'implémentation est bien plus faible sur un processeur à exécution dans le désordre. L'implémentation du SMT prend un processeur à exécution dans le désordre, ajoute plusieurs ''program counter'' et les circuits adéquats. L'unité d'émission choisit quelles instructions envoyer aux ALU, en utilisant l'exécution dans le désordre : tant qu'elles sont indépendantes, elles peuvent s’exécuter en parallèle. Et deux instructions de deux ''threads'' différents sont indépendantes ! Il y a donc un lien étroit entre exécution dans le désordre et SMT !
Ironiquement, avec l'amélioration des techniques d'exécution dans le désordre, le SMT commence à perdre de sa superbe. Plus l'exécution dans le désordre est efficace, plus le SMT est inutile. Si l'exécution dans le désordre est trop efficace, les cycles gâchés sont trop rares pour que le SMT ne servent pas à grande chose. Quand les cycles gâchés représentent grand maximum 10% du temps d’exécution grâce à l'usage de l’exécution dans le désordre, le SMT n'a plus grand-chose à remplir et le second programme s’exécuterait trop lentement pour ça vaille le coup.
Le SMT s'implémente généralement avec une seule fenêtre d’instruction et avec le renommage de registres. Le renommage de registres fait qu'on n'a pas à utiliser de fenêtrage de registres, juste à augmenter la taille du banc de registres. L'idée est que l'on concatène le ''thread ID'' au nom de registre avant de faire le renommage. Comme cela, on garantit que deux registres architecturaux identiques mais référencés dans des ''threads'' différents, correspondront à des registres physiques différents. Ainsi, l'unité d'émission a juste à vérifier les dépendances entre registres, pas besoin de gérer les ''thread ID''. En faisant cela, la logique d'émission est beaucoup plus simple. Par contre, les tables pour le renommage de registre sont dupliqués, avec autant de table que de ''thread'', chaque ''thread'' a la sienne.
Comme avec le FGMT, en cas des mauvaise prédiction de branchement, seules les instructions du ''thread'' fautif doivent être vidées. Pour cela, les files/fenêtres d'instruction doivent savoir à quel ''thread'' appartient tel ou telle instruction, pareil pour le ''Reorder Buffer''. Pour cela, on ajoute, dans chaque entrée de ces structures, le ''thread ID'' de l'instruction.
Avec le SMT, les différents ''threads'' doivent se partager certaines ressources matérielles : la fenêtre d'instruction, le banc de registres, la ''load store queue'', le cache, l'unité de prédiction de branchement ou d'autres structures matérielles. Le partage de ces ressources entre ''threads'' est dynamique, sauf pour quelques exceptions. La conséquence est qu'il peut y avoir une perte de performance si on lance deux ''threads'' et qu'il n'y a pas assez de ressources matérielles pour en profiter. Les ''threads'' entrent en compétition pour y accéder. Parlons un petit peu du partage du cache et de l'unité de prédiction de branchement.
===Le partage des caches===
La quasi-totalité des architectures multithreadées utilisent un cache partagé, et non des caches dédiés, ce qui impose de gérer le partage des caches entre les différents ''threads''.
La première idée est de partitionner le cache en plusieurs morceaux, avec un par ''thread''. Le '''partitionnement statique''' donne des portions égales à chaque programme, ce qui n'est pas toujours optimal mais a l'avantage d'être facile à implémenter. À l'opposé, le '''partitionnement dynamique''' partage le cache selon les besoins des ''thread'' en cours d'exécution. Si un programme a besoin de plus de place que l'autre, il peut réserver un peu plus de place que son concurrent. Mais la gestion du partitionnement est alors plus compliquée, et demande de partitionner efficacement le cache, sans quoi les deux programmes exécutés risquent de se marcher dessus et d'entrer en compétition pour le cache.
[[File:Partitionnement de l'Instruction Buffer avec l'hyperthreading.png|centre|vignette|upright=2|Partitionnement des caches avec l'hyperthreading.]]
Une autre possibilité est de ne pas partitionner et de laisser les différents ''threads'' accéder au cache comme bon leur semble. Si le cache est physiquement tagué, il n'y a aucune modification à faire. Mais si le cache est virtuellement tagué, une même adresse virtuelle peut correspondre à des adresses physiques différentes pour deux ''threads'' différents. Pour éviter cela, il faut ajouter l'identifiant de ''thread'' dans chaque ligne de cache, dans les bits de contrôle. La détection des succès ou défaut de cache tient compte de l'identifiant de ''thread'' lors des comparaisons de tags, afin d'éviter qu'un ''thread'' lise une donnée d'un autre ''thread''.
À l'inverse des processeurs mono-cœur, les processeurs multithreadés préfèrent des caches dont le débit binaire est plus important que la latence mémoire. La raison est que deux ''threads'' consomment deux fois plus de données qu'un seul et le débit binaire du cache doit suivre. Par contre, la latence est moins importante car les cycles gâchés qu'elle créé sont remplis par le ''multithreading'' matériel. Si un défaut de cache prend 4 cycles, le ''multithreading'' matériel va rapidement trouver des instructions à exécuter pendant ces cycles.
Tout ce qui vient d'être dit s'applique aussi sur la TLB. Il est possible de la partitionner entre plusieurs ''threads''. La plupart du temps, le partitionnement est statique sur les TLB dédiées aux instructions, alors que les TLB pour les données sont partagées dynamiquement. C'est le cas sur les architectures Skylake d'Intel, où les 128 entrées de la TLB d'instruction de niveau 1 ont découpées en deux sections de 64 entrées, une par programme/''thread'', les autres TLB étant partitionnées dynamiquement. Une autre solution est d'ajouter un identifiant de ''thread'' dans chaque entrée de la TLB, sur le même principe que celui vu dans le chapitre sur la TLB. Le gain en performance est bien plus important, pour un cout en hardware limité.
===L'unité de prédiction de branchement===
Un autre cache est lui aussi partitionné ou complété avec des ''thread ID'' : le ''branch target buffer''. Rappelons que c'est un cache spécialisé pour les adresses de branchements. L'unité de prédiction de branchements doit idéalement séparer les branchements entre chaque ''thread'' pour obtenir de bonnes performances, soit en partitionnant le BTB, soit en ajoutant ajouter un ''Thread ID'' dans chaque entrée de ce cache. Mais ce n'est pas obligatoire.
Les unités de prédiction de branchement vues dans les chapitres précédents peuvent parfaitement fonctionner telles quelles sur un processeur multithreadé. Le ''branch target buffer'' mémorise alors des branchements de ''threads'' différents, il ne sait pas à quel ''thread'' appartient tel branchement, mais le tout fonctionne. Le problème est que les prédictions pour un branchement sont parasitées par les branchements d'autres ''threads'', surtout pour les unités se basant sur un historique global. Malgré tout, on obtient un résultat tolérable, les performances sont assez bonnes et cela s'explique avec ce qui suit.
Déjà, il arrive que les branchements des autres ''threads'' aient un effet positif sur la prédiction pour un ''thread''. De telles interférences positives sont rares, mais possible si les deux ''threads'' travaillent sur des données partagées, et c'est alors les unités de prédiction de branchement normales, sans partitionnement ni ''thread ID'' qui fonctionnent mieux, sous certaines circonstances.
De plus, quand une mauvaise prédiction de branchement est détectée, il faut vider le pipeline des instructions chargées à tort. Et les instructions fautives appartiennent toutes à un même ''thread'', les instructions des autres ''threads'' ne sont pas concernées. Le vidage du pipeline est donc sélectif. Là encore, la vidange du pipeline se base sur les ''Thread ID'' propagés dans le pipeline. Vu que le vidage du pipeline est sélectif, partiel, la perte de performance en cas de mauvaise prédiction de branchement est donc plus faible, et cela surcompense le fait que les prédictions de branchement sont parasitées par les branchements d’autres ''threads''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Architectures multiprocesseurs et multicœurs
| prevText=Architectures multiprocesseurs et multicœurs
| next=Les architectures à parallélisme de données
| nextText=Les architectures à parallélisme de données
}}
</noinclude>
qna2gkvs422joz6jrfue2nlz4a8oy3f
763258
763257
2026-04-08T14:47:07Z
Mewtow
31375
763258
wikitext
text/x-wiki
Vous pensez surement qu'il faut obligatoirement plusieurs cœurs pour exécuter plusieurs programmes en parallèle, mais sachez que c'est faux ! Les processeurs mono-cœur en sont capables, en alternant entre les programmes à exécuter. Plusieurs programmes s’exécutent donc sur le même processeur, mais chacun à leur tour et non en même temps. D'ordinaire, cette alternance est gérée par le système d'exploitation, mais certains processeurs gèrent cette alternance eux-mêmes, directement au niveau matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, le processeur donne la main à un autre programme. Les processeurs en question sont appelés des processeurs multithreadés, ou encore des '''architectures multithreadées''', en référence au terme ''thread'', qui est plus ou moins équivalent à celui de programme dans ce cours. Ils exécutent un ''thread'' à la fois, mais changent de ''thread'' plus ou moins régulièrement.
Pour comprendre le pourquoi des architectures multithreadées, il faut rappeler qu'il arrive que l'unité de calcul d'un processeur ne fasse rien, par exemple pendant que le processeur accède à la mémoire. Les cycles d'horloge où l'unité de calcul est inutilisée sont des '''cycles gâchés'''. L’exécution dans le désordre réduit ces cycles gâchés, mais les architectures multithreadées sont une solution alternative et complémentaire. Elles visent à ce que les cycles gâchés d'un ''thread'' soient remplis par les calculs d'autre ''thread''.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Le ''Simultaneous MultiThreading'' est spécifique aux processeurs superscalaires, alors que les autres techniques fonctionnent sur tous les processeurs, qu'ils soient superscalaire ou simple-émission.
: Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''.
==Le multithreading temporel==
Le FGMT et le CGMT sont regroupés sous le terme de '''multithreading temporel'''. Les deux partagent en effet un même point commun : à chaque cycle, les instructions émises par l'unité d'émission proviennent d'un seul programme. La distinction n'a de sens que sur les processeurs à exécution multiple, ce sera plus clair dans la suite du chapitre quand on comparera ''multithreading'' temporel et SMT. La différence entre FGMT et CGMT est la fréquence des changements de ''thread'' : à chaque cycle ou presque pour le FGMT, lors d'un évènement bien précis pour le CGMT.
Mais FGMT et CGMT ont une implémentation similaire. Le processeur subit des changements pour gérer plusieurs ''threads'' en cours d'exécution.
Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement''', dont le fonctionnement dépend du processeur, comme nous le verrons plus bas.
Ensuite, le processeur attribue un numéro à chaque ''thread'', appelé le ''thread ID''. Il y en autant que de ''threads'' exécutables simultanément par le processeur. Par exemple, si le processeur gère au maximum 8 ''threads'' simultanés, l'identifiant de ''thread'' va de 0 à 7 et est codé sur 3 bits. Le ''thread ID'' est propagé dans le pipeline, histoire de savoir à quel ''thread'' appartient l'instruction en cours. Il sert notamment pour adresser le banc de registre, mais aussi pour d'autres choses.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
Les processeurs à ''multithreading'' temporel sont généralement des processeurs sans exécution dans le désordre, et n'ont donc pas de renommage de registres. Avec eux, il est obligatoire de dupliquer les registres pour que chaque programme ait son ensemble de registres architecturaux rien qu'à lui. Cela demande soit un banc de registre par programme, soit un banc de registre commun géré par fenêtrage de registre (chaque programme ayant sa propre fenêtre de registres).
Il faut aussi dupliquer les ''load-store queue'', pour séparer les lectures/écritures en attente de chaque ''thread''. Là encore, il est possible d'utiliser une ''load-store queue'' unique dans laquelle on ajoute des informations pour savoir à quel ''thread'' appartient telle ou telle lecture/écriture.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2.5|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''Coarse Grained Multithreading''===
[[File:Coarse Grained Multithreading.png|vignette|upright=1|Coarse Grained Multithreading.]]
Le '''Coarse Grained Multithreading''' change de programme quand un évènement bien précis a lieu. L'évènement en question fait que l'ALU restera inutilisé pendant un moment : accès à la mémoire, branchements, etc.
Sur certains processeurs CGMT, il y a une instruction précise pour changer de ''thread''. La commutation de ''thread'' est alors totalement ou partiellement décidée par le logiciel. Mais il s'agit là d'un cas particulier.
Le cas le plus fréquent est de changer de ''thread'' lors d'un défaut de cache. Vu que l'accès à la RAM est quelque chose de très lent, il est intéressant d'exécuter des instructions d'un autre ''thread'' pour recouvrir l'accès à la RAM. L'idée est similaire à ce qu'on a avec les lectures non-bloquantes et/ou l'exécution dans le désordre : pendant que l'unité d'accès mémoire gère le défaut de cache, on alimente l'unité de calcul avec des calculs indépendants. Sauf qu'avec le ''multithreading'', les calculs proviennent d'un autre ''thread''.
Un point important est que le cache doit être un cache non-bloquant, sans quoi le ''multithreading'' matériel ne fonctionne tout simplement pas. Par exemple, prenons une architecture qui change de ''thread'' à chaque défaut de cache. Si on veut supporter plus de deux ''threads'', il faut que plusieurs ''threads'' subissent un défaut de cache pour que le ''multithreading'' ait de l'intérêt, ce qui implique un cache non-bloquant.
[[File:Multithreading et mitigation de la latence mémoire.png|centre|vignette|upright=1.5|Multithreading et mitigation de la latence mémoire.]]
Avec le CGMT, on est certain que toutes les instructions en cours d’exécution appartiennent au même ''thread''. Quand il passe d'un ''thread'' à l'autre, le processeur attend naturellement que le pipeline soit complétement vidé avant de charger les instructions du ''thread'' suivant. Le processeur peut donc se débrouiller avec un simple '''registre de ''thread''''' qui mémorise le ''thread ID'' du ''thread'' en cours d'exécution. Le registre de ''thread'' est directement connecté à l'entrée d'adresse du banc de registre, pour gérer le fenêtrage de registres. Il est aussi connecté à la ''load-store queue'', et surtout au multiplexeur de choix de ''thread'', dans l'unité de chargement.
[[File:Implementation du CGMT.png|centre|vignette|upright=2|Implémentation du CGMT]]
L'unité d'ordonnancement détermine quel ''thread'' charger dans le pipeline, elle sélectionne un ''thread ID''. Pour faciliter son travail, cette unité contient un registre qui mémorise quels sont les ''threads'' actifs. Les ''threads'' bloqués par un défaut de cache sont marqués comme inactifs tant que le défaut de cache n'est pas résolu. Évidemment, dès qu'un défaut de cache est résolu, l'unité d'accès mémoire prévient l'unité de choix de ''thread'' pour que celui-ci marque le ''thread'' adéquat comme de nouveau actif. Le registre est composé de N bits sur un processeur qui gère N threads maximum : chaque bit est associé à un ''thread'' et indique s'il est actif ou non.
===Le ''Fine Grained MultiThreading''===
[[File:Fine Grained Multithreading.png|vignette|upright=2|''Barrel processor''.]]
Le '''''Fine Grained Multithreading''''' regroupe deux types de processeurs différents. Le premier type est celui des '''''barrel processors''''', qui changent de programme à chaque cycle d'horloge. Idéalement, chaque étage du pipeline est utilisé par une instruction différente. L'avantage est que, à l'intérieur d'un ''thread'', une nouvelle instruction démarre quand la précédente est terminée. Le résultat d'une instruction est déjà dans les registres quand l'instruction suivante s’exécute. Il n'y a donc pas de dépendances entre instructions successives, les circuits liés aux dépendances de données sont fortement simplifiés, le réseau de contournement de l'ALU disparait, l'unité de prédiction de branchement disparait (car le résultat d'un branchement est connu avant que le même ''thread'' exécute sa prochaine instruction).
Mais les accès mémoire sont gérés à part des autres instructions. Lorsque un ''thread'' effectue un accès mémoire, il est mis en pause et d'autres ''threads'' sont lancés à sa place. Prenons l'exemple d'un processeur qui gère 16 ''threads'' maximum : si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Mais pour que cela fonctionne sans encombre, on est obligé d'avoir un nombre assez élevé de programmes en cours d’exécution. Pour donner un exemple, le processeur MTA était capable d'exécuter un 128 ''threads'' maximum, pour un pipeline de 21 étages. La marge est élevée, mais cela compense le fait que le processeur n'a pas de cache, avec des accès mémoire prenant 150-170 cycles d'horloge ! Le désavantage est que les registres étaient dupliqués 128 fois ! Le processeur avait près d'un millier de registres, ce qui est énorme pour l'époque.
[[File:Full multithreading.png|vignette|upright=2|Processeur à ''Fine Grained Multithreading''.]]
Utiliser un ''barrel processor'' au mieux demande donc qu'il y ait un grand nombre de ''threads'' en cours d'exécution. Mais quelques processeurs FGMT font autrement, en se rapprochant du CGMT. Ils peuvent exécuter un ''thread'' durant quelques cycles successifs, plus d'une dizaine, voire plus. Cependant, il s'agit de processeurs sans exécution dans le désordre, qui sont bloqués dès qu'ils tombent sur une instruction dépendante. Et justement, ils masquent ces blocages en changeant de thread au lieu d'émettre des bulles de pipeline. La technique est beaucoup utilisée sur les cartes graphiques modernes, et sur certains processeurs spécialisés.
Le désavantage est qu'ils doivent intégrer des ''scoreboards'' pour gérer les dépendances de données, sauf sur quelques processeurs laissaient la détection des dépendances au compilateur ! 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, avant de changer de ''thread''.
Qu'il s'agisse des ''barrel processor'' ou des autres processeurs FGMT, tous ont une implémentation similaire. Contrairement aux processeurs CGMT, il n'y a pas de registre de ''thread ID'' unique. En effet, le processeur doit savoir, pour chaque instruction dans le pipeline, à quel ''thread'' elle appartient. Pour cela, les ''thread ID'' sont générés lors du chargement et propagés dans le pipeline en même temps que les instructions, où il est utilisé par le banc de registre, dans les ''load store queues'', etc. Pour résumer : propagation du ''thread ID'' dans le pipeline et disparition du registre de ''thread'' global.
Avec le FGMT, le processeur charge et décode les instructions, avant de les placer dans plusieurs files d'instruction. Il y a une file d'instruction par ''thread''. Le choix de la file d'instruction est réalisé par un multiplexeur, commandé par une super-unité d'émission qui décide quel ''thread'' émet ses instructions (''thread issue unit'').
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2.5|FGMT sur un processeur.]]
L'unité d'émission en question contient un ''scoreboard'' par ''thread'', et combine leurs résultats pour décider quel ''thread'' émet une instruction. Sur les ''barrel processor'', le ''scoreboard'' ne gère que les dépendances liées aux accès mémoire, pour gérer les ''threads'' mis en pause et les re-démarrer quand la lecture est terminée. Sur les autres processeurs, les ''scoreboard'' gèrent les dépendances entre instructions.
Les unités de choix de ''thread'' fonctionnent différemment entre les ''barrel processors'' et ceux qui peuvent exécuter plusieurs instructions successives d'un même ''thread''. Pour ces derniers, l'implémentation demande une coopération entre l'unité d'émission et l'unité de chargement. Le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Si un thread émet une instruction, ce même thread charge une instruction au même moment. Sauf si la file d'instruction du thread est déjà pleine, auquel cas un autre thread est choisi.
Il est possible de tenir compte du contenu de la fenêtre d'instruction pour décider quels ''threads'' sont éligibles au chargement. Il est possible de mettre en pause un ''thread'' si celui-ci accumule un peu trop d'instructions dans la fenêtre d'instruction. C'est en effet signe que ce ''thread'' est bloqué par un accès mémoire, une instruction multicycle ou des dépendances de données. À l'inverse, il est possible de prioriser les ''threads'' qui n'ont presque aucune instruction dans la fenêtre d'instruction. Car ce sont des ''threads'' qui s'exécutent rapidement au point de vider la fenêtre d'instruction. L'idée est de charger en priorité les instructions du ''thread'' pour lequel la file d'instruction est la moins remplie. L'unité de choix du ''thread'' utilise cette information pour déterminer quel ''thread'' charger au prochain cycle.
==Le ''Simultaneous multithreading'' : une spécificité des processeurs superscalaires==
Les deux techniques vues au-dessus peuvent s'adapter sur les processeurs à émission multiple, à savoir qui sont capables d'émettre plusieurs instructions simultanément. La seule contrainte est que l'implémentation du FGMT est compliquée sur les architectures superscalaires, ce qui fait que les ''barrel processors'' sont généralement des processeurs VLIW ou sans émission multiple. Mais il existe une technique de ''multithreading'' matériel spécialement pensée pour les processeurs à émission multiple : le ''Simultaneous multithreading''. La comprendre est assez simple si on la compare au FGMT et au CGMT.
Avec le ''multithreading'' temporel, lors d'un cycle d'horloge, les instructions émises appartiennent au même ''thread'' matériel. Il n'y a pas de situation où une instruction du ''thread'' 1 est émise en même temps qu'une instruction du ''thread'' 2.
{|
|[[File:CGMT sur processeur superscalaire.png|vignette|upright=1.5|CGMT sur processeur superscalaire]]
|[[File:FGMT sur processeur superscalaire.png|vignette|upright=1.5|FGMT sur processeur superscalaire]]
|}
Le '''Simultaneous Multi-Threading''', abrégé en SMT, permet d'émettre simultanément des instructions provenant de ''threads'' séparés. Elle fonctionne sur les processeurs superscalaires, mais n'est pas possible sur les processeurs VLIW.
[[File:Simultaneous Multi-Threading.png|centre|vignette|upright=2|Simultaneous Multi-Threading]]
===L'implémentation matérielle du SMT===
Dans les faits, tous les processeurs SMT sont des processeurs à exécution dans le désordre. Non pas que ce soit obligatoire, juste que le cout d'implémentation est bien plus faible sur un processeur à exécution dans le désordre. L'implémentation du SMT prend un processeur à exécution dans le désordre, ajoute plusieurs ''program counter'' et les circuits adéquats. L'unité d'émission choisit quelles instructions envoyer aux ALU, en utilisant l'exécution dans le désordre : tant qu'elles sont indépendantes, elles peuvent s’exécuter en parallèle. Et deux instructions de deux ''threads'' différents sont indépendantes ! Il y a donc un lien étroit entre exécution dans le désordre et SMT !
Ironiquement, avec l'amélioration des techniques d'exécution dans le désordre, le SMT commence à perdre de sa superbe. Plus l'exécution dans le désordre est efficace, plus le SMT est inutile. Si l'exécution dans le désordre est trop efficace, les cycles gâchés sont trop rares pour que le SMT ne servent pas à grande chose. Quand les cycles gâchés représentent grand maximum 10% du temps d’exécution grâce à l'usage de l’exécution dans le désordre, le SMT n'a plus grand-chose à remplir et le second programme s’exécuterait trop lentement pour ça vaille le coup.
Le SMT s'implémente généralement avec une seule fenêtre d’instruction et avec le renommage de registres. Le renommage de registres fait qu'on n'a pas à utiliser de fenêtrage de registres, juste à augmenter la taille du banc de registres. L'idée est que l'on concatène le ''thread ID'' au nom de registre avant de faire le renommage. Comme cela, on garantit que deux registres architecturaux identiques mais référencés dans des ''threads'' différents, correspondront à des registres physiques différents. Ainsi, l'unité d'émission a juste à vérifier les dépendances entre registres, pas besoin de gérer les ''thread ID''. En faisant cela, la logique d'émission est beaucoup plus simple. Par contre, les tables pour le renommage de registre sont dupliqués, avec autant de table que de ''thread'', chaque ''thread'' a la sienne.
Comme avec le FGMT, en cas des mauvaise prédiction de branchement, seules les instructions du ''thread'' fautif doivent être vidées. Pour cela, les files/fenêtres d'instruction doivent savoir à quel ''thread'' appartient tel ou telle instruction, pareil pour le ''Reorder Buffer''. Pour cela, on ajoute, dans chaque entrée de ces structures, le ''thread ID'' de l'instruction.
Avec le SMT, les différents ''threads'' doivent se partager certaines ressources matérielles : la fenêtre d'instruction, le banc de registres, la ''load store queue'', le cache, l'unité de prédiction de branchement ou d'autres structures matérielles. Le partage de ces ressources entre ''threads'' est dynamique, sauf pour quelques exceptions. La conséquence est qu'il peut y avoir une perte de performance si on lance deux ''threads'' et qu'il n'y a pas assez de ressources matérielles pour en profiter. Les ''threads'' entrent en compétition pour y accéder. Parlons un petit peu du partage du cache et de l'unité de prédiction de branchement.
===Le partage des caches===
La quasi-totalité des architectures multithreadées utilisent un cache partagé, et non des caches dédiés, ce qui impose de gérer le partage des caches entre les différents ''threads''.
La première idée est de partitionner le cache en plusieurs morceaux, avec un par ''thread''. Le '''partitionnement statique''' donne des portions égales à chaque programme, ce qui n'est pas toujours optimal mais a l'avantage d'être facile à implémenter. À l'opposé, le '''partitionnement dynamique''' partage le cache selon les besoins des ''thread'' en cours d'exécution. Si un programme a besoin de plus de place que l'autre, il peut réserver un peu plus de place que son concurrent. Mais la gestion du partitionnement est alors plus compliquée, et demande de partitionner efficacement le cache, sans quoi les deux programmes exécutés risquent de se marcher dessus et d'entrer en compétition pour le cache.
[[File:Partitionnement de l'Instruction Buffer avec l'hyperthreading.png|centre|vignette|upright=2|Partitionnement des caches avec l'hyperthreading.]]
Une autre possibilité est de ne pas partitionner et de laisser les différents ''threads'' accéder au cache comme bon leur semble. Si le cache est physiquement tagué, il n'y a aucune modification à faire. Mais si le cache est virtuellement tagué, une même adresse virtuelle peut correspondre à des adresses physiques différentes pour deux ''threads'' différents. Pour éviter cela, il faut ajouter l'identifiant de ''thread'' dans chaque ligne de cache, dans les bits de contrôle. La détection des succès ou défaut de cache tient compte de l'identifiant de ''thread'' lors des comparaisons de tags, afin d'éviter qu'un ''thread'' lise une donnée d'un autre ''thread''.
À l'inverse des processeurs mono-cœur, les processeurs multithreadés préfèrent des caches dont le débit binaire est plus important que la latence mémoire. La raison est que deux ''threads'' consomment deux fois plus de données qu'un seul et le débit binaire du cache doit suivre. Par contre, la latence est moins importante car les cycles gâchés qu'elle créé sont remplis par le ''multithreading'' matériel. Si un défaut de cache prend 4 cycles, le ''multithreading'' matériel va rapidement trouver des instructions à exécuter pendant ces cycles.
Tout ce qui vient d'être dit s'applique aussi sur la TLB. Il est possible de la partitionner entre plusieurs ''threads''. La plupart du temps, le partitionnement est statique sur les TLB dédiées aux instructions, alors que les TLB pour les données sont partagées dynamiquement. C'est le cas sur les architectures Skylake d'Intel, où les 128 entrées de la TLB d'instruction de niveau 1 ont découpées en deux sections de 64 entrées, une par programme/''thread'', les autres TLB étant partitionnées dynamiquement. Une autre solution est d'ajouter un identifiant de ''thread'' dans chaque entrée de la TLB, sur le même principe que celui vu dans le chapitre sur la TLB. Le gain en performance est bien plus important, pour un cout en hardware limité.
===L'unité de prédiction de branchement===
Un autre cache est lui aussi partitionné ou complété avec des ''thread ID'' : le ''branch target buffer''. Rappelons que c'est un cache spécialisé pour les adresses de branchements. L'unité de prédiction de branchements doit idéalement séparer les branchements entre chaque ''thread'' pour obtenir de bonnes performances, soit en partitionnant le BTB, soit en ajoutant ajouter un ''Thread ID'' dans chaque entrée de ce cache. Mais ce n'est pas obligatoire.
Les unités de prédiction de branchement vues dans les chapitres précédents peuvent parfaitement fonctionner telles quelles sur un processeur multithreadé. Le ''branch target buffer'' mémorise alors des branchements de ''threads'' différents, il ne sait pas à quel ''thread'' appartient tel branchement, mais le tout fonctionne. Le problème est que les prédictions pour un branchement sont parasitées par les branchements d'autres ''threads'', surtout pour les unités se basant sur un historique global. Malgré tout, on obtient un résultat tolérable, les performances sont assez bonnes et cela s'explique avec ce qui suit.
Déjà, il arrive que les branchements des autres ''threads'' aient un effet positif sur la prédiction pour un ''thread''. De telles interférences positives sont rares, mais possible si les deux ''threads'' travaillent sur des données partagées, et c'est alors les unités de prédiction de branchement normales, sans partitionnement ni ''thread ID'' qui fonctionnent mieux, sous certaines circonstances.
De plus, quand une mauvaise prédiction de branchement est détectée, il faut vider le pipeline des instructions chargées à tort. Et les instructions fautives appartiennent toutes à un même ''thread'', les instructions des autres ''threads'' ne sont pas concernées. Le vidage du pipeline est donc sélectif. Là encore, la vidange du pipeline se base sur les ''Thread ID'' propagés dans le pipeline. Vu que le vidage du pipeline est sélectif, partiel, la perte de performance en cas de mauvaise prédiction de branchement est donc plus faible, et cela surcompense le fait que les prédictions de branchement sont parasitées par les branchements d’autres ''threads''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Architectures multiprocesseurs et multicœurs
| prevText=Architectures multiprocesseurs et multicœurs
| next=Les architectures à parallélisme de données
| nextText=Les architectures à parallélisme de données
}}
</noinclude>
m6v3x97bqabtx27uibolf6pr3m8o84v
763259
763258
2026-04-08T14:47:25Z
Mewtow
31375
/* Le multithreading temporel */
763259
wikitext
text/x-wiki
Vous pensez surement qu'il faut obligatoirement plusieurs cœurs pour exécuter plusieurs programmes en parallèle, mais sachez que c'est faux ! Les processeurs mono-cœur en sont capables, en alternant entre les programmes à exécuter. Plusieurs programmes s’exécutent donc sur le même processeur, mais chacun à leur tour et non en même temps. D'ordinaire, cette alternance est gérée par le système d'exploitation, mais certains processeurs gèrent cette alternance eux-mêmes, directement au niveau matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, le processeur donne la main à un autre programme. Les processeurs en question sont appelés des processeurs multithreadés, ou encore des '''architectures multithreadées''', en référence au terme ''thread'', qui est plus ou moins équivalent à celui de programme dans ce cours. Ils exécutent un ''thread'' à la fois, mais changent de ''thread'' plus ou moins régulièrement.
Pour comprendre le pourquoi des architectures multithreadées, il faut rappeler qu'il arrive que l'unité de calcul d'un processeur ne fasse rien, par exemple pendant que le processeur accède à la mémoire. Les cycles d'horloge où l'unité de calcul est inutilisée sont des '''cycles gâchés'''. L’exécution dans le désordre réduit ces cycles gâchés, mais les architectures multithreadées sont une solution alternative et complémentaire. Elles visent à ce que les cycles gâchés d'un ''thread'' soient remplis par les calculs d'autre ''thread''.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Le ''Simultaneous MultiThreading'' est spécifique aux processeurs superscalaires, alors que les autres techniques fonctionnent sur tous les processeurs, qu'ils soient superscalaire ou simple-émission.
: Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''.
==Le multithreading temporel==
Le FGMT et le CGMT sont regroupés sous le terme de '''multithreading temporel'''. Les deux partagent en effet un même point commun : à chaque cycle, les instructions émises par l'unité d'émission proviennent d'un seul programme. La distinction n'a de sens que sur les processeurs à exécution multiple, ce sera plus clair dans la suite du chapitre quand on comparera ''multithreading'' temporel et SMT. La différence entre FGMT et CGMT est la fréquence des changements de ''thread'' : à chaque cycle ou presque pour le FGMT, lors d'un évènement bien précis pour le CGMT.
Le processeur subit des changements pour gérer plusieurs ''threads'' en cours d'exécution. En premier lieu, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement''', dont le fonctionnement dépend du processeur, comme nous le verrons plus bas.
Ensuite, le processeur attribue un numéro à chaque ''thread'', appelé le ''thread ID''. Il y en autant que de ''threads'' exécutables simultanément par le processeur. Par exemple, si le processeur gère au maximum 8 ''threads'' simultanés, l'identifiant de ''thread'' va de 0 à 7 et est codé sur 3 bits. Le ''thread ID'' est propagé dans le pipeline, histoire de savoir à quel ''thread'' appartient l'instruction en cours. Il sert notamment pour adresser le banc de registre, mais aussi pour d'autres choses.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
Les processeurs à ''multithreading'' temporel sont généralement des processeurs sans exécution dans le désordre, et n'ont donc pas de renommage de registres. Avec eux, il est obligatoire de dupliquer les registres pour que chaque programme ait son ensemble de registres architecturaux rien qu'à lui. Cela demande soit un banc de registre par programme, soit un banc de registre commun géré par fenêtrage de registre (chaque programme ayant sa propre fenêtre de registres).
Il faut aussi dupliquer les ''load-store queue'', pour séparer les lectures/écritures en attente de chaque ''thread''. Là encore, il est possible d'utiliser une ''load-store queue'' unique dans laquelle on ajoute des informations pour savoir à quel ''thread'' appartient telle ou telle lecture/écriture.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2.5|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''Coarse Grained Multithreading''===
[[File:Coarse Grained Multithreading.png|vignette|upright=1|Coarse Grained Multithreading.]]
Le '''Coarse Grained Multithreading''' change de programme quand un évènement bien précis a lieu. L'évènement en question fait que l'ALU restera inutilisé pendant un moment : accès à la mémoire, branchements, etc.
Sur certains processeurs CGMT, il y a une instruction précise pour changer de ''thread''. La commutation de ''thread'' est alors totalement ou partiellement décidée par le logiciel. Mais il s'agit là d'un cas particulier.
Le cas le plus fréquent est de changer de ''thread'' lors d'un défaut de cache. Vu que l'accès à la RAM est quelque chose de très lent, il est intéressant d'exécuter des instructions d'un autre ''thread'' pour recouvrir l'accès à la RAM. L'idée est similaire à ce qu'on a avec les lectures non-bloquantes et/ou l'exécution dans le désordre : pendant que l'unité d'accès mémoire gère le défaut de cache, on alimente l'unité de calcul avec des calculs indépendants. Sauf qu'avec le ''multithreading'', les calculs proviennent d'un autre ''thread''.
Un point important est que le cache doit être un cache non-bloquant, sans quoi le ''multithreading'' matériel ne fonctionne tout simplement pas. Par exemple, prenons une architecture qui change de ''thread'' à chaque défaut de cache. Si on veut supporter plus de deux ''threads'', il faut que plusieurs ''threads'' subissent un défaut de cache pour que le ''multithreading'' ait de l'intérêt, ce qui implique un cache non-bloquant.
[[File:Multithreading et mitigation de la latence mémoire.png|centre|vignette|upright=1.5|Multithreading et mitigation de la latence mémoire.]]
Avec le CGMT, on est certain que toutes les instructions en cours d’exécution appartiennent au même ''thread''. Quand il passe d'un ''thread'' à l'autre, le processeur attend naturellement que le pipeline soit complétement vidé avant de charger les instructions du ''thread'' suivant. Le processeur peut donc se débrouiller avec un simple '''registre de ''thread''''' qui mémorise le ''thread ID'' du ''thread'' en cours d'exécution. Le registre de ''thread'' est directement connecté à l'entrée d'adresse du banc de registre, pour gérer le fenêtrage de registres. Il est aussi connecté à la ''load-store queue'', et surtout au multiplexeur de choix de ''thread'', dans l'unité de chargement.
[[File:Implementation du CGMT.png|centre|vignette|upright=2|Implémentation du CGMT]]
L'unité d'ordonnancement détermine quel ''thread'' charger dans le pipeline, elle sélectionne un ''thread ID''. Pour faciliter son travail, cette unité contient un registre qui mémorise quels sont les ''threads'' actifs. Les ''threads'' bloqués par un défaut de cache sont marqués comme inactifs tant que le défaut de cache n'est pas résolu. Évidemment, dès qu'un défaut de cache est résolu, l'unité d'accès mémoire prévient l'unité de choix de ''thread'' pour que celui-ci marque le ''thread'' adéquat comme de nouveau actif. Le registre est composé de N bits sur un processeur qui gère N threads maximum : chaque bit est associé à un ''thread'' et indique s'il est actif ou non.
===Le ''Fine Grained MultiThreading''===
[[File:Fine Grained Multithreading.png|vignette|upright=2|''Barrel processor''.]]
Le '''''Fine Grained Multithreading''''' regroupe deux types de processeurs différents. Le premier type est celui des '''''barrel processors''''', qui changent de programme à chaque cycle d'horloge. Idéalement, chaque étage du pipeline est utilisé par une instruction différente. L'avantage est que, à l'intérieur d'un ''thread'', une nouvelle instruction démarre quand la précédente est terminée. Le résultat d'une instruction est déjà dans les registres quand l'instruction suivante s’exécute. Il n'y a donc pas de dépendances entre instructions successives, les circuits liés aux dépendances de données sont fortement simplifiés, le réseau de contournement de l'ALU disparait, l'unité de prédiction de branchement disparait (car le résultat d'un branchement est connu avant que le même ''thread'' exécute sa prochaine instruction).
Mais les accès mémoire sont gérés à part des autres instructions. Lorsque un ''thread'' effectue un accès mémoire, il est mis en pause et d'autres ''threads'' sont lancés à sa place. Prenons l'exemple d'un processeur qui gère 16 ''threads'' maximum : si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Mais pour que cela fonctionne sans encombre, on est obligé d'avoir un nombre assez élevé de programmes en cours d’exécution. Pour donner un exemple, le processeur MTA était capable d'exécuter un 128 ''threads'' maximum, pour un pipeline de 21 étages. La marge est élevée, mais cela compense le fait que le processeur n'a pas de cache, avec des accès mémoire prenant 150-170 cycles d'horloge ! Le désavantage est que les registres étaient dupliqués 128 fois ! Le processeur avait près d'un millier de registres, ce qui est énorme pour l'époque.
[[File:Full multithreading.png|vignette|upright=2|Processeur à ''Fine Grained Multithreading''.]]
Utiliser un ''barrel processor'' au mieux demande donc qu'il y ait un grand nombre de ''threads'' en cours d'exécution. Mais quelques processeurs FGMT font autrement, en se rapprochant du CGMT. Ils peuvent exécuter un ''thread'' durant quelques cycles successifs, plus d'une dizaine, voire plus. Cependant, il s'agit de processeurs sans exécution dans le désordre, qui sont bloqués dès qu'ils tombent sur une instruction dépendante. Et justement, ils masquent ces blocages en changeant de thread au lieu d'émettre des bulles de pipeline. La technique est beaucoup utilisée sur les cartes graphiques modernes, et sur certains processeurs spécialisés.
Le désavantage est qu'ils doivent intégrer des ''scoreboards'' pour gérer les dépendances de données, sauf sur quelques processeurs laissaient la détection des dépendances au compilateur ! 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, avant de changer de ''thread''.
Qu'il s'agisse des ''barrel processor'' ou des autres processeurs FGMT, tous ont une implémentation similaire. Contrairement aux processeurs CGMT, il n'y a pas de registre de ''thread ID'' unique. En effet, le processeur doit savoir, pour chaque instruction dans le pipeline, à quel ''thread'' elle appartient. Pour cela, les ''thread ID'' sont générés lors du chargement et propagés dans le pipeline en même temps que les instructions, où il est utilisé par le banc de registre, dans les ''load store queues'', etc. Pour résumer : propagation du ''thread ID'' dans le pipeline et disparition du registre de ''thread'' global.
Avec le FGMT, le processeur charge et décode les instructions, avant de les placer dans plusieurs files d'instruction. Il y a une file d'instruction par ''thread''. Le choix de la file d'instruction est réalisé par un multiplexeur, commandé par une super-unité d'émission qui décide quel ''thread'' émet ses instructions (''thread issue unit'').
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2.5|FGMT sur un processeur.]]
L'unité d'émission en question contient un ''scoreboard'' par ''thread'', et combine leurs résultats pour décider quel ''thread'' émet une instruction. Sur les ''barrel processor'', le ''scoreboard'' ne gère que les dépendances liées aux accès mémoire, pour gérer les ''threads'' mis en pause et les re-démarrer quand la lecture est terminée. Sur les autres processeurs, les ''scoreboard'' gèrent les dépendances entre instructions.
Les unités de choix de ''thread'' fonctionnent différemment entre les ''barrel processors'' et ceux qui peuvent exécuter plusieurs instructions successives d'un même ''thread''. Pour ces derniers, l'implémentation demande une coopération entre l'unité d'émission et l'unité de chargement. Le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Si un thread émet une instruction, ce même thread charge une instruction au même moment. Sauf si la file d'instruction du thread est déjà pleine, auquel cas un autre thread est choisi.
Il est possible de tenir compte du contenu de la fenêtre d'instruction pour décider quels ''threads'' sont éligibles au chargement. Il est possible de mettre en pause un ''thread'' si celui-ci accumule un peu trop d'instructions dans la fenêtre d'instruction. C'est en effet signe que ce ''thread'' est bloqué par un accès mémoire, une instruction multicycle ou des dépendances de données. À l'inverse, il est possible de prioriser les ''threads'' qui n'ont presque aucune instruction dans la fenêtre d'instruction. Car ce sont des ''threads'' qui s'exécutent rapidement au point de vider la fenêtre d'instruction. L'idée est de charger en priorité les instructions du ''thread'' pour lequel la file d'instruction est la moins remplie. L'unité de choix du ''thread'' utilise cette information pour déterminer quel ''thread'' charger au prochain cycle.
==Le ''Simultaneous multithreading'' : une spécificité des processeurs superscalaires==
Les deux techniques vues au-dessus peuvent s'adapter sur les processeurs à émission multiple, à savoir qui sont capables d'émettre plusieurs instructions simultanément. La seule contrainte est que l'implémentation du FGMT est compliquée sur les architectures superscalaires, ce qui fait que les ''barrel processors'' sont généralement des processeurs VLIW ou sans émission multiple. Mais il existe une technique de ''multithreading'' matériel spécialement pensée pour les processeurs à émission multiple : le ''Simultaneous multithreading''. La comprendre est assez simple si on la compare au FGMT et au CGMT.
Avec le ''multithreading'' temporel, lors d'un cycle d'horloge, les instructions émises appartiennent au même ''thread'' matériel. Il n'y a pas de situation où une instruction du ''thread'' 1 est émise en même temps qu'une instruction du ''thread'' 2.
{|
|[[File:CGMT sur processeur superscalaire.png|vignette|upright=1.5|CGMT sur processeur superscalaire]]
|[[File:FGMT sur processeur superscalaire.png|vignette|upright=1.5|FGMT sur processeur superscalaire]]
|}
Le '''Simultaneous Multi-Threading''', abrégé en SMT, permet d'émettre simultanément des instructions provenant de ''threads'' séparés. Elle fonctionne sur les processeurs superscalaires, mais n'est pas possible sur les processeurs VLIW.
[[File:Simultaneous Multi-Threading.png|centre|vignette|upright=2|Simultaneous Multi-Threading]]
===L'implémentation matérielle du SMT===
Dans les faits, tous les processeurs SMT sont des processeurs à exécution dans le désordre. Non pas que ce soit obligatoire, juste que le cout d'implémentation est bien plus faible sur un processeur à exécution dans le désordre. L'implémentation du SMT prend un processeur à exécution dans le désordre, ajoute plusieurs ''program counter'' et les circuits adéquats. L'unité d'émission choisit quelles instructions envoyer aux ALU, en utilisant l'exécution dans le désordre : tant qu'elles sont indépendantes, elles peuvent s’exécuter en parallèle. Et deux instructions de deux ''threads'' différents sont indépendantes ! Il y a donc un lien étroit entre exécution dans le désordre et SMT !
Ironiquement, avec l'amélioration des techniques d'exécution dans le désordre, le SMT commence à perdre de sa superbe. Plus l'exécution dans le désordre est efficace, plus le SMT est inutile. Si l'exécution dans le désordre est trop efficace, les cycles gâchés sont trop rares pour que le SMT ne servent pas à grande chose. Quand les cycles gâchés représentent grand maximum 10% du temps d’exécution grâce à l'usage de l’exécution dans le désordre, le SMT n'a plus grand-chose à remplir et le second programme s’exécuterait trop lentement pour ça vaille le coup.
Le SMT s'implémente généralement avec une seule fenêtre d’instruction et avec le renommage de registres. Le renommage de registres fait qu'on n'a pas à utiliser de fenêtrage de registres, juste à augmenter la taille du banc de registres. L'idée est que l'on concatène le ''thread ID'' au nom de registre avant de faire le renommage. Comme cela, on garantit que deux registres architecturaux identiques mais référencés dans des ''threads'' différents, correspondront à des registres physiques différents. Ainsi, l'unité d'émission a juste à vérifier les dépendances entre registres, pas besoin de gérer les ''thread ID''. En faisant cela, la logique d'émission est beaucoup plus simple. Par contre, les tables pour le renommage de registre sont dupliqués, avec autant de table que de ''thread'', chaque ''thread'' a la sienne.
Comme avec le FGMT, en cas des mauvaise prédiction de branchement, seules les instructions du ''thread'' fautif doivent être vidées. Pour cela, les files/fenêtres d'instruction doivent savoir à quel ''thread'' appartient tel ou telle instruction, pareil pour le ''Reorder Buffer''. Pour cela, on ajoute, dans chaque entrée de ces structures, le ''thread ID'' de l'instruction.
Avec le SMT, les différents ''threads'' doivent se partager certaines ressources matérielles : la fenêtre d'instruction, le banc de registres, la ''load store queue'', le cache, l'unité de prédiction de branchement ou d'autres structures matérielles. Le partage de ces ressources entre ''threads'' est dynamique, sauf pour quelques exceptions. La conséquence est qu'il peut y avoir une perte de performance si on lance deux ''threads'' et qu'il n'y a pas assez de ressources matérielles pour en profiter. Les ''threads'' entrent en compétition pour y accéder. Parlons un petit peu du partage du cache et de l'unité de prédiction de branchement.
===Le partage des caches===
La quasi-totalité des architectures multithreadées utilisent un cache partagé, et non des caches dédiés, ce qui impose de gérer le partage des caches entre les différents ''threads''.
La première idée est de partitionner le cache en plusieurs morceaux, avec un par ''thread''. Le '''partitionnement statique''' donne des portions égales à chaque programme, ce qui n'est pas toujours optimal mais a l'avantage d'être facile à implémenter. À l'opposé, le '''partitionnement dynamique''' partage le cache selon les besoins des ''thread'' en cours d'exécution. Si un programme a besoin de plus de place que l'autre, il peut réserver un peu plus de place que son concurrent. Mais la gestion du partitionnement est alors plus compliquée, et demande de partitionner efficacement le cache, sans quoi les deux programmes exécutés risquent de se marcher dessus et d'entrer en compétition pour le cache.
[[File:Partitionnement de l'Instruction Buffer avec l'hyperthreading.png|centre|vignette|upright=2|Partitionnement des caches avec l'hyperthreading.]]
Une autre possibilité est de ne pas partitionner et de laisser les différents ''threads'' accéder au cache comme bon leur semble. Si le cache est physiquement tagué, il n'y a aucune modification à faire. Mais si le cache est virtuellement tagué, une même adresse virtuelle peut correspondre à des adresses physiques différentes pour deux ''threads'' différents. Pour éviter cela, il faut ajouter l'identifiant de ''thread'' dans chaque ligne de cache, dans les bits de contrôle. La détection des succès ou défaut de cache tient compte de l'identifiant de ''thread'' lors des comparaisons de tags, afin d'éviter qu'un ''thread'' lise une donnée d'un autre ''thread''.
À l'inverse des processeurs mono-cœur, les processeurs multithreadés préfèrent des caches dont le débit binaire est plus important que la latence mémoire. La raison est que deux ''threads'' consomment deux fois plus de données qu'un seul et le débit binaire du cache doit suivre. Par contre, la latence est moins importante car les cycles gâchés qu'elle créé sont remplis par le ''multithreading'' matériel. Si un défaut de cache prend 4 cycles, le ''multithreading'' matériel va rapidement trouver des instructions à exécuter pendant ces cycles.
Tout ce qui vient d'être dit s'applique aussi sur la TLB. Il est possible de la partitionner entre plusieurs ''threads''. La plupart du temps, le partitionnement est statique sur les TLB dédiées aux instructions, alors que les TLB pour les données sont partagées dynamiquement. C'est le cas sur les architectures Skylake d'Intel, où les 128 entrées de la TLB d'instruction de niveau 1 ont découpées en deux sections de 64 entrées, une par programme/''thread'', les autres TLB étant partitionnées dynamiquement. Une autre solution est d'ajouter un identifiant de ''thread'' dans chaque entrée de la TLB, sur le même principe que celui vu dans le chapitre sur la TLB. Le gain en performance est bien plus important, pour un cout en hardware limité.
===L'unité de prédiction de branchement===
Un autre cache est lui aussi partitionné ou complété avec des ''thread ID'' : le ''branch target buffer''. Rappelons que c'est un cache spécialisé pour les adresses de branchements. L'unité de prédiction de branchements doit idéalement séparer les branchements entre chaque ''thread'' pour obtenir de bonnes performances, soit en partitionnant le BTB, soit en ajoutant ajouter un ''Thread ID'' dans chaque entrée de ce cache. Mais ce n'est pas obligatoire.
Les unités de prédiction de branchement vues dans les chapitres précédents peuvent parfaitement fonctionner telles quelles sur un processeur multithreadé. Le ''branch target buffer'' mémorise alors des branchements de ''threads'' différents, il ne sait pas à quel ''thread'' appartient tel branchement, mais le tout fonctionne. Le problème est que les prédictions pour un branchement sont parasitées par les branchements d'autres ''threads'', surtout pour les unités se basant sur un historique global. Malgré tout, on obtient un résultat tolérable, les performances sont assez bonnes et cela s'explique avec ce qui suit.
Déjà, il arrive que les branchements des autres ''threads'' aient un effet positif sur la prédiction pour un ''thread''. De telles interférences positives sont rares, mais possible si les deux ''threads'' travaillent sur des données partagées, et c'est alors les unités de prédiction de branchement normales, sans partitionnement ni ''thread ID'' qui fonctionnent mieux, sous certaines circonstances.
De plus, quand une mauvaise prédiction de branchement est détectée, il faut vider le pipeline des instructions chargées à tort. Et les instructions fautives appartiennent toutes à un même ''thread'', les instructions des autres ''threads'' ne sont pas concernées. Le vidage du pipeline est donc sélectif. Là encore, la vidange du pipeline se base sur les ''Thread ID'' propagés dans le pipeline. Vu que le vidage du pipeline est sélectif, partiel, la perte de performance en cas de mauvaise prédiction de branchement est donc plus faible, et cela surcompense le fait que les prédictions de branchement sont parasitées par les branchements d’autres ''threads''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Architectures multiprocesseurs et multicœurs
| prevText=Architectures multiprocesseurs et multicœurs
| next=Les architectures à parallélisme de données
| nextText=Les architectures à parallélisme de données
}}
</noinclude>
tp7bxvrhjw6r5lj6f809alxc6prj36s
763260
763259
2026-04-08T14:48:55Z
Mewtow
31375
/* Le multithreading temporel */
763260
wikitext
text/x-wiki
Vous pensez surement qu'il faut obligatoirement plusieurs cœurs pour exécuter plusieurs programmes en parallèle, mais sachez que c'est faux ! Les processeurs mono-cœur en sont capables, en alternant entre les programmes à exécuter. Plusieurs programmes s’exécutent donc sur le même processeur, mais chacun à leur tour et non en même temps. D'ordinaire, cette alternance est gérée par le système d'exploitation, mais certains processeurs gèrent cette alternance eux-mêmes, directement au niveau matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, le processeur donne la main à un autre programme. Les processeurs en question sont appelés des processeurs multithreadés, ou encore des '''architectures multithreadées''', en référence au terme ''thread'', qui est plus ou moins équivalent à celui de programme dans ce cours. Ils exécutent un ''thread'' à la fois, mais changent de ''thread'' plus ou moins régulièrement.
Pour comprendre le pourquoi des architectures multithreadées, il faut rappeler qu'il arrive que l'unité de calcul d'un processeur ne fasse rien, par exemple pendant que le processeur accède à la mémoire. Les cycles d'horloge où l'unité de calcul est inutilisée sont des '''cycles gâchés'''. L’exécution dans le désordre réduit ces cycles gâchés, mais les architectures multithreadées sont une solution alternative et complémentaire. Elles visent à ce que les cycles gâchés d'un ''thread'' soient remplis par les calculs d'autre ''thread''.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Le ''Simultaneous MultiThreading'' est spécifique aux processeurs superscalaires, alors que les autres techniques fonctionnent sur tous les processeurs, qu'ils soient superscalaire ou simple-émission.
: Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''.
==Le multithreading temporel==
Le FGMT et le CGMT sont regroupés sous le terme de '''multithreading temporel'''. Les deux partagent en effet un même point commun : à chaque cycle, les instructions émises par l'unité d'émission proviennent d'un seul programme. La distinction n'a de sens que sur les processeurs à exécution multiple, ce sera plus clair dans la suite du chapitre quand on comparera ''multithreading'' temporel et SMT. La différence entre FGMT et CGMT est la fréquence des changements de ''thread'' : à chaque cycle ou presque pour le FGMT, lors d'un évènement bien précis pour le CGMT.
Le processeur subit des changements pour gérer plusieurs ''threads'' en cours d'exécution. En premier lieu, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement''', dont le fonctionnement dépend du processeur, comme nous le verrons plus bas. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
Ensuite, le processeur attribue un numéro à chaque ''thread'', appelé le ''thread ID''. Il y en autant que de ''threads'' exécutables simultanément par le processeur. Par exemple, si le processeur gère au maximum 8 ''threads'' simultanés, l'identifiant de ''thread'' va de 0 à 7 et est codé sur 3 bits. Le ''thread ID'' est propagé dans le pipeline, histoire de savoir à quel ''thread'' appartient l'instruction en cours. Il sert notamment pour adresser le banc de registre, mais aussi pour d'autres choses.
Les processeurs à ''multithreading'' temporel sont généralement des processeurs sans exécution dans le désordre, et n'ont donc pas de renommage de registres. Avec eux, il est obligatoire de dupliquer les registres pour que chaque programme ait son ensemble de registres architecturaux rien qu'à lui. Cela demande soit un banc de registre par programme, soit un banc de registre commun géré par fenêtrage de registre (chaque programme ayant sa propre fenêtre de registres).
Il faut aussi dupliquer les ''load-store queue'', pour séparer les lectures/écritures en attente de chaque ''thread''. Là encore, il est possible d'utiliser une ''load-store queue'' unique dans laquelle on ajoute des informations pour savoir à quel ''thread'' appartient telle ou telle lecture/écriture.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2.5|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''Coarse Grained Multithreading''===
[[File:Coarse Grained Multithreading.png|vignette|upright=1|Coarse Grained Multithreading.]]
Le '''Coarse Grained Multithreading''' change de programme quand un évènement bien précis a lieu. L'évènement en question fait que l'ALU restera inutilisé pendant un moment : accès à la mémoire, branchements, etc.
Sur certains processeurs CGMT, il y a une instruction précise pour changer de ''thread''. La commutation de ''thread'' est alors totalement ou partiellement décidée par le logiciel. Mais il s'agit là d'un cas particulier.
Le cas le plus fréquent est de changer de ''thread'' lors d'un défaut de cache. Vu que l'accès à la RAM est quelque chose de très lent, il est intéressant d'exécuter des instructions d'un autre ''thread'' pour recouvrir l'accès à la RAM. L'idée est similaire à ce qu'on a avec les lectures non-bloquantes et/ou l'exécution dans le désordre : pendant que l'unité d'accès mémoire gère le défaut de cache, on alimente l'unité de calcul avec des calculs indépendants. Sauf qu'avec le ''multithreading'', les calculs proviennent d'un autre ''thread''.
Un point important est que le cache doit être un cache non-bloquant, sans quoi le ''multithreading'' matériel ne fonctionne tout simplement pas. Par exemple, prenons une architecture qui change de ''thread'' à chaque défaut de cache. Si on veut supporter plus de deux ''threads'', il faut que plusieurs ''threads'' subissent un défaut de cache pour que le ''multithreading'' ait de l'intérêt, ce qui implique un cache non-bloquant.
[[File:Multithreading et mitigation de la latence mémoire.png|centre|vignette|upright=1.5|Multithreading et mitigation de la latence mémoire.]]
Avec le CGMT, on est certain que toutes les instructions en cours d’exécution appartiennent au même ''thread''. Quand il passe d'un ''thread'' à l'autre, le processeur attend naturellement que le pipeline soit complétement vidé avant de charger les instructions du ''thread'' suivant. Le processeur peut donc se débrouiller avec un simple '''registre de ''thread''''' qui mémorise le ''thread ID'' du ''thread'' en cours d'exécution. Le registre de ''thread'' est directement connecté à l'entrée d'adresse du banc de registre, pour gérer le fenêtrage de registres. Il est aussi connecté à la ''load-store queue'', et surtout au multiplexeur de choix de ''thread'', dans l'unité de chargement.
[[File:Implementation du CGMT.png|centre|vignette|upright=2|Implémentation du CGMT]]
L'unité d'ordonnancement détermine quel ''thread'' charger dans le pipeline, elle sélectionne un ''thread ID''. Pour faciliter son travail, cette unité contient un registre qui mémorise quels sont les ''threads'' actifs. Les ''threads'' bloqués par un défaut de cache sont marqués comme inactifs tant que le défaut de cache n'est pas résolu. Évidemment, dès qu'un défaut de cache est résolu, l'unité d'accès mémoire prévient l'unité de choix de ''thread'' pour que celui-ci marque le ''thread'' adéquat comme de nouveau actif. Le registre est composé de N bits sur un processeur qui gère N threads maximum : chaque bit est associé à un ''thread'' et indique s'il est actif ou non.
===Le ''Fine Grained MultiThreading''===
[[File:Fine Grained Multithreading.png|vignette|upright=2|''Barrel processor''.]]
Le '''''Fine Grained Multithreading''''' regroupe deux types de processeurs différents. Le premier type est celui des '''''barrel processors''''', qui changent de programme à chaque cycle d'horloge. Idéalement, chaque étage du pipeline est utilisé par une instruction différente. L'avantage est que, à l'intérieur d'un ''thread'', une nouvelle instruction démarre quand la précédente est terminée. Le résultat d'une instruction est déjà dans les registres quand l'instruction suivante s’exécute. Il n'y a donc pas de dépendances entre instructions successives, les circuits liés aux dépendances de données sont fortement simplifiés, le réseau de contournement de l'ALU disparait, l'unité de prédiction de branchement disparait (car le résultat d'un branchement est connu avant que le même ''thread'' exécute sa prochaine instruction).
Mais les accès mémoire sont gérés à part des autres instructions. Lorsque un ''thread'' effectue un accès mémoire, il est mis en pause et d'autres ''threads'' sont lancés à sa place. Prenons l'exemple d'un processeur qui gère 16 ''threads'' maximum : si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Mais pour que cela fonctionne sans encombre, on est obligé d'avoir un nombre assez élevé de programmes en cours d’exécution. Pour donner un exemple, le processeur MTA était capable d'exécuter un 128 ''threads'' maximum, pour un pipeline de 21 étages. La marge est élevée, mais cela compense le fait que le processeur n'a pas de cache, avec des accès mémoire prenant 150-170 cycles d'horloge ! Le désavantage est que les registres étaient dupliqués 128 fois ! Le processeur avait près d'un millier de registres, ce qui est énorme pour l'époque.
[[File:Full multithreading.png|vignette|upright=2|Processeur à ''Fine Grained Multithreading''.]]
Utiliser un ''barrel processor'' au mieux demande donc qu'il y ait un grand nombre de ''threads'' en cours d'exécution. Mais quelques processeurs FGMT font autrement, en se rapprochant du CGMT. Ils peuvent exécuter un ''thread'' durant quelques cycles successifs, plus d'une dizaine, voire plus. Cependant, il s'agit de processeurs sans exécution dans le désordre, qui sont bloqués dès qu'ils tombent sur une instruction dépendante. Et justement, ils masquent ces blocages en changeant de thread au lieu d'émettre des bulles de pipeline. La technique est beaucoup utilisée sur les cartes graphiques modernes, et sur certains processeurs spécialisés.
Le désavantage est qu'ils doivent intégrer des ''scoreboards'' pour gérer les dépendances de données, sauf sur quelques processeurs laissaient la détection des dépendances au compilateur ! 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, avant de changer de ''thread''.
Qu'il s'agisse des ''barrel processor'' ou des autres processeurs FGMT, tous ont une implémentation similaire. Contrairement aux processeurs CGMT, il n'y a pas de registre de ''thread ID'' unique. En effet, le processeur doit savoir, pour chaque instruction dans le pipeline, à quel ''thread'' elle appartient. Pour cela, les ''thread ID'' sont générés lors du chargement et propagés dans le pipeline en même temps que les instructions, où il est utilisé par le banc de registre, dans les ''load store queues'', etc. Pour résumer : propagation du ''thread ID'' dans le pipeline et disparition du registre de ''thread'' global.
Avec le FGMT, le processeur charge et décode les instructions, avant de les placer dans plusieurs files d'instruction. Il y a une file d'instruction par ''thread''. Le choix de la file d'instruction est réalisé par un multiplexeur, commandé par une super-unité d'émission qui décide quel ''thread'' émet ses instructions (''thread issue unit'').
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2.5|FGMT sur un processeur.]]
L'unité d'émission en question contient un ''scoreboard'' par ''thread'', et combine leurs résultats pour décider quel ''thread'' émet une instruction. Sur les ''barrel processor'', le ''scoreboard'' ne gère que les dépendances liées aux accès mémoire, pour gérer les ''threads'' mis en pause et les re-démarrer quand la lecture est terminée. Sur les autres processeurs, les ''scoreboard'' gèrent les dépendances entre instructions.
Les unités de choix de ''thread'' fonctionnent différemment entre les ''barrel processors'' et ceux qui peuvent exécuter plusieurs instructions successives d'un même ''thread''. Pour ces derniers, l'implémentation demande une coopération entre l'unité d'émission et l'unité de chargement. Le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Si un thread émet une instruction, ce même thread charge une instruction au même moment. Sauf si la file d'instruction du thread est déjà pleine, auquel cas un autre thread est choisi.
Il est possible de tenir compte du contenu de la fenêtre d'instruction pour décider quels ''threads'' sont éligibles au chargement. Il est possible de mettre en pause un ''thread'' si celui-ci accumule un peu trop d'instructions dans la fenêtre d'instruction. C'est en effet signe que ce ''thread'' est bloqué par un accès mémoire, une instruction multicycle ou des dépendances de données. À l'inverse, il est possible de prioriser les ''threads'' qui n'ont presque aucune instruction dans la fenêtre d'instruction. Car ce sont des ''threads'' qui s'exécutent rapidement au point de vider la fenêtre d'instruction. L'idée est de charger en priorité les instructions du ''thread'' pour lequel la file d'instruction est la moins remplie. L'unité de choix du ''thread'' utilise cette information pour déterminer quel ''thread'' charger au prochain cycle.
==Le ''Simultaneous multithreading'' : une spécificité des processeurs superscalaires==
Les deux techniques vues au-dessus peuvent s'adapter sur les processeurs à émission multiple, à savoir qui sont capables d'émettre plusieurs instructions simultanément. La seule contrainte est que l'implémentation du FGMT est compliquée sur les architectures superscalaires, ce qui fait que les ''barrel processors'' sont généralement des processeurs VLIW ou sans émission multiple. Mais il existe une technique de ''multithreading'' matériel spécialement pensée pour les processeurs à émission multiple : le ''Simultaneous multithreading''. La comprendre est assez simple si on la compare au FGMT et au CGMT.
Avec le ''multithreading'' temporel, lors d'un cycle d'horloge, les instructions émises appartiennent au même ''thread'' matériel. Il n'y a pas de situation où une instruction du ''thread'' 1 est émise en même temps qu'une instruction du ''thread'' 2.
{|
|[[File:CGMT sur processeur superscalaire.png|vignette|upright=1.5|CGMT sur processeur superscalaire]]
|[[File:FGMT sur processeur superscalaire.png|vignette|upright=1.5|FGMT sur processeur superscalaire]]
|}
Le '''Simultaneous Multi-Threading''', abrégé en SMT, permet d'émettre simultanément des instructions provenant de ''threads'' séparés. Elle fonctionne sur les processeurs superscalaires, mais n'est pas possible sur les processeurs VLIW.
[[File:Simultaneous Multi-Threading.png|centre|vignette|upright=2|Simultaneous Multi-Threading]]
===L'implémentation matérielle du SMT===
Dans les faits, tous les processeurs SMT sont des processeurs à exécution dans le désordre. Non pas que ce soit obligatoire, juste que le cout d'implémentation est bien plus faible sur un processeur à exécution dans le désordre. L'implémentation du SMT prend un processeur à exécution dans le désordre, ajoute plusieurs ''program counter'' et les circuits adéquats. L'unité d'émission choisit quelles instructions envoyer aux ALU, en utilisant l'exécution dans le désordre : tant qu'elles sont indépendantes, elles peuvent s’exécuter en parallèle. Et deux instructions de deux ''threads'' différents sont indépendantes ! Il y a donc un lien étroit entre exécution dans le désordre et SMT !
Ironiquement, avec l'amélioration des techniques d'exécution dans le désordre, le SMT commence à perdre de sa superbe. Plus l'exécution dans le désordre est efficace, plus le SMT est inutile. Si l'exécution dans le désordre est trop efficace, les cycles gâchés sont trop rares pour que le SMT ne servent pas à grande chose. Quand les cycles gâchés représentent grand maximum 10% du temps d’exécution grâce à l'usage de l’exécution dans le désordre, le SMT n'a plus grand-chose à remplir et le second programme s’exécuterait trop lentement pour ça vaille le coup.
Le SMT s'implémente généralement avec une seule fenêtre d’instruction et avec le renommage de registres. Le renommage de registres fait qu'on n'a pas à utiliser de fenêtrage de registres, juste à augmenter la taille du banc de registres. L'idée est que l'on concatène le ''thread ID'' au nom de registre avant de faire le renommage. Comme cela, on garantit que deux registres architecturaux identiques mais référencés dans des ''threads'' différents, correspondront à des registres physiques différents. Ainsi, l'unité d'émission a juste à vérifier les dépendances entre registres, pas besoin de gérer les ''thread ID''. En faisant cela, la logique d'émission est beaucoup plus simple. Par contre, les tables pour le renommage de registre sont dupliqués, avec autant de table que de ''thread'', chaque ''thread'' a la sienne.
Comme avec le FGMT, en cas des mauvaise prédiction de branchement, seules les instructions du ''thread'' fautif doivent être vidées. Pour cela, les files/fenêtres d'instruction doivent savoir à quel ''thread'' appartient tel ou telle instruction, pareil pour le ''Reorder Buffer''. Pour cela, on ajoute, dans chaque entrée de ces structures, le ''thread ID'' de l'instruction.
Avec le SMT, les différents ''threads'' doivent se partager certaines ressources matérielles : la fenêtre d'instruction, le banc de registres, la ''load store queue'', le cache, l'unité de prédiction de branchement ou d'autres structures matérielles. Le partage de ces ressources entre ''threads'' est dynamique, sauf pour quelques exceptions. La conséquence est qu'il peut y avoir une perte de performance si on lance deux ''threads'' et qu'il n'y a pas assez de ressources matérielles pour en profiter. Les ''threads'' entrent en compétition pour y accéder. Parlons un petit peu du partage du cache et de l'unité de prédiction de branchement.
===Le partage des caches===
La quasi-totalité des architectures multithreadées utilisent un cache partagé, et non des caches dédiés, ce qui impose de gérer le partage des caches entre les différents ''threads''.
La première idée est de partitionner le cache en plusieurs morceaux, avec un par ''thread''. Le '''partitionnement statique''' donne des portions égales à chaque programme, ce qui n'est pas toujours optimal mais a l'avantage d'être facile à implémenter. À l'opposé, le '''partitionnement dynamique''' partage le cache selon les besoins des ''thread'' en cours d'exécution. Si un programme a besoin de plus de place que l'autre, il peut réserver un peu plus de place que son concurrent. Mais la gestion du partitionnement est alors plus compliquée, et demande de partitionner efficacement le cache, sans quoi les deux programmes exécutés risquent de se marcher dessus et d'entrer en compétition pour le cache.
[[File:Partitionnement de l'Instruction Buffer avec l'hyperthreading.png|centre|vignette|upright=2|Partitionnement des caches avec l'hyperthreading.]]
Une autre possibilité est de ne pas partitionner et de laisser les différents ''threads'' accéder au cache comme bon leur semble. Si le cache est physiquement tagué, il n'y a aucune modification à faire. Mais si le cache est virtuellement tagué, une même adresse virtuelle peut correspondre à des adresses physiques différentes pour deux ''threads'' différents. Pour éviter cela, il faut ajouter l'identifiant de ''thread'' dans chaque ligne de cache, dans les bits de contrôle. La détection des succès ou défaut de cache tient compte de l'identifiant de ''thread'' lors des comparaisons de tags, afin d'éviter qu'un ''thread'' lise une donnée d'un autre ''thread''.
À l'inverse des processeurs mono-cœur, les processeurs multithreadés préfèrent des caches dont le débit binaire est plus important que la latence mémoire. La raison est que deux ''threads'' consomment deux fois plus de données qu'un seul et le débit binaire du cache doit suivre. Par contre, la latence est moins importante car les cycles gâchés qu'elle créé sont remplis par le ''multithreading'' matériel. Si un défaut de cache prend 4 cycles, le ''multithreading'' matériel va rapidement trouver des instructions à exécuter pendant ces cycles.
Tout ce qui vient d'être dit s'applique aussi sur la TLB. Il est possible de la partitionner entre plusieurs ''threads''. La plupart du temps, le partitionnement est statique sur les TLB dédiées aux instructions, alors que les TLB pour les données sont partagées dynamiquement. C'est le cas sur les architectures Skylake d'Intel, où les 128 entrées de la TLB d'instruction de niveau 1 ont découpées en deux sections de 64 entrées, une par programme/''thread'', les autres TLB étant partitionnées dynamiquement. Une autre solution est d'ajouter un identifiant de ''thread'' dans chaque entrée de la TLB, sur le même principe que celui vu dans le chapitre sur la TLB. Le gain en performance est bien plus important, pour un cout en hardware limité.
===L'unité de prédiction de branchement===
Un autre cache est lui aussi partitionné ou complété avec des ''thread ID'' : le ''branch target buffer''. Rappelons que c'est un cache spécialisé pour les adresses de branchements. L'unité de prédiction de branchements doit idéalement séparer les branchements entre chaque ''thread'' pour obtenir de bonnes performances, soit en partitionnant le BTB, soit en ajoutant ajouter un ''Thread ID'' dans chaque entrée de ce cache. Mais ce n'est pas obligatoire.
Les unités de prédiction de branchement vues dans les chapitres précédents peuvent parfaitement fonctionner telles quelles sur un processeur multithreadé. Le ''branch target buffer'' mémorise alors des branchements de ''threads'' différents, il ne sait pas à quel ''thread'' appartient tel branchement, mais le tout fonctionne. Le problème est que les prédictions pour un branchement sont parasitées par les branchements d'autres ''threads'', surtout pour les unités se basant sur un historique global. Malgré tout, on obtient un résultat tolérable, les performances sont assez bonnes et cela s'explique avec ce qui suit.
Déjà, il arrive que les branchements des autres ''threads'' aient un effet positif sur la prédiction pour un ''thread''. De telles interférences positives sont rares, mais possible si les deux ''threads'' travaillent sur des données partagées, et c'est alors les unités de prédiction de branchement normales, sans partitionnement ni ''thread ID'' qui fonctionnent mieux, sous certaines circonstances.
De plus, quand une mauvaise prédiction de branchement est détectée, il faut vider le pipeline des instructions chargées à tort. Et les instructions fautives appartiennent toutes à un même ''thread'', les instructions des autres ''threads'' ne sont pas concernées. Le vidage du pipeline est donc sélectif. Là encore, la vidange du pipeline se base sur les ''Thread ID'' propagés dans le pipeline. Vu que le vidage du pipeline est sélectif, partiel, la perte de performance en cas de mauvaise prédiction de branchement est donc plus faible, et cela surcompense le fait que les prédictions de branchement sont parasitées par les branchements d’autres ''threads''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Architectures multiprocesseurs et multicœurs
| prevText=Architectures multiprocesseurs et multicœurs
| next=Les architectures à parallélisme de données
| nextText=Les architectures à parallélisme de données
}}
</noinclude>
bykafmpwa5ee3h71uwhlwd100m6e0kq
763290
763260
2026-04-08T17:13:58Z
Mewtow
31375
/* Le multithreading temporel */
763290
wikitext
text/x-wiki
Vous pensez surement qu'il faut obligatoirement plusieurs cœurs pour exécuter plusieurs programmes en parallèle, mais sachez que c'est faux ! Les processeurs mono-cœur en sont capables, en alternant entre les programmes à exécuter. Plusieurs programmes s’exécutent donc sur le même processeur, mais chacun à leur tour et non en même temps. D'ordinaire, cette alternance est gérée par le système d'exploitation, mais certains processeurs gèrent cette alternance eux-mêmes, directement au niveau matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, le processeur donne la main à un autre programme. Les processeurs en question sont appelés des processeurs multithreadés, ou encore des '''architectures multithreadées''', en référence au terme ''thread'', qui est plus ou moins équivalent à celui de programme dans ce cours. Ils exécutent un ''thread'' à la fois, mais changent de ''thread'' plus ou moins régulièrement.
Pour comprendre le pourquoi des architectures multithreadées, il faut rappeler qu'il arrive que l'unité de calcul d'un processeur ne fasse rien, par exemple pendant que le processeur accède à la mémoire. Les cycles d'horloge où l'unité de calcul est inutilisée sont des '''cycles gâchés'''. L’exécution dans le désordre réduit ces cycles gâchés, mais les architectures multithreadées sont une solution alternative et complémentaire. Elles visent à ce que les cycles gâchés d'un ''thread'' soient remplis par les calculs d'autre ''thread''.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Le ''Simultaneous MultiThreading'' est spécifique aux processeurs superscalaires, alors que les autres techniques fonctionnent sur tous les processeurs, qu'ils soient superscalaire ou simple-émission.
: Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''.
==Le multithreading temporel==
Le FGMT et le CGMT sont regroupés sous le terme de '''multithreading temporel'''. Les deux partagent en effet un même point commun : à chaque cycle, les instructions émises par l'unité d'émission proviennent d'un seul programme. La distinction n'a de sens que sur les processeurs à exécution multiple, ce sera plus clair dans la suite du chapitre quand on comparera ''multithreading'' temporel et SMT. La différence entre FGMT et CGMT est la fréquence des changements de ''thread'' : à chaque cycle ou presque pour le FGMT, lors d'un évènement bien précis pour le CGMT.
Le processeur subit des changements pour gérer plusieurs ''threads'' en cours d'exécution. En premier lieu, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement''', dont le fonctionnement dépend du processeur, comme nous le verrons plus bas. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
Ensuite, le processeur attribue un numéro à chaque ''thread'', appelé le ''thread ID''. Il y en autant que de ''threads'' exécutables simultanément par le processeur. Par exemple, si le processeur gère au maximum 8 ''threads'' simultanés, l'identifiant de ''thread'' va de 0 à 7 et est codé sur 3 bits. Le ''thread ID'' est propagé dans le pipeline, histoire de savoir à quel ''thread'' appartient l'instruction en cours. Il sert notamment pour adresser le banc de registre, mais aussi pour d'autres choses.
Les processeurs à ''multithreading'' temporel sont généralement des processeurs sans exécution dans le désordre, et n'ont donc pas de renommage de registres. Il est donc obligatoire de dupliquer les registres, pour que chaque programme ait ses registres architecturaux rien qu'à lui. Cela demande soit un banc de registre par programme, soit un banc de registre commun géré par fenêtrage de registre (chaque programme ayant sa propre fenêtre de registres). Il faut aussi dupliquer les ''load-store queue'', pour séparer les lectures/écritures en attente de chaque ''thread''. Il est possible d'utiliser une ''load-store queue'' unique dans laquelle chaque lecture/écriture mémorise le ''thread ID'', pour identifier le ''thread'' qui l'a émise.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2.5|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''Coarse Grained Multithreading''===
[[File:Coarse Grained Multithreading.png|vignette|upright=1|Coarse Grained Multithreading.]]
Le '''Coarse Grained Multithreading''' change de programme quand un évènement bien précis a lieu. L'évènement en question fait que l'ALU restera inutilisé pendant un moment : accès à la mémoire, branchements, etc.
Sur certains processeurs CGMT, il y a une instruction précise pour changer de ''thread''. La commutation de ''thread'' est alors totalement ou partiellement décidée par le logiciel. Mais il s'agit là d'un cas particulier.
Le cas le plus fréquent est de changer de ''thread'' lors d'un défaut de cache. Vu que l'accès à la RAM est quelque chose de très lent, il est intéressant d'exécuter des instructions d'un autre ''thread'' pour recouvrir l'accès à la RAM. L'idée est similaire à ce qu'on a avec les lectures non-bloquantes et/ou l'exécution dans le désordre : pendant que l'unité d'accès mémoire gère le défaut de cache, on alimente l'unité de calcul avec des calculs indépendants. Sauf qu'avec le ''multithreading'', les calculs proviennent d'un autre ''thread''.
Un point important est que le cache doit être un cache non-bloquant, sans quoi le ''multithreading'' matériel ne fonctionne tout simplement pas. Par exemple, prenons une architecture qui change de ''thread'' à chaque défaut de cache. Si on veut supporter plus de deux ''threads'', il faut que plusieurs ''threads'' subissent un défaut de cache pour que le ''multithreading'' ait de l'intérêt, ce qui implique un cache non-bloquant.
[[File:Multithreading et mitigation de la latence mémoire.png|centre|vignette|upright=1.5|Multithreading et mitigation de la latence mémoire.]]
Avec le CGMT, on est certain que toutes les instructions en cours d’exécution appartiennent au même ''thread''. Quand il passe d'un ''thread'' à l'autre, le processeur attend naturellement que le pipeline soit complétement vidé avant de charger les instructions du ''thread'' suivant. Le processeur peut donc se débrouiller avec un simple '''registre de ''thread''''' qui mémorise le ''thread ID'' du ''thread'' en cours d'exécution. Le registre de ''thread'' est directement connecté à l'entrée d'adresse du banc de registre, pour gérer le fenêtrage de registres. Il est aussi connecté à la ''load-store queue'', et surtout au multiplexeur de choix de ''thread'', dans l'unité de chargement.
[[File:Implementation du CGMT.png|centre|vignette|upright=2|Implémentation du CGMT]]
L'unité d'ordonnancement détermine quel ''thread'' charger dans le pipeline, elle sélectionne un ''thread ID''. Pour faciliter son travail, cette unité contient un registre qui mémorise quels sont les ''threads'' actifs. Les ''threads'' bloqués par un défaut de cache sont marqués comme inactifs tant que le défaut de cache n'est pas résolu. Évidemment, dès qu'un défaut de cache est résolu, l'unité d'accès mémoire prévient l'unité de choix de ''thread'' pour que celui-ci marque le ''thread'' adéquat comme de nouveau actif. Le registre est composé de N bits sur un processeur qui gère N threads maximum : chaque bit est associé à un ''thread'' et indique s'il est actif ou non.
===Le ''Fine Grained MultiThreading''===
[[File:Fine Grained Multithreading.png|vignette|upright=2|''Barrel processor''.]]
Le '''''Fine Grained Multithreading''''' regroupe deux types de processeurs différents. Le premier type est celui des '''''barrel processors''''', qui changent de programme à chaque cycle d'horloge. Idéalement, chaque étage du pipeline est utilisé par une instruction différente. L'avantage est que, à l'intérieur d'un ''thread'', une nouvelle instruction démarre quand la précédente est terminée. Le résultat d'une instruction est déjà dans les registres quand l'instruction suivante s’exécute. Il n'y a donc pas de dépendances entre instructions successives, les circuits liés aux dépendances de données sont fortement simplifiés, le réseau de contournement de l'ALU disparait, l'unité de prédiction de branchement disparait (car le résultat d'un branchement est connu avant que le même ''thread'' exécute sa prochaine instruction).
Mais les accès mémoire sont gérés à part des autres instructions. Lorsque un ''thread'' effectue un accès mémoire, il est mis en pause et d'autres ''threads'' sont lancés à sa place. Prenons l'exemple d'un processeur qui gère 16 ''threads'' maximum : si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Mais pour que cela fonctionne sans encombre, on est obligé d'avoir un nombre assez élevé de programmes en cours d’exécution. Pour donner un exemple, le processeur MTA était capable d'exécuter un 128 ''threads'' maximum, pour un pipeline de 21 étages. La marge est élevée, mais cela compense le fait que le processeur n'a pas de cache, avec des accès mémoire prenant 150-170 cycles d'horloge ! Le désavantage est que les registres étaient dupliqués 128 fois ! Le processeur avait près d'un millier de registres, ce qui est énorme pour l'époque.
[[File:Full multithreading.png|vignette|upright=2|Processeur à ''Fine Grained Multithreading''.]]
Utiliser un ''barrel processor'' au mieux demande donc qu'il y ait un grand nombre de ''threads'' en cours d'exécution. Mais quelques processeurs FGMT font autrement, en se rapprochant du CGMT. Ils peuvent exécuter un ''thread'' durant quelques cycles successifs, plus d'une dizaine, voire plus. Cependant, il s'agit de processeurs sans exécution dans le désordre, qui sont bloqués dès qu'ils tombent sur une instruction dépendante. Et justement, ils masquent ces blocages en changeant de thread au lieu d'émettre des bulles de pipeline. La technique est beaucoup utilisée sur les cartes graphiques modernes, et sur certains processeurs spécialisés.
Le désavantage est qu'ils doivent intégrer des ''scoreboards'' pour gérer les dépendances de données, sauf sur quelques processeurs laissaient la détection des dépendances au compilateur ! 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, avant de changer de ''thread''.
Qu'il s'agisse des ''barrel processor'' ou des autres processeurs FGMT, tous ont une implémentation similaire. Contrairement aux processeurs CGMT, il n'y a pas de registre de ''thread ID'' unique. En effet, le processeur doit savoir, pour chaque instruction dans le pipeline, à quel ''thread'' elle appartient. Pour cela, les ''thread ID'' sont générés lors du chargement et propagés dans le pipeline en même temps que les instructions, où il est utilisé par le banc de registre, dans les ''load store queues'', etc. Pour résumer : propagation du ''thread ID'' dans le pipeline et disparition du registre de ''thread'' global.
Avec le FGMT, le processeur charge et décode les instructions, avant de les placer dans plusieurs files d'instruction. Il y a une file d'instruction par ''thread''. Le choix de la file d'instruction est réalisé par un multiplexeur, commandé par une super-unité d'émission qui décide quel ''thread'' émet ses instructions (''thread issue unit'').
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2.5|FGMT sur un processeur.]]
L'unité d'émission en question contient un ''scoreboard'' par ''thread'', et combine leurs résultats pour décider quel ''thread'' émet une instruction. Sur les ''barrel processor'', le ''scoreboard'' ne gère que les dépendances liées aux accès mémoire, pour gérer les ''threads'' mis en pause et les re-démarrer quand la lecture est terminée. Sur les autres processeurs, les ''scoreboard'' gèrent les dépendances entre instructions.
Les unités de choix de ''thread'' fonctionnent différemment entre les ''barrel processors'' et ceux qui peuvent exécuter plusieurs instructions successives d'un même ''thread''. Pour ces derniers, l'implémentation demande une coopération entre l'unité d'émission et l'unité de chargement. Le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Si un thread émet une instruction, ce même thread charge une instruction au même moment. Sauf si la file d'instruction du thread est déjà pleine, auquel cas un autre thread est choisi.
Il est possible de tenir compte du contenu de la fenêtre d'instruction pour décider quels ''threads'' sont éligibles au chargement. Il est possible de mettre en pause un ''thread'' si celui-ci accumule un peu trop d'instructions dans la fenêtre d'instruction. C'est en effet signe que ce ''thread'' est bloqué par un accès mémoire, une instruction multicycle ou des dépendances de données. À l'inverse, il est possible de prioriser les ''threads'' qui n'ont presque aucune instruction dans la fenêtre d'instruction. Car ce sont des ''threads'' qui s'exécutent rapidement au point de vider la fenêtre d'instruction. L'idée est de charger en priorité les instructions du ''thread'' pour lequel la file d'instruction est la moins remplie. L'unité de choix du ''thread'' utilise cette information pour déterminer quel ''thread'' charger au prochain cycle.
==Le ''Simultaneous multithreading'' : une spécificité des processeurs superscalaires==
Les deux techniques vues au-dessus peuvent s'adapter sur les processeurs à émission multiple, à savoir qui sont capables d'émettre plusieurs instructions simultanément. La seule contrainte est que l'implémentation du FGMT est compliquée sur les architectures superscalaires, ce qui fait que les ''barrel processors'' sont généralement des processeurs VLIW ou sans émission multiple. Mais il existe une technique de ''multithreading'' matériel spécialement pensée pour les processeurs à émission multiple : le ''Simultaneous multithreading''. La comprendre est assez simple si on la compare au FGMT et au CGMT.
Avec le ''multithreading'' temporel, lors d'un cycle d'horloge, les instructions émises appartiennent au même ''thread'' matériel. Il n'y a pas de situation où une instruction du ''thread'' 1 est émise en même temps qu'une instruction du ''thread'' 2.
{|
|[[File:CGMT sur processeur superscalaire.png|vignette|upright=1.5|CGMT sur processeur superscalaire]]
|[[File:FGMT sur processeur superscalaire.png|vignette|upright=1.5|FGMT sur processeur superscalaire]]
|}
Le '''Simultaneous Multi-Threading''', abrégé en SMT, permet d'émettre simultanément des instructions provenant de ''threads'' séparés. Elle fonctionne sur les processeurs superscalaires, mais n'est pas possible sur les processeurs VLIW.
[[File:Simultaneous Multi-Threading.png|centre|vignette|upright=2|Simultaneous Multi-Threading]]
===L'implémentation matérielle du SMT===
Dans les faits, tous les processeurs SMT sont des processeurs à exécution dans le désordre. Non pas que ce soit obligatoire, juste que le cout d'implémentation est bien plus faible sur un processeur à exécution dans le désordre. L'implémentation du SMT prend un processeur à exécution dans le désordre, ajoute plusieurs ''program counter'' et les circuits adéquats. L'unité d'émission choisit quelles instructions envoyer aux ALU, en utilisant l'exécution dans le désordre : tant qu'elles sont indépendantes, elles peuvent s’exécuter en parallèle. Et deux instructions de deux ''threads'' différents sont indépendantes ! Il y a donc un lien étroit entre exécution dans le désordre et SMT !
Ironiquement, avec l'amélioration des techniques d'exécution dans le désordre, le SMT commence à perdre de sa superbe. Plus l'exécution dans le désordre est efficace, plus le SMT est inutile. Si l'exécution dans le désordre est trop efficace, les cycles gâchés sont trop rares pour que le SMT ne servent pas à grande chose. Quand les cycles gâchés représentent grand maximum 10% du temps d’exécution grâce à l'usage de l’exécution dans le désordre, le SMT n'a plus grand-chose à remplir et le second programme s’exécuterait trop lentement pour ça vaille le coup.
Le SMT s'implémente généralement avec une seule fenêtre d’instruction et avec le renommage de registres. Le renommage de registres fait qu'on n'a pas à utiliser de fenêtrage de registres, juste à augmenter la taille du banc de registres. L'idée est que l'on concatène le ''thread ID'' au nom de registre avant de faire le renommage. Comme cela, on garantit que deux registres architecturaux identiques mais référencés dans des ''threads'' différents, correspondront à des registres physiques différents. Ainsi, l'unité d'émission a juste à vérifier les dépendances entre registres, pas besoin de gérer les ''thread ID''. En faisant cela, la logique d'émission est beaucoup plus simple. Par contre, les tables pour le renommage de registre sont dupliqués, avec autant de table que de ''thread'', chaque ''thread'' a la sienne.
Comme avec le FGMT, en cas des mauvaise prédiction de branchement, seules les instructions du ''thread'' fautif doivent être vidées. Pour cela, les files/fenêtres d'instruction doivent savoir à quel ''thread'' appartient tel ou telle instruction, pareil pour le ''Reorder Buffer''. Pour cela, on ajoute, dans chaque entrée de ces structures, le ''thread ID'' de l'instruction.
Avec le SMT, les différents ''threads'' doivent se partager certaines ressources matérielles : la fenêtre d'instruction, le banc de registres, la ''load store queue'', le cache, l'unité de prédiction de branchement ou d'autres structures matérielles. Le partage de ces ressources entre ''threads'' est dynamique, sauf pour quelques exceptions. La conséquence est qu'il peut y avoir une perte de performance si on lance deux ''threads'' et qu'il n'y a pas assez de ressources matérielles pour en profiter. Les ''threads'' entrent en compétition pour y accéder. Parlons un petit peu du partage du cache et de l'unité de prédiction de branchement.
===Le partage des caches===
La quasi-totalité des architectures multithreadées utilisent un cache partagé, et non des caches dédiés, ce qui impose de gérer le partage des caches entre les différents ''threads''.
La première idée est de partitionner le cache en plusieurs morceaux, avec un par ''thread''. Le '''partitionnement statique''' donne des portions égales à chaque programme, ce qui n'est pas toujours optimal mais a l'avantage d'être facile à implémenter. À l'opposé, le '''partitionnement dynamique''' partage le cache selon les besoins des ''thread'' en cours d'exécution. Si un programme a besoin de plus de place que l'autre, il peut réserver un peu plus de place que son concurrent. Mais la gestion du partitionnement est alors plus compliquée, et demande de partitionner efficacement le cache, sans quoi les deux programmes exécutés risquent de se marcher dessus et d'entrer en compétition pour le cache.
[[File:Partitionnement de l'Instruction Buffer avec l'hyperthreading.png|centre|vignette|upright=2|Partitionnement des caches avec l'hyperthreading.]]
Une autre possibilité est de ne pas partitionner et de laisser les différents ''threads'' accéder au cache comme bon leur semble. Si le cache est physiquement tagué, il n'y a aucune modification à faire. Mais si le cache est virtuellement tagué, une même adresse virtuelle peut correspondre à des adresses physiques différentes pour deux ''threads'' différents. Pour éviter cela, il faut ajouter l'identifiant de ''thread'' dans chaque ligne de cache, dans les bits de contrôle. La détection des succès ou défaut de cache tient compte de l'identifiant de ''thread'' lors des comparaisons de tags, afin d'éviter qu'un ''thread'' lise une donnée d'un autre ''thread''.
À l'inverse des processeurs mono-cœur, les processeurs multithreadés préfèrent des caches dont le débit binaire est plus important que la latence mémoire. La raison est que deux ''threads'' consomment deux fois plus de données qu'un seul et le débit binaire du cache doit suivre. Par contre, la latence est moins importante car les cycles gâchés qu'elle créé sont remplis par le ''multithreading'' matériel. Si un défaut de cache prend 4 cycles, le ''multithreading'' matériel va rapidement trouver des instructions à exécuter pendant ces cycles.
Tout ce qui vient d'être dit s'applique aussi sur la TLB. Il est possible de la partitionner entre plusieurs ''threads''. La plupart du temps, le partitionnement est statique sur les TLB dédiées aux instructions, alors que les TLB pour les données sont partagées dynamiquement. C'est le cas sur les architectures Skylake d'Intel, où les 128 entrées de la TLB d'instruction de niveau 1 ont découpées en deux sections de 64 entrées, une par programme/''thread'', les autres TLB étant partitionnées dynamiquement. Une autre solution est d'ajouter un identifiant de ''thread'' dans chaque entrée de la TLB, sur le même principe que celui vu dans le chapitre sur la TLB. Le gain en performance est bien plus important, pour un cout en hardware limité.
===L'unité de prédiction de branchement===
Un autre cache est lui aussi partitionné ou complété avec des ''thread ID'' : le ''branch target buffer''. Rappelons que c'est un cache spécialisé pour les adresses de branchements. L'unité de prédiction de branchements doit idéalement séparer les branchements entre chaque ''thread'' pour obtenir de bonnes performances, soit en partitionnant le BTB, soit en ajoutant ajouter un ''Thread ID'' dans chaque entrée de ce cache. Mais ce n'est pas obligatoire.
Les unités de prédiction de branchement vues dans les chapitres précédents peuvent parfaitement fonctionner telles quelles sur un processeur multithreadé. Le ''branch target buffer'' mémorise alors des branchements de ''threads'' différents, il ne sait pas à quel ''thread'' appartient tel branchement, mais le tout fonctionne. Le problème est que les prédictions pour un branchement sont parasitées par les branchements d'autres ''threads'', surtout pour les unités se basant sur un historique global. Malgré tout, on obtient un résultat tolérable, les performances sont assez bonnes et cela s'explique avec ce qui suit.
Déjà, il arrive que les branchements des autres ''threads'' aient un effet positif sur la prédiction pour un ''thread''. De telles interférences positives sont rares, mais possible si les deux ''threads'' travaillent sur des données partagées, et c'est alors les unités de prédiction de branchement normales, sans partitionnement ni ''thread ID'' qui fonctionnent mieux, sous certaines circonstances.
De plus, quand une mauvaise prédiction de branchement est détectée, il faut vider le pipeline des instructions chargées à tort. Et les instructions fautives appartiennent toutes à un même ''thread'', les instructions des autres ''threads'' ne sont pas concernées. Le vidage du pipeline est donc sélectif. Là encore, la vidange du pipeline se base sur les ''Thread ID'' propagés dans le pipeline. Vu que le vidage du pipeline est sélectif, partiel, la perte de performance en cas de mauvaise prédiction de branchement est donc plus faible, et cela surcompense le fait que les prédictions de branchement sont parasitées par les branchements d’autres ''threads''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Architectures multiprocesseurs et multicœurs
| prevText=Architectures multiprocesseurs et multicœurs
| next=Les architectures à parallélisme de données
| nextText=Les architectures à parallélisme de données
}}
</noinclude>
4w0m3gz8textqtfl4pgw75etepth6ve
Les cartes graphiques/Les Render Output Target
0
67394
763297
761583
2026-04-08T19:20:00Z
Mewtow
31375
/* Le cache de profondeur */
763297
wikitext
text/x-wiki
Pour rappel, les étapes précédentes du pipeline graphiques manipulaient non pas des pixels, mais des fragments. Pour rappel, la distinction entre fragment et pixel est pertinente quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. La couleur finale dépend de la couleur de tous ces points d'intersection. Intuitivement, l'objet le plus proche est censé cacher les autres et c'est donc lui qui décide de la couleur du pixel, mais cela demande de déterminer quel est l'objet le plus proche. De plus, certains objets sont transparents et la couleur finale est un mélange de la couleur de plusieurs points d'intersection.
Tout demande de calculer un pseudo-pixel pour chaque point d'intersection et de combiner leurs couleurs pour obtenir le résultat final. Les pseudo-pixels en question sont des '''fragments'''. Chaque fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont donc combinés pour obtenir la couleur finale de ce pixel. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc.
Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le '''Raster Operations Pipeline''' (ROP), aussi appelé ''Render Output Target'', situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo.
==Les fonctions des ROP==
Les ROP incorporent plusieurs fonctionnalités qui sont assez diverses. Leur seul lien est qu'il est préférable de les implémenter en matériel plutôt qu'en logiciel, et en-dehors des unités de textures. Il s'agit de fonctionnalités assez simples, basiques, mais nécessaires au fonctionnement de tout rendu 3D. Elles ont aussi pour particularité de beaucoup accéder à la mémoire vidéo. C'est la raison pour laquelle le ROP est situé en fin de pipeline, proche de la mémoire vidéo. Voyons quelles sont ces fonctionnalités.
===La gestion de la profondeur (tests de visibilité)===
Le premier rôle du ROP est de trier les fragments du plus proche au plus éloigné, pour gérer les situations où un triangle en cache un autre (quand un objet en cache un autre, par exemple). Prenons un mur rouge opaque qui cache un mur bleu. Dans ce cas, un pixel de l'écran sera associé à deux fragments : un pour le mur rouge, et un pour le bleu. Vu que le mur de devant est opaque, seul le fragment de ce mur doit être choisi : celui du mur qui est devant. Et il s'agit là d'un exemple simple, mais il est fréquent qu'un objet soit caché par plusieurs objets. En moyenne, un objet est caché par 3 à 4 objets dans un rendu 3d de jeu vidéo.
Pour cela, chaque fragment a une coordonnée de profondeur, appelée la coordonnée z, qui indique la distance de ce fragment à la caméra. La coordonnée z est un nombre qui est d'autant plus petit que l'objet est près de l'écran. La profondeur est calculée à la rastérisation, ce qui fait que les ROP n'ont pas à la calculer, juste à trier les fragments en fonction de leur profondeur.
[[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]]
Pour savoir quels fragments sont à éliminer (car cachés par d'autres), la carte graphique utilise ce qu'on appelle un '''tampon de profondeur'''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un fragment a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, le fragment est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
===La gestion de la transparence : test alpha et ''alpha blending''===
Les ROPs s'occupent aussi de la gestion de la transparence. La transparence/opacité d'un pixel/texel est codée par un nombre, la '''composante alpha''', qui est ajouté aux trois couleurs RGB. Plus la composante alpha est élevée, plus le pixel est opaque. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. La gestion de la transparence par les ROP est le fait de plusieurs fonctionnalités distinctes, les deux principales étant le test alpha et l'''alpha blending''.
L''''''alpha test''''' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous d'un seuil, le fragment est simplement abandonné. Chaque fragment passe une étape de test alpha qui vérifie si la valeur alpha est au-dessus de ce seuil ou non. S'il ne passe pas le test, le fragment est abandonné, il ne passe pas à l'étape de test de profondeur, ni aux étapes suivantes. Il s'agit d'une technique binaire de gestion de la transparence, qui est complétée par d'autres techniques. De nos jours, cette technologie est devenue obsolète.
Elle optimisait le rendu de textures où les pixels sont soit totalement opaques, soit totalement transparents. Un exemple est le rendu du feuillage dans un jeu 3D : on a une texture de feuille plaquée sur un rectangle, les portions vertes étant totalement opaques et le reste étant totalement transparent. L'avantage est que cela évitait de mettre à jour le tampon de profondeur pour des fragments totalement transparents.
Maintenant, le test alpha ne permet pas de gérer des situations où on voit quelque chose à travers un objet transparent. Si un fragment transparent est placé devant un autre fragment, la couleur du pixel sera un mélange de la couleur du fragment transparent, et de la couleur du (ou des) fragments placé·s derrière. Le calcul à effectuer est très simple, et se limite en une simple moyenne pondérée par la transparence de la couleur des deux pixels. On parle alors d''''''alpha blending'''''.
[[File:Texture splatting.png|centre|vignette|upright=2.0|Application de textures.]]
Les fragments arrivant par paquets, calculés uns par uns par les unités de texture et de shaders, le calcul des couleurs est effectué progressivement. Pour cela, la carte graphique doit mettre en attente les résultats temporaires des mélanges pour chaque pixel. C'est le rôle du '''tampon de couleur''', l'équivalent du tampon de profondeur pour les couleurs des pixels. À chaque fragment reçu, le ROP lit la couleur du pixel associé dans le tampon de couleur, fait ou non la moyenne pondérée avec le fragment reçu et enregistre le résultat. Ces opérations de test et d'''alpha blending'' sont effectuées par un circuit spécialisé qui travaille en parallèle des circuits de calcul de la profondeur.
Il faut noter que le rendu de la transparence se marie assez mal avec l'usage d'un tampon de profondeur. Le tampon de profondeur marche très bien quand on a des fragments totalement opaques : il a juste à mémoriser la coordonnée z du pixel le plus proche. Mais avec des fragments transparents, les choses sont plus compliquées, car plusieurs fragments sont censés être visibles, et on ne sait pas quelle coordonnée z stocker. L'interaction entre profondeur et transparence est réglée par diverses techniques. Avec l'''alpha blending'', c'est la cordonnée du fragment le plus proche qui est mémorisée dans le tampon de profondeur.
===Le tampon de ''stencil''===
Le '''''stencil''''' est une fonctionnalité des API graphiques et des cartes graphiques depuis déjà très longtemps. Il sert pour générer des effets graphiques très variés, qu'il serait vain de lister ici. Il a notamment été utilisé pour combattre le phénomène de ''z-fighting'' mentionné plus haut, il est utilisé pour calculer des ombres volumétriques (le moteur de DOOM 3 en faisait grand usage à la base), des réflexions simples, des lightmaps ou shadowmaps, et bien d'autres.
Pour le résumer, on peut le voir comme une sorte de tampon de profondeur programmable, dans la coordonnée z est remplacée par une valeur arbitraire, dont le programmeur peut faire ce qu'il veut. La valeur est de plus une valeur entière, pas flottante. L'idée est que chaque pixel/fragment se voit attribuer une valeur entière, généralement codée sur un octet, que les programmeurs peuvent faire varier à loisir. L'octet ajouté est appelé l''''octet de ''stencil'''''. L'octet a une certaine valeur, qui est calculée par la carte graphique au fur et à mesure que les fragments sont traités. Il ne remplace pas la coordonnée de profondeur, mais s'ajoute à celle-ci.
L'ensemble des octets de ''stencil'' est mémorisée dans un tableau en mémoire vidéo, avec un octet par pixel du ''framebuffer''. Le tableau porte le nom de '''tampon de ''stencil'''''. Il s'agit d'un tableau distinct du tampon de profondeur ou du tampon de couleur, du moins en théorie. Dans les faits, les techniques liées au tampon de ''stencil'' font souvent usage du tampon de profondeur, pour beaucoup d'effets graphiques avancés. Aussi, le tampon de ''stencil'' est souvent fusionné avec le tampon de profondeur. L'ensemble forme un tableau qui associe 32 bits à chaque" pixel : 24 bits pour une coordonnée z, 8 pour l'octet de ''stencil''.
Chaque fragment a sa propre valeur de ''stencil'' qui est calculée par la carte graphique, généralement par les ''shaders''. Lors du passage d'un fragment les ROPs, la carte graphique lit le pixel associé dans le tampon de ''stencil''. Puis il compare l'octet de ''stencil'' avec celui du fragment traité. Si le test échoue, le fragment ne passe pas à l'étape de test de profondeur et est abandonné. S'il passe, le tampon de ''stencil'' est mis à jour.
Par mis à jour, on veut dire que le ROP peut faire diverses manipulations dessus : l'incrémenter, le décrémenter, le mettre à 0, inverser ses bits, remplacer par l'octet de ''stencil'' du fragment, etc. Les opérations possibles sont bien plus nombreuses qu'avec le tampon de profondeur, qui se contente de remplacer la coordonnée z par celle du fragment. C'est toujours possible, on peut remplacer l'octet de ''stencil'' dans le tampon de ''stencil'' par celui du fragment s'il passe le test. Mais pour les techniques de rendu plus complexes, c'est une autre opération qui est utilisée, comme incrémenter l'octet dans le tampon de ''stencil''.
===Les effets de brouillard===
Les '''effets de brouillard''' sont des effets graphiques assez intéressants. Ils sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie.
L'idée est d'avoir un ''view frustum'' limité : le plan limite au-delà duquel on ne voit pas les objets est assez proche de la caméra. Mais si le plan limite est trop proche, cela donnera une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance.
Pour calculer le brouillard, on mélange la couleur finale du pixel avec une ''couleur de brouillard'', la couleur de brouillard étant pondérée par la profondeur. Au-delà d'une certaine distance, l'objet est intégralement dans le brouillard : le brouillard domine totalement la couleur du pixel. En dessous d'une certaine distance, le brouillard est à zéro. Entre les deux, la couleur du brouillard et de l'objet devront toutes les deux être prises en compte dans les calculs. La formule de calcul exacte varie beaucoup, elle est souvent linéaire ou exponentielle.
Notons que ce calcul implique à la fois de l'''alpha blending'' mais aussi la coordonnée de profondeur, ce qui en fait que son implémentation dans les ROPs est l'idéal. Aussi, les premières cartes graphiques calculaient le brouillard dans les ROP, en fonction de la coordonnée de profondeur du fragment. De nos jours, il est calculé par les ''pixel shaders'' et les ROP n'incorporent plus de technique de brouillard spécialisée. Vu que les pixels shaders peuvent s'en charger, cela fait moins de circuits dans les ROPs pour un cout en performance mineur. Et ce d'autant plus que les effets de brouillard sont devenus assez rares de nos jours. Autant les émuler dans les pixels shaders que d'utiliser des circuits pour une fonction devenue anecdotique.
===Les autres fonctions des ROPs===
Les ROPs gèrent aussi des techniques de '''''dithering''''', qui permettent d'adoucir des images lorsqu'elles sont redimensionnées et stockées avec une précision plus faible que la précision de calcul.
Les ROPS implémentent aussi des techniques utilisées sur les ''blitters'' des anciennes cartes d'affichage 2D, comme l'application d''''opérations logiques''' sur chaque pixel enregistré dans le framebuffer. Les opérations logiques en question peuvent prendre une à deux opérandes. Les opérandes sont soit un pixel lu dans le ''framebuffer'', soit un fragment envoyé au ROP. Les opérations logiques à une opérande peuvent inverser, mettre à 0 ou à 1 le pixel dans le framebuffer, ou faire la même chose sur le fragment envoyé en opérande. Les opérations à deux opérandes lisent un pixel dans le framebuffer, et font un ET/OU/XOR avec le fragment opérande (une des deux opérandes peut être inversée). Elles sont utilisées pour faire du traitement d'image ou du rendu 2D, rarement pour du rendu 3D.
Les ROPs gèrent aussi des '''masques d'écritures''', qui permettent de décider si un pixel doit être écrit ou non en mémoire. Il est possible d'inhiber certaines écritures dans le tampon de profondeur ou le tampon de couleur, éventuellement le tampon de stencil. Inhiber la mise à jour d'un pixel dans le tampon de profondeur est utile pour gérer la transparence. Si un pixel est transparent, même partiellement, il ne faut pas mettre à jour le tampon de profondeur, et cela peut être géré par ce système de masquage. Les masquages des couleurs permettent de ne modifier qu'une seule composante R/G/B au lieu de modifier les trois en même temps, pour faire certains effets visuels.
==L'architecture matérielle d'un ROP==
Les ROP contiennent des circuits pour gérer la profondeur des fragments. Il effectuent un test de profondeur, à savoir que les fragments correspondant à un même pixel sont comparés pour savoir lequel est devant l'autre. Ils contiennent aussi des circuits pour gérer la transparence des fragments. Le ROP gère aussi l'antialiasing, de concert avec l'unité de rastérisation. D'autres fonctionnalités annexes sont parfois implémentées dans les ROP. Par exemple, les vielles cartes graphiques implémentaient les effets de brouillards dans les ROPs. Le tout est suivi d'une unité qui enregistre le résultat final en mémoire, où masques et opérations logiques sont appliqués.
Les différentes opérations du ROP doivent se faire dans un certain ordre. Par exemple, gérer la transparence demande que les calculs de profondeur se fassent globalement après ou pendant l'''alpha blending''. Ou encore, les masques et opérations logiques se font à la toute fin du rendu. L'ordre des opérations est censé être le suivant : test ''alpha'', test du ''stencil'', test de profondeur, ''alpha blending''. Du moins, la carte graphique doit donner l'impression que c'est le cas. Elle peut optimiser le tout en traitant le tampon de profondeur, de couleur et de ''stencil'' en même temps, mais donner les résultats adéquats.
[[File:Render Output Pipeline-processor.png|centre|vignette|upright=2|Render Output Pipeline-processor]]
[[File:GeForce 6800 Pixel blending.png|droite|thumb|R.O.P des GeForce 6800.]]
Un ROP est typiquement organisé comme illustré ci-dessous et ci-contre. Il récupère les fragments calculés par les pixels shaders et/ou les unités de texture, via un circuit d'interconnexion spécialisé. Chaque ROP est connecté à toutes les unités de ''shader'', même si la connexion n'est pas forcément directe. Toute unité de ''shader'' peut envoyer des pixels à n'importe quel ROP. Les circuits d'interconnexion sont généralement des réseaux d'interconnexion de type ''crossbar'', comme illustré ci-contre (le premier rectangle rouge).
Notons que les circuits de gestion de la profondeur et de la transparence sont séparés dans les schémas ci-contre et ci-dessous. Il s'agit là d'une commodité qui ne reflète pas forcément l'implémentation matérielle. Et si ces deux circuits sont séparés, ils communiquent entre eux, notamment pour gérer la profondeur des fragments transparents.
Les circuits de gestion de la profondeur et de la couleur gèrent diverses techniques de compression pour économiser de la mémoire et de la bande passante mémoire. Ajoutons à cela que ces deux unités contiennent des caches spécialisés, qui permettent de réduire fortement les accès mémoires, très fréquents dans cette étape du pipeline graphique.
Il est à noter que sur certaines cartes graphiques, l'unité en charge de calculer les couleurs peut aussi servir à effectuer des comparaisons de profondeur. Ainsi, si tous les fragments sont opaques, on peut traiter deux fragments à la fois. C'était le cas sur la Geforce FX de Nvidia, ce qui permettait à cette carte graphique d'obtenir de très bonnes performances dans le jeu DOOM3.
==Les optimisations intégrées aux ROPs==
Le ROP effectue beaucoup de lectures et écritures en mémoire vidéo. Or, la bande passante mémoire est limitée, ce qui fait que le ROP est un goulot d'étranglement assez important pour le rendu 3D. Heureusement, de nombreuses optimisations permettent d'optimiser le tout. Elles agissent sur la lecture du tampon de profondeur, mais aussi sur le ''framebuffer''.
===Le ''fast clear'' du ''framebuffer''===
Une première optimisation porte sur le ''framebuffer''. Le ''framebuffer''est souvent réutilisé d'une image sur l'autre. Quand une image a été envoyée à l'écran, le ''framebuffer'' est remis à zéro pour accueillir une nouvelle image. Et ce avec ou sans ''double buffering''. La mise à zéro est censée se faire en remettant réellement le ''framebuffer'' à zéro, en écrivant des 0 pour chaque pixel du ''framebuffer''. Mais il y a moyen de s'en passer.
Pour cela, l'idée est que le ''framebuffer'' est découpé en ''tiles'', des carrés de 4, 8, 16 pixels de côté. Les ''tiles'' ont généralement la même taille que les ''tiles'' utilisées pour la rastérisation, mais passons sur ce détail. L'idée est de mémoriser, pour chaque ''tile'', si elle est mise à 0 ou non. Il suffit de cela d'un seul bit par ''tile'', appelé le bit RESET. L'ensemble des bits RESET est mémorisé dans une petite mémoire SRAM, intégrée aux ROPs.
Lorsqu'on souhaite remettre à zéro le ''framebuffer'', il suffit de mettre à 0 tous les bits RESET dans cette SRAM, pas besoin d’accéder à la mémoire vidéo. Avant toute lecture dans le ''framebuffer'', le ROP lit cette SRAM pour vérifier si la ''tile'' en question a été remise à 0. Si ce n'est pas le cas, il lit le pixel voulu depuis le ''framebuffer''. Mais si c'est le cas, alors le ROP ne fait pas la lecture et fournit un pixel à zéro à la place, qui est utilisé pour l'''alpha blending'' ou autre. La moindre écriture dans une ''tile'' met le bit RESET à 0 : la ''tile'' entière est considérée comme non-remise à zéro, même si un seul pixel a été modifié dedans.
Notons que l'usage d'une granularité par ''tile'' est un compromis. On peut ne peut pas utiliser un bit par pixel, car cela demanderait d'utiliser une SRAM énorme. De même, utiliser un seul bit pour tout le ''framebuffer'' ruinerait totalement l'optimisation : le ''framebuffer'' entier serait considéré comme non-RESET dès la première écriture d'un pixel dedans, on ne sauverait qu'un nombre trop limité d'accès mémoire.
===La z-compression===
La technique de '''z-compression''' compresse le tampon de profondeur. Plus précisément, elle découpe le tampon de profondeur en ''tiles'', en blocs carrés, qui sont compressés séparément les uns des autres. La taille des ''tiles'' est souvent la même que celle utilisée par le rastériseur pour la rastérisation grossière. Par exemple, la ''z-compression'' des cartes graphiques ATI radeon 9800, découpait le tampon de profondeur en ''tiles'' de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM (''Differential differential pulse code modulation'').
Précisons que cette compression ne change pas la taille occupée par le tampon de profondeur, mais seulement la quantité de données lue/écrite. La raison est que les ''tiles'' doivent avoir une place fixe en mémoire. Par exemple, si une ''tile'' non-compressée prend 64 octets, on trouvera une ''tile'' tous les 64 octets en mémoire vidéo, afin de simplifier les calculs d'adresse, afin que le ROP sache facilement où se trouve la ''tile'' à lire/écrire. Avec une vraie compression, les ''tiles'' se trouveraient à des endroits très variables d'une image à l'autre.
Par contre, la z-compression réduit la quantité de données écrite dans le tampon de profondeur. Par exemple, au lieu d'écrire une ''tile'' non-compressée de 64 octets, on écrira une ''tile'' de seulement 6 octets, les 58 octets restants étant pas lus ou écrits. On obtient un gain en performance, pas en mémoire.
[[File:AMD HyperZ.svg|centre|vignette|upright=2|AMD HyperZ]]
Le format de compression ajoute un bit par ''tile'', qui indique si elle est compressée ou non. Le bit qui indique si la ''tile'' est compressée permet de laisser certaines ''tiles'' non-compressés, dans le cas où la compression ne permet pas de gagner de la place. La compression ajoute souvent un second bit, qui indique si la ''tile'' est à zéro ou non, sur le même modèle que pour le ''framebuffer''. Il accélère la remise à zéro du tampon de profondeur. Au lieu de réellement remettre tout le tampon de profondeur à 0, il suffit de réécrire un bit par ''tile''. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant.
Les deux bits en question peuvent être placés à deux endroits différents. La première solution serait d'utiliser une portion de la mémoire vidéo, mais cela demanderait de faire deux lectures par accès au tampon de profondeur. La vraie solution est d'utiliser une SRAM reliée aux ROPs, qui est assez grande pour mémoriser tout le tampon de profondeur, du moins avec deux bits par ''tile''.
===Le cache de profondeur===
Une optimisation complémentaire ajoute une ou plusieurs mémoires caches dans le ROP, dans le circuit de profondeur. Ce '''cache de profondeur''' stocke des portions du tampon de profondeur qui ont été lues ou écrite récemment. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent.
Sur certaines cartes graphiques, les données dans le cache de profondeur sont stockées sous forme compressées dans le cache de profondeur, là encore pour augmenter la taille effective du cache. D'autres cartes graphiques ont un cache qui stocke des données décompressées dans le cache de profondeur. Tout est question de compromis entre accès rapide au cache et augmentation de la taille du cache.
Il faut savoir que les autres unités de la carte graphique peuvent lire le tampon de profondeur, en théorie. Cela peut servir pour certaines techniques de rendu, comme pour le ''shadowmapping''. De ce fait, il arrive que le cache de profondeur contienne des données qui sont copiées dans d'autres caches, comme les caches des processeurs de shaders. Le cache de profondeur n'est pas gardé cohérent avec les autres caches du GPU, ce qui signifie que les écritures dans le cache de profondeur ne sont pas propagées dans les autres caches du GPU. Si on modifie des données dans ce cache, les autres caches qui ont une copie de ces données auront une version périmée de la donnée. C'est souvent un problème, sauf dans le cas du cache de profondeur, pour lequel ce n'est pas nécessaire. Cela évite d'implémenter des techniques de cohérence des caches couteuses en circuits et en performance, alors qu'elles n'auraient pas d'intérêt dans ce cas précis.
===Le ''z-fast pass''===
Le ''z-fast pass'' améliore la performance des '''prépasses z''', une technique utilisée par de nombreux moteurs de jeux vidéo. L'idée est que le moteur de jeu effectue plusieurs passes, chacune faisant un truc précis, la prépasse z étant l'une de ces passes. Lors d'une prépasse z, le moteur de jeu calcule la scène 3D, rastérise l'image, et remplit le tampon de profondeur uniquement. Il le place pas de textures, ne calcule pas de pixels shaders, il se préoccupe uniquement des coordonnées de profondeur des pixels. Au final, le rendu ne donne que le tampon de profondeur, qui est utilisé par les passes suivantes.
L'utilité est très variable, mais il y a deux raisons pour effectuer une prépasse z : la performance, mais aussi certains effets graphiques. Par exemple, les effets d'occlusion ambiante "''screen space''" utilisent le tampon de profondeur pour faire leur travail. Il en est de même pour les ''shadowmaps'', qui effectuent une prépasse z par ombre à afficher. Une autre utilisation est que cela permet d'utiliser élimination des pixels cachés très performante. On effectue une prépasse z pour calculer le tampon de profondeur final, qui est ensuite utilisé par les passes suivantes pour éliminer les pixels cachés. Ainsi, les pixels cachés ne sont pas texturés et pixel shadés, avec certitude.
Toujours est-il qu'une prépasse z utilise les ROP "à moitié", dans le sens où seul le tampon de profondeur est utilisé, par la gestion des couleurs. Mais il se trouve que les circuits qui servent pour l''alpha blending'' peuvent être réutilisés pour faire les comparaisons de profondeur ! Le résultat est que les ROP peuvent fonctionner à double vitesse lors d'une prépasse z ! Cela demande cependant de concevoir les circuits du ROP pour en profiter. L'optimisation est parfois appelée le '''''z-fast pass'''''.
Tous les GPU depuis la Geforce FX en sont capables. Il y a cependant quelques contraintes. Premièrement, le ROP doit être configuré de manière à n’accéder qu'au tampon de profondeur, ils ne doivent pas dessiner dans le ''framebuffer''. L'''alpha blending'' doit être désactivé, de même que l'alpha-test. D'autres contraintes supplémentaires sont parfois présentes, surtout sur les vieux GPUs. Par exemple, l'antialiasing doit être désactivé lors de la prépasse z. Et mine de rien, cela ne marche que pour les prépasses z pures. Par exemple, certaines techniques de rendu différé augmentent la prépasse z pour que celle-ci ne calcule pas que le tampon de profondeur, mais aussi d'autres informations comme les normales : elles ne profitent pas de cette optimisation.
{{NavChapitre | book=Les cartes graphiques
| prev=Les unités de texture
| prevText=Les unités de texture
| next=Le support matériel du lancer de rayons
| nextText=Le support matériel du lancer de rayons
}}{{autocat}}
hyy9tbohpdfwoc19uqeay2qrg54coez
763298
763297
2026-04-08T19:23:17Z
Mewtow
31375
/* Les optimisations intégrées aux ROPs */
763298
wikitext
text/x-wiki
Pour rappel, les étapes précédentes du pipeline graphiques manipulaient non pas des pixels, mais des fragments. Pour rappel, la distinction entre fragment et pixel est pertinente quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. La couleur finale dépend de la couleur de tous ces points d'intersection. Intuitivement, l'objet le plus proche est censé cacher les autres et c'est donc lui qui décide de la couleur du pixel, mais cela demande de déterminer quel est l'objet le plus proche. De plus, certains objets sont transparents et la couleur finale est un mélange de la couleur de plusieurs points d'intersection.
Tout demande de calculer un pseudo-pixel pour chaque point d'intersection et de combiner leurs couleurs pour obtenir le résultat final. Les pseudo-pixels en question sont des '''fragments'''. Chaque fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont donc combinés pour obtenir la couleur finale de ce pixel. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc.
Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le '''Raster Operations Pipeline''' (ROP), aussi appelé ''Render Output Target'', situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo.
==Les fonctions des ROP==
Les ROP incorporent plusieurs fonctionnalités qui sont assez diverses. Leur seul lien est qu'il est préférable de les implémenter en matériel plutôt qu'en logiciel, et en-dehors des unités de textures. Il s'agit de fonctionnalités assez simples, basiques, mais nécessaires au fonctionnement de tout rendu 3D. Elles ont aussi pour particularité de beaucoup accéder à la mémoire vidéo. C'est la raison pour laquelle le ROP est situé en fin de pipeline, proche de la mémoire vidéo. Voyons quelles sont ces fonctionnalités.
===La gestion de la profondeur (tests de visibilité)===
Le premier rôle du ROP est de trier les fragments du plus proche au plus éloigné, pour gérer les situations où un triangle en cache un autre (quand un objet en cache un autre, par exemple). Prenons un mur rouge opaque qui cache un mur bleu. Dans ce cas, un pixel de l'écran sera associé à deux fragments : un pour le mur rouge, et un pour le bleu. Vu que le mur de devant est opaque, seul le fragment de ce mur doit être choisi : celui du mur qui est devant. Et il s'agit là d'un exemple simple, mais il est fréquent qu'un objet soit caché par plusieurs objets. En moyenne, un objet est caché par 3 à 4 objets dans un rendu 3d de jeu vidéo.
Pour cela, chaque fragment a une coordonnée de profondeur, appelée la coordonnée z, qui indique la distance de ce fragment à la caméra. La coordonnée z est un nombre qui est d'autant plus petit que l'objet est près de l'écran. La profondeur est calculée à la rastérisation, ce qui fait que les ROP n'ont pas à la calculer, juste à trier les fragments en fonction de leur profondeur.
[[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]]
Pour savoir quels fragments sont à éliminer (car cachés par d'autres), la carte graphique utilise ce qu'on appelle un '''tampon de profondeur'''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un fragment a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, le fragment est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
===La gestion de la transparence : test alpha et ''alpha blending''===
Les ROPs s'occupent aussi de la gestion de la transparence. La transparence/opacité d'un pixel/texel est codée par un nombre, la '''composante alpha''', qui est ajouté aux trois couleurs RGB. Plus la composante alpha est élevée, plus le pixel est opaque. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. La gestion de la transparence par les ROP est le fait de plusieurs fonctionnalités distinctes, les deux principales étant le test alpha et l'''alpha blending''.
L''''''alpha test''''' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous d'un seuil, le fragment est simplement abandonné. Chaque fragment passe une étape de test alpha qui vérifie si la valeur alpha est au-dessus de ce seuil ou non. S'il ne passe pas le test, le fragment est abandonné, il ne passe pas à l'étape de test de profondeur, ni aux étapes suivantes. Il s'agit d'une technique binaire de gestion de la transparence, qui est complétée par d'autres techniques. De nos jours, cette technologie est devenue obsolète.
Elle optimisait le rendu de textures où les pixels sont soit totalement opaques, soit totalement transparents. Un exemple est le rendu du feuillage dans un jeu 3D : on a une texture de feuille plaquée sur un rectangle, les portions vertes étant totalement opaques et le reste étant totalement transparent. L'avantage est que cela évitait de mettre à jour le tampon de profondeur pour des fragments totalement transparents.
Maintenant, le test alpha ne permet pas de gérer des situations où on voit quelque chose à travers un objet transparent. Si un fragment transparent est placé devant un autre fragment, la couleur du pixel sera un mélange de la couleur du fragment transparent, et de la couleur du (ou des) fragments placé·s derrière. Le calcul à effectuer est très simple, et se limite en une simple moyenne pondérée par la transparence de la couleur des deux pixels. On parle alors d''''''alpha blending'''''.
[[File:Texture splatting.png|centre|vignette|upright=2.0|Application de textures.]]
Les fragments arrivant par paquets, calculés uns par uns par les unités de texture et de shaders, le calcul des couleurs est effectué progressivement. Pour cela, la carte graphique doit mettre en attente les résultats temporaires des mélanges pour chaque pixel. C'est le rôle du '''tampon de couleur''', l'équivalent du tampon de profondeur pour les couleurs des pixels. À chaque fragment reçu, le ROP lit la couleur du pixel associé dans le tampon de couleur, fait ou non la moyenne pondérée avec le fragment reçu et enregistre le résultat. Ces opérations de test et d'''alpha blending'' sont effectuées par un circuit spécialisé qui travaille en parallèle des circuits de calcul de la profondeur.
Il faut noter que le rendu de la transparence se marie assez mal avec l'usage d'un tampon de profondeur. Le tampon de profondeur marche très bien quand on a des fragments totalement opaques : il a juste à mémoriser la coordonnée z du pixel le plus proche. Mais avec des fragments transparents, les choses sont plus compliquées, car plusieurs fragments sont censés être visibles, et on ne sait pas quelle coordonnée z stocker. L'interaction entre profondeur et transparence est réglée par diverses techniques. Avec l'''alpha blending'', c'est la cordonnée du fragment le plus proche qui est mémorisée dans le tampon de profondeur.
===Le tampon de ''stencil''===
Le '''''stencil''''' est une fonctionnalité des API graphiques et des cartes graphiques depuis déjà très longtemps. Il sert pour générer des effets graphiques très variés, qu'il serait vain de lister ici. Il a notamment été utilisé pour combattre le phénomène de ''z-fighting'' mentionné plus haut, il est utilisé pour calculer des ombres volumétriques (le moteur de DOOM 3 en faisait grand usage à la base), des réflexions simples, des lightmaps ou shadowmaps, et bien d'autres.
Pour le résumer, on peut le voir comme une sorte de tampon de profondeur programmable, dans la coordonnée z est remplacée par une valeur arbitraire, dont le programmeur peut faire ce qu'il veut. La valeur est de plus une valeur entière, pas flottante. L'idée est que chaque pixel/fragment se voit attribuer une valeur entière, généralement codée sur un octet, que les programmeurs peuvent faire varier à loisir. L'octet ajouté est appelé l''''octet de ''stencil'''''. L'octet a une certaine valeur, qui est calculée par la carte graphique au fur et à mesure que les fragments sont traités. Il ne remplace pas la coordonnée de profondeur, mais s'ajoute à celle-ci.
L'ensemble des octets de ''stencil'' est mémorisée dans un tableau en mémoire vidéo, avec un octet par pixel du ''framebuffer''. Le tableau porte le nom de '''tampon de ''stencil'''''. Il s'agit d'un tableau distinct du tampon de profondeur ou du tampon de couleur, du moins en théorie. Dans les faits, les techniques liées au tampon de ''stencil'' font souvent usage du tampon de profondeur, pour beaucoup d'effets graphiques avancés. Aussi, le tampon de ''stencil'' est souvent fusionné avec le tampon de profondeur. L'ensemble forme un tableau qui associe 32 bits à chaque" pixel : 24 bits pour une coordonnée z, 8 pour l'octet de ''stencil''.
Chaque fragment a sa propre valeur de ''stencil'' qui est calculée par la carte graphique, généralement par les ''shaders''. Lors du passage d'un fragment les ROPs, la carte graphique lit le pixel associé dans le tampon de ''stencil''. Puis il compare l'octet de ''stencil'' avec celui du fragment traité. Si le test échoue, le fragment ne passe pas à l'étape de test de profondeur et est abandonné. S'il passe, le tampon de ''stencil'' est mis à jour.
Par mis à jour, on veut dire que le ROP peut faire diverses manipulations dessus : l'incrémenter, le décrémenter, le mettre à 0, inverser ses bits, remplacer par l'octet de ''stencil'' du fragment, etc. Les opérations possibles sont bien plus nombreuses qu'avec le tampon de profondeur, qui se contente de remplacer la coordonnée z par celle du fragment. C'est toujours possible, on peut remplacer l'octet de ''stencil'' dans le tampon de ''stencil'' par celui du fragment s'il passe le test. Mais pour les techniques de rendu plus complexes, c'est une autre opération qui est utilisée, comme incrémenter l'octet dans le tampon de ''stencil''.
===Les effets de brouillard===
Les '''effets de brouillard''' sont des effets graphiques assez intéressants. Ils sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie.
L'idée est d'avoir un ''view frustum'' limité : le plan limite au-delà duquel on ne voit pas les objets est assez proche de la caméra. Mais si le plan limite est trop proche, cela donnera une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance.
Pour calculer le brouillard, on mélange la couleur finale du pixel avec une ''couleur de brouillard'', la couleur de brouillard étant pondérée par la profondeur. Au-delà d'une certaine distance, l'objet est intégralement dans le brouillard : le brouillard domine totalement la couleur du pixel. En dessous d'une certaine distance, le brouillard est à zéro. Entre les deux, la couleur du brouillard et de l'objet devront toutes les deux être prises en compte dans les calculs. La formule de calcul exacte varie beaucoup, elle est souvent linéaire ou exponentielle.
Notons que ce calcul implique à la fois de l'''alpha blending'' mais aussi la coordonnée de profondeur, ce qui en fait que son implémentation dans les ROPs est l'idéal. Aussi, les premières cartes graphiques calculaient le brouillard dans les ROP, en fonction de la coordonnée de profondeur du fragment. De nos jours, il est calculé par les ''pixel shaders'' et les ROP n'incorporent plus de technique de brouillard spécialisée. Vu que les pixels shaders peuvent s'en charger, cela fait moins de circuits dans les ROPs pour un cout en performance mineur. Et ce d'autant plus que les effets de brouillard sont devenus assez rares de nos jours. Autant les émuler dans les pixels shaders que d'utiliser des circuits pour une fonction devenue anecdotique.
===Les autres fonctions des ROPs===
Les ROPs gèrent aussi des techniques de '''''dithering''''', qui permettent d'adoucir des images lorsqu'elles sont redimensionnées et stockées avec une précision plus faible que la précision de calcul.
Les ROPS implémentent aussi des techniques utilisées sur les ''blitters'' des anciennes cartes d'affichage 2D, comme l'application d''''opérations logiques''' sur chaque pixel enregistré dans le framebuffer. Les opérations logiques en question peuvent prendre une à deux opérandes. Les opérandes sont soit un pixel lu dans le ''framebuffer'', soit un fragment envoyé au ROP. Les opérations logiques à une opérande peuvent inverser, mettre à 0 ou à 1 le pixel dans le framebuffer, ou faire la même chose sur le fragment envoyé en opérande. Les opérations à deux opérandes lisent un pixel dans le framebuffer, et font un ET/OU/XOR avec le fragment opérande (une des deux opérandes peut être inversée). Elles sont utilisées pour faire du traitement d'image ou du rendu 2D, rarement pour du rendu 3D.
Les ROPs gèrent aussi des '''masques d'écritures''', qui permettent de décider si un pixel doit être écrit ou non en mémoire. Il est possible d'inhiber certaines écritures dans le tampon de profondeur ou le tampon de couleur, éventuellement le tampon de stencil. Inhiber la mise à jour d'un pixel dans le tampon de profondeur est utile pour gérer la transparence. Si un pixel est transparent, même partiellement, il ne faut pas mettre à jour le tampon de profondeur, et cela peut être géré par ce système de masquage. Les masquages des couleurs permettent de ne modifier qu'une seule composante R/G/B au lieu de modifier les trois en même temps, pour faire certains effets visuels.
==L'architecture matérielle d'un ROP==
Les ROP contiennent des circuits pour gérer la profondeur des fragments. Il effectuent un test de profondeur, à savoir que les fragments correspondant à un même pixel sont comparés pour savoir lequel est devant l'autre. Ils contiennent aussi des circuits pour gérer la transparence des fragments. Le ROP gère aussi l'antialiasing, de concert avec l'unité de rastérisation. D'autres fonctionnalités annexes sont parfois implémentées dans les ROP. Par exemple, les vielles cartes graphiques implémentaient les effets de brouillards dans les ROPs. Le tout est suivi d'une unité qui enregistre le résultat final en mémoire, où masques et opérations logiques sont appliqués.
Les différentes opérations du ROP doivent se faire dans un certain ordre. Par exemple, gérer la transparence demande que les calculs de profondeur se fassent globalement après ou pendant l'''alpha blending''. Ou encore, les masques et opérations logiques se font à la toute fin du rendu. L'ordre des opérations est censé être le suivant : test ''alpha'', test du ''stencil'', test de profondeur, ''alpha blending''. Du moins, la carte graphique doit donner l'impression que c'est le cas. Elle peut optimiser le tout en traitant le tampon de profondeur, de couleur et de ''stencil'' en même temps, mais donner les résultats adéquats.
[[File:Render Output Pipeline-processor.png|centre|vignette|upright=2|Render Output Pipeline-processor]]
[[File:GeForce 6800 Pixel blending.png|droite|thumb|R.O.P des GeForce 6800.]]
Un ROP est typiquement organisé comme illustré ci-dessous et ci-contre. Il récupère les fragments calculés par les pixels shaders et/ou les unités de texture, via un circuit d'interconnexion spécialisé. Chaque ROP est connecté à toutes les unités de ''shader'', même si la connexion n'est pas forcément directe. Toute unité de ''shader'' peut envoyer des pixels à n'importe quel ROP. Les circuits d'interconnexion sont généralement des réseaux d'interconnexion de type ''crossbar'', comme illustré ci-contre (le premier rectangle rouge).
Notons que les circuits de gestion de la profondeur et de la transparence sont séparés dans les schémas ci-contre et ci-dessous. Il s'agit là d'une commodité qui ne reflète pas forcément l'implémentation matérielle. Et si ces deux circuits sont séparés, ils communiquent entre eux, notamment pour gérer la profondeur des fragments transparents.
Les circuits de gestion de la profondeur et de la couleur gèrent diverses techniques de compression pour économiser de la mémoire et de la bande passante mémoire. Ajoutons à cela que ces deux unités contiennent des caches spécialisés, qui permettent de réduire fortement les accès mémoires, très fréquents dans cette étape du pipeline graphique.
Il est à noter que sur certaines cartes graphiques, l'unité en charge de calculer les couleurs peut aussi servir à effectuer des comparaisons de profondeur. Ainsi, si tous les fragments sont opaques, on peut traiter deux fragments à la fois. C'était le cas sur la Geforce FX de Nvidia, ce qui permettait à cette carte graphique d'obtenir de très bonnes performances dans le jeu DOOM3.
==Les optimisations intégrées aux ROPs==
Le ROP effectue beaucoup de lectures et écritures en mémoire vidéo. Or, la bande passante mémoire est limitée, ce qui fait que le ROP est un goulot d'étranglement assez important pour le rendu 3D. Heureusement, de nombreuses optimisations permettent d'optimiser le tout. Elles agissent sur la lecture du tampon de profondeur, mais aussi sur le ''framebuffer''.
===Le ''fast clear'' du ''framebuffer''===
Une première optimisation porte sur le ''framebuffer''. Le ''framebuffer''est souvent réutilisé d'une image sur l'autre. Quand une image a été envoyée à l'écran, le ''framebuffer'' est remis à zéro pour accueillir une nouvelle image. Et ce avec ou sans ''double buffering''. La mise à zéro est censée se faire en remettant réellement le ''framebuffer'' à zéro, en écrivant des 0 pour chaque pixel du ''framebuffer''. Mais il y a moyen de s'en passer.
Pour cela, l'idée est que le ''framebuffer'' est découpé en ''tiles'', des carrés de 4, 8, 16 pixels de côté. Les ''tiles'' ont généralement la même taille que les ''tiles'' utilisées pour la rastérisation, mais passons sur ce détail. L'idée est de mémoriser, pour chaque ''tile'', si elle est mise à 0 ou non. Il suffit de cela d'un seul bit par ''tile'', appelé le bit RESET. L'ensemble des bits RESET est mémorisé dans une petite mémoire SRAM, intégrée aux ROPs.
Lorsqu'on souhaite remettre à zéro le ''framebuffer'', il suffit de mettre à 0 tous les bits RESET dans cette SRAM, pas besoin d’accéder à la mémoire vidéo. Avant toute lecture dans le ''framebuffer'', le ROP lit cette SRAM pour vérifier si la ''tile'' en question a été remise à 0. Si ce n'est pas le cas, il lit le pixel voulu depuis le ''framebuffer''. Mais si c'est le cas, alors le ROP ne fait pas la lecture et fournit un pixel à zéro à la place, qui est utilisé pour l'''alpha blending'' ou autre. La moindre écriture dans une ''tile'' met le bit RESET à 0 : la ''tile'' entière est considérée comme non-remise à zéro, même si un seul pixel a été modifié dedans.
Notons que l'usage d'une granularité par ''tile'' est un compromis. On peut ne peut pas utiliser un bit par pixel, car cela demanderait d'utiliser une SRAM énorme. De même, utiliser un seul bit pour tout le ''framebuffer'' ruinerait totalement l'optimisation : le ''framebuffer'' entier serait considéré comme non-RESET dès la première écriture d'un pixel dedans, on ne sauverait qu'un nombre trop limité d'accès mémoire.
===La z-compression===
La technique de '''z-compression''' compresse le tampon de profondeur. Plus précisément, elle découpe le tampon de profondeur en ''tiles'', en blocs carrés, qui sont compressés séparément les uns des autres. La taille des ''tiles'' est souvent la même que celle utilisée par le rastériseur pour la rastérisation grossière. Par exemple, la ''z-compression'' des cartes graphiques ATI radeon 9800, découpait le tampon de profondeur en ''tiles'' de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM (''Differential differential pulse code modulation'').
Précisons que cette compression ne change pas la taille occupée par le tampon de profondeur, mais seulement la quantité de données lue/écrite. La raison est que les ''tiles'' doivent avoir une place fixe en mémoire. Par exemple, si une ''tile'' non-compressée prend 64 octets, on trouvera une ''tile'' tous les 64 octets en mémoire vidéo, afin de simplifier les calculs d'adresse, afin que le ROP sache facilement où se trouve la ''tile'' à lire/écrire. Avec une vraie compression, les ''tiles'' se trouveraient à des endroits très variables d'une image à l'autre.
Par contre, la z-compression réduit la quantité de données écrite dans le tampon de profondeur. Par exemple, au lieu d'écrire une ''tile'' non-compressée de 64 octets, on écrira une ''tile'' de seulement 6 octets, les 58 octets restants étant pas lus ou écrits. On obtient un gain en performance, pas en mémoire.
[[File:AMD HyperZ.svg|centre|vignette|upright=2|AMD HyperZ]]
Le format de compression ajoute un bit par ''tile'', qui indique si elle est compressée ou non. Le bit qui indique si la ''tile'' est compressée permet de laisser certaines ''tiles'' non-compressés, dans le cas où la compression ne permet pas de gagner de la place. La compression ajoute souvent un second bit, qui indique si la ''tile'' est à zéro ou non, sur le même modèle que pour le ''framebuffer''. Il accélère la remise à zéro du tampon de profondeur. Au lieu de réellement remettre tout le tampon de profondeur à 0, il suffit de réécrire un bit par ''tile''. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant.
Les deux bits en question peuvent être placés à deux endroits différents. La première solution serait d'utiliser une portion de la mémoire vidéo, mais cela demanderait de faire deux lectures par accès au tampon de profondeur. La vraie solution est d'utiliser une SRAM reliée aux ROPs, qui est assez grande pour mémoriser tout le tampon de profondeur, du moins avec deux bits par ''tile''.
===Le cache de profondeur===
Une optimisation complémentaire ajoute une ou plusieurs mémoires caches dans le ROP, dans le circuit de profondeur. Ce '''cache de profondeur''' stocke des portions du tampon de profondeur qui ont été lues ou écrite récemment. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent.
Sur certaines cartes graphiques, les données dans le cache de profondeur sont stockées sous forme compressées dans le cache de profondeur, là encore pour augmenter la taille effective du cache. D'autres cartes graphiques ont un cache qui stocke des données décompressées dans le cache de profondeur. Tout est question de compromis entre accès rapide au cache et augmentation de la taille du cache.
Il faut savoir que les autres unités de la carte graphique peuvent lire le tampon de profondeur, en théorie. Cela peut servir pour certaines techniques de rendu, comme pour le ''shadowmapping''. De ce fait, il arrive que le cache de profondeur contienne des données qui sont copiées dans d'autres caches, comme les caches des processeurs de shaders. Le cache de profondeur n'est pas gardé cohérent avec les autres caches du GPU, ce qui signifie que les écritures dans le cache de profondeur ne sont pas propagées dans les autres caches du GPU. Si on modifie des données dans ce cache, les autres caches qui ont une copie de ces données auront une version périmée de la donnée. C'est souvent un problème, sauf dans le cas du cache de profondeur, pour lequel ce n'est pas nécessaire. Cela évite d'implémenter des techniques de cohérence des caches couteuses en circuits et en performance, alors qu'elles n'auraient pas d'intérêt dans ce cas précis.
===Le ''z-fast pass''===
Le ''z-fast pass'' améliore la performance des '''prépasses z''', une technique utilisée par de nombreux moteurs de jeux vidéo. L'idée est que le moteur de jeu effectue plusieurs passes, chacune faisant un truc précis, la prépasse z étant l'une de ces passes. Lors d'une prépasse z, le moteur de jeu calcule la scène 3D, rastérise l'image, et remplit le tampon de profondeur uniquement. Il le place pas de textures, ne calcule pas de pixels shaders, il se préoccupe uniquement des coordonnées de profondeur des pixels. Au final, le rendu ne donne que le tampon de profondeur, qui est utilisé par les passes suivantes.
L'utilité est très variable, mais il y a deux raisons pour effectuer une prépasse z : la performance, mais aussi certains effets graphiques. Par exemple, les effets d'occlusion ambiante "''screen space''" utilisent le tampon de profondeur pour faire leur travail. Il en est de même pour les ''shadowmaps'', qui effectuent une prépasse z par ombre à afficher. Une autre utilisation est que cela permet d'utiliser élimination des pixels cachés très performante. On effectue une prépasse z pour calculer le tampon de profondeur final, qui est ensuite utilisé par les passes suivantes pour éliminer les pixels cachés. Ainsi, les pixels cachés ne sont pas texturés et pixel shadés, avec certitude.
Toujours est-il qu'une prépasse z utilise les ROP "à moitié", dans le sens où seul le tampon de profondeur est utilisé, par la gestion des couleurs. Mais il se trouve que les circuits qui servent pour l''alpha blending'' peuvent être réutilisés pour faire les comparaisons de profondeur ! Le résultat est que les ROP peuvent fonctionner à double vitesse lors d'une prépasse z ! Cela demande cependant de concevoir les circuits du ROP pour en profiter. L'optimisation est parfois appelée le '''''z-fast pass'''''.
Tous les GPU depuis la Geforce FX en sont capables. Il y a cependant quelques contraintes. Premièrement, le ROP doit être configuré de manière à n’accéder qu'au tampon de profondeur, ils ne doivent pas dessiner dans le ''framebuffer''. L'''alpha blending'' doit être désactivé, de même que l'alpha-test. D'autres contraintes supplémentaires sont parfois présentes, surtout sur les vieux GPUs. Par exemple, l'antialiasing doit être désactivé lors de la prépasse z. Et mine de rien, cela ne marche que pour les prépasses z pures. Par exemple, certaines techniques de rendu différé augmentent la prépasse z pour que celle-ci ne calcule pas que le tampon de profondeur, mais aussi d'autres informations comme les normales : elles ne profitent pas de cette optimisation.
{{NavChapitre | book=Les cartes graphiques
| prev=Les unités de texture
| prevText=Les unités de texture
| next=Le support matériel du lancer de rayons
| nextText=Le support matériel du lancer de rayons
}}{{autocat}}
d5r3ot605o7gqkf1c5c7xwdnjdszux8
Les cartes graphiques/Les unités de texture
0
67395
763299
754811
2026-04-08T20:01:10Z
Mewtow
31375
/* L'implémentation : logicielle versus matérielle */
763299
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière. Reste à appliquer cette logique pour chaque objet pouvant projeter un ombre...
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du points de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
a2q422lwabyb4jdu4mnxhsiphdwdlba
763300
763299
2026-04-08T20:02:53Z
Mewtow
31375
/* Annexe : les shadowmap hardware */
763300
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière. Reste à appliquer cette logique pour chaque objet pouvant projeter un ombre...
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du points de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
893uluwg78648izl6g6xxe46yuuu8fl
763301
763300
2026-04-08T20:06:40Z
Mewtow
31375
/* Annexe : les shadowmap hardware */
763301
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière. Reste à appliquer cette logique pour chaque objet pouvant projeter un ombre...
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du points de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
11k0wxdoryqijcct16g3amaj6x9lx4u
763302
763301
2026-04-08T20:07:12Z
Mewtow
31375
/* Annexe : les shadowmap hardware */
763302
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
ha9r2pdpv3kq8etk3frxt4iq4r7la4z
Mathc initiation/Fichiers h : c61
0
76758
763313
762492
2026-04-09T08:45:44Z
Xhungab
23827
763313
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/005s| Sommaire]]
:
{{Partie{{{type|}}}| L'intégrale de flux de surface}}
:
En analyse vectorielle, on appelle flux d'un champ vectoriel deux quantités scalaires analogues, selon qu'on le calcule à travers une surface ou une courbe. [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/3d-flux/v/conceputal-understanding-of-flux-in-three-dimensions Khanacademy : conceputal understanding of flux in three dimensions]
:
<br>
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/Fichiers h : c61a1|x_hfile.h ............ Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]]
* [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]]
* [[Mathc initiation/Fichiers h : c61a5|x_nxy.h .............. Le vecteur normal n]]
* [[Mathc initiation/a314|x_nxz.h .............. Le vecteur normal n]]
* [[Mathc initiation/Fichiers h : c61a6|x_nyz.h .............. Le vecteur normal n]]
* [[Mathc initiation/Fichiers h : c61a7|x_fdxdy.h ............ Calculer l'intégrale de flux en xy]]
* [[Mathc initiation/a315|x_fdxdz.h ............ Calculer l'intégrale de flux en xz]]
* [[Mathc initiation/Fichiers h : c61a8|x_fdydx.h ............ Calculer l'intégrale de flux en yx]]
* [[Mathc initiation/Fichiers h : c61a9|x_fdydz.h ............ Calculer l'intégrale de flux en yz]]
* [[Mathc initiation/a312|x_fdzdx.h ............ Calculer l'intégrale de flux en zx]]
* [[Mathc initiation/a313|x_fdzdy.h ............ Calculer l'intégrale de flux en zy]]
:
<br>
les fonctions f :
* [[Mathc initiation/Fichiers h : c61fa|f.h]]
:
<br>
Exemples d'application :
* [[Mathc initiation/Fichiers c : c61ca|c00a.c ............ ex : en xy ]]
* [[Mathc initiation/Fichiers c : c61cb|c00b.c ............ ex : en yx ]]
* [[Mathc initiation/Fichiers c : c61cc|c00c.c ............ ex : en yz ]]
:
{{AutoCat}}
2ykqcxfafffiv8yjzo1xy162j778xaf
Mathc initiation/Fichiers h : c61fa
0
76768
763316
715360
2026-04-09T09:03:31Z
Xhungab
23827
763316
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
Installer ce fichier dans votre répertoire de travail.
{| class="wikitable"
|+ Texte de la légende
|-
|
{{Fichier|fa.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as fa.h */
/* ---------------------------------- */
#define LOOP 2*250
/* ---------------------------------- */
double M(
double x,
double y,
double z)
{
return( (3*x) );
}
char Meq[] = "+ 3*x";
/* ---------------------------------- */
double N(
double x,
double y,
double z)
{
return( (3*y) );
}
char Neq[] = "+ 3*y";
/* ---------------------------------- */
double P(
double x,
double y,
double z)
{
return( (z) );
}
char Peq[] = " + z";
/* ---------------------------------- */
/* ---------------------------------- */
double f(
double x,
double y)
{
return( (9-x*x-y*y) );
}
char feq[] = "9-x**2-y**2";
/* ---------------------------------- */
/* ---------------------------------- */
double v(
double y)
{
return( sqrt(9-y*y) );
}
char veq[] = "+sqrt(9-y**2)";
/* ---------------------------------- */
double u(
double y)
{
return( -sqrt(9-y*y) );
}
char ueq[] = "-sqrt(9-y**2)";
/* ---------------------------------- */
/* ---------------------------------- */
double by = 3.; char byeq[] = "3";
double ay = -3.; char ayeq[] = "-3";
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
||
{{Fichier|fb.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as fb.h */
/* ---------------------------------- */
#define LOOP 2*250
/* ---------------------------------- */
double M(
double x,
double y,
double z)
{
return( (x) );
}
char Meq[] = "x";
/* ---------------------------------- */
double N(
double x,
double y,
double z)
{
return( (y) );
}
char Neq[] = "y";
/* ---------------------------------- */
double P(
double x,
double y,
double z)
{
return( (z) );
}
char Peq[] = "z";
/* ---------------------------------- */
/* ---------------------------------- */
double f(
double x,
double y)
{
return( (1-x*x-y*y) );
}
char feq[] = "1-x**2-y**2";
/* ---------------------------------- */
/* ---------------------------------- */
double v(
double x)
{
return( sqrt(1-x*x) );
}
char veq[] = "+sqrt(1-x**2)";
/* ---------------------------------- */
double u(
double x)
{
return( -sqrt(1-x*x) );
}
char ueq[] = "-sqrt(1-x**2)";
/* ---------------------------------- */
/* ---------------------------------- */
double bx = 1.; char bxeq[] = "+1";
double ax = -1.; char axeq[] = "-1";
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
|-
|
{{Fichier|fc.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as fc.h */
/* ---------------------------------- */
#define LOOP 2*250
/* ---------------------------------- */
double M(
double x,
double y,
double z)
{
return( (x+y) );
}
char Meq[] = "x+y";
/* ---------------------------------- */
double N(
double x,
double y,
double z)
{
return( (z) );
}
char Neq[] = "+ z";
/* ---------------------------------- */
double P(
double x,
double y,
double z)
{
return( (x*z) );
}
char Peq[] = " + xz";
/* ---------------------------------- */
/* ---------------------------------- */
double k(
double y,
double z)
{
return( (1) );
}
char keq[] = "1";
/* ---------------------------------- */
/* ---------------------------------- */
double v(
double z)
{
return( +1 );
}
char veq[] = "+1";
/* ---------------------------------- */
double u(
double z)
{
return( -1 );
}
char ueq[] = "-1";
/* ---------------------------------- */
/* ---------------------------------- */
double bz = 1.; char bzeq[] = "+1";
double az = -1.; char azeq[] = "-1";
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
||
|}
{{AutoCat}}
a3p8su8q2g2pp4guun9i0hgsggei5nr
Mathc initiation/Fichiers h : c62
0
76774
763314
762493
2026-04-09T08:45:58Z
Xhungab
23827
763314
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/005s| Sommaire]]
:
{{Partie{{{type|}}}| L'intégrale de flux de surface simplifiée }}
:
En analyse vectorielle, on appelle flux d'un champ vectoriel deux quantités scalaires analogues, selon qu'on le calcule à travers une surface ou une courbe. [[https://en.wikipedia.org/wiki/Surface_integral wikipedia]]
:
<br>
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/Fichiers h : c62a1|x_hfile.h ............ Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]]
* [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]]
* [[Mathc initiation/Fichiers h : c62a7|x_fdxdy.h ............ Calculer l'intégrale de flux en xy]]
* [[Mathc initiation/Fichiers h : c62a8|x_fdxdy.h ............ Calculer l'intégrale de flux en yx]]
* [[Mathc initiation/Fichiers h : c62a9|x_fdxdy.h ............ Calculer l'intégrale de flux en yz]]
:
<br>
les fonctions f :
* [[Mathc initiation/Fichiers h : c61fa|f.h]]
:
<br>
Exemples d'application :
* [[Mathc initiation/Fichiers c : c62ca|c00a.c ............ ex : en xy ]]
* [[Mathc initiation/Fichiers c : c62cb|c00b.c ............ ex : en yx ]]
* [[Mathc initiation/Fichiers c : c62cc|c00c.c ............ ex : en yz ]]
:
{{AutoCat}}
ko8x9ffsu1cepuxsucoyt4v9n9j3ljh
Wikilivres:GUS2Wiki
4
78643
763296
762719
2026-04-08T19:16:14Z
Alexis Jazz
81580
Updating gadget usage statistics from [[Special:GadgetUsage]] ([[phab:T121049]])
763296
wikitext
text/x-wiki
{{#ifexist:Project:GUS2Wiki/top|{{/top}}|This page provides a historical record of [[Special:GadgetUsage]] through its page history. To get the data in CSV format, see wikitext. To customize this message or add categories, create [[/top]].}}
Les données suivantes sont en cache et ont été mises à jour pour la dernière fois le 2026-04-07T11:38:05Z. {{PLURAL:5000|1=Un seul résultat|5000 résultats}} au maximum {{PLURAL:5000|est disponible|sont disponibles}} dans le cache.
{| class="sortable wikitable"
! Gadget !! data-sort-type="number" | Nombre d’utilisateurs !! data-sort-type="number" | Utilisateurs actifs
|-
|AncreTitres || 33 || 0
|-
|ArchiveLinks || 12 || 0
|-
|Barre de luxe || 35 || 2
|-
|BoutonsLiens || 41 || 0
|-
|CategoryAboveAll || 19 || 0
|-
|CategorySeparator || 22 || 0
|-
|CoinsArrondis || 99 || 1
|-
|CollapseSidebox || 35 || 1
|-
|CouleurContributions || 33 || 1
|-
|CouleursLiens || 25 || 1
|-
|DeluxeAdmin || 5 || 1
|-
|DeluxeEdit || 36 || 1
|-
|DeluxeHistory || 46 || 2
|-
|DeluxeImport || 19 || 1
|-
|DeluxeRename || 16 || 1
|-
|DeluxeSummary || 36 || 2
|-
|DevTools || 17 || 1
|-
|DirectPageLink || 25 || 1
|-
|Emoticons || 43 || 1
|-
|EmoticonsToolbar || 45 || 1
|-
|FastRevert || 36 || 2
|-
|FixArrayAltLines || 22 || 1
|-
|FlecheHaut || 66 || 1
|-
|GoogleTrans || 28 || 0
|-
|HotCats || 81 || 4
|-
|JournalDebug || 21 || 1
|-
|JournalEnTable || 2 || 1
|-
|LeftPaneSwitch || 5 || 1
|-
|ListeABordure || 30 || 1
|-
|LiveRC || 30 || 0
|-
|LocalLiveClock || 27 || 1
|-
|Logo || 42 || 0
|-
|MobileView || 18 || 1
|-
|NavigAdmin || 38 || 0
|-
|OngletEditCount || 44 || 2
|-
|OngletEditZeroth || 66 || 1
|-
|OngletGoogle || 28 || 1
|-
|OngletPurge || 54 || 2
|-
|OptimizedSuivi || 18 || 0
|-
|Popups || 61 || 0
|-
|RenommageCategorie || 6 || 2
|-
|RestaurationDeluxe || 12 || 1
|-
|RevertDiff || 35 || 4
|-
|ScriptAutoVersion || 16 || 1
|-
|ScriptSidebox || 31 || 0
|-
|ScriptToolbar || 42 || 0
|-
|SisterProjects || 16 || 1
|-
|SkinPreview || 4 || 1
|-
|Smart patrol || 2 || 1
|-
|SourceLanguage || 24 || 1
|-
|SousPages || 57 || 3
|-
|SpaceToolbar || 29 || 0
|-
|TableUnicode || 55 || 0
|-
|Tableau || 66 || 1
|-
|TitreDeluxe || 56 || 1
|-
|TitreHierarchique || 17 || 0
|-
|UTCLiveClock || 26 || 0
|-
|UnicodeEditRendering || 29 || 1
|-
|WikEd || 29 || 0
|-
|autonum || 2 || 0
|-
|massblock || 3 || 1
|-
|monBrouillon || 19 || 2
|-
|perpagecustomization || 16 || 1
|-
|recentchangesbox || 15 || 0
|-
|searchFocus || 18 || 0
|-
|searchbox || 30 || 2
|}
* [[Spécial:GadgetUsage]]
* [[m:Meta:GUS2Wiki/Script|GUS2Wiki]]
<!-- data in CSV format:
AncreTitres,33,0
ArchiveLinks,12,0
Barre de luxe,35,2
BoutonsLiens,41,0
CategoryAboveAll,19,0
CategorySeparator,22,0
CoinsArrondis,99,1
CollapseSidebox,35,1
CouleurContributions,33,1
CouleursLiens,25,1
DeluxeAdmin,5,1
DeluxeEdit,36,1
DeluxeHistory,46,2
DeluxeImport,19,1
DeluxeRename,16,1
DeluxeSummary,36,2
DevTools,17,1
DirectPageLink,25,1
Emoticons,43,1
EmoticonsToolbar,45,1
FastRevert,36,2
FixArrayAltLines,22,1
FlecheHaut,66,1
GoogleTrans,28,0
HotCats,81,4
JournalDebug,21,1
JournalEnTable,2,1
LeftPaneSwitch,5,1
ListeABordure,30,1
LiveRC,30,0
LocalLiveClock,27,1
Logo,42,0
MobileView,18,1
NavigAdmin,38,0
OngletEditCount,44,2
OngletEditZeroth,66,1
OngletGoogle,28,1
OngletPurge,54,2
OptimizedSuivi,18,0
Popups,61,0
RenommageCategorie,6,2
RestaurationDeluxe,12,1
RevertDiff,35,4
ScriptAutoVersion,16,1
ScriptSidebox,31,0
ScriptToolbar,42,0
SisterProjects,16,1
SkinPreview,4,1
Smart patrol,2,1
SourceLanguage,24,1
SousPages,57,3
SpaceToolbar,29,0
TableUnicode,55,0
Tableau,66,1
TitreDeluxe,56,1
TitreHierarchique,17,0
UTCLiveClock,26,0
UnicodeEditRendering,29,1
WikEd,29,0
autonum,2,0
massblock,3,1
monBrouillon,19,2
perpagecustomization,16,1
recentchangesbox,15,0
searchFocus,18,0
searchbox,30,2
-->
cg2lf11d0r8v8vvpr9w010o7hsodkx8
Mathc initiation/a458
0
80896
763315
762494
2026-04-09T08:46:13Z
Xhungab
23827
763315
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/005s| Sommaire]]
:
{{Partie{{{type|}}}| L'intégrale de flux de surface définie paramétriquement simplifiée}}
:
En mathématiques, une intégrale de surface est une intégrale définie sur toute une surface qui peut être courbe dans l'espace. Pour une surface donnée, on peut intégrer sur un champ scalaire ou sur un champ vectorie [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-parametrization/v/introduction-to-parametrizing-a-surface-with-two-parameters Khanacademy : introduction to parametrizing a surface with two parameters] ... ... ... [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-integrals-introduction/v/introduction-to-the-surface-integral Khanacademy : introduction to the surface integral]
:
L'intégrale de surface définie paramétriquement :
(b (v(s)
int( int( ||R_s x R_t|| dtds =
(a (u(s)
L'intégrale de flux de surface définie paramétriquement :
(b (v(s)
int( int( f(Rx(s,t), Ry(s,t), Rz(s,t)) . |R_s x R_t| ||R_s x R_t|| dtds =
(a (u(s) ||R_s x R_t||
L'intégrale de flux de surface définie paramétriquement simplifiées :
(b (v(s)
int( int( f(Rx(s,t), Ry(s,t), Rz(s,t)) . |R_s x R_t| dtds =
(a (u(s)
<br>
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a451|x_afile.h ............ Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]]
* [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]]
* [[Mathc initiation/a452|x_rsxrt.h ............ Le produit en croix]]
* [[Mathc initiation/a453|x_pfs_ts.h ......... L'intégrale de flux de surface]]
* [[Mathc initiation/a454|x_pfs_st.h ......... L'intégrale de flux de surface]]
:
<br>
les fonctions f :
* [[Mathc initiation/a455|f.h]]
:
<br>
Exemples d'application :
* [[Mathc initiation/a456|c00a.c .... dtds ]]
* [[Mathc initiation/a457|c00b.c .... dsdt ]]
{{AutoCat}}
79zx4j2ax241asq2ql41afpx3d53cbq
Les cartes graphiques/La microarchitecture des processeurs de shaders
0
81538
763250
763244
2026-04-08T13:50:45Z
Mewtow
31375
763250
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Un processeur contient de nombreux circuits, mais certains d'entre eux sont sdpécialisés dans les calculs. On les appelle des 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, en dehors d'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===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
===Le pipeline d'un processeur de shader et son unité de contrôle===
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la 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. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
t0dxe0crq211hw90iiicqjls8dn8l2u
763251
763250
2026-04-08T13:54:24Z
Mewtow
31375
763251
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
===Le pipeline d'un processeur de shader et son unité de contrôle===
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la 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. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
5eyydcxpttxcvw3tgsfcju5qanndf79
763252
763251
2026-04-08T13:54:40Z
Mewtow
31375
/* Le pipeline d'un processeur de shader et son unité de contrôle */
763252
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la 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. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
g46ilfdrsfkyigtwkag5tqe7nkrmttd
763253
763252
2026-04-08T14:14:03Z
Mewtow
31375
/* Le pipeline d'un processeur de shader et son unité de contrôle */
763253
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la 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. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
4qk4mtw2fgn6uivecn4ysfpgi2lcjnu
763254
763253
2026-04-08T14:15:19Z
Mewtow
31375
/* Le Fine Grained Multithreading avec un scoreboard élaboré */
763254
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la 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. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
c42vfobxu67rssedbfseffm661sphdb
763255
763254
2026-04-08T14:15:35Z
Mewtow
31375
/* L'encodage explicite des dépendances sur les GPU post-2010 */
763255
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la 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. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
6pe6wzwemmiab8l2hspzdyg7m4pd2ml
763256
763255
2026-04-08T14:15:52Z
Mewtow
31375
/* Le Fine Grained Multithreading avec un scoreboard élaboré */
763256
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la 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. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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 ''Fine Grained Multithreading'' avec encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
b53wsxcfimzyw9p5ymu64hizlq47g3p
763261
763256
2026-04-08T14:50:02Z
Mewtow
31375
/* Le multithreading matériel des processeurs de shaders */
763261
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''thread'' important pour masquer la latence. Pour donner un exemple, prenons les Geforce 6 et 7, qui avaient des processeurs séparés pour les ''vertex'' et pixel shaders. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
lzcg69bs1u7p95pia56l2pnp72yduun
763262
763261
2026-04-08T14:54:57Z
Mewtow
31375
/* Le Coarse Grained Multithreading de l'époque DirectX 9 */
763262
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
hhq8da6gzk32mdzb5mfm8yc7rw73g65
763263
763262
2026-04-08T14:55:26Z
Mewtow
31375
/* Le Coarse Grained Multithreading de l'époque DirectX 9 */
763263
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
dcx2wtj1t2xfvc7phit3eyd42oweju4
763264
763263
2026-04-08T14:55:47Z
Mewtow
31375
/* Le Fine Grained Multithreading avec un scoreboard élaboré */
763264
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
ky6r647anrykcafj6w6e73m2ub77h6d
763265
763264
2026-04-08T15:04:28Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763265
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les GPU qui ont suivi utilisaient une forme élaborée de ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Cependant, les accès mémoires ne sont pas dans ce cas, et les ''threads'' sont mis en pause quand ils lancent un accès mémoire. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
hqssv0ff7g1wreysj85bg8fzbkg4vtv
763266
763265
2026-04-08T15:04:47Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763266
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les GPU qui ont suivi utilisaient une forme élaborée de ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
afka089wcq42y8nd697ti3nr92g73et
763267
763266
2026-04-08T15:05:39Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763267
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les GPU qui ont suivi utilisaient une forme élaborée de ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
fwnz158npvnmvoy0oelaii6ccv9ay3o
763268
763267
2026-04-08T15:10:34Z
Mewtow
31375
/* Le CGMT avec lectures non-bloquantes */
763268
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Par la suite, les processeurs de ''shaders'' ont intégré une optimisation pour éviter de changer de ''thread'' si une lecture survient. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. La technique fonctionne si la lecture est suivie par d'autres instructions, disons que c'est des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupére la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les GPU qui ont suivi utilisaient une forme élaborée de ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
7xfk0pirn3tf2maaeu487jxes27ek1y
763269
763268
2026-04-08T15:10:44Z
Mewtow
31375
/* Le CGMT avec lectures non-bloquantes */
763269
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Par la suite, les processeurs de ''shaders'' ont intégré une optimisation pour éviter de changer de ''thread'' si une lecture survient. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. La technique fonctionne si la lecture est suivie par d'autres instructions, disons que c'est des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupére la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 10, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les GPU qui ont suivi utilisaient une forme élaborée de ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
o83uu37iqm1h99ox7464r0qbd0pxqn7
763270
763269
2026-04-08T15:17:13Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763270
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Par la suite, les processeurs de ''shaders'' ont intégré une optimisation pour éviter de changer de ''thread'' si une lecture survient. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. La technique fonctionne si la lecture est suivie par d'autres instructions, disons que c'est des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupére la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 10, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les GPU qui ont suivi utilisaient une forme élaborée de ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Mais sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent, si le ''thread'' est bloqué en raison d'une dépendance quelconque. La différence avec le CGMT est que les lectures ne sont plus les seuls évènements qui peuvent bloquer un ''thread''.
L’apparition des processeurs de shaders unifiés a coïncidé avec une augmentation du nombre d'étapes du pipeline. Je rappelle que le nombre d'étapes varie selon le processeur. Il a augmenté avec les GPU DirectX 10, ce qui a commencé à poser quelques problèmes. Les situations où deux instructions utilisent les mêmes registres a augmenté, ce qui fait que les dépendances de données sont devenus un problème. Pour régler cela, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures : toute dépendance de donnée déclenche un changement de ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
cc0zpsmme5fn8365o4ulwax2n8bs74n
763271
763270
2026-04-08T15:19:00Z
Mewtow
31375
/* Le pipeline d'un processeur de shader */
763271
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Par la suite, les processeurs de ''shaders'' ont intégré une optimisation pour éviter de changer de ''thread'' si une lecture survient. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. La technique fonctionne si la lecture est suivie par d'autres instructions, disons que c'est des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupére la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 10, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les GPU qui ont suivi utilisaient une forme élaborée de ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Mais sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent, si le ''thread'' est bloqué en raison d'une dépendance quelconque. La différence avec le CGMT est que les lectures ne sont plus les seuls évènements qui peuvent bloquer un ''thread''.
L’apparition des processeurs de shaders unifiés a coïncidé avec une augmentation du nombre d'étapes du pipeline. Je rappelle que le nombre d'étapes varie selon le processeur. Il a augmenté avec les GPU DirectX 10, ce qui a commencé à poser quelques problèmes. Les situations où deux instructions utilisent les mêmes registres a augmenté, ce qui fait que les dépendances de données sont devenus un problème. Pour régler cela, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures : toute dépendance de donnée déclenche un changement de ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
swjpgtasliprsls7seulydjjruhzowj
763272
763271
2026-04-08T15:20:58Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763272
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Par la suite, les processeurs de ''shaders'' ont intégré une optimisation pour éviter de changer de ''thread'' si une lecture survient. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. La technique fonctionne si la lecture est suivie par d'autres instructions, disons que c'est des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupére la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 10, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
Les GPU qui ont suivi utilisaient une forme élaborée de ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Mais sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent, si le ''thread'' est bloqué en raison d'une dépendance quelconque. La différence avec le CGMT est que les lectures ne sont plus les seuls évènements qui peuvent bloquer un ''thread''.
L’apparition des processeurs de shaders unifiés a coïncidé avec une augmentation du nombre d'étapes du pipeline. Je rappelle que le nombre d'étapes varie selon le processeur. Il a augmenté avec les GPU DirectX 10, ce qui a commencé à poser quelques problèmes. Les situations où deux instructions utilisent les mêmes registres a augmenté, ce qui fait que les dépendances de données sont devenus un problème. Pour régler cela, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures : toute dépendance de donnée déclenche un changement de ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
hba5imvv6bbhm0kbi2d2v1p3ps9s66t
763273
763272
2026-04-08T15:22:51Z
Mewtow
31375
/* Le multithreading matériel des processeurs de shaders */
763273
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Par la suite, les processeurs de ''shaders'' ont intégré une optimisation pour éviter de changer de ''thread'' si une lecture survient. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. La technique fonctionne si la lecture est suivie par d'autres instructions, disons que c'est des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupére la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 10, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
Les GPU qui ont suivi utilisaient une forme élaborée de ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Mais sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent, si le ''thread'' est bloqué en raison d'une dépendance quelconque. La différence avec le CGMT est que les lectures ne sont plus les seuls évènements qui peuvent bloquer un ''thread''.
L’apparition des processeurs de shaders unifiés a coïncidé avec une augmentation du nombre d'étapes du pipeline. Je rappelle que le nombre d'étapes varie selon le processeur. Il a augmenté avec les GPU DirectX 10, ce qui a commencé à poser quelques problèmes. Les situations où deux instructions utilisent les mêmes registres a augmenté, ce qui fait que les dépendances de données sont devenus un problème. Pour régler cela, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures : toute dépendance de donnée déclenche un changement de ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
7g42kkdy9qywpfp09flfrivqwfej6mh
763274
763273
2026-04-08T15:23:30Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763274
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Par la suite, les processeurs de ''shaders'' ont intégré une optimisation pour éviter de changer de ''thread'' si une lecture survient. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. La technique fonctionne si la lecture est suivie par d'autres instructions, disons que c'est des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupére la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 10, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
Les GPU qui ont suivi utilisaient une forme élaborée de ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Mais sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent, si le ''thread'' est bloqué en raison d'une dépendance quelconque. La différence avec le CGMT est que les lectures ne sont plus les seuls évènements qui peuvent bloquer un ''thread''.
L’apparition des processeurs de shaders unifiés a coïncidé avec une augmentation du nombre d'étapes du pipeline. Je rappelle que le nombre d'étapes varie selon le processeur. Il a augmenté avec les GPU DirectX 10, ce qui a commencé à poser quelques problèmes. Les situations où deux instructions utilisent les mêmes registres a augmenté, ce qui fait que les dépendances de données sont devenus un problème. Pour régler cela, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures : toute dépendance de donnée déclenche un changement de ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
frko6tamm8tjoj2c0mphubpxlbpj52f
763275
763274
2026-04-08T15:30:03Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763275
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Par la suite, les processeurs de ''shaders'' ont intégré une optimisation pour éviter de changer de ''thread'' si une lecture survient. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. La technique fonctionne si la lecture est suivie par d'autres instructions, disons que c'est des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupére la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 10, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading''===
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, hsitoire d'exécuetr plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et il a fallu résoudre ce problème.
Pour cela, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT). Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Mais sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent, si le ''thread'' est bloqué en raison d'une dépendance quelconque. Pour régler cela, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures : toute dépendance de donnée déclenche un changement de ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
omml3v9r3ixyrn9k6kfo0scew6x2ua4
763276
763275
2026-04-08T15:35:39Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763276
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Par la suite, les processeurs de ''shaders'' ont intégré une optimisation pour éviter de changer de ''thread'' si une lecture survient. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. La technique fonctionne si la lecture est suivie par d'autres instructions, disons que c'est des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupére la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 10, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' des années 2005-2010===
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, hsitoire d'exécuetr plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Cependant, cela ne résout pas le problème des accès mémoire. Pour cela, les ''threads'' sont mis en pause en cas d'accès mémoire. Et cela impacte la commutation des ''threads'' à chaque cycle. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
[[File:Full multithreading.png|thumb|Full multithreading]]
Mais sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent, si le ''thread'' est bloqué en raison d'une dépendance quelconque. Pour régler cela, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures : toute dépendance de donnée déclenche un changement de ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
auma303ap4kxf6ox9f9m02ugn05gmxg
763277
763276
2026-04-08T15:38:13Z
Mewtow
31375
/* Le Fine Grained Multithreading des années 2005-2010 */
763277
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le CGMT avec lectures non-bloquantes===
Par la suite, les processeurs de ''shaders'' ont intégré une optimisation pour éviter de changer de ''thread'' si une lecture survient. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. La technique fonctionne si la lecture est suivie par d'autres instructions, disons que c'est des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupére la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. 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, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 10, les GPU combinent les lectures non-bloquantes avec le CGMT. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, 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. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' des années 2005-2010===
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, hsitoire d'exécuetr plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Cependant, cela ne résout pas le problème des accès mémoire. Pour cela, les ''threads'' sont mis en pause en cas d'accès mémoire. Et cela impacte la commutation des ''threads'' à chaque cycle. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent, si le ''thread'' est bloqué en raison d'une dépendance quelconque. Pour régler cela, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures : toute dépendance de donnée déclenche un changement de ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
6jhhivcz92urxz3sppc4uq696k1i5ba
763278
763277
2026-04-08T15:38:21Z
Mewtow
31375
/* Le CGMT avec lectures non-bloquantes */
763278
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''Fine Grained Multithreading'' des années 2005-2010===
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, hsitoire d'exécuetr plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Cependant, cela ne résout pas le problème des accès mémoire. Pour cela, les ''threads'' sont mis en pause en cas d'accès mémoire. Et cela impacte la commutation des ''threads'' à chaque cycle. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent, si le ''thread'' est bloqué en raison d'une dépendance quelconque. Pour régler cela, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc toujours le ''scoreboard'' qui commande le changement de ''thread'', mais il ne se limite pas aux accès aux textures : toute dépendance de donnée déclenche un changement de ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
gmak2ngahqpid6a5edlkekqi2v2umba
763279
763278
2026-04-08T15:45:11Z
Mewtow
31375
/* Le Fine Grained Multithreading des années 2005-2010 */
763279
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''Fine Grained Multithreading'' des années 2005-2010===
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, hsitoire d'exécuetr plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Cependant, cela ne résout pas le problème des accès mémoire. Pour cela, les ''threads'' sont mis en pause en cas d'accès mémoire. Et cela impacte la commutation des ''threads'' à chaque cycle. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent. En clair, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
7b6b91grcgqibcjyepvjqnbfd9b33ro
763280
763279
2026-04-08T15:46:40Z
Mewtow
31375
/* L'encodage explicite des dépendances sur les GPU post-2010 */
763280
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''Fine Grained Multithreading'' des années 2005-2010===
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, hsitoire d'exécuetr plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Cependant, cela ne résout pas le problème des accès mémoire. Pour cela, les ''threads'' sont mis en pause en cas d'accès mémoire. Et cela impacte la commutation des ''threads'' à chaque cycle. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent. En clair, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
mzgmq4vhwd95xnzuividenavmctp5gp
763281
763280
2026-04-08T15:48:21Z
Mewtow
31375
/* Le Fine Grained Multithreading des années 2005-2010 */
763281
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne fonctionne cependant que si les accès mémoire sont peu fréquents et que les ''threads'' font plus de calculs qu'autre chose. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
La technique s'adapte aussi pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Il s'agit de ''Coarse Grained Multithreading'', une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''Fine Grained Multithreading'' des années 2005-2010===
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Cependant, cela ne résout pas le problème des accès mémoire. Pour cela, les ''threads'' sont mis en pause en cas d'accès mémoire. Et cela impacte la commutation des ''threads'' à chaque cycle. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent. En clair, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
4eem8i927kjlunzh894otyxe8vtca8z
763282
763281
2026-04-08T16:57:39Z
Mewtow
31375
/* Le Coarse Grained Multithreading de l'époque DirectX 9 */
763282
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Le ''Fine Grained Multithreading'' des années 2005-2010===
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Cependant, cela ne résout pas le problème des accès mémoire. Pour cela, les ''threads'' sont mis en pause en cas d'accès mémoire. Et cela impacte la commutation des ''threads'' à chaque cycle. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent. En clair, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
qxlhittb4konykr4cy8d2bvvswhcra0
763283
763282
2026-04-08T16:57:50Z
Mewtow
31375
/* Le Coarse Grained Multithreading de l'époque DirectX 9 */
763283
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Le ''Fine Grained Multithreading'' des années 2005-2010===
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Cependant, cela ne résout pas le problème des accès mémoire. Pour cela, les ''threads'' sont mis en pause en cas d'accès mémoire. Et cela impacte la commutation des ''threads'' à chaque cycle. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand les conditions l'imposent. En clair, le processeur change de ''thread'' dès qu'une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
jwm4reaklpuppdj5zv3zaiunjnz5aig
763284
763283
2026-04-08T17:07:22Z
Mewtow
31375
/* Le Fine Grained Multithreading des années 2005-2010 */
763284
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Le ''Fine Grained Multithreading'' des années 2005-2010===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Cependant, cela ne résout pas le problème des accès mémoire. Pour cela, les ''threads'' sont mis en pause en cas d'accès mémoire. Et cela impacte la commutation des ''threads'' à chaque cycle. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
d1hy8usgeu0t818wsjss5a00w0yxdl0
763285
763284
2026-04-08T17:08:29Z
Mewtow
31375
/* Le Fine Grained Multithreading des années 2005-2010 */
763285
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Le ''Fine Grained Multithreading'' des années 2005-2010===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmis les ''thread'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
lajrroaovw4bno5nsizb6bnhdh8bk4d
763286
763285
2026-04-08T17:09:25Z
Mewtow
31375
/* Le Fine Grained Multithreading des années 2005-2010 */
763286
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Le ''Fine Grained Multithreading'' des années 2005-2010===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d''''émission multiple''', qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
izglc7resl1onwogod9lbau1vt5a2vn
763287
763286
2026-04-08T17:11:37Z
Mewtow
31375
/* Le Fine Grained Multithreading des années 2005-2010 */
763287
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Le ''Fine Grained Multithreading'' des années 2005-2010===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Un autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
bvuk2ovfp5526ivjyq2fni20x6f4d9w
763288
763287
2026-04-08T17:11:59Z
Mewtow
31375
/* Le Fine Grained Multithreading des années 2005-2010 */
763288
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Le ''Fine Grained Multithreading'' des années 2005-2010===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Un autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
ed1q93kfi575b647fmijfufezlr1d5z
763289
763288
2026-04-08T17:12:15Z
Mewtow
31375
/* Le Fine Grained Multithreading des années 2005-2010 */
763289
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Le ''Fine Grained Multithreading'' des années 2005-2010===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème. Et pour résoudre ces problèmes, les GPU ont basculé du CGMT au ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Un autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
5wje7ldifi54esl1x18cwu03uctx9zy
763291
763289
2026-04-08T17:21:38Z
Mewtow
31375
/* Le Fine Grained Multithreading des années 2005-2010 */
763291
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' aidé du ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Un autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
d0n52fbvyqmdew78k3l1nhy67auwgss
763292
763291
2026-04-08T17:30:01Z
Mewtow
31375
/* Le Multithreading aidé du scoreboard des années 2005-2010 */
763292
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période Direct X 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' aidé du ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Un autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
39nxcky6gpwqyke30zpxhw46a2ttt7e
763293
763292
2026-04-08T17:31:58Z
Mewtow
31375
/* Le multithreading matériel des processeurs de shaders */
763293
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' aidé du ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread''.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
2o9eu8nz5rw17aeraeb8un39wrzwnak
763294
763293
2026-04-08T17:32:36Z
Mewtow
31375
/* Le Multithreading aidé du scoreboard des années 2005-2010 */
763294
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' aidé du ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
lump7ib9u974mbke5cnqeeppftgmohd
763295
763294
2026-04-08T17:33:21Z
Mewtow
31375
/* Le Multithreading aidé du scoreboard des années 2005-2010 */
763295
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
gprhw0thj2ek52mcjasnrz1cnngwxm5
763303
763295
2026-04-08T22:28:48Z
Mewtow
31375
/* L'unité de contrôle d'un processeur de shader */
763303
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
==L'unité de contrôle d'un processeur de shader==
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
opazkgxxrw6j8zqb15k9i4oo3cp2tss
763304
763303
2026-04-08T22:32:22Z
Mewtow
31375
/* L'unité de contrôle d'un processeur de shader */
763304
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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.
==Le chemin de données d'un processeur de shader==
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
Les processeurs de shader ne font pas exception et incorporent ces circuits. La première différence avec un CPU est que leur unité de contrôle est très simple, car elle n'utilise pas les optimisations 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. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
===L'unité de contrôle d'un processeur de shader===
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
t7jhq4z44vqtuiy974mr1wahrl6fpd3
763305
763304
2026-04-08T22:37:37Z
Mewtow
31375
/* Le chemin de données d'un processeur de shader */
763305
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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'intérieur d'un processeur de shader==
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
===Le chemin de données d'un processeur de shader===
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
===L'unité de contrôle d'un processeur de shader===
L'unité de contrôle d'un GPU a quelques petites différences avec celles d'un CPU moderne. Les unités de contrôle des GPU n'utilisent pas les optimisations 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. A la place, elles utilisent des techniques alternatives qu'on décrira pdans la suite du chapitre, qui sont peu gourmandes en transistors. En conséquence, les unités de contrôle sont très simples, prennent peu de place, utilisent peu de transistors. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===Résumé final===
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre, comme on le verra dans la section suivante. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
cg9ru3t6vq15tl2fp394xlg7chh7t3l
763306
763305
2026-04-08T22:44:03Z
Mewtow
31375
/* Résumé final */
763306
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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'intérieur d'un processeur de shader==
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
===Le chemin de données d'un processeur de shader===
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
===L'unité de contrôle d'un processeur de shader===
L'unité de contrôle d'un GPU a quelques petites différences avec celles d'un CPU moderne. Les unités de contrôle des GPU n'utilisent pas les optimisations 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. A la place, elles utilisent des techniques alternatives qu'on décrira pdans la suite du chapitre, qui sont peu gourmandes en transistors. En conséquence, les unités de contrôle sont très simples, prennent peu de place, utilisent peu de transistors. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===Exemple et résumé final===
Pour finir, nous allons voir un exemple final, celui des GPU Radeon X1000 series, de microarchitecture R500, Terascale. Leur microarchitecture est résumée dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, appelé l'''ultra threaded dispatcher'' sur ces GPU. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU).
De nombreux circuits sont partagés entre plusieurs processeurs. Par exemple, ils partagent un cache L2, dans lequel ils viennent récupérer les données nécessaires. Le GPU contient aussi des contrôleurs mémoire, qui lisent ou écrivent des données en mémoire vidéo. Les contrôleurs mémoire servent surtout d'interface entre la mémoire vidéo et le cache L2, mais ils peuvent aussi envoyer des données lue aux processeurs de shaders.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut et est nommée ''Fetch, Decode, Schedule''. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
kvvrur1zusiosyxs9z6v6njfotrd3st
763307
763306
2026-04-08T22:46:21Z
Mewtow
31375
/* Exemple et résumé final */
763307
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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'intérieur d'un processeur de shader==
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
===Le chemin de données d'un processeur de shader===
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
===L'unité de contrôle d'un processeur de shader===
L'unité de contrôle d'un GPU a quelques petites différences avec celles d'un CPU moderne. Les unités de contrôle des GPU n'utilisent pas les optimisations 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. A la place, elles utilisent des techniques alternatives qu'on décrira pdans la suite du chapitre, qui sont peu gourmandes en transistors. En conséquence, les unités de contrôle sont très simples, prennent peu de place, utilisent peu de transistors. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===Exemple et résumé final===
Pour finir, nous allons voir un exemple final, celui des GPU Radeon X1000 series, de microarchitecture R500, Terascale. Leur microarchitecture est résumée dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, appelé l'''ultra threaded dispatcher'' sur ces GPU. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU).
De nombreux circuits sont partagés entre plusieurs processeurs. Par exemple, ils partagent un cache L2, dans lequel ils viennent récupérer les données nécessaires. Le GPU contient aussi des contrôleurs mémoire, qui lisent ou écrivent des données en mémoire vidéo. Les contrôleurs mémoire servent surtout d'interface entre la mémoire vidéo et le cache L2, mais ils peuvent aussi envoyer des données lue aux processeurs de shaders.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut et est nommée ''Fetch, Decode, Schedule''. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
Niveau interconnexions, les flèches montrent plusieurs choses. Premièrement, l'unité mémoire est reliée aux bancs de registres, ainsi qu'à la mémoire locale. Les accès à la mémoire locale passent par l'unité mémoire, qui sert d'intermédiaire obligatoire. L'unité SIMD est connectée aux registres SIMD, l'unité scalaire est reliée au banc de registres scalaire. Rien d'étonnant. L'unité SIMD peut aussi lire des scalaires pour certaines instructions SIMD verticales, ce qui fait qu'elle est aussi connecté au banc de registres scalaires.
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
owr1p259u868c8hjr33degj9siisyci
763308
763307
2026-04-08T22:47:11Z
Mewtow
31375
/* Exemple et résumé final */
763308
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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'intérieur d'un processeur de shader==
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
===Le chemin de données d'un processeur de shader===
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
===L'unité de contrôle d'un processeur de shader===
L'unité de contrôle d'un GPU a quelques petites différences avec celles d'un CPU moderne. Les unités de contrôle des GPU n'utilisent pas les optimisations 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. A la place, elles utilisent des techniques alternatives qu'on décrira pdans la suite du chapitre, qui sont peu gourmandes en transistors. En conséquence, les unités de contrôle sont très simples, prennent peu de place, utilisent peu de transistors. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===Exemple et résumé final===
Pour finir, nous allons voir un exemple final, celui des GPU Radeon X1000 series, de microarchitecture R500, Terascale. Leur microarchitecture est résumée dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, appelé l'''ultra threaded dispatcher'' sur ces GPU. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU).
De nombreux circuits sont partagés entre plusieurs processeurs. Par exemple, ils partagent un cache L2, dans lequel ils viennent récupérer les données nécessaires. Le GPU contient aussi des contrôleurs mémoire, qui lisent ou écrivent des données en mémoire vidéo. Les contrôleurs mémoire servent surtout d'interface entre la mémoire vidéo et le cache L2, mais ils peuvent aussi envoyer des données lue aux processeurs de shaders.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut et est nommée ''Fetch, Decode, Schedule'' : ''Schedule'' est un synonyme de ''Issue''. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
Niveau interconnexions, les flèches montrent plusieurs choses. Premièrement, l'unité mémoire est reliée aux bancs de registres, ainsi qu'à la mémoire locale. Les accès à la mémoire locale passent par l'unité mémoire, qui sert d'intermédiaire obligatoire. L'unité SIMD est connectée aux registres SIMD, l'unité scalaire est reliée au banc de registres scalaire. Rien d'étonnant. L'unité SIMD peut aussi lire des scalaires pour certaines instructions SIMD verticales, ce qui fait qu'elle est aussi connecté au banc de registres scalaires.
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
t8ld62cfo52r2x02zyw6s0bwcvhw8uw
763309
763308
2026-04-08T22:47:30Z
Mewtow
31375
/* L'unité de contrôle d'un processeur de shader */
763309
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. 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, en dehors d'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).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
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. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité 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'intérieur d'un processeur de shader==
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
===Le chemin de données d'un processeur de shader===
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
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.
===L'unité de contrôle d'un processeur de shader===
L'unité de contrôle d'un GPU a quelques petites différences avec celles d'un CPU moderne. Les unités de contrôle des GPU n'utilisent pas les optimisations 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. A la place, elles utilisent des techniques alternatives qu'on décrira pdans la suite du chapitre, qui sont peu gourmandes en transistors. En conséquence, les unités de contrôle sont très simples, prennent peu de place, utilisent peu de transistors. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité de ''Fetch'' qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' 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 l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===Exemple et résumé final===
Pour finir, nous allons voir un exemple final, celui des GPU Radeon X1000 series, de microarchitecture R500, Terascale. Leur microarchitecture est résumée dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, appelé l'''ultra threaded dispatcher'' sur ces GPU. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU).
De nombreux circuits sont partagés entre plusieurs processeurs. Par exemple, ils partagent un cache L2, dans lequel ils viennent récupérer les données nécessaires. Le GPU contient aussi des contrôleurs mémoire, qui lisent ou écrivent des données en mémoire vidéo. Les contrôleurs mémoire servent surtout d'interface entre la mémoire vidéo et le cache L2, mais ils peuvent aussi envoyer des données lue aux processeurs de shaders.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut et est nommée ''Fetch, Decode, Schedule'' : ''Schedule'' est un synonyme de ''Issue''. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
Niveau interconnexions, les flèches montrent plusieurs choses. Premièrement, l'unité mémoire est reliée aux bancs de registres, ainsi qu'à la mémoire locale. Les accès à la mémoire locale passent par l'unité mémoire, qui sert d'intermédiaire obligatoire. L'unité SIMD est connectée aux registres SIMD, l'unité scalaire est reliée au banc de registres scalaire. Rien d'étonnant. L'unité SIMD peut aussi lire des scalaires pour certaines instructions SIMD verticales, ce qui fait qu'elle est aussi connecté au banc de registres scalaires.
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est 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. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
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 ces 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.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient 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 de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. 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.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée 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 dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. 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. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==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. 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 ''threads''/''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 cœurs, 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 !
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. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD 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. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent 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.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
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'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage 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'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par 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.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
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.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==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.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
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).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
g4bhokxwz7nhjifbi27tphvoxoxgf7y
Économie Petits
0
81983
763317
748380
2026-04-09T09:17:40Z
Xhungab
23827
763317
wikitext
text/x-wiki
= L'Économie pour les Petits =
<div style="text-align:center;"></div>
<div style="text-align:center;"></div>
<div style="text-align:center;">[http://archive.org/details/EconomieToutPetits archive.org/details/EconomieToutPetits]</div>
<div style="text-align:center;">Matthieu Giroux</div>
<div style="text-align:center;">Éditions LIBERLOG</div>
<div style="text-align:center;">Éditeur n° 978-2-9531251 </div>
<div style="text-align:center;">Droits d'auteur 2017</div>
<div style="text-align:center;">Licence Creative Common by SA</div>
= Préface =
Les enfants s'intéressent à la [https://fr.wikipedia.org/wiki/Soci%C3%A9t%C3%A9_(sciences_sociales) société] très tôt. L'[https://fr.wikipedia.org/wiki/%C3%89conomie économie] permet de comprendre la société afin de l'améliorer. Si l'enfant possède la capacité à évoluer tout en connaissant la société, il voudra utiliser l'[https://fr.wikipedia.org/wiki/%C3%89conomie_r%C3%A9elle économie réelle] pour faire des choix importants, tout en améliorant la société.
Je m'inspire des [https://fr.wikisource.org/wiki/Principes_de_la_science_sociale livres] de [https://fr.wikipedia.org/wiki/Henry_Charles_Carey Henry Charles Carey] pour les définitions et la démarche économiques. [https://fr.wikisource.org/wiki/Robinson_Cruso%C3%A9_(Defoe) Robinson Crusoé] est cité par Henry Charles Carey. Je m’inspire de [https://fr.wikisource.org/wiki/Monadologie la monadologie] [https://fr.wikisource.org/wiki/Auteur:Gottfried_Wilhelm_Leibniz de] [https://fr.wikipedia.org/wiki/Gottfried_Wilhelm_Leibniz Leibniz] pour la notion d’infini et de vie. Je me réfère à [https://fr.wikipedia.org/w/index.php?title=Serge_Carfantan Serge Carfantan] pour les notions d’[https://www.babelio.com/livres/Carfantan-Connaissance-de-la-totalite/949967 univers et de vie]. [https://fr.wikipedia.org/wiki/Solidarit%C3%A9_et_progr%C3%A8s Solidarité et Progrès] permet de montrer l’histoire de la société. Vous pouvez consulter les vlogs d’économie [https://www.youtube.com/@Heu7reka Heu?reka] voire [https://www.youtube.com/@ElucidMedia Élucid] [https://www.babelio.com/livres/Heureka-Tout-sur-leconomie-ou-presque/1553666 pour l’économie actuelle]. Vous pouvez consulter [https://fr.wikipedia.org/wiki/David_Elbaz David Elbaz] pour les notions d’[https://www.babelio.com/livres/Elbaz-Les-dix-mille-et-une-nuits-de-lunivers--La-danse/1464408 univers] et à [https://fr.wikipedia.org/wiki/Aur%C3%A9lien_Barrau Aurélien Barrau] [https://ia903000.us.archive.org/6/items/TempsAme/Trous%20noirs%20et%20espace%20pour%20les%20enfants%20conf%C3%A9rence%20de%20Aur%C3%A9lien.mp4 pour la gravité]. Vous pouvez vous référer à [https://www.babelio.com/auteur/Philippe-Guillemant/216706 Philippe Guillemant], difficile d’accès, pour les notions de robotique, de [https://www.babelio.com/livres/Guillemant-La-physique-du-futur-lumineux-Dialogues-entre-ar/1574329 vie, d’univers et d’infini de la vie]. Il y a comme référence aussi les expériences scientifiques de l’apprentissage de la parole avec [https://fr.wikipedia.org/wiki/Noam_Chomsky Chomsky].
= À lire =
Je vous conseille les [https://fr.wikipedia.org/wiki/Liseuse liseuses] à [https://fr.wikipedia.org/wiki/Papier_%C3%A9lectronique encre électronique] tactiles et sans lumière non affiliées à un site web de vente de [https://fr.wikipedia.org/wiki/Livre_num%C3%A9rique livrels].
Même si elles sont plus chères, elles vous permettront de lire l'ensemble des auteurs présents dans mes livrels gratuits. Vous pouvez aller acheter les livrels payants des éditeurs sur leurs sites web ou dans le site web d’une librairie.
Il existe dorénavant des écrans couleurs e-ink qui ne permettent peut-être pas de lire les vidéos. Ce sont les écrans à huile ou miroirs couleurs qui permettent de lire les vidéos. Il s'agit de nanotechnologies.
Toutes les images de ce livre ont été trouvées sur [https://commons.wikimedia.org wikimedia commons]. Vous pouvez retrouver les licences et les fichiers grâce aux noms des fichiers insérés.
= Pourquoi ? =
== Pourquoi j'aime les desserts ? ==
[[Image:Torta_casera_argentina_de_biscochuelo_de_vainilla_con_crema_chantillí_y_duraznos_al_natural.jpg|thumb|top]]
Les desserts possèdent en eux du sucre. Le sucre est très apprécié de notre langue, celle qui goutte le dessert. Seulement, cela ne s'arrête pas là. Le dessert est placé à la fin des repas. Ainsi beaucoup d'enfants s'impatientent à vouloir arriver à la fin du repas. Alors le dessert prend encore plus de [https://fr.wikipedia.org/wiki/Valeur_(%C3%A9conomie) valeur]. La valeur c'est donc un manque. Si les parents ne veulent pas donner du dessert, on attend alors encore plus impatiemment l'autre repas. C'est bien un manque qui crée l'envie de donner de la valeur au dessert. La valeur, c'est donc vouloir quelque chose dont on a besoin, ou que l'on aime.
=== Mes Notes ===
Lire [https://fr.wikisource.org/wiki/Robinson_Cruso%C3%A9_(Defoe) Robinson Crusoé] en se demandant ce qu'est la valeur.
== Pourquoi certains ont-ils beaucoup ? ==
[[Image:Baby_Playing_with_Stacking_Rings.JPG|thumb|top]]
Sais-tu que les [https://fr.wikipedia.org/wiki/Industrie industries] produisent beaucoup de jouets à la fois ? Certains enfants ont beaucoup de jouets parce que les industries en produisent beaucoup. Une industrie crée en [https://fr.wikipedia.org/wiki/Production_en_s%C3%A9rie série] le même jouet de différentes manières. Une série ce sont des jouets identiques fabriqués avant tout par des machines. Les machines sont de grands outils, des mécanismes qui créent chacune une ou plusieurs pièces du jouet. Il y a une petite intervention d'[https://fr.wikipedia.org/wiki/Ouvrier ouvriers], ceux qui travaillent dans les industries, pour terminer le travail des machines.
Les industries sont à protéger, parce que créer en série des jouets demande beaucoup de temps. Comme disent certains, le temps c'est de l'argent. Avec l'argent on peut acheter des jouets. Mais celui qui a beaucoup d'argent n'est pas forcé de créer une industrie, surtout s'il utilise l'argent pour lui-même. En effet, beaucoup pensent qu'il s'agit de penser à soi, alors que penser aux autres permet de mieux évoluer. Évoluer apporte pourtant du bonheur. Évoluer permet aussi de participer à la création d'industries.
Ainsi, si on ne protège pas ses industries, on peut ne plus pouvoir acheter de jouets, de nourriture, d'outils. Sinon on peut découvrir des jouets qui nous transportent dans des beaux voyages.
Si on a trop de jouets, ceux-ci perdent de la valeur. En effet, si on est gâté de jouets, on sait qu'on peut en avoir quand on veut. On ne désire plus forcément jouer, parce qu'on ne s'impatiente plus de jouer. Ainsi, les jouets perdent de la valeur quand il y en a trop.
Aussi des jouets rares, dont on a entendu parler des atouts, sont voulus par toi-même si tu ne les as pas essayés. La rareté connue donne de la valeur à un jouet. La publicité essaie de te faire croire que ton jouet est rare, ce qui est faux. Pour constater cela demande à visiter une industrie.
=== Mes Notes ===
Demander à visiter une industrie.
== Peut-on produire à l'infini ? ==
Tout est créé de façon finie. Par contre on ne peut déterminer tout ce qui a été créé. Mieux, la vie possède en elle une notion d'infini, parce que rien ne peut s'organiser par hasard, parce que la découverte de quelque chose de nouveau et de réel relève du magique. En effet, comment se fait-il qu'on puisse faire tant de choses avec de l'inerte ? Comment se fait-il que la vie naisse à partir de l'inerte ? Nous nous sommes posés cette question en nous mentant souvent à nous-mêmes. Ton corps ne peut pas être créé par hasard selon le livre [http://web.archive.org/web/20240210023515/https://dieulasciencelespreuves.com/ Dieu, la science, les preuves]. Il y aurait donc un autre [https://fr.wikipedia.org/wiki/Univers univers] qui créerait la [https://fr.wikipedia.org/wiki/Vie vie] dans le nôtre. Ainsi il y a une notion d'infini dans la vie, vie sans doute née par hasard, mais vie créant de la vie. Leibniz et Vernadsky expliquent très bien cette notion d'[https://fr.wikisource.org/wiki/Monadologie_(%C3%89dition_Bertrand,_1886)/1714 infini de la vie] dans l'univers.
Pour aller plus loin, la [https://fr.wikipedia.org/wiki/Gravitation gravité], qui est l'attraction d'un corps par un autre, où qu'ils fussent, aurait besoin d'un autre univers pour fonctionner puisqu’elle se propage dans le vide. Des scientifiques pensent que la gravité serait localisée. Mais ça n’est pas possible puisqu’elle agit partout en même temps avec une force répartie.
=== Mes Notes ===
Lire [https://fr.wikisource.org/wiki/Monadologie_(%C3%89dition_Bertrand,_1886)/1714 la Monadologie] de Leibniz. Écrire ensuite.
== C'est quoi être riche ? ==
[[Image:Louis_Armstrong_restored.jpg|thumb|top]]
Ceux qui possèdent un [https://fr.wikipedia.org/wiki/Outil outil] mais qui ne savent pas l'utiliser n'en n’ont pas besoin. Ainsi ceux qui possèdent un outil qu’ils ne savent pas utiliser s'en débarrassent. Quelqu'un qui possède beaucoup de choses, mais qui ne les utilise pas, ne les possède pas réellement. Ainsi il est permis aux pauvres d'utiliser les maisons inutilisées. Par exemple on peut faire appel à la mairie pour réquisitionner des logements inutilisés. C'est économique. L'économie consiste à utiliser ce qui est déjà fait. Le riche possède cette maison. Mais à quoi lui sert-elle s'il ne l'utilise pas ?
=== Mes Notes ===
Méditer sur ce que tu n'utilises pas. Pourquoi ne pas le [https://fr.wikipedia.org/wiki/Vente vendre] ou le [https://fr.wikipedia.org/wiki/Don_(acte) donner] ?
== Pourquoi mon jouet est-il cassé ? ==
[[Image:Drawing_of_Broken_Toys.jpg|thumb|top]]
Ce n'est pas normal qu'un jouet casse facilement. On doit le faire durer longtemps. Ainsi créer des jouets qui ne durent pas longtemps est anti-économique. On n'économise pas suffisamment de [https://fr.wikipedia.org/wiki/Travail_d%27une_force travail] avec. S'il fallait recréer des jouets trop de fois, on finirait pas faire travailler tout le monde à cette tâche.
=== Mes Notes ===
Essayer de réparer un jouet cassé. S'il se répare, c'est qu'il a été bien conçu.
== Pourquoi certains possèdent tout ? ==
La réponse est que certains riches ont le droit de créer la [https://fr.wikipedia.org/wiki/Monnaie monnaie]. Si une seule personne possède la possibilité de créer la monnaie, il ne l'utilise que pour lui. Par contre si une communauté, comme un pays, possède la création monétaire, l'argent bénéficie à tout le pays. Le pays peut alors se développer parce que la création monétaire est partagée. Sinon il régresse.
=== Mes Notes ===
Demande à tes parents si la monnaie est à création publique ou privée. Ils risquent de te répondre difficilement.
== C'est quoi la nature ? ==
[[Image:American_mastodon_Skelton.jpg|thumb|top]]
La [https://fr.wikipedia.org/wiki/Nature nature] c'est ce qui vit autour de toi. La nature est soit visible, soit invisible. Elle est tellement petite qu'on ne la voit pas. Les scientifiques créationnistes pensent qu'il y a une [https://fr.wikipedia.org/wiki/%C3%82me âme] derrière chaque vie. Cette âme permet d'évoluer et de trouver de nouvelles idées, contrairement aux [https://fr.wikipedia.org/wiki/Robot robots].
Tes parents peuvent savoir comment la nature fonctionne, mais ils ne savent pas pourquoi s'ils sont [https://fr.wikipedia.org/wiki/Ath%C3%A9isme athées] ou [https://fr.wikipedia.org/wiki/Agnosticisme agnostiques]. Un athée croit souvent à lui-même ou à l'argent par exemple, alors qu'un agnostique croit qu'il ne croit pas. La science permet de savoir comment la nature fonctionne, afin de trouver des idées [https://fr.wikipedia.org/wiki/Science scientifiques] pour l'améliorer. L'humain peut améliorer la nature par la science. Les scientifiques trouvent de nouvelles idées pour améliorer la nature, afin de créer de nouveaux outils.
=== Mes Notes ===
Écrire pour savoir comment on [https://fr.wikipedia.org/wiki/%C3%89volution_(biologie) évolue]. Que se cache-t-il derrière ?
== Pourquoi mes questions étonnent ? ==
[[Image:Vasari,_Giorgiodel_Sarto,_Andrea_-_Holy_Family_-_Google_Art_Project.jpg|thumb|top]]
Tes questions peuvent surprendre tes parents. En effet, tu apportes certaines fois à tes parents des réponses à leur indécision. Mais comment se fait-il que tu saches déjà faire ça ? Et bien, parce que tu as déjà fais le plus difficile en apprenant à parler et à marcher. En effet, tu as essayé de parler ou de marcher, tu as donc testé. Puis tu as réfléchi pour te dire que tu étais mal compris ou pour mieux bouger. Alors ton esprit a élaboré une hypothèse, mieux que pour un ordinateur, pour apprendre à parler ou à marcher. Cela s'appelle la réflexion scientifique, la seule chose que les robots ne savent pas faire. Tu apprends mieux qu'un ordinateur, parce que tu utilises la réflexion scientifique pour apprendre à parler et à marcher. Ainsi seuls les métiers scientifiques, agricoles et industriels sont réellement importants. Le métier de tes parents peut donc changer. En effet, pour trouver de nouvelles hypothèses, il faut pratiquer sur les limites de la société, afin de faire évoluer les machines, les outils.
=== Mes Notes ===
Lire un livre sur Platon, celui qui a défini la réflexion scientifique dans [https://fr.wikisource.org/wiki/La_R%C3%A9publique_(trad._Cousin) La République] [https://fr.wikisource.org/wiki/La_R%C3%A9publique_(trad._Cousin)/Livre_deuxi%C3%A8me Livre 2].
== Qu'est-ce que le travail ? ==
[[Image:Afghan_agricultural_workers_in_2009.JPG|thumb|top]]
La compréhension de ce qu'est le travail est très récente, donc difficile à comprendre. Le travail c'est produire des [https://fr.wikipedia.org/wiki/Projet projets]. Un projet c'est un objectif à atteindre, avec un début et une fin. Seulement tu le sais sans doute un peu, sans te l'être dit.
En économie, on sait depuis le XIXe siècle que le [https://fr.wikipedia.org/wiki/Travail_d%27une_force travail] c'est de l'[https://fr.wikipedia.org/wiki/%C3%89nergie énergie] à fournir. Mais qu'est-ce que l'énergie ? Et bien quand tu travailles tu bouges et tu peux avoir chaud. La chaleur est une production d'énergie. Quand l'air est chaud, cela veut dire que le soleil a amené de l'énergie sur terre. On peut produire de l'énergie en brûlant aussi.
L'économie mesure comment produire du travail utile. On sait donc maintenant que l'[https://fr.wikipedia.org/wiki/%C3%89conomie économie] mesure l'énergie pour produire des choses. Plus l'énergie est concentrée en un point, plus on peut produire de travail.
=== Mes Notes ===
Lire le début du [https://fr.wikisource.org/wiki/R%C3%A9flexions_sur_la_puissance_motrice_du_feu livre de Nicolas Léonard Sadi Carnot, sur wikisource].
= La société =
== C'est quoi la société ? ==
[[Image:Family_Portrait_in_a_Landscape_WGA.jpg|thumb|top]]
Ta famille est une société. Une entreprise est une société. Un pays est une société. Une société est une organisation d'individus. Décrire une société permet donc de savoir comment s'organise la société. Décrire ta famille permet de décrire une partie de la société.
=== Mes Notes ===
Écrire sur ta famille pour deviner ce qui est similaire à la société.
== Comment comprendre la société ? ==
[[Image:Museo_de_Arqueología_de_Alta_montaña_en_la_provincia_de_Salta.jpg|thumb|top]]
C'est l'[https://fr.wikipedia.org/wiki/Architecture architecture] qui permet de comprendre la société. L'architecture consiste à construire quelque chose avec des pièces et un plan, c'est à dire la description de ce que l'on veut construire. Quand on a construit quelque chose, on peut donc essayer de comprendre comment est construite la société.
=== Mes Notes ===
Demander à tes parents de t'aider à construire quelque chose, pour concevoir autour de toi.
== Pourquoi y a-t-il la société ? ==
[[Image:Aachen_Cathedral_North_View_at_Evening.jpg|thumb|top]]
On ne sait pas exactement pourquoi la société est là. On connaît son histoire. Pour expliquer pourquoi il y a la société, il faut se demander pourquoi chacun est là. Demande cela à tes amis. Ils te répondront dans le vague.
Pour comprendre pourquoi chacun est là, il faut comprendre pourquoi tu vis. Tout ce qu'on sait sur la vie est que l'univers peut créer la vie, qu'il y a quelqu'un qui crée la vie, on ne sait pas exactement pourquoi. Certains pensent que la vie existait avant notre univers.
D'autres ne se posent aucune question sur la vie. Ils sont athées. Ceux qui disent que quelqu'un crée la vie sont croyants. Ils disent qu'un Dieu ou différents Dieux créent la vie. Ils se réunissent dans les lieux de culte pour prier les autres.
=== Mes Notes ===
Penses-tu qu'on sait pourquoi la société s'organise, si on ne sait pas pourquoi il y a la vie ? Ceci est une quête de recherche de vérité.
== Pourquoi respecter les [https://fr.wikipedia.org/wiki/Loi règles] ? ==
[[Image:Hôpital-Camfrout_Arrêté_03-09-1946.jpg|thumb|top]]
Tu dois te demander pourquoi tes parents t'imposent des règles à respecter. Tout d'abord, il ne faut pas casser n'importe quoi, pour qu'on puisse l'utiliser. C'est une loi élémentaire de la société. Nous ne pourrions pas survivre si tout était cassé. Ainsi des lois équitables permettent à une société de se développer. Les lois sont créées par le principe d'égalité dans un pays autonome, égalité qui consiste à dire que la liberté des uns s'arrête là où commence celle des autres. Un avocat peut défaire les lois qui ne vont pas dans ce sens.
=== Mes Notes ===
Penses-tu avoir respecté la liberté des autres ? Discutes-en avec tes parents.
== Qu'est-ce que le [https://fr.wikipedia.org/wiki/Capital capital] ? ==
[[Image:Bruche-Chirgoutte.JPG|thumb|top]]
Le capital c'est ce qui permet de créer des jouets, des outils, de la nourriture. Le capital qui permet de créer la nourriture c'est la vie, la terre, l'argent. L'argent est un moyen qui permet à l'agriculteur, celui qui cultive la terre, de trouver rapidement son capital terre. Ainsi, lorsqu'il possède une terre, la vie lui fournit de la nourriture. Les industries utilisent l'argent et la technique pour créer des produits. L'argent permet à un industriel de mettre en place et de faire évoluer rapidement son industrie. Tout cela demande à respecter les lois qui régissent la société.
=== Mes Notes ===
Penses-tu posséder un capital ? Réfléchis et demande à tes parents en leur montrant ce texte.
== Qu'est-ce que l'appropriation ? ==
[[Image:01_Soiron-_Maisons_bourgeoises_(1).jpg|thumb|top]]
Il existe deux types de mouvements dans la société. Soit beaucoup de nouveaux propriétaires s'affichent. Un propriétaire est quelqu'un qui possède au moins une maison. Ces propriétaires possèdent donc leur maison. Soit peu de propriétaires possèdent de plus en plus de choses. À la fin, il n’y a pratiquement que des locations. En effet, si certains propriétaires possèdent la création monétaire, ils peuvent tout acheter. En effet, la monnaie n'est pas forcément créée pour tous.
Ceux qui achètent leur argent sont alors dépossédés, petit à petit. Ainsi, lorsque la création monétaire appartient au pays, à l'état-nation donc, il existe un mouvement de développement de nouveaux propriétaires. Sinon, ceux qui possèdent la création monétaire font monter les prix sur ce qu'ils achètent pour gagner encore plus. On appelle ce phénomène l'inflation. Ainsi le prix des maisons augmente trop parce que le gain est énorme avec les maisons. Puis, quand ils ont acheté trop de maisons, ils achètent le reste, en possédant surtout les intermédiaires qui vendent les produits. On appelle ce phénomène la concentration. Il se produit alors une montée de tous les prix. Alors les dépossédés ne peuvent plus acheter quoi que ce soit. Il se produit alors une révolte ou une révolution, révolution où la création monétaire devient publique et appartient à l'État. Alors se produit un mouvement de création d'une quantité non négligeable de propriétaires.
=== Mes Notes ===
Est-il facile de devenir propriétaire en ce moment ?
== Pourquoi ce qui est rare et voulu est cher ? ==
[[Image:Ring_Athena_Louvre_Bj1086.jpg|thumb|top]]
Ta maman aime sans doute les bagues. Seulement elle ne veut pas n'importe quoi quoi comme bague. Elle veut des bagues en [https://fr.wikipedia.org/wiki/Or or] ou en [https://fr.wikipedia.org/wiki/Diamant diamant]. L'or c'est du métal jaune. Le diamant est une pierre transparente, un cristal en fait. Au début, ces deux éléments étaient rares.
Maintenant, nous pouvons produire des diamants en comprimant des pierres. Aussi ceux qui possèdent de l'or possèdent beaucoup de monnaie parce que l'or est utilisé pour montrer les richesses d'un pays. Maintenant donc, ce sont les bagues en or qui ont plus de valeur que les bagues avec un diamant sans or. Les trafiquants utilisent cette envie de posséder pour monter les prix. Les prix sont réajustés lorsque l'état est équitable. Ainsi l'or vaut de plus en plus cher. Les diamants eux ont baissé nettement de prix quand on a pu les produire.
=== Mes Notes ===
Qu'aimerais-tu posséder ? Est-ce donc facile pour toi de le posséder ?
== Participer à construire la société ==
[[Image:JauresACarmaux.jpg|thumb|top]]
Tu as sans doute dans la tête quelque chose que tu aimerais faire. Seulement, pour faire cette chose, il te faut l'aide des autres. Seulement les autres ne veulent pas forcément te suivre. Il faut donc les convaincre. Il s’agit donc de faire de la [https://fr.wikipedia.org/wiki/Politique politique]. La politique consiste à convaincre les autres de réaliser son projet.
Ceux qui font de la politique s'enrichissent des autres. Ainsi, si tu fais de la politique, tu te rendras peut-être compte que tu as tort, mais tu sauras comment avoir raison. Celui qui a raison a la vision sur le plus long terme. Se nourrir des autres permet de s'enrichir pour son bonheur. Faire de la politique demande certes à se remettre en cause, mais faire de la politique m'enrichis considérablement.
=== Mes Notes ===
T'es tu souvent remis en cause ? Si oui, tu peux faire de la politique.
= Comment ? =
== Ai-je les bons jouets ? ==
[[Image:Maria_Montessori_(portrait).jpg|thumb|top]]
Si tu veux un jouet qui te passionne demande à tes parents les jouets de madame [https://fr.wikipedia.org/wiki/P%C3%A9dagogie_Montessori Montessori]. Elle a créé des jouets qui te font évoluer. Maintenant, grâce aux industries, tous les enfants pourraient avoir ces jouets. Tous les enfants pourraient évoluer grâce aux industries.
Un jouet Montessori est expliqué par les parents, puis tu l'utilises comme un outil, par jeu, pour ton bonheur. Tu peux ainsi apprendre plus facilement à créer des industries, pour évoluer, pour alors avoir du bonheur.
=== Mes Notes ===
Ai-je des jouets créatifs selon Montessori ?
== D'où vient ce que j'ai ? ==
Tout ce que tu as vient de la nature. Tout ce que tu as autour de toi a été créé à partir de la Terre, que ce soit le vivant ou l'inerte. Tout ce que crée l'humain est inerte. L'humain par contre peut modifier la vie, en égalant donc Dieu.
[[Fichier:Axe-dynamic-color.png|vignette]]
Quand l'humain détermine ce dont il a besoin dans la nature, il détermine ce qu'on appelle des ressources. Une ressource est ce qui permet à l'humain de créer ses outils, ou les jouets des enfants.
Au départ les ressources de l'humain étaient les bois et les pierres, dans la période qu'on appelle la [https://fr.wikipedia.org/wiki/Pr%C3%A9histoire préhistoire]. L'humain ne faisait qu'associer ces éléments.
Puis l'humain a commencé à séparer ce qu'il y avait dans la nature, en chauffant essentiellement. Mais il a séparé les éléments pour ensuite les associer, comme il le faisait avant. Pour séparer les éléments, il en associait. Les grecs avaient indiqué qu'il y avait des éléments difficiles à séparer, qu'on appelle atomes.
C'est un russe nommé [https://fr.wikipedia.org/wiki/Dmitri_Mendele%C3%AFev Mendeleïev] qui a déterminé longtemps après comment étaient les éléments difficilement séparables, les atomes.
Peu après, on a déterminé que les atomes pouvaient produire énormément d'énergie, ce qui crée la chaleur. Les [https://fr.wikipedia.org/wiki/Atome atomes] sont de l'énergie inerte. On a déterminé au XIXe siècle que ce qui produisait de l'énergie, donc de la chaleur, pouvait travailler à la place de l'humain, afin que l'humain n'ait plus qu'à créer.
Une ressource est donc un ensemble d'atomes dont l'humain a besoin pour produire de l'énergie, des outils ou jouets inertes, voire modifier la vie.
=== Mes Notes ===
Aller dans un musée de la préhistoire pour voir comment étaient associés nos premiers outils.
== Quel métier choisir ? ==
[[Image:Tableau_Louis_Pasteur.jpg|thumb|top]]
Après 18 ans, tu seras dans le monde du travail. Il y aura probablement un [https://fr.wikipedia.org/wiki/Revenu_de_base revenu de base] qui te permettra de survivre. Beaucoup de tâches auront été automatisées. Le seul métier dans lequel les robots ne peuvent pas agir facilement est celui de la création, que ce soit artistique, productive, journalistique ou recherche.
On peut croire que les robots peuvent faire cela, mais ils n'inventent rien de nouveau. Ils ne font qu'adapter ce qu’a fait l’humain. Concevoir un spectacle non abstrait nécessite une réflexion scientifique, que le robot n'a pas. Nous serons donc dans les métiers de la conception pour réaliser notre travail.
Il existe des métiers qui individualisent. Par exemple, le métier de [https://fr.wikipedia.org/wiki/Vendeur vendeur] tend à individualiser. Le vendeur a intérêt à être avec quelqu'un de seul pour vendre plus de produits parce qu’on se méfie quand on est plusieurs. En effet, celui qui ne parle pas avec le vendeur réfléchit. Le vendeur veut acheter à pas cher, pour vendre cher, ce qui fait pression sur les [https://fr.wikipedia.org/wiki/Industrie industries]. Or les industries sont plus importantes que les services comme la vente, parce qu’elles sont indispensables pour consommer. Elles ont besoin d'ingénieurs qui concevront les produits.
Les métiers de la création peuvent individualiser ou pas, en fonction surtout des services qui ont besoin de vendre. Si le produit est inutile alors on essaie de le vendre puis on le déstocke pour finir par l’oublier. C'est au concepteur de voir quelle politique il applique. Ainsi il faudra être un très bon concepteur parce que nous serons beaucoup à pouvoir le devenir.
Soit le concepteur pense à l'avenir, soit le concepteur divertit, c'est à dire fait penser à autre chose. Le [https://fr.wikipedia.org/wiki/Divertissement divertissement] permet à la [https://fr.wikipedia.org/wiki/Finance finance] de rendre les gens pessimistes, pour permettre à la finance de posséder la création monétaire en mettant les gens les uns contre les autres. Parler d'avenir permet au concepteur de donner envie de faire comme lui, ou comme tout créatif. Il s'agit de transformer le quotidien pour évoluer, comme ce livre peut le faire. Donner envie d'évoluer permet d'inciter à l'optimisme, parce que la vie est changement.
=== Mes Notes ===
Qu'aimerais-tu concevoir ?
== Pourquoi les prix montent-ils ? ==
Ceux qui [https://fr.wikipedia.org/wiki/Cr%C3%A9ation_mon%C3%A9taire créent la monnaie] ont la responsabilité de la montée des prix, qui empêche d'acquérir des outils facilement. En effet, si beaucoup de monnaie est créée, celle-ci perd de la valeur. Alors les prix montent parce que le financier, celui qui crée la monnaie, utilise son argent dans l'économie. Alors on ne peut plus facilement s'acheter de la nourriture le temps que la nourriture soit créée.
Le financier, comme il a du pouvoir, peut acheter à un endroit à pas cher, pour vendre à un autre cher, parce que le produit a plus de valeur à cet endroit. On appelle cela le [https://fr.wikipedia.org/wiki/Trafic trafic]. Le trafic, anti-économique, consiste à augmenter les écarts de prix. Le financier redoute les gens créatifs parce que les gens créatifs qui ont la tête sur les épaules vont trouver la source des problèmes grâce au dialogue. C'est pour cela qu'il empêche de vendre des jouets Montessori.
=== Mes Notes ===
Essaye de savoir ce qui n'est pas rare et cher malgré tout.
== Comment on en est arrivé là ==
[[Image:Railway_bridge_over_Saimaa_Canal.jpg|thumb|top]]
Il y a toute une histoire derrière notre société. Au départ nous étions des animaux, des [https://fr.wikipedia.org/wiki/Singe singes]. Puis, en Afrique, beaucoup d'espèces humaines sont nées. Une espèce ce sont des individus pouvant facilement se reproduire entre eux. Notre espèce humaine a maîtrisé les marées et le feu en colonisant l'Afrique. Elle a trouvé d'autres espèces humaines en dehors de l’Afrique, comme [https://fr.wikipedia.org/wiki/Homme_de_N%C3%A9andertal Néandertal] et [https://fr.wikipedia.org/wiki/Homo_erectus Homo Erectus]. Certains, comme les chinois, curieux, se sont beaucoup mariés avec eux. Ils ont acquis un beau patrimoine d'humanité.
Seulement le feu ne permettait que de garder longtemps la nourriture. La cueillette a permis de créer les premiers villages. Puis la culture a permis de réutiliser les graines, ce qui fait pousser les plantes. Puis la maîtrise de l'eau, l'irrigation, a permis de produire encore plus de nourriture. On a alors créé des grandes villes.
Puis l'invention de la roue a permis de résoudre un peu les problèmes d'éloignement, qui empêchaient le commerce, permettant alors de créer des artisanats. Les artisanats ont commencé à créer des manufactures, utilisant des machines manuelles.
Puis on a construit les premières retenues d'eau, afin d'avoir plus d'eau pour produire. Puis on a créé les premiers canaux, des rivières artificielles, permettant de communiquer plus facilement pour le commerce.
Puis on a maîtrisé le soleil avec les moulins à vent, permettant de manger une meilleure farine de pain, quand il y avait du vent. On a donc régulièrement gagné en qualité de vie.
Puis la maîtrise du [https://fr.wikipedia.org/wiki/Charbon charbon], inutilisé jusqu'alors, a permis de créer les premiers [https://fr.wikipedia.org/wiki/Train trains] par l'énergie libérée par le charbon, empêchant alors le trafic par leur accès aisé. Puis le [https://fr.wikipedia.org/wiki/Moteur moteur] à essence, qui permet de tracter les voitures en 2017, a permis de créer les grands travaux, des grands barrages et des canaux très longs et très larges. On a encore produit plus. Il reste encore à créer des canaux et des trains en Afrique et en Amérique du Sud.
Dernièrement on [https://fr.wikipedia.org/wiki/%C3%89nergie_nucl%C3%A9aire casse le minuscule], ce qui ne se voit pas, l'atome. On pensait qu'on ne pouvait pas casser les atomes, mais on peut en casser certains. On peut même fusionner des atomes. On appelle cela le nucléaire. Le nucléaire n'est pas utilisé à toutes ses capacités. En effet, le [https://archive.org/search?query=nucl%C3%A9aire+thorium nucléaire de l'atome thorium] permettrait de verdir les déserts et de coloniser la mer, en dessalant l'eau de mer. En effet, on ne peut pas boire l'eau de mer à cause du sel.
=== Mes Notes ===
Lire des livres accessibles sur l'histoire de l'énergie ou des grands travaux.
== Qui a permis cela ? ==
[[Image:Raffael_058.jpg|thumb|top]]
Ce sont des découvertes scientifiques qui ont permis d'en arriver là. Ce sont les Africains qui ont [https://archive.org/search?query=coovi+gom%C3%A8z+%C3%A9criture inventé l'écriture] et la roue. Les Grecs, avec leur démocratie, c'est à dire le pouvoir du peuple par le peuple, ont permis de publier la démarche scientifique. Avant, c'étaient ceux qui avaient le temps de réfléchir qui créaient les outils. On utilisait l'écriture.
[https://fr.wikisource.org/wiki/Auteur:Platon Platon], élève grec de [https://fr.wikipedia.org/wiki/Socrate Socrate], a publié la réflexion scientifique, afin de comprendre comment transformer la nature. Puis [https://fr.wikipedia.org/wiki/Johannes_Kepler Kepler] a créé la révolution astronomique, en étudiant les planètes. Alors [https://fr.wikipedia.org/wiki/Denis_Papin Denis Papin], avec l'aide de [https://fr.wikipedia.org/wiki/Gottfried_Wilhelm_Leibniz Leibniz], ont permis la création de la [https://fr.wikipedia.org/wiki/Machine_%C3%A0_vapeur machine à vapeur], fonctionnant au charbon. Puis [https://fr.wikipedia.org/wiki/Sadi_Carnot_(physicien) Nicolas Sadi Carnot] a permis l'avènement du moteur à essence. Puis [https://fr.wikipedia.org/wiki/Albert_Einstein Einstein] a permis l’avant dernier bon en avant, le bon en avant [https://fr.wikipedia.org/wiki/%C3%89nergie_nucl%C3%A9aire nucléaire] qui consiste à casser des atomes. Le dernier bon en avant est celui de la [https://fr.wikipedia.org/wiki/Robotique robotique].
=== Mes Notes ===
Se renseigner sur ces scientifiques.
== Qui a organisé la société ? ==
[[Image:Lefebvre_-_Jean-Baptiste_Colbert.jpg|thumb|top]]
L'humain a la capacité à être social. Cela nous vient du [https://fr.wikipedia.org/wiki/Singe singe]. Seulement transformer la société relève maintenant de la [https://fr.wikipedia.org/wiki/Cr%C3%A9ativit%C3%A9 créativité] scientifique. Les [https://fr.wikipedia.org/wiki/Chine_historique chinois] ont toujours eu une grande nation. Les [https://fr.wikipedia.org/wiki/Gr%C3%A8ce_antique grecs] ont créé la première démocratie.
[https://fr.wikipedia.org/wiki/Jean-Baptiste_Colbert Colbert], en France, a créé une révolution dans l'organisation du développement, avant la révolution française. Colbert a permis la création de routes et de canaux, les premiers grands travaux nationaux. C'était un génie de la création de richesses par l'association et le protectionnisme.
[https://fr.wikipedia.org/wiki/Lazare_Carnot Lazare Carnot], à la Révolution, a permis de créer les écoles françaises les plus prestigieuses de France.
[https://fr.wikipedia.org/wiki/Alexander_Hamilton Hamilton], aux États-Unis a lutté pour la monnaie à création publique, afin de sublimer le travail de Colbert, grâce à [https://fr.wikipedia.org/wiki/Henry_Charles_Carey Henry Charles Carey].
Puis l'éducation allemande a été bâtie, par [https://fr.wikipedia.org/wiki/Alexander_von_Humboldt Humboldt] et [https://fr.wikipedia.org/wiki/Friedrich_von_Schiller Schiller]. Alors [https://fr.wikipedia.org/wiki/Johann_Friedrich_Herbart Johann Friedrich Herbart], mal repris, a mis en place l'émancipation de l'élève allemand, c'est à dire le savoir-être allemand. [https://fr.wikipedia.org/wiki/Albert_Einstein Einstein] vient de ces écoles.
Aux États-Unis, [https://fr.wikipedia.org/wiki/Franklin_Delano_Roosevelt Roosevelt] a détruit la spéculation et la dette due à la finance qui possédait la création monétaire en 1933, dette créée par l’achat d'argent, pour permettre une nouvelle émancipation ensuite.
=== Mes Notes ===
Se renseigner sur ces éducateurs ou politiciens.
== Histoire d'un [https://fr.wikipedia.org/wiki/D%C3%A9veloppement_%C3%A9conomique_et_social développement] ==
[[Image:Les_Clayes_sous_Bois_Monument_Jean_Moulin.jpg|thumb|top]]
Je vais te raconter comment le développement a lieu. Tout d'abord, il existe un moment où peu ont tout. Seulement la population se rend de plus en plus compte de la supercherie. Seulement ceux qui possèdent cachent leur mise, de plus en plus difficilement. Ceux qui possèdent craignent le plus les gens créatifs responsables, car ces derniers veulent le développement des autres en général, parce que le développement, que la finance ne veut pas, demande le changement. Or ceux qui ne sont pas créatifs n'aiment pas le changement. Seulement, si les créatifs disparaissent, c'est la fin d'un pays, parce que les créatifs permettent justement à un pays de se développer. Donc les créatifs finissent par convaincre les autres de devenir créatif. Alors le changement vers le développement peut s'opérer. Si le développement ne revient pas, ceux qui possèdent peu doivent partir du pays, ce qu'ils ont dans les faits déjà fait depuis longtemps.
Au début de la monnaie à création privée, les créatifs politisés, appelés résistants, sont moins de cinq pour cents, puis il y a un boom lorsqu'ils convainquent de développer le pays alors que le pays est en récession. Quand la monnaie à création publique est mise en place et qu’on développe le pays, beaucoup deviennent créatifs et heureux parce qu’on a alors créé une [https://fr.wikipedia.org/wiki/R%C3%A9publique république], même s’il n’y a pas de [https://fr.wikipedia.org/wiki/D%C3%A9mocratie démocratie]. Par exemple, les outils créés pour se développer par le [https://fr.wikipedia.org/wiki/Protectionnisme protectionnisme] permettent de faciliter le travail et de passer son temps à créer tout en pouvant exporter des produits.
=== Mes Notes ===
Aimerais-tu évoluer pour rester créatif ?
== Comment développer la société ? ==
[[Image:Solidarité_et_progrès_Paris_2015_2.jpg|thumb|top]]
Pour développer la société, il faut donc avoir l'accès à une monnaie à création publique ou posséder beaucoup d’énergie à pas cher. On peut d'abord chercher le soutien d'un pays possédant une monnaie publique ou passer à l’[https://fr.wikipedia.org/wiki/%C3%89nergie_nucl%C3%A9aire énergie nucléaire]. Seulement si tout le monde possède une énergie pas cher comme le nucléaire ça ne marche que partiellement. Mais on peut aussi instaurer la monnaie publique dans son pays.
Tout d'abord, ceux qui possèdent la [https://fr.wikipedia.org/wiki/Cr%C3%A9ation_mon%C3%A9taire création monétaire] ont créé une dépendance à l'argent pour cacher leur possession de la création monétaire. Alors on ne sait pas ce qu'on peut exactement acheter avec l'argent puisque on peut acheter l'argent avec l'argent. Il faut donc faire le ménage monétaire pour détruire dans la dette de l'état la monnaie qui a été créée par la monnaie. Aussi on régule la bourse qui permet de créer de la monnaie avec de la monnaie quand la finance possède la création monétaire. Ce qui est important, c'est ce que tes parents possèdent comme argent. Il faut donc garder cet argent.
Seulement l'argent qui sert à ceux qui possèdent la création monétaire ne vaut rien puisqu’elle n’est pas utilisée. Il faut donc garder l'argent de ceux qui travaillent, pour détruire l'argent qui empêche de développer le pays, au moins la partie dette.
L'argent est stockée dans les banques. Les banques qui stockent l'argent de tes parents s'appellent les banques de dépôts. Les banques qui stockent l'argent de la finance, ceux qui possèdent ou possédaient la création monétaire, s'appellent les banques financières. Seulement ces deux banques sont mélangées. On s'est aperçu qu'en coupant les banques en deux, pour séparer les dépôts de tes parents de la finance, que l'argent de la spéculation financière coulait. En effet, la finance utilise les dépôts bancaires pour elle.
Alors on peut créer une monnaie à création publique. La monnaie publique va servir au pays à aider l'industrie et l'agriculture, ce qui produit donc. En effet, la finance se moquait de cela, parce qu'elle avait suffisamment pour elle. Alors on aide au maximum l'agriculture avec la création de canaux et d'énergie, énergie permettant de réaliser les canaux. Aussi on aide le commerce des industries, grâce aux trains. Comme cela, le pays se développe. Les industries, à savoir l'industrie et l'agriculture, font vivre le reste du pays. Le pays se développe. On peut vivre longtemps dans le pays. La qualité de vie s'améliore.
=== Mes Notes ===
Se renseigner sur le [https://fr.wikipedia.org/wiki/Glass-Steagall_Act Glass-Steagall], ce qui permet de faire le ménage dans les banques entre la spéculation et les dépôts.
<!--{{AutoCat}}
{{Minilivre}}
[[Catégorie:Économie]]-->
[[Catégorie:Minilivres]]
[[Catégorie:Économie]]
9tzffu4m0shlznz01sbxbpsgz9g01n1
La grammaire fondamentale de l'ido/Mots grammaticaux/Conjonctions
0
83726
763310
762004
2026-04-09T07:27:21Z
Francucelo
123176
/* Exercices */
763310
wikitext
text/x-wiki
Les conjonctions sont des mots qui servent à relier des mots, des expressions, des phrases, des paragraphes, etc.
{| class="wikitable"
|+Conjonctions de coordination
!Ido
!Français
|-
|do
|donc
|-
|e/ed
|et
|-
|ma
|mais
|-
|o/od
|ou
|-
|nek... nek...
|Ni… ni…
|-
|sive... sive...
|Soit... soit...
|-
|ne nur... ma anke...
|Non seulement… mais aussi…
|}
== Exercices ==
Essayez de comprendre le sens des phrases suivantes. La signification des racines des mots lexicaux est indiquée ci-dessous :
* Me e mea amiko parolis, ma pluvis, do ni livis.
** Amik : ami(e)
** Parol : parler
** Pluv : pleuvoir
** Liv : partir
{{Boîte déroulante|titre=Voir les réponses|contenu=* Moi et mon ami parlions, mais il pleuvait, donc nous sommes partis.}}
{{AutoCat}}
s7og7wjctg17sykc1qdqrwcreu4yqet
La grammaire fondamentale de l'ido/Mots grammaticaux/Numéraux
0
83743
763311
762005
2026-04-09T07:52:59Z
Francucelo
123176
/* Exercices */
763311
wikitext
text/x-wiki
L'ido a treize numéraux :
{| class="wikitable"
!Chiffre
!Numéral
|-
|0
|zero
|-
|1
|un
|-
|2
|du
|-
|3
|tri
|-
|4
|quar
|-
|5
|kin
|-
|6
|ses
|-
|7
|sep
|-
|8
|ok
|-
|9
|non
|-
|10
|dek
|-
|100
|cent
|-
|1000
|mil
|}
== Exprimer les nombres ==
Pour exprimer un nombre, on combine les numéraux. L'ordre d'énonciation va de la valeur la plus élevée à la plus faible : on commence par les milliers, puis les centaines, ensuite les dizaines et enfin les unités. Si une position ne contient aucun chiffre (c'est-à-dire si elle est nulle), on ne la mentionne pas. Chaque position est représentée par un mot, issu de la combinaison des mots qui désignent les différentes valeurs.
Pour former des nombres composés de deux chiffres ou plus, on suit la règle suivante : on relie les chiffres (du plus petit au plus grand) par la lettre « a ».
* Par exemple, 30 000 se dit « Tri-a-dek-a-mil », ce qui signifie littéralement « trois fois dix mille ». On écrit généralement « Triadekamil » sans trait d'union.
De plus, pour la phonétique, il faut ajouter un « -e- » entre le chiffre des dizaines et celui des unités (lorsqu'il y en a). Ce lien fait que le chiffre des dizaines et celui des unités forment un seul mot. L'accent tonique reste alors sur l'avant-dernière syllabe.
* Par exemple, 42 se dit « Quaradek-e-du », l'accent tonique étant sur le « e ».
Pour exprimer un nombre décimal, on lit d'abord la partie entière, puis on ajoute une « komo » (virgule) avant de lire les décimales une à une.
* Par exemple, 3,14 se dit « Tri komo un quar ».
{| class="wikitable"
|+Chiffres concrets
!Chiffre
!Ido
|-
|11
|dek-e-un
|-
|12
|dek-e-du
|-
|13
|dek-e-tri
|-
|14
|dek-e-quar
|-
|15
|dek-e-kin
|-
|16
|dek-e-ses
|-
|17
|dek-e-sep
|-
|18
|dek-e-ok
|-
|19
|dek-e-non
|-
|20
|duadek
|-
|30
|triadek
|-
|40
|quaradek
|-
|50
|kinadek
|-
|60
|sesadek
|-
|70
|sepadek
|-
|80
|okadek
|-
|90
|nonadek
|-
|200
|duacent
|-
|404
|quaracent quar
|-
|2000
|duacent
|-
|2048
|duacent quaradek-e-ok
|-
|10,000
|dekamil
|-
|20,000
|duadekamil
|-
|100,000
|centamil
|-
|200,000
|duacentamil
|-
|999,999
|nonacentamil nonadekamil nonamil nonacent nonadek-e-non
|}
== Exprimer la quantité ==
Les nombres sont placés devant le nom pour en indiquer la quantité. Le nom doit changer de terminaison (singulier ou pluriel) en fonction du nombre (voir la section « [[La grammaire fondamentale de l'ido/Mots lexicaux|Mots]] [[La grammaire fondamentale de l'ido/Mots lexicaux|lexicaux : Noms]] »).
* Par exemple « la tri kati » (les trois chats).
== Expression de la séquence ==
Pour exprimer un ordre, on peut placer le nombre après le groupe nominal ou transformer le nombre en adjectif. Pour transformer un nombre en adjectif ordinal, il faut ajouter le suffixe « esm » après le nombre et terminer par la terminaison « a ».
* Par exemple « chapitro quaradek-e-du » (chapitre quarante-deux) et « quaradek-e-duesma chapitro » (quarante-deuxième chapitre)
== Exercices ==
Essayez de formuler les phrases suivants, les racines nécessaires sont indiquées ci-dessous.
* 2,718
* 867 documents
** Document : Dokumentar
* 101e personne
** Personne : Person
{{Boîte déroulante|titre=Voir les réponses|contenu=* Du komo sep un ok.
* Okacent sesadek-e-sep dokumentari.
* Cent unesma persono.
** Ou : Persono cent un.}}
{{AutoCat}}
rla400rjc7c8h7wxh29yx1sgbn8vj6c
763312
763311
2026-04-09T07:53:23Z
Francucelo
123176
/* Exercices */
763312
wikitext
text/x-wiki
L'ido a treize numéraux :
{| class="wikitable"
!Chiffre
!Numéral
|-
|0
|zero
|-
|1
|un
|-
|2
|du
|-
|3
|tri
|-
|4
|quar
|-
|5
|kin
|-
|6
|ses
|-
|7
|sep
|-
|8
|ok
|-
|9
|non
|-
|10
|dek
|-
|100
|cent
|-
|1000
|mil
|}
== Exprimer les nombres ==
Pour exprimer un nombre, on combine les numéraux. L'ordre d'énonciation va de la valeur la plus élevée à la plus faible : on commence par les milliers, puis les centaines, ensuite les dizaines et enfin les unités. Si une position ne contient aucun chiffre (c'est-à-dire si elle est nulle), on ne la mentionne pas. Chaque position est représentée par un mot, issu de la combinaison des mots qui désignent les différentes valeurs.
Pour former des nombres composés de deux chiffres ou plus, on suit la règle suivante : on relie les chiffres (du plus petit au plus grand) par la lettre « a ».
* Par exemple, 30 000 se dit « Tri-a-dek-a-mil », ce qui signifie littéralement « trois fois dix mille ». On écrit généralement « Triadekamil » sans trait d'union.
De plus, pour la phonétique, il faut ajouter un « -e- » entre le chiffre des dizaines et celui des unités (lorsqu'il y en a). Ce lien fait que le chiffre des dizaines et celui des unités forment un seul mot. L'accent tonique reste alors sur l'avant-dernière syllabe.
* Par exemple, 42 se dit « Quaradek-e-du », l'accent tonique étant sur le « e ».
Pour exprimer un nombre décimal, on lit d'abord la partie entière, puis on ajoute une « komo » (virgule) avant de lire les décimales une à une.
* Par exemple, 3,14 se dit « Tri komo un quar ».
{| class="wikitable"
|+Chiffres concrets
!Chiffre
!Ido
|-
|11
|dek-e-un
|-
|12
|dek-e-du
|-
|13
|dek-e-tri
|-
|14
|dek-e-quar
|-
|15
|dek-e-kin
|-
|16
|dek-e-ses
|-
|17
|dek-e-sep
|-
|18
|dek-e-ok
|-
|19
|dek-e-non
|-
|20
|duadek
|-
|30
|triadek
|-
|40
|quaradek
|-
|50
|kinadek
|-
|60
|sesadek
|-
|70
|sepadek
|-
|80
|okadek
|-
|90
|nonadek
|-
|200
|duacent
|-
|404
|quaracent quar
|-
|2000
|duacent
|-
|2048
|duacent quaradek-e-ok
|-
|10,000
|dekamil
|-
|20,000
|duadekamil
|-
|100,000
|centamil
|-
|200,000
|duacentamil
|-
|999,999
|nonacentamil nonadekamil nonamil nonacent nonadek-e-non
|}
== Exprimer la quantité ==
Les nombres sont placés devant le nom pour en indiquer la quantité. Le nom doit changer de terminaison (singulier ou pluriel) en fonction du nombre (voir la section « [[La grammaire fondamentale de l'ido/Mots lexicaux|Mots]] [[La grammaire fondamentale de l'ido/Mots lexicaux|lexicaux : Noms]] »).
* Par exemple « la tri kati » (les trois chats).
== Expression de la séquence ==
Pour exprimer un ordre, on peut placer le nombre après le groupe nominal ou transformer le nombre en adjectif. Pour transformer un nombre en adjectif ordinal, il faut ajouter le suffixe « esm » après le nombre et terminer par la terminaison « a ».
* Par exemple « chapitro quaradek-e-du » (chapitre quarante-deux) et « quaradek-e-duesma chapitro » (quarante-deuxième chapitre)
== Exercices ==
Essayez de formuler les phrases suivantes, les racines nécessaires sont indiquées ci-dessous.
* 2,718
* 867 documents
** Document : Dokumentar
* 101e personne
** Personne : Person
{{Boîte déroulante|titre=Voir les réponses|contenu=* Du komo sep un ok.
* Okacent sesadek-e-sep dokumentari.
* Cent unesma persono.
** Ou : Persono cent un.}}
{{AutoCat}}
t0l36caewz39qrntv9xu5vbujov3p1a