Une bibliothèque de templates pour CUDA Sylvain Collange, Marc Daumas et David Defour Montpellier, 16 octobre 2008
Types de parallèlisme de données Données indépendantes n threads pour n jeux de données distincts Ex : rendu graphique Code d'un thread = corps de boucle parallèle Données dépendantes n threads pour 1 jeu de données Ex : multiplication de deux matrices 100x100 Utilisation de bibliothèques GPU : cublas, cufft, CUDPP... Mixte n*m threads pour n jeux de données Ex : calcul sur 100 matrices 10x10 Pas de solution existante sur GPU 2
Application Résolution de simplexes pour solveurs de Bernstein Collaboration avec Dominique Michelucci, Université de Bourgogne Résoudre ~10000 simplexes de 100x100 Portage sur GPU en cours de développement 3
Objectifs Développer une bibliothèque logicielle sur GPU Mettre en œuvre des algorithmes parallèles pour du parallélisme «mixte» Simplifier la programmation pour GPU Rester efficace 4
Plan NVidia CUDA Algorithmes parallèles et CUDPP Notre bibliothèque 5
Répartir le travail Sur CPU multicœur / SMP / NUMA Parallélisme à gros grain Découpler les données des threads pour limiter les conflits et communications Sur GPU Parallélisme à grain fin Entrelacer les données des threads pour optimiser la localité et exploiter les mémoires locales T0 T1 T2 T3 T0 T1 T2 T3 6
Architecture GPU NVidia simplifiée Unités de calcul Cœur Registres Mém partagée Mém constantes Unité mémoire Controleur memoire Mémoire globale Cluster x8 7
NVidia CUDA Compilateur et bibliothèque pour les GPU NVidia Organisation des threads par l'utilisateur Code SPMD : un seul programme pour tous les threads 8
Organisation logicielle des threads Ordonnancement des threads par le GPU/driver Tous les threads d'un bloc sont ordonnancés sur le même cœur Les blocs sont ordonnancés en fonction des ressources disponibles 9
Mémoires 10
Localité Exécution en SIMT (Single Instruction, Multiple Threads) Le programmeur écrit du code sur des données scalaires Le matériel exécute ce code sur des vecteurs Branchement Peut prendre plusieurs directions différentes dans le vecteur Il faut exécuter tous les cas, masquer les résultats Cas particulier : tous les threads du vecteur suivent le même chemin 11
Instructions load/store Chaque thread du vecteur peut demander une adresse différente Devient gather/scatter Cas particulier : toutes les adresses dans une même ligne mémoire Une seule requête mémoire à faire (coalesced reads/writes) Gain de performance significatif Privilégier la localité 12
Limitations Pas de mécanismes d'abstraction de la mémoire Mémoire partagée à allouer manuellement Calculs d'index en fonction du numéro de thread Pour respecter les règles de coalescing Architecture peu documentée 13
Plan NVidia CUDA Algorithmes parallèles et CUDPP Notre bibliothèque 14
Algorithmes parallèles Réduction Sommation, produit scalaire Somme préfixe (scan) Multiplication matrice creuse x vecteur Compaction Transposition Optimisation des motifs d'accès mémoire 15
CUDPP CUDA Data Parallel Primitives Library University of California Davis, NVidia Bibliothèque C sur CPU Algorithmes de scan CUDPPConfiguration config; config.op = CUDPP_ADD; config.datatype = CUDPP_FLOAT; config.algorithm = CUDPP_SCAN; config.options = CUDPP_OPTION_FORWARD CUDPP_OPTION_EXCLUSIVE; CUDPPHandle scanplan = 0; cudppplan(&scanplan, config, numelements, 1, 0); cudppscan(scanplan, d_odata, d_idata, numelements); 16
CUDPP Avantages Pas besoin de programmer en CUDA Algorithmes parallèles efficaces Limitations Pas de parallèlisme possible entre plusieurs calculs : opérations effectuées séquentiellement Coût de lancement du calcul, communications avec le CPU Types de données et opérations possibles limitées : opérateurs paramétrables mais non programmables 17
Plan NVidia CUDA Algorithmes parallèles et CUDPP Notre bibliothèque 18
Contenu Des conteneurs Pour abstraire la gestion des mémoires Des algorithmes parallèles Pour la communication entre threads d'un bloc Source et destination dans les registres Réduction parallèle (vote), broadcast,... Des fonctions haut-niveau Source ou destination en mémoire globale 19
CUDA : C ou C++? Support officiel C++ sur CPU sauf exceptions C uniquement sur GPU «sauf templates simples» En pratique Utilisation de classes et templates dans CUDPP Projet auquel participe NVidia Front-end CUDA basé sur le front-end C++ d'edg Respecte 100% de la norme C++ Rumeurs : support du C++ dans une prochaine version de CUDA? Toujours possible de passer par un compilateur C++ C 20
Métaprogrammation Les templates C++ fournissent un langage fonctionnel Exécuté à la compilation Récursivité possible template<int n> struct fact { enum { val = n * fact<n-1>::val }; }; template<> struct fact<0> { enum { val = 1 }; }; fact<6>::val -> 720 Permet de générer des constantes et du code 21
Découpage d'un bloc CUDA Une dimension explicite, une dimension implicite Quelle est la plus interne? Calcul sur n blocs indépendants de taille m Array Of Structures Calcul sur un bloc de m vecteurs de taille p Structure Of Arrays Dépend de l'application On généralise : n blocs indépendants de m vecteurs de taille p Les dimensions n et p sont implicites Classe Shape passée en argument de template Dimensions connues à la compilation n m m p p m n 22
Mémoire partagée En CUDA : allocation statique uniquement Pour toute la durée de l'exécution Pas de pile Une classe pour gérer la mémoire Allocation statique en pile template<class T, class Shape, int Size, class ParentFrame = root_frame, int Alignment = 4> struct shared_array { device shared_array(shape s); device T & operator[] (size_t index); }; Brique de base de la bibliothèque // Dimensions du bloc // Nombre d'éléments // (dimension explicite) // Cadre de pile 23
Mémoire privée Où stocker les données privées? Registres : rapide (0-4 cycles), limité (~32/thread), non indexable Mémoire partagée : rapide (4 cycles), limitée (~8/th), indexable Mémoire locale : lente (500 cycles), ~illimitée, indexable Mémoire globale : lente (500 cycles), ~illimitée, indexable En CUDA : quatre syntaxes différentes Choix à faire au début de la conception Devrait être fait lors de l'optimisation Abstraction du type de mémoire : template<class T, class Shape, int Size, class ParentFrame, StorageArea Storage> struct private_array; template<class T, class Shape, class ParentFrame, StorageArea Storage> struct private_scalar; 24
Réduction Opération à effectuer Somme, min, max, etc. Fonction passée en paramètre template Réduction dans la dimension m Utilisation d'un arbre de réduction Algorithme récursif exécuté à la compilation Aucun contrôle de flot dans le code GPU Passage par la mémoire partagée Résultat aux threads d'indice 0 dans la dimension m m f p? n 0 25
Broadcast Suit généralement une réduction Tous les threads de la dimension m reçoivent la valeur du thread i Passage par la mémoire partagée m p n i 26
Algorithmes dérivés Fonctions intégrées Accès en mémoire globale avec adressage implicite Lecture de scalaire en mémoire globale (read+broadcast) Réductions depuis un tableau en mémoire Recherche d'un élément satisfaisant un prédicat dans un tableau Objectif : porter les fonctions standard de <algorithm> for_each, transform, fill, find, search, count, max_element... Presque toutes implémentables avec les briques de base Nécessité de trouver un équivalent parallèle aux itérateurs 27
Decuda Langage machine GPU NVidia non documenté Désassembleur issu d'un travail de reconstruction à partir de la sortie binaire du compilateur Wladimir van der Laan, Rijksuniversiteit Groningen, NL Permet de connaître exactement le code généré 28
Problèmes rencontrés Front-end C++ Pas de fonctions membres template Niveaux de protection (private,...) non respectés par l'émulation logicielle Erreurs internes du compilateur Back-end Échec de l'inférence du type de mémoire pointée «Optimisations» nécessitant trop de registres Langage pas encore stabilisé 29
Résultats Réduction depuis la mémoire avec 512 threads Comparaison avec la réduction 6 de Mark Harris (code C optimisé de NVidia) 5 4.5 4 3.5 3 2.5 2 Byte/clock Harris Byte/clock CUTL 1.5 1 0.5 0 1024 2048 32K 256K 4MB 64MB Pas de surcoût notable dû à la généricité 30
Conclusion Évolution progressive des langages GPU bas-niveau Assembleurs en 2002 (DX shaders, ARBfp) Dérivés du C simplifiés en 2003 (Cg, GLSL, HLSL) Dérivé du C parallèle en 2005 (Brook) C parallèle en 2006 (CUDA) Prochaines étapes C++ Bibliothèques de structures de données et d'algorithmes Reste à construire 31
OpenCL? Présenté à SIGGRAPH 08 // This kernel computes FFT of length 1024. The 1024 length FFT is decomposed into // calls to a radix 16 function, another radix 16 function and then a radix 4 function kernel void fft1d_1024 ( global float2 *in, global float2 *out, local float *smemx, local float *smemy) { int tid = get_local_id(0); int blockidx = get_group_id(0) * 1024 + tid; float2 data[16]; // starting index of data to/from global memory in = in + blockidx; out = out + blockidx; globalloads(data, in, 64); // coalesced global reads fftradix16pass(data); // in-place radix-16 pass twiddlefactormul(data, tid, 1024, 0); // local shuffle using local memory localshuffle(data, smemx, smemy, tid, (((tid & 15) * 65) + (tid >> 4))); fftradix16pass(data); // in-place radix-16 pass twiddlefactormul(data, tid, 64, 4); // twiddle factor multiplication localshuffle(data, smemx, smemy, tid, (((tid >> 4) * 64) + (tid & 15))); // four radix-4 function calls fftradix4pass(data); fftradix4pass(data + 4); fftradix4pass(data + 8); fftradix4pass(data + 12); // coalesced global writes globalstores(data, out, 64); } 32