CONVOLUTION SUR PROCESSEURS GRAPHIQUES]



Documents pareils
Introduction à CUDA.

Segmentation d'images à l'aide d'agents sociaux : applications GPU

Initiation au HPC - Généralités

Hiérarchie matériel dans le monde informatique. Architecture d ordinateur : introduction. Hiérarchie matériel dans le monde informatique

UEO11 COURS/TD 1. nombres entiers et réels codés en mémoire centrale. Caractères alphabétiques et caractères spéciaux.

Contrôle Non Destructif : Implantation d'algorithmes sur GPU et multi-coeurs. Gilles Rougeron CEA/LIST Département Imagerie Simulation et Contrôle

Introduction à MATLAB R

Architecture des ordinateurs

Vision industrielle et télédétection - Détection d ellipses. Guillaume Martinez 17 décembre 2007

Métriques de performance pour les algorithmes et programmes parallèles

IMAGES NUMÉRIQUES MATRICIELLES EN SCILAB

T. Gasc 1,2,3, F. De Vuyst 1, R. Motte 3, M. Peybernes 4, R. Poncet 5

Reconstruction de bâtiments en 3D à partir de nuages de points LIDAR

Info0804. Cours 6. Optimisation combinatoire : Applications et compléments

Une bibliothèque de templates pour CUDA

Limitations of the Playstation 3 for High Performance Cluster Computing

INTRODUCTION A L ELECTRONIQUE NUMERIQUE ECHANTILLONNAGE ET QUANTIFICATION I. ARCHITECTURE DE L ELECRONIQUE NUMERIQUE

Dans l Unité 3, nous avons parlé de la

Une dérivation du paradigme de réécriture de multiensembles pour l'architecture de processeur graphique GPU

Comme chaque ligne de cache a 1024 bits. Le nombre de lignes de cache contenu dans chaque ensemble est:

INITIATION AU LANGAGE C SUR PIC DE MICROSHIP

Calcul multi GPU et optimisation combinatoire

L analyse d images regroupe plusieurs disciplines que l on classe en deux catégories :

. Plan du cours. . Architecture: Fermi ( ), Kepler (12-?)

TP SIN Traitement d image

Architecture des Ordinateurs. Partie II:

M2-Images. Rendu Temps Réel - OpenGL 4 et compute shaders. J.C. Iehl. December 18, 2013

Informatique Générale

Programmation C. Apprendre à développer des programmes simples dans le langage C

Leçon 1 : Les principaux composants d un ordinateur

Codage d information. Codage d information : -Définition-

Tout savoir sur le matériel informatique

nom : Collège Ste Clotilde

Licence Sciences et Technologies Examen janvier 2010

Arithmétique binaire. Chapitre. 5.1 Notions Bit Mot

Synthèse d'images I. Venceslas BIRI IGM Université de Marne La

Génération de code binaire pour application multimedia : une approche au vol

IRL : Simulation distribuée pour les systèmes embarqués

Chapitre 4 : Les mémoires

TP : Gestion d une image au format PGM

Architecture des calculateurs

Représentation des Nombres

Potentiels de la technologie FPGA dans la conception des systèmes. Avantages des FPGAs pour la conception de systèmes optimisés

INTRODUCTION À L ANALYSE FACTORIELLE DES CORRESPONDANCES

Rapport de stage Master 2

TD : Codage des images

Projet Matlab : un logiciel de cryptage

Outil d aide au choix Serveurs Lot 4 Marché Groupement de Recherche

Systèmes de transmission

V- Manipulations de nombres en binaire

Résolution de systèmes linéaires par des méthodes directes

Etude comparative de différents motifs utilisés pour le lancé de rayon

Cours 1 : Introduction Ordinateurs - Langages de haut niveau - Application

IV- Comment fonctionne un ordinateur?

Cours Informatique 1. Monsieur SADOUNI Salheddine

Architecture des Systèmes d Information Architecture des Systèmes d Information

Structure de base d un ordinateur

NOTIONS DE RESEAUX INFORMATIQUES

Chapitre V : La gestion de la mémoire. Hiérarchie de mémoires Objectifs Méthodes d'allocation Simulation de mémoire virtuelle Le mapping

Fiche technique CPU 314SC/DPM (314-6CG13)

White Paper - Livre Blanc

03/04/2007. Tâche 1 Tâche 2 Tâche 3. Système Unix. Time sharing

BeSpoon et l homme Connecté

Chapitre 22 Optimisation pour diffusion à l'écran, pour le web

Sur un ordinateur portable ou un All-in-One tactile, la plupart des éléments mentionnés précédemment sont regroupés. 10) 11)

Introduction aux SGBDR

Hubert & Bruno Lundi 12 octobre 2009 SAINT-QUENTIN (02)

Rapport d activité. Mathieu Souchaud Juin 2007

Cours n 12. Technologies WAN 2nd partie

DE L ALGORITHME AU PROGRAMME INTRO AU LANGAGE C 51

WHITE PAPER. Quels avantages la déduplication offre-t-elle aux entreprises? Livre blanc Acronis

1. Structure d un programme C. 2. Commentaire: /*..texte */ On utilise aussi le commentaire du C++ qui est valable pour C: 3.

Master IMA - UMPC Paris 6 RDMM - Année Fiche de TP


3 Approximation de solutions d équations

Généralités sur le Langage Java et éléments syntaxiques.

1 Architecture du cœur ARM Cortex M3. Le cœur ARM Cortex M3 sera présenté en classe à partir des éléments suivants :

Rappels sur les suites - Algorithme

IN Cours 1. 1 Informatique, calculateurs. 2 Un premier programme en C

Introduction à l architecture des ordinateurs. Adrien Lebre Décembre 2007

Séminaire RGE REIMS 17 février 2011

Un ordinateur, c est quoi?

Table des matières PRESENTATION DU LANGAGE DS2 ET DE SES APPLICATIONS. Introduction

1/24. I passer d un problème exprimé en français à la réalisation d un. I expressions arithmétiques. I structures de contrôle (tests, boucles)

Nom de l application

Les technologies du Big Data

LES CARTES À POINTS : POUR UNE MEILLEURE PERCEPTION

INFO 2 : Traitement des images

Architecture des ordinateurs

Une version javascript sera disponible directement dans le cours prochainement.

