Aller au contenu principal
own2pwn
gpu/gpu-simt.tsx

Comment un GPU exécute 32 threads en même temps

Pipeline IFL/IIL/RASL, divergence de flow et SIMT stack, SIMT deadlock, ordonnancement des warps, Operand Collector : tout ce qui se passe vraiment a l'interieur d'un coeur SIMT moderne.

Maxime Jérôme··22 min de lecture

Prerequis

  • Comprendre le fonctionnement d'un CPU : unites de calcul (ALU = Arithmetic Logic Unit, IFU = Integer Function Unit, SFU = Special Function Unit, FPU = Floating Point Unit), pipelining et execution speculative, caches
  • Introduction aux GPUs
  • Modele de programmation GPU

Hello ! o/

Dans cette section on va etudier l'architecture et la microarchitecture des GPUs modernes : les coeurs SIMT (Single Instruction Multiple Threads) qui implementent le calcul, et le systeme de memoire GPU.

Aujourd'hui les GPUs executent des dizaines de milliers de threads concurrents, donc il est necessaire d'avoir une architecture qui puisse tenir ce nombre de threads. On pourrait se dire que le systeme de cache est inutile car il y a deja des emplacements memoire sur le GPU qui offrent un acces tres rapide, mais la memoire cache GPU permet de reduire le nombre d'acces hors-GPU, donc par exemple sur la RAM (memoire vive) ou le DD (Disque Dur, memoire virtuelle).

Le pipeline GPU : IFL, IIL, RASL

Comme le CPU, le GPU execute une instruction dans un pipeline. Le pipeline GPU peut etre divise en 2 grandes parties : le SIMT front-end et le SIMD (Single Instruction Multiple Data) back-end. Ce pipeline consiste en 3 boucles qui agissent ensemble : IFL (Instruction-Fetch Loop), IIL (Instruction-Issue Loop) et RASL (Register-Access Scheduling Loop).

pipeline-gpu-3-boucles.txt
  ┌─────────────────────────────────────────────────────────────────────┐
  │                         PIPELINE GPU                                │
  │                                                                     │
  │  ┌──────── IFL (Instruction-Fetch Loop) ────────┐                  │
  │  │                                               │                  │
  │  │  ┌────────┐   ┌─────────┐   ┌────────┐   ┌──────────┐          │
  │  │  │ Fetch  │──►│ I-Cache │──►│ Decode │──►│ I-Buffer │          │
  │  │  └────────┘   └─────────┘   └────────┘   └────┬─────┘          │
  │  └──────────────────────────────────────────────┐ │                │
  │                                                  │ │                │
  │  ┌──────── IIL (Instruction-Issue Loop) ─────────┼─┘               │
  │  │  ┌──────────┐   ┌────────────┐   ┌───────┐   ┌────────────┐    │
  │  │  │ I-Buffer │──►│ Scoreboard │──►│ Issue │──►│ SIMT Stack │    │
  │  │  └──────────┘   └────────────┘   └───────┘   └─────┬──────┘    │
  │  └────────────────────────────────────────────────────┐│           │
  │                                                        ││           │
  │  ┌────── RASL (Register-Access Scheduling Loop) ───────┘│          │
  │  │  ┌──────────────────┐   ┌─────┐   ┌─────┐           │          │
  │  │  │ Operand Collector│──►│ ALU │──►│ MEM │◄──────────┘          │
  │  │  └──────────────────┘   └─────┘   └─────┘                      │
  │  └──────────────────────────────────────────────────────────────── ┘
  └─────────────────────────────────────────────────────────────────────┘
Vue d'ensemble du pipeline GPU en 3 boucles IFL / IIL / RASL.

Voici comment chaque boucle se decompose :

BoucleEtapesRole
IFLFetch, I-Cache, Decode, I-BufferCharge et decode les instructions depuis la memoire
IILI-Buffer, Scoreboard (Table des marques), Issue, SIMT StackSelectionne et emet les instructions prets a etre executees
RASLOperand Collector (OC), ALU, MEMCollecte les operandes depuis les banques de registres, execute, acces memoire

