Vous êtes sur la page 1sur 108

Introduction à CUDA C

Amina Guermouche

Télécom SudParis
Premiers pas Les blocks Threads Optimisations

CUDA C

• Basé sur le standard C


• Extension du langage pour la programmation hétérogène
• API pour gérer les GPU, la mémoire, . . .
• 3 niveaux d’abstraction : groupe de threads, mémoire partagée
et une barrière de synchronisation

2 / 75
Premiers pas Les blocks Threads Optimisations

Fonctionnement

CPU (Host) GPU (Device)

int main(int __device__


argc, char ma_fonction_gpu
**argv) ()

3 / 75
Premiers pas Les blocks Threads Optimisations

Fonctionnement

CPU (Host) GPU (Device)

int main(int __global__ __device__


argc, char ma_fonction_interface ma_fonction_gpu
**argv) () ()

3 / 75
Premiers pas Les blocks Threads Optimisations

Fonctionnement

1 Host : Copier les données d’entrée de la mémoire du CPU à la


mémoire du GPU
2 Device : Charger les instructions sur le GPU
3 Device : Copier les données vers la mémoire du CPU

4 / 75
Premiers pas Les blocks Threads Optimisations

Interrogation du GPU
• Quel est le nombre de GPUs disponibles
• Quelle est la taille de la mémoire disponible ?
• Quelles sont les caractéristiques des GPUs ?
cudaDeviceProp prop ;
i n t count ;
c u d a G e t D e v i c e C o u n t (& c o u n t ) ;

f o r ( i n t i = 0 ; i < c o u n t ; i ++)
{
c u d a G e t D e v i c e P r o p e r t i e s (& prop , i ) ;
p r i n t f ( ’ ’ T a i l l e t o t a l de l a memoire g l o b a l e
%l d \n ’ ’ , p r o p . t o t a l G l o b a l M e m ) ;
}

• On peut même choisir le GPU qu’on veut selon des


critères ! ! ! !
cudaChooseDevice(&dev, &prop)
5 / 75
Premiers pas Les blocks Threads Optimisations

Exercice 1

6 / 75
Premiers pas Les blocks Threads Optimisations

Hello, World !

i n t main ( v o i d ) {

p r i n t f ( ’ ’ H e l l o , World ! \ n ’ ’ ) ;
return 0;
}

• Compilation avec nvcc (compilateur NVIDIA)


• nvcc ne se plaint pas s’il n’y a pas de code pour le device

7 / 75
Premiers pas Les blocks Threads Optimisations

Hello, World !

__global__ void kernel (void){}

i n t main ( v o i d ) {
kernel<<<1,1>>>();
p r i n t f ( ’ ’ H e l l o , World ! \ n ’ ’ ) ;
return 0;
}

• Compilation avec nvcc (compilateur NVIDIA)


• nvcc ne se plaint pas s’il n’y a pas de code pour le device

7 / 75
Premiers pas Les blocks Threads Optimisations

Hello, World !
Code du device

__global__ v o i d k e r n e l ( v o i d )

• __global__ indique que :


→ Le code s’exécute sur le device
→ Le code est appelé du host
• La partie device et interface est gérée par le compilateur nvidia
• La partie host par le compilateur C
• La syntaxe est obligatoire
• __global__ ne retourne pas de valeur, JAMAIS
ok on utilise le GPU pour appeler la fonction kernel qui ne fait rien,
super ! ! ! !
8 / 75
Premiers pas Les blocks Threads Optimisations

Et si on faisait faire quelque chose au GPU

__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
∗ c = ∗ a + ∗b ;
}

• add(...) sera appelé du host


• add(...) sera exécutée sur le device
• et la mémoire ?

9 / 75
Premiers pas Les blocks Threads Optimisations

Et si on faisait faire quelque chose au GPU

__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
∗ c = ∗ a + ∗b ;
}

• add(...) sera appelé du host


• add(...) sera exécutée sur le device
• et la mémoire ?
• a, b, et c pointent sur la mémoire du device
• Comment allouer de la mémoire sur le GPU ?
cudaMalloc(), cudaFree(), cudaMemcpy()

9 / 75
Premiers pas Les blocks Threads Optimisations

Et si on faisait faire quelque chose au GPU (1/2)

__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
∗ c = ∗ a + ∗b ;
}
i n t main ( v o i d ) {
int a , b, c ;
i n t ∗gpu_a , ∗gpu_b , ∗gpu_c ;
int size = sizeof ( int ) ;

10 / 75
Premiers pas Les blocks Threads Optimisations

Et si on faisait faire quelque chose au GPU (1/2)

__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
∗ c = ∗ a + ∗b ;
}
i n t main ( v o i d ) {
int a , b, c ;
i n t ∗gpu_a , ∗gpu_b , ∗gpu_c ;
int size = sizeof ( int ) ;
// allocation de l’espace pour le device
cudaMalloc( (void **)&gpu_a, size) ;
cudaMalloc( (void **)&gpu_b, size) ;
cudaMalloc( (void **)&gpu_c, size) ;
a=2 ;
b=7 ;

10 / 75
Premiers pas Les blocks Threads Optimisations

Et si on faisait faire quelque chose au GPU (2/2)


// C o p i e d e s d o n n e e s v e r s l e D e v i c e
cudaMemcpy ( gpu_a , &a , s i z e ,
cudaMemcpyHostToDevice ) ;
cudaMemcpy ( gpu_b , &b , s i z e ,
cudaMemcpyHostToDevice ) ;

add <<< 1 , 1 >>> ( gpu_a , gpu_b , gpu_c ) ;

return 0
}

11 / 75
Premiers pas Les blocks Threads Optimisations

Et si on faisait faire quelque chose au GPU (2/2)


// C o p i e d e s d o n n e e s v e r s l e D e v i c e
cudaMemcpy ( gpu_a , &a , s i z e ,
cudaMemcpyHostToDevice ) ;
cudaMemcpy ( gpu_b , &b , s i z e ,
cudaMemcpyHostToDevice ) ;

add <<< 1 , 1 >>> ( gpu_a , gpu_b , gpu_c ) ;

//Copie du resultat vers Host


cudaMemcpy (&c, gpu_c, size, cudaMemcpyDeviceToHost) ;

return 0
}

11 / 75
Premiers pas Les blocks Threads Optimisations

Et si on faisait faire quelque chose au GPU (2/2)


// C o p i e d e s d o n n e e s v e r s l e D e v i c e
cudaMemcpy ( gpu_a , &a , s i z e ,
cudaMemcpyHostToDevice ) ;
cudaMemcpy ( gpu_b , &b , s i z e ,
cudaMemcpyHostToDevice ) ;

