CPU - Parallélisme

publicité
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 )
Téléchargement