Les cartes graphiques

Chapitre 3.2 : Microarchitecture

Guy Grave

Autrefois, la séparation entre unités de texture et unités de sommet était motivée par un argument simple : les unités de sommet n'accèdent presque jamais à la mémoire, contrairement aux unités de traitement de pixels. Ces dernières doivent très souvent lire des données en mémoire RAM depuis une texture.

L'accès à la mémoire est très lent : on dépasse facilement une centaine de cycles d'horloge. La présence de caches de textures permet d'atténuer la latence des accès mémoire, mais elle ne peut rien faire si les données à lire ne sont pas dans le cache de textures.

Commentez

- Titre : Les cartes graphiques

- Auteur : Guy Grave

- Parution : 29 mai 2014

- Mise à jour : 1 décembre 2019

- Licence : Le contenu de cet article est rédigé par Guy Grave et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Pas de Modification 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- Navigation

- I. Introduction

- II. Microarchitecture de base

- II-A. Mémoire d'instructions

- II-B. Processeur

- II-C. Accès mémoires

- II-D. ALU

- III. Latence mémoire

- III-A. Parallélisme

- III-B. Multithreading matériel

- IV. SIMT

- IV-A. Threads

- IV-B. Warps

- Navigation

Navigation

Tutoriel précédent : processeur de shaders

Sommaire

Tutoriel suivant : multiGPU

I - Introduction

Autrefois, la séparation entre unités de texture et unités de sommet était motivée par un argument simple : les unités de sommet n'accèdent presque jamais à la mémoire, contrairement aux unités de traitement de pixels. Ces dernières doivent très souvent lire des données en mémoire RAM depuis une texture.

L'accès à la mémoire est très lent : on dépasse facilement une centaine de cycles d'horloge. La présence de caches de textures permet d'atténuer la latence des accès mémoire, mais elle ne peut rien faire si les données à lire ne sont pas dans le cache de textures.

II - Microarchitecture de base

Notre processeur de vertex est relié à des registres d'entrée. Les attributs des sommets arrivent par là et dès qu'ils sont disponibles, le processeur peut aller les lire dans ces registres. Ces derniers sont accessibles uniquement en lecture pour plus de facilité. Ces registres sont au nombre de seize et chacun d'entre eux peut stocker quatre nombres flottants.

Le processeur de vertex est aussi relié à des registres de sortie, au reste du pipeline et notamment au rasteriser.

II-A - Mémoire d'instructions

Fait important, les instructions du programme à appliquer sur les sommets ne sont pas chargées depuis la mémoire vidéo. Celle-ci est en effet surbookée et ne peut pas supporter un rythme d'une instruction par cycle d'horloge. Pour être franc, charger les instructions depuis la mémoire vidéo serait à l'origine de gros temps d'attente entre les instructions. Pour éviter cela, le programme à appliquer sur les sommets est chargé dans une petite mémoire, une sorte de « cache » intégré dans le processeur. Les instructions du programme seront lues depuis cette mémoire. Le même programme étant appliqué sur un grand nombre de sommets, il vaut mieux le charger une fois pour toutes dans cette mémoire au lieu de charger nos instructions depuis la mémoire à chaque sommet.

II-B - Processeur

Ce processeur contient un séquenceur qui charge les instructions depuis la mémoire et pilote les autres composants afin d'exécuter l'instruction voulue. Il contient une unité de calcul chargée d'effectuer les opérations sur nos sommets.

Le processeur contient aussi des registres afin de stocker les résultats temporaires des différents calculs.

L'unité de calcul est parfois composée de plusieurs sous-unités :

- des sous-unités pour les opérations simples ;

- et une unité qui sert pour les calculs complexes (exponentielle, logarithme, racine carrée…).

Comme évoqué plus haut, ce processeur contient deux types de registres :

- les registres constants ;

- et les registres généraux, pour les résultats temporaires.

Ces deux types de registres sont placés dans des mémoires (des fichiers de registre) différentes au sein du processeur.

II-C - Accès mémoires

Avec les cartes les plus récentes, il est possible d'échanger des données entre mémoire vidéo et registres généraux. Les processeurs de traitement de sommets peuvent ainsi lire des données depuis la mémoire vidéo. Pour cela, une unité de texture est ajoutée dans le processeur de traitement de sommets.

II-D - ALU

L'unité de calcul elle-même peut être composée de plusieurs sous-unités de calcul. On verra cela dans le chapitre suivant. Tout ce que je peux dire, c'est qu'il n'est pas rare qu'un processeur supporte deux unités de calcul :

- une qui est spécialisée dans les instructions simples comme des additions, multiplications, etc. ;

- et une autre, spécialisée dans les instructions complexes comme les logarithmes et les exponentielles.

C'est notamment le cas de l'unité de vertex shader des Geforce 6800 :

L'architecture des unités de calcul des processeurs de pixels shader a quelques spécificités, notamment sur les cartes graphiques assez anciennes. Les accès aux textures jouant les trouble-fête.

III - Latence mémoire

Tous les pixels doivent accéder à une texture pour être coloriés ; certains traitements devant être effectués par un pixel shader. Toutefois, un accès à une texture est long. Pour lire ou écrire une texture, il faut accéder à la mémoire vidéo qui est vraiment lente. Attendre une bonne centaine de cycles d'horloge lors d'un accès à une texture est un minimum si celle-ci est lue depuis la mémoire vidéo.

Cette lenteur pose quelques problèmes à nos processeurs de shaders. Pour éviter que notre processeur passe trop de temps à attendre que la mémoire ait terminé sa lecture, celui-ci dispose de techniques élaborées. Sans ces dernières, le processeur est bloqué lorsqu'il démarre un nouvel accès mémoire.