add <<< 1 , 1 >>> ( gpu_a , gpu_b , gpu_c ) ;

//Copie du resultat vers Host


cudaMemcpy (&c, gpu_c, size, cudaMemcpyDeviceToHost) ;

//Liberation de l’espace alloue


cudaFree(gpu_a) ;
cudaFree( gpu_b) ;
cudaFree ( gpu_c) ;

return 0
}

11 / 75
Premiers pas Les blocks Threads Optimisations

Et si on faisait faire quelque chose au GPU (2/2)


// C o p i e d e s d o n n e e s v e r s l e D e v i c e
checkCudaErrors(cudaMemcpy ( gpu_a , &a , s i z e ,
cudaMemcpyHostToDevice ) ) ;
cudaMemcpy ( gpu_b , &b , s i z e ,
cudaMemcpyHostToDevice ) ;

add <<< 1 , 1 >>> ( gpu_a , gpu_b , gpu_c ) ;

//Copie du resultat vers Host


cudaMemcpy (&c, gpu_c, size, cudaMemcpyDeviceToHost) ;

//Liberation de l’espace alloue


cudaFree(gpu_a) ;
cudaFree( gpu_b) ;
cudaFree ( gpu_c) ;

return 0
}

11 / 75
Premiers pas Les blocks Threads Optimisations

addition de 2 entiers : super utilisation du parallélisme

• Comment exécuter le code en parallèle ?


On veut faire N fois add en parallèle
add<<<1, 1>>> (gpu_a, gpu_b, gpu_c)

12 / 75
Premiers pas Les blocks Threads Optimisations

addition de 2 entiers : super utilisation du parallélisme

• Comment exécuter le code en parallèle ?


On veut faire N fois add en parallèle
add<<<1, 1>>> (gpu_a, gpu_b, gpu_c)

add<<<N, 1>>> (gpu_a, gpu_b, gpu_c)

12 / 75
Premiers pas Les blocks Threads Optimisations

addition de 2 entiers : super utilisation du parallélisme

• Comment exécuter le code en parallèle ?


On veut faire N fois add en parallèle
add<<<1, 1>>> (gpu_a, gpu_b, gpu_c)

add<<<N, 1>>> (gpu_a, gpu_b, gpu_c)


• Dans ce cas, autant faire un add sur un vecteur

a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7]

+ + + + + + + +

b[0] b[1] b[2] b[3] b[4] b[5] b[6] b[7] • Comment sont exprimés les indices sur
le GPU ?
c[0] c[1] c[2] c[3] c[4] c[5] c[6] c[7]

12 / 75
Premiers pas Les blocks Threads Optimisations

Programmation parallèle en CUDA


• Chaque appel parallèle à add(...) est appelé block
• L’accès à un block donné se fait via blockIdx.x
• Chaque blockIdx.x référence un élément du tableau

__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
c [ blockIdx.x ] = a [ blockIdx.x ] + b [ blockIdx.x ] ;
}

13 / 75
Premiers pas Les blocks Threads Optimisations

Programmation parallèle en CUDA


• Chaque appel parallèle à add(...) est appelé block
• L’accès à un block donné se fait via blockIdx.x
• Chaque blockIdx.x référence un élément du tableau

__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
c [ blockIdx.x ] = a [ blockIdx.x ] + b [ blockIdx.x ] ;
}

Block0 Block1

c[0] = a[0] + b[0] c[1] = a[1] + b[1]

Block2 Block3

c[2] = a[2] + b[2] c[3] = a[3] + b[3]


13 / 75
Premiers pas Les blocks Threads Optimisations

Programmation parallèle en CUDA : le main

#d e f i n e N 512 // nombre d ’ e l e m e n t s du t a b l e a u
i n t main ( v o i d ) {
i n t ∗a , ∗b , ∗ c ;
i n t ∗gpu_a , ∗gpu_b , ∗gpu_c ;
int size = N ∗ sizeof ( int ) ;
// a l l o c a t i o n de l ’ e s p a c e p o u r l e d e v i c e }
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_a , s i z e ) ;
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_b , s i z e ) ;
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_c , s i z e ) ;

a=( i n t ∗ ) m a l l o c ( s i z e ) ;
b=( i n t ∗ ) m a l l o c ( s i z e ) ;
r a n d o m _ i n t s ( a , N) ;
r a n d o m _ i n t s ( b , N) ;

14 / 75
Premiers pas Les blocks Threads Optimisations

Programmation parallèle en CUDA : le main

// C o p i e d e s d o n n e e s v e r s l e D e v i c e
cudaMemcpy ( gpu_a , a , s i z e , cudaMemcpyHostToDevice ) ;
cudaMemcpy ( gpu_b , b , s i z e , cudaMemcpyHostToDevice ) ;

add <<< N, 1 >>> ( gpu_a , gpu_b , gpu_c ) ;

// C o p i e du r e s u l t a t
cudaMemcpy ( c , gpu_c , s i z e , cudaMemcpyDeviceToHost ) ;

free (a) ; free (b) ; free (c) ;


c u d a F r e e ( gpu_a ) ;
c u d a F r e e ( gpu_b ) ;
c u d a F r e e ( gpu_c ) ;
return 0;
}

15 / 75
Premiers pas Les blocks Threads Optimisations

Et si on avait un vecteur à 2 dimensions (une matrice


donc) ?

• Le nombre de blocks lancés représentent une grille (grid )


• Le nombre de blocks par dimension est limité
(maxGridSize[3])
• blockIdx.x, blockIdx.y, blockIdx.z
• dim3 grid(DIM, DIM) initialise la variable grid de type dim3
qui indique la dimension de la grille (2D)
• gridDim.x, gridDim.y donnent la dimension de la grille

16 / 75
Premiers pas Les blocks Threads Optimisations

Grille et blocks

gridDim.x

gridDim.y Block0 Block1

Block2 Block3

17 / 75
Premiers pas Les blocks Threads Optimisations

Addition de deux matrices


#d e f i n e N 512 // t a i l l e d ’ une d i m e n s i o n de l a
matrice

__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
int x = blockIdx . x ;
int y = blockIdx . y ;
i n t i n d i c e = x + y ∗ gridDim . x ;
c [ indice ] = a[ indice ] + b[ indice ];
}

