Calcul parallèle sur des GPU NVIDIA ou un superordinateur dans chaque foyer. Utilisation efficace du calcul GPU du GPU au CPU

Utiliser le calcul GPU avec C++ AMP

Jusqu’à présent, en discutant des techniques de programmation parallèle, nous n’avons considéré que les cœurs de processeur. Nous avons acquis certaines compétences en parallélisant des programmes sur plusieurs processeurs, en synchronisant l'accès aux ressources partagées et en utilisant des primitives de synchronisation à grande vitesse sans utiliser de verrous.

Cependant, il existe une autre façon de paralléliser les programmes : unités de traitement graphique (GPU), ayant plus de cœurs que même les processeurs hautes performances. Les cœurs GPU sont excellents pour mettre en œuvre des algorithmes de traitement de données parallèles, et leur grand nombre compense largement les inconvénients liés à l'exécution de programmes dessus. Dans cet article, nous allons nous familiariser avec l'une des façons d'exécuter des programmes sur un GPU, en utilisant un ensemble d'extensions du langage C++ appelé C++AMP.

Les extensions C++ AMP sont basées sur le langage C++, c'est pourquoi cet article présentera des exemples en C++. Cependant, avec une utilisation modérée du mécanisme d'interaction dans. NET, vous pouvez utiliser les algorithmes C++ AMP dans vos programmes .NET. Mais nous en reparlerons à la fin de l'article.

Introduction à l'AMP C++

Essentiellement, un GPU est le même processeur qu'un autre, mais avec un ensemble d'instructions spéciales, un grand nombre de cœurs et son propre protocole d'accès à la mémoire. Cependant, il existe de grandes différences entre les GPU modernes et les processeurs conventionnels, et les comprendre est essentiel pour créer des programmes qui utilisent efficacement la puissance de traitement du GPU.

    Les GPU modernes ont un très petit jeu d’instructions. Cela implique certaines limitations : manque de capacité à appeler des fonctions, ensemble limité de types de données pris en charge, manque de fonctions de bibliothèque, etc. Certaines opérations, telles que les branchements conditionnels, peuvent coûter beaucoup plus cher que des opérations similaires effectuées sur des processeurs conventionnels. Évidemment, déplacer de grandes quantités de code du CPU vers le GPU dans de telles conditions nécessite des efforts importants.

    Le nombre de cœurs d’un GPU moyen est nettement plus élevé que celui d’un processeur conventionnel moyen. Cependant, certaines tâches sont trop petites ou ne peuvent pas être décomposées en parties suffisamment volumineuses pour bénéficier du GPU.

    La prise en charge de la synchronisation entre les cœurs GPU effectuant la même tâche est très médiocre et totalement absente entre les cœurs GPU effectuant des tâches différentes. Cette circonstance nécessite une synchronisation du processeur graphique avec un processeur conventionnel.

