Skip to main content
Deux concepts importants lorsqu’il est question de synchronisation du GPU sont le pipelining et la cohérence des caches. En termes simples, le GPU peut être vu comme un pipeline composé de nombreuses étapes, où les appels de dessin et les répartitions de calcul (dispatches) entrent par le processeur de commandes et se retirent une fois leur exécution terminée. Il faut donc des mécanismes qui garantissent l’exactitude lorsque plus d’une charge de travail doit accéder à la même mémoire. Si un producteur écrit à une adresse mémoire et qu’un consommateur (ou plusieurs) tente de la lire, nous voulons nous assurer que le producteur a fini d’écrire avant que les consommateurs commencent à lire. La section Pipelining décrit les nombreux mécanismes utilisés pour éviter le chevauchement des charges de travail. Un autre détail important concernant le GPU est que les différents blocs matériels possèdent leurs propres caches mémoire, et que ceux-ci ne sont pas cohérents (ils ne se synchronisent pas et ne s’espionnent pas automatiquement). Pour revenir à l’exemple précédent, si un consommateur lit une adresse mémoire dans un bloc après qu’un producteur a écrit dans cette mémoire (dans un autre bloc du GPU), il faut un moyen de s’assurer que les modifications apportées à cette mémoire sont visibles par tous les consommateurs. La section Cohérence des caches présente les mécanismes offerts pour le garantir.

Pipelining

Pour mettre en œuvre la synchronisation correcte et la plus efficace, il est important de comprendre les blocs matériels du GPU XBOX Series X|S, du moment où il reçoit une commande pour lancer une action jusqu’à la fin de l’exécution de cette action. Le processeur de commandes (CP, Command Processor) est le bloc du GPU qui reçoit les commandes de la mémoire (en termes simples, les commandes que nous ajoutons dans les D3D12CommandLists) sous forme de paquets (qui peuvent être visualisés dans la vue de désassemblage des commandes de PIX). Le CP est un bloc complexe comportant de nombreux éléments, mais nous pouvons le simplifier en le divisant en trois :
  • Command Processor Fetcher (CPF), qui récupère les paquets en mémoire et les envoie au CPG ou au CPC pour traitement ultérieur.
  • Command Processor Graphics (CPG), qui gère et traite les commandes pour le pipeline graphique.
  • Command Processor for Compute (CPC), qui s’exécute en parallèle avec le CPG et gère et traite les commandes pour le pipeline de calcul (utilisé par le calcul asynchrone).
Dans le CPG, le Pre-Fetch Parser (PFP) reçoit les paquets et les exécute ou les envoie au Micro-Engine (ME) au moyen d’une file FIFO. Pour les dessins et les répartitions, le ME signale à la Shader Processing Unit (SPI, unité qui gère l’exécution du travail des nuanceurs) de lancer le dessin ou la répartition. Une fois le dessin ou la répartition terminé (c’est-à-dire que la dernière vague a fini de s’exécuter et, pour les dessins plus précisément, que le back-end de rendu a terminé son travail), nous atteignons la fin du pipeline. Pour le CPC, les paquets sont récupérés par le Micro-Engine Compute (MEC), qui signale à la SPI de lancer la répartition. Pour vous aider à visualiser, voici un diagramme simplifié Interface utilisateur graphique, application Description générée automatiquement
  • Le PFP récupère les données des tampons d’index et gère les arguments indirects, les informations de prédication et la synchronisation avec le ME.
  • Le ME peut déclencher des vidages, lancer des dessins et des répartitions, et dispose d’emplacements CPDMA pour lancer des copies.
  • Le Constant Engine (CE) est un bloc à l’intérieur du CPG qui possède de la RAM servant à stocker les constantes nécessaires à chaque commande de dessin. Il copie ces constantes dans L2, puis signale au ME que les données sont prêtes (afin que le ME puisse retenir les commandes de dessin jusqu’à ce que les constantes soient en place). Le ME peut aussi signaler au CE que les dessins sont terminés afin qu’il puisse réutiliser ses ressources.
  • Le CP Direct Memory Access (CPDMA) se charge d’effectuer les copies (depuis et vers la mémoire ou les registres) et peut être déclenché par le PFP, le ME ou le MEC. Il peut copier depuis et vers n’importe quelle mémoire virtuelle.
Il existe aussi des événements programmables que nous pouvons insérer et qui s’exécuteront à différents endroits du pipeline :
  • Les événements de fin de nuanceur (EoS, End of Shader) s’exécutent une fois que toutes les vagues de la répartition ou du dessin sont terminées, mais n’attendent pas que le back-end de rendu ait terminé (dans le scénario du pipeline fixe).
  • Le bas du pipeline (BoP, Bottom of Pipe) correspond au moment où tout le travail des nuanceurs et du back-end de rendu est terminé, mais ne donne aucune garantie quant aux écritures en mémoire.
    • Pour le calcul, EoS et BoP sont identiques puisqu’il n’y a pas de travail du back-end de rendu.