Introduction au calcul parallèle avec OpenCL

Simulation d'un examen anthropomorphique en imagerie TEMP à l iode 131 par simulation Monte Carlo GATE

Projet de traitement d'image - SI 381 reconstitution 3D d'intérieur à partir de photographies

L ORDINATEUR. Les composants. La carte mère. Le processeur. Fréquence

Matériel & Logiciels (Hardware & Software)

Travaux pratiques. Compression en codage de Huffman Organisation d un projet de programmation

Les algorithmes de base du graphisme

Manuel d utilisation 26 juin Tâche à effectuer : écrire un algorithme 2

Transcription:

2010 Semestre 4 Thème TI IUT de Cachan [ITHMES DE CONVOLUTION SUR PROCESSEURS GRAPHIQUES] Programmation massivement parallèle «Le langage est source de malentendus.» St. Exupéry Date de réception du rapport... à... Impression : jeudi 24 juin 2010

Remerciements Je tiens à équipe du groupe Problèmes Inverses du Laboratoire des Signaux et Systèmes pour leur accueil chaleureux. Je remercie tout particulièrement M. Nicolas GAC et M. Rémy BOYER, mes tuteurs entreprise et IUT, pour leur disponibilité et conseils durant ces dix semaines de stage. Je tiens également à remercier M. Aditya GAUTAM pour son UT de Cachan pour leur soutien durant mes deux années de formation. Résumé Les algorithmes de convolution se retrouvent majoritairement dans les domaines du traitement du signal, médicale) ou plus simplement en électronique numérique (filtrage de signaux). Bien que très utilisés, ces algorithmes présentent une complexité arithmétique élevée qui induit des temps de calcul importants. Nous nous proposons hmes en les implémentant sur une cible massivement parallèle : les processeurs graphiques. Abstract Convolution algorithms are mainly found in signal processing fields, such as imaging (scientific or medical) or even in electronics (digital signal filters). Although they are widely used, theses algorithms are highly compute- intensive and therefore induce very high computation times. Our goal is to accelerate theses algorithms by implementing them on a highly parallel target: graphical processors. Mots- clés GPU, CPU, Parallélisme, CUDA, Open CL, Nvidia, Séquentiel, Optimisation, Convolution, Matrice, image, Volume, Voxel, Pixel, Noyau, Rayon, méga- pixels, DRAM, PCI- Express, Bande passante, Shared Memory, Thread, SIMT, Streaming Processor, Cuda Cores, Host, Device. Université Paris- Sud, IUT de Cachan Département GEII- 1

Table des matières Remerciements Résumé Abstract Mots- clés Introduction 1 Convolution matricielle et processeur graphique, un couple idéal... 1 1.1 Convolution et complexité arithmétique, un choix reste possible... 1 1.1.1 Convolution par noyau, le prix de la polyvalence... 1 1.1.2 Convolution séparable, le choix de la performance... 3 1.2 Le processeur graphique, une architecture taillée pour le calcul... 4 1.2.1 La genèse du calcul scientifique sur processeur graphique... 4 1.2.2... 6 1.3 Un couple permettant une exécution hautement parallèle... 8 2 Implémentations et optimisations... 9 2.1... 9 2.2 Première étape : la convolution 2D... 11 2.2.1 Une première approche séquentielle... 11 2.2.2 Implémentations GPU naïves... 13 2.2.3 Implémentation GPU optimisée... 15 2.3 Deuxième étape... 18 2.3.1 Ajouter simplement une dimension... 18 2.3.2 Implémentations GPU naïves... 19 2.3.3 Implémentation GPU Optimisée... 21 3 Résultats : qualité, performance, constats et limites... 24 3.1 Qualité des résultats... 24 3.2 Benchmark des convolutions 2D... 25 3.3 Benchmark des convolutions 3D... 27 3.5... 29 3.5 : Fourrier... 32 3.6 Limites... 33 Conclusion... 34 Perspectives... 34 Sources... 35 Université Paris- Sud, IUT de Cachan Département GEII- 1

Introduction processeur graphique (GPU) émenter un algorithme de convolution 2D matricielle. cette première expérience grâce à un stage au Laboratoire des Signaux et Systèmes de Supélec Gif La version 3D sera notamment utilisée dans TomoX, un projet de reconstruction tomographique à rayon X faisant emploi de la puissance des processeurs graphiques. Les algorithmes de convolution mettent en jeux la multiplication de deux matrices et présentent donc une complexité arithmétique élevée. La stratégie de programmation massivement parallèle offerte par les GPU permet de réduire considérablement les temps de calculs induits par ces algorithmes. Dans un premier temps nous exposerons le principe des algorithmes de convolution des processeurs graphiques, ainsi nous montrerons en quoi la convolution est adaptée a une exécution parallèle. Dans un second temps, nous présenterons brièvement la structure de programmation sur GPU et verrons comment la convolution 2D à été implémentée. Nous passerons en revue puis présenterons le passage de la version bidimensionnelle à son homologue tridimensionnelle. stage dans sa globalité. Université Paris- Sud, IUT de Cachan Département GEII- 1

1 Convolution matricielle et processeur graphique, un couple idéal 1.1 Convolution et complexité arithmétique, un choix reste possible 1.1.1 Convolution par noyau, le prix de la polyvalence Mathématiquement, une convolution mesure la quantité de chevauchement entre deux fonctions. On utilise un lution se note généralement «f * g» et est définie comme : : On peut étendre la convolution à deux dimensions en effectuant deux sommes successives et en ajoutant un indice pour la deuxième dimension. Un pixel résultat N I multiplié par le noyau K de rayon R : Une représentation visuelle serait : Noyau Image convoluée Schéma : université Harvay Mudd Université Paris- Sud, IUT de Cachan Département GEII- 1 1

des volumes. Par ailleurs, la complexité arithmétique de la convolution par noyau est, par définition, très élevée. En effet pour une image de taille m et un noyau de taille n, la convolution demande multiplications. Son homologue tridimensionnelle en demande! très fréquent par exemple en astronomie), en traitant des volumes on obtient des temps élevés dès les faibles résolutions., sous Matlab il faut presque 30s pour convoluer séquentiellement une image 8 MPx avec un noyau de 9 pixels de large. Autre cas pratique, dans le projet TomoX, la convolution est utilisée sur des volumes et noyaux séquentielle sous Matlab ne demande pas moins de 7 min 30! le cas de TomoX), on comprend ainsi! Université Paris- Sud, IUT de Cachan Département GEII- 1 2

