• Aucun résultat trouvé

1.3 Outils de programmation et de parallélisation

1.3.1 Outils natifs

1.3.1.1 CUDA

CUDA, ou Compute Unified Device Architecture, est une architecture de calcul parallèle dé- veloppée par Nvidia permettant de programmer les GPU de la marque. CUDA inclut un en- semble d’instructions et un moteur de calcul parallèle sur le GPU. CUDA propose un modèle pour l’exécution de codes ainsi qu’un modèle mémoire adapté à l’architecture mémoire des GPU (cf. section 1.2.2.2).

Le modèle d’exécution

Le modèle de programmation de CUDA diffère des modèles multithreadés des GPP. Le dé- veloppeur CUDA visualise sa machine sous la forme d’un hôte ou host, un GPP classique, et d’un ou plusieurs processeurs de calcul massivement parallèle équipés d’un très grand nombre d’unités de calculs. Le langage CUDA est directement lié à l’architecture des GPU Nvidia. Le modèle de programmation est basé sur la notion de kernel. Un kernel est une fonction qui est appelée depuis le host et exécutée sur le GPU par un très grand nombre de threads de calcul en parallèle. Le host utilise des données résident en mémoire RAM sur le GPU. Le code CUDA est basé sur du C ANSI, étendu avec un certain nombre de mots clés permettant d’identifier les kernels, et les types de données utilisés.

Néanmoins, CUDA prend aussi en charge un sous-ensemble du C++ au travers des tem- plates, des références, de la surcharge d’opérateurs et de classes basiques. Le compilateur Nvi- dia, NVCC, divise le code entre la partie host et la partie device. La partie host est compilée comme du C ou C++ classique (avec le compilateur par défaut de la plateforme GCC/Vi- sual/ICC). Quant à la partie device, elle est d’abord transformée en un langage intermédiaire (PTX), puis en assembleur GPU. En fin de chaine, les deux parties sont liées dans un exécutable unique qui contient à la fois le code GPP et le code GPU.

Le meilleur moyen d’obtenir de bonnes performances avec une telle architecture est de pro- grammer d’une manière assez proche de celle du SIMD (Single Instruction Multiple Data) car le grand nombre de coeurs de calculs peut exécuter le même code de manière synchrone sur différentes données. Nvidia parle de modèle SIMT (Single Instruction Multiple Threads). Lors d’un appel à un kernel, il est exécuté sous la forme d’une grille de threads (threads très légers à ordonnancer, en comparaison aux threads GPP).

La création de ces threads est effectuée sur le GPU et est extrêmement rapide. Le temps d’instanciation a été mesuré et varie autour de 10 µs quelle que soit la quantité de threads [Vol- kov and Demmel, 2008]. La meilleure manière d’obtenir de bonnes performances est donc d’exécuter une très grande quantité de threads (plusieurs milliers) qui seront exécutés en pa- rallèle. Le code d’un kernel va par exemple regrouper le corps d’une boucle for en C et sera

FIGURE1.21 – Hiérarchie de groupement des threads.

exécuté de manière massivement parallèle sur le GPU au lieu de manière séquentielle.

Un kernel nécessite un paramétrage manuel du nombre de threads concurrents. Chaque thread qui exécute un kernel dispose d’un identifiant accessible via une variable. Les threads sont groupés en blocs et ces blocs sont organisés dans la grille (grid). Ces deux niveaux de hié- rarchie sont présentés sur la figure 1.21. Cela permet d’exprimer simplement un calcul sur un domaine de type vecteur/matrice par exemple. Les threads d’un bloc peuvent coopérer entre eux au travers la mémoire shared.

Le modèle mémoire

En CUDA, host et device disposent d’espaces mémoire séparés, tel que présenté au niveau ma- tériel, où les GPU disposent de leur propre DRAM. Afin d’exécuter un kernel sur le device, le développeur doit allouer de la mémoire sur ce device, puis transférer les données depuis le host. De la même manière, après l’exécution, le développeur doit transférer les données vers le host et libérer la mémoire du GPU. Ces fonctionnalités sont mises à disposition du développeur au travers de l’API CUDA.

FIGURE1.22 – Hiérarchie de groupement des threads.

La figure 1.22 présente un schéma du modèle mémoire de CUDA. Chaque thread dispose d’une mémoire privée locale. Il dispose aussi d’une mémoire shared visible par tous les threads de son bloc et dont la durée de vie est la même que celle du bloc. Enfin, tous les threads ont accès à une mémoire globale ou global, la DRAM du GPU. Les GPU disposent aussi d’espaces mémoire uniquement utilisables en lecture, que sont la mémoire constant et la mémoire texture. Ces différents espaces mémoires sont optimisés pour différentes types d’utilisation. Ces deux mémoires et la mémoire globale ont une durée de vie qui ne dépend pas des kernels et peuvent persister tout au long d’une application.

Il est important de noter que des différentes optimisations sont liées au matériel et donc aux bandes passantes, latences, et autres spécificités matérielles. Par exemple, la mémoire sha- red dispose de latences du même ordre que les registres du moment que les accès sont réalisés sans conflits tandis que les accès en mémoire globale sont deux ordres de grandeur plus lents (400 à 800 cycles en comparaison à une dizaine de cycles pour un accès en shared).

L’environnement de développement

CUDA dispose, en plus de son interface propre, d’une interface OpenCL et d’une DirectCom- pute (DirectX). De nombreux wrappers ont été développés pour Python, Java, .NET ou encore Matlab. Les outils de compilation sont disponibles sous Windows, Linux et Mac OS X. De plus, Nvidia a mis à disposition un environnement associé à Visual Studio, Nsight, permettant le profiling de l’application au travers de nombreux éléments d’analyse, ainsi que d’un outil de débogage. Ces outils sont précieux, dans un domaine encore jeune et là où par exemple AMD, ne dispose que d’outils beaucoup moins aboutis pour ses GPU.

La maitrise de l’architecture des GPU Nvidia et de l’environnement de développement a été principalement acquise lors d’une campagne d’implémentations et de benchmarks réalisés à l’iEF, en parallèle de ma thèse [Courbin et al., 2009] [Pédron et al., 2010].