i n t main ( v o i d ) {
i n t ∗a , ∗b , ∗ c ;
i n t ∗gpu_a , ∗gpu_b , ∗gpu_c ;
int size = N ∗ sizeof ( int ) ;
...
dim3 g r i d (N, N) ;
add <<<g r i d , 1 >>> ( dev_a , dev_b , dev_c ) ;
...
}
18 / 75
Premiers pas Les blocks Threads Optimisations

Exercice 2

19 / 75
Premiers pas Les blocks Threads Optimisations

Threads

• Un block peut être divisé en plusieurs threads parallèles

• CUDA définit un unique id par thread threadIdx.x


• On utilise threadIdx.x au lieu de blockIdx.x
__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
c [ threadIdx.x ] = a [ threadIdx.x ] + b [ threadIdx.x ] ;
}

20 / 75
Premiers pas Les blocks Threads Optimisations

Programmation parallèle en CUDA : le main

#d e f i n e N 512 // nombre d ’ e l e m e n t s du t a b l e a u
i n t main ( v o i d ) {
i n t ∗a , ∗b , ∗ c ;
i n t ∗gpu_a , ∗gpu_b , ∗gpu_c ;
int size = N ∗ sizeof ( int ) ;
// a l l o c a t i o n de l ’ e s p a c e p o u r l e d e v i c e }
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_a , s i z e ) ;
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_b , s i z e ) ;
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_c , s i z e ) ;

a=( i n t ∗ ) m a l l o c ( s i z e ) ;
b=( i n t ∗ ) m a l l o c ( s i z e ) ;
r a n d o m _ i n t s ( a , N) ;
r a n d o m _ i n t s ( b , N) ;

21 / 75
Premiers pas Les blocks Threads Optimisations

Programmation parallèle en CUDA : le main

// C o p i e d e s d o n n e e s v e r s l e D e v i c e
cudaMemcpy ( gpu_a , a , s i z e , cudaMemcpyHostToDevice ) ;
cudaMemcpy ( gpu_b , b , s i z e , cudaMemcpyHostToDevice ) ;

// Lancement de l ’ o p e r a t i o n a v e c N t h r e a d s
add <<< 1 , N >>> ( gpu_a , gpu_b , gpu_c ) ;

// C o p i e du r e s u l t a t
cudaMemcpy ( c , gpu_c , s i z e , cudaMemcpyDeviceToHost ) ;

free (a) ; free (b) ; free (c) ;


c u d a F r e e ( gpu_a ) ;
c u d a F r e e ( gpu_b ) ;
c u d a F r e e ( gpu_c ) ;
return 0;
}

22 / 75
Premiers pas Les blocks Threads Optimisations

Des blocks et des threads

• Les threads sont numérotés de 0 à nb_thread par block


• threadIdx.x est par block
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
blockIdx.x=0 blockIdx.x=1 blockIdx.x=2 blockIdx.x=3

• Si on a M threads/block, l’indice dans un vecteur est calculé


par :
indice = threadIdx.x + blockIdx.x * M
• Le nombre de threads par block est donné par la variable
blockDim.x
indice = threadIdx.x + blockIdx.x * blockDim.x

23 / 75
Premiers pas Les blocks Threads Optimisations

add avec blocks et threads


#d e f i n e N ( 2 0 4 8 ∗ 2 0 4 8 ) // t a i l l e du t a b l e a u
#d e f i n e THREAD_PER_BLOCK 512 // nombre de t h r e a d s

__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
i n t i n d i c e = t h r e a d I d x . x + b l o c k I d x . x ∗ blockDim .
x;
c [ indice ] = a[ indice ] + b[ indice ];
}

i n t main ( v o i d ) {
i n t ∗a , ∗b , ∗ c ;
i n t ∗gpu_a , ∗gpu_b , ∗gpu_c ;
int size = N ∗ sizeof ( int ) ;
// a l l o c a t i o n de l ’ e s p a c e p o u r l e d e v i c e }
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_a , s i z e ) ;
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_b , s i z e ) ;
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_c , s i z e ) ;

24 / 75
Premiers pas Les blocks Threads Optimisations

Programmation parallèle en CUDA : le main

a=( i n t ∗ ) m a l l o c ( s i z e ) ;
b=( i n t ∗ ) m a l l o c ( s i z e ) ;
ra n d o m _ i n t s ( a , N) ;
ra n d o m _ i n t s ( b , N) ;

// C o p i e d e s d o n n e e s v e r s l e D e v i c e
cudaMemcpy ( gpu_a , a , s i z e , cudaMemcpyHostToDevice ) ;
cudaMemcpy ( gpu_b , b , s i z e , cudaMemcpyHostToDevice ) ;

// Lancement de l ’ o p e r a t i o n a v e c THREAD_PER_BLOCK
par block
add <<< N/THREAD_PER_BLOCK , THREAD_PER_BLOCK >>>
( gpu_a , gpu_b , gpu_c ) ;

// C o p i e du r e s u l t a t
cudaMemcpy ( c , gpu_c , s i z e , cudaMemcpyDeviceToHost ) ;

25 / 75
Premiers pas Les blocks Threads Optimisations

Programmation parallèle en CUDA : le main

free (a) ; free (b) ; free (c) ;


c u d a F r e e ( gpu_a ) ;
c u d a F r e e ( gpu_b ) ;
c u d a F r e e ( gpu_c ) ;
return 0;
}

26 / 75
Premiers pas Les blocks Threads Optimisations

Exercice 3

27 / 75
Premiers pas Les blocks Threads Optimisations

Vecteurs de taille quelconque

• Vecteur de taille non multiple de blockDim.x


__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ,
int n){
int indice = threadIdx . x + blockIdx . x ∗
blockDim . x ;
if (indice < n)
c [ indice ] = a[ indice ] + b[ indice ];
}

• Au niveau du main
add <<< ( N+THREAD_PER_BLOCK-1 ) /
THREAD_PER_BLOCK, THREAD_PER_BLOCK >>> (
gpu_a , gpu_b , gpu_c , N) ;

28 / 75
Premiers pas Les blocks Threads Optimisations

Des blocks et des threads

Block(0,0) Block(1,0) Block(2,0)

