Programmation Graphique Haute Performance Initiation `a

Transcription

Programmation Graphique Haute Performance Initiation `a
Programmation Graphique Haute Performance
Initiation à la programmation CUDA
March 23, 2012
L’objectif de ce TD est de s’initier à la programmation CUDA au travers de petits exercices de
traitement d’images.
Les guides de programmation et de référence CUDA ce trouvent dans le répertoire /opt/local/cuda/doc/:
• CUDA_C_Programming_Guide.pdf
• CUDA_Toolkit_Reference_Manual.pdf
1
Prise en main
Bien qu’il soit possible de faire cohabiter OpenGL et CUDA au sein d’une même application, pour des
raisons de simplicité nous commencerons ce TD avec une base de code indépendante. Télécharger et
décompresser l’archive associée. Le projet contient:
• un répertoire cmake/ contenant des scripts facilitant la compilation des fichier .cu par nvcc et gcc
avec cmake,
• un fichier CMakeLists.txt configurant CUDA et l’exécutable à générer,
• un répertoire data/ contenant quelques images pour les tests,
• le répertoire des fichiers sourcessrc/.
Comme habituellement, créez un répertoire de build:
$ mkdir build-cuda
$ cd build-cuda
configurez le avec la commande cmake suivante et compilez avec make:
$ CC=gcc-4.4 CXX=g++-4.4 cmake -DCUDA_INSTALL_PREFIX=/opt/local/cuda -DFOUND_CUDART=/opt/local/cud
$ make
Cela crée un exécutable imgfilter prenant deux arguments, un fichier image source et un fichier image
de destination. Pour tester:
$ ./imgfilter chemin_vers_td_cuda/data/lena.png lena_bin.png
Si des messages d’erreurs apparaissent, lacez la commande glxinfo afin d’activer le GPU (parfois
nécessaire après un redémarrage du PC).
Pour l’instant, l’application réalise les opérations suivantes:
• Chargement de l’image source avec Qt via une QImage (main.cpp, fonction main()).
• L’image est convertie en niveau de gris et stockée sous la forme d’un tableau de float via la classe
FloatGrayImage (FloatImage.h). La conversion en une image en niveau de gris permet de simplifier
l’écriture des filtres. Les valeurs des pixels vont de 0 (noir) à 1 (blanc).
1
• Cette image est ensuite traitée par la fonction binarize_cuda() définie dans le fichier (binarize.cu).
• Cette fonction alloue un buffer d_img sur le GPU, copie le tableau de pixels de l’image dans ce
buffer puis applique le kernel binarize_kernel() sur chacun des pixels de l’image.
• Ce kernel est actuellement vide et fera l’objet du prochain exercice.
• Le contenue du buffer d_img est ensuite copié dans l’image, et la mémoire GPU est libérée.
• A la fin de la fonction main(), l’image est convertie en une QImage puis sauvegardée sur disque.
Comme actuellement le kernel est vide, le contenue de l’image n’est pas modifié. Le résultat est donc
une version en niveau de gris de l’image d’entrée.
2
Binarisation
Pour ce premier exercice, vous devrez compléter la fonction binarize_kernel() afin de retourner une
image noir (0) et blanc (1) en utilisant le seuil threshold. Compilez et testez.
Testez avec une image haute résolution (ex: data/highres_img1.jpg), et comparez les performances
avec différente tailles de bloc (ex: 1 × 256, 8 × 8, 16 × 16, 256 × 1). Quelle est la configuration la plus
performante? Pourquoi?
3
Application d’un filtre 3×3
L’objectif de ce deuxième exercice est de convoluer l’image en entrée par un filtre discret 3×3. Le
masque d’un filtre passe-bas est fourni dans la fonction main(), et une solution de référence (séquentielle
et C++) vous est fournie dans le fichier main.cpp. Vous devrez mettre en oeuvre une version parallèle
avec CUDA. Pour cela, une ébauche de code vous est fourni dans le fichier convolution.cu. Pour des
raison de performance, le filtre 3×3 sera transféré au kernel via la mémoire constante. Comme expliqué
dans les commentaires du fichier convolution.cu, cela nécessite de passer par une variable globale déclarée
avec __constant__. Les valeurs du filtre sont copiée avec la fonction cudaMemcpyToSymbol. Dans un
premier temps, vous ferez l’hypothèse que le filtre n’est appliqué qu’une seule fois (n=1). Une fois que
cette première étape est validée, vous adapterez votre code afin d’appliquer le filtre un nombre arbitraire
de filtre. Pour cela, vous devrez mettre en oeuvre une méthode de ping-pong où les images d’entrée et
sortie du kernel sont alternée à chaque passe. En principe vous n’avez pas à changer le code du kernel,
uniquement le code CUDA appelant les cernes.
En utilisant une des une images haute résolution, comparez les performance entre le code CUDA et
le code séquentiel.
4
Etalage de la dynamique
L’objectif de ce troisième exercice est de mettre en oeuvre un filtre d’étalage de la dynamique d’une
image. Le principe est de calculer les valeurs minimal et maximal de l’image puis d’appliquer une
fonction linéaire de telle sorte que les valeurs des pixels s’étendent de 0 à 1.
Dans un premier temps vous utiliserez la fonction extract_minmax_cpu() (fichier dynamic.cu) pour
calculer les valeurs minimal et maximal. Votre travail consiste donc à implémenter avec CUDA l’application
de la fonction linéaire d’étalage de la dynamique. Vous vous appuierez sur l’ébauche de code du fichier
dynamic.cu et sur ce que vous avez fait pour la binarisation.
Testez avec l’image data/lena_lowcontrast.png.
Une fois que cette première étape fonctionne, vous implémenterez une version parallèle CUDA de
l’extraction des valeurs minimal et maximal. Vous implémenterez une version simple réalisant log(n)
passes et un mécanisme de ping-pong comme pour la convolution.
Testez et comparez les performances avec la version CPU.
2
5
Réduction rapide avec la mémoire partagée
L’objectif de ce dernier exercice est d’accélérer l’extraction des valeurs minimal et maximal en exploitant
la mémoire partagée. Vous vous appuierez sur la méthode vue en cours. Ne modifiez pas directement le
code du kernel précédent mais implémentez cette variante dans un autre kernel.
3