1.1.2 Convolution séparable, le choix de la performance La seconde est de travailler sur e et unique manière : certains noyaux de convolution peuvent être séparés en autant de vecteurs unidimensionnels que de dimensions les à la 3D rajoute un troisième vecteur de profondeur) 2D Schémas : réalisation personnelle La convolution de ces vecteurs redonne le noyau de convolution originel. Il est ainsi possible de séparer l en autant de passes que de vecteurs. Cette propriété permet ainsi de diminuer la complexité arithmétique de la convolution : En gardant une image de taille m et un noyau de taille n, la convolution séparable ne requiert en effet que multiplications par passe ( pour la version 3D) en 2D et en 3D! séparables. Ce sera donc le cahier des charges qui permettra ou non la convolution séparable. Université Paris- Sud, IUT de Cachan Département GEII- 1 3

1.2 Le processeur graphique, une architecture taillée pour le calcul 1.2.1 La genèse du calcul scientifique sur processeur graphique GPU est un sigle anglais signifiant Graphic Processing Unit, en français il est communément appelé processeur graphique. Les GPUs se trouvent sur les cartes graphiques de nos ordinateurs et sont à celles- ci ce que le processeur central (Central Processing Unit ou CPU) est à la carte mère. Une carte graphique Nvidia GTX480, on distingue le GPU frappé du logo au centre de la carte Photo : hardware.fr Les GPUs étaient habituellement destinés aux rendus en temps réel possédaient une architecture spécialisée dans ces traitements (majoritairement des unités dédiée au traitement de pixel et vertex shaders). En 2006, Nvidia, le principal fabriquant de GPU haut de gamme, à procédé à un r : Ge force 8 Series). Délaissant les architectures spécialisées, les GPUs sont devenus universels, leur architecture est devenue unifiée. Cela a procuré un double avantage General- Purpose Computing on Graphical Processing Units Université Paris- Sud, IUT de Cachan Département GEII- 1 4

Graphe : nvidia programming guide Afin de rendre accessible cette nouvelle puissance de calcul, Nvidia à développé un langage de programmation particulier, ou plus précisément une extension au très répandu langage C. Ce nouveau langage, dénommé CUDA pour Compute Unified Device Architecturearchitecture matérielle de calcul unifiée Ce calculatoires bien particulières. Ain dernières ainsi effectuées par le décomposable en éléments indépendants (pas de dépendance entre les données calculées) afin de pouvoir tirer parti de leur puissance de calcul offerte par la parallélisassions. En conséquen possible de dévaloriser le GPU en lui donnant à résoudre un problème ne tirant pas partit de cette parallélisassions et inversement. Il faut ainsi voir les GPUs comme des coprocesseurs accélérant certains calculs bien particuliers et non pas comme un remplacent des CPUs. Université Paris- Sud, IUT de Cachan Département GEII- 1 5

1.2.2 en détail, comparaison avec le CPU Les changements architecturels introduits par le G80 permettent comprend pourquoi les GPUs sont si doués aux calculs massivement parallèles. Schématisation On remarque en effet que la partie exécution (en vert) occupe une place bien plus importante sur le GPU. La partie que de tâches simples. La est quand à elle similaire. de la réalité serait la suivante : Schémas : nvidia programming guide Université Paris- Sud, IUT de Cachan Département GEII- 1 6