Comme mentionné précédemment, des mécanismes sont nécessaires pour s’assurer que les charges de travail présentant des conflits de mémoire ne s’exécutent pas en parallèle :
  • Clôtures (fences)
    • Primitives de synchronisation composées d’au moins deux paquets. Le premier écrit une valeur de référence à un emplacement mémoire. Ce paquet est un événement qui peut être configuré pour s’exécuter comme un événement EoS ou comme un événement BoP. Le second paquet est une attente qui bloque un moteur (PFP ou ME), empêchant les paquets suivants de passer, tout en interrogeant continuellement l’emplacement mémoire jusqu’à ce que la valeur lue satisfasse une fonction de comparaison. À ce moment, le moteur bloqué est libéré et le travail peut se poursuivre.
  • Vidage partiel
    • Les vidages partiels font attendre le CP jusqu’à ce que tout le travail lié aux nuanceurs de sommets, de pixels ou de calcul (selon le paquet) soit terminé. Le ME bloque tous les paquets suivants jusqu’à ce qu’il reçoive un signal de la SPI.
Pour mettre tout cela ensemble dans un exemple : si nous voulions lire une ressource (dans un nuanceur de pixels) dans laquelle une répartition de calcul a précédemment écrit, nous pouvons utiliser l’un ou l’autre de ces mécanismes pour nous assurer que ces opérations sont effectuées dans le bon ordre (remarque : nous ne traitons ici que du pipelining; la cohérence des caches n’est pas encore examinée).
  • Pour la clôture, nous créerions une clôture EoS qui écrit en mémoire après le travail de calcul (le premier paquet de clôture viendrait donc après la répartition). Puis, le second paquet de clôture bloquerait le moteur ME jusqu’à ce que la valeur en mémoire soit mise à jour (nous ne pouvons pas bloquer sélectivement les étapes en aval du ME; dans ce scénario, le choix du moteur à bloquer sera donc ME ou PFP).
  • Cela peut aussi être géré au moyen d’un vidage partiel CS, qui bloque le ME jusqu’à ce qu’il reçoive le signal que tout le travail des nuanceurs de calcul est terminé avant de débloquer le moteur. Le vidage partiel ne nécessite qu’un seul paquet, contre deux pour la clôture (il est donc moins coûteux en matière d’opérations mémoire). Mais la clôture offre plus de flexibilité quant au travail à attendre pendant que le moteur est bloqué (puisque l’écriture en mémoire peut avoir lieu après une répartition précise, plutôt qu’après tout le travail de calcul).
Forme Description générée automatiquement L’image ci-dessus montre comment le travail d’un producteur peut chevaucher celui d’un consommateur si aucun de ces mécanismes n’est en place.|

Mécanismes de synchronisation de niveau matériel offerts sur XBOX Series X|S :

Il est utile de connaître les paquets produits par le pilote lorsque nous utilisons des clôtures ou des vidages partiels (ou une API de synchronisation comme les barrières), puisque ceux-ci apparaissent dans les captures PIX chaque fois que nous examinons un événement de synchronisation (et plus de détails sur le masque de bits de chaque paquet sont visibles dans l’onglet de désassemblage des commandes).
  • ACQUIRE_MEM : ce paquet est utilisé par le pilote pour s’assurer qu’une surface n’est plus utilisée (commandes de dessin actives) et que tous les caches qui y sont associés sont vidés avant les opérations suivantes. Le pilote l’utilise pour les opérations qui nécessitent le vidage du CB et du DB (tout le travail CB/DB terminé) ainsi que l’invalidation d’un ou de plusieurs des caches suivants ($K, $I, L0, L1, L2, LM, caches CB, caches DB), et/ou la réécriture pour L2 (suivez ce lien pour la définition des caches). Son fonctionnement est le suivant : il attend d’abord que l’adresse mémoire en question ne soit plus référencée par du travail en cours, puis procède au vidage des caches qui y sont associés. Il peut bloquer le PFP ou le ME (ce paquet s’exécute en haut du pipeline).
  • RELEASE_MEM : permet d’insérer un événement (fin de pipeline ou fin de nuanceur) dans le pipeline. Par exemple, il peut servir à mettre à jour une valeur de clôture ou à effectuer des opérations mémoire (invalidation ou vidage de caches).
  • WAIT_REG_MEM : ce paquet sert à attendre qu’un emplacement dans l’espace des registres ou de la mémoire satisfasse une fonction de comparaison. Il peut servir à évaluer des clôtures. Le paquet permet de définir la fonction de comparaison (par exemple, equal ref vérifie si la valeur en mémoire correspond à la valeur de référence), et la référence est la valeur avec laquelle nous voulons comparer. Il définit aussi quel moteur attendra (PFP, ME ou CE), un masque pour définir les bits à comparer, et l’intervalle d’interrogation (la fréquence à laquelle le moteur vérifiera l’espace mémoire ou de registres, en cycles d’horloge).
  • EVENT_WRITE : permet d’insérer des événements dans le pipeline. Les vidages partiels VS, PS et CS sont de type EVENT_WRITE.
  • PFP_SYNC_ME : force le PFP à se synchroniser avec le bloc ME. Le PFP transmet le paquet au ME et retient les paquets entrants jusqu’à ce qu’il soit libéré par le ME (le ME a terminé le travail et la file FIFO entre le ME et le PFP est vide).
  • DMA_DATA : permet au PFP ou au ME de demander au CP DMA de copier vers ou depuis des registres ou la mémoire (en un seul paquet atomique). Le moteur qui demande la copie (PFP ou ME) peut, de façon facultative, attendre les résultats. Il est possible de préciser si la copie est synchrone ou asynchrone.