Block(0,1) Block(0,2 Block(0,3)

thread(0,0) thread(1,0) thread(2,0) thread(3,0)

thread(0,1) thread(1,1) thread(2,1) thread(3,1)

thread(0,2) thread(1,2) thread(2,2) thread(2,2)

29 / 75
Premiers pas Les blocks Threads Optimisations

Des blocks et des threads

blockIdx.x

threadIdx.x

blockIdx.y threadIdx.y

30 / 75
Premiers pas Les blocks Threads Optimisations

Des blocks et des threads

blockIdx.x

threadIdx.x

blockIdx.y threadIdx.y

30 / 75
Premiers pas Les blocks Threads Optimisations

Des blocks et des threads

blockIdx.x

threadIdx.x

blockIdx.y threadIdx.y

blockIdx.x*blockDim.x threadIdx.x

30 / 75
Premiers pas Les blocks Threads Optimisations

Des blocks et des threads

blockIdx.x

blockIdx.y*blockDim.y
threadIdx.x

threadIdx.y

blockIdx.y threadIdx.y

blockIdx.x*blockDim.x threadIdx.x

30 / 75
Premiers pas Les blocks Threads Optimisations

Addition de 2 matrices

• colonne = blockIdx.x*blockDim.x+theadIdx.x
• ligne = blockIdx.y*blockDim.y+theadIdx.y
blockIdx.x

blockIdx.y*blockDim.y
threadIdx.x

threadIdx.y

blockIdx.y threadIdx.y

blockIdx.x*blockDim.x threadIdx.x

31 / 75
Premiers pas Les blocks Threads Optimisations

Addition de deux matrices


#d e f i n e N 2048 // t a i l l e de l a m a t r i c e
#d e f i n e THREAD_PER_BLOCK 512 // nombre de t h r e a d s

__global__ v o i d add ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
i n t c o l o n n e = b l o c k I d x . x ∗ blockDim . x+t h e a d I d x . x ;
i n t l i g n e = b l o c k I d x . y ∗ blockDim . y+t h e a d I d x . y ;
i n t i n d i c e = l i g n e ∗ N + colonne ;
// N = g r i d D i m . x ∗ blockDim . x ;
c [ indice ] = a[ indice ] + b[ indice ];
}

i n t main ( i n t a r g c , c h a r ∗∗ a r g v )
{
...
dim3 b l o c k (THREAD_PER_BLOCK, THREAD_PER_BLOCK) ;
dim3 g r i d (N/ blockDim . x , N/ blockDim . y ) ;
add <<<g r i d , b l o c k >>> ( dev_a , dev_b , dev_c ) ;
...
}
32 / 75
Premiers pas Les blocks Threads Optimisations

Pourquoi s’embêter avec des threads et des blocks ?

• 1 thread = 1 coeur
• 1 block = 1SM
• 1 block est exécuté sur 1SM
• Blocks :
• Les blocks sont exécutés dans n’importe quel ordre,
séquentiellement ou en parallèle
• L’avantage est que ça scale automatiquement avec le nombre
de SM

Block0 Block1
Block0 Block1 Block2 Block3
Block2 Block3

33 / 75
Premiers pas Les blocks Threads Optimisations

Pourquoi s’embêter avec des threads et des blocks ?

• 1 thread = 1 coeur
• 1 block = 1SM
• 1 block est exécuté sur 1SM
• Blocks :
• Les blocks sont exécutés dans n’importe quel ordre,
séquentiellement ou en parallèle
• L’avantage est que ça scale automatiquement avec le nombre
de SM
• Threads
• Contrairement aux blocks, les threads peuvent
• Communiquer
• Se synchroniser
• Ces opérations sont à l’intérieur d’un block

33 / 75
Premiers pas Les blocks Threads Optimisations

Les paramètres du kernel

• Les blocks :
• Le nombre de blocks doit être supérieur au nombre de SM
(pour que tous travaillent)
• Il devrait y avoir plusieurs blocks par SM, afin que d’autres
blocks s’exécutent pendant une synchronisation
→ Si une synchronisation est utilisée, il vaut mieux utiliser
plusieurs petits blocks qu’un grand
• Les threads :
• Les SM ordonnancent les threads par groupe SIMD de 32
(warp) sur Quadro 620
• Les threads d’un warp sont synchronisés

→ Les threads dans un block sont exécutés par groupe de 32


• Un SM peut exécuter plusieurs blocks de manière concurrente

34 / 75
Premiers pas Les blocks Threads Optimisations

Les paramètres du kernel

• Les blocks :
• Le nombre de blocks doit être supérieur au nombre de SM
(pour que tous travaillent)
• Il devrait y avoir plusieurs blocks par SM, afin que d’autres
blocks s’exécutent pendant une synchronisation
→ Si une synchronisation est utilisée, il vaut mieux utiliser
plusieurs petits blocks qu’un grand
• Les threads :
• Les SM ordonnancent les threads par groupe SIMD de 32
(warp) sur Quadro 620
• Les threads d’un warp sont synchronisés

→ Les threads dans un block sont exécutés par groupe de 32


• Un SM peut exécuter plusieurs blocks de manière concurrente
• Utiliser des blocks de taille multiple de la taille du warp

34 / 75
Premiers pas Les blocks Threads Optimisations

Les instructions de contrôles dans les warps

• Les instructions de contrôle (if, while, for, switch, do) affectent


les performances, car les thread d’un warp vont diverger

• Les différents chemins sont sérialisés

• Lorsque toutes les exécutions sur les différents chemins sont


finies, les threads convergent vers le même chemin

• Les conditions doivent minimiser les divergences


• Par exemple une condition dépendant de
(threadIdx/warp_size)

35 / 75
Premiers pas Les blocks Threads Optimisations

Execice 4

36 / 75
Premiers pas Les blocks Threads Optimisations

Produit scalaire (dot product)

a b
a0 ∗ b0
a1 ∗ b1
c
a2 ∗ b2
+
a3 ∗ b3
a4 ∗ b4
a5 ∗ b5

c = (a0 , a1 , a2 , a3 , a4 , a5 ).(b0 , b1 , b2 , b3 , b4 , b5 )
= a0 b0 + a1 b1 + a2 b2 + a3 b3 + a4 b4 + a5 b5

37 / 75
Premiers pas Les blocks Threads Optimisations

Produit scalaire (dot product) : 1 seul block

__global__ v o i d d o t ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
// c h a q u e t h r e a d c a l c u l e l e p r o d u i t d ’ une p a i r e
i n t temp = a [ t h r e a d I d x . x ] ∗ b [ t h r e a d I d x . x ] ;
}

38 / 75
Premiers pas Les blocks Threads Optimisations

Produit scalaire (dot product) : 1 seul block

__global__ v o i d d o t ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
// c h a q u e t h r e a d c a l c u l e l e p r o d u i t d ’ une p a i r e
i n t temp = a [ t h r e a d I d x . x ] ∗ b [ t h r e a d I d x . x ] ;
}

• Le calcul est local au processus


→ Les variables temp ne sont pas accessibles aux autres processus
→ Mais il faut partager les données pour faire la somme finale

38 / 75
Premiers pas Les blocks Threads Optimisations

Partager les données entre les threads (d’un même block)

• Les threads d’un block partagent une zone mémoire appelée


shared memory
• Caractéristiques
• Extrêmement rapide
• on-chip
• Déclarée avec __shared__
• Des blocks sur le même SM partage la même mémoire
partagée globale. Donc pour une mémoire de 48KB avec N
blocks sur le même SM, chaque block possédera 48/N de
mémoire partagée

39 / 75
Premiers pas Les blocks Threads Optimisations

Produit scalaire (dot product) : 1 seul block

#d e f i n e N 512 // t a i l l e du t a b l e a u
__global__ v o i d d o t ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
__shared__ i n t tmp [ N ]
// c h a q u e t h r e a d c a l c u l e l e p r o d u i t d ’ une p a i r e
i n t temp [ t h r e a d I d x . x ] = a [ t h r e a d I d x . x ] ∗ b [
threadIdx . x ] ;

// Le t h r e a d 0 e f f e c t u e l a somme
i f ( 0 == t h r e a d I d x . x ) {
i n t sum = 0 ;
f o r ( i n t i = 0 ; i < N ; i ++)
sum = sum + temp [ i ] ;
∗ c = sum ;
}
}

40 / 75
Premiers pas Les blocks Threads Optimisations

Synchronisation des threads d’un même block

• Grâce à la fonction __syncthreads()


• Synchronise uniquement les threads d’un même block

41 / 75
Premiers pas Les blocks Threads Optimisations

Produit scalaire (dot product) : 1 seul block


#d e f i n e N 512 // t a i l l e du t a b l e a u
__global__ v o i d d o t ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
__shared__ i n t tmp [ N ]
// c h a q u e t h r e a d c a l c u l e l e p r o d u i t d ’ une p a i r e
temp [ t h r e a d I d x . x ] = a [ t h r e a d I d x . x ] ∗ b [ t h r e a d I d x .
x ];

// S y n c h r o n i s a t i o n p o u r e t r e s u r que t o u t l e s
t h r e a d s ont f i n i
__syncthreads ( ) ;

// Le t h r e a d 0 e f f e c t u e l a somme
i f ( 0 == t h r e a d I d x . x ) {
i n t sum = 0 ;
f o r ( i n t i = 0 ; i < N ; i ++)
sum = sum + temp [ i ] ;
∗ c = sum ;
}
}
42 / 75
Premiers pas Les blocks Threads Optimisations

Produit scalaire (dot product) : 1 seul block

i n t main ( v o i d ) {
i n t ∗a , ∗b , ∗ c ;
i n t ∗gpu_a , ∗gpu_b , ∗gpu_c ;
int size = N ∗ sizeof ( int ) ;
// a l l o c a t i o n de l ’ e s p a c e p o u r l e d e v i c e }
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_a , s i z e ) ;
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_b , s i z e ) ;
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_c , s i z e o f ( i n t ) ) ;

a=( i n t ∗ ) m a l l o c ( s i z e ) ;
b=( i n t ∗ ) m a l l o c ( s i z e ) ;
r a n d o m _ i n t s ( a , N) ;
r a n d o m _ i n t s ( b , N) ;

43 / 75
Premiers pas Les blocks Threads Optimisations

Programmation parallèle en CUDA : le main


// C o p i e d e s d o n n e e s v e r s l e D e v i c e
cudaMemcpy ( gpu_a , a , s i z e , cudaMemcpyHostToDevice ) ;
cudaMemcpy ( gpu_b , b , s i z e , cudaMemcpyHostToDevice ) ;

// Lancement de l ’ o p e r a t i o n a v e c N t h r e a d s e t un
seul block
d o t <<< 1 , N >>> ( gpu_a , gpu_b , gpu_c ) ;

// C o p i e du r e s u l t a t
cudaMemcpy(&c , gpu_c , s i z e o f ( i n t ) ,
cudaMemcpyDeviceToHost ) ;

free (a) ; free (b) ;


c u d a F r e e ( gpu_a ) ;
c u d a F r e e ( gpu_b ) ;
c u d a F r e e ( gpu_c ) ;
return 0;
}

44 / 75
Premiers pas Les blocks Threads Optimisations

Plus de parallélisme : plusieurs blocks

a b

∗ sum
∗ +


∗ c


∗ sum
∗ +


45 / 75
Premiers pas Les blocks Threads Optimisations

Produit scalaire (dot product)


#d e f i n N ( 2 0 4 8 ∗ 2 0 4 8 )
#d e f i n e THREAD_PER_BLOCK 512 // t a i l l e du t a b l e a u
__global__ v o i d d o t ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
__shared__ i n t tmp [THREADS_PER_BLOCK]
// c h a q u e t h r e a d c a l c u l e l e p r o d u i t d ’ une p a i r e
i n t i n d i c e = t h r e a d I d x . x + b l o c k I d x . x ∗ blockDim .
x;
temp [ i n d i c e ] = a [ i n d i c e ] ∗ b [ i n d i c e ] ;

// S y n c h r o n i s a t i o n ( d a n s l e b l o c k )
__syncthreads ( ) ;

// Le t h r e a d 0 e f f e c t u e l a somme
i f ( 0 == t h r e a d I d x . x ) {
i n t sum = 0 ;
f o r ( i n t i = 0 ; i < THREADS_PER_BLOCK ; i ++)
sum = sum + temp [ i ] ;
∗ c = sum ;
}
} 46 / 75
Premiers pas Les blocks Threads Optimisations

Race condition

• c est dans la mémoire globale


• plusieurs thread 0 peuvent y accéder en même temps

47 / 75
Premiers pas Les blocks Threads Optimisations

Race condition

• c est dans la mémoire globale


• plusieurs thread 0 peuvent y accéder en même temps
• Les opérations atomiques :
• Les opération de lecture, modification et écriture sont
ininterruptibles
• Plusieurs opérations atomiques possibles avec CUDA :
• atomicAdd() • atomicInc()
• atomicSub() • atomicDec()
• atomicMin() • atomicExch()
• atomicMax() • atomicCAS()

47 / 75
Premiers pas Les blocks Threads Optimisations

Produit scalaire (dot product)


#d e f i n N ( 2 0 4 8 ∗ 2 0 4 8 )
#d e f i n e THREAD_PER_BLOCK 512 // t a i l l e du t a b l e a u
__global__ v o i d d o t ( i n t ∗a , i n t ∗b , i n t ∗ c ) {
__shared__ i n t tmp [THREADS_PER_BLOCK]
// c h a q u e t h r e a d c a l c u l e l e p r o d u i t d ’ une p a i r e
i n t i n d i c e = t h r e a d I d x . x + b l o c k I d x . x ∗ blockDim .
x;
temp [ i n d i c e ] = a [ i n d i c e ] ∗ b [ i n d i c e ] ;

// S y n c h r o n i s a t i o n ( d a n s l e b l o c k )
__syncthreads ( ) ;

// Le t h r e a d 0 e f f e c t u e l a somme
i f ( 0 == t h r e a d I d x . x ) {
i n t sum = 0 ;
f o r ( i n t i = 0 ; i < THREADS_PER_BLOCK ; i ++)
sum = sum + temp [ i ] ;
atomicAdd ( ∗ c , sum ) ;
}
} 48 / 75
Premiers pas Les blocks Threads Optimisations

Produit scalaire (dot product)

i n t main ( v o i d ) {
i n t ∗a , ∗b , ∗ c ;
i n t ∗gpu_a , ∗gpu_b , ∗gpu_c ;
int size = N ∗ sizeof ( int ) ;
// a l l o c a t i o n de l ’ e s p a c e p o u r l e d e v i c e }
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_a , s i z e ) ;
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_b , s i z e ) ;
c u d a M a l l o c ( ( v o i d ∗ ∗ )&gpu_c , s i z e ) ;

a=( i n t ∗ ) m a l l o c ( s i z e ) ;
b=( i n t ∗ ) m a l l o c ( s i z e ) ;
c=( i n t ∗ ) m a l l o c ( s i z e o f ( i n t ) ) ;
r a n d o m _ i n t s ( a , N) ;
r a n d o m _ i n t s ( b , N) ;

49 / 75
Premiers pas Les blocks Threads Optimisations

Produit scalaire (dot product)


// C o p i e d e s d o n n e e s v e r s l e D e v i c e
cudaMemcpy ( gpu_a , a , s i z e , cudaMemcpyHostToDevice ) ;
cudaMemcpy ( gpu_b , b , s i z e , cudaMemcpyHostToDevice ) ;

// Lancement de l ’ o p e r a t i o n a v e c THREAD_PER_BLOCK
par block
add <<< N/THREAD_PER_BLOCK,THREAD_PER_BLOCK >>> (
gpu_a , gpu_b , gpu_c ) ;

// C o p i e du r e s u l t a t
cudaMemcpy ( c , gpu_c , s i z e , cudaMemcpyDeviceToHost ) ;

free (a) ; free (b) ;


c u d a F r e e ( gpu_a ) ;
c u d a F r e e ( gpu_b ) ;
c u d a F r e e ( gpu_c ) ;
return 0;
}

50 / 75
Premiers pas Les blocks Threads Optimisations

Mesure du temps

• Les transferts de données sont synchrones


• L’appel au kernel ne l’est pas

cudaMemcpy ( d_x , x , N∗ s i z e o f ( f l o a t ) ,
cudaMemcpyHostToDevice ) ;
cudaMemcpy ( d_y , y , N∗ s i z e o f ( f l o a t ) ,
cudaMemcpyHostToDevice ) ;

t 1 = myCPUTimer ( ) ;
s a x p y <<<(N+255) / 2 5 6 , 256>>>(N, 2 . 0 , d_x , d_y ) ;
cudaDeviceSynchronize () ;
t 2 = myCPUTimer ( ) ;

cudaMemcpy ( y , d_y , N∗ s i z e o f ( f l o a t ) ,
cudaMemcpyDeviceToHost ) ;

51 / 75
Premiers pas Les blocks Threads Optimisations

Mesure du temps

• CUDA event API


• Les opérations sont séquentielles sur le GPU

cudaEvent_t s t a r t , s t o p ;
c u d a E v e n t C r e a t e (& s t a r t ) ;
c u d a E v e n t C r e a t e (& s t o p ) ;
...
cudaEventRecord ( s t a r t ) ;
s a x p y <<<(N+255) / 2 5 6 , 256>>>(N, 2 . 0 f , d_x , d_y ) ;
cudaEventRecord ( stop ) ;

c u d a E v e n t S y n c h r o n i z e ( s t o p ) ; // G a r a n t i t que l ’
evenement s ’ e s t e x e c u t e
...
f l o a t milliseconds = 0;
c u d a E v e n t E l a p s e d T i m e (& m i l l i s e c o n d s , s t a r t , s t o p ) ;

52 / 75
Premiers pas Les blocks Threads Optimisations

Exercice 5

53 / 75
Premiers pas Les blocks Threads Optimisations

Les optimisations de base

http:
//docs.nvidia.com/cuda/cuda-c-best-practices-guide/

X Les paramètres du kernel


X Les instructions de contrôle
× Les mémoires

54 / 75
Premiers pas Les blocks Threads Optimisations

Throughput de la mémoire

1 Minimiser les transferts de faible BW


→ Minimiser les transfert Host<->Device

2 Minimiser les transferts mémoire globale<->Device


→ Favoriser la mémoire partagée et les caches
• La mémoire partagée est équivalente à un cache géré par
l’utilisateur

55 / 75
Premiers pas Les blocks Threads Optimisations

La mémoire globale

• Lorsque tous les threads font un load, le hardware détecte si


les threads accèdent à un espace mémoire contigu
• Dans ce cas, le hardware groupe (coalesces) les accès en un
seul accès à différentes location de la DRAM
2e load t0 t1 t2 t3

1er load t0 t1 t2 t3

a(0, 0)a(0, 1)a(0, 2)a(0, 3)a(1, 0)a(1, 1)a(1, 2)a(1, 3)a(2, 0)a(2, 1)a(2, 2)a(2, 3)a(3, 0)a(3, 1)a(3, 2)a(3, 3)

56 / 75
Premiers pas Les blocks Threads Optimisations

La mémoire globale

• Lorsque tous les threads font un load, le hardware détecte si


les threads accèdent à un espace mémoire contigu
• Dans ce cas, le hardware groupe (coalesces) les accès en un
seul accès à différentes location de la DRAM
2e load t0 t1 t2 t3

1er load t0 t1 t2 t3

a(0, 0)a(0, 1)a(0, 2)a(0, 3)a(1, 0)a(1, 1)a(1, 2)a(1, 3)a(2, 0)a(2, 1)a(2, 2)a(2, 3)a(3, 0)a(3, 1)a(3, 2)a(3, 3)

