Travaux pratiques d'Informatique Parallèle et distribuée, UMONS/Polytech, TP 3 1
3.Programmation des processeurs graphiques avec l'architecture de
programmation parallèle CUDA
3.1.Objectifs
Les objectifs de la séance sont de:
se familiariser avec la syntaxe de CUDA
se familiariser avec le compilateur nvcc
réaliser des programmes tournant sur les processeurs graphiques
3.2.Généralités
CUDA (Compute Unified Device Architecture) est une architecture de programmation parallèle
développée par NVIDIA. Elle étend le langage C avec des fonctionnalités permettant la programmation
sur processeur graphique.
La compilation d'un code CUDA nécessite l'emploi du compilateur nvcc développé par
NVIDIA. Ce compilateur s'utilise de la même manière que gcc :
nvcc code.cu -o executable
3.3.Déroulement de l'exécution
Un programme tournant sur GPU s'exécute en 5
étapes :
1. La mémoire dédiée au CPU (mémoire hôte) et la
mémoire dédiée au GPU (mémoire device) étant
distinctes, les données utilisées sur le processeur
graphique doivent être transférées dans la mémoire
dédiée à ce dernier.
2. Le CPU demande au GPU d'exécuter le programme
(le kernel) et spécifie le nombre de threads à lancer.
3. Au cours du traitement, les données sont chargées
depuis la mémoire device dans les registres des
ALUs ou dans les autres mémoires (partagée,
constante, …) des multiprocesseurs.
4. Les résultats du calcul sont placés dans la mémoire
device.
5. Les résultats sont transférés dans la mémoire hôte.
1
UMONS/Polytech
Pierre Manneback, Sébastien Frémal, Sidi Ahmed Mahmoudi 2014-15
Travaux pratiques d'Informatique Parallèle et distribuée, UMONS/Polytech, TP 3 2
3.4.Programmation
a) Sélection du processeur graphique :
La sélection du GPU utilisé par le programme s'effectuer à l'aide de la fonction
cudaSetDevice :
cudaError_t cudaSetDevice (int device)
Cette fonction indique au programme qu'il doit utiliser le GPU d'identifiant device. S'il y a n
GPU sur une machine, device peut valoir une valeur être comprise entre 0 et n-1.
b)Allocation en mémoire device et transfert des données CPU => GPU
L'allocation de la mémoire s'exécute à l'aide de la fonction cudaMalloc :
cudaError_t cudaMalloc (void ** devPtr, size_t size)
Cette fonction réserve, dans la mémoire device, un espace mémoire de taille size dont l'adresse
est contenue dans le pointeur devPtr.
Le transfert des données s'exécute via la fonction cudaMemcpy :
cudaError_t cudaMemcpy (void * dst, const void * src, size_t count,
enum cudaMemcpyKind kind)
Cette fonction transfert count octets de l'espace mémoire src vers l'espace mémoire dst. La
variable kind doit être mise à :
cudaMemcpyHostToDevice dans le cas d'un transfert CPU ==> GPU
cudaMemcpyDeviceToHost dans le cas d'un transfert GPU ==> CPU
c)Appel du kernel
Le kernel est une fonction destinée à tourner sur GPU. Elle se distingue des fonctions habituelles
par son en-tête précédé par l'un de ces mots-clés :
__global__ : indique que la fonction est appelée depuis le CPU
__device__ : indique que la fonction est appelée depuis le GPU
Lorsque le GPU exécute un kernel, les instructions sont chargées dans les multiprocesseurs et les
ALUs exécutent simultanément la même instruction (mode de fonctionnement SIMD – Single Instruction
Multiple Data). Le nombre de thread s'exécutant ainsi en parallèle est déterminé par l'utilisateur lors de
l'appel au kernel :
kernel<<<Nombre_de_blocs,Nombre_de_threads_par_bloc>>>(arguments);
Les threads se situent dans des blocs et le nombre total de thread lancé est égal à Nombre_de_blocs*
Nombre_de_threads_par_bloc. Ces dernières variables peuvent soit être des scalaires, soit des
variables de types dim3 :
dim3 var(x,y,z)
Dans ce dernier cas, les threads et les blocs occupent plusieurs dimensions, pouvant ainsi mieux
représenter la répartition des données. Par exemple, l'appel kernel<<<(2,2,1),
(2,2,1)>>>(arguments); crée la grille de threads présentée à la page suivante.
UMONS/Polytech
Pierre Manneback, Sébastien Frémal, Sidi Ahmed Mahmoudi 2014-15
Travaux pratiques d'Informatique Parallèle et distribuée, UMONS/Polytech, TP 3 3
d)Identification des threads
Le même programme s'exécutant sur tous les coeurs, l'identifiant du thread est le moyen unique
de différencier les exécutions. Celui-ci peut-être récupéré grâce aux variables blockDim, blockSize
et threadId. Dans le cas d'une grille de threads en deux dimensions, voici comment situer le thread sur
les deux axes :
int x = blockId.x * blockDim.x + threadId.x;
int y = blockId.y * blockDim.y + threadId.y;
e) Les types de mémoires dédiées au processeur graphique
La gestion de la mémoire doit être assurée par l'utilisateur et il est donc important d'en connaître
les caractéristiques. Cette séance de travaux pratiques exploite trois types de mémoires :
1. La mémoire globale : cette mémoire correspond à la mémoire RAM du processeur graphique et
possède la plus grande latence d'accès aux données (de 400 à 600 cycles d'horloge). C'est là que
les données sont placées par la fonction cudaMemcpy.
2. Les registres : cette mémoire correspond aux registres de travail des ALUs, ce qui en fait la
mémoire qui possède la latence la plus basse (1 cycle d'horloge). Une donnée est placée dans un
registre lorsqu'elle est placée dans une variable déclarée au sein d'un kernel.
3. La mémoire partagée : cette mémoire se trouve dans le SM. Une donnée placée en mémoire
partagée est accessible par tous les threads d'un même bloc. Pour déclarer une variable dans la
mémoire partagée, il suffit de faire précéder sa déclaration au sein du kernel par le mot-clé
__shared__ (Exemple : __shared__ int variable;). Elle possède une latence de
4 cycles d'horloge.
Cette mémoire étant partagée entre plusieurs threads, il se peut qu'une synchronisation soit
nécessaire afin de s'assurer que les données soient bien écrites en mémoire. Pour ce faire, il suffit
d'appeler la fonction __syncthreads() qui synchronise les threads d'un même bloc.
Type de mémoire UtilitéTaille Latence (cycles)
Globale Mémoire principale Jusqu'à 4 Go 400 à 600
Registres Propre à chaque thread 8192 * 4 octets 1
Partagée Communications entre
threads
16 ko 4
UMONS/Polytech
Pierre Manneback, Sébastien Frémal, Sidi Ahmed Mahmoudi 2014-15
Travaux pratiques d'Informatique Parallèle et distribuée, UMONS/Polytech, TP 3 4
f) Récupération des résultats et libération de la mémoire
La récupération des résultats sur le CPU s'effectue grâce à la fonction cudaMemcpy, avec la
variable kind mise à cudaMemcpyDeviceToHost.
Lorsque l'espace mémoire allouée sur le GPU peut être libéré, un appel à cudaFree doit être
exécuté :
cudaError_t cudaFree(void * devPtr)
devPtr contient l'adresse mémoire à libérer.
3.5.Optimisation
a)Nombre de threads par multiprocesseur
L'utilisateur crée les threads par bloc et le GPU les exécute sur ses coeurs par lot de 32. Le
nombre de threads par bloc est un moyen d'influencer l'exécution et donc les performances. Voici les
contraintes qui déterminent l'assignement des blocs de threads aux multiprocesseurs :
p*b <= 768 : maximum 768 threads peuvent être attribué à un multiprocesseur
r*p*b <= R : il ne faut pas dépasser la quantité de registres disponibles
s*b <= S : il ne faut pas dépasser la quantité de mémoire partagée disponible
b <= 8 : limite matérielle
p <= 512 : limite matérielle
Avec :
p = nombre de threads par bloc
b = nombre de blocs assignés au multiprocesseur
r = nombre de registres utilisés par threads
R = nombre de registres disponibles pour un multiprocesseur
s = quantité de mémoire partagée utilisée par bloc
S = quantité de mémoire partagée disponible par multiprocesseur
Il est possible de voir la quantité de registres et
la taille de la mémoire partagée utilisés en utilisant
l'option de compilation --ptxas-option=-v.
b)Fusion des accès mémoire
Lorsque des threads adjacents accèdent en mémoire
globale à des espaces adjacents, les accès mémoires
sont fusionnés. Ce mécanisme permet de nettement
diminuer la latence d'accès aux données.
UMONS/Polytech
Pierre Manneback, Sébastien Frémal, Sidi Ahmed Mahmoudi 2014-15
Travaux pratiques d'Informatique Parallèle et distribuée, UMONS/Polytech, TP 3 5
3.6.Exercice
1. Écrire un programme séquentiel qui effectue la multiplication d'un vecteur par une matrice à
nombre de lignes variable.
2. Porter sur processeur graphique le programme conçu au point 1.
3. Observer l'impact du nombre de threads par bloc sur les temps d'exécution.
4. Dérouler la boucle à l'intérieur du kernel.
5. Fusionner les accès à la mémoire globale.
6. Fixer la taille du vecteur à 16 et utiliser la mémoire partagée et les registres.
UMONS/Polytech
Pierre Manneback, Sébastien Frémal, Sidi Ahmed Mahmoudi 2014-15
1 / 5 100%
La catégorie de ce document est-elle correcte?
Merci pour votre participation!

Faire une suggestion

Avez-vous trouvé des erreurs dans linterface ou les textes ? Ou savez-vous comment améliorer linterface utilisateur de StudyLib ? Nhésitez pas à envoyer vos suggestions. Cest très important pour nous !