Une seule boucle : le cycle de base

Considerons d'abord une seule boucle. A chaque cycle, le hardware selectionne un warp. L'IP/PC (Instruction Pointer / Program Counter) du warp doit acceder a la memoire de l'instruction pour trouver la prochaine instruction, comme dans un CPU. Une fois l'instruction fetchee, elle est decodee pour comprendre son opcode et les registres qu'elle utilise, a partir du fichier de registre (qui fait partie de l'Operand Collector).

En parallele de la lecture du fichier de registre, les valeurs du masque d'execution SIMT sont determinees. C'est ce qui aide les threads dans leur flow de controle : tous les threads n'ont pas le meme flot de controle, le masque permet de savoir lesquels executent reellement l'instruction courante.

Une fois qu'on a toutes les informations (masque d'execution + registres sources), l'execution procede en SIMD : une seule instruction, plusieurs donnees.

cycle-warp.txt
  [Warp selectionne]
       │
       ▼
  ┌─────────┐     ┌─────────────┐     ┌────────────┐
  │ IP / PC │────►│ I-Cache /   │────►│  Decode    │
  │  du     │     │ Fetch inst. │     │  (opcode + │
  │  warp   │     │             │     │  registres)│
  └─────────┘     └─────────────┘     └─────┬──────┘
                                            │
                              ┌─────────────┴─────────────────┐
                              │                               │
                              ▼                               ▼
                     ┌─────────────────┐          ┌──────────────────────┐
                     │  Lecture du     │          │  Masque d'execution  │
                     │  fichier de     │          │  SIMT determine      │
                     │  registres      │          │  (quels threads      │
                     └────────┬────────┘          │   executent ?)       │
                              │                   └──────────┬───────────┘
                              └─────────────┬────────────────┘
                                            │
                                            ▼
                                   ┌─────────────────┐
                                   │ Execution SIMD  │
                                   │ (1 instr. /     │
                                   │  N donnees)     │
                                   └─────────────────┘
Cycle elementaire de traitement d'un warp dans le pipeline.

Les unites d'execution

Dans un GPU moderne on trouve plusieurs types d'unites de calcul. Certaines sont specialisees pour reduire la latence de fonctions specifiques :

UniteRoleExemple
ALUOperations arithmetiques et logiques generalesadd, mul, and, or
FPUCalcul en virgule flottantefadd, fmul, fma
IFUOperations sur les entiersiadd, imad
SFUFonctions speciales accelerees materiellementsin, cos, reciprocal, SHA-1
Tensor CoreOperations matricielles rapides (Volta+)GEMM, convolutions IA

Generalement ces ALU se regroupent par nombre de threads dans un warp : 32 pour Nvidia, 64 pour AMD. Mais ce n'est pas obligatoire : si on les regroupe par 16 unites, on peut augmenter la frequence (au prix d'une consommation d'energie plus elevee) et pipeliner leur execution.

Modele d'execution SIMT et SIMT Stack

Une fonctionnalite cle des GPUs modernes est le modele d'execution SIMT. Le but : donner au programmeur l'illusion que chaque thread s'execute de maniere independante. Dans nos GPUs modernes, ca se fait par la combinaison d'une unite de prediction traditionnelle et d'une pile de predicats de masques : la SIMT Stack.

Cette combinaison aide l'execution SIMT dans 2 situations :

  • Le flow de controle imbrique : quand le flow de controle d'un thread depend d'un autre.
  • Sauter des calculs quand tous les threads d'un warp ne prennent pas un chemin de flow de controle particulier. Ca evite de calculer les divergences de certaines branches.

Structure de la SIMT Stack

La pile SIMT contient un registre TOS (Top Of Stack) qui represente le sommet de la pile, avec 3 entrees par niveau :

  • Re-convergence : l'adresse ou les threads se regroupent apres leurs flots de controle respectifs.
  • Prochaine divergence : l'adresse du prochain point de branchement.
  • Masque : un ensemble de bits (un bit par thread du warp), ou 1 signifie "ce thread execute ce chemin" et 0 signifie "ce thread est masque sur ce chemin".
