Objectifs du TP :
Ce TP a pour objectif de pratiquer la programmation d'un GPU au sein d'un noeud de calcul CPU+GPU : il consiste à
implanter un premier produit de matrices denses sur un GPU. On développera différents "kernels", et pour
chacun on mesurera les performances d'un produit de matrices denses sur
un GPU, en fonction de la granularité de la grille de blocs de
threads. On étudiera la qualité de la "coalescence" de chaque version
du kernel, pour identifier les solutions les plus intéressantes. Enfin,
on
comparera les performances obtenues sur GPU avec celle obtenues sur le
CPU.
Plate-forme
de développement :
Les
machines utilisées seront celles des clusters Tx ou Cameron du DCE de
CentraleSupélec :
- Tx : chaque machine contient un CPU Intel XEON quad-core
hyperthreadés, et un GPU NVIDIA GTX-2080Ti (architecture Turing)
- Cameron
: chaque machine contient un CPU Intel XEON hexa-core
hyperthreadés, et un GPU NVIDIA GTX-1080 (architecture Pascal)
L'environnement
CUDA C et C++ est disponible sur chaque machine (et donc le
compilateur "nvcc"
et les drivers pour utiliser le GPU).
Vous utiliserez les
comptes de TP "23ppsgpu_i", où i est une valeur entière
entre 1 et 10.
Depuis votre poste de travail en mode graphique avec dcejs :
- allocation de ressource : demandez 1 machine en mode exclusif (configuration par défaut)
- action : démarrez VNC
OU BIEN depuis votre poste de travail en mode alphanumérique :
- vous vous connecterez
par ssh sur la machine chome.metz.supelec.fr en indiquant le login 23ppsgpu_i
- par un simple terminal (xterm depuis Linux, ou powershell depuis Windows), puis en entrant la commande suivante :
ssh -l 23ppsgpu_i chome.metz.supelec.fr
- une fois un terminal ouvert sur chome.metz.supelec.fr vous réserverez UN nœud sur le cluster à l'aide d'une commande slurm:
- Pendant le TP : srun --reservation=XXX -N 1 --exclusive --pty bash
--reservation=XXX : pré-réservation de machines, de nom XXX (demander ce nom à l'enseignant)
-N 1 : UN noeud
--exclusive : être seul sur le noeud et pouvoir utiliser tous les coeurs CPU
--pty bash : lancer un shell (bash) pour une session interactive
- Après le TP (si besoin) :
- pour obtenir un noeud Tx (GTX 2080 Ti): srun -p gpu_tp -C tx -N 1 --exclusive --pty bash
- pour obtenir un noeud Cameron (GTX 1080) : srun -p gpu_tp -C cam -N 1 --exclusive --pty bash
Travail à effectuer :
Remarques préliminaires :
- Le squelette de programme que vous utiliserez contient un code de produit de matrices denses en OpenMP et CUDA.
- La
partie OpenMP est complète, et est destinée à permettre de vérifier les résultats obtenus en CUDA.
- La partie CUDA est en partie développée, mais il vous reste à compléter le fichier gpu.cu :
- Le squelette est compilable et contient une aide intégrée : exécutez 'make' puis './MatrixProduct -h'.
- Pour valider votre premier code vous compilerez en Double Précision (le type "T_real" devient le type "double") avec "-DDP" dans le Makefile, les résultats seront identiques sur CPU et sur GPU, mais les performances des GPU
s'effondreront (car il s'agit de cartes GPU grand public non adaptées à la Double Précision).
- Pour
faire vos mesures de performances vous compilerez en Simple Précision
(le type "T_real" devient le type "float") avec "#-DDP" dans le
Makefile. La simple précision est adaptée
aux capacités des GeForce GTX1080 et RTX2080, mais il se peut que vous observiez des
différences entre les calculs sur CPU et sur GPU!!
1 - Implantation d'une grille "2D" de blocs "1D" et du kernel K0 :
- Récupérez et compilez le squelette de programme OpenMP+CUDA.
- Squelette, ou bien allez le recopier sur votre compte de TP par la commande :
cp
~vialle/PPS-GPU/MatrixProduct-CUDA-basics-enonce-1.zip .
- Compilez ce squelette et testez son fonctionnement sur CPU (exécutez la commande ./MatrixProduct -h
pour voir les détails de fonctionnement de l'application).
- Dans le fichier 'gpu.cu' complétez les routines de transferts de données : 'gpuSetDataOnGPU' et 'gpuGetResultOnCPU.
- Vous devez compléter ces routines avec des appels à 'cudaMemcpyFromSymbol' et 'cudaMemcpyToSymbol'.
- Compilez et exécutez votre code : vérifiez qu'il ne signale pas d'erreur.
- Implantez le kernel K0 et sa grille de blocs de threads dans le fichier 'gpu.cu' pour que :
- chaque thread calcule un élément complet de la matrice C = AxB,
- un bloc de threads soit un segment 1D selon la dimension X,
- un bloc de threads calcule les éléméments successifs d'une partie d'une ligne de C (les colonnes de C doivent être
associées à la dimension X des blocs),
- les lignes successives de C seront traitées par des blocs 1D différents (les lignes de C doivent être associées à la dimension Y des blocs).
- Testez
votre implantation sur une matrice de
1024x1024 DOUBLE (option -DDP active dans le 'Makefile' et changement de SIZE dans le 'main.h'), et vérifiez que vous obtenez les mêmes valeurs que sur
CPU (MatrixProduct -t CPU -cpu-k 1 -cpu-nt 4, sur Tx, et -cpu-nt 6 sur Cameron)
- Testez votre implantation sur une matrice de 1025x1025DOUBLE (vérifiez que vous obtenez les mêmes valeurs que sur
CPU).
- Mesurez les performances du kernel 0 obtenues sur une matrice de 4096x4096 FLOAT éléments (#-DDP dans le Makefile).
- Récupérez le fichier Excel de saisi des résultats, et complétez-le au fur et à mesure du TP.
- Faites
varier la taille de vos blocs 1D de threads, et mesurer les
performances obtenues pour des blocs de 32 à 1024 threads.
- Puis faites varier la taille des blocs de 32 à 1 thread.
- Est-ce que la courbe de performance obtenue semble conforme à la théorie ? pourquoi ?
- Comparez aux meilleures performances obtenues sur CPU multi-coeurs en OpenMP avec un kernel de même niveau (kernel 0),
- MatrixProduct -t CPU -cpu-k 0 -cpu-nt 8 sur Tx
- MatrixProduct -t CPU -cpu-k 0 -cpu-nt 12 sur Cameron
- Calculez le speedup GPU vs CPU.
2 - Etude de la coalescence du kernel K0 :
Rappel : le kernel K0 se déploie avec
des blocs 1D selon l'axe X, où chaque bloc traite des colonnes
successives d'une même ligne de C.
- Etudier la "coalescence" :
- des lectures de A,
- des lectures de B,
- des écritures dans C
- Que serait devenu la coalescence :
- en utilisant la transposée de A ?
- en utilisant la transposée de B ?
- On considère maintenant des blocs 1D selon l'axe X, MAIS où chaque bloc traite des lignes successives d'une même colonne de C.
- Etudier à nouveau la coalescence obtenue lors des lectures de A ou de
la transposée de A,
- et lors la lecture de B ou de la transposée de B,
- et lors des écritures de
C.
- Quelle est la meilleure solution ?
- Echangez le calcul des numéros de ligne et de colonne traitées
par chaque thread, pour vous mettre dans le cas où un bloc 1D en X
traite des lignes successives d'une même colonne de C.
- Mesurez les
performances obtenues.
- L'évolution des performances est-elle conforme aux prévisions ?
3- Implantation d'une grille "2D" de blocs "2D" et du kernel K1 :
- Créez
le kernel K1 en généralisant votre kernel K0 (et sa grille de
blocs) pour qu'il supporte des blocs 2D (rectangulaires) de threads.
- Testez
et validez votre implantation sur une matrice de
1024x1024 FLOAT : vérifiez que vous obtenez les mêmes valeurs qu'avec le kernel K0.
- Puis sur une matrice de 1025x1025 FLOAT.
Attention : la taille maximale d'un bloc est de 1024 threads !
- Mesurez les performances obtenues sur une matrice de 4096x4096 FLOAT.
- Mesurez
les performances obtenues avec des blocs "XxY" pour des tailles
correspondant aux cases jaunes du fichier Excel.
Notamment pour des
tailles XxY de 8x8, 16x16, 32x32, puis de 32x8, 8x32, et 32x16 et 16x32
threads.
- Comparez les performances du kernel K1 avec :
- les meilleures performances sur GPU du kernel K0,
- les meilleures performances obtenues sur CPU multi-coeurs en OpenMP kernel K0 (le K1 sur CPU est l'équivalent des K4-J7 sur GPU),
- calculez le speedup GPU vs CPU.
- La coalescence est-t-elle toujours respectée (justifiez votre réponse) ?