Nous distinguons deux éléments élémentaires : CUDA CORE Un Cuda Core, aussi appelé Streaming Processor (SP), en français processeur de flux ou plus simplement n. Au niveau logiciel un SP correspond à un thread. Il exécute du les instructions séquentiellement. Chaque SP possède 2 Ko de registres qui sont utilisés pour stocker les variables temporaires (par exemple pour les boucles) liée au thread même. SIMT Un SIMT pour Single Instruction Multiple Threads. Chaque SIMT exécute de façon parallèle comprend, pour les générations récentes hors GF100-, 16 Ko de mémoire cache (dite shared) accessible à Les GPU possèdent une hiérarchie mémoire particulière afin de pouvoir constamment alimenter les cuda cores en données. Détaillons et classons ces différentes mémoires. Du plus bas au plut haut niveau par rapport au GPU : Une très faible quantité de SRAM (16 Ko) est présente on- die (sur la puce) et joue le rôle de cache exécutionel shared, cette mémoire est dupliquée pour chaque groupe de 8 processeurs de flux. Une faible quantité de la DRAM globale (64 Ko) est cachée par registre vers le GPU (et donc en lecture seule peconstant elle permet un accès rapide (le temps de lecture du registre soit exécution. La mémoire DRAM Globale est quant à elle de taille beaucoup plus élevée (de quelques centaines de méga- octets à plusieurs giga- octets pour les modèles les plus onéreux). En contrepartie elle possède un temps de est souvent désignée par le mot clef device. Désignée par le mot clef host, elle forme le lien de données entre le CPU et le GPU. Elle converse avec le CPU par le bus externe PCI- Express. Dans sa version 2.0 il possède une bande passante théorique full- duplex (bidirectionnelle et simultanée) de 250 Mo/s par ligne. Les cartes graphiques récentes étant généralement connectées par 16 lignes, ce débit est porté à 4 Go/s en full- duplex. Université Paris- Sud, IUT de Cachan Département GEII- 1 7

1.3 Un couple permettant une exécution hautement parallèle La convolution, et dans une moindre mesure la convolution séparable présentent une complexité arithmétique élevée. Cependant graphiques. Un critère fondamental rentre en ligne de compte : le taux En effet si les instructions exécutées présentent une : Ce principe est énoncé par la qr en fonction du taux parallélisables s et du nombre de processeurs N Évolution du gain en vitesse d'exécution d'un programme en fonction du nombre de processeurs pour différentes valeurs de 1-s On remarque qu fortement avec seulement 1 non parallèllisables (courbe bleue) et ce même avec un nombre de processeurs elevé. Illustrations : wikipedia.fr/loi d Amdhal Cette loi démontre s parallélisables élevé. La convolution présente cette particularité, en effet chaque pixel (ou voxel) résultat est une somme de produits totalement indépendante du résultat voisin! Le taux de parallélisation est donc maximal (cas de la courbe verte) Cet sur processeurs graphiques. Université Paris- Sud, IUT de Cachan Département GEII- 1 8

2 Implémentations et optimisations 2.1 Cuda est une API (Application Programming Interface ou interface de programmation) du constructeur de GPU Nvidia. évoquée, Cuda est une solution commerciale et est donc réservée aux processeurs graphiques de marque Nvidia. Cuda est distribué gratuitement, la seule condition sine qua non est de posséder un GPU Nvidia. Cuda est une extension du très répandu langage C qui, moyennant un bref apprentissage de sa stratégie de programmation, permet, dénommées kernels, exécutées N fois en parallèle par N différent threads. On observe dès lors une hiérarchie Maitre / Esclave entre le code hôte (host) séquentiel et le code GPU (device) parallèle. kernel est défini lors du lancement dudit kernel exécution du kernel) repérant sa position. Un exemple simple de kernel ainsi que de son appel serait : // Kernel definition global void VecAdd(float* A, float* B, float* C) { int i = threadidx.x;; C[i] = A[i] + B[i];; } int main() {... // Kernel invocation with N threads VecAdd<<<1, N>>>(A, B, C);; } Dans ce code chaque thread va exécuter une addition. Pour threadid) peux prendre une (thradid.x) deux (threadid.y) ou trois (threadid.z) dimensions. Apparaissent dès lors blocs de threads (blockid) à une deux ou trois dimension suivant la même structure. : hread et sa position sont identique (Nx, Ny), Précisons par ailleurs que le nombre maximal de threads par bloc est fixé à 512 (limitation matérielle) Université Paris- Sud, IUT de Cachan Département GEII- 1 9

précédent étendons- le en deux dimensions : // Kernel definition global void MatAdd(float A[N][N], float B[N][N], float C[N][N]) { int i = threadidx.x;; int j = threadidx.y;; C[i][j] = A[i][j] + B[i][j];; } int main() {... // Kernel invocation with one block of N * N * 1 threads int numblocks = 1;; dim3 threadsperblock(n, N);; MatAdd<<<numBlocks, threadsperblock>>>(a, B, C);; } Soulignons enfin que les blocs de threads sont organisés en une grille 1D ou 2D (la grille ne peux, pour les générations actuelles être 3D, nous reviendrons sur ce point. La grille est définie en autant de blocs de taille identique tels que le nombre total de thread soit égal au nombre de thread par bloc multiplié par le nombre de blocs. matérielles Le nombre de blocs est la plupart du temps régis par la taille des données traitées (par exemple pour une image de résolution on utilisera blocs. Fort de cette première approche de la stratégie de programmation Cuda nous pouvons implémenter nos propres kernels dédiés au traitement de la convolution. Extraits de code : nvidia programming guide Université Paris- Sud, IUT de Cachan Département GEII- 1 10

2.2 Première étape : la convolution 2D 2.2.1 Une première approche séquentielle Dans un premier temps nous avons implémenté la convolution de façon point de référenc On copie dans u tableau 2D «cadre» (frame) accès à des zones mémoires non initialisées lorsque convolue Effets de bord : on accède à des zones mémoires non initialisées Schémas : réalisation personnelle Université Paris- Sud, IUT de Cachan Département GEII- 1 11

Regardons de façon détaillée le code : 1 2 3 4 for(long int y = kernel_radius_y; y < (image_y + kernel_radius_y); y++) for(long int x = kernel_radius_x; x < (image_x + kernel_radius_x); x++) { float add_tmp = 0 ; for(long int y_k = - kernel_radius_y ; y_k <= (int)(kernel_radius_y) ; y_k++) for(long int x_k = - kernel_radius_x ; x_k <= (int)(kernel_radius_x) ; x_k++) add_tmp += frame [y+y_k + (x+x_k*frame_x] * kernel[kernel_radius_y+y_k + (kernel_radius_x+x_k)*kernel_x]; resultat[y- kernel_radius_y + (x - kernel_radius_x ) * image_x] = add_tmp; } On exécute la convolution en prenant comme image source ce cadre. Pour cela il nous faut 4 boucles imbriquées. (1) Les deux boucles suivantes permettent de parcourir, pour chaque pixel source, les valeurs constantes du noyau de convolution (2) On accumule ensuite (3), pour chaque pixel source, les produits des pixels entourant le pixel source (et ce sur une zone de taille identique au noyau de convolution), au final (4) on écrit la somme résultante dans le pixel Université Paris- Sud, IUT de Cachan Département GEII- 1 12

2.2.2 Implémentations GPU naïves Occupons nous dans un premier temps de la version non séparable. les paramètres de lancement du kernel parallèle. Pour simplifier le traitement on va attribuer le calcul de chaque pixel résultat à un thread. On aura ainsi autant de exécution en bloc de 16*16 threads. La grille de threads est ainsi constituée de blocs de threads dans chaque dimension. Regardons plus en détail le code du kernel : global void ConvKernel(float* d_result,float* d_data) { float tmp_sum = 0; float pixel_extrait = 0; 1 2 3 int pixel_y = mul24(blockidx.y, blockdim.y) + threadidx.y; int pixel_x = mul24(blockidx.x, blockdim.x) + threadidx.x; int pixel_pos = mul24(pixel_x, c_image_x) + pixel_y ; for (int y = - (c_kernel_radius_y) ; y <= ((int)(c_kernel_radius_y)); y++) for (int x = - (c_kernel_radius_x) ; x <= ((int)(c_kernel_radius_x)); x++) { pixel_extrait= ( ( (pixel_x + x) < 0 ) ( (pixel_y + y) < 0 ) ( (pixel_x + x - c_image_x) < blockdim.x ) ( (pixel_y + y - c_image_y) < blockdim.y ) )? 0 : d_data[pixel_y+y + (pixel_x+x)*c_image_x ]; 4 5 tmp_sum += pixel_extrait * c_kernel[c_kernel_radius_y+y + (c_kernel_radius_x+x)*c_kernel_x]; } d_result[pixel_pos] = tmp_sum; } Le kernel se charge de calculer un seul et unique pixel résultat. Il suffira de renseigner la position du pixel traité en utilisant les variables (1) NB : mul24(x, y) bits au lieu de 32, elle est équivalente à x*y Nous parcourons ensuite le contenu du noyau de convolution (2). : on regarde la position des pixels extraits et (3) Nous accumulons alors les produits des pixels entourant le pixel source (appelés pixel_extrait) (4) Enfin, on écrit la somme résultante dans le pixel co résultat (5) Université Paris- Sud, IUT de Cachan Département GEII- 1 13

exécution en deux passes et donc deux kernels. Le premier produits avec le vecteur horizondans un buffer. Le buffer est repris en source par le deuxième kernel On configure des blocs de (16,4) threads pour le kernel horizontal et de (4,16) threads pour le kernel vertical. Cette organisation permet de traiter 4 rangées de 16 pixels dans la passe horizontale ainsi que 4 colonnes de 16 pixels dans la passe verticale. De ce fait, la grille de threads est constituée de blocs pour le kernel horizontal et inversement pour le kernel vertical. Nous ne détaillerons pas le code des kernels de la version non séparable ( XXX) Université Paris- Sud, IUT de Cachan Département GEII- 1 14

2.2.3 Implémentation GPU optimisée exécution as au maximum le DRAM Globale qui, nous le rappelons, possède un temps Nous allons dès lors mètre en place les mémoires constant et shared. Cherchant la meilleure performance nous allons utiliser ces optimisations directement sur la version séparable. Nous allons placer en mémoire constant les données récurrentes utilisées par les kernels : les noyaux de convolution ainsi que les tailles des données. La mémoire partagée va elle contenir des blocs de pixels. Nous cherchons à fournir le maximum de calculs accéder, lors de chaque itération du kernel, aux données provenant de la même mémoire shared processeurs de chaque SIMT)par itération. Nous définissons les blocs de threads de la même façon que la version non- optimisée, cependant nous allons définir la grille horizontalement en 16*8 blocs pour le kernel horizontal. Inversement elle sera définie verticalement en 16*8 blocs pour la passe verticale. Cette organisation permet de lancer un thread sur chacun des 8 processeurs de flux composant les SIMT. Nous devons dès lors déclarer une mémoire shared contenant 8 blocs de 16 pixels en largeur. Nous devons ici faire problème nous ajoutons deux blocs «fantômes»ces 8 premiers blocs (remarque : cela limite la taille maximale du rayon de convolution à 16 pixels, soit un noyau de 34 pixels de large au maximum). Effets de bord à la périphérie des blocs de mémoire shared Illustrations : Nvidia Convolution Separable Whitepaper Université Paris- Sud, IUT de Cachan Département GEII- 1 15

Regardons en détail le code du kernel cupant de la passe horizontale (première partie) 1 2 3 global void ConvKernelShared_Sep_row (float* d_output,float* d_input) { shared float s_data[block_size_row_y][(computed_block_size + 2*EDGE_HALO_BLOCK_SIZE) * BLOCK_SIZE_ROW_X]; const int pixel_y = IMUL(blockIdx.y, blockdim.y) + threadidx.y; const int pixel_x = IMUL( (IMUL(blockIdx.x, COMPUTED_BLOCK_SIZE) - EDGE_HALO_BLOCK_SIZE ), blockdim.x ) + threadidx.x; const int pixel_pos = IMUL(pixel_y, c_image_x) + pixel_x ; d_output += pixel_pos ; d_input += pixel_pos ; 5 4 for (int pos = 0 ; pos < EDGE_HALO_BLOCK_SIZE ; pos++) { if ( pixel_x >= - pos*block_size_row_x) s_data[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x] ; else s_data[threadidx.y][threadidx.x + pos*block_size_row_x] = 0 ; } 6 for (int pos = EDGE_HALO_BLOCK_SIZE ; pos < COMPUTED_BLOCK_SIZE + EDGE_HALO_BLOCK_SIZE ; pos++) s_data[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x] ; 8 7 for (int pos = COMPUTED_BLOCK_SIZE+EDGE_HALO_BLOCK_SIZE ; pos <COMPUTED_BLOCK_SIZE+2*EDGE_HALO_BLOCK_SIZE ; pos++) { if ( c_image_x - pixel_x > pos*block_size_row_x ) s_data[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x]; else s_data[threadidx.y][threadidx.x + pos*block_size_row_x] = 0 ; } syncthreads(); NB : BLOCK_SIZE_ROW_X = 16, BLOCK_SIZE_ROW_Y = 4, représentent la taille des blocs en abscisse et ordonnée COMPUTED_BLOCK_SIZE = 8, représente le nombre de blocs traités dans la mémoire shared EDGE_HALO_BLOCK_SIZE shared Université Paris- Sud, IUT de Cachan Département GEII- 1 16

Dans cette première partie du kernelshared (1). On indexe ensuite la position du premier pixel traité sur les indices de thread (2). On spécifie (3) On commence à remplir la partie extérieure gauche de la mémoire shared (4) (5). On remplit ensuite la partie centrale de la mémoire shared (6). Enfin on remplit la partie extérieure droite (7) (8) On peut maintenant passer à la partie calcul de la convolution du kernel : 1 syncthreads();; float tmp_sum = 0;; 2 3 for (int pos = EDGE_HALO_BLOCK_SIZE ;; pos < COMPUTED_BLOCK_SIZE + EDGE_HALO_BLOCK_SIZE ;; pos++) { tmp_sum = 0 ;; for (int k = - (c_kernel_radius_x) ;; k <= ((int)(c_kernel_radius_x));; k++) tmp_sum += s_data[threadidx.y][threadidx.x + pos*block_size_row_x + k] * c_kernel_h[c_kernel_radius_x + k];; 4 } d_output[pos*block_size_row_x] = tmp_sum;; } sent (1) Nous parcourons ensuite le contenu de la mémoire shared (2). Nous accumulons alors, pour 8 pixels de la mémoire shared, les produits des pixels entourant les pixels (3). (4) Le code complet est disponible en annexe (XX) Université Paris- Sud, IUT de Cachan Département GEII- 1 17

2.3 Deuxième étape 2.3.1 Ajouter simplement une dimension 3D. La grille de threads est en effet limitée à 2 dimensions ce qui risque de compliquer outre mesure la gestion des indices. Nous avons choisi de rajouter la boucle gérant la profondeur non pas dans le kernel mais lors du lancement. En effet nous pouvons imaginer e 3D comme la convolution de Z slices 2D. Z slices y x z Z slices Schémas : réalisation personnelle Université Paris- Sud, IUT de Cachan Département GEII- 1 18

2.3.2 Implémentations GPU naïves Pour utiliser la méthode présentée nous devons adapter nos kernels 2D. En effet notre noyau étant en 3D, nous devons récupérer les valeurs des pixels résultats des slices placées avant et après la slice actuelle (et ce sur toute la profondeur du noyau). Slice volume n- 1 Slice volume n Slice volume n+1 5 noyau n- 1 10 5 Z noyau n noyau n+1 Slice volume n 20 Schémas : réalisation personnelle Université Paris- Sud, IUT de Cachan Département GEII- 1 19

Cette alors ne pas sommer le résultat de la slice hors image). : global void ConvKernel_InternalSlices (float* d_result,float* d_data) { float tmp_sum = 0; float pixel_extrait_prev = 0; float pixel_extrait_curr = 0; float pixel_extrait_next = 0; 1 int pixel_y = mul24(blockidx.y, blockdim.y) + threadidx.y; int pixel_x = mul24(blockidx.x, blockdim.x) + threadidx.x; int pixel_pos = mul24(pixel_x, c_volume_x) + pixel_y ; 2 3 for (int y = - (c_kernel_radius_y) ; y <= ((int)(c_kernel_radius_y)); y++) for (int x = - (c_kernel_radius_x) ; x <= ((int)(c_kernel_radius_x)); x++) { if ( ( (pixel_x + x) < 0 ) ( (pixel_y + y) < 0 ) ( (pixel_x + x - c_volume_x) < blockdim.x ) ( (pixel_y + y - c_volume_y) < blockdim.y ) ) pixel_extrait_prev = pixel_extrait_curr = pixel_extrait_next = 0 ; 4 else { pixel_extrait_prev = d_data[ pixel_y+y + mul24((pixel_x+x), c_volume_x) - c_slice_jump ]; pixel_extrait_curr = d_data[ pixel_y+y + mul24((pixel_x+x), c_volume_x)]; pixel_extrait_next = d_data[ pixel_y+y + mul24((pixel_x+x), c_volume_x) + c_slice_jump ]; } int posk_y = c_kernel_radius_y + y; int posk_x = c_kernel_radius_x + x; 5 6 tmp_sum += pixel_extrait_prev * c_kernel[ posk_y*c_kernel_x + posk_x*c_kernel_x*c_kernel_y] + pixel_extrait_curr * c_kernel[1 + posk_y*c_kernel_x + posk_x*c_kernel_x*c_kernel_y] + pixel_extrait_next * c_kernel[2 + posk_y*c_kernel_x + posk_x*c_kernel_x*c_kernel_y]; } d_result[pixel_pos] = tmp_sum ; } On renseigne la position du voxel de la slice courante (1) Nous parcourons ensuite le contenu du noyau de convolution (2) et testons la position du voxel afin de gérer les effets de bord (3). Hors bord, on extrait les voxels entourant celui traité et ce, pour la slice précédente, la slice courante et la slice suivante. NB Nous accumulons alors voisins par leur noyau respectif (4). Enfin, la somme est écrite dans le voxel résultat de la slice courante(5) Université Paris- Sud, IUT de Cachan Département GEII- 1 20

