Recherche…


Introduction

Cette rubrique présente certains des mécanismes sous-jacents du calcul parallèle nécessaires pour comprendre et utiliser OpenCL.

Fils et exécution

La clé du parallélisme est d'utiliser plusieurs threads pour résoudre un problème (duh.), Mais il existe des différences avec la programmation multithread classique dans la façon dont les threads sont organisés.

Parlons d'abord de votre GPU typique, pour simplifier, je vais me concentrer sur

Un GPU possède de nombreux cœurs de traitement, ce qui le rend idéal pour exécuter de nombreux threads en parallèle. Ces cœurs sont organisés en Streaming Processors (SM, terme NVidia), dont un GPU a un nombre donné.

Tous les threads exécutés dans un SM sont appelés un «bloc de thread». Il peut y avoir plus de threads sur un SM que sur des cœurs. Le nombre de cœurs définit la "taille de chaîne" (terme NVidia). Les fils à l'intérieur d'un bloc de fil sont traités dans ce que l'on appelle des "warps".

Un exemple rapide à suivre: un NVidia SM classique a 32 cœurs de traitement, sa taille de déformation est donc de 32. threads).

La taille de la chaîne est plutôt importante lors du choix ultérieur du nombre de threads.

Tous les threads à l'intérieur d'une même chaîne partagent un seul compteur d'instructions. Cela signifie que ces 32 threads sont vraiment synchronisés en ce sens que chaque thread exécute chaque commande en même temps. Il y a ici un piège de performance: cela s'applique également aux instructions de branchement dans votre noyau!

Exemple: J'ai un noyau qui a une instruction if et deux branches. 16 de mes threads à l'intérieur d'un warp exécuteront une branche, les 16 autres branches deux. Jusqu'à l'instruction if, tous les threads à l'intérieur du warp sont synchronisés. Maintenant la moitié d'entre eux choisissent une branche différente. Ce qui se passe, c'est que l'autre moitié restera inactive jusqu'à ce que la mauvaise instruction ait fini de s'exécuter sur les 16 premiers threads. Ensuite, ces threads seront dormants jusqu'à ce que les 16 autres threads aient terminé leur branche.

Comme vous pouvez le voir, de mauvaises habitudes de branchement peuvent ralentir considérablement votre code parallèle, car les deux instructions sont exécutées dans le pire des cas. Si tous les threads d'un warp décident qu'ils n'ont besoin que de l'une des instructions, l'autre est complètement ignorée et aucun délai ne se produit.

Synchroniser les threads n'est pas simple non plus. Vous ne pouvez synchroniser les threads qu'avec un seul SM. Tout ce qui est en dehors du SM est indissociable du noyau. Vous devrez écrire des noyaux séparés et les lancer l'un après l'autre.

Mémoire GPU

Le GPU offre six régions de mémoire différentes. Ils diffèrent par leur latence, leur taille et leur accessibilité à partir de différents threads.

  • Mémoire globale: La plus grande mémoire disponible et l'un des rares à échanger des données avec l'hôte. Cette mémoire a la latence la plus élevée et est disponible pour tous les threads.
  • Mémoire constante: Une partie en lecture seule de la mémoire globale, qui ne peut être lue que par d'autres threads. Son avantage est la faible latence par rapport à la mémoire globale
  • Texture Memory: également une partie de la mémoire constante, spécialement conçue pour les textures
  • Mémoire partagée: cette région mémoire est placée près du SM et ne peut être accessible que par un seul bloc de thread. Il offre une latence bien inférieure à la mémoire globale et un peu moins de latence que la mémoire constante.
  • Registers: Uniquement accessible par un seul thread et la mémoire la plus rapide de tous. Mais si le compilateur détecte qu'il n'y a pas assez de registres pour les besoins du noyau, il externalisera les variables dans la mémoire locale.
  • Mémoire locale: une partie de la mémoire accessible uniquement dans la région de mémoire globale. Utilisé comme sauvegarde pour les registres, à éviter si possible.

Accès à la mémoire

Le scénario type de votre utilisation de la mémoire consiste à stocker les données source et les données traitées dans la mémoire globale. Lorsqu'un threadblock démarre, il copie d'abord toutes les parties pertinentes dans la mémoire partagée avant de placer leurs pièces dans les registres.

La latence d'accès à la mémoire dépend également de votre stratégie de mémoire. Si vous accédez aveuglément aux données, vous obtiendrez les pires performances possibles.

Les différentes mémoires sont organisées en "banques". Chaque demande de mémoire pour une banque peut être traitée en un seul cycle d'horloge. Le nombre de banques dans la mémoire partagée est égal à la taille de la chaîne. La vitesse de la mémoire peut être augmentée en évitant les accès conflictuels à la banque à l’intérieur d’une chaîne.

Pour copier de la mémoire partagée depuis ou vers la mémoire globale, le moyen le plus rapide consiste à «aligner» vos appels de mémoire. Cela signifie que le premier thread d'un warp doit accéder au premier élément de la banque de la mémoire partagée et de la mémoire globale. Le deuxième thread le deuxième élément et ainsi de suite. Cet appel sera optimisé en une seule instruction de transfert de mémoire qui copie la banque entière dans la mémoire cible en une fois.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow