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