III-A - Parallélisme

Notre unité de texture est située dans notre processeur de shader avec toutes les autres unités de calcul. Ceci étant, les processeurs de shaders ont une particularité : l'unité de texture peut fonctionner parallèlement aux autres unités. Cela a un avantage : on peut masquer la latence des accès mémoire en effectuant des calculs en parallèle. Pendant que le processeur lit notre texture, il calcule en parallèle. Ainsi, si des calculs sont indépendants du résultat d'un accès mémoire, on peut « recouvrir » l'accès mémoire par ces calculs. Pas besoin d'attendre que l'accès à la mémoire soit terminé pour poursuivre l'exécution du shader.

Dans ces conditions, un shader doit donc avoir une grande quantité d'instructions à exécuter pour masquer la latence des accès mémoire. Par exemple, si un accès mémoire dure 200 cycles d'horloge, notre processeur de shader doit disposer de 200 instructions à exécuter pour masquer totalement l'accès à la texture. Autant prévenir : ce n'est jamais le cas.

De plus, notre shader effectue souvent plusieurs accès mémoire rapprochés. Dans ces conditions, un accès mémoire peut vouloir démarrer alors que le précédent n'a pas terminé son exécution. Dans le cas le plus simple, notre unité de texture ne peut pas gérer plusieurs lectures en parallèle ; dans ces conditions, la lecture la plus récente doit attendre que l'ancienne termine. Cette lecture mise en attente bloque toutes les instructions qui la suivent. Une solution consiste à mettre bout à bout tous les accès aux textures.

III-B - Multithreading matériel

Trouver suffisamment d'instructions indépendantes d'une lecture dans un shader n'est donc pas une chose facile. Les améliorations au niveau du compilateur de shaders (qui traduit des shaders d'un langage comme HLSL et assembleur) présent dans les pilotes peuvent aider, mais la marge est vraiment limitée. Pour trouver des instructions indépendantes d'une lecture en mémoire, le mieux est d'aller chercher dans d'autres shaders… Et c'est réellement ce qui est fait. Chaque shader doit s'exécuter sur toute une image. Dans les faits, chaque shader va former ce que l'on appelle des threads, qui sont composés :

- d'un shader à exécuter ;

- et d'un morceau d'image qui lui est attribué et qu'il va devoir traiter.

L'idée est de permettre à un processeur de shader de traiter plusieurs threads et de répartir les instructions de ces threads sur l'unité de calcul suivant les besoins. Ainsi, un thread qui attend la mémoire et n'a aucune instruction à exécuter n'utilisera pas l'unité de calcul. On peut alors laisser un autre thread utiliser l'unité de calcul durant ce temps d'attente.

Il existe différentes manières de remplir notre unité de calcul avec des instructions en provenance de plusieurs threads. Suivant la méthode utilisée, on peut se retrouver avec des gains plus ou moins intéressants et une unité de calcul plus ou moins utilisée. Dans ce qui va suivre, on va détailler ces différentes façons.

L'ensemble est détaillé dans mon tutoriel sur les architectures parallèles.

IV - SIMT

Les cartes graphiques récentes fonctionnent un peu différemment de ce qu'on a vu jusqu'à présent. Sur ces cartes graphiques, les processeurs de shaders ne sont pas vraiment des processeurs SIMD, dans le sens où ils n'exécutent pas des opérations sur des vecteurs, comme on l'a vu auparavant. Du point de vue du jeu d'instructions, ces processeurs vont exécuter des instructions normales et non vectorielles sur des pixels individuels.

Ces pixels individuels seront rassemblés en groupes de 16 à 32 pixels à l'exécution par le processeur lui-même. Pour faire une comparaison, ces processeurs pourront, à l'exécution, effectuer la même instruction sur des pixels différents qu'ils vont alors rassembler dans un vecteur. On parle alors de SIMT.

IV-A - Threads

Dans la documentation Nvidia, chaque suite d'instructions indépendantes, chaque programme qui manipule un pixel indépendant à la fois (de manière plus générale, chaque instruction), c'est ce que l'on appelle « un thread ». Chacun de ces threads se voit attribuer un Program Counter, des registres, et un identifiant qui permet de distinguer ce thread parmi tous les autres. Eh oui, car ce thread n'est pas le seul à s'exécuter : la carte graphique exécute en simultané une quantité faramineuse de threads : plus d'une centaine.

Le processeur de shader se voit attribuer un certain nombre de threads par un circuit spécialisé chargé de distribuer ces threads à tous les processeurs de shaders.

IV-B - Warps

Lors de l'exécution, le processeur de shader va regarder si plusieurs des threads cherchent à exécuter la même instruction sur des pixels différents. Si c'est le cas, il fusionne ces instructions identiques entre threads et les transforme en une seule instruction vectorielle. Le résultat de l'instruction vectorielle exécutée est appelé « un warp ».

Il faut noter que les instructions conditionnelles et les branchements posent des problèmes avec des warps : si jamais le résultat d'un branchement ne donne pas le même résultat dans différents threads d'un même warp, on a un problème. Dans ce cas, le processeur se charge d'effectuer la prédication en interne : il utilise quelque chose qui fait le même travail que des instructions de prédication qui utilisent « vector mask register ». Ce processus en question est un peu moins efficace. Dans ce cas, chaque thread est traité un par un : tous les threads pris s'exécutent les uns après les autres, suivis par les threads non pris. Ce mécanisme se base sur une pile matérielle qui mémorise les threads à exécuter selon un certain ordre.

Sur certaines cartes graphiques récentes, le processeur peut démarrer l'exécution de plusieurs warps à la fois.

Navigation

Tutoriel précédent : processeur de shaders

Sommaire

Tutoriel suivant : multiGPU