Une image L’image ci-dessus montre la vue de désassemblage des commandes de PIX pour une barrière (cette vue peut être ajoutée à partir de l’éditeur de disposition de PIX). Elle utilise un paquet RELEASE_MEM pour effectuer des vidages et des invalidations de caches (CACHE_FLUSH_AND_INV_TS_EVENT), tout en écrivant une valeur de 32 bits à l’EoP. Vous pouvez voir l’adresse à laquelle la valeur est écrite (address_lo et address_hi) ainsi que la valeur (data_lo, 0x5). Une image Ensuite, le paquet suivant est un WAIT_REG_MEM, qui attend que l’écriture en mémoire du paquet précédent soit terminée. En le développant, on voit le type de fonction de comparaison (equal_ref), s’il lit dans un registre ou en mémoire (mem_space, mémoire), la valeur attendue (reference, 0x5) et l’adresse à laquelle il va lire (addr_lo et addr_hi). Remarquez que celles-ci correspondent aux adresses du paquet RELEASE_MEM. Cette barrière effectue donc des opérations mémoire, suivies d’une clôture qui bloque jusqu’à ce que celles-ci soient terminées.

Cohérence des caches

Ce qui précède ne raconte qu’une partie de l’histoire, puisque nous devons encore nous assurer que les accès mémoire sont effectués correctement. Le GPU possède une hiérarchie de mémoire beaucoup plus complexe que celle d’un processeur, étant donné sa charge de travail hautement parallèle. Ses nombreux caches ne sont pas cohérents; il faut donc en faire davantage en matière de synchronisation. Concepts importants lorsqu’il est question de caches :
  • Réécriture (WB, Writeback) : opération par laquelle une plage de lignes de cache (ou toutes) est réécrite dans la mémoire principale (ou dans un cache de niveau supérieur dans la hiérarchie).
  • Invalidation : lorsqu’un cache est invalidé, une plage de ses lignes de cache (ou toutes) est marquée comme non valide, et les lectures futures sont transmises au cache suivant dans la hiérarchie pour récupérer une valeur à jour.
  • Écriture immédiate (write-through) : les écritures ont lieu dans le cache et sont aussi transmises au niveau suivant.
  • Bit modifié (dirty bit) : si une ligne de cache est marquée comme modifiée, cela signifie que le cache a été modifié et que la mémoire principale (et les autres caches) possèdent une copie périmée. Un vidage (WB) sera nécessaire (ainsi que le marquage de ces données comme non valides dans les autres caches afin qu’ils puissent récupérer la valeur à jour).
Le processeur de commandes peut signaler des vidages de caches en haut ou en bas du pipeline. Par exemple, les paquets RELEASE_MEM peuvent déclencher des opérations mémoire (vidages) suivies d’une écriture en mémoire de clôture. Cela garantit que les opérations mémoire sont prêtes avant que le signal de clôture ne se produise. AQUIRE_MEM déclenche aussi des opérations mémoire, mais celles-ci ont lieu au ToP. Voici un diagramme des caches du GPU XBOX Series X|S Interface utilisateur graphique, diagramme Description générée automatiquement

Les caches pertinents sur XBOX Series X|S sont

De plus, les différents caches adoptent des stratégies différentes quant au moment de la réécriture ou de l’invalidation : Pour revenir à l’exemple de la transition d’une écriture CS à une lecture PS, nous pouvons maintenant examiner les caches. Les écritures UAV utilisent les caches L0-L1-L2 et LM. Les SRV utilisent aussi ces mêmes caches. Puisque nous passons d’un état d’écriture à un état de lecture, L0 et L1 seront invalidés. LM sera aussi invalidé puisque le nuanceur de pixels pourrait potentiellement y lire. (L’invalidation de LM aura lieu seulement si la ressource possède des métadonnées.)
Last modified on October 6, 2026