ls. Cette méthode nécessite cependant un total de 9 kernel (3 pour les slices externes au début du volume, autant à la fin et 3 pour les slices centrales). Dans un souci de performance nous allons réduire ce nombre à 6 en incorporant la passe gérant la profondeur dans la passe horizontale. Notons cependant que ce raccourci est seulement rendu convoluons les données des slices suivantes et valeurs du noyau de profondeur. de la version 2D, les paramètres de lancement des kernels restent inchangés. Encore une fois, nous ne détaillerons pas le code des kernels XXX) 2.3.3 Implémentation GPU Optimisée lancement. Par ailleurs nous adoptons aussi la technique de la boucle en Z sur le lancement des kernels 2D. calculée. La gestion des effets de bords Université Paris- Sud, IUT de Cachan Département GEII- 1 21

: 5 1 2 3 4 global void ConvKernel_InternalSlice_row_shared (float* d_output,float* d_input) { shared float s_data_prev[block_size_row_y][(computed_block_size + 2*EDGE_HALO_BLOCK_SIZE) * BLOCK_SIZE_ROW_X]; shared float s_data_curr[block_size_row_y][(computed_block_size + 2*EDGE_HALO_BLOCK_SIZE) * BLOCK_SIZE_ROW_X]; shared float s_data_next[block_size_row_y][(computed_block_size + 2*EDGE_HALO_BLOCK_SIZE) * BLOCK_SIZE_ROW_X]; const int pixel_y = IMUL(blockIdx.y, blockdim.y) + threadidx.y; const int pixel_x = IMUL( (IMUL(blockIdx.x, COMPUTED_BLOCK_SIZE) - EDGE_HALO_BLOCK_SIZE ), blockdim.x ) + threadidx.x; const int pixel_pos = IMUL(pixel_y, c_volume_x) + pixel_x ; d_output += pixel_pos ; d_input += pixel_pos ; for (int pos = 0 ; pos < EDGE_HALO_BLOCK_SIZE ; pos++){ if ( pixel_x >= - pos*block_size_row_x) { s_data_prev[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x - c_slice_jump] ; s_data_curr[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x] ; s_data_next[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x + c_slice_jump] ; } else { s_data_prev[threadidx.y][threadidx.x + pos*block_size_row_x] = 0 ; s_data_curr[threadidx.y][threadidx.x + pos*block_size_row_x] = 0 ; s_data_next[threadidx.y][threadidx.x + pos*block_size_row_x] = 0 ; }} 6 for (int pos = EDGE_HALO_BLOCK_SIZE ; pos < COMPUTED_BLOCK_SIZE + EDGE_HALO_BLOCK_SIZE ; pos++){ s_data_prev[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x - c_slice_jump] ; s_data_curr[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x] ; s_data_next[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x + c_slice_jump] ; } 8 7 for (int pos = COMPUTED_BLOCK_SIZE + EDGE_HALO_BLOCK_SIZE ; pos < COMPUTED_BLOCK_SIZE + 2*EDGE_HALO_BLOCK_SIZE ; pos++){ if ( c_volume_x - pixel_x > pos*block_size_row_x ) { s_data_prev[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x - c_slice_jump] ; s_data_curr[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x] ; s_data_next[threadidx.y][threadidx.x + pos*block_size_row_x] = d_input[pos*block_size_row_x + c_slice_jump] ; } else { s_data_prev[threadidx.y][threadidx.x + pos*block_size_row_x] = 0 ; s_data_curr[threadidx.y][threadidx.x + pos*block_size_row_x] = 0 ; s_data_next[threadidx.y][threadidx.x + pos*block_size_row_x] = 0 ; }} syncthreads(); Université Paris- Sud, IUT de Cachan Département GEII- 1 22

Dans cette première partie du kernelblocs de mémoire shared pour chacune des slice (1). On indexe ensuite la position du premier voxel traité sur les indices de thread (2)(3) On commence à remplir les parties extérieures gauches des mémoires shared (4) (5). On remplit ensuite les parties centrales des mémoires shared (6). Enfin on remplit les parties extérieures droites (7) uve pas en dehors des slices (8) On peut maintenant passer à la partie calcul de la convolution du kernel : 1 syncthreads(); float tmp_sum = 0;; 2 for (int pos = { tmp_sum = 0 ;; EDGE_HALO_BLOCK_SIZE ;; pos < COMPUTED_BLOCK_SIZE + EDGE_HALO_BLOCK_SIZE ;; pos++) 3 4 for (int k = - (c_kernel_radius_x) ;; k <= ((int)(c_kernel_radius_x));; k++) tmp_sum += s_data_prev[threadidx.y][threadidx.x+pos*block_size_row_x+k]* c_kernel_h[c_kernel_radius_x+k] * c_kernel_p[0] + s_data_curr[threadidx.y][threadidx.x+pos*block_size_row_x+k]* c_kernel_h[c_kernel_radius_x+k] * c_kernel_p[1] + s_data_next[threadidx.y][threadidx.x+pos*block_size_row_x+k]* c_kernel_h[c_kernel_radius_x+k] * c_kernel_p[2];; d_output[pos*block_size_row_x] = tmp_sum;; } } sent (1) Nous parcourons ensuite le contenu des mémoires shared (2). Nous accumulons alors, pour 8 pixels de chacune des mémoires shared, voisins par leur noyau respectif (3). NB : Nous sommes dans le cas particulier de la passe horizontale, nous retrouvons donc la multiplication par le Kernel de profondeur. Enfin, la somme est écrite dans le voxel résultat du buffer de la courante pour être ensuite traité par le kernel vertical (4). Le code complet est disponible en annexe (XX) Université Paris- Sud, IUT de Cachan Département GEII- 1 23

3 Résultats : qualité, performance, constats et limites 3.1 Qualité des résultats implémentations. Nous avons mis en place une étape de comparaison reposant sur le calcul de la L2- norm (aussi dénommée erreur euclidienne), méthode trouvée dans un des exemples sur SDK Nvidia. Remarque : arte graphique. La carte est reliée au CPU par un bus PCI express 2.0 câblé en mode 16x Le CPU est un Intel Xeon cadencé à 2. X? Ghz Université Paris- Sud, IUT de Cachan Département GEII- 1 24

3.2 Benchmark des convolutions 2D Précisons avant tout que la taille du noyau est ici fixée à 3 pixels de large. Facteur d'acclélération global des différentes convolutions en fonction de la taille de l'image convoluée CPU GPU GPU Séparable GPU Séparable avec Shared memory 26,0 Facteur d'accélération (x vitesse CPU Matlab) 21,0 16,0 11,0 6,0 1,0 20,1 18,8 18,7 16,5 11,6 9,9 8,1 8,8 8,0 7,0 5,9 3,3 3,6 2,1 1,4 1,7 2,1 1,5 512 1024 2048 3072 4096 8192 Résolution (Px) Université Paris- Sud, IUT de Cachan Département GEII- 1 25

La convolution la plus rapide est celle faisant usage de la mémoire shared avec une exécution près de 20 fois plus rapide que Matlab. Nous obtenons cependant des résultats tout à fait corrects pour les versions sans mémoire shared. Notons par une fois rappelons que le noyau est fixé à 3 pixels, avec un noyau plus grand on obtiendrait un gain pour des masquer les différents tests et mécanisme shared finit par diminuer passé un certain seuil. (POURQUOI?) luer une image 8 MPx,! Université Paris- Sud, IUT de Cachan Département GEII- 1 26

3.3 Benchmark des convolutions 3D 26,0 Facteur d'acclélération global des différentes convolutions en fonction de la taille du volume convolué CPU GPU GPU Séparable GPU Séparable avec Shared memory Facteur d'accélération (x vitesse CPU Matlab) 21,0 16,0 11,0 6,0 5,9 16,7 11,9 8,3 20,2 13,3 8,5 4,6 3,7 1,0 64 128 256 Résolution (Vx) Université Paris- Sud, IUT de Cachan Département GEII- 1 27

Précisons avant tout que la taille du noyau est ici fixée à 3 voxels de large. Ce graphe prése- à- vis de la fonction Matlab Convn. La convolution la plus rapide est celle faisant usage de la mémoire shared avec une exécution près de 20 fois plus rapide que Matlab. taille suffisamment élevée (ici 128 Voxels de large) delà de volumes de 704 Voxels. En effet la carte graphique sur laquelle les tests sont effectués ne comporte que 4 Go de mémoire DRAM, nos volumes étant codés en flottants ils occupent 4 o par élément. De ce fait, la taille maximale des volumes chargés doit respecter l : Où x est la largeur du volume en voxels, DRAM la taille de la mémoire en Go et N le nombre de variables de la taille du volume chargées Pour la version non séparable cette limite est Pour les versions séparables, Afin de pouvoir comparer les résultats nous nous sommes arrêté à 704 Voxels de large. revient le temps de calcul passerai de 7min30 à 20s! Remarque : Pour satisfaire le projet TomoX qui travaille sur des volumes de 1024 Vx (ce qui représente 4 Go de données par variable) il faudrait séparer le volume et lancer les kernels de façon séquentielle sur ces sous- parties de volume. Université Paris- Sud, IUT de Cachan Département GEII- 1 28