56 / 75
Premiers pas Les blocks Threads Optimisations

La mémoire partagée

• À utiliser pour éviter les accès non alignés

• La mémoire est partagée en modules de taille égale, appelés


bank

• Il y a autant de bank que de threads dans un warp

• L’accès aux bank est simultané

→ Toute lecture/écriture de n adresses dans n bank différents est


simultané

57 / 75
Premiers pas Les blocks Threads Optimisations

La mémoire partagée

Thread0 Bank0 Thread0 Bank0

Thread1 Bank1 Thread1 Bank1

Thread2 Bank2 Thread2 Bank2

Thread3 Bank3 Thread3 Bank3

Thread4 Bank4 Thread4 Bank4

Thread5 Bank5 Thread5 Bank5

Thread6 Bank6 Thread6 Bank6

Thread7 Bank7 Thread7 Bank7

58 / 75
Premiers pas Les blocks Threads Optimisations

Bank conflict

• Si deux accès sont dans différentes adresses du même bank, il


y a conflit
/ L’accès en cas de conflit est sérialisé

• La mémoire partagée est rapide tant qu’il n’y a pas de bank


conflict

• Le hardware divise un accès mémoire avec conflit en autant


d’accès nécessaire pour ne plus avoir de conflit
/ La BW est réduite par un facteur égal au nombre d’accès créés
pour éviter les conflits

59 / 75
Premiers pas Les blocks Threads Optimisations

Bank conflict

Thread0 Bank0 Thread0 Bank0 Thread0 Bank0

Thread1 Bank1 Thread1 Bank1 Thread1 Bank1

Thread2 Bank2 Thread2 Bank2 Thread2 Bank2

Thread3 Bank3 Thread3 Bank3 Thread3 Bank3

Thread4 Bank4 Thread4 Bank4 Thread4 Bank4

Thread5 Bank5 Thread5 Bank5 Thread5 Bank5

Thread6 Bank6 Thread6 Bank6 Thread6 Bank6

Thread7 Bank7 Thread7 Bank7 Thread7 Bank7

60 / 75
Premiers pas Les blocks Threads Optimisations

Exercice 6

61 / 75
Premiers pas Les blocks Threads Optimisations

Multiplication de matrice

• C(0,0) ->
blockIdx.x=0,blockIdx.y=0 B(0,0)
• C(0,1) ->
blockIdx.x=1,blockIdx.y=0
• C(1,0) ->
blockIdx.x=0,blockIdx.y=1 B(1,0)
• C(1,1) ->
blockIdx.x=1,blockIdx.y=1

A(0,0) A(0,1) C(0,0)

62 / 75
Premiers pas Les blocks Threads Optimisations

Multiplication de matrice

• 1 block calcule une tuile


• 1 thread calcule un élément de la B(0,0)
tuile

B(1,0)

A(0,0) A(0,1) C(0,0)

62 / 75
Premiers pas Les blocks Threads Optimisations

Multiplication de matrice

• Les blocks A(0,0) et B(0,0) sont


chargés en mémoire partagée de B(0,0)
façon collaborative par les threads
(chacun charge un élément de
chaque tuile)
• Les threads font le calcul partiel de C
B(1,0)
• Chaque valeur est gardée dans le
registre du thread

A(0,0) A(0,1) C(0,0)

62 / 75
Premiers pas Les blocks Threads Optimisations

Multiplication de matrice

• Les blocks A(0,1) et B(1,0) sont


chargés en mémoire partagée B(0,0)
• Les threads terminent le calcul de C
• À la fin, C est stockée en mémoire
globale
B(1,0)

A(0,0) A(0,1) C(0,0)

62 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : A


• col = blockIdx.x*blockDim.x+theadIdx.x
• ligne = blockIdx.y*blockDim.y+theadIdx.y

blockIdx.x

blockIdx.y*blockDim.y
threadIdx.x

threadIdx.y

blockIdx.y threadIdx.y

blockIdx.x*blockDim.x threadIdx.x

63 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : A


• col = blockIdx.x*blockDim.x+theadIdx.x
• ligne = blockIdx.y*blockDim.y+theadIdx.y
• TILE_WIDTH = blockDim.x
blockIdx.x

donnée
blockIdx.y*blockDim.y
thread

threadIdx.y

blockIdx.y

tx

63 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : A


• col = blockIdx.x*blockDim.x+theadIdx.x
• ligne = blockIdx.y*blockDim.y+theadIdx.y
• TILE_WIDTH = blockDim.x
blockIdx.x

donnée

thread
ligne * nbColumnsA

blockIdx.y

tx

63 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : A


• col = blockIdx.x*blockDim.x+theadIdx.x
• ligne = blockIdx.y*blockDim.y+theadIdx.y
• TILE_WIDTH = blockDim.x
blockIdx.x

donnée

thread
ligne * nbColumnsA

blockIdx.y

tx TILE_WIDTH

63 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : A


• col = blockIdx.x*blockDim.x+theadIdx.x
• ligne = blockIdx.y*blockDim.y+theadIdx.y
• TILE_WIDTH = blockDim.x
blockIdx.x

donnée

thread
ligne * nbColumnsA

blockIdx.y

tx 2*TILE_WIDTH

63 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : A

• col = blockIdx.x*blockDim.x+theadIdx.x
• ligne = blockIdx.y*blockDim.y+theadIdx.y
• TILE_WIDTH = blockDim.x
• indice = ligne * nb_colA + TILE_WIDTH * tileId +
threadIdx.x

64 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : B

donnée threadIdx.y

thread

col

65 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : B


• La matrice est stockée par ligne :
• Les 2 éléments sont séparés par le nombre d’éléments dans une
ligne multiplié par la taille de la tuile
• Une fois dans la bonne tuile, il faut accéder à la bonne ligne, en
n’oubliant pas que le stockage est fait par ligne (ty*nb_col)
donnée threadIdx.y

thread

col

65 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : B


• La matrice est stockée par ligne :
• Les 2 éléments sont séparés par le nombre d’éléments dans une
ligne multiplié par la taille de la tuile
• Une fois dans la bonne tuile, il faut accéder à la bonne ligne, en
n’oubliant pas que le stockage est fait par ligne (ty*nb_col)
donnée threadIdx.y

thread nb_col*TILE_WIDTH

col

65 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : B


