CUDA et son espace mémoire shared-memory
CUDA Device Memory Space Each thread can: (Device) Grid R/W per-threadregisters very fast Block (, ) R/W per-threadlocal memory- fast R/W per-blockshared memory fast 6kB/SM, can be difficult to make it fast Read only per-grid constant memory - fast (cache) Read only per-grid texture memory- fast (cache) The host can R/W global, constant, and texture memories Shared Memory Registers R/W per-gridglobal memory- very slow Host Block (, ) Registers Shared Memory Registers Registers Thread (, ) Thread (, ) Thread (, ) Thread (, ) Local Memory Local Memory Local Memory Local Memory Global Memory Constant Memory Texture Memory 7
CUDA Variable Type Qualifiers Variable declaration Memory Scope Lifetime local thread thread device local int LocalVar; device shared int SharedVar; shared block block device int GlobalVar; global grid application constant grid application device constant int ConstantVar; device is optional when used with local, shared, or constant Automatic variables without any qualifier reside inregister a Except arraysthat reside in local memory 75
Shared Memory Ex : Réduction revisitée Réduction classique : log(n) passes... démarrer une nouvelle passe est coûteux nombreux accès mémoires redondants synchronisation globales des threads Idée Copier une partie du tableau en shared memory threads/bloc k floats accumulés / bloc (SM=6kB OK) divise par k la taille du vecteur à chaque passe (au lieu de /) synchronisation au sein d'un bloc uniquement accès à la mémoire globale réduits Réduction partielle «in-place» en utilisant exclusivement la shared memory!! adopter la bonne stratégie pour réduire les divergences
Vector Reduction with Branch Divergence Thread +... Thread + Thread +5..7..7 Thread 6 5 6 6+7 Thread 8 7 8 8+9 Thread 9 + 8.. 8..5 iterations David Kirk/NVIDIA and Wen-mei W. Hwu, 7-9 ECE 98AL, University of Illinois, Urbana-Champaign Array elements 77
Shared Memory Ex : Réduction revisitée shared float partialsum[n]; int i = *threadidx.x; int offset = *blockidx.x*blockdim.x; partialsum[i] = src[offset + i]; partialsum[i+] = src[offset + i+]; for(int stride=; stride<=blockdim.x ; stride*=) { syncthreads(); if( (i%(*stride)==) && (i+stride<*blockdim.x)) partialsum[i] = partialsum[i] + partialsum[i+stride]; } if(i==) dst[blockidx.x] = partialsum[];
Better!! Thread +6 5 6 7 8 9 5+ David Kirk/NVIDIA and Wen-mei W. Hwu, 7-9 ECE 98AL, University of Illinois, Urbana-Champaign 79
Shared Memory Ex : Réduction revisitée int i = threadidx.x; int offset = *blockidx.x*blockdim.x; shared float partialsum[n] ; partialsum[i] = data[offset+i]; partialsum[i+blockdim.x] = data[offset+i+blockdim.x]; for(int stride=blockdim.x; stride> && i<stride; stride=stride/) { syncthreads(); partialsum[i] = partialsum[i] + partialsum[i+stride]; } if(t==) data[blockidx.x] = partialsum[] ; approche à combiner avec le N-body simulation...
Shared Memory - Exemple produit matriciel Ex : produit matriciel B Exercice WIDTH= #loads =?? #(mul+add) =?? A C WIDTH WIDTH opération de base pour résoudre Ax=b C = A*B parallélisme naturel : tache = calcul d'un élément C(i,j) = produit scalaire accès mémoires prohibitifs C(i,j) WIDTH WIDTH
Shared Memory - Exemple produit matriciel int i = threadidx.x + blockidx.x*blockdim.x, j = threadidx.y + blockidx.y*blockdim.y; float c = ; for (int k = ; k<n; k++) { c += A[i+(j+k)*N] * B[i+k+j*N]; } C[i+j*N] = c ; slow
Shared Memory - Exemple produit matriciel TILE_WIDTH Exercice WIDTH= TILE_WIDTH= #loads =?? #madd =?? A C Cdsub TILE_WIDTH TILE_WIDTH WIDTH WIDTH TILE_WIDTH B considérer une sous matrice de C = bloc de threads pré-charger les sous blocs de A et B en shared memory WIDTH Idée TILE_WIDTHE TILE_WIDTH WIDTH 8
Shared Memory - Exemple produit matriciel int i = threadidx.x, j = threadidx.y; int oi = blockidx.x*blockdim.x, oj = blockidx.y*blockdim.y ; shared float tilea[t*t], tileb[t*t]; float c = ; for (int k = ; k<n; k+=t) { tilea[i+j*t] = A[oi+i+(k+j)*N] ; tileb[i+j*t] = B[k+i+(j+oj)*N] ; slow syncthreads(); for(int k = ; k<t; ++k) c += tilea[i+k*t] * tileb[k+j*t] ; } fast
Shared Memory - Exemple produit matriciel TILE_WIDTH Masquage des accès mémoires: charger les blocs bleus dans des registres pour chaque bloc copier les blocs A des registres vers la shared memory copier les blocs suivant (oranges) dans des registres calculer Cij pour les blocs courants TILE_WIDTH TILE_WIDTH WIDTH C Cdsub WIDTH TILE_WIDTH B considérer une sous matrice de C = bloc de threads pré-charger les sous blocs de A et B en shared memory WIDTH Idée TILE_WIDTHE TILE_WIDTH WIDTH 85
Shared Memory - Exemple produit matriciel int i = threadidx.x, j = threadidx.y; int oi = blockidx.x*blockdim.x, oj = blockidx.y*blockdim.y ; shared float tilea[t*t], tileb[t*t]; float a = A[oi+i+j*N], b = B[i+(j+oj)*N]; c = ; for (int k = ; k<n; k+=t) { tilea[i+j*t] = a ; tileb[i+j*t] = b ; syncthreads(); a = A[oi+i+(j+k)*N] ; b = B[i+k+(j+oj)*N] ; } for(int k = ; k<t; ++k) c += tilea[i+k*t] * tileb[k+j*t] ;
Parallel prefix-sum
88 Parallel prefix sum Prefix sum entrée un tableau A[i], i=..n- en sortie un tableau B, tq : B[i] = A[] + + A[i-] = réduction de chacun des préfixes (autres opérateurs : min, max, etc.) input prefix sum 7 5 6 9 9 6 algo séquentiel trivial version parallèle : plus complexe! implémentation disponible dans le SDK de Cuda (thrust) :) Belloch, 99, Prefix Sums and Their Applications
89 Parallel prefix sum algorithme fondamental domaines d'applications : algo de tri : radix sort, quicksort comparaison de chaines de caractères, analyse lexical compacter, générer des données évaluation des polynômes (x, x*x, x*x*x, x*x*x*x,...) opérations sur les arbres histogramme MapReduce etc. Belloch, 99, Prefix Sums and Their Applications
9 Ex. : évaluer un polynôme Passe : évaluer tous les monômes, x, x^, x^, input prefix sum (with products) x x x x x x x x x x^ x^ x^ x^ x^5 x^6 x^7 x^8 x^9 x^ x^6 x^7 x^8 x^9 x^ Passe : multiplications par les coefficients monômes : coeffs : x x^ x^ * * * - Passe : réduction (somme) x^ x^ x^5... -. *. 5 -
MapReduce avancé Version séquentielle for(int i= ; i<n ; ++i) if(p(i)) result = reduce(result, func(data[i])) ; Version parallèle Map appliquer la même fonction func aux données pour lesquels le prédicat P(i) est vrai Parallélisme : thread élément avec P(i)==true Si sous-ensemble non structuré compacter les données via prefix-sum Reduce réduire les résultats du Map à une seule valeur
Branchement dynamique & gestion des données creuses Exemple, on veux appliquer une fonction couteuse uniquement sur quelques éléments d'un tableau répartis de manière aléatoire ex : application d'un filtre sur les discontinuités d'une image, culling, raffinement, etc. Kernel : global void my_kernel(...) { int id = blockidx.x*blockdim.x + threadidx.x ; if( is_active(id) ) compute(id) ; } inefficace à cause du SPMD Solution en deux passes: «compacter» les données «actives» nouveau kernel sans «if» if( is_active(id) ) if (id<nb_actives) Comment? «parallel prefix sum» 9
9 Exemple : filtre median input sélection des pixels à traiter (sélection préfix-sum packing) output filtre médian
9 Autres exemples traitements spéciaux sur les silhouettes (ex., Sobel)
Prefix sum pour compacter des données creuses Principe générer un tableau A[i] contenant des et A[i]== donnée #i est active global void selection(int* A) { int id = blockidx.x*blockdim.x + threadidx.x ; A[id] = is_active(id)? : ; } appliquer un préfix sum B[i] B[i] = nombre de données actives précédente à la donnée i = emplacement de la donnée i dans un tableau compact B[N] = nombre de données actives créer un tableau C compact des indices actifs i : global void pack_indices(const int* A, const int* B, int* C) { int id = blockidx.x*blockdim.x + threadidx.x ; if( A[i] ) C[ B[id] ] = id } 95
96 Prefix sum pour compacter des données creuses input points selection mask (A) prefix sum (B) selected indices: (C) 5 6 7 8 9 6 9 appliquer notre calcul en utilisant C[] global void my_kernel(const int* C,...) { int id = blockidx.x*blockdim.x + threadidx.x ; compute( C[id] ) ; } variantes : générer un tableau compact des indices i et/ou compacter directement les données 5
97 Prefix sum pour générer des données Objectif chaque donnée initiale i génère un nombre variable M[i] de nouvelle donnée ex : raffinement de maillage, subdivision, construction d'arbre, etc. un thread par donnée initiale deux problèmes : où écrire les données??? performances dues au SPMD global void generate_data(float* C,...) { int id = blockidx.x*blockdim.x + threadidx.x ; int n = how_many(id) ; for(int i= ; i<n ; ++i) { c[??? ] = generate(id, i) ; } }
98 Prefix sum pour générer des données Solution : où prefix sum SPMD découplage génération des paramètres/évaluation input upsampling levels prefix sum generated parameters 9 9 7 non optimal du point de vue SPMD (boucles for non homogènes) mais calculs simples et rapides calculs complexes generated data
99 Exemple Raffinement dynamique d'un nuage de points input points 5 6 7 8 9 selection mask prefix sum selected indices: 6 9 #splats / points 9 9 prefix sum 7 generated splats splats projected onto the smooth surface visible? 5
Raffinement dynamique d'un nuage de points smooth surface - splatting - MLS projection temporal coherence input points - culling - up-sampling
Raffinement dynamique d'un nuage de points Upsampling : ni ri m m LOD :
Raffinement dynamique d'un nuage de points Cohérence temporelle 5 6 selection V[] 6 7 9 B[] 9 6 7 B'[] V[] copy upsample 7 8 9
Exercice : parallel prefix sum reductions partielles V V V V V5 5 V6 V7 7 V V V V6 5 6 7 down-sweeps V
Exercice : parallel prefix sum reductions partielles V V V V V5 5 V6 V7 7 V V V V6 5 6 7 down-sweeps V
5 Exercice : parallel prefix sum
6 Exercice : Quick Sort Rappels : Comment paralléliser ce tri partiel?
7 Autres interfaces de programmations CUDA : C/C++, GPU Nvidia Interfaces : Fortran, Python, Java, MatLab, etc. Bibliothèques : thrust, CuBlas, CuFFT, etc. OpenCL : C, OpenACC : C/Fortran, Générique Compilateur PGI, (implémentation dans gcc en cours) OpenMP Vec a[n] ; Vec b ; float c[n] ; #pragma acc parallel for shared(n,a,b,c) for (i=; i<n; i++) c[i] = a[i].x*b.x + a[i].y*b.y + a[i].z*b.z; #include <stdio.h> #define N int main() { double pi =.f; long i; #pragma acc parallel loop reduction(+:pi) for (i=; i<n; i++) { double t= (double)((i+.5)/n) ; pi +=./(.+t*t); } printf("pi=%6.5f\n",pi/n); return ; }
8 Autres architectures many-cores
9 Nvidia GTX 68 GTX 58 GTX 68 gravure : 8nm 9 cores! 9 ops/cycle warps actifs «super scalaire» ordonnancement statique des opérations déterministes optimisé pour le graphique ~5 cores au total SM : cores fréquence double 6 ops/cycle warps actifs gravure : nm 5 cores au total
Nvidia GTX 68
Radeon 797 Radeon warp size : 6 Evolutions 58 : 5 instructions de front sur 6 éléments (threads, pixels, vertices, etc.) 69 : instructions de front sur 6 éléments (threads, pixels, vertices, etc.) unité SIMD de 6 cores 8 cycles/instruction warps actifs (8 éléments) 8 cycles : 8 éléments traités avec opérations en parallèle 79 : instruction sur 6 éléments (threads, pixels, vertices, etc.) unités SIMD de 6 cores cycles/instruction warps actifs (56 élements) cycles : 56 éléments traités avec seule opération ~ cores au total
Radeon 797 VLIW = Radeon 69 GCN = Radeon 79
Intel Xeon Phi
Intel Xeon Phi
5 Intel Xeon Phi