Programmation Parallèle sur CPU et GPU (GPU=Graphics Processing Unit) [email protected] www.labri.fr/perso/guenneba/pghp_IO16 2 Plan du cours • 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 3 Objectifs • 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 4 Motivations applicatives • 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. 5 Motivations applicatives • 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 • Exemple sur un smartphone : cons o nombre d'opérations (+,*,etc.) 6 7 Comment augmenter les performances ? → Améliorer les algorithmes (voir autres cours) → Augmenter la puissance de calcul ? → Comment ? → Comment exploiter cette puissance ? 8 Loi de Moore... • Le nombre de transistors qui peut être intégré facilement dans un microprocesseur double tout les deux ans 9 Motivations pour le multi-cores • 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 !! 10 Motivations pour le multi-cores • 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é • 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 un seul coeur 11 Motivations pour le multi-cores • 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 12 Motivations pour le multi-cores → multi-core everywhere – CPU – GPU – super-calculateur – systèmes embarqués – smart-phones carte graphique (GPU :1000-3000 coeurs) co-processeur dédié (GPU ou centaine de CPUs) serveur embarqué (ex. Jetson) (4 cœurs CPU + 200-300 cœurs GPU) [↔ supercalculateur en 2000] 13 mais... • 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 15 CPU – Hiérarchie mémoire RAM (NUMA) Cache - L2 Cache - L1 regs x1000 bigger ; ~400 cycles x100 bigger (900x900 floats) ; 40-100 cycles x100 bigger (90x90 floats) ; 1-4 cycles small (8x8 floats) ; 1 cycle ALU 16 CPU - Parallélisme • 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 17 CPU – Parallélisme 1/3 • 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 ! 1op = 4 mini ops = 4 cycles a = a * b; c = c * d; e = e * f; g = g * h; 4 ops in 7 cycles ! [démo] time CPU – Parallélisme 1/3 In-order / Out-of-order • 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 18 19 CPU – Parallélisme 2/3 • 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 5 -5 -6 * -2 → 12 2 4 8 reg0 reg1 reg2 CPU – Parallélisme 2/3 SIMD • 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) 20 CPU – Parallélisme 2/3 SIMD mémoire fast slower (aligned) (not aligned) don't be silly! 21 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] 23 CPU – Parallélisme 3/3 • 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 PC (instrumentation du code) – hyper-threading shared memory • assigner deux threads sur un même cœur • exécution alternée au niveau des instructions ... CPU1 CPU2 CPUn • permet un meilleur taux d'utilisation des unitées • parfois contre-productifs ! 24 Peak performance • 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 27 Programmation multi-threads • 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é 28 OpenMP – premier exemple • 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] 29 OpenMP – 2ème exemple • 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] 30 OpenMP – 3ème exemple • 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) 31 Atomic operations • 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 #1 Read-only mais variable d'état interne RW → critical section (bad) #2 Destination Image ... Random Number Generator #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 RNG/thread #2 Destination Image ... ... RNG[T] #1 #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 RNG[2] #2 M≈N2 ( ) 2 N T → O T Img[T] Cout mémoire : Destination Image Seconde passe → parallel for #T → 1 RNG/thread O ( M /T ) , ∑ ... ... ... RNG[T] Img[2] 2 N T Exemple : sampling et histogramme • Multi-threading optimisé : 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 CDF Y RNG[1] #1 cpt[1] RNG[2] #2 cpt[2] #T cpt[T] O ( ( N T )/T ) 2 Cout mémoire : N T ≪ N #1 RNG[2] #2 RNG[T] Passe 3 : remplir ligne par ligne, 1 thread par ligne Destination Image ... Passe 2 : sommer les compteurs RNG[1] ... Passe 1 : générer le nombre de samples par ligne (et par thread) O ( M /T ) CDF X ... ... ... RNG[T] ∑ cpt #T O ( M /T )