simt-stack.txt
  SIMT Stack (warp de 4 threads pour l'exemple)
  ┌──────────────────────────────────────────────────────────┐
  │  TOS (Top Of Stack)                                      │
  │ ┌──────────────────┬───────────────┬────────────────────┐│
  │ │  Re-convergence  │  Divergence   │  Masque (4 threads)││
  │ │  @reconverge     │  @branch      │  [ T0 T1 T2 T3 ]   ││
  │ ├──────────────────┼───────────────┼────────────────────┤│
  │ │  Entree 1        │  @A           │  [ 1  1  1  1 ]    ││  <-- tous actifs
  │ ├──────────────────┼───────────────┼────────────────────┤│
  │ │  Entree 2        │  @B           │  [ 1  1  1  0 ]    ││  <-- T3 diverge vers F
  │ ├──────────────────┼───────────────┼────────────────────┤│
  │ │  Entree 3 (T3)   │  @F           │  [ 0  0  0  1 ]    ││  <-- T3 seul sur chemin F
  │ └──────────────────┴───────────────┴────────────────────┘│
  └──────────────────────────────────────────────────────────┘

  Exemple : T0/T1/T2 prennent l'instruction B, T3 prend l'instruction F.
  Apres execution des 2 chemins, tous se retrouvent au point de re-convergence.
SIMT Stack : structure TOS avec champs re-convergence, divergence et masque.
Divergence de warp
Ici le 4e thread part dans l'instruction F, et les 3 autres dans B. Les threads doivent quand meme tous se retrouver au point de re-convergence. Pendant que T0/T1/T2 executent B, T3 est masque (son bit = 0). Puis les roles s'inversent pour le chemin F.
divergence-flow.txt
  PC    Masque       Description
  ───   ──────────   ────────────────────────────
  @A    [1 1 1 1]    Tous les threads actifs
  @B    [1 1 1 0]    T0/T1/T2 prennent le chemin B
  @C    [1 1 1 0]    ...
  @D    [1 1 1 0]    ...
  @F    [0 0 0 1]    T3 prend le chemin F (T0/T1/T2 masques)
  @G    [0 0 0 1]    ...
  @RCV  [1 1 1 1]    Re-convergence : tous les threads reprennent ensemble
Flow de divergence d'un warp : deux chemins executes sequentiellement, puis re-convergence.

SIMT Deadlock

On va maintenant etudier le SIMT deadlock et des architectures sans pile SIMT : comment notre coeur SIMT fonctionne quand on a des problemes de synchronisation, par exemple un thread au point de re-convergence alors que d'autres sont encore dans leurs flots de controle divergents.

Le probleme : mutex dans un warp

Voici le code problematique :

cuda
A : *mutex = 0;
B : while (!atomicCAS(mutex, 0, 1)) ;
C : // Section critique
    atomicExch(mutex, 0);

La ligne A initialise une variable partagee : un mutex a 0 pour indiquer que le lock est disponible. Sur la ligne B, chaque thread d'un warp execute l'operation atomicCAS (Compare-And-Swap) sur l'adresse memoire contenant le mutex. Dans un CAS : on lit le mutex, on le compare au 2e operande (ici 0). Si la valeur du mutex est 0, le CAS le met a jour avec le 3e operande (ici 1).

Les acces memoire sont concurrents donc serialises : un seul thread verra le mutex a 0, les autres le verront a 1 (la valeur aura ete swappee par le premier thread). Dans la pile SIMT, un seul thread prend un flot de controle different car les autres doivent attendre que le mutex repasse a 0. La section C est un point de re-convergence, mais chaque thread y ira de facon sequentielle : jamais 2 threads n'executent C simultanement.

C'est le SIMT deadlock : chaque thread est dependant de l'autre. Le thread qui tient le mutex ne peut pas avancer car les autres threads du warp (qui attendent en boucle) font partie du meme warp et bloquent l'emission des instructions.

simt-deadlock.txt
  Warp [T0  T1  T2  T3]
  ──────────────────────────────────────────────────────────
  Cycle N   : tous executent atomicCAS(mutex, 0, 1)
  Resultat  : T0 voit mutex=0 (swap reussi), T1/T2/T3 voient mutex=1
  ──────────────────────────────────────────────────────────
  Masque IIL: [1   0   0   0]  T0 prend la section critique
              [0   1   1   1]  T1/T2/T3 restent en boucle while
  ──────────────────────────────────────────────────────────
  Probleme  : T0 ne peut pas avancer (les autres threads
              du meme warp spinlockent et bloquent le warp)
              --> DEADLOCK
SIMT deadlock : T0 a le mutex, T1/T2/T3 spinlock, mais tous sont dans le meme warp.

Solution : BPM, BS, TS

Comme solution a ce probleme, on a le BPM (Barrier Participation Mask) : un masque qui traque quel thread d'un warp participe a un SIMT deadlock ou a une barriere de convergence. Associe a ca, deux autres champs :

  • BS (Barrier State) : indique quels threads ont atteint un etat de barriere de convergence.
  • TS (Thread State) : pour chaque thread du warp, son etat : pret a executer, bloque a une barriere de convergence, ou fini.
bpm-bs-ts.txt
  Par warp :
  ┌─────────────────────────────────────────────────────────────┐
  │  BPM (Barrier Participation Mask)                           │
  │  [ T0: 1 | T1: 1 | T2: 1 | T3: 1 ]                        │
  │  Qui participe a cette barriere de convergence ?            │
  ├─────────────────────────────────────────────────────────────┤
  │  BS (Barrier State)                                         │
  │  [ T0: 0 | T1: 0 | T2: 0 | T3: 0 ]                        │
  │  Qui a DEJA atteint le point de barriere ?                  │
  ├─────────────────────────────────────────────────────────────┤
  │  TS (Thread State)                                          │
  │  T0: READY | T1: BLOCKED | T2: BLOCKED | T3: DONE          │
  │  Etat individuel de chaque thread                           │
  └─────────────────────────────────────────────────────────────┘

  Quand BS == BPM : tous les participants sont arrives
  --> la barriere peut etre levee, le warp reprend.
BPM / BS / TS : les trois champs qui permettent de detecter et resoudre les SIMT deadlocks.

Instructions ADD, WAIT, YIELD

Plusieurs instructions permettent la detection du SIMT deadlock :

InstructionDeclenchementEffet sur BS / BPM
ADDA chaque divergenceMet a jour BPM pour enregistrer les threads qui divergent
WAITA chaque re-convergenceMet a jour BS quand un thread atteint le point de re-convergence
YIELDCas speculatifSpeculatif, nous n'en discutons pas ici

Ces instructions ADD et WAIT changent le BS, qui change le BPM, ce qui permet de traquer les SIMT deadlocks.

Architecture Volta (Nvidia)
Pour l'architecture Volta, la re-convergence n'est pas executee par tous les threads concernes en meme temps. Elle est executee par chaque warp respectif, puis demande un temps de synchronisation bloquant pour regrouper les warps.

Ordonnancement des warps

On retrouve dans le GPU la meme question que pour le CPU pour optimiser l'execution de code : comment et dans quel ordre executer les warps ?

Si on doit attendre qu'un warp finisse de s'executer pour en executer un autre, ca augmente considerablement la latence. Dans un monde ideal on peut cacher cette latence en utilisant un systeme de multithreading a grain fin avec le meme principe que le pipeline CPU : par exemple une fois l'IF (Instruction Fetch) fini, on refait une IF pour ne pas perdre de temps. Mais ca demande au thread d'avoir ses propres registres, ce qui est tres couteux.

Si on augmente le nombre de warps, ca diminuera le nombre de coeurs par puce et augmentera considerablement la taille du fichier de registre. Il y a aussi la solution du CCWS (Cache-Conscious Warp Scheduling) qui dit que lorsque les threads accedeant a des donnees disjointes menent a des structures de donnees complexes, il serait mieux pour un thread donne d'etre repetitivement selectionne pour maximaliser la localite. La selection de warps est donc problematique en termes d'optimisation.

warp-scheduling.txt
  Cycle  Warp selectionne  Etape
  ─────  ────────────────  ────────────────────────────
    1         W0            Fetch instruction A
    2         W1            Fetch instruction B      (W0 attend le cache)
    3         W2            Fetch instruction C      (W0/W1 attendent)
    4         W0            Decode / Issue A         (resultat du fetch)
    5         W1            Decode / Issue B
    6         W3            Fetch instruction D
    ...
  --> Les latences d'un warp sont cachees par l'execution des autres warps.
Ordonnancement de warps : cacher la latence par interleaving.

Pipeline a 2 boucles et le Scoreboard

Ajoutons un nouveau niveau de complexite : ne pas savoir si l'instruction suivante depend d'une ancienne instruction qui n'a pas fini de s'executer. Si on a ces informations, on pourrait reduire le nombre de warps pour executer plus d'instructions. Pour ca, les GPUs implementent l'I-Buffer : les instructions sont placees apres l'acces au cache, ce qui aide a la detection des dependances.

Scoreboard (Table des marques)

Les CPUs utilisent un tableau de marque (scoreboard) : un tableau de bits representant chaque operande et sa completion par rapport aux instructions non completees. Ca permet d'eliminer les dependances de nom et introduit le besoin de logique associative et des stations de reservation. Les GPUs utilisent une execution "in-order" des tableaux de marque, ce qui evite les problemes de hazard sur read-after-write et write-after-write.

C'est simple sur un seul thread. Mais sur un warp c'est different : avec au moins 128 registres par warp et 64 warps par coeur, le tableau de marque demande 8 192 bits pour etre implemente.

Le probleme d'une execution multi-threadee "in-order" : une fois une instruction completee, les prochaines doivent sonder le tableau des marques. Sonder le tableau des marques necessite au moins 4 operandes, donc 256 lectures pour permettre a tous les warps de continuer. C'est tres couteux.

Solutions possibles :

  • Reduire le nombre de warps pouvant sonder la table des marques a chaque cycle (reduit le parallelisme).
  • Solution courante : mettre un cardinal de la table des marques de 3 ou 4 et mettre en place une instruction NOR qui attend que chaque bit de la table des marques soit a 0 pour executer l'instruction.
scoreboard.txt
  Scoreboard (extrait, warp W0)
  ┌──────────────────────────────────────────────────────────┐
  │  Registre  R0  R1  R2  R3  R4  R5  R6  R7  ...  R127   │
  │  En cours   0   1   0   0   1   0   0   0  ...    0     │
  │                 ^               ^                         │
  │                 |               |                         │
  │         load en cours     add en cours                    │
  │                                                          │
  │  Instruction suivante demande R1 et R4                   │
  │  --> bits a 1 : BLOQUEE, on attend la completion         │
  │  --> tous les bits a 0 : instruction peut s'executer     │
  └──────────────────────────────────────────────────────────┘
Scoreboard GPU : tableau de bits par warp, un bit par registre en attente de completion.

Pipeline a 2 boucles : fonctionnement

Dans une architecture a deux boucles :

  • La premiere boucle selectionne un warp pour l'I-Buffer, verifie le PC et accede a la prochaine instruction (dans le cache).
  • La seconde boucle selectionne une instruction dans l'I-Buffer qui n'a plus aucune dependance et l'ajoute a l'execution.

Le meme warp est donc utilise pour plusieurs operations en parallele.

Pipeline a 3 boucles : l'Operand Collector

Dans un systeme a 3 boucles : cacher la latence GPU necessite beaucoup de warps par coeur. Pour supporter le switch de warp cycle par cycle, il faut un grand fichier de registre (de l'ordre de 256 KB par exemple).

L'implementation naive d'un fichier de registre necessite un port par operande par instruction emise par cycle. Une maniere de reduire la taille : simuler un grand nombre de ports en utilisant des banques de memoires a port unique. C'est la structure appelee OC (Operand Collector), introduite par l'architecture Fermi de Nvidia.

Banques de registres

register-banks.txt
  Fichier de registre divise en banques (exemple : 4 banques)
  ┌───────────┬───────────┬───────────┬───────────┐
  │  Banque 0 │  Banque 1 │  Banque 2 │  Banque 3 │
  │  R0, R4   │  R1, R5   │  R2, R6   │  R3, R7   │
  │  R8, R12  │  R9, R13  │  R10, R14 │  R11, R15 │
  │  ...      │  ...      │  ...      │  ...      │
  └───────────┴───────────┴───────────┴───────────┘

  Instruction : add R_dst, R5, R1
    R5 -> Banque 1
    R1 -> Banque 1
    CONFLIT : les 2 operandes sont dans la meme banque !
    --> 2 cycles necessaires pour lire R5 et R1

  Instruction : add R_dst, R5, R2
    R5 -> Banque 1
    R2 -> Banque 2
    OK : pas de conflit, lecture en 1 cycle.
Organisation en banques de registres : chaque banque est a port unique, les conflits de banque coutent des cycles supplementaires.

Le probleme des banques : quand une operation demande par exemple une lecture sur R5 et R1 (tous deux dans la banque 1), ca cree un conflit et demande 2 cycles pour acceder aux donnees. Et si toutes les operations utilisent une seule banque, le principe devient totalement inefficace.

L'Operand Collector en detail

C'est la que l'OC rentre en jeu. Il y a des CU (Collection Units) qui vont enlever ce conflit. Leur principe est similaire a la station de reservation dans l'algorithme de Tomasulo : l'arbitre selectionne les operandes de sorte qu'il n'y ait pas de conflits dans un cycle donne, tout en considerant le WB (Write-Back).

operand-collector.txt
  ┌──────────────────────────────────────────────────────────────────┐
  │                   Operand Collector Unit (OCU)                   │
  │                                                                  │
  │  Instructions en attente (I-Buffer)                              │
  │  [Instr A: besoin R5, R1]  [Instr B: besoin R2, R6]             │
  │         │                         │                              │
  │         ▼                         ▼                              │
  │  ┌─────────────────────────────────────────────────────────┐    │
  │  │  Arbitre / Collection Units                              │    │
  │  │  Cycle N  : lit R5 (banque 1), R2 (banque 2) -- OK      │    │
  │  │  Cycle N+1: lit R1 (banque 1), R6 (banque 2) -- OK      │    │
  │  │  Evite les conflits en interleaving les acces            │    │
  │  └──────────────────────────┬──────────────────────────────┘    │
  │                             │                                    │
  │                             ▼                                    │
  │         ┌───────┐      ┌───────┐      ┌───────┐                 │
  │         │  ALU  │      │  FPU  │      │  SFU  │                 │
  │         └───────┘      └───────┘      └───────┘                 │
  │                                                                  │
  │  Write-Back (WB) considere pour eviter les collisions de banque  │
  └──────────────────────────────────────────────────────────────────┘
OCU (Operand Collector Unit) : les Collection Units arbitrent les acces aux banques pour eviter les conflits.

Le probleme WAR (Write After Read)

Le seul probleme connu de l'OC : il n'y a pas d'ordre d'execution entre les differentes instructions, ce qui peut engendrer le probleme de WAR (Write After Read). Une instruction ecrit sur un registre avant qu'une instruction precedente l'ait lu.

Solutions proposees :

  • Permettre a seulement une instruction de collecter les operandes dans l'OC un par un. Ca reduirait de 10% les performances de l'OC selon les etudes.
  • Pour proposer du parallelisme dans l'OC : un "bloomboard" pour traquer les lectures de registres.
  • Architecture Maxwell de Nvidia : introduction d'une "barriere de dependance a la lecture" geree par des instructions de controle, evitant les hazards WAR pour certaines instructions.
war-hazard.txt
  Ordre prevu   : Instr A lit R3 --> Instr B ecrit R3
  Ordre reel    : Instr B ecrit R3 --> Instr A lit R3 (valeur corrompue !)

  ┌────────────────────────────────────────────────────────┐
  │  Cycle N  : Instr A en cours de collecte (lit R3)      │
  │  Cycle N  : Instr B est aussi en OC (va ecrire R3)     │
  │                                                        │
  │  Si Instr B passe devant (pas d'ordre garanti) :       │
  │  --> R3 est ecrase AVANT qu'Instr A l'ait lu           │
  │  --> Hazard WAR (Write After Read)                     │
  └────────────────────────────────────────────────────────┘

  Solution Maxwell : barriere de dependance a la lecture
  --> l'instruction de controle indique explicitement
      "attendre que toutes les lectures de R3 soient terminees
       avant d'autoriser l'ecriture"
Hazard WAR dans l'Operand Collector : une ecriture depasse une lecture qui n'est pas encore terminee.

Hasards structurels et replay d'instructions

Dans le pipeline GPU il y a aussi des gestions de hasards structurels. Par exemple, l'etape de lecture de registre peut ne plus avoir d'OCU (Operand Collector Units) disponibles. La majorite des hasards structurels sont relies a la memoire systeme : une seule instruction memoire executee par un warp peut etre decoupee en une multitude d'operations et peut utiliser a plein potentiel la portion du pipeline.

Dans un pipeline CPU, la solution est de commencer a executer les instructions suivantes non dependantes. Mais dans un systeme multi-threade c'est moins apprecie pour deux raisons :

  • La taille du fichier de registre et le nombre d'etapes dans le pipeline GPU pour supporter un pipeline graphique peut impacter le chemin critique.
  • Caler une instruction derriere un warp peut decaler les instructions d'autres warps en cas de synchronisation (comme sur l'architecture Pascal).

Pour eviter ces problemes, les GPUs implementent une sorte d'instruction a rejouer. C'est utilise dans certains CPUs pour reparer les predictions speculatives : quand une instruction ne peut pas s'executer (ressource non disponible, dependance non resolue), elle est remise en file pour etre re-essayee plus tard, sans bloquer le reste du pipeline.

Replay vs. stall
Le replay d'instructions est preferable au stall dans un contexte GPU multi-warp : on ne bloque pas toute la machine, on renvoie juste cette instruction dans la queue et on passe au warp suivant. La latence est cachee par les autres warps actifs.

Synthese : les 3 boucles ensemble

pipeline-complet.txt
  SIMT Front-End                        SIMD Back-End
  ┌─────────────────────────────────────────────────────────────────┐
  │                                                                  │
  │  IFL (Instruction-Fetch Loop)                                    │
  │  Fetch ──► I-Cache ──► Decode ──► I-Buffer                       │
  │                                     │                            │
  │  IIL (Instruction-Issue Loop)        │                            │
  │  I-Buffer ──► Scoreboard ──► Issue ──► SIMT Stack                │
  │                                          │                       │
  │  RASL (Register-Access Scheduling Loop)  │                       │
  │  Operand Collector ◄─────────────────────┘                       │
  │       │                                                          │
  │       ├──► ALU  (entiers, FP, logique)                           │
  │       ├──► FPU  (virgule flottante)                              │
  │       ├──► SFU  (fonctions speciales)                            │
  │       └──► MEM  (Load / Store)                                   │
  │                 │                                                │
  │                 ▼                                                │
  │           Write-Back (WB) vers le fichier de registre            │
  │                                                                  │
  └─────────────────────────────────────────────────────────────────┘

  A chaque cycle : le scheduler selectionne un warp eligible
  (registres prets, pas de dependance dans le scoreboard)
  et injecte son instruction dans RASL.
Recapitulatif des 3 boucles IFL/IIL/RASL et leurs interactions.

Sources : General-Purpose Graphics Processor Architecture (Aamodt, Fung, Rogers) ; SIMT Deadlock (Ahmed, UBC) ; Basic Concepts in GPU Computing (Hao Gao)