TP 3 : Programmation CUDA avec la Shared Memory

5ème année ingénieur de Polytech Paris-Sud

Stéphane Vialle, CentraleSupélec & LISN Stéphane.Vialle@centralesupelec.fr   

Objectifs du TP :

Ce TP a pour objectif de pratiquer la programmation optimisée d'un GPU en utilisant la shared memory de chaque Stream-Multiprocessor. On étudiera notamment la mise au point de synchronisations minimales mais suffisantes des threads (au sein d'un bloc), puis le traitement des boundaries pour la mise au point de kernel génériques.

Plate-forme de développement : 

Les machines utilisées seront celles des clusters Tx ou Cameron du DCE de CentraleSupélec :

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 :

OU BIEN depuis votre poste de travail en mode alphanumérique :

ssh -l 23ppsgpu_i chome.metz.supelec.fr
  1. 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

  1. 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 "2D" et du kernel K2 utilisant la shared memory

  1. 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 ~gpu_vialle/PPS-GPU/MatrixProduct-CUDA-shm-enonce-3.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).
    1. Implantez le kernel K2 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 bloc 2D carré qui exploite la shared memory
    • les accès à la mémoire globale soient coalescents en lecture et en écriture
    • votre programme fonctionne pour des tailles de matrices telles que : SIZE = q.BLOCK_SIZE_XY
  • Testez votre implantation sur une matrice de 4096x4096 FLOAT (#-DDP dans le 'Makefile'), et vérifiez que vous obtenez les mêmes valeurs qu'avec le kernel K1 sur GPU
  1. Mesurez les performances du kernel K2 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 2D carrés de threads, et mesurer les performances obtenues pour des blocs de 1x1 à 32x32 threads.
  • Est-ce que les performances obtenues semblent conforme à la théorie ? pourquoi ?
2 - Calcul du nombre d'accès en mémoire globale
  1. Calculez le nombre d'accès à la mémoire globale (en lecture et en écriture) en utilisant la shared memory (kernel K2), avec des matrices de SIZExSIZE éléments et des blocs de BSXYxBSXY.
  • en comptant les accès demandés par les threads
  • en comptant les accès "en râteau" réalisés par les warps, fonction de la coalescence des accès : un accès en râteau pouvant ramener 32 données (coalescence parfaite) ou seulement quelques données, ou même une seule en cas de non-coalescence ou de coalescence dégénérée.
  1.  En déduire le nombre d'accès à la mémoire globale durant tout le produit de matrices :
  • sous forme d'accès demandés par l'ensemble des threads
  • sous forme d'accès "en râteau" réalisés par l'ensemble des warps
    1. Calculez le gain obtenu en nombre d'accès par rapport au kernel K1 (kernel 2D sans shared memory)
  • en comptant les accès demandés par l'ensemble des threads
  • en comptant les accès "en râteau" réalisés par l'ensemble des warp
  1. Faites l'appliction numérique du gain pour des matrices de 4096x4096 et des blocs de 32x32 eléments.
    • est-ce que les gains de performances mesurés expérimentalement vont dans le sens des gains théoriques en nombre d'accès "en râteau" réalisés par les warps ?
3 - Implantation d'une grille "2D" de blocs "2D" et du kernel K3 utilisant la shared memory pour une utilisation GENERIQUE
  1. Implantez un kernel K3 en généralisant votre kernel K2 pour qu'il s'applique à des matrices de taille quelconque
    • Etudier les différents problèmes de boundaries qui se posent
    • Testez le fonctionnement de votre code sur des matrices de 4096x4096 FLOAT, et comparez les résultats à ceux du kernel K1
    • PUIS testez votre programme sur des matrices de 4097x4097 FLOAT, et comparez les résultats à ceux du kernel K1
  1. Mesurez les performances de votre kernel K3
    • Mesurez les performances sur des matrices de 4096x4096 FLOAT pour des blocs de 32x32
    • Complétez le fichier Excel
    • Comparez les aux performances du kernel K2 : notez-vous un impact des boundaries ?