Mineure HPC & Support au Big Data
Cours de 3ème année à Supélec

BE de programmation CUDA

(parallélisation sur un GPU - part 1)

Stéphane Vialle

Objectifs du BE :

Ce BE 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 Cameron ou Tx de CentraleSupélec :
  • Tx : chaque machine contient un CPU Intel XEON quad-core hyperthreadés, et un GPU NVIDIA RTX2080 (architecture Turing)
  • Cameron : chaque machine contient un CPU Intel XEON hexa-core hyperthreadés, et un GPU NVIDIA GTX1080 (architecture Pascal)
Vous utiliserez les comptes de TP "gpucs1_1" à "gpucs1_20". Depuis votre poste de travail vous vous connecterez par ssh sur la machine ghome.metz.supelec.fr, puis vous vous connecterez à la machine term2.grid.metz.supelec.fr où vous réserverez UN noeud sur l'un des clusters à l'aide des commandes OAR.
  • pour réserver UN noeuds sur Tx pour 4h depuis Term2 : oarsub -q day -p "cluster='Tx'" -l nodes=1,walltime=4:00:00 -I
  • pour réserver UN noeuds sur Cameron pour 4h depuis Term2 : oarsub -q day -p "cluster='Cameron'" -l nodes=1,walltime=4:00:00 -I

Documents à rendre : 

Vous rendrez un SEUL document par binôme : un fichier Word (.doc) ou PDF :
  • Taille maximale de vos rapports : <voir taille maximale annoncée en cours>
  • Date limite de remise de vos rapports : <voir date annoncée en cours>
  • Remise par e-mail à Stephane Vialle (Stephane.Vialle@centralesupelec.fr)

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 servira de calcul de référence. Elle est destiner à 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 seront faibles (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 TRX2080,
    • mais il se peut que vous observiez des différences entre les calculs sur CPU et sur GPU!!
1 - Implantation d'un kernel K0 déployé par une grille "2D" de blocs "1D" :
  1. Récupérez le code source OpenMP+CUDA à compléter :
    • ou bien recopiez le sur votre compte de TP par la commande : cp ~vialle/tmp/MatrixProduct-CUDA-enonce.zip .
Récupérez aussi le fichier Excel de saisi des résultats, et complétez-le au fur et à mesure du TP.

Compilez ce code et testez son fonctionnement sur CPU (commencez par exécuter la commande ./MatrixProduct -h).
  1. Complétez les routines 'gpuSetDataOnGPU' et 'gpuGetResultOnCPU'qui réalisent des transferts CPU/GPU. Vous devez compléter ces routines avec des appels à 'cudaMemcpyFromSymbol' et 'cudaMemcpyToSymbol'.
Toutes les routines à compléter se trouvent dans le fichier 'gpu.cu', et la taille des matrices et des blocs sont définies dans le fichier 'main.h'.
  1. Implantez le kernel K0 et sa grille "2D" de blocs "1D" de threads, pour que :
  • chaque thread calcule un élément complet de la matrice C (matrice résultat),
  • un bloc de threads soit un segment 1D de threads selon la dimension X des blocs,
  • les colonnes successives d'une ligne de C soient calculées par des threads consécutifs en X au sein d'un bloc,
  • les lignes successives de C seront traitées par des blocs 1D différents.
Testez votre implantation sur une matrice de 1024x1024 DOUBLE (option -DDP active dans le Makefile), et vérifiez que vous obtenez les mêmes valeurs que sur CPU (MatrixProduct -t CPU -cpu-k 0 -cpu-nt 8 sur Tx et -cpu-nt 12 sur Cameron)
  1. Puis completez le kernel K0 (et sa grille de blocs de threads) pour que votre implantation fonctionne sur une matrice de taille non multiple de la taille des blocs.
Testez votre kernel K0 sur une matrice de 1025x1025 DOUBLE (vérifiez que vous obtenez les mêmes valeurs que sur CPUs).
  1. Une fois votre kernel K0 au point, mesurez les performances obtenues sur une matrice de 4096x4096 FLOAT éléments (#-DDP dans le Makefile).
  • 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 à 4 threads.
  • Est-ce que la courbe de performance obtenue semble conforme à la théorie ? pourquoi ?
  1. Comparez aux meilleurs 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 :
  1. En ayant utilisé K0 avec les matrices A et B, quelle était :
    • la coalescence des lectures de A,
    • la coalescence des lectures de B,
    • la coalescence des écritures dans C
  1. En adaptant K0 pour utiliser des matrices transposées :
    • quelle serait la coalescence en utilisant la transposée de A ?
    • quelle serait la colaescence en utilisant la transposée de B ?
  1. En adaptant K0 pour que chaque bloc traite des lignes successives d'une même colonne de C.
    • en utilisant A et B ?
    • en utilisant la transposée de A ?
    • en utilisant la transposée de B ?
    • Faites les tests en modifiant rapidement K0 :
      • intervertisez simplement les calculs des numéros de ligne et de colonne du thread dans K0,
      • continuez à utiliser A et B,
      • la performance évolue-t-elle dans le sens prévu ?

3- Implantation d'un kernel K1 utilisant une grille "2D" de blocs et kernels "2D" :
  1. Créez le kernel K1 en généralisant votre kernel K0 (et sa grille de blocs) pour qu'il supporte des blocs "2D" de threads. 
Testez votre implantation sur une matrice de 1024x1024 éléments, puis sur une matrice de 1025x1025 éléments : vérifiez que vous obtenez les mêmes valeurs qu'avec le kernel K0 (attention : la taille maximale d'un bloc est de 1024 threads).
  1. Une fois que votre programme est au point, mesurez les performances obtenues sur une matrice de 4096x4096 FLOAT.
Mesurez les performances obtenues avec des blocs "XxY" pour des tailles correspondant en priorité aux cases jaunes du fichier Excel.
  1. Comparez ces performances à celles obtenues avec des blocs 1D du même nombre total de threads, comparez aux meilleurs performances obtenues sur CPU multi-coeurs en OpenMP, et calculez le speedup GPU vs CPU.