Programmation Parallèle sur CPU et GPU (GPU=Graphics Processing Unit) gael.guennebaud@inria.fr www.labri.fr/perso/guenneba/pghp_io16
Plan du cours 2 Motivations pour le parallélisme et les GPUs single core multi-core many-core CPU Architecture Modèle de programmation en mémoire partagé OpenMP GPU (Graphics Processing Unit) Architecture Modèle de programmation many-core CUDA
Objectifs 3 Acquérir les bases pour éviter les erreurs de bases dans vos propres codes poursuivre en auto-formation au besoin échanger avec des spécialistes savoir comparer deux solutions matérielles savoir adapter le matériel aux besoins
Motivations applicatives 4 Toujours plus de performance... plus rapide : de x10 à x100 ou plus!! résoudre des problèmes plus gros plus de précisions rendre possible de nouvelles applications, nouveaux algorithmes réduire la consommation etc.
Motivations applicatives 5 Exemples : Simu électromag, en un point : intégration 4D Code initial en (mauvais) MatLab : 20min Code optimisé / CPU : 0.5s!! Simu sur GPU via MatLab : a life changer! utilisation de CUDA en 3A-voie B (simu) 3A-voie A (instrumentation) : Embarqué traitement/reconstruction efficace des données... Free-form optics
Code optimisé et consommation énergétique 6 Exemple sur un smartphone : cons o nombre d'opérations (+,*,etc.)
Comment augmenter les performances? 7 Améliorer les algorithmes (voir autres cours) Augmenter la puissance de calcul? Comment? Comment exploiter cette puissance?
Loi de Moore... 8 Le nombre de transistors qui peut être intégré facilement dans un microprocesseur double tout les deux ans
Motivations pour le multi-cores 9 Finesse de gravure 32nm en 2010, 22nm en 2012, 14nm en 2014, demain : 10nm plus de transistors par circuit Leviers pour augmenter les performances avec un seul processeur : augmenter la fréquence? difficile, consommation++, chaleur++ augmenter la complexité des processeurs opérations spécialisées, logique (ex. prédiction de branchement), cache, etc. x2 logic x1.4 performance (Pollack's rule)!! vidéo!!
Motivations pour le multi-cores 10 Leviers pour augmenter les performances (cont.) multiplier le nombre d'unités de calcul : parallélisme au niveau des instructions out-of-order execution, pipelining difficile (beaucoup de logique) et limité parallélisme au niveau des données (SIMD) une seule instruction appliquée sur plusieurs registres unités vectorielles : SSE, AVX, NEON, Cell SPE, (GPU) efficace pour certaines applications, mais relativement difficile à exploiter et reste limité un seul coeur parallélisme au niveau des threads mettre plusieurs processeurs cote-à-cote sur un même chip multi-core, many-core (>100) multi-processor : plusieurs chip sur une même carte mère
Motivations pour le multi-cores 11 Plusieurs processeurs par circuits, plusieurs circuits par carte réels gains théoriques: 2 «petits» processeurs x1.8 perf 1 processeur 2 fois plus gros x1.4 perf consommation et dissipation de chaleur réduite N processeurs légèrement plus lents consomment autant qu'un seul rapide activation individuelle des processeurs accès mémoires plusieurs CPU peuvent accéder en même temps à la mémoire permet d'augmenter la bande passante même sans en réduire la latence simplicité plus simple de concevoir et fabriquer pleins de «petits» CPU simples, qu'un seul gros et complexe améliore également la robustesse et absence de pannes/bugs
Motivations pour le multi-cores 12 multi-core everywhere CPU GPU super-calculateur systèmes embarqués smart-phones serveur carte graphique (GPU :1000-3000 coeurs) co-processeur dédié (GPU ou centaine de CPUs) embarqué (ex. Jetson) (4 cœurs CPU + 200-300 cœurs GPU) [ supercalculateur en 2000]
mais... 13 Programmer est difficile... Programmer parallèle est encore plus difficile! trouver des tâches pouvant être exécuter en même temps coordination entre les tâches, éviter les surcoûts...
Architecture des CPUs
CPU Hiérarchie mémoire 15 RAM (NUMA) x1000 bigger ; ~400 cycles Cache - L2 x100 bigger (900x900 floats) ; 40-100 cycles Cache - L1 x100 bigger (90x90 floats) ; 1-4 cycles regs small (8x8 floats) ; 1 cycle ALU
CPU - Parallélisme 16 3 niveaux de parallélisme : 1 parallélisme au niveau des instructions 2 SIMD Single Instruction Multiple Data 3 multi/many-cores multi-threading mécanismes complémentaires
CPU Parallélisme 1/3 17 parallélisme au niveau des instructions pipelining une opération = une unité de calcul (ex : addition) opération décomposée en plusieurs sous-taches une sous-unité par sous-tache 1 cycle par sous-tache plusieurs opérations peuvent s'enchainer sur une même unité requière des instructions non dépendantes! a = a * b; c = c * d; e = e * f; g = g * h; 1op = 4 mini ops = 4 cycles [démo] 4 ops in 7 cycles! time
CPU Parallélisme 1/3 In-order / Out-of-order 18 In-order processors instruction fetch attendre que les opérandes soient prêtes dépendances avec les opérations précédentes ou temps d'accès mémoire/caches exécuter l'instruction par l'unité respective Out-of-orders processors instruction fetch mettre l'instruction dans une file d'attente dès que les opérandes d'une des instructions de la file d'attente sont prêtes, exécuter l'instruction par l'unité respective couplée à la prédiction de branchement... réduit les temps où le processeur est en attente simplifie le travail du développeur/compilateur :) requière des instructions non dépendantes
CPU Parallélisme 2/3 19 SIMD Single Instruction Multiple Data Principe : exécuter la même opération sur plusieurs données en même temps Jeux d'instructions vectoriels, ex : SSE (x86) : registres 128 bits (4 float, 2 double, 4 int) AVX (x86) : registres 256 bits (8 float, 4 double, 8 int) NEON (ARM) : registres 128 bits (4 float, 4 int)... 4-3 -12-1 -6 * 5-2 -5 12 2 4 8 reg0 reg1 reg2
CPU Parallélisme 2/3 SIMD 20 Mise en oeuvre pratique, 3 possibilités : vectorisation explicite via des fonctions «intrinsics» (pour les experts) utiliser des bibliothèques tierces et optimisées believe in your compiler! Nombreuses limitations : réaliser des opérations intra-registres est difficile et très couteux les données doivent être stockées séquentiellement en mémoire (voir slide suivant)
CPU Parallélisme 2/3 SIMD 21 mémoire fastslower (aligned) (not aligned) don't be silly!
CPU Parallélisme 2/3 SIMD 22 Exemple de stockage pour des points 3D: Array of Structure (AoS) struct Vec3 { float x, y, z; } ; Vec3 points[100] ; not SIMD friendly Structure of Arrays (SoA) struct Points { float x[100]; float y[100]; float z[100]; }; Points points; [démo]
CPU Parallélisme 3/3 23 multi/many-cores Principe : chaque cœur exécute son propre flot d'instruction (=thread) thread = «processus léger» un thread par cœur à la fois assignation et ordonnancement par l'os les threads communiquent via la mémoire partagée mise en œuvre, ex : bibliothèques tierces via le compilateur et OpenMP (instrumentation du code) hyper-threading assigner deux threads sur un même cœur exécution alternée au niveau des instructions permet un meilleur taux d'utilisation des unitées parfois contre-productifs! PC shared memory CPU1 CPU2... CPUn
Peak performance 24 Example : Intel i7 Quad CPU @ 2.6GHz (x86_64,avx,fma) pipelining/ooo 2 * (mul + add) / cycle (cas idéal) AVX x 8 ops simple précision à la fois multi-threading x 4 fréquence x 2.6G peak performance: 332.8 GFlops
26 Programmation multi-thread avec mémoire partagé - OpenMP
Programmation multi-threads 27 Très nombreuses approches, différents paradigmes exemples de modèles : Google's «map-reduce» divide and conquer Nvidia's data parallelism big homogeneous array Erlang's fonctional programming side-effect free OpenMP intégré au compilateur instrumentation manuelle du code en C/C++ via : #pragma omp... + ensembles de fonctions... fournit différents outils et paradigmes flexibilité
OpenMP premier exemple 28 Exécuter un même bloc par plusieurs threads : #pragma omp parallel { // on récupère le numéro du thread // (entre 0 et nombre_total_de_thread-1) int i = omp_get_thread_num(); } cout << "Thread #" << i << " says hello!\n"; et premières difficultés! ici tous les threads accèdent de manière concurrentielle à la même ressource (la sortie standard) solution : autoriser un seul thread à la fois section critique via #pragma omp critical [démo]
OpenMP 2ème exemple 29 Paralléliser une boucle #pragma omp parallel for for(int i=0 ; i<m ; ++i) {... } les m itérations de la boucles sont réparties entre les N threads une itération est exécutée une seule fois et par un seul thread un thread traite une seule itération à la fois nombreuses stratégie d'affectation ex. : statique ou dynamique Exercice : comment calculer le produit scalaire en un vecteur B et tous les éléments d'un tableau ex, conversion d'une image RGB en Luminance, projection de m points sur un plan, etc. quelles taches peuvent être effectuées en parallèle? [démo]
OpenMP 3ème exemple 30 Exercice : comment calculer la somme des éléments d'un tableau en parallèle? quelles taches peuvent être effectuées en parallèle? Race condition race condition (résultat différent en fonction de l'ordre d'exécution) correct behavior (critical section ou atomic)
Atomic operations 31 Principe opération ou ensemble d'opérations s'exécutant sans pouvant être interrompues pour le reste du système : comme si son exécution était instantanée nécessitent des mécanismes de synchronisation misent en œuvre au niveau du matériel Exemples Read-Write Fetch-and-add x = x + a ; Test-and-set int test_and_set (int *val){ int old = *val; *val = 1; return old; } Compare-and-swap
Comment paralléliser son code et ses algorithmes? Dépend de nombreux facteurs Nature du problème Dimensions du problème Matériel visé Pas de solution universelle Pas de solution automatique (compilateur) Acquérir de l'expérience étudier des cas «simples» retrouver des motifs connus au sein d'applications complexes
Défis Trouver et exploiter la concurrence 1 - identifier les taches indépendantes approche hiérarchique 2 - regrouper les taches trouver la bonne granularité regarder le problème sous un autre angle Computational thinking (J. Wing) nombreuses façons de découper un même problème en taches Identifier et gérer les dépendances Dépend du découpage! Coordination entre les tâches (avoid races and deadlocks) éviter les attentes
Défis Autres facteurs de performance : Contrôler les surcouts limiter les échanges de données limiter les calculs supplémentaires Équilibrer la charge de chaque unité Saturation des ressources (bande-passante mémoire) Avant toute optimisation : avoir un code qui fonctionne! identifier les taches couteuses! (on peut avoir des surprises) penser à la complexité algorithmique!
Qques motifs de parallélisation Map & Reduce «Map» appliquer la même fonction à chacun des éléments d'un tableau parallélisme évident : chaque élément peut être traité par un thread différent surjection de l'ensemble des M éléments l'ensemble des N threads scheduling static 1 thread un bloc de M/N éléments ou 1 thread les éléments d'indices i*n scheduling dynamic dès qu'un thread est libre, l'associer au premier élément non traité «Reduce» ou «Reduction» réduire un tableau (ou sous-tableau) à une seule valeur Exemples : somme, produit, min, max, etc. (moyenne, calcul intégral,etc.) Si faible nombre de threads N : découper le problème en N sous problèmes puis réduire les N valeurs en 1 de manière séquentielle. généralisation à une approche hiérarchique
Exemple : sampling et histogramme Approche séquentielle CDF 2D Random Number Generator Main Thread Destination Image for i=1..m { (x,y) = cdf.inverse(random(),random()); img(x,y) += incr; }
... Exemple : sampling et histogramme Multi-threading naif CDF 2D Read-only Random Number Generator #1 #2 Destination Image Read-only mais variable d'état interne RW critical section (bad) #T Read-write critical section (bad) #pragma omp parallel for for i=1..m { (x,y) = cdf.inverse(random(),random()); img(x,y) += incr; }
...... Exemple : sampling et histogramme Multi-threading V1 CDF 2D RNG[1] RNG[2] #1 #2 Destination Image RNG[T] 1 RNG/thread #T Read-write ou atomic (~OK) #pragma omp parallel for for i=1..m { (x,y)=cdf.inverse(rng[tid].val(),rng[tid].val()); #pragma omp atomic img(x,y) += incr; }
......... Exemple : sampling et histogramme Multi-threading V2 Img[1] CDF 2D RNG[1] #1 Img[2] Destination Image RNG[2] #2 RNG[T] #T Seconde passe parallel for 1 RNG/thread Img[T] O ( N 2 T T ) O ( M /T ), M N 2 Cout mémoire : N 2 T
............... Exemple : sampling et histogramme Multi-threading optimisé : CDF Y RNG[1] #1 cpt[1] Découper l'image par thread nombreuses variantes ex : remplir ligne par ligne meilleure cohérence des caches :) nombre d'échantillons par ligne variable scheduling dynamic RNG[2] #2 cpt[2] cpt CDF X Passe 3 : remplir ligne par ligne, 1 thread par ligne RNG[T] #T cpt[t] RNG[1] #1 Passe 1 : générer le nombre de samples par ligne (et par thread) O ( M /T ) Cout mémoire : Passe 2 : sommer les compteurs O (( N T )/T ) N T N 2 RNG[2] RNG[T] #2 #T Destination Image O ( M /T )