3.5 Les transferts mémoire, un Les résultats présentés précédemment mesurexécution sur GPU). 1000,0 Facteur d'accélération pour la partie calculatoire des différentes convolutions en fonction de la taille de l'image convoluée CPU GPU GPU Séparable GPU Séparable avec Shared memory Facteur d'accélération (x vitesse CPU Matlab) 100,0 10,0 44,5 6,9 3,6 62,4 5,5 2,7 307,1 23,8 11,8 350,1 353,4 359,7 28,0 14,0 15,8 8,4 5,6 4,4 1,0 512 1024 2048 3072 4096 8192 Resolution (Px) Université Paris- Sud, IUT de Cachan Département GEII- 1 29

Facteur d'accélération pour la partie calcul des différenetes convolutions en fonction de la taille du volume convolué CPU GPU GPU Séparable GPU Séparable avec Shared memory 1000,0 Facteur d'accélération (x vitesse CPU Matlab) 100,0 10,0 24,1 10,6 6,7 139,0 68,7 32,7 28,5 13,7 13,4 1,0 64 128 256 Résolution (Vx) ts mémoire. Ainsi la partie algorithmique est pres de 20 fois plus rapide quand on traite des images et 7 fois plus rapide p de transfert mémoire par rapport temps total xécution de la fonction. Université Paris- Sud, IUT de Cachan Département GEII- 1 30

Pourcentage de transfert mémoire 100 90 80 70 60 50 40 30 20 10 0 Pourcentage de transfert mémoire par rapport au temps d'éxecution total en fonction de la résolution de l'image convoluée 95,3 94,8 94,6 94,6 94,7 94,4 75,6 62,1 GPU GPU Séparable GPU Séparable avec Shared memory 61,7 44 58,3 58,7 41,1 42,3 512 1024 2048 3072 4096 8192 Résolution (Px) 44,5 29,7 18,3 29,8 temps à faire des transferts mémoire. Par ailleurs, q mémoire diminue puisque la quantité de calcul augmente de façon bien plus importante que la taille des données à transférer (le temps de transfert.8 MP Vérifions la cohérence avec le pourcentage de transfert des versions 3D. Pourcentage de transfert mémoire par rapport au temps d'éxécution total en fonction de la résolution du volume convolué Pourcentage de transfert mémoire 90 80 70 60 50 40 30 20 10 0 GPU GPU Séparable GPU Séparable avec Shared memory 85,5 75,4 77,1 56,4 58,3 59,5 44,2 39,9 36,4 64 128 256 Résolution (Vx) Nous pouvons dresser un constat similaire, quoi que tempéré par la taille des données à transférer qui augmente cette fois ci plus vite que lorsque Université Paris- Sud, IUT de Cachan Département GEII- 1 31

3.5 : Fourrier enir la convolution de deux matrices. Cette méthode utilise la transformation de Fourier des signaux discret appelée ou TFD. Cette transformation équivaut à une convolution si : On exécute la TFD sur ces signaux périodisés On multiplie ces deux TFD On fait la TFD Inverse de cette multiplication Nous évoquons cette méthode uniquement à titre de comparaison, en effet elle présente une propriété intéressante est constant quel que soit la taille du noyau utilisé. Vitesse d'éxecution totale des convolutions 2D GPU linéaires et fréquentielles en fonction de la taille du noyau Convolution 2D GPU Séparable avec Shared Convolution 2D FFT GPU 160 140 120 Vitesse (MPx/s) 100 80 60 40 20 0 1 2 4 8 16 32 64 128 256 512 Taille noyau (Px) tilise des noyaux très grands (plus de 250 pixels de large sur notre système) elle rattrape la convolution séparable. Université Paris- Sud, IUT de Cachan Département GEII- 1 32

3.6 Limites Nos implémentations GPU possèdent certaines limites que nous signalons dans cette partie. La version GPU 2D Séparable voit la taille de son noyau limitée à 33 pixels de large, cela est du à la déclaration des blocs «fantômes» de la mémoire shared. Pour lever cette limitation il faut modifier la constante EDGE_HALO_BLOCK_SIZE (en la fixant à 2 on autorise par exemple un noyau de 65 pixels de large). La version GPU 3D Séparable, bien que très performante, ne peux exécuter que des noyaux 3.3.3. Pour lever cette limitation il faudrait modifier le code des inconvénient puisque cette implémentation est destinée au projet TomoX qui utilise exclusivement des noyaux 3.3.3. Université Paris- Sud, IUT de Cachan Département GEII- 1 33

Conclusion Cuda, bien que récent, résultats en peu de temps. graphiques. Cette première expérience de programmation parallèle se révélera être sans aucun doute un atout important dans un futur proche. En effet les processeurs centraux, tels les GPU, tendent à une multiplication du amènera de plus en plus de programmes à adopter une stratégie de programmation parallèle. Par ailleurs ce stage de me familiariser avec un environnement de travail libre (linux CentOs) ce indows) Perspectives les transferts mémoire. Il exécution. On pourrait ainsi transférer une partie des données, commencer les calculs et, exécution, transférer les données suivantes. Cette GF100 «Fermi» (nom commercial Ge Force GTX480) qui a été pensée presque exclusivement pour Cuda. Elle permet, outre des étendue de 16 à 64 Ko ainsi on- die globale de niveau 1 (même principe que les CPU). Mentionnons enfin OpenCL (CL pour Computing Language), un autre langage de programmation. Pendant open- source de Cuda/des CPU multi- Université Paris- Sud, IUT de Cachan Département GEII- 1 34

Sources Index de la documentation Cuda - Cuda Zone Dossier sur la nouvelle architecture "FERMI" de Nvidia Représentation graphique de la convolution 2D PDF de référence : Nvidia GPU Programming Guide Documentation Gimp sur les matrices de convolution Whitepaper Nvidia sur la convolution séparable Article Wikipedia sur le déroulement des boucles Article Wikipedia sur la TFD Whitepaper Nvidia sur la convolution FFT blog Matlab décrivant la convolution séparable Université Paris- Sud, IUT de Cachan Département GEII- 1 35