• La matrice est stockée par ligne :
• Les 2 éléments sont séparés par le nombre d’éléments dans une
ligne multiplié par la taille de la tuile
• Une fois dans la bonne tuile, il faut accéder à la bonne ligne, en
n’oubliant pas que le stockage est fait par ligne (ty*nb_col)
donnée threadIdx.y

thread nb_col*TILE_WIDTH

nb_col*ty
col

65 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : B


• La matrice est stockée par ligne :
• Les 2 éléments sont séparés par le nombre d’éléments dans une
ligne multiplié par la taille de la tuile
• Une fois dans la bonne tuile, il faut accéder à la bonne ligne, en
n’oubliant pas que le stockage est fait par ligne (ty*nb_col)
donnée threadIdx.y

thread

2*nb_col*TILE_WIDTH
col

nb_col*ty

65 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : B

• col = blockIdx.x*blockDim.x+theadIdx.x
• ligne = blockIdx.y*blockDim.y+theadIdx.y
• TILE_WIDTH = blockDim.x
• indice = col + (nb_colB * TILE_WIDTH * tileId + ty
* nb_col)

66 / 75
Premiers pas Les blocks Threads Optimisations

Les indices dans le produit de matrices par tuile : C


• col = blockIdx.x*blockDim.x+theadIdx.x
• ligne = blockIdx.y*blockDim.y+theadIdx.y
• indice = nbCol * ligne + col
blockIdx.x

nbCol * ligne

blockIdx.y

col

67 / 75
Premiers pas Les blocks Threads Optimisations

Nombre de threads dans les blocks pour le produit de


matrices

• Chaque SM du Quadro K620 possède 48KB de mémoire


partagée par block
• TILE_WIDTH = 16 :
• 2 vecteur de mémoire partagée de float d’une taille de
→ 2 x 4B x 16 x 16 = 2KB pour un block
• Donc jusqu’à 24 blocks pouvant s’exécuter sur le même SM
• TILE_WIDTH = 32
• 2 vecteur de mémoire partagée de float d’une taille de
→ 2 x 4B x 32 x 32 = 8KB pour un block
• Donc jusqu’à 6 blocks pouvant s’exécuter sur le même SM
• Mais dans la vraie vie, on utilise des bibliothèques (cuBlas)

68 / 75
Premiers pas Les blocks Threads Optimisations

Parallélisme de tâches avec des GPU : les streams

69 / 75
Premiers pas Les blocks Threads Optimisations

Les streams

• Un stream : Une séquence d’opérations (comme une file


d’attente pour le device)
• Stream par défaut : stream 0
• Opérations de lecture et d’écriture synchrone (HostToDevice
et DeviceToHost) avec le host
• Les kernels sont asynchrones avec le host par défaut (des
opérations CPU sont possibles)

70 / 75
Premiers pas Les blocks Threads Optimisations

Les streams

• Stream différents du stream 0


• Les opérations sur un même stream sont ordonnées (FIFO) et
syncrhones
• Les opérations dans des streams différents peuvent s’exécuter
en parallèle
• Les opérations entre les différents streams peuvent s’intercaler

71 / 75
Premiers pas Les blocks Threads Optimisations

Exemple d’exécution

Séquentiel Memory Copy (H2D) Kernel Memory Copy (D2H)

temps

Concurrent H2D K1 D2H Gain en performance

H2D K2 D2H

H2D K3 D2H

temps

72 / 75
Premiers pas Les blocks Threads Optimisations

Exemple de code avec streams

cudaStream_T s t r e a m 1 , s t r e a m 2 , s t r e a m 3 , s t r e a m 4 ;
c u d a S t r e a m C r e a t e (& s t r e a m 1 ) ;
...
c u d a M a l l o c (&data_dev1 , s i z e ) ;
c u d a M a l l o c H o s t (& data_host1 , s i z e ) ;
...
cudaMemcpyAsync ( data_dev1 , data_host1 , s i z e ,
cudaMemcpyHostToDevice , s t r e a m 1 ) ;
k e r n e l 2 <<< g r i d , b l o c k , 0 , s t r e a m 2 >>> ( . . . ,
data_dev2 , . . . ) ;
k e r n e l 3 <<< g r i d , b l o c k , 0 , s t r e a m 3 >>> ( . . . ,
data_dev3 , . . . ) ;
cudaMemcpyAsync ( data_host4 , data_dev4 , s i z e ,
cudaMemcpyDeviceToHost , s t r e a m 4 ) ;
...

73 / 75
Premiers pas Les blocks Threads Optimisations

L’ordre d’émission des opérations est important

• Stream 1 : K1a, K1b


• Stream 2 : K2a, K2b
Ordre d’émission

K1a
K1b
K2a
K2b

74 / 75
Premiers pas Les blocks Threads Optimisations

L’ordre d’émission des opérations est important

• Stream 1 : K1a, K1b


• Stream 2 : K2a, K2b
Ordre d’émission

temps

K1a K1a
K1b K1b K2a
K2a K2b
K2b

74 / 75
Premiers pas Les blocks Threads Optimisations

L’ordre d’émission des opérations est important

• Stream 1 : K1a, K1b


• Stream 2 : K2a, K2b
Ordre d’émission

Ordre d’émission
temps

K1a K1a K1a


K1b K1b K2a K2a
K2a K2b K1b
K2b K2b

74 / 75
Premiers pas Les blocks Threads Optimisations

L’ordre d’émission des opérations est important

• Stream 1 : K1a, K1b


• Stream 2 : K2a, K2b
Ordre d’émission

Ordre d’émission
K2a
temps

temps
K1a K1a K1a K1a
K1b K1b K2a K2a K1b K2b
K2a K2b K1b
K2b K2b

74 / 75
Premiers pas Les blocks Threads Optimisations

L’ordre d’émission des opérations est important

• Stream 1 : K1a, K1b


• Stream 2 : K2a, K2b
Ordre d’émission

Ordre d’émission

Ordre d’émission
K2a K1b
temps

temps

temps
K1a K1a K1a K1a K1a K1a
K1b K1b K2a K2a K1b K2a K2a K2b
K2b
K2a K2b K1b K2b
K2b K2b K1b

74 / 75
Premiers pas Les blocks Threads Optimisations

Pour aller plus loin

• MPI + CUDA :
http://on-demand.gputechconf.com/gtc/2014/
presentations/S4236-multi-gpu-programming-mpi.pdf
• Mémoire unifiée CPU + GPU :
http://www.drdobbs.com/parallel/
unified-memory-in-cuda-6-a-brief-overvie/
240169095?pgno=1

75 / 75

Vous aimerez peut-être aussi