La question se pose immédiatement : quelles tâches peuvent être résolues sur un GPU ? Gardez à l’esprit que tous les algorithmes ne sont pas adaptés à une exécution sur un GPU. Par exemple, les GPU n'ont pas accès aux périphériques d'E/S, vous ne pourrez donc pas améliorer les performances d'un programme qui récupère les flux RSS d'Internet à l'aide d'un GPU. Cependant, de nombreux algorithmes de calcul peuvent être transférés vers le GPU et massivement parallélisés. Vous trouverez ci-dessous quelques exemples de tels algorithmes (cette liste n'est en aucun cas exhaustive) :

    netteté croissante et décroissante des images et autres transformations ;

    Transformée de Fourier Rapide;

    transposition et multiplication matricielles;

    tri des numéros ;

    inversion de hachage directe.

Le blog Microsoft Native Concurrency est une excellente source d'exemples supplémentaires, qui fournit des extraits de code et des explications pour divers algorithmes implémentés dans C++ AMP.

C++ AMP est un framework inclus avec Visual Studio 2012 qui offre aux développeurs C++ un moyen simple d'effectuer des calculs sur le GPU, ne nécessitant qu'un pilote DirectX 11. Microsoft a publié C++ AMP en tant que spécification ouverte qui peut être implémentée par n'importe quel fournisseur de compilateur.

Le framework C++ AMP vous permet d'exécuter du code dans accélérateurs graphiques, qui sont des appareils informatiques. À l'aide du pilote DirectX 11, le framework C++ AMP détecte dynamiquement tous les accélérateurs. C++ AMP comprend également un émulateur d'accélérateur logiciel et un émulateur de processeur conventionnel, WARP, qui sert de solution de secours sur les systèmes sans GPU ou avec GPU mais ne dispose pas d'un pilote DirectX 11 et utilise plusieurs cœurs et instructions SIMD.

Commençons maintenant à explorer un algorithme qui peut facilement être parallélisé pour être exécuté sur un GPU. L'implémentation ci-dessous prend deux vecteurs de longueur égale et calcule le résultat ponctuel. Il est difficile d'imaginer quelque chose de plus simple :

Void VectorAddExpPointwise(float* premier, float* second, float* résultat, int longueur) ( for (int i = 0; i< length; ++i) { result[i] = first[i] + exp(second[i]); } }

Pour paralléliser cet algorithme sur un processeur classique, vous devez diviser la plage d'itérations en plusieurs sous-plages et exécuter un thread d'exécution pour chacune d'elles. Nous avons consacré beaucoup de temps dans les articles précédents à exactement cette façon de paralléliser notre premier exemple de recherche de nombres premiers - nous avons vu comment cela peut être fait en créant des threads manuellement, en passant des tâches à un pool de threads et en utilisant Parallel.For et PLINQ pour paralléliser automatiquement. Rappelons également que lors de la parallélisation d'algorithmes similaires sur un processeur conventionnel, nous avons pris un soin particulier à ne pas diviser le problème en tâches trop petites.

Pour le GPU, ces avertissements ne sont pas nécessaires. Les GPU ont plusieurs cœurs qui exécutent les threads très rapidement et le coût du changement de contexte est nettement inférieur à celui des processeurs conventionnels. Vous trouverez ci-dessous un extrait essayant d'utiliser la fonction parallèle_for_eachà partir du framework C++ AMP :

#inclure #inclure en utilisant la concurrence des espaces de noms ; void VectorAddExpPointwise (float* en premier, float* en second, float* résultat, int length) ( array_view avFirst(longueur, premier); tableau_view avSecond(longueur, seconde); tableau_view avResult(longueur, résultat); avResult.discard_data(); parallel_for_each(avResult.extent, [=](index<1>i) restrict(amp) ( avResult[i] = avFirst[i] + fast_math::exp(avSecond[i]); )); avResult.synchronize(); )

Examinons maintenant chaque partie du code séparément. Notons tout de suite que la forme générale de la boucle principale a été conservée, mais la boucle for initialement utilisée a été remplacée par un appel à la fonction parallel_for_each. En fait, le principe de conversion d'une boucle en appel de fonction ou de méthode n'est pas nouveau pour nous - une telle technique a déjà été démontrée à l'aide des méthodes Parallel.For() et Parallel.ForEach() de la bibliothèque TPL.

Ensuite, les données d'entrée (paramètres premier, deuxième et résultat) sont encapsulées avec des instances tableau_view. La classe array_view est utilisée pour envelopper les données transmises au GPU (accélérateur). Son paramètre de modèle spécifie le type de données et sa dimension. Afin d'exécuter des instructions sur un GPU qui accèdent aux données initialement traitées sur un processeur conventionnel, quelqu'un ou quelque chose doit se charger de copier les données sur le GPU, car la plupart des cartes graphiques modernes sont des appareils distincts dotés de leur propre mémoire. Les instances array_view résolvent ce problème : elles fournissent une copie des données à la demande et uniquement lorsque cela est vraiment nécessaire.

Lorsque le GPU termine la tâche, les données sont copiées. En instanciant array_view avec un argument const, nous garantissons que le premier et le second sont copiés dans la mémoire GPU, mais pas recopiés. De même, appeler jeter_data(), nous excluons la copie du résultat de la mémoire d'un processeur ordinaire vers la mémoire de l'accélérateur, mais ces données seront copiées dans le sens opposé.

La fonction parallel_for_each prend un objet d'étendue qui spécifie la forme des données à traiter et une fonction à appliquer à chaque élément de l'objet d'étendue. Dans l'exemple ci-dessus, nous avons utilisé une fonction lambda dont le support figurait dans la norme ISO C++2011 (C++11). Le mot-clé restrict (amp) demande au compilateur de vérifier si le corps de la fonction peut être exécuté sur le GPU et désactive la plupart des syntaxes C++ qui ne peuvent pas être compilées en instructions GPU.

Paramètre de fonction Lambda, index<1>objet, représente un index unidimensionnel. Il doit correspondre à l'objet d'étendue utilisé - si nous devions déclarer l'objet d'étendue comme étant bidimensionnel (par exemple, en définissant la forme des données source comme une matrice bidimensionnelle), l'index devrait également être à deux dimensions. -dimensionnel. Un exemple d’une telle situation est donné ci-dessous.

Enfin, l'appel de méthode synchroniser()à la fin de la méthode VectorAddExpPointwise, il garantit que les résultats du calcul de array_view avResult, produits par le GPU, sont recopiés dans le tableau de résultats.

Ceci conclut notre première introduction au monde de C++ AMP, et nous sommes maintenant prêts pour des recherches plus détaillées, ainsi que des exemples plus intéressants démontrant les avantages de l'utilisation du calcul parallèle sur un GPU. L'addition de vecteurs n'est pas un bon algorithme et n'est pas le meilleur candidat pour démontrer l'utilisation du GPU en raison de la surcharge importante liée à la copie des données. La sous-section suivante montrera deux autres exemples intéressants.

Multiplication matricielle

Le premier « vrai » exemple que nous examinerons est la multiplication matricielle. Pour l'implémentation, nous prendrons un simple algorithme de multiplication matricielle cubique, et non l'algorithme de Strassen, qui a un temps d'exécution proche du cubique ~O(n 2,807). Étant donné deux matrices, une matrice m x w A et une matrice w x n B, le programme suivant les multipliera et renverra le résultat, une matrice m x n C :

Void MatrixMultiply(int* A, int m, int w, int* B, int n, int* C) ( pour (int i = 0; i< m; ++i) { for (int j = 0; j < n; ++j) { int sum = 0; for (int k = 0; k < w; ++k) { sum += A * B; } C = sum; } } }

Il existe plusieurs façons de paralléliser cette implémentation, et si vous souhaitez paralléliser ce code pour qu'il s'exécute sur un processeur classique, le bon choix serait de paralléliser la boucle externe. Cependant, le GPU a un nombre de cœurs assez important, et en parallélisant uniquement la boucle externe, on ne pourra pas créer un nombre suffisant de jobs pour charger tous les cœurs de travail. Par conséquent, il est logique de paralléliser les deux boucles externes, en laissant la boucle interne intacte :

Void MatrixMultiply (int* A, int m, int w, int* B, int n, int* C) ( array_view avA(m, w, A); tableau_view avB(w, n, B); tableau_view avC(m,n,C); avC.discard_data(); parallel_for_each(avC.extent, [=](index<2>idx) restrict(amp) ( int somme = 0; pour (int k = 0; k< w; ++k) { sum + = avA(idx*w, k) * avB(k*w, idx); } avC = sum; }); }

Cette implémentation ressemble encore beaucoup à l'implémentation séquentielle de la multiplication matricielle et à l'exemple d'addition vectorielle donné ci-dessus, à l'exception de l'index, qui est désormais bidimensionnel et accessible dans la boucle interne à l'aide de l'opérateur. Dans quelle mesure cette version est-elle plus rapide que l’alternative séquentielle fonctionnant sur un processeur classique ? En multipliant deux matrices (entiers) de taille 1024 x 1024, la version séquentielle sur un CPU classique prend en moyenne 7350 millisecondes, tandis que la version GPU - tenez bon - prend 50 millisecondes, soit 147 fois plus rapide !

Simulation du mouvement des particules

Les exemples de résolution de problèmes sur le GPU présentés ci-dessus ont une implémentation très simple de la boucle interne. Il est clair que ce ne sera pas toujours le cas. Le blog Native Concurrency, lié ci-dessus, montre un exemple de modélisation des interactions gravitationnelles entre les particules. La simulation implique un nombre infini d'étapes ; à chaque étape, de nouvelles valeurs des éléments du vecteur accélération sont calculées pour chaque particule puis leurs nouvelles coordonnées sont déterminées. Ici, le vecteur de particules est parallélisé - avec un nombre suffisamment grand de particules (de plusieurs milliers et plus), vous pouvez créer un nombre suffisamment grand de tâches pour charger tous les cœurs GPU de travail.

La base de l'algorithme est la mise en œuvre de la détermination du résultat des interactions entre deux particules, comme indiqué ci-dessous, qui peut facilement être transféré au GPU :

// ici float4 sont des vecteurs à quatre éléments // représentant les particules impliquées dans les opérations void bodybody_interaction (float4& accélération, const float4 p1, const float4 p2) restrict(amp) ( float4 dist = p2 – p1; // maintenant utilisé ici float absDist = dist.x*dist.x + dist.y*dist.y + dist.z*dist.z; float invDist = 1.0f / sqrt(absDist); float invDistCube = invDist*invDist*invDist; accélération + = dist*PARTICLE_MASS*invDistCube; )

Les données initiales à chaque étape de modélisation sont un tableau avec les coordonnées et les vitesses des particules, et à la suite des calculs, un nouveau tableau avec les coordonnées et les vitesses des particules est créé :

Particule Struct ( position float4, vitesse ; // implémentations du constructeur, constructeur de copie et // opérateur = avec restrict(amp) omis pour économiser de l'espace ); void simulation_step (tableau & précédent, tableau & ensuite, les corps int) (étendue<1>ext(corps); parallel_for_each (ext, [&](index<1>idx) restrict(amp) ( particule p = précédent; float4 accélération(0, 0, 0, 0); for (int body = 0; body< bodies; ++body) { bodybody_interaction (acceleration, p.position, previous.position); } p.velocity + = acceleration*DELTA_TIME; p.position + = p.velocity*DELTA_TIME; next = p; }); }

A l’aide d’une interface graphique appropriée, la modélisation peut s’avérer très intéressante. L'exemple complet fourni par l'équipe C++ AMP est disponible sur le blog Native Concurrency. Sur mon système équipé d'un processeur Intel Core i7 et d'une carte graphique Geforce GT 740M, la simulation de 10 000 particules s'exécute à environ 2,5 ips (pas par seconde) en utilisant la version séquentielle exécutée sur le processeur standard, et à 160 ips en utilisant la version optimisée en cours d'exécution. sur le GPU - une énorme augmentation des performances.

Avant de conclure cette section, il existe une autre fonctionnalité importante du framework C++ AMP qui peut encore améliorer les performances du code exécuté sur le GPU. Prise en charge des GPU cache de données programmable(souvent appelé la memoire partagée). Les valeurs stockées dans ce cache sont partagées par tous les threads d'exécution dans une seule tuile. Grâce au carrelage mémoire, les programmes basés sur le framework C++ AMP peuvent lire les données de la mémoire de la carte graphique dans la mémoire partagée de la mosaïque, puis y accéder à partir de plusieurs threads d'exécution sans récupérer les données de la mémoire de la carte graphique. L’accès à la mémoire partagée mosaïque est environ 10 fois plus rapide que la mémoire de la carte graphique. En d’autres termes, vous avez des raisons de continuer à lire.

Pour fournir une version en mosaïque de la boucle parallèle, la méthode parallel_for_each est passée domaine tiled_extent, qui divise l'objet d'étendue multidimensionnelle en tuiles multidimensionnelles, et le paramètre lambda tiled_index, qui spécifie l'ID global et local du thread dans la tuile. Par exemple, une matrice 16x16 peut être divisée en tuiles 2x2 (comme indiqué dans l'image ci-dessous) puis transmise à la fonction parallel_for_each :

Étendue<2>matrice (16,16); Tiled_extent<2,2>TiledMatrix = matrice.tile<2,2>(); parallel_for_each(tiledMatrix, [=](tiled_index<2,2>idx) restrict(amp) ( // ... ));

Chacun des quatre threads d'exécution appartenant à une même mosaïque peut partager les données stockées dans le bloc.

Lors de l'exécution d'opérations avec des matrices, dans le cœur du GPU, au lieu de l'index standard<2>, comme dans les exemples ci-dessus, vous pouvez utiliser idx.global. Une utilisation appropriée de la mémoire locale en mosaïque et des index locaux peut fournir des gains de performances significatifs. Pour déclarer une mémoire en mosaïque partagée par tous les threads d'exécution dans une seule tuile, les variables locales peuvent être déclarées avec le spécificateur Tile_static.

En pratique, la technique consistant à déclarer la mémoire partagée et à initialiser ses blocs individuels dans différents threads d'exécution est souvent utilisée :

Parallel_for_each(tiledMatrix, [=](tiled_index<2,2>idx) restrict(amp) ( // 32 octets sont partagés par tous les threads du bloc tile_static int local; // attribue une valeur à l'élément pour ce thread d'exécution local = 42; ));

Évidemment, les avantages de l'utilisation de la mémoire partagée ne peuvent être obtenus que si l'accès à cette mémoire est synchronisé ; c'est-à-dire que les threads ne doivent pas accéder à la mémoire tant qu'elle n'a pas été initialisée par l'un d'entre eux. La synchronisation des threads dans une mosaïque est effectuée à l'aide d'objets tuile_barrier(qui rappelle la classe Barrier de la bibliothèque TPL) - ils ne pourront poursuivre l'exécution qu'après avoir appelé la méthode Tile_barrier.Wait(), qui ne rendra le contrôle que lorsque tous les threads auront appelé Tile_barrier.Wait. Par exemple:

Parallel_for_each(tiledMatrix, (tiled_index<2,2>idx) restrict(amp) ( // 32 octets sont partagés par tous les threads du bloc tile_static int local; // attribue une valeur à l'élément pour ce thread d'exécution local = 42; // idx.barrier est une instance de Tile_barrier idx.barrier.wait(); // Ce thread peut désormais accéder au tableau "local" // en utilisant les index des autres threads d'exécution ! ));

Il est maintenant temps de traduire ce que vous avez appris en un exemple concret. Revenons à l'implémentation de la multiplication matricielle, réalisée sans recourir à l'organisation de la mémoire en mosaïque, et y ajoutons l'optimisation décrite. Supposons que la taille de la matrice soit un multiple de 256 - cela nous permettra de travailler avec des blocs de 16 x 16. La nature des matrices permet une multiplication bloc par bloc, et nous pouvons profiter de cette fonctionnalité (en fait, diviser matrices en blocs est une optimisation typique de l'algorithme de multiplication matricielle, permettant une utilisation plus efficace du cache CPU).

L'essence de cette technique se résume à ce qui suit. Pour trouver C i,j (l'élément de la ligne i et de la colonne j dans la matrice de résultat), vous devez calculer le produit scalaire entre A i,* (i-ième ligne de la première matrice) et B *,j (j -ième colonne de la deuxième matrice). Cependant, cela équivaut à calculer les produits scalaires partiels de la ligne et de la colonne, puis à additionner les résultats. Nous pouvons utiliser ce fait pour convertir l'algorithme de multiplication matricielle en une version en carrelage :

Void MatrixMultiply(int* A, int m, int w, int* B, int n, int* C) ( array_view avA(m, w, A); tableau_view avB(w, n, B); tableau_view avC(m,n,C); avC.discard_data(); parallel_for_each(avC.extent.tile<16,16>(), [=](tiled_index<16,16>idx) restrict(amp) ( int sum = 0; int localRow = idx.local, localCol = idx.local; for (int k = 0; k

L'essence de l'optimisation décrite est que chaque thread de la mosaïque (256 threads sont créés pour un bloc 16 x 16) initialise son élément en 16 x 16 copies locales de fragments des matrices originales A et B. Chaque thread de la mosaïque nécessite une seule ligne et une colonne de ces blocs, mais tous les threads ensemble accéderont à chaque ligne et chaque colonne 16 fois. Cette approche réduit considérablement le nombre d'accès à la mémoire principale.

Pour calculer l'élément (i, j) dans la matrice de résultat, l'algorithme nécessite la i-ème ligne complète de la première matrice et la j-ème colonne de la deuxième matrice. Lorsque les threads sont en mosaïque 16x16 représentés dans le diagramme et que k = 0, les régions ombrées des première et deuxième matrices seront lues dans la mémoire partagée. L'élément informatique du fil d'exécution (i, j) dans la matrice de résultat calculera le produit scalaire partiel des k premiers éléments de la i-ème ligne et de la j-ème colonne des matrices d'origine.

Dans cet exemple, l’utilisation d’une organisation en mosaïque offre une énorme amélioration des performances. La version en mosaïque de multiplication matricielle est beaucoup plus rapide que la version simple, prenant environ 17 millisecondes (pour les mêmes matrices d'entrée 1024 x 1024), soit 430 fois plus rapide que la version fonctionnant sur un processeur conventionnel !

Avant de terminer notre discussion sur le framework C++ AMP, nous aimerions mentionner les outils (dans Visual Studio) disponibles pour les développeurs. Visual Studio 2012 propose un débogueur d'unité de traitement graphique (GPU) qui vous permet de définir des points d'arrêt, d'examiner la pile d'appels et de lire et modifier les valeurs des variables locales (certains accélérateurs prennent directement en charge le débogage GPU ; pour d'autres, Visual Studio utilise un simulateur logiciel) et un profileur qui vous permet d'évaluer les avantages qu'une application tire de la parallélisation des opérations à l'aide d'un GPU. Pour plus d’informations sur les fonctionnalités de débogage dans Visual Studio, consultez l’article Procédure pas à pas. Débogage d'une application C++ AMP" sur MSDN.

Alternatives de calcul GPU dans .NET

Jusqu'à présent, cet article n'a montré que des exemples en C++. Cependant, il existe plusieurs façons d'exploiter la puissance du GPU dans les applications gérées. Une solution consiste à utiliser des outils d'interopérabilité qui vous permettent de décharger le travail avec les cœurs GPU vers des composants C++ de bas niveau. Cette solution est idéale pour ceux qui souhaitent utiliser le framework C++ AMP ou qui ont la possibilité d'utiliser des composants C++ AMP prédéfinis dans des applications gérées.

Une autre façon consiste à utiliser une bibliothèque qui fonctionne directement avec le GPU à partir du code managé. Il existe actuellement plusieurs bibliothèques de ce type. Par exemple, GPU.NET et CUDAfy.NET (deux offres commerciales). Vous trouverez ci-dessous un exemple du référentiel GPU.NET GitHub démontrant l'implémentation du produit scalaire de deux vecteurs :

Vide statique public MultiplyAddGpu(double a, double b, double c) ( int ThreadId = BlockDimension.X * BlockIndex.X + ThreadIndex.X; int TotalThreads = BlockDimension.X * GridDimension.X; for (int ElementIdx = ThreadId; ElementIdx

Je suis d'avis qu'il est beaucoup plus simple et efficace d'apprendre une extension de langage (basée sur C++ AMP) que d'essayer d'orchestrer des interactions au niveau de la bibliothèque ou d'apporter des modifications significatives au langage IL.

Ainsi, après avoir examiné les possibilités de programmation parallèle dans .NET et d'utilisation du GPU, personne ne doute que l'organisation du calcul parallèle est un moyen important d'augmenter la productivité. Sur de nombreux serveurs et postes de travail à travers le monde, la puissance de traitement inestimable des processeurs et des GPU reste inutilisée car les applications ne l'utilisent tout simplement pas.

La bibliothèque parallèle de tâches nous offre une opportunité unique d'inclure tous les cœurs de processeur disponibles, même si cela nécessitera de résoudre certains problèmes intéressants de synchronisation, de fragmentation excessive des tâches et de répartition inégale du travail entre les threads d'exécution.

Le framework C++ AMP et d'autres bibliothèques de calcul parallèle GPU polyvalentes peuvent être utilisés avec succès pour paralléliser les calculs sur des centaines de cœurs GPU. Enfin, il existe une opportunité jusqu'alors inexplorée de gagner en productivité grâce à l'utilisation des technologies informatiques distribuées dans le cloud, qui sont récemment devenues l'une des principales orientations du développement des technologies de l'information.

L'une des fonctionnalités les plus cachées de la récente mise à jour de Windows 10 est la possibilité de vérifier quelles applications utilisent votre unité de traitement graphique (GPU). Si vous avez déjà ouvert le Gestionnaire des tâches, vous avez probablement examiné l'utilisation de votre processeur pour voir quelles applications utilisent le plus de processeur. Les dernières mises à jour ont ajouté une fonctionnalité similaire, mais pour les processeurs graphiques GPU. Cela vous aide à comprendre à quel point vos logiciels et vos jeux sont intensifs sur votre GPU sans avoir à télécharger de logiciels tiers. Il existe une autre fonctionnalité intéressante qui permet de décharger votre CPU vers le GPU. Je recommande de lire comment choisir.

Pourquoi n’ai-je pas de GPU dans le gestionnaire de tâches ?

Malheureusement, toutes les cartes vidéo ne seront pas en mesure de fournir au système Windows les statistiques nécessaires à la lecture du GPU. Pour en être sûr, vous pouvez utiliser rapidement l'outil de diagnostic DirectX pour vérifier cette technologie.

  1. Cliquez sur " Commencer" et écrivez dans la recherche dxdiag pour exécuter l'outil de diagnostic DirectX.
  2. Allez dans l'onglet "" Écran",à droite dans la colonne " Conducteurs"vous devez avoir Modèle WDDM version supérieure à 2.0 pour l'utilisation des graphiques GPU dans le gestionnaire de tâches.

Activer le graphique GPU dans le gestionnaire de tâches

Pour voir l'utilisation du GPU pour chaque application, vous devez ouvrir le gestionnaire de tâches.

  • Appuyez sur une combinaison de boutons Ctrl + Maj + Échap pour ouvrir le gestionnaire de tâches.
  • Faites un clic droit dans le gestionnaire de tâches sur la case "vierge" Nom" et vérifiez dans le menu déroulant GPU Vous pouvez également noter Noyau GPU pour voir quels programmes l'utilisent.
  • Désormais dans le gestionnaire de tâches, le graphique GPU et le cœur GPU sont visibles à droite.


Afficher les performances globales du GPU

Vous pouvez surveiller l'utilisation globale du GPU pour la surveiller sous de lourdes charges et l'analyser. Dans ce cas, vous pouvez voir tout ce dont vous avez besoin dans l'onglet " Performance" en sélectionnant processeur graphique.


Chaque élément du GPU est décomposé en graphiques individuels pour vous donner encore plus d'informations sur la façon dont votre GPU est utilisé. Si vous souhaitez modifier les graphiques affichés, vous pouvez cliquer sur la petite flèche à côté du nom de chaque tâche. Cet écran affiche également la version et la date de votre pilote, ce qui constitue une bonne alternative à l'utilisation de DXDiag ou du Gestionnaire de périphériques.


En parlant de calcul parallèle sur GPU, nous devons nous rappeler à quelle époque nous vivons, aujourd'hui est une époque où tout dans le monde est tellement accéléré que vous et moi perdons la notion du temps, sans remarquer à quel point il passe. Tout ce que nous faisons est associé à une grande précision et rapidité de traitement de l'information, dans de telles conditions, nous avons certainement besoin d'outils pour traiter toutes les informations dont nous disposons et les convertir en données. De plus, lorsque nous parlons de telles tâches, nous devons nous rappeler que ces tâches sont nécessaires non seulement pour les grandes organisations ou les méga-entreprises, mais aussi pour les utilisateurs ordinaires qui résolvent leurs problèmes de vie liés à la haute technologie à la maison sur des ordinateurs personnels ! L'émergence de NVIDIA CUDA n'a pas été surprenante, mais plutôt justifiée, car il faudra bientôt traiter des tâches beaucoup plus chronophages sur PC qu'auparavant. Un travail qui prenait auparavant beaucoup de temps ne prendra désormais que quelques minutes et, par conséquent, cela affectera l'image globale du monde entier !

Qu’est-ce que le calcul GPU ?

Le calcul GPU consiste à utiliser le GPU pour calculer des tâches techniques, scientifiques et quotidiennes. Le calcul GPU implique l'utilisation du CPU et du GPU avec un échantillonnage hétérogène entre eux, à savoir : la partie séquentielle des programmes est prise en charge par le CPU, tandis que les tâches de calcul chronophages sont laissées au GPU. Grâce à cela, la parallélisation des tâches se produit, ce qui conduit à un traitement plus rapide de l'information et réduit le temps d'exécution du travail ; le système devient plus productif et peut traiter simultanément un plus grand nombre de tâches qu'auparavant. Cependant, pour obtenir un tel succès, le support matériel seul ne suffit pas ; dans ce cas, le support logiciel est également nécessaire pour que l'application puisse transférer les calculs les plus chronophages vers le GPU.

Qu'est-ce que CUDA

CUDA est une technologie de programmation d'algorithmes en langage C simplifié qui sont exécutés sur les processeurs graphiques des accélérateurs GeForce de huitième génération et plus, ainsi que sur les cartes Quadro et Tesla correspondantes de NVIDIA. CUDA vous permet d'inclure des fonctions spéciales dans le texte d'un programme C. Ces fonctions sont écrites dans le langage de programmation C simplifié et exécutées sur le GPU. La version initiale du SDK CUDA a été introduite le 15 février 2007. Pour traduire avec succès le code dans ce langage, le SDK CUDA inclut le propre compilateur C de ligne de commande nvcc de NVIDIA. Le compilateur nvcc est basé sur le compilateur ouvert Open64 et est conçu pour traduire le code hôte (code principal, code de contrôle) et le code de périphérique (code matériel) (fichiers avec l'extension .cu) en fichiers objets adaptés à l'assemblage du programme final ou de la bibliothèque dans n'importe quel environnement de programmation, tel que Microsoft Visual Studio.

Capacités technologiques

  1. Un langage C standard pour le développement d'applications parallèles sur GPU.
  2. Bibliothèques d'analyse numérique prêtes à l'emploi pour une transformation de Fourier rapide et un progiciel de base d'algèbre linéaire.
  3. Pilote CUDA spécial pour l'informatique avec transfert de données rapide entre GPU et CPU.
  4. Possibilité d'interfacer le pilote CUDA avec les pilotes graphiques OpenGL et DirectX.
  5. Prise en charge des systèmes d'exploitation Linux 32/64 bits, Windows XP 32/64 bits et MacOS.

Avantages de la technologie

  1. L'interface de programmation d'applications CUDA (API CUDA) est basée sur le langage de programmation standard C avec certaines limitations. Cela simplifie et fluidifie le processus d'apprentissage de l'architecture CUDA.
  2. La mémoire partagée de 16 Ko entre les threads peut être utilisée pour un cache organisé par l'utilisateur avec une bande passante plus large que lors de la récupération à partir de textures classiques.
  3. Transactions plus efficaces entre la mémoire CPU et la mémoire vidéo.
  4. Prise en charge matérielle complète des opérations entières et au niveau du bit.

Exemple d'application technologique

cRark

La partie la plus longue de ce programme est la teinture. Le programme dispose d'une interface console, mais grâce aux instructions fournies avec le programme lui-même, vous pouvez l'utiliser. Vous trouverez ci-dessous de brèves instructions pour configurer le programme. Nous testerons la fonctionnalité du programme et le comparerons avec un autre programme similaire qui n'utilise pas NVIDIA CUDA, en l'occurrence le programme bien connu « Advanced Archive Password Recovery ».

À partir de l'archive cRark téléchargée, nous n'avons besoin que de trois fichiers : crark.exe, crark-hp.exe et password.def. Crerk.exe est un utilitaire de console permettant d'ouvrir les mots de passe RAR 3.0 sans fichiers cryptés à l'intérieur de l'archive (c'est-à-dire que lors de l'ouverture de l'archive, nous voyons les noms, mais nous ne pouvons pas décompresser l'archive sans mot de passe).

Crerk-hp.exe est un utilitaire de console permettant d'ouvrir les mots de passe RAR 3.0 avec cryptage de l'intégralité de l'archive (c'est-à-dire que lors de l'ouverture de l'archive, nous ne voyons ni le nom ni les archives elles-mêmes et ne pouvons pas décompresser l'archive sans mot de passe).

Password.def est n'importe quel fichier texte renommé avec très peu de contenu (par exemple : 1ère ligne : ## 2ème ligne : ?* , dans ce cas le mot de passe sera déchiffré en utilisant tous les caractères). Password.def est le directeur du programme cRark. Le fichier contient les règles pour déchiffrer le mot de passe (ou la zone de caractères que crark.exe utilisera dans son travail). Plus de détails sur les possibilités de choix de ces caractères sont écrits dans le fichier texte obtenu lors de l'ouverture de celui téléchargé sur le site de l'auteur du programme cRark : russian.def.

Préparation

Je dirai tout de suite que le programme ne fonctionne que si votre carte vidéo est basée sur un GPU prenant en charge le niveau d'accélération CUDA 1.1. Ainsi, une série de cartes vidéo basées sur la puce G80, comme la GeForce 8800 GTX, ne sont plus nécessaires, car elles prennent en charge matériellement l'accélération CUDA 1.0. Le programme sélectionne uniquement les mots de passe pour les archives RAR des versions 3.0+ à l'aide de CUDA. Il est nécessaire d'installer tous les logiciels liés à CUDA, à savoir :

Nous créons n'importe quel dossier n'importe où (par exemple sur le lecteur C:) et l'appelons n'importe quel nom, par exemple « 3.2 ». Nous y plaçons les fichiers : crark.exe, crark-hp.exe et password.def et une archive RAR protégée/chiffrée par mot de passe.

Ensuite, vous devez lancer la console de ligne de commande Windows et accéder au dossier créé. Sous Windows Vista et 7, vous devez appeler le menu « Démarrer » et saisir « cmd.exe » dans le champ de recherche ; sous Windows XP, à partir du menu « Démarrer », vous devez d'abord appeler la boîte de dialogue « Exécuter » et saisir « cmd .exe » dedans. Après avoir ouvert la console, entrez une commande comme : cd C:\folder\, cd C:\3.2 dans ce cas.

Nous tapons deux lignes dans un éditeur de texte (vous pouvez également enregistrer le texte sous forme de fichier .bat dans le dossier avec cRark) pour deviner le mot de passe d'une archive RAR protégée par mot de passe avec des fichiers non cryptés :

Écho off;
cmd /K crark (nom de l'archive).rar

pour deviner le mot de passe d'une archive RAR protégée par mot de passe et cryptée :

Écho off;
cmd /K crark-hp (nom de l'archive).rar

Copiez 2 lignes du fichier texte sur la console et appuyez sur Entrée (ou exécutez le fichier .bat).

résultats

Le processus de décryptage est illustré dans la figure :

La vitesse de deviner sur cRark à l'aide de CUDA était de 1625 mots de passe/seconde. En une minute trente-six secondes, un mot de passe à 3 caractères a été sélectionné : « q)$ ». A titre de comparaison : la vitesse de recherche dans Advanced Archive Password Recovery sur mon processeur dual-core Athlon 3000+ est d'un maximum de 50 mots de passe/seconde et la recherche aurait dû durer 5 heures. Autrement dit, la sélection par force brute d'une archive RAR dans cRark à l'aide d'une carte vidéo GeForce 9800 GTX+ est 30 fois plus rapide que sur un processeur.

Pour ceux qui disposent d'un processeur Intel, d'une bonne carte mère avec une fréquence de bus système élevée (FSB 1600 MHz), le débit CPU et la vitesse de recherche seront plus élevés. Et si vous disposez d'un processeur quad-core et d'une paire de cartes vidéo du niveau GeForce 280 GTX, la vitesse de forçage brut des mots de passe s'accélérera considérablement. Pour résumer l’exemple, il faut dire que ce problème a été résolu grâce à la technologie CUDA en seulement 2 minutes au lieu de 5 heures, ce qui indique le fort potentiel de cette technologie !

conclusions

Après avoir examiné aujourd'hui la technologie de calcul parallèle CUDA, nous avons clairement vu la puissance et l'énorme potentiel de développement de cette technologie en utilisant l'exemple d'un programme de récupération de mot de passe pour les archives RAR. Il faut dire sur les perspectives de cette technologie, cette technologie trouvera certainement sa place dans la vie de chaque personne qui décidera de l'utiliser, qu'il s'agisse de tâches scientifiques, ou de tâches liées au traitement vidéo, ou encore de tâches économiques qui nécessitent de la rapidité, des calculs précis, tout cela conduira inévitablement à une augmentation de la productivité du travail qui ne peut être ignorée. Aujourd’hui, l’expression « superordinateur domestique » commence déjà à entrer dans le lexique ; Il est absolument évident que pour concrétiser un tel projet, chaque foyer dispose déjà d’un outil appelé CUDA. Depuis la sortie des cartes basées sur la puce G80 (2006), un grand nombre d'accélérateurs basés sur NVIDIA ont été lancés et prennent en charge la technologie CUDA, qui peut réaliser les rêves de superordinateurs dans chaque foyer. En promouvant la technologie CUDA, NVIDIA renforce son autorité aux yeux des clients en fournissant des capacités supplémentaires à leurs équipements, que beaucoup ont déjà achetés. On ne peut que croire que CUDA va bientôt se développer très rapidement et permettre aux utilisateurs de profiter pleinement de toutes les capacités du calcul parallèle sur GPU.

Il ne peut jamais y avoir trop de noyaux...

Les GPU modernes sont des bêtes monstrueuses et rapides, capables de mâcher des gigaoctets de données. Cependant, l'homme est rusé et, quelle que soit la puissance de calcul, il se pose des problèmes de plus en plus complexes, alors vient le moment où nous devons malheureusement admettre qu'une optimisation est nécessaire 🙁

Cet article décrit les concepts de base afin de faciliter la navigation dans la théorie de l'optimisation des GPU et les règles de base afin que ces concepts soient moins souvent abordés.

Raisons pour lesquelles les GPU sont efficaces pour travailler avec de grandes quantités de données nécessitant un traitement :

  • ils ont de grandes capacités pour l'exécution parallèle de tâches (de très nombreux processeurs)
  • bande passante mémoire élevée

Bande passante mémoire- c'est la quantité d'informations - un bit ou un gigaoctet - qui peut être transférée par unité de temps - une seconde ou un cycle de processeur.

L'une des tâches d'optimisation consiste à utiliser un débit maximal - pour augmenter les performances débit(idéalement, cela devrait être égal à la bande passante mémoire).

Pour améliorer l'utilisation de la bande passante :

  • augmenter la quantité d'informations - utiliser la bande passante au maximum (par exemple, chaque thread fonctionne avec float4)
  • réduire la latence – délai entre les opérations

Latence– le laps de temps entre le moment où le responsable du traitement a demandé une cellule mémoire spécifique et le moment où les données sont devenues disponibles pour le processeur pour exécuter des instructions. Nous ne pouvons en aucun cas influencer le délai lui-même - ces limitations sont présentes au niveau matériel. C'est grâce à ce délai que le processeur peut gérer simultanément plusieurs threads - alors que le thread A a demandé de lui allouer de la mémoire, le thread B peut calculer quelque chose et le thread C peut attendre que les données demandées lui parviennent.

Comment réduire la latence si la synchronisation est utilisée :

  • réduire le nombre de threads dans un bloc
  • augmenter le nombre de groupes de blocs

Utilisation complète des ressources GPU – Occupation GPU

Dans les conversations intellectuelles sur l'optimisation, le terme apparaît souvent : occupation du GPU ou occupation du noyau– cela reflète l’efficacité de l’utilisation des ressources de la carte vidéo. Je voudrais noter séparément que même si vous utilisez toutes les ressources, cela ne signifie pas que vous les utilisez correctement.

La puissance de calcul du GPU est constituée de centaines de processeurs gourmands en calcul ; lors de la création d'un programme - le noyau - le fardeau de la répartition de la charge incombe au programmeur. Une erreur peut entraîner l’inutilisation d’une grande partie de ces précieuses ressources. Maintenant, je vais vous expliquer pourquoi. Il va falloir partir de loin.

Permettez-moi de vous rappeler que la chaîne ( chaîne dans la terminologie NVidia, front d'onde – dans la terminologie AMD) est un ensemble de threads qui exécutent simultanément la même fonction du noyau sur le processeur. Les threads réunis par le programmeur en blocs sont divisés en chaînes par un planificateur de threads (séparément pour chaque multiprocesseur) - pendant qu'une chaîne fonctionne, la seconde attend le traitement des demandes de mémoire, etc. Si certains threads de distorsion effectuent encore des calculs, tandis que d'autres ont déjà fait tout ce qu'ils pouvaient, il y a une utilisation inefficace de la ressource informatique - communément appelée capacité inutilisée.

Chaque point de synchronisation, chaque branche logique peut générer une telle situation d'inactivité. La divergence maximale (branchement de la logique d'exécution) dépend de la taille de la chaîne. Pour les GPU NVidia, c'est 32, pour AMD, c'est 64.

Pour réduire les temps d'arrêt du multiprocesseur pendant l'exécution de Warp :

  • minimiser le temps d’attente aux barrières
  • minimiser la divergence de la logique d'exécution dans la fonction du noyau

Pour résoudre efficacement ce problème, il est logique de comprendre comment se forment les déformations (dans le cas de plusieurs dimensions). En fait, l’ordre est simple : d’abord en X, puis en Y et enfin en Z.

le noyau est lancé avec des blocs de taille 64x16, les threads sont divisés en chaînes dans l'ordre X, Y, Z - c'est-à-dire les 64 premiers éléments sont divisés en deux chaînes, puis la seconde, etc.

Le noyau fonctionne avec des blocs 16x64. Les premier et deuxième 16 éléments sont ajoutés à la première chaîne, les troisième et quatrième à la deuxième chaîne, etc.

Comment réduire la divergence (rappelez-vous que le branchement n'est pas toujours la cause d'une perte de performances critique)

  • lorsque les flux adjacents ont des chemins d'exécution différents - ils comportent de nombreuses conditions et transitions - recherchez des moyens de restructurer
  • recherchez une charge déséquilibrée de threads et supprimez-la de manière décisive (c'est à ce moment-là que non seulement nous avons des conditions, mais à cause de ces conditions, le premier thread calcule toujours quelque chose, et le cinquième ne remplit pas cette condition et est inactif)

Comment tirer le meilleur parti de vos ressources GPU

Les ressources GPU ont malheureusement aussi leurs limites. Et à proprement parler, avant de lancer la fonction noyau, il est logique de déterminer les limites et de prendre en compte ces limites lors de la répartition de la charge. Pourquoi c'est important?

Les cartes vidéo ont des restrictions sur le nombre total de threads qu'un multiprocesseur peut exécuter, le nombre maximum de threads dans un bloc, le nombre maximum de warps sur un processeur, des restrictions sur différents types de mémoire, etc. Toutes ces informations peuvent être demandées soit par programme, via l'API appropriée, soit à l'aide des utilitaires du SDK. (modules deviceQuery pour les appareils NVidia, CLInfo - pour les cartes vidéo AMD).

Pratique générale:

  • le nombre de blocs de threads/groupes de travail doit être un multiple du nombre de processeurs de flux
  • la taille du bloc/groupe de travail doit être un multiple de la taille de la chaîne

Il convient de garder à l'esprit que le minimum absolu est de 3 à 4 warps/wayfronts tournant simultanément sur chaque processeur ; les guides avisés conseillent de partir de la considération d'au moins sept wayfronts. En même temps, n’oubliez pas les restrictions matérielles !

Garder tous ces détails en tête devient vite ennuyeux, alors pour calculer l'occupation du GPU, NVidia a proposé un outil inattendu : une calculatrice Excel (!) pleine de macros. Là, vous pouvez entrer des informations sur le nombre maximum de threads pour SM, le nombre de registres et la taille de la mémoire totale (partagée) disponible sur le processeur de flux, ainsi que les paramètres de lancement de fonction utilisés - et il affiche l'efficacité de l'utilisation des ressources comme un pourcentage (et vous vous arrachez les cheveux en réalisant que pour utiliser tous les noyaux, il vous manque des registres).

Informations d'utilisation :
http://docs.nvidia.com/cuda/cuda-c-best-practices-guide/#calculating-occupancy

Opérations GPU et mémoire

Les cartes vidéo sont optimisées pour les opérations de mémoire 128 bits. Ceux. idéalement, chaque manipulation de mémoire devrait idéalement modifier 4 valeurs de quatre octets à la fois. Le principal problème pour un programmeur est que les compilateurs GPU modernes ne savent pas comment optimiser de telles choses. Cela doit être fait directement dans le code de fonction et entraîne en moyenne une augmentation de performance d'une fraction de pour cent. La fréquence des demandes de mémoire a un impact beaucoup plus important sur les performances.

Le problème est le suivant : chaque requête renvoie une donnée dont la taille est un multiple de 128 bits. Et chaque thread n’en utilise qu’un quart (dans le cas d’une variable ordinaire de quatre octets). Lorsque des threads adjacents travaillent simultanément avec des données situées séquentiellement dans des cellules mémoire, cela réduit le nombre total d'accès à la mémoire. Ce phénomène est appelé opérations combinées de lecture et d’écriture ( accès fusionné – bien ! lire et écrire) – et avec la bonne organisation du code ( accès rapide à une partie contiguë de la mémoire – mauvais !) peut améliorer considérablement les performances. Lorsque vous organisez votre accès principal - rappelez-vous - contigu - au sein des éléments d'une ligne de mémoire, travailler avec des éléments de colonne n'est plus aussi efficace. Vous voulez plus de détails ? J'ai aimé ce pdf - ou google pour " techniques de fusion de mémoire “.

La position de leader dans la catégorie « goulot d’étranglement » est occupée par une autre opération de mémoire – copier des données de la mémoire hôte vers le GPU . La copie ne s'effectue pas n'importe comment, mais à partir d'une zone mémoire spécialement allouée par le pilote et le système : lorsqu'il y a une demande de copie de données, le système y copie d'abord ces données, puis les télécharge ensuite sur le GPU. La vitesse de transport des données est limitée par la bande passante du bus PCI Express xN (où N est le nombre de lignes de données) à travers lequel les cartes vidéo modernes communiquent avec l'hôte.

Cependant, la copie inutile d'une mémoire lente sur l'hôte représente parfois un coût injustifié. La solution est d'utiliser ce qu'on appelle mémoire épinglée – une zone mémoire spécialement marquée, afin que le système d'exploitation ne puisse effectuer aucune opération avec elle (par exemple, la vider dans swap/move à sa discrétion, etc.). Le transfert de données de l'hôte vers la carte vidéo s'effectue sans la participation du système d'exploitation - de manière asynchrone, via DMLA (Accès direct à la mémoire).

Et enfin, un peu plus sur la mémoire. La mémoire partagée sur un multiprocesseur est généralement organisée sous forme de bancs de mémoire contenant des mots de 32 bits - données. Le nombre de banques, selon la bonne tradition, varie d'une génération de GPU à l'autre - 16/32. Si chaque thread accède à une banque de données distincte, tout va bien. Sinon, nous recevons plusieurs requêtes de lecture/écriture sur une banque et nous obtenons un conflit ( conflit de banque de mémoire partagée). De tels appels conflictuels sont sérialisés et donc exécutés séquentiellement plutôt qu'en parallèle. Si tous les threads accèdent à une banque, une réponse « diffusion » est utilisée ( diffuser) et il n'y a pas de conflit. Il existe plusieurs façons de gérer efficacement les conflits d'accès, j'ai bien aimé description des principales techniques pour se débarrasser des conflits d'accès aux banques mémoire – .

Comment rendre les opérations mathématiques encore plus rapides ? N'oubliez pas que :

  • Les calculs en double précision sont une opération à charge élevée avec fp64 >> fp32
  • les constantes de la forme 3.13 dans le code, par défaut, sont interprétées comme fp64 si 3.14f n'est pas explicitement spécifié
  • Pour optimiser les mathématiques, ce serait une bonne idée de consulter les guides pour voir si le compilateur a des indicateurs
  • Les fabricants incluent dans leurs SDK des fonctionnalités qui exploitent les fonctionnalités des appareils pour atteindre des performances (souvent au détriment de la portabilité)

Il est logique que les développeurs CUDA accordent une attention particulière au concept. flux cuda vous permettant d'exécuter plusieurs fonctions du noyau sur un périphérique à la fois ou de combiner la copie asynchrone des données de l'hôte vers le périphérique lors de l'exécution de fonctions. OpenCL ne fournit pas encore une telle fonctionnalité :)

Déchets pour profilage :

NVifia Visual Profiler est un utilitaire intéressant qui analyse à la fois les noyaux CUDA et OpenCL.

P.S. En guise de guide plus complet sur l'optimisation, je peux vous recommander de rechercher sur Google toutes sortes de guide des bonnes pratiques pour OpenCL et CUDA.

  • ,

Calcul GPU

La technologie CUDA (Compute Unified Device Architecture) est une architecture logicielle et matérielle qui permet l'informatique à l'aide de processeurs graphiques NVIDIA prenant en charge la technologie GPGPU (informatique aléatoire sur cartes vidéo). L'architecture CUDA est apparue pour la première fois sur le marché avec la sortie de la puce NVIDIA de huitième génération - G80 et est présente dans toutes les séries ultérieures de puces graphiques utilisées dans les familles d'accélérateurs GeForce, ION, Quadro et Tesla.

Le SDK CUDA permet aux programmeurs d'implémenter des algorithmes pouvant être exécutés sur les GPU NVIDIA dans un dialecte simplifié spécial du langage de programmation C et d'inclure des fonctions spéciales dans le texte d'un programme C. CUDA donne au développeur la possibilité, à sa discrétion, d'organiser l'accès au jeu d'instructions de l'accélérateur graphique, de gérer sa mémoire et d'y organiser des calculs parallèles complexes.

Histoire

En 2003, Intel et AMD se sont lancés dans une course commune pour trouver le processeur le plus puissant. Pendant plusieurs années, à la suite de cette course, les vitesses d'horloge ont considérablement augmenté, notamment après la sortie de l'Intel Pentium 4.

Après l'augmentation des fréquences d'horloge (entre 2001 et 2003, la fréquence d'horloge du Pentium 4 a doublé, passant de 1,5 à 3 GHz), et les utilisateurs ont dû se contenter de dixièmes de gigahertz, mis sur le marché par les constructeurs (de 2003 à 2005, les fréquences d'horloge ont augmenté de 3 à 3,8 GHz).

Les architectures optimisées pour les hautes fréquences d'horloge, comme Prescott, ont également commencé à rencontrer des difficultés, et pas seulement celles de production. Les fabricants de puces sont confrontés à des défis pour surmonter les lois de la physique. Certains analystes prédisaient même que la loi de Moore cesserait de s'appliquer. Mais cela ne s’est pas produit. Le sens originel de la loi est souvent déformé, mais elle concerne le nombre de transistors à la surface du noyau de silicium. Pendant longtemps, une augmentation du nombre de transistors dans un processeur s'est accompagnée d'une augmentation correspondante des performances, ce qui a conduit à une distorsion du sens. Mais ensuite la situation est devenue plus compliquée. Les développeurs de l'architecture CPU se sont rapprochés de la loi de la réduction de la croissance : le nombre de transistors à ajouter pour l'augmentation requise des performances est devenu de plus en plus important, conduisant à une impasse.

La raison pour laquelle les fabricants de GPU n'ont pas rencontré ce problème est très simple : les processeurs sont conçus pour obtenir des performances maximales sur un flux d'instructions qui traitent différentes données (à la fois des nombres entiers et des nombres à virgule flottante), effectuent un accès aléatoire à la mémoire, etc. Jusqu'à présent, les développeurs essayaient de fournir un plus grand parallélisme des instructions, c'est-à-dire d'exécuter autant d'instructions que possible en parallèle. Par exemple, avec le Pentium, l'exécution superscalaire est apparue, alors que sous certaines conditions il était possible d'exécuter deux instructions par cycle d'horloge. Pentium Pro a reçu une exécution d'instructions dans le désordre, ce qui a permis d'optimiser le fonctionnement des unités de calcul. Le problème est qu'il existe des limites évidentes à l'exécution d'un flux séquentiel d'instructions en parallèle, donc augmenter aveuglément le nombre d'unités de calcul n'apporte aucun avantage puisqu'elles resteront inactives la plupart du temps.

Le fonctionnement du GPU est relativement simple. Elle consiste à prendre un groupe de polygones d’un côté et à générer un groupe de pixels de l’autre. Les polygones et les pixels sont indépendants les uns des autres et peuvent donc être traités en parallèle. Ainsi, dans un GPU, il est possible d'allouer une grande partie du cristal en unités de calcul qui, contrairement au CPU, seront réellement utilisées.

Le GPU ne diffère pas seulement du CPU de cette manière. L'accès à la mémoire dans le GPU est très couplé - si un texel est lu, alors après quelques cycles d'horloge, le texel voisin sera lu ; Lorsqu'un pixel est enregistré, après quelques cycles d'horloge, le pixel voisin sera enregistré. En organisant intelligemment la mémoire, vous pouvez atteindre des performances proches du débit théorique. Cela signifie que le GPU, contrairement au CPU, ne nécessite pas un énorme cache, puisque son rôle est d'accélérer les opérations de texturation. Il suffit de quelques kilo-octets contenant quelques texels utilisés dans les filtres bilinéaires et trilinéaires.

Premiers calculs sur GPU

Les premières tentatives de telles applications se limitaient à l'utilisation de certaines fonctions matérielles, telles que la rastérisation et le Z-buffering. Mais au cours du siècle actuel, avec l’avènement des shaders, les calculs matriciels ont commencé à s’accélérer. En 2003, chez SIGGRAPH, une section distincte a été réservée au calcul GPU, et elle s'appelait GPGPU (General-Purpose computing on GPU).

Le plus connu est BrookGPU, un compilateur pour le langage de programmation de streaming Brook, conçu pour effectuer des calculs non graphiques sur le GPU. Avant son apparition, les développeurs utilisant les capacités des puces vidéo pour les calculs choisissaient l'une des deux API courantes : Direct3D ou OpenGL. Cela a sérieusement limité l'utilisation des GPU, car les graphiques 3D utilisent des shaders et des textures que les spécialistes de la programmation parallèle ne sont pas tenus de connaître ; ils utilisent des threads et des cœurs. Brook a pu les aider à rendre leur tâche plus facile. Ces extensions de streaming du langage C, développées à l'Université de Stanford, cachaient l'API 3D aux programmeurs et présentaient la puce vidéo comme un coprocesseur parallèle. Le compilateur a traité le fichier .br avec du code et des extensions C++, produisant du code lié à une bibliothèque compatible DirectX, OpenGL ou x86.

L'apparition de Brook a suscité l'intérêt de NVIDIA et d'ATI et a ensuite ouvert un tout nouveau secteur : les ordinateurs parallèles basés sur des puces vidéo.

Par la suite, certains chercheurs du projet Brook ont ​​rejoint l'équipe de développement de NVIDIA pour introduire une stratégie de calcul parallèle matériel-logiciel, ouvrant ainsi de nouvelles parts de marché. Et le principal avantage de cette initiative de NVIDIA est que les développeurs connaissent toutes les capacités de leurs GPU dans les moindres détails, et il n'est pas nécessaire d'utiliser l'API graphique, et vous pouvez travailler avec le matériel directement à l'aide du pilote. Le résultat des efforts de cette équipe a été NVIDIA CUDA.

Domaines d'application des calculs parallèles sur GPU

Lors du transfert de calculs vers le GPU, de nombreuses tâches atteignent une accélération de 5 à 30 fois par rapport aux processeurs universels rapides. Les plus grands nombres (de l'ordre d'une accélération de 100x voire plus !) sont obtenus avec un code peu adapté aux calculs utilisant des blocs SSE, mais assez pratique pour les GPU.

Ce ne sont là que quelques exemples d'accélérations du code synthétique sur le GPU par rapport au code vectorisé SSE sur le CPU (selon NVIDIA) :

Microscopie à fluorescence : 12x.

Dynamique moléculaire (calcul de force non liée) : 8-16x ;

Électrostatique (sommation coulombienne directe et multiniveau) : 40-120x et 7x.

Un tableau que NVIDIA affiche dans toutes les présentations montre la vitesse des GPU par rapport aux CPU.

Liste des principales applications dans lesquelles le GPU computing est utilisé : analyse et traitement d'images et de signaux, simulation physique, mathématiques computationnelles, biologie computationnelle, calculs financiers, bases de données, dynamique des gaz et liquides, cryptographie, radiothérapie adaptative, astronomie, traitement audio , bioinformatique, simulations biologiques, vision par ordinateur, exploration de données, cinéma et télévision numériques, simulations électromagnétiques, systèmes d'information géographique, applications militaires, planification minière, dynamique moléculaire, imagerie par résonance magnétique (IRM), réseaux de neurones, recherche océanographique, physique des particules, protéines simulation de pliage, chimie quantique, lancer de rayons, visualisation, radar, simulation de réservoir, intelligence artificielle, analyse de données satellite, exploration sismique, chirurgie, échographie, vidéoconférence.

Avantages et limites de CUDA

Du point de vue d'un programmeur, un pipeline graphique est un ensemble d'étapes de traitement. Le bloc de géométrie génère les triangles et le bloc de rastérisation génère les pixels affichés sur le moniteur. Le modèle de programmation GPGPU traditionnel ressemble à ceci :

Pour transférer les calculs vers le GPU au sein de ce modèle, une approche particulière est nécessaire. Même l'ajout élément par élément de deux vecteurs nécessitera de dessiner la figure sur l'écran ou dans un tampon hors écran. La figure est pixellisée, la couleur de chaque pixel est calculée à l'aide d'un programme donné (pixel shader). Le programme lit les données d'entrée des textures pour chaque pixel, les ajoute et les écrit dans le tampon de sortie. Et toutes ces nombreuses opérations sont nécessaires pour quelque chose qui est écrit avec un seul opérateur dans un langage de programmation classique !

Par conséquent, l’utilisation du GPGPU à des fins informatiques générales présente la limite d’être trop difficile à former pour les développeurs. Et il y a suffisamment d'autres restrictions, car un pixel shader n'est qu'une formule pour la dépendance de la couleur finale d'un pixel sur ses coordonnées, et le langage des pixel shaders est un langage pour écrire ces formules avec une syntaxe de type C. Les premières méthodes GPGPU constituent une astuce intéressante qui vous permet d’utiliser la puissance du GPU, mais sans aucune commodité. Les données y sont représentées par des images (textures) et l'algorithme est représenté par le processus de rastérisation. Il convient de noter en particulier le modèle très spécifique de mémoire et d’exécution.

L'architecture logicielle et matérielle de NVIDIA pour le calcul GPU diffère des modèles GPGPU précédents en ce sens qu'elle vous permet d'écrire des programmes pour le GPU dans un vrai langage C avec une syntaxe standard, des pointeurs et la nécessité d'un minimum d'extensions pour accéder aux ressources informatiques des puces vidéo. CUDA est indépendant des API graphiques et possède certaines fonctionnalités conçues spécifiquement pour l'informatique générale.

Avantages de CUDA par rapport à l'approche traditionnelle du calcul GPGPU

CUDA donne accès à 16 Ko de mémoire partagée par thread par multiprocesseur, qui peut être utilisée pour organiser un cache avec une bande passante plus élevée que les récupérations de texture ;

Transfert de données plus efficace entre le système et la mémoire vidéo ;

Pas besoin d'API graphiques avec redondance et surcharge ;

Adressage, collecte et dispersion de la mémoire linéaire, capacité d'écrire à des adresses arbitraires ;

Prise en charge matérielle des opérations sur les entiers et les bits.

Principales limites de CUDA :

Manque de prise en charge de la récursivité pour les fonctions exécutables ;

La largeur minimale du bloc est de 32 threads ;

Architecture CUDA fermée appartenant à NVIDIA.

Les faiblesses de la programmation avec les méthodes GPGPU précédentes sont que ces méthodes n'utilisent pas d'unités d'exécution de vertex shader dans les architectures non unifiées précédentes, les données sont stockées dans des textures et sorties dans un tampon hors écran, et les algorithmes multi-passes utilisent des unités de pixel shader. Les limitations du GPGPU peuvent inclure : une utilisation insuffisante des capacités matérielles, des limitations de la bande passante mémoire, le manque d'opération de dispersion (collecte uniquement), l'utilisation obligatoire de l'API graphique.

Les principaux avantages de CUDA par rapport aux méthodes GPGPU précédentes proviennent du fait que l'architecture est conçue pour utiliser efficacement le calcul non graphique sur le GPU et utilise le langage de programmation C sans nécessiter le portage des algorithmes vers une forme conviviale pour le concept de pipeline graphique. . CUDA offre une nouvelle voie vers le calcul GPU qui n'utilise pas d'API graphiques, offrant un accès aléatoire à la mémoire (dispersion ou rassemblement). Cette architecture ne présente pas les inconvénients du GPGPU et utilise toutes les unités d'exécution, et étend également les capacités grâce aux mathématiques entières et aux opérations de décalage de bits.

CUDA ouvre certaines fonctionnalités matérielles non disponibles dans les API graphiques, telles que la mémoire partagée. Il s'agit d'une petite mémoire (16 kilo-octets par multiprocesseur) à laquelle les blocs de threads ont accès. Il vous permet de mettre en cache les données les plus fréquemment consultées et peut fournir des vitesses plus rapides que l'utilisation de récupérations de textures pour cette tâche. Ce qui, à son tour, réduit la sensibilité du débit des algorithmes parallèles dans de nombreuses applications. Par exemple, il est utile pour l'algèbre linéaire, la transformée de Fourier rapide et les filtres de traitement d'image.

L'accès à la mémoire est également plus pratique dans CUDA. Le code de l'API graphique génère des données sous forme de 32 valeurs à virgule flottante simple précision (valeurs RGBA simultanément dans huit cibles de rendu) dans des zones prédéfinies, et CUDA prend en charge l'écriture dispersée - un nombre illimité d'enregistrements à n'importe quelle adresse. De tels avantages permettent d'exécuter certains algorithmes sur le GPU qui ne peuvent pas être implémentés efficacement à l'aide des méthodes GPGPU basées sur des API graphiques.

De plus, les API graphiques stockent nécessairement les données dans des textures, ce qui nécessite un conditionnement préalable de grands tableaux dans des textures, ce qui complique l'algorithme et oblige à utiliser un adressage spécial. Et CUDA vous permet de lire des données à n'importe quelle adresse. Un autre avantage de CUDA est l'échange de données optimisé entre le CPU et le GPU. Et pour les développeurs qui souhaitent un accès de bas niveau (par exemple, lors de l'écriture d'un autre langage de programmation), CUDA offre des capacités de programmation en langage assembleur de bas niveau.

Inconvénients de CUDA

L’un des rares inconvénients de CUDA est sa mauvaise portabilité. Cette architecture ne fonctionne que sur les puces vidéo de cette société, et pas sur toutes, mais à commencer par les séries GeForce 8 et 9 et les Quadro, ION et Tesla correspondantes. NVIDIA cite le chiffre de 90 millions de puces vidéo compatibles CUDA.

Alternatives CUDA

Un cadre pour écrire des programmes informatiques liés au calcul parallèle sur divers processeurs graphiques et centraux. Le framework OpenCL comprend un langage de programmation basé sur la norme C99 et une interface de programmation d'application (API). OpenCL fournit un parallélisme au niveau des instructions et des données et est une implémentation de la technique GPGPU. OpenCL est un standard complètement ouvert et son utilisation est libre de droits.

L'objectif d'OpenCL est de compléter OpenGL et OpenAL, qui sont des standards industriels ouverts pour l'infographie et l'audio 3D, en tirant parti de la puissance du GPU. OpenCL est développé et maintenu par le consortium à but non lucratif Khronos Group, qui comprend de nombreuses grandes entreprises, notamment Apple, AMD, Intel, nVidia, Sun Microsystems, Sony Computer Entertainment et d'autres.

CAL/IL (Couche d'abstraction de calcul/Langage intermédiaire)

La technologie ATI Stream est un ensemble de technologies matérielles et logicielles qui permettent d'utiliser les GPU AMD conjointement avec un processeur pour accélérer de nombreuses applications (pas seulement graphiques).

Les applications d'ATI Stream incluent des applications à forte intensité de calcul telles que l'analyse financière ou le traitement de données sismiques. L'utilisation d'un processeur de flux a permis d'augmenter de 55 fois la vitesse de certains calculs financiers par rapport à la résolution du même problème en utilisant uniquement le processeur central.

NVIDIA ne considère pas la technologie ATI Stream comme un concurrent très sérieux. CUDA et Stream sont deux technologies différentes qui se trouvent à des niveaux de développement différents. La programmation des produits ATI est beaucoup plus complexe : leur langage ressemble davantage à un langage assembleur. CUDA C, quant à lui, est un langage de bien plus haut niveau. Écrire dessus est plus pratique et plus facile. Ceci est très important pour les grandes sociétés de développement. Si nous parlons de performances, nous pouvons voir que sa valeur maximale dans les produits ATI est plus élevée que dans les solutions NVIDIA. Mais encore une fois, tout dépend de la manière d’obtenir ce pouvoir.

DirectX11 (DirectCompute)

Interface de programmation d'applications faisant partie de DirectX, un ensemble d'API de Microsoft conçu pour s'exécuter sur des ordinateurs compatibles IBM PC exécutant les systèmes d'exploitation Microsoft Windows. DirectCompute est conçu pour effectuer du calcul général sur des GPU, une implémentation du concept GPGPU. DirectCompute a été initialement publié dans le cadre de DirectX 11, mais est ensuite devenu disponible pour DirectX 10 et DirectX 10.1.

NVDIA CUDA dans la communauté scientifique russe.

Depuis décembre 2009, le modèle logiciel CUDA est enseigné dans 269 universités à travers le monde. En Russie, des cours de formation sur CUDA sont dispensés dans les universités d'État de Moscou, Saint-Pétersbourg, Kazan, Novossibirsk et Perm, l'Université internationale sur la nature de la société et de l'homme « Doubna », l'Institut commun de recherche nucléaire, l'Institut d'électronique de Moscou Technologie, Université d'État de l'énergie d'Ivanovo, BSTU. V. G. Choukhov, MSTU im. Bauman, Université technique chimique russe du nom. Mendeleïev, Centre scientifique russe « Institut Kurchatov », Centre interrégional de supercalculateurs de l'Académie des sciences de Russie, Institut technologique de Taganrog (TTI SFU).