Abstractions Performantes Pour Cartes Graphiques



Documents pareils
Évaluation et implémentation des langages

Éléments de programmation et introduction à Java

Machines virtuelles Cours 1 : Introduction

Cours 1 : La compilation

FAMILLE EMC VPLEX. Disponibilité continue et mobilité des données dans et entre les datacenters AVANTAGES

Une bibliothèque de templates pour CUDA

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

Cours d introduction à l informatique. Partie 2 : Comment écrire un algorithme? Qu est-ce qu une variable? Expressions et instructions

Rapport de Synthèse. Création d un Générateur de modèle PADL pour le langage C++ Sébastien Colladon

Logiciel Libre Cours 3 Fondements: Génie Logiciel

INTERSYSTEMS CACHÉ COMME ALTERNATIVE AUX BASES DE DONNÉES RÉSIDENTES EN MÉMOIRE

basée sur le cours de Bertrand Legal, maître de conférences à l ENSEIRB Olivier Augereau Formation UML

VMWare Infrastructure 3

I00 Éléments d architecture

10 tâches d administration simplifiées grâce à Windows Server 2008 R2. 1. Migration des systèmes virtuels sans interruption de service

Enseignant: Lamouchi Bassem Cours : Système à large échelle et Cloud Computing

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

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

Développement d un interpréteur OCL pour une machine virtuelle UML.

Julien MATHEVET Alexandre BOISSY GSID 4. Rapport RE09. Load Balancing et migration

FAMILLE EMC VPLEX. Disponibilité continue et mobilité des données dans et entre les datacenters

Architecture matérielle des systèmes informatiques

Bien architecturer une application REST

INITIATION AU LANGAGE C SUR PIC DE MICROSHIP

Initiation au HPC - Généralités

Éléments d informatique Cours 3 La programmation structurée en langage C L instruction de contrôle if

IFT2255 : Génie logiciel

Cours Informatique 1. Monsieur SADOUNI Salheddine

Algorithme. Table des matières

Anne Tasso. Java. Le livre de. premier langage. 10 e édition. Avec 109 exercices corrigés. Groupe Eyrolles, , ISBN :

Solution A La Gestion Des Objets Java Pour Des Systèmes Embarqués

Le Processus RUP. H. Kadima. Tester. Analyst. Performance Engineer. Database Administrator. Release Engineer. Project Leader. Designer / Developer

Modernisation et gestion de portefeuilles d applications bancaires

Conception des systèmes répartis

Patrons de Conception (Design Patterns)

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

Institut Supérieure Aux Etudes Technologiques De Nabeul. Département Informatique

MODELISATION UN ATELIER DE MODELISATION «RATIONAL ROSE»

Cours 1 : Qu est-ce que la programmation?

Introduction à la programmation orientée objet, illustrée par le langage C++ Patrick Cégielski

Evaluation des performances de programmes parallèles haut niveau à base de squelettes

Rappels sur les suites - Algorithme

Sciences de Gestion Spécialité : SYSTÈMES D INFORMATION DE GESTION

Analyse de sécurité de logiciels système par typage statique

Prise en compte des ressources dans les composants logiciels parallèles

Introduction aux systèmes temps réel. Iulian Ober IRIT

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

Semarchy Convergence for MDM La Plate-Forme MDM Évolutionnaire

Nom de l application

Cours en ligne Développement Java pour le web

Chapitre 1 : Introduction aux bases de données

SQL Server Installation Center et SQL Server Management Studio

UE Programmation Impérative Licence 2ème Année

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

La dernière base de données de Teradata franchit le cap du big data grâce à sa technologie avancée

Multiprogrammation parallèle générique des méthodes de décomposition de domaine

4.2 Unités d enseignement du M1

UN EXEMPLE DE CYBERENSEIGNEMENT EN CHIMIE

Gé nié Logiciél Livré Blanc

Hétérogénéité pour atteindre une consommation énergétique proportionnelle dans les clouds

Logiciel Libre Cours 2 Fondements: Programmation

D une part, elles ne peuvent faire table rase de la richesse contenue dans leur système d information.

Architectures web/bases de données

DA MOTA Anthony - Comparaison de technologies : PhoneGap VS Cordova

Programmation d'agents intelligents Vers une refonte des fils de raisonnement. Stage de fin d'études Master IAD 2006

La plate-forme DIMA. Master 1 IMA COLI23 - Université de La Rochelle

Les diagrammes de modélisation

Contributions à l expérimentation sur les systèmes distribués de grande taille

Chapitre VI- La validation de la composition.

KX-NCP500 / KX-NCP1000

Limitations of the Playstation 3 for High Performance Cluster Computing

Université de Bangui. Modélisons en UML

UFR d Informatique. FORMATION MASTER Domaine SCIENCES, TECHNOLOGIE, SANTE Mention INFORMATIQUE

Programme scientifique Majeure ARCHITECTURE DES SYSTEMES D INFORMATION. Mentions Ingénierie des Systèmes d Information Business Intelligence

UNIFIED. Nouvelle génération d'architecture unifiée pour la protection des données D TA. dans des environnements virtuels et physiques PROTECTION

4. Utilisation d un SGBD : le langage SQL. 5. Normalisation

Manuel de System Monitor

7 avantages à la virtualisation des applications stratégiques de votre entreprise

Conditions : stage indemnisé, aide au logement possible, transport CEA en Ile-de-France gratuit.

ÉdIteur officiel et fournisseur de ServIceS professionnels du LogIcIeL open Source ScILab

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

OCL - Object Constraint Language

Point sur la virtualisation

La visio-conférence holographique : Pourquoi? Comment?

PROJET DE MODELISATION CASERNE SERGEANT BLANDAN

Rapport d activité. Mathieu Souchaud Juin 2007

Premiers Pas avec OneNote 2013

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

Livret du Stagiaire en Informatique

Vulgarisation Java EE Java EE, c est quoi?

Une SGDT simple pour entreprises

Introduction au langage C

Leçon 1 : Les principaux composants d un ordinateur

Introduction à la Programmation Parallèle: MPI

La Thèse de Maths dont vous êtes le héros

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

NFP111 Systèmes et Applications Réparties

Encadré par : Michel SIMATIC

Transcription:

UNIVERSITÉ PIERRE ET MARIE CURIE ÉCOLE DOCTORALE INFORMATIQUE, TÉLÉCOMMUNICATIONS ET ÉLECTRONIQUE Abstractions Performantes Pour Cartes. Graphiques MATHIAS BOURGOIN sous la direction d Emmanuel Chailloux et de Jean-Luc Lamotte THÈSE pour obtenir le titre de Docteur en Sciences mention Informatique Emmanuel Chailloux Jean-Luc Lamotte Marco Danelutto Jocelyn Sérot Carlos Agon Joël Falcou Gaétan Hains Stéphane Vialle Directeurs Université Pierre et Marie Curie Université Pierre et Marie Curie Rapporteurs Università di Pisa Université Blaise Pascal Examinateurs Institut de Recherche et Coordination AcoustiqueMusique Université Pierre et Marie Curie Université Paris-Sud Université Paris-Est Créteil Val de Marne École Supérieure d Électricité

Document mis en page avec X LATEX et la police E

«With great power, comes great responsability.» P. Parker alias Spider-Man

Remerciements Je tiens tout d abord à remercier mes deux directeurs de thèse, Emmanuel et Jean-Luc, qui au cours de ma thèse ont su, parfois malgré la distance, m orienter et me guider dans mes recherches et réflexions sans m ôter l autonomie et la liberté nécessaires à mon travail de recherche. Je suis particulièrement heureux d avoir réalisé cette thèse avec eux deux, sur un sujet qui me correspondait bien et qui associait parfaitement deux aspects de l informatique souvent considérés (à tort) comme opposés. Deux aspects que j ai découverts lors de mes années de licence, mais surtout lors de mes années de master. Je tiens d ailleurs à remercier toute l équipe enseignante qui m a mené jusqu ici. Je remercie très chaleureusement Marco et Jocelyn pour avoir accepté de rapporter ma thèse et pour leurs remarques bienveillantes qui m ont permis d améliorer mon manuscrit. Je remercie évidemment aussi Carlos, Joël, Gaétan et Stéphane d avoir accepté d être membres du jury de ma thèse. J en profite d ailleurs pour remercier l équipe de l école doctorale et en particulier Christian Queinnec, son directeur, qui ont toujours été très efficaces et réactifs pour répondre à mes questions au cours de la thèse et particulièrement sur la fin. Je pense bien sûr aussi à toute l équipe de la licence et du master d informatique, mais aussi à l équipe de la licence de mécanique et à son directeur de l époque qui a su, à sa manière, m orienter vers l informatique. Je remercie à nouveau l équipe enseignante de la licence et du master d informatique, cette fois-ci pour leur aide et leur soutien lors de la découverte de l enseignement universitaire, de l autre côté du miroir. J en profite pour remercier tous les membres de l équipe APR, et en particulier Michèle Soria, que j avais rencontrés en tant qu étudiant, lors des nombreux enseignements auxquels ils participent et qui plus tard lors de mes stages et de ma thèse m ont offert un cadre de recherche chaleureux et dynamique. Je remercie aussi tout le personnel administratif et les ingénieurs chargés de la gestion de nos machines. Je remercie aussi le projet OpenGPU sans qui cette thèse n aurait sans doute pas eu lieu. Merci à tous ses acteurs, et en particulier à Silkan pour la prise de relais. Mes années d études n auraient bien évidemment pas été les mêmes sans Adrien qui m a accompagné de la licence au master et sans qui je n aurais sans doute pas fini la moitié de mes projets. Merci Adrien, pour ta présence, ton écoute et parfois ta patience avec un camarade probablement pas toujours très facile et souvent trop exigeant. Mais surtout, merci pour ta camaraderie et pour ton enthousiasme, qui ne s éteint heureusement pas, une fois passées les douves de Jussieu. Merci ensuite à Benjamin et Philippe que j ai rencontrés lors d un stage de master et que j ai retrouvés plus tard lors de ma thèse. Merci à eux pour m avoir guidé, à leur manière, dans la thèse qu ils découvraient avec un peu d avance sur moi. Je n oublie bien sûr pas les autres membres du bureau 325, Vivien, Guillaume, Étienne, Aurélien, Jérémie et les quelques stagiaires qui sont passés par là. Sans vous, QWOP, GIRP, Isaac, Jamestown et son mode farce, la thèse aurait été plus fade, mais sans moi, Carri et ses GPU l ambiance aurait été plus froide, j en suis sûr.

5 Je remercie aussi ceux qui ont su me faire découvrir l informatique d abord en tant que loisir et qui m ont donné envie d aller voir plus loin. Je pense en particulier aux communautés des sites PlayerAdvance et Dev-fr et en particulier à Gregory, alias Mollusk. Merci à vous, et merci à Nintendo et Sony d avoir conçu des machines qui m ont donné l envie d aller voir sous le capot. Enfin, je remercie ma famille qui m a accompagné jusqu ici et qui j en suis sûr me suivra encore à l avenir. Merci à mes parents qui m ont soutenu et qui ont su rester patients lorsqu il le fallait, mais qui m ont aussi toujours poussé à continuer. Merci à ma soeur, qui m a toujours écouté et aidé quand j en avais besoin. Merci à ma belle famille, qui a, elle aussi, su m apporter le soutien dont j avais parfois besoin. Enfin, merci à Sandrine, qui bien qu elle soit passée par là peu de temps avant, a su, à nouveau, vivre une thèse, cette fois-ci du côté de l accompagnant. Merci d avoir joué ce rôle à merveille, associant soutien moral, mais aussi soutien scientifique (merci pour les relectures et les répétitions). Merci d avoir, toi aussi, su me remotiver quand par moment c était difficile. Bien sûr, je remercie tous ceux qui m ont aidé de près ou de loin, qui m ont accompagné, d une manière ou d une autre. Tous ceux qui un jour ont permis que cette thèse soit un moment plus agréable ou qui ont su m apporter leur aide quand j en avais besoin, souvent sans même le savoir.

Table des matières Table des matières 6 Table des figures 9 Liste des tableaux 10 Avant-propos 13 1 Introduction 15 I Abstraction et performances................................... 17 I.1 Langages de programmation.............................. 17 I.2 Compilation et interprétation............................. 18 I.3 Typage........................................... 19 I.4 Programmation parallèle et hautes performances................. 20 I.5 Bibliothèques de programmation........................... 21 I.6 Conclusion........................................ 22 II Cartes graphiques et accélérateurs de calcul......................... 23 II.1 Historique......................................... 23 II.2 Processeurs généralistes et accélérateurs de calcul................. 25 II.3 Outils pour la programmation GPGPU........................ 28 II.4 Applications GPGPU................................... 32 II.5 Outils, bibliothèques et langages............................ 32 III Objectifs et contributions de cette thèse............................ 35 IV Plan de la thèse........................................... 36 2 Langages pour la programmation GPGPU : sémantique opérationnelle et typage 39 I Un langage pour le programme hôte : SPML......................... 41 I.1 Grammaire de SPML................................... 41 I.2 Quelles propriétés pour SPML?............................ 42 I.3 Sémantique opérationnelle de SPML......................... 44 I.4 Propriétés de SPML................................... 58 6

TABLE DES MATIÈRES 7 II Un langage pour les noyaux de calcul............................. 59 II.1 Quelles propriétés pour Sarek?............................. 59 II.2 Grammaire de Sarek................................... 60 II.3 Sémantique opérationnelle de Sarek......................... 62 II.4 Propriétés de Sarek.................................... 72 III Système de types.......................................... 73 III.1 Environnements et notations.............................. 73 III.2 Typage des expressions................................. 74 III.3 Liaison entre SPML et Sarek.............................. 77 IV Conclusion............................................. 78 3 Programmation GPGPU : implantation avec OCaml 79 I Choix d implantation....................................... 82 I.1 Pourquoi OCaml?.................................... 82 I.2 Pourquoi une bibliothèque?.............................. 82 II Présentation de la bibliothèque SPOC............................. 83 II.1 Unification des systèmes OpenCL et Cuda...................... 83 II.2 Transferts de données.................................. 86 II.3 Exemple de programme SPOC............................. 90 III Noyaux de calcul.......................................... 92 III.1 Noyaux de calcul externes................................ 92 III.2 Un langage intégré dédié aux noyaux : Sarek..................... 94 III.3 Différences avec le formalisme............................. 98 IV Tests de performance....................................... 99 IV.1 Petits exemples...................................... 100 IV.2 Bibliothèques optimisées................................ 102 IV.3 Comparaison avec d autres langages de haut niveau................ 103 IV.4 Calcul multi-gpu.................................... 105 V Cas d utilisation : portage du programme PROP....................... 107 V.1 Présentation du programme étudié.......................... 108 V.2 Portage avec SPOC.................................... 110 V.3 Résultats et performances............................... 111 VI Conclusion............................................. 113 4 Squelettes et composition 117 I Squelettes parallèles pour la programmation GPGPU.................... 119 II Implantation avec des noyaux externes............................ 123 III Implantation avec Sarek..................................... 126 IV Exemple d utilisation....................................... 127 V Tests de performance....................................... 128 VI Conclusion............................................. 130 5 Conclusion générale et perspectives 131 I Conclusion générale....................................... 133

8 TABLE DES MATIÈRES II Perspectives............................................ 136 II.1 Rapprocher Sarek d OCaml............................... 137 II.2 Squelettes et extensions................................. 137 II.3 Portage vers d autres langages............................. 138 III Perspectives offertes par l évolution du matériel et des systèmes............. 138 III.1 Évolution matérielle................................... 138 III.2 Cuda 5.5, OpenCL 2.0 : l avenir de la programmation GPGPU.......... 140 IV Vers une programmation GPGPU unifiée........................... 141 A Sémantique opérationnelle de SPML 143 I Grammaire du cœur de SPML.................................. 143 II Notations.............................................. 144 III Sémantique opérationnelle................................... 144 B Comparaison : Cuda, OpenCL et SPOC 147 I Initialisation............................................ 148 II Détection des dispositifs compatibles............................. 148 III Initialisation des données.................................... 150 IV Préparation du noyau de calcul................................. 150 V Allocation sur le dispositif.................................... 152 VI Transfert de données depuis l hôte vers le GPGPU...................... 152 VII Exécution du noyau de calcul.................................. 153 VIII Transfert des valeurs de retour vers l hôte........................... 154 IX Utilisation des résultats par l hôte................................ 154 Bibliographie 155 Acronymes 163 Glossaire 164

Table des figures 1.1 Grille modélisant l architecture mémoire des GPGPU...................... 31 2.1 Cœur de SPML............................................. 42 2.2 Partie GPGPU de SPML........................................ 42 2.3 Exemple : programme SPML..................................... 43 2.4 Grammaire de Sarek.......................................... 60 2.5 Exemple : module Sarek........................................ 61 2.6 Exemple : noyau de calcul en Sarek................................. 62 2.7 Exemple : Programme divergent qui ne termine pas....................... 69 2.8 Sarek : Types et environnements de typage............................ 73 3.1 Compatibilité des systèmes GPGPU implantés par différents constructeurs......... 84 3.2 Compatibilité système et matérielle de SPOC........................... 84 3.3 Type OCaml de la fonction d initialisation............................. 85 3.4 Type OCaml d un device........................................ 85 3.5 Type OCaml de la fonction de création de vecteurs........................ 87 3.6 Type OCaml des vecteurs paramétrables.............................. 88 3.7 Exemple : déclaration d un vecteur paramétré avec OCaml et C................ 89 3.8 Types OCaml des fonctions de transferts explicites........................ 89 3.9 Exemple : programme hôte avec transferts automatiques via OCaml et SPOC........ 91 3.10 Exemple : déclaration d un noyau GPGPU avec SPOC...................... 93 3.11 Type OCaml de la classe générée pour chaque noyau...................... 93 3.12 Compilation dynamique d un noyau GPGPU externe...................... 94 3.13 Addition de vecteur avec Sarek.................................... 95 3.14 Compilation statique de Sarek.................................... 96 3.15 Compilation dynamique de Sarek.................................. 97 3.16 Code CudaOpenCL généré depuis un noyau Sarek (fig.3.13).................. 99 3.17 Mesure de performances : Bibliothèques optimisées - Multiplication de matrices..... 102 3.18 Mesure de performances : Comparaison - Addition de vecteurs................ 104 3.19 Mesure de performances : Comparaison - Multiplication de matrices............. 105 3.20 Répartition des tâches sur systèmes multi-gpus......................... 107 9

3.21 Répartition des tâches et puissance théorique sur systèmes multi-gpus........... 108 3.22 Mesure de performances : PROP - Occupation mémoire cas Grand.............. 114 4.1 Définition OCaml de la classe skeleton............................... 123 4.2 Définition OCaml d un squelette Map avec un noyau de calcul externe............ 124 4.3 Types OCaml : Fonctions de manipulation des squelettes.................... 124 4.4 Exemple : utilisation de Map avec des constructions fonctionnelles.............. 125 4.5 Type OCaml : Fonction de compositions.............................. 125 4.6 Exemple : Squelettes avec Sarek................................... 126 4.7 Type OCaml : transformation de noyaux Sarek avec map.................... 126 4.8 Transformation de noyaux de calcul Sarek avec des squelettes................. 127 4.9 Exemples : Puissance itérée..................................... 129 A.1 Cœur de SPML............................................. 143 Liste des tableaux 1.1 Caractéristiques GPU - CPU..................................... 29 1.2 Comparaison des bandes passantes mémoire de plusieurs architectures........... 31 2.1 Primitives de SPML.......................................... 46 2.2 Opérations mémoire de SPML.................................... 47 2.3 Variables globales de Sarek dédiées à la programmation GPGPU................ 60 2.4 Bibliothèque d exécution de Sarek................................. 61 3.1 Schéma de compilation de Sarek vers IR.............................. 97 3.2 Extrait de la génération de code Cuda et OpenCL......................... 98 3.3 Mesure de performances : temps (en s) et accélérations..................... 101 3.4 Meusre de performances : Systèmes Multi-GPU utilisés..................... 105 3.5 Mesure de performances : Multi-GPU - Temps (en s) et accélérations............. 106 3.6 Mesure de performances : PROP - Caractéristiques des jeux de données........... 111 3.7 Mesure de performances : PROP - Cas Petit et Moyen...................... 112 3.8 Mesure de performances : PROP - Cas Grand........................... 112 4.1 Mesure de performances : Puissance itérée............................ 130 10

5.1 Mesure de performances : Comparaison - architecture multicœur............... 135

Avant-propos Cette thèse porte sur l étude d abstractions performantes pour les cartes graphiques qui sont normalement utilisées pour la gestion de l affichage et le traitement 3D dans les ordinateurs. Elles sont très performantes, mais aussi très spécialisées. Récemment, l usage des cartes graphiques a été détourné pour leur permettre de réaliser des calculs généralistes, normalement effectués par le processeur central de l ordinateur (Central Processing Unit (CPU)). Cette utilisation généraliste des unités de calculs graphiques (General Purpose Graphics Processing Unit (GPGPU) en anglais) demande une programmation spécifique de très bas niveau d abstraction dont le modèle est très proche de celui imposé par le matériel et qui demande de manipuler explicitement de nombreux paramètres matériels (comme la mémoire). Elle implique en particulier de réaliser de nombreux transferts de données entre la mémoire centrale de l ordinateur (associée au processeur central) et la mémoire du GPGPU. Par ailleurs, la programmation GPGPU demande d associer deux types de programmes (i) des programmes hôtes, qui sont exécutés pas le CPU et sont responsables des transferts mémoires et de la gestion des dispositifs GPGPU (qu on appellera simplement GPGPU par la suite), et (ii) des noyaux de calcul, qui sont des programmes assez courts exécutés par le GPGPU. La programmation GPGPU s appuie aujourd hui sur deux principaux systèmes, Compute Unified Device Architecture (Cuda) et Open Compute Language (OpenCL) qui sont très proches, mais incompatibles. Par ailleurs, certains dispositifs matériels ne sont exploitables qu avec l un de ces deux systèmes, ce qui rend difficile le développement d applications portables. Ceci rend l exploitation de l ensemble des performances des ordinateurs d autant plus complexe que ceux-ci sont de plus en plus hétérogènes, associant plusieurs GPGPU, parfois différents les uns des autres. Dans ce contexte, cette thèse s est attachée au développement d outils de haut niveau d abstraction pour la programmation GPGPU afin de la simplifier, tout en maintenant le haut niveau de performance qu elle peut offrir. Plus précisément, nos principaux objectifs étaient de libérer le programmeur de la gestion mémoire imposée par la programmation GPGPU, mais aussi d offrir une solution portable et hétérogène. Pour cela, nous avons tout d abord conçu un langage de programmation dédié à la programmation GPGPU et spécifié son comportement à travers sa sémantique opérationnelle. Par la suite, nous l avons implanté en utilisant le langage de programmation de haut niveau OCaml. Ainsi, la partie hôte correspond à une bibliothèque spécifique pour OCaml, SPOC, qui offre différentes propriétés : 13

14 AVANT PROPOS elle unifie les deux systèmes Cuda et OpenCL pour permettre le développement d applications portables, mais aussi hétérogènes, car elle permet de manipuler indifféremment et conjointement tout type de dispositifs GPGPU compatible avec au moins l un des deux systèmes. elle offre des transferts automatiques en associant à certains ensembles de données une information sur leur position dans le système (en mémoire CPU ou en mémoire GPU). Avec cette information, elle réalise automatiquement les transferts dès que les données sont utilisées à une nouvelle position. elle permet de manipuler des noyaux de calcul GPGPU externes, décrits avec les outils des systèmes Cuda ou OpenCL, et ainsi de profiter de bibliothèques de calculs optimisés. Pour accompagner cette bibliothèque, nous avons développé un langage intégré à OCaml, dédié à la description des noyaux de calcul, Sarek. Celui-ci se base sur les langages dédiés aux noyaux de calcul fournis avec les systèmes Cuda et OpenCL, mais avec une syntaxe proche de celle d OCaml et un système de type fort, associé à une vérification statique des types. De plus, ce langage est compilé, à la volée, en noyaux de calcul Cuda ou OpenCL en fonction du type de dispositif chargé d exécuter le noyau. Ainsi, le langage Sarek accroît la dimension portable et hétérogène de notre solution. Bien sûr, nous avons réalisé différents tests de notre solution. D abord, avec de petits exemples, pour vérifier que le niveau de performance était conservé, mais aussi pour vérifier qu elle offre effectivement portabilité et hétérogénéité. Par la suite, nous avons réalisé un portage d une application numérique complexe depuis Fortran et Cuda vers OCaml avec nos outils. Ainsi, nous avons vérifié que l application de nos outils permet effectivement d atteindre un très haut niveau de performance. SPOC et Sarek peuvent donc être utilisés à la fois par des programmeurs OCaml qui souhaitent utiliser des dispositifs GPGPU, mais aussi par la communauté du calcul numérique et plus particulièrement pour du calcul haute-performance. Afin d offrir davantage d abstractions, mais aussi de permettre d offrir des optimisations spécifiques, nous avons développé ces outils en nous attachant à les rendre facilement extensibles. Ainsi, la dernière partie de cette thèse décrit le développement de quelques squelettes de programmation pour la programmation GPGPU. Ces squelettes sont des constructions algorithmiques paramétrables qui permettent d automatiser certains calculs. Là encore, différents tests ont confirmé le haut niveau de performance atteint par notre implantation. Ce travail a été réalisé au Laboratoire d Informatique de Paris 6 (LIP6) de l Université Pierre et Marie Curie (UPMC, Paris), sous la direction des Pr. Emmanuel Chailloux et Jean-Luc Lamotte. Une partie de ces recherches a été réalisée dans le cadre du projet OpenGPU 1 du pôle Systematic. 1. www.opengpu.net

CHAPITRE 1 Introduction «We have in our possession a chip that could revolutionize medicine as we know it, by performing 100 billion operations a second (... ) and then we thought : hay! let s use it for games!» Publicité pour 3dfx Sommaire I Abstraction et performances................................ 17 I.1 Langages de programmation........................... 17 I.2 Compilation et interprétation.......................... 18 I.3 Typage........................................ 19 I.4 Programmation parallèle et hautes performances.............. 20 I.5 Bibliothèques de programmation........................ 21 I.6 Conclusion..................................... 22 II Cartes graphiques et accélérateurs de calcul...................... 23 II.1 Historique...................................... 23 II.2 Processeurs généralistes et accélérateurs de calcul.............. 25 II.3 Outils pour la programmation GPGPU..................... 28 II.4 Applications GPGPU................................ 32 II.5 Outils, bibliothèques et langages......................... 32 III Objectifs et contributions de cette thèse......................... 35 IV Plan de la thèse........................................ 36 15

16 CHAPITRE 1. INTRODUCTION

I. ABSTRACTION ET PERFORMANCES 17 CETTE THÈSE a pour objet l étude d abstractions performantes pour les cartes graphiques. Avant de présenter la programmation des cartes graphiques il est donc important de discuter des trois notions suivantes : abstractions, performances ainsi que programmation dont découlent les deux premières. I Abstraction et performances I.1 Langages de programmation Les programmes informatiques sont conçus pour s exécuter sur des ordinateurs dont l architecture est complexe. Ces machines ont une unité centrale de calcul (Central Processing Unit - CPU, en anglais) capable d exécuter du code spécifique, lié à leur architecture. Ce langage machine composé de 0 et de 1 représente une suite d instructions et de données à traiter. Il est très éloigné des langages naturels que manipulent les humains, et donc les programmeurs. Afin de simplifier la programmation, des langages de programmation ont été développés. Ces langages ont pour principal objectif d être manipulables par des humains pour décrire les instructions et les données composant un programme et d être exploitables sur des architectures matérielles spécifiques. Un programme exécutable sur une architecture ne sera pas obligatoirement exécutable sur une autre architecture. Le langage natif des machines peut-être considéré comme la méthode la plus concrète de programmer, il est dédié à un type de CPU. Les langages de programmation permettant d offrir davantage d abstractions, c est-à-dire d offrir des constructions plus éloignées des aspects matériels de l ordinateur et de libérer le programmeur de la gestion explicite de ces mêmes propriétés matérielles des ordinateurs, ils sont indépendants du type de CPU. Ils se décomposent en plusieurs catégories qui apportent différents niveaux d abstractions sur le matériel, les instructions des programmes ou sur les données. On s attachera ici au modèle de programmation séquentiel dans lequel un programme exécute l ensemble des instructions qui le composent dans un ordre prédéfini par la structure du programme. La majorité des langages de programmation de ce modèle se répartit en quatre grandes familles : les langages (i)impératifs, (ii)fonctionnels, (iii)orientés objet ou (iv)logiques. Les langages impératifs sont structurés sous forme de séquences d instructions manipulant la mémoire de l ordinateur. C est le modèle le plus proche du fonctionnement des machines. Il s appuie sur la modification (mutation) d états mémoire. Il implique donc souvent de manipuler explicitement les composants de la machine, en particulier la mémoire. Ce paradigme apporte peu d abstractions sur les opérations et la mémoire. Fortran, Pascal ou C sont des exemples de langages impératifs. Les langages fonctionnels permettent d abstraire le modèle d exécution de la machine en manipulant des fonctions plus proches du modèle mathématique. La mémoire n est plus modifiée par les instructions : les programmes sont constitués de fonctions qui via des valeurs d entrée, produisent de nouvelles valeurs en sortie, rejetant ou limitant la mutation des données. Le plus souvent, ils permettent d abstraire une partie des ressources matérielles de la machine, en particulier la mémoire qui y est gérée automatiquement. On citera les langages de la famille Lisp (Scheme, Common Lisp, Le Lisp,... ), ceux de la famille ML (OCaml, SML, F#,... ) et la famille des langages dits «paresseux» ou à évaluation retardée comme LazyML, Miranda ou Haskell comme représentants des langages fonctionnels.

18 CHAPITRE 1. INTRODUCTION Les langages orientés objet manipulent des objets, c est-à-dire des ensembles de données et de méthodes s appliquant sur ces données. La combinaison de différents objets permet de structurer le programme. Les langages objets à classe, comme Java, s appuient sur la notion d héritage entre classes pour permettre de spécialiser des parties du code. Les méthodes sont le plus souvent décrites en utilisant les paradigmes impératifs ou fonctionnels. SmallTalk, Java, C++, C#, JavaScript ou Python font partie de la famille des langages orientés objet. La programmation logique définit les programmes comme un ensemble de règles logiques. Ces règles sont ensuite automatiquement appliquées à un ensemble de faits élémentaires pour calculer la sortie du programme. Elle dépend de systèmes automatiques d application des règles. Ces systèmes simplifient la tâche du programmeur, mais peuvent s avérer coûteux en temps de calcul et surtout ne permettent pas de prédire facilement le comportement du programme au cours de son exécution. Le principal langage logique est Prolog. Les langages multiparadigmes permettent d accroître l abstraction en autorisant le programmeur à choisir son modèle de programmation et à en combiner plusieurs au sein d un même programme. Ils offrent souvent les abstractions sur la mémoire des langages fonctionnels tout en autorisant une programmation proche du modèle de la machine. On citera ici les langages OCaml, Python, Scala, Objective C, C++ et Java même si la plupart des langages modernes peuvent intégrer cette famille. I.2 Compilation et interprétation Les programmes écrits avec des langages de programmation sont naturellement incompréhensibles par un ordinateur. Afin de permettre l exécution d un programme par une machine, il est nécessaire d opérer une étape de traduction du langage de programmation vers le langage machine, ou vers des actions décrites en langage machine. Cette traduction peut principalement s opérer de deux manières : par compilation ou interprétation. La compilation consiste à utiliser un outil, le compilateur, capable de traduire l intégralité du programme en langage machine, avant l exécution. L interprétation consiste à utiliser un interprète pour lire le programme en exécutant, directement, au cours de la lecture, les opérations du programme. La méthode choisie peut influer sur la portabilité du programme, cest-à-dire sa capacité à s exécuter sur plusieurs machines d architectures différentes. Afin d exécuter un programme compilé sur plusieurs architectures, il est nécessaire d avoir un ou plusieurs compilateurs capables de cibler ces architectures. De la même manière, il est nécessaire de posséder un interprète pour chaque architecture dans le cas des langages interprétés. En général, l écriture d un interprète est plus simple que celle d un compilateur. Les langages interprétés sont considérés comme plus portables que les langages compilés. En contrepartie, l interprétation entraîne une charge supplémentaire pour l ordinateur et ralentit l exécution du programme. Les programmes compilés seront donc considérés comme plus performants, c est-à-dire qu ils exécuteront un même programme plus rapidement. De plus, la phase de compilation permet d analyser l ensemble du code, pour éventuellement produire un code machine optimisé plus performant. Elle permet aussi de détecter des erreurs avant l exécution du programme. Certains langages sont plus souvent utilisés compilés comme le langage C, tandis que d autres sont plutôt interprétés comme Scheme. Il reste cependant possible de développer des interprètes pour C comme Ch[1] ou TCC[2], ainsi que des compilateurs pour Scheme comme Bigloo[3]

I. ABSTRACTION ET PERFORMANCES 19 ou pour Python comme Cython[4]. Les deux approches peuvent aussi être associées. Il est désormais commun de manipuler des langages de programmation compilable vers du code machine ciblant des machines abstraites. Une machine abstraite est un modèle théorique qui représente un processeur et son jeu d instructions. Une machine abstraite ne représente le plus souvent pas un processeur réel. On utilisera alors des interprètes pour exécuter le programme. Ces interprètes vont exécuter le code de la machine abstraite, en utilisant un ensemble d opérations décrites en instructions-machine du processeur réel qui exécute l interprète. On les appelle des machines virtuelles. Cette approche permet de bénéficier de la compilation pour produire un code optimisé et concis tout en profitant des machines virtuelles pour offrir plus de portabilité. Par ailleurs, les interprètes sont désormais souvent capables de compiler des parties de code vers du code natif. Ce système de compilation à la volée (en anglais, jit - just in time) permet d apporter de meilleures performances à certaines sections des programmes interprétés. L utilisation de machines virtuelles capable de compiler du code à la volée permet d obtenir des performances très proches de celles obtenues avec les langages compilés tout en offrant une grande portabilité. On citera, par exemple, Java comme langage compilé pour une machine virtuelle avec compilation JIT. I.3 Typage Afin de garantir davantage de sureté d exécution, la plupart des langages de programmation utilisent des systèmes de types. Les types permettent de faire la différence entre les données, par exemple entre un nombre entier et une chaîne de caractères, mais aussi entre les opérations, une opération sur des nombres entiers sera différente d une opération sur des caractères, et ainsi de limiter l utilisation de ces opérations. Les systèmes de types permettent donc d apporter davantage de contrôle au programmeur sur les données qu il manipule et sur les opérations sur ces données. Les langages de programmation offrent différents systèmes de types, qui varient en fonction du moment où la vérification de type est effectuée (typage statique ou dynamique), et en fonction de la précision de cette vérification (typage faible ou fort). Typage statique et dynamique. Le typage statique vérifie lors de la phase de compilation que le programme est bien typé, c est-à-dire qu il n y a pas d opération sur des valeurs d un type incompatible. Le typage dynamique, au contraire, effectue la vérification au plus tard, lors de l exécution du programme. Au moment de réaliser une opération sur des valeurs, il va s assurer que leurs types sont bien compatibles avec l opération. On considère que le typage statique offre davantage de sécurité. En effet, il permet d assurer au programmeur que son programme ne déclenchera pas à l exécution d erreurs de typage et donc qu il ne s arrêtera pas brusquement pour cette raison. Il permet aussi de s affranchir de la vérification dynamique des types qui entraîne un surcoût en temps de calcul dans l exécution du programme. En contrepartie, il est plus compliqué à mettre en œuvre dans les langages et peut refuser des programmes qui s exécuteraient correctement. Cependant, c est, la plupart du temps, un atout pour les programmeurs, car il permet de détecter, au plus tôt (avant l exécution du programme), beaucoup d erreurs autrement très difficiles à corriger. Les langages C, Java et OCaml sont des langages typés statiquement ; Scheme, Smalltalk ou Python sont des exemples de langages typés dynamiquement.

20 CHAPITRE 1. INTRODUCTION Typage faible et fort. Les systèmes de types sont souvent qualifiés de forts ou faibles. Un système de type fort est un système de type qui rejette toute opération sur des données incompatibles. Au contraire, un système de type faible va autoriser certaines opérations sur des types proches des types compatibles. Les systèmes de types forts associés au typage statique sont ceux qui rejetteront le plus de programmes, mais permettront de détecter le maximum d erreur. En contrepartie, un système de type dynamique plus faible permettra au programmeur de contourner les limites du typage pour écrire des instructions autrement interdites. Même avec un système de types fort, il reste possible de définir des fonctions génériques, capables de manipuler différents types de données. Différentes solutions existent pour mettre en place cette généricité comme le polymorphisme ou la surcharge. Le polymorphisme permet de définir des fonctions qui seront compatibles avec tout type de données et pourront renvoyer différents types de données. Par exemple, une fonction de création de couples pourra associer tout type de données et produire un couple, dépendant du type de ses deux paramètres. Elle pourra ainsi associer un nombre entier avec une chaîne de caractères ou bien un caractère seul avec un autre couple. La surcharge permettra par exemple en Java de définir différentes fonctions, ayant le même nom, mais prenant différents types de paramètres, et offrant un traitement différent, en fonction des paramètres. Ainsi, le compilateur, ou l interprète sélectionneront la fonction la plus adaptée en fonction des paramètres utilisés. Parmi les langages typés statiquement on retrouvera des systèmes de types faibles comme avec le langage C et des systèmes de types forts comme avec OCaml. Il en est de même pour les langages typés dynamiquement, Ruby s appuie sur un système de types fort, JavaScript sur un système de types faible. I.4 Programmation parallèle et hautes performances Afin d accélérer matériellement l exécution des programmes, il existe deux principales solutions. (i)augmenter la fréquence d exécution de l unité de calcul (le processeur) et ainsi le nombre d opérations qu il peut effectuer par secondes ou alors (ii)multiplier le nombre d unités de calcul pour permettre l exécution de plusieurs tâches en parallèle. Là où la première solution ne demande aucun travail au programmeur, la seconde demande de correctement exploiter les multiples processeurs et donc d écrire un code spécifique. L augmentation de fréquence a actuellement atteint une limite difficile à franchir (principalement pour des raisons de dissipation de chaleur) et c est donc le parallélisme qui est utilisé désormais pour augmenter les performances. Il existe plusieurs types d architectures parallèles : les systèmes à mémoire partagée et les systèmes distribués. Ces deux systèmes se programment différemment. En mémoire partagée, les différentes unités de calcul partagent un même espace mémoire. Il est nécessaire de s assurer que les différents processeurs n écrivent pas au même moment dans le même emplacement mémoire. En mémoire distribuée, chaque unité de calcul possède son propre espace mémoire. Il est nécessaire d organiser la copie des données entre les différents espaces mémoires. Chaque modèle impose d appliquer des stratégies de synchronisation et d ordonnancement des tâches particulières qui complexifient énormément l écriture des programmes. On peut observer une relation entre architectures parallèles et modèles de programmation. La programmation impérative modifie directement la mémoire et s approche davantage de la mémoire partagée. Au contraire, la programmation fonctionnelle travaille davantage par copie et partage de valeurs et s approche de ce fait des systèmes distribués. De même, on préférera associer

I. ABSTRACTION ET PERFORMANCES 21 différents types de calculs à une famille de langages. En particulier, on différenciera le calcul numérique, qui s attache à l approximation numérique de problèmes mathématiques, du calcul symbolique qui s attache à la manipulation d expressions mathématiques. Le premier manipule des ensembles de données de très grande taille dont la copie n est pas envisageable et préférera les langages impératifs à effet de bord, tandis que le second préférera les langages fonctionnels qui travaillent par copie. Dans le cadre de la programmation parallèle, nous distinguerons la programmation dite hauteperformance. En effet, l informatique et la programmation s attachent à de multiples secteurs et on ne programme pas de la même manière des sites internet, des jeux vidéo ou des machines-outils. La programmation haute-performance s attache à l exploitation d architectures particulières pour obtenir les plus hautes performances sur des applications de calcul numérique scientifique. Elle est souvent associée à l utilisation de supercalculateurs. Les supercalculateurs sont des systèmes complexes, souvent composés de multiples ordinateurs (des nœuds) reliés entre eux. Chaque nœud possède lui même une architecture complexe (plusieurs unités de calcul). Ils associent architecture distribuée entre les différents nœuds et partagée au sein de chaque nœud du calculateur. Dans ce cadre, il est commun de cibler une seule architecture (un seul supercalculateur) pour l écriture des programmes. Les programmeurs utilisent alors des outils très proches du langage machine pour exploiter au mieux leur architecture cible. Ce travail d optimisation, souvent réalisé à la main, par des programmeurs experts du domaine et spécialistes de l architecture cible, permet d atteindre un haut niveau de performances des supercalculateurs. Certains programmes sont compatibles avec plusieurs cibles, ceci demande un travail d optimisation supplémentaire pour chaque cible. La portabilité est donc particulièrement difficile (et coûteuse) si on veut maintenir un haut niveau de performances. La programmation haute-performance a donc tendance à s attacher principalement à l optimisation et aux performances limitant l utilisation d outils de haut niveau d abstraction. I.5 Bibliothèques de programmation I.5.a Bibliothèques Les langages de programmation sont souvent accompagnés de bibliothèques. Ces bibliothèques sont des ensembles d opérations utilisables par les programmeurs. Ces opérations permettent de simplifier la programmation ou de profiter d optimisations particulières. Les bibliothèques participent à la montée en abstraction des langages de programmation en réduisant l impact des aspects matériels sur les programmes. Elles permettent d abstraire des opérations en cachant au programmeur toute la complexité qu elles contiennent. Il existe par exemple plusieurs méthodes pour multiplier des matrices, les versions les plus performantes sont très complexes à mettre en œuvre, et nécessitent de maîtriser au plus près l architecture matérielle des ordinateurs. [5, 6] et [7] présentent par exemple des optimisations particulières destinées à accroitre les performances des multiplications de matrices sur une architecture de carte graphique particulière. Néanmoins, de nombreuses bibliothèques existent pour permettre aux programmeurs de profiter de hautes performances dans leur langage favori. On pourra aussi noter que le support des architectures parallèles peut être apporté à un langage à travers des bibliothèques. Par exemple, la bibliothèque Message Passing Interface (MPI)[8], qui existe pour de nombreux langages permet l écriture de programmes ciblant les archi-

22 CHAPITRE 1. INTRODUCTION tectures distribuées. De son côté, Open Multi Processing (OpenMP)[9] inclut une bibliothèque pour l écriture de programmes parallèles en mémoire partagée. I.5.b Squelettes algorithmiques Afin de décrire un algorithme complexe, il est commun d appliquer des «recettes» connues. Dans le domaine de l ingénierie logicielle, on appelle ces recettes des patrons de conception (design patterns en anglais). Ils permettent de structurer les programmes pour répondre à un problème donné. Ils peuvent être spécialisés dans la résolution d un calcul complexe ou dans l utilisation d architectures difficiles à exploiter. Pour automatiser leur utilisation et simplifier davantage la tâche des programmeurs, certains patrons ont été transformés en fonctions paramétrables, c est-à-dire des constructions algorithmiques, qui à l aide de certains paramètres pourront reproduire l ensemble du patron sélectionné. Ces constructions algorithmiques, souvent regroupées dans des bibliothèques, sont appelées des squelettes algorithmiques[10, 11, 12, 13]. Il existe deux principaux types de squelettes, les squelettes de données et les squelettes de tâches[14]. Les premiers exploitent des structures de données distribuées entre les unités de calcul pour produire du parallélisme. On pourra citer le squelette map qui à partir d un ensemble de données, applique un même calcul à chaque élément de l ensemble pour produire un nouvel ensemble. Les seconds s appuient sur les relations entre différentes tâches pour paralléliser ce qui peut l être. Par exemple, le squelette pipe associe deux tâches en utilisant le résultat de la première comme données d entrée de la seconde. Les squelettes permettent de simplifier l écriture des programmes, mais aussi de profiter de constructions optimisées pour la résolution de l algorithme qu ils implantent. I.6 Conclusion La notion d abstraction regroupe l ensemble des possibilités offertes aux programmeurs pour s affranchir des notions concrètes liées aux machines. En particulier, l utilisation de langages de programmation permet de s éloigner du langage machine très difficile à manipuler pour un être humain. L utilisation d un langage multiparadigme permet aussi au programmeur de bénéficier d un large choix de méthodes de programmation. L utilisation de compilateurs, d interprètes et de machines virtuelles libère le programmeur de la nécessité d écrire de multiples programmes pour cibler des architectures différentes. Les langages de programmation permettent aussi via les systèmes de type de structurer les données des programmes. Le typage statique vérifie par ailleurs que le programme satisfait les contraintes de type ce qui limite les erreurs. Certains langages apportent davantage d abstractions en offrant une manipulation automatique de composants de la machine, comme la mémoire. Certains offrent aussi des constructions pour simplifier l écriture des programmes et automatiser des tâches autrement plus complexes. Les bibliothèques et squelettes de programmation permettent d accroître encore le niveau d abstraction en s attachant cette fois à l automatisation des calculs et de leur organisation au sein du programme. D un autre côté, la recherche de performance s attache souvent à l optimisation des programmes pour des architectures données. La recherche des performances passe souvent par l utilisation d outils de bas niveau d abstraction, et il est souvent considéré que l utilisation de langages de très haut niveau limite l accès à de bonnes performances, car ils empêchent d optimiser le programme manuellement au plus près du langage machine. En pratique, le domaine de la programmation haute-

II. CARTES GRAPHIQUES ET ACCÉLÉRATEURS DE CALCUL 23 performance s attache principalement au calcul numérique et préférera utiliser des langages impératifs avec gestion manuelle de la mémoire. Néanmoins, les bibliothèques de calculs optimisés et les squelettes algorithmiques permettent d aider les programmeurs à obtenir de hauts niveaux de performance sans empêcher l accès aux outils de haut niveau d abstraction. Dans cette thèse, nous verrons que les cartes graphiques ont des architectures très proches de celles des supercalculateurs et que les méthodes de programmations qui leur sont associées sont d assez bas niveau d abstraction. L objectif de cette thèse est donc d étudier le développement d outils de plus haut niveau d abstraction tout en se focalisant sur les performances dans le cadre de la programmation des cartes graphiques. II Cartes graphiques et accélérateurs de calcul Cette thèse a pour objectif l étude d abstractions performantes pour la programmation des cartes graphiques. En effet, les cartes graphiques sont des périphériques complexes à programmer, mais capables de délivrer de très hautes performances. Il paraît donc nécessaire d offrir des outils de haut niveau d abstraction pour simplifier leur utilisation tout en s assurant de conserver un haut niveau de performance. Dans ce cadre, il est important de distinguer les cartes graphiques (Graphics Processing Unit (GPU), en anglais) des CPU, ainsi que les méthodes de programmation qui leur sont associées. Pour ce faire, nous allons dresser un rapide historique des cartes graphiques et de leur programmation. Nous étudierons par la suite le cas plus général des accélérateurs de calcul (dont font partie les cartes graphiques), d une part, car ils sont concurrents des cartes graphiques pour le calcul hautes-performances et d autre part, car les méthodes de programmation qui leur sont associées sont proches et tendent à converger vers des outils communs. II.1 Historique Naissance. Les cartes graphiques ont été développées dans les années 1980 dans le but de soulager les processeurs de la charge que demandaient la gestion de l affichage de texte et, plus tard, la gestion séparée de chaque pixel de l écran. Elles sont des processeurs secondaires connectés au CPU et dotés de leur propre espace mémoire. Avec la demande de plus en plus importante en matière de graphisme incluant de la couleur, des animations et un nombre de pixels à afficher de plus en plus important, les cartes graphiques ont commencé à se perfectionner. Chargées principalement du calcul et du transfert des images produites par l ordinateur vers l écran, elles ont évolué pour permettre l affichage (et le calcul) d images en trois dimensions. La 3D est une activité très coûteuse en calcul, en particulier dans le cas d opérations en temps réel comme dans les jeux vidéo. C est sur ce créneau, et sur celui du dessin assisté par ordinateur qu elles vont se spécialiser. Le jeu vidéo 3D a permis la démocratisation des cartes graphiques en apportant la diffusion de masse et donc la baisse des coûts. Les environnements 3D vont se complexifier, ajoutant des textures aux modèles géométriques. Ces textures, des images à appliquer sur tout ou partie de l environnement 3D, demandent une importante quantité de mémoire ainsi qu une grande vitesse de transfert entre le CPU et la carte graphique. En effet, même si la carte graphique s occupe de l affichage, c est le CPU qui organise les tâches qu elle doit exécuter, et qui définit les données à afficher. L importante demande en matière de jeux vidéo va accélérer l évolution des cartes graphiques,

24 CHAPITRE 1. INTRODUCTION leur offrant d un côté plus de capacité de calcul pour davantage d effets en temps réel, et d un autre côté plus de mémoire pour augmenter la quantité d information à traiterafficher à l écran. La bande passante entre le CPU et la carte graphique va elle aussi être augmentée, passant des bus ISAVESA à PCI puis AGP et maintenant PCI-Express. De la même manière, l organisation de la mémoire intégrée dans les cartes graphiques va se hiérarchiser et se complexifier pour offrir des débits de plus en plus importants. Shaders et programmation des cartes graphiques. Bien qu on puisse définir un ensemble de tâches à faire exécuter par la carte graphique, ces tâches restaient prédéfinies et non programmables. La création d effets complexes et non prédéfinis demandait une forte implication du CPU dans les calculs et donc une importante perte de performances. En effet, enfin de proposer davantage de réalisme, les applications 3D ont commencé à intègrer de plus en plus de code numérique issu de la physique (gestion de drapés de vêtements, effet de vent, écoulement d eau, effet de lumière complexes, destruction de décors, etc). Afin de donner plus de flexibilité aux programmeurs et de permettre davantage d effets graphiques sans exploiter les CPU, la société Pixar définit en 1988 un langage, le shading language[15], pour décrire des opérations manipulant des éléments 3D depuis le GPU, des shaders. Un shader est un programme dédié au traitement graphique, qui sera exécuté par la carte graphique. Il permet de manipuler les données graphiques (pixel, vertex) directement depuis la carte graphique et de profiter des multiples unités de calcul qu elle possède. D abord dédiés à la génération d effets visuels précalculés, les shaders ont fini par devenir des programmes utilisables en temps réel. Différentes implantations de shading language ont vu le jour. Les trois principales étant HLSL (High Level Shader Language)[16] proposé par Microsoft dans son Application Programming Interface (API) Direct3D, OpenGL Shading Language (GLSL)[17] proposé par l OpenGL Architecture Review Board dans l API Open Graphic Library (OpenGL) et Cg (C for Graphics)[18] proposé par l entreprise NVidia. Ces trois langages, très proches du langage C, permettent la manipulation de pixels, textures et modèles 3D depuis la carte graphique, en temps réel. NVidia a proposé en 2001 la première carte graphique grand public capable d exploiter des shaders, via des unités de calcul dédiées, la Geforce 3. On distingue trois familles de shaders, les vertex shaders qui manipulent la position des points des éléments 3D dans l espace, les geometry shaders qui manipulent directement des ensembles de points et les pixel shaders qui manipulent les couleurs à afficher. Ces trois types de shaders sont appliqués les uns après les autres au sein du pipeline des cartes graphiques. Ceci a poussé à l ajout d unités de calcul supplémentaires, dédiées aux shaders, dans les cartes graphiques et a permis un accès simplifié au pipeline graphique pour les programmeurs. En effet, il est alors devenu possible de décrire des séries d opérations à réaliser sur les cartes graphiques et de combiner ces opérations entre elles. Les cartes graphiques sont depuis constituées de nombreuses unités de calcul et possèdent un espace mémoire important. C est avec l arrivée d unités de calcul sur les valeurs flottantes sur les cartes graphiques qu on a commencé à s intéresser à leur utilisation pour la résolution de problèmes numériques. En effet, nombre d entre eux sont modélisables sous la forme de calculs à réaliser sur des matrices. Or c est justement ce que permettent de faire les shading languages, et ce, avec des performances parfois nettement supérieures à celle des CPU classiques. La programmation généraliste sur unité de calcul graphique (GPGPU) était née.

II. CARTES GRAPHIQUES ET ACCÉLÉRATEURS DE CALCUL 25 Programmation GPGPU. Afin de les exploiter correctement, les programmeurs doivent travestir leurs opérations scientifiques sous la forme d opérations graphiques, manipulant leurs matrices de résultats sous la forme de textures et modèles 3D. Cette opération de programmation est particulièrement lourde et rend la tâche très difficile. Les constructeurs de cartes graphiques, pour cibler ce nouveau marché, ont alors proposé des outils, des Software Development Kit (SDK), pour aider dans cette tâche. On notera en particulier BrookGPU [19] proposé en 2003 par Stanford University et Close To Metal (plus tard transformé en Stream SDK) puis renommé en Accelerated Parallel Processing (APP)-SDK[20], [21] développé en 2006 par ATI. Ces deux langages ciblent directement la communauté du calcul numérique, permettant l expression de calculs numériques et les transformant en shaders pour les exécuter sur les cartes graphiques. Néanmoins, ces deux outils restent de très bas niveau d abstraction et sont limités par les possibilités des shaders. Avec l amélioration des performances des cartes graphiques, et leur coût relativement faible, la communauté scientifique s est davantage intéressée à leur utilisation. Cependant, le manque d outils dédiés et la relative difficulté de programmation restaient un frein pour une adoption plus massive. Afin de pallier cela, NVidia publie en 2007 le kit de développement Cuda[22]. Celui-ci permet de manipuler les cartes graphiques plus simplement, mais aussi plus précisément. Il permet en particulier de manipuler l ensemble de la mémoire des cartes graphiques et autorise une gestion plus fine de l ordonnancement des commandes à exécuter qu avec les outils existants. En 2008, le Khronos Group (désormais responsable du standard OpenGL), sous l impulsion de la société Apple, développe en association avec plus d une vingtaine de grands constructeurs et d universités la spécification du standard OpenCL[23]. Comme Cuda, OpenCL est une API associée à un langage de programmation dédié à la programmation GPGPU. C est courant 2009 que les premières implantations d OpenCL commencent à voir le jour. Aujourd hui, OpenCL et Cuda sont les principaux systèmes disponibles pour le développement GPGPU. Microsoft a depuis proposé DirectCompute[24] qui s intègre à son API graphique DirectX, tandis que Google développe RenderScript pour son système d exploitation Android. II.2 Processeurs généralistes et accélérateurs de calcul Processeurs multicœurs. Les scientifiques se sont intéressés à la programmation des cartes graphiques, car elles sont dimensionnées pour être performantes en calcul numérique et offrent un rapport performancecoût et performanceconsommation supérieur aux autres solutions existantes. Parallèlement à l évolution des cartes graphiques, les fabriquants de processeurs (fondeurs) se sont concentrés à améliorer les performances. Pour ce faire, ils ont d abord cherché à augmenter la fréquence des processeurs. Cette amélioration permet d accroître le nombre d instructions qu un processeur peut exécuter par seconde et permet donc d obtenir de meilleures performances sans modifier les programmes. En 1975, Gordon Moore postule que le nombre de transistors sur un microprocesseur double tous les deux ans [25]. Ce postulat, la seconde loi de Moore, est toujours valide actuellement. Cependant, ces dernières années, l augmentation de fréquence des processeurs est devenue difficile à réaliser, car elle entraîne une augmentation de la consommation des composants, mais aussi, et surtout, une augmentation de la chaleur produite par le processeur. Afin de continuer à satisfaire les besoins des utilisateurs et ainsi améliorer les performances des ordinateurs, les constructeurs ont commencé à proposer des puces proposant des architectures multicœurs, c est-à-

26 CHAPITRE 1. INTRODUCTION dire contenant plusieurs unités de calcul «autonomes». Les premiers processeurs x86 (l architecture la plus utilisée sur les ordinateurs personnels) multicœurs sont apparus en 2005. Bien que permettant d effectuer plusieurs opérations en parallèle, et donc d accroître les performances globales du système, ils demandent aux programmeurs de modifier leurs programmes pour en bénéficier. Parallèlement à l augmentation de fréquence et à la multiplication des cœurs, d autres méthodes ont été employées pour offrir plus de performances aux ordinateurs. On notera en particulier l utilisation de coprocesseurs, c est-à-dire d unités de calcul spécialisées. Ces unités sont dédiées à des traitements particuliers. Par exemple, l unité de calcul flottant (Floating Point Unit (FPU)) permet d accélérer des opérations mathématiques sur les flottants. Une autre solution consiste à intégrer dans les microprocesseurs des capacités de calcul vectoriel, c est-à-dire la possibilité de traiter un vecteur de plusieurs valeurs en une seule instruction. Cette technique, appelée Single Instruction Multiple Data (SIMD), permet d accroître les performances, mais demande de modifier les programmes pour manipuler des vecteurs au lieu de scalaires (valeurs uniques). Parmi les jeux d instructions SIMD les plus connus, on notera Streaming SIMD Extensions (SSE)[26] pour l architecture processeur x86, Altivec[27] pour PowerPC et le récent Advanced Vector Extensions (AVX)[28] pour les derniers processeurs x86 64-bit d Intel. Pour bénéficier de ces nouvelles instructions, il faut soit travailler au niveau assembleur, soit attendre les modifications des compilateurs qui généreront des codes les intégrant. Certaines extensions sont aussi accessibles à travers des bibliothèques Vecmathlib[29] qui donne accès à des fonctions vectorielles bénéficiant des accélérations SSE at AVX depuis C++ ou bien CamlG4[30] qui permet de profiter des extensions AltiVec depuis OCaml. Afin de simplifier la tâche des programmeurs, les outils se sont adaptés à ces évolutions. En particulier, compilateurs et bibliothèques ont été modifiés pour respectivement transformer du code pour tirer parti des nouvelles architectures complexes et offrir des fonctions pré-optimisées. On notera ici en particulier le cas de la vectorisation automatique[31] qui demande au compilateur de détecter les suites d opérations scalaires transformables en opérations vectorielles, et de l autre côté l ensemble des bibliothèques mathématiques d algèbre linéaire Basic Linear Algebra Subprograms (BLAS)[32] et Linear Algebra PACKage (LAPACK)[33] qui sont développées pour chaque architecture afin d offrir les meilleures performances. Accélérateurs de calcul. Pour améliorer davantage les performances, les constructeurs ont développé des accélérateurs de calculs. Ces accélérateurs, semblables aux coprocesseurs, ont pour mission de permettre l exécution très rapide de calculs spécifiques. De même que pour les cartes graphiques, ces dernières années ont vu l apparition d accélérateurs de calculs généralistes et programmables. Dans ce contexte, on notera en particulier le cas du CELL BE[34] qui en 2008 a permis au supercalculateur Roadrunner d être le premier à atteindre le petaflop[35], des FPGA[36] (Field- Programmable Gate Array - circuit logique programmable) qui la même année permirent au Cray XT5 Jaguar[37] d atteindre lui aussi le petaflop en se plaçant en deuxième place du top500[38] et des systèmes hybrides qui combinent CPU et GPGPU sur une seule puce. Le CELL est un processeur combinant un processeur Power PC, le Power Processing Element (PPE), et, plusieurs coprocesseurs, les Synergestic Processing Element (SPE). L ensemble est connecté via un bus très rapide, l Element Interconnect Bus (EIB), dont la bande passante théorique est supérieure à 300GBs. Les performances théoriques du PPE comme des SPE sont de 25,6 Giga (10 9 ) FLoating point Operations Per Second (GFLOPS). Un CELL pouvant contenir jusqu à 8 SPE offrait

II. CARTES GRAPHIQUES ET ACCÉLÉRATEURS DE CALCUL 27 des performances théoriques de 230,4GFLOPS en simple précision et 20,8 GFLOPS en double précision. En comparaison, la même année (2008), Intel propose l Itanium Montecito dont les performances en simple précision atteignent 25,6 GFLOPS et 12,8GFLOPS en double précision, avec une bande passante mémoire de 21,6GBs. Bien que proposant des performances bien supérieures aux CPU classiques de l époque, le CELL est très difficile à programmer. En effet, le modèle de programmation s approche de la programmation parallèle en mémoire distribuée, avec les SPE considérés comme des nœuds séparés du PPE. La mémoire locale des SPE est très faible (256ko) et oblige les programmeurs à organiser leur programme pour alimenter les SPE en calcul et en données à traiter en continu. Pour profiter de la grande vitesse de l EIB, il est aussi nécessaire d organiser des communications à la fois entre PPE et SPE, mais aussi entre les SPE. De plus, le modèle de communication entre SPE et avec le PPE est très limité. Les outils développés autour du CELL sont très rares, et consistent principalement en des bibliothèques de calculs optimisés. Les outils proposés ne sont pas dans la norme et restent extrêmement complexes à utiliser. On pourra néanmoins citer la bibliothèque SKELL BE[39] qui permet de décrire et composer des squelettes parallèles ciblant le processeur CELL avec le langage C++. Par ailleurs, la recherche des performances nécessite de connaître parfaitement le fonctionnement des SPE et l ensemble des techniques d optimisation de calcul vectoriel. Très difficilement exploitable sans formation spécifique et approfondie, demandant l écriture de codes spécifiques et donc nouveaux et très peu portables, il est resté très peu utilisé du côté du calcul numérique. Le CELL sera malgré tout très répandu en particulier dans le domaine du jeu vidéo, car c est le processeur qui équipe la console de jeu PlayStation 3 de Sony. À travers elle, il a aussi permis de nombre d universités et étudiants de s équiper avec un processeur performant et complexe, à moindre coût[40]. Les Field-Programmable Gate Array (circuit logique programmable) (FPGA) sont des circuits logiques programmables, c est à dire des circuits intégrés qu on peut reprogrammer après leur fabrication. Ils sont constitués de nombreux blocs logiques (jusqu à quelques millions) connectés entre eux. L utilisateur peut programmer la fonction de chaque bloc logique ainsi que les interconnexions entre les blocs. Ceci permet de spécialiser le comportement de la puce à un problème donné, et de profiter du grand nombre de blocs logiques pour développer des solutions parallèles très performantes. Cependant, les FPGA offrent en général des performances plus faibles qu un circuit équivalent non reconfigurable et surtout ne sont programmables qu avec des langages de description matérielle de très bas niveau d abstraction. Comme pour le CELL, il existe des bibliothèques optimisées pour les FPGA. Les langages utilisés pour programmer les FPGA sont d assez bas niveau comme Verilog[41] ou VHDL[42]. Le langage SystemC[43] permet de décrire les programmes à l aide d un ensemble de classes C++. D autres travaux ont été réalisés pour permettre la manipulation des FPGA avec des langages dédiés comme le langage CAPH[44] permettant d offrir davantage d abstraction sur les unités de calcul et les données à manipuler. Avec l utilisation des GPGPU comme accélérateurs de calculs, des constructeurs ont proposé des puces hybrides associant un CPU multicœur classique avec un GPGPU. C est en particulier le cas des processeurs Sandy Bridge et Haswell d Intel et des APU d AMD. Ces puces permettent de bénéficier des performances des GPGPU tout en limitant la complexité pour le programmeur. En particulier pour les derniers APU d AMD, pour lesquels la mémoire est unifiée et commune au CPU et au GPGPU (Hybrid Uniform Memory Access), permettant de s affranchir des transferts mémoires imposés par la programmation GPGPU classique. Cependant, la puissance des GPGPU intégrés à ces puces reste

28 CHAPITRE 1. INTRODUCTION limitée au regard de celle offerte par les GPGPU dédiés, de même génération. Ce type de processeur est exploitable via OpenCL, mais demande des optimisations particulières, différentes de celles utilisées pour les GPGPU seuls, pour offrir un maximum de performances. À nouveau, cette technologie s appuie sur le jeu vidéo 3D pour se démocratiser puisqu elle équipera la prochaine PlayStation 4 de Sony. L apparition des systèmes multicœurs, la complexification des processeurs et l ajout d accélérateurs de calculs, parfois même associé au CPU au sein de la même puce, ont considérablement augmenté la complexité des programmes. Bien qu offrant de très bonnes performances, le CELL ou les FPGA n offrent pas d outils d assez haut niveau pour être exploitables massivement par les programmeurs. Le coût de développement d une application sur ce type d architecture a poussé à étudier d autres solutions, comme les cartes graphiques. En effet, elles sont bien connues (au moins dans le monde du graphisme) et offrent de bonnes performances. De nos jours, tout ordinateur personnel ou smartphone possède une carte graphique, ce qui facilite aussi leur démocratisation. Cependant, comme nous l avons vu pour le CELL et les FPGA ce sont aussi les outils et le modèle de programmation qui permettent aux programmeurs d utiliser de nouvelles architectures. Il est par ailleurs intéressant de noter que des outils dédiés à la programmation des cartes graphiques peuvent être utilisés pour exploiter d autres accélérateurs de calcul. C est en particulier le cas d OpenCL qui permet de cibler cartes graphiques, processeurs multicœurs, accélérateurs (comme le CELL et les FPGA) et systèmes hybrides. II.3 Outils pour la programmation GPGPU De nos jours, les cartes graphiques sont des architectures très différentes des CPU classiques, mais qui offrent un très haut niveau de performance. La table 1.1 présente différentes caractéristiques des GPU et les compare aux CPU multicœurs classiques. En particulier, les GPU possèdent un bien plus grand nombre d unités de calcul, fonctionnant à une fréquence un peu plus faible que celles des CPU. Ils possèdent désormais des quantités de mémoire très proches, mais la mémoire des GPU offre une bande passante bien plus élevée. Ces différences matérielles permettent aux GPU d atteindre des niveaux de performances plusieurs dizaines de fois plus élevés qu avec les CPU. Cependant, les unités de calcul des GPU sont très simples, et ne possèdent pas d éléments complexes comme, par exemple, les unités de prédiction de branchement, qu on peut retrouver sur les CPU. Afin d effectuer des calculs plus généralistes avec les GPU des outils dédiés ont été développés : Cuda et OpenCL. Cuda est développé par la société NVidia. C est un système propriétaire qui n est utilisable, par défaut, que sur les architectures vendues par NVidia. C est un système qui combine une API et une extension des langages C et C++. L extension de langage permet la définition des programmes qui s exécuteront sur les GPU, qu on appelle des noyaux. L API permet la manipulation des noyaux et des données nécessaires aux calculs. NVidia propose deux API incompatibles pour CudaL : l API Driver de très bas niveau et l API Runtime qui s appuie sur la première en proposant des fonctions de plus haut niveau pour simplifier l utilisation de Cuda. Elles sont globalement équivalentes, la seconde offrant du sucre syntaxique pour simplifier l écriture des programmes et automatisant certains traitements. Cependant, pour une utilisation fine des GPGPU, par exemple pour ordonnancer l exécution de différents noyaux de calcul avec plusieurs threads CPU, il est indispensable d utiliser

II. CARTES GRAPHIQUES ET ACCÉLÉRATEURS DE CALCUL 29 # Unités de calcul CPU GPU Mobile GPU Desktop GPU HPC i7-3770k Geforce Geforce Radeon Tesla GTX 680M GTX 680 7970HD K20X 4 1344 1536 1536 2688 Fréquence 3,50GHz 720 MHz 1,01GHz 925MHz 732 MHz Mémoire max 32GB 2GB 2GB 6GB 6GB Bande passante mémoire GFLOPS simple précision GFLOPS double précision 25,6GBs 115,2 GBs 192,2GBs 264GBs 250GBs 225 952,3 3090 4300 3950 112 59,11-119 128 1075 1310 TABLE 1.1 : Caractéristiques GPU - CPU l API Driver de plus bas niveau. Une autre différence vient du mode d installation des deux bibliothèques, la première est utilisable sur tout ordinateur possédant un driver propriétaire NVidia pour sa carte graphique tandis que la seconde demande l installation des bibliothèques de développement Cuda. Il existe un grand nombre de bibliothèques de calcul pour Cuda, on citera les principales bibliothèques d algèbre linéaire, certaines propriétaires comme CUBLAS[45] et CULA[46], d autres libres comme MAGMA[47]. OpenCL est un standard, développé par un consortium industriel, le Khronos Group. C est le même groupe qui est responsable des standards OpenGL, WebGL (OpenGL pour navigateurs web) et WebCL (OpenCL pour navigateurs). OpenCL vise à permettre le développement d application GPGPU pour un ensemble d architectures allant de la carte graphique au processeur classique, en passant par des accélérateurs particuliers comme les FPGA ou le processeur CELL BE. Dans ce cadre, OpenCL permet davantage de portabilité que Cuda. Cependant, les architectures ciblées pouvant être très différentes, il est souvent nécessaire de modifier le code de l application en fonction de l architecture ciblée (surtout dans le cas de programmes qui recherchent un haut niveau de performance). Comme Cuda, OpenCL associe une API et une extension de langage, cette fois-ci uniquement pour le langage C. L API d OpenCL, visant à être utilisée sur des architectures très différentes, est de très bas niveau et très verbeuse. Elle est très proche de l API Driver de Cuda. OpenCL étant plus récent, il existe moins d outils et bibliothèques pour ce système. Néanmoins, son aspect portable et standardisé pousse de nombreux développeurs à l adopter. Des portages des bibliothèques Cuda vers OpenCL sont en cours et des API de plus haut niveau comme la bibliothèque C++ Bolt[48] d AMD permettent de s approcher des outils proposés pour Cuda. Les systèmes Cuda et OpenCL, sont très similaires, ils s appuient tous deux sur le Stream Processing. En particulier, l API Driver de Cuda est très similaire à la bibliothèque fournie avec OpenCL. Ils s appuient tous les deux sur un modèle de programmation identique, le Stream Processing. Les différences se situent principalement sur le modèle de distribution et la portabilité. Cuda est propriétaire, distribué par NVida et n est compatible qu avec les dispositifs NVidia. OpenCL de son côté

30 CHAPITRE 1. INTRODUCTION est un standard. De nombreuses implantations existent pour différentes architectures. Nvidia propose une implantation d OpenCL tout comme AMD, Intel, IBM (y compris pour le CELL), et bien d autres. Ceci fait d OpenCL une solution plus portable bien que l optimisation d un programme GPGPU sera différente en fonction des architectures. Les programmes sont donc bien portables, pas les performances. Par ailleurs, afin de cibler des architectures très différentes, OpenCL fournit une bibliothèque très paramétrable, mais aussi très verbeuse. L écriture d un programme OpenCL sera donc un peu plus complexe que pour un programme Cuda. Par ailleurs, Cuda fournit l API Runtime, qui simplifie un peu l expression des programmes. De plus, Cuda est plus ancien qu OpenCL et NVidia a fortement soutenu son système. De ce fait, de nombreuses bibliothèques de calculs optimisés ont été développées pour Cuda et ce n est que très récemment que ces bibliothèques ont été portées vers OpenCL. OpenCL est donc une solution plus portable, mais plus complexe à mettre en œuvre, Cuda est propriétaire, mais fournit de nombreux outils pour simplifier la programmation. Le Stream Processing. Les GPGPU sont des architectures hautement parallèles qui exigent des modèles de programmation spécifiques. Cuda et OpenCL exploitent un modèle de type Stream Processing (Traitement de flux) qui définit les GPGPU comme des multiprocesseurs, chacun constitué de nombreuses unités de calcul. En s appuyant sur le modèle SIMD (Single Instruction Multiple Data), il simplifie la description du parallélisme en limitant les calculs à l application d un même noyau à chaque élément d un ensemble de données (le "flux"). Chaque unité de calcul matérielle va exécuter le noyau sur un élément du flux de données en parallèle. Pour généraliser les architectures GPGPU, les deux systèmes demandent de représenter virtuellement les GPGPU comme un ensemble d unités de calcul que nous appellerons des threads, des threads répartis en blocs, des blocs, eux-mêmes placés dans une grille. La figure 1.1 présente une grille virtuelle et son architecture mémoire. Celle-ci est très simplifiée avec uniquement deux blocs de deux threads chacun. En pratique, ce sont des centaines de blocs avec des milliers de threads qui peuvent être lancés. Une fois définie, la grille sera alors projetée sur les multiprocesseurs matériels du dispositif utilisé, chaque multiprocesseur exécutant un ou plusieurs blocs virtuels. En effet, il est possible de définir une grille contenant plus de blocs que le dispositif ne dispose de multiprocesseurs physiques. Par ailleurs, les GPGPU sont considérés avoir une mémoire dédiée, ce qui implique le transfert de données depuis la mémoire du CPU. La mémoire GPGPU est de plus divisée en plusieurs catégories : une mémoire globale, accessible par tous les threads de la grille, une mémoire partagée au sein d un bloc, une mémoire locale à chaque thread et des mémoires spécifiques dépendant du matériel utilisé. Cuda et OpenCL offrent une API CC++ utilisable depuis des programmes CPU classiques associés à un sous-ensemble du langage C pour exprimer les noyaux. Les programmeurs doivent manuellement gérer les GPGPU, c est-à-dire explicitement ordonnancer les lancements de noyaux, mais aussi les transferts mémoire depuis la mémoire CPU vers celles des GPGPU. Comme l interface PCI- Express qui relie GPGPU et CPU est relativement lente comparé à celle de la mémoire des GPU (voir

II. CARTES GRAPHIQUES ET ACCÉLÉRATEURS DE CALCUL 31 Grille Bloc 0 Bloc 1 Mémoire partagée Mémoire partagée Registres Registres Registres Registres Thread (0,0) Thread (1,0) Thread (0,1) Thread (1,1) Mémoire Locale Mémoire Locale Mémoire Locale Mémoire Locale Mémoire Globale Mémoires Spécifiques. Mémoire Unité virtuelle de calcul FIGURE 1.1 : Grille modélisant l architecture mémoire des GPGPU table 1.2), il devient important d optimiser l ordonnancement des transferts mémoires pour éviter les goulots d étranglement et obtenir de hautes performances. Bande Passante Accéleration Accéleration (GBs) PCI-E 2.0 PCI-E 3.0 PCI-E (one way) 2.0 8 1 0.5 PCI-E (one way) 3.0 16 2 1 CPU Intel i7-3930k 51,2 6,4 3,2 CPU AMD FX-8150 21 2,6 1,3 GPU NVIDIA GTX-580 192,4 24 12 GPU Radeon HD 7970 264 33 16,5 TABLE 1.2 : Comparaison des bandes passantes mémoire de plusieurs architectures Actuellement, la programmation GPGPU consiste principalement en l utilisation des API CC++ fournies par Cuda et OpenCL. Celles-ci sont de très bas niveau d abstraction et orientent la programmation vers le paradigme impératif. La programmation GPGPU combine programmation parallèle à mémoire distribuée et à mémoire partagée. En effet, les GPU sont séparés du processeur central et la gestion de l ensemble par le CPU se rapproche d un système distribué, une fois sur le CPU,

32 CHAPITRE 1. INTRODUCTION l utilisation de la mémoire globale est comparable à une mémoire partagée classique. Néanmoins, la mémoire partagée, sur le GPGPU, apporte un grain de parallélisme supplémentaire et permet de considérer les différents blocs utilisés pour le calcul d un noyau comme des nœuds d un système distribué. Cependant, il n est pas possible de communiquer entre les blocs et en particulier de synchroniser différents blocs entre eux. Cet environnement très hétérogène, associé à l utilisation d API de bas niveau rend la programmation GPGPU très difficile. II.4 Applications GPGPU La programmation GPGPU permet de bénéficier des hautes performances des cartes graphiques. Elle permet, en particulier d accélérer les programmes avec un important ratio quantité de calcul taille des données. Il faut par ailleurs que l ensemble des calculs sur les différents jeux de données soit le plus uniforme possible. Les cartes graphiques sont, par ailleurs, naturellement optimisées pour le calcul matriciel (nécessaire pour le calcul du rendu de scènes en trois dimensions). De nombreux problèmes scientifiques se modélisent par des opérations matricielles et consistent en l application uniforme d un jeu d opérations sur de grands ensembles de données. Pour ce type de problèmes, la programmation GPGPU promet des gains importants en performance. Ces dernières années, de nombreux programmes ont été portés vers les architectures GPGPU pour accroître leurs performances. Le classement du top500 a vu apparaitre des supercalculateurs équipés de GPGPU ou accélérateurs. Ils sont actuellement (classement de Juin 2013 1 ) 54 dans le classement. Le premier (Tianhe-2) utilise par exemple des accélérateurs de calculs compatibles OpenCL tandis que le second (Titan) utilise des GPGPU Nvidia compatible Cuda et OpenCL. Ceci démontre bien l intérêt croissant que porte la communauté scientifique et plus particulièrement la communauté HPC aux GPGPU. Cependant, les GPGPU étant désormais présents dans la plupart des ordinateurs personnels, des applications grand public ont été développées pour en tirer parti. C est le cas de nombreuses applications d encodage vidéo ou de retouche photo. Le jeu vidéo (qui a beaucoup aidé au développement des cartes graphiques) bénéficie lui aussi des GPGPU pour le développement d effets visuels complexes, mais aussi de moteurs physiques réalistes. II.5 Outils, bibliothèques et langages Actuellement, le développement d applications GPGPU demande majoritairement 2 l utilisation de Cuda ou OpenCL. Afin d aider les programmeurs, des outils complémentaires ont été développés. II.5.a Outils Les constructeurs de GPGPU mettent en avant leurs matériels en mettant à disposition des outils dédiés. Il existe en particulier des dévermineurs (debuggers en anglais), des traducteurs et des compilateurs capables de manipuler les programmes GPGPU. Dévermineurs. 1. http:www.top500.orglists201306 2. des alternatives existent comme l utilisation directe de shaders OpenGL ou les API DirectCompute pour Windows uniquement et RenderScript pour le système Android

II. CARTES GRAPHIQUES ET ACCÉLÉRATEURS DE CALCUL 33 Souvent dérivés des dévermineurs développés pour la programmation 3D, ils permettent de détecter les erreurs dans les noyaux de calcul et dans la mémoire des GPGPU. On notera en particulier cuda-gdb[49], proposé par NVidia qui s appuie sur le dévermineur open-source gdb et gdebugger[50] qui est un dévermineur OpenCL et OpenGL. Bien que capables d explorer les noyaux, ils restent limités par l aspect hyper parallèle et non déterministe lié à l utilisation de mémoires partagées entre les unités de calcul, des GPGPU, mais aussi par le fait qu ils sont souvent incompatibles avec certaines méthodes de programmation ou fonctions des API OpenCL ou Cuda. De plus ils sont souvent dédiés soit à OpenCL, soit à Cuda, complexifiant le développement d applications très hétérogènes et portables. Traducteurs et compilateurs. Les outils fournis par NVidia pour Cuda ne ciblent que les cartes graphiques NVidia. Afin de rendre Cuda plus portable, des efforts ont été faits pour développer des compilateurs ciblant d autres architectures. C est en particulier le cas de Cuda-x86[51] qui produit du code pour CPU x86, d Ocelot[52] qui compile du Cuda pour les GPU AMD et NVidia, mais aussi pour les CPU x86. Enfin, afin de rendre Cuda plus portable des traducteurs ont été développés comme CU2CL[53] qui transforment du code Cuda en code OpenCL. II.5.b Bibliothèques Afin de simplifier la programmation, des bibliothèques ont été développées. Elles offrent soit des ensembles de fonctions optimisées, soit des abstractions de plus haut niveau pour la programmation GPGPU. Bibliothèques Optimisées. Les constructeurs développent des bibliothèques dédiées à leurs architectures. Nvidia propose par exemple CUBLAS et AMD APPML[54] (Accelerated Parallel Processing Math Libraries) pour remplacer l utilisation des bibliothèques BLAS des CPU. Il existe aussi des bibliothèques conçues par des développeurs tiers. MAGMA pour Cuda, MAGMACL et ViennaCL[55, 56] pour OpenCL sont des bibliothèques open-source contenant une partie des fonctions des bibliothèques BLAS et LAPACK. CULA est une bibliothèque propriétaire d algèbre linéaire pour Cuda. Il existe aussi des bibliothèques spécifiques pour des domaines d applications particuliers comme GPUCV[57] ou CUVILib[58] pour le traitement d images et la vision artificielle et GPU BLAST[59] ou G-DNAG-MSA[60] pour la bioinformatique. Bibliothèques d abstractions. Les bibliothèques d abstractions se distinguent des précédentes, car elles se concentrent sur la simplification de certaines tâches et non plus sur des fonctions optimisées. Cuda par exemple est fourni avec l API Driver de très bas niveau, mais aussi avec l API Cuda Runtime qui automatise certaines tâches. Cette bibliothèque permet surtout de réduire la verbosité de Cuda Driver. NVidia propose, par ailleurs, la bibliothèque C++ Thrust[61] et AMD la bibliothèque C++ Bolt qui offrent des itérateurs sur des ensembles de données manipulables par le CPU et les GPGPU afin d automatiser le calcul. Il existe par ailleurs d autres bibliothèques de squelettes comme SkePU[62] et SkelCL[63] qui fournissent des constructions similaires pour les tableaux. SkelCL offre un backend ciblant OpenCL

34 CHAPITRE 1. INTRODUCTION alors que SkePU cible aussi Cuda, OpenMP et du code C séquentiel offrant plus de portabilité. Il existe aussi des bibliothèques de squelettes s attachant à la gestion et à l organisation automatique des tâches définies par l utilisateur. C est par exemple le cas de StarPU[64, 65] qui permet de simplifier la description d applications complexes ciblant des architectures hétérogènes. De même que pour les bibliothèques optimisées, des bibliothèques de squelettes ont été développées pour cibler des domaines d application spécifiques, c est le cas de la bibliothèque développée dans le cadre du projet SKIPPER[66] qui cible le traitement d image et la vision numérique, mais aussi de la bibliothèque SkelGIS[67] dédiée à la manipulation d information géographique. II.5.c Langages. Analyse de programmes. Cuda et OpenCL sont les deux principaux systèmes de programmation des dispositifs GPGPU. Ils sont fournis avec des extensions et bibliothèques pour les langages C et C++ pour la partie hôte et avec des langages, sous-ensembles de C ou C++, pour la partie noyaux. Des travaux ont été réalisés afin de spécifier ces systèmes et en particulier la partie noyau. On peut citer en particulier la thèse de Guodong Li[68], qui propose dans son chapitre 6 une spécification du modèle de programmation des noyaux de calcul en Cuda, en TLA+[69, 70]. En enrichissant PUG, un solveur SMT (Satisfiablity Modulo Theories), pour manipuler l ensemble du langage des noyaux de calcul Cuda, il propose une vérification automatique des noyaux pour détecter des erreurs de programmation, mais aussi proposer des optimisations. En particulier, PUG peut vérifier les erreurs sur les tailles de données ou les conflits d accès mémoire. GPUVerify[71] est un autre outil qui s appuie sur une spécification formelle des noyaux de calcul GPGPU pour détecter des erreurs supplémentaires et prédire une possible perte de performance. Il s appuie sur une sémantique opérationnelle ciblant particulièrement la divergence et les barrières des noyaux de calcul. [72] présente une formalisation du système Cuda. Elle est de très bas niveau et décrit en détail les interactions entre threads lors de l exécution d un noyau de calcul ainsi que l utilisation des différentes mémoires des dispositifs GPGPU. Bindings et extensions. Afin de permettre un plus grand choix de langages, des bindings ont été réalisés pour de nombreux autres langages, comme PyCudaPyOpenCL[73] pour Python et JCUDA ou JOCL[74] pour Java. Ces bindings permettent d utiliser directement les fonctions de l API Cuda ou OpenCL dans les langages qu ils concernent. Cependant, ils n exploitent pas les spécificités des langages et n apportent aucune abstraction supplémentaire sur les API Cuda ou OpenCL. D autres approches ont été développées pour permettre de profiter d abstractions fournies par des langages de haut niveau pour la programmation GPGPU. Ces approches consistent principalement à l ajout d un DSL (Domain Specific Language, langage dédié) intégré à un langage de haut niveau. Il existe, par exemple, Obsidian[75] pour Haskell et ScalaCL[76] pour Scala qui permettent de profiter de certaines constructions du langage pour la description des calculs GPGPU. Ces bibliothèques s appuient sur des squelettes pour représenter des opérations vectorielles. Ces opérations sont ensuite automatiquement transformées en noyaux de calcul GPGPU pour accélérer l exécution du programme. De la même manière, plusieurs bibliothèques et extensions de langage pour F#, comme Accelerator[77], Alea.Cubase[78] ou Brahma[79], ont été développées pour la programmation GPGPU. Elles s appuient sur des DSL très proches des sous-ensembles du C de Cuda et OpenCL pour définir des noyaux et offrent un ensemble de fonctions pour manipuler données et noyaux comme pour Cuda et

III. OBJECTIFS ET CONTRIBUTIONS DE CETTE THÈSE 35 OpenCL. Nous reviendrons sur différentes solutions pour la programmation GPGPU dans le chapitre 3, afin de les comparer avec notre contribution. III Objectifs et contributions de cette thèse Cette thèse a pour objectif l étude d abstractions performantes pour la programmation des cartes graphiques. Les cartes graphiques sont des dispositifs complexes dont la programmation est principalement possible via des outils d assez bas niveau d abstraction. Elle s attachera donc au développement d outils de haut niveau permettant de simplifier la programmation GPGPU tout en conservant les hautes performances qu elle permet d atteindre. Pour ce faire on ciblera certains problèmes posés par les outils actuels pour la programmation GPGPU, notamment concernant la portabilité, la gestion mémoire, la manipulation et la composition des noyaux. Nous ciblerons aussi l apport d abstractions complémentaires, en particulier via l apport de squelettes algorithmiques ainsi que d un langage dédié à la programmation GPGPU. Portabilité. Cuda et OpenCL sont deux systèmes incompatibles, par exemple, des données transférées via Cuda ne sont pas utilisables par un noyau OpenCL. De plus, Cuda comme OpenCL proposent différents niveaux de fonctionnalités qui ne sont pas toujours utilisables et dépendent de l architecture matérielle servant à exécuter le programme. Par exemple, le calcul en double précision n est disponible que sur certaines architectures même s il tend à se généraliser aujourd hui. Tout ceci implique de prendre en compte les spécificités des systèmes, mais aussi des architectures cibles dans le développement d une application GPGPU. Ainsi, nous souhaitons proposer une solution permettant de profiter indifféremment des deux systèmes et qui saurait profiter des spécificités présente sur certaines architectures. Hétérogénéité. Au-delà de la portabilité, nous souhaitons offrir une solution simple pour profiter de systèmes très hétérogènes, associant des dispositifs matériels différents, avec des spécificités différentes, mais aussi utilisables via OpenCL ou Cuda. Par exemple, nous souhaitons être capables de profiter d un système associant un CPU multicœur Intel compatible OpenCL (via le SDK fourni par Intel)) avec une carte graphique NVidia compatible Cuda et une carte graphique AMD compatible OpenCL (via le SDK fourni par AMD). Enfin, pour faciliter le développement, nous souhaitons que les programmeurs puissent cibler des architectures et systèmes sans avoir besoin de posséder le matériel ciblé. Gestion Mémoire. La programmation GPGPU implique, en général, l utilisation de dispositifs dédiés détachés du CPU et de sa mémoire. Ceci implique de copier les données à traiter de la mémoire CPU vers la mémoire du GPGPU. Une fois le calcul fait, les résultats obtenus doivent à nouveau être transférés vers le CPU. De plus, les GPU possèdent différents niveaux de mémoire avec des tailles et des bandes passantes différentes. Nous souhaitons offrir une solution qui permette de libérer les programmeurs de la gestion des transferts mémoires, afin de leur permettre de se concentrer davantage sur les calculs. De plus, l ordonnancement et la gestion des transferts peuvent avoir un impact important sur les

36 CHAPITRE 1. INTRODUCTION performances et sont une importante source de bugs dans les programmes GPGPU. En automatisant cette tâche via un algorithme suffisamment efficace, nous souhaitons offrir de bonnes performances tout en réduisant considérablement les risques de bugs. Manipulation de noyaux. La programmation GPGPU consiste en l utilisation des GPGPU via l écriture de noyaux de calcul. Nous souhaitons évidemment permettre l utilisation de ces noyaux. En particulier, dans le souci de tirer le maximum de performances, il est nécessaire de permettre l utilisation de noyaux écrits à la main avec les outils classiques, afin, en particulier, d autoriser le binding avec les bibliothèques optimisées existantes. Par ailleurs, les outils actuels, associant des programmes CPU avec des noyaux GPGPU, compilés séparément et parfois liés dynamiquement, à l exécution, apportent peu de vérification statique (et donc de détection de bugs). Il paraît donc nécessaire d apporter davantage de vérification statique sur la liaison entre noyaux et programme CPU. Squelettes de programmation. Comme nous l avons vu, un moyen efficace d apporter davantage d abstractions tout en permettant de maintenir, ou d accroître les performances est d offrir une bibliothèque de squelettes algorithmiques. Dans cette optique, une bonne solution devra donc permettre l écriture de squelettes de programmation et leur composition pour la description d algorithmes efficaces. Langage dédié et typage statique. L utilisation de noyaux externes permet d optimiser les noyaux via les outils de très bas niveau offerts par les systèmes Cuda et OpenCL, mais limite aussi les possibilités de liaison avec une solution de plus haut niveau. Pour cela, nous souhaitons offrir un langage dédié à la description des noyaux. Ce langage devra être intégré à notre solution afin de permettre de simplifier l analyse du programme général et ainsi d apporter plus de vérification statique et d optimisations automatiques. En particulier, ce langage doit être fortement et statiquement typé pour détecter au plus tôt les erreurs. Il devra aussi offrir une connexion avec le langage hôte pour permettre un partage de données transparent entre CPU et GPGPU. Enfin, ce langage sera aussi focalisé sur la portabilité, en permettant la description de noyaux exécutables sur des architectures Cuda comme OpenCL. Il devra aussi être utilisable pour la description de squelettes et leur composition. IV Plan de la thèse Cette thèse vise à étudier l apport d abstractions dans la programmation GPGPU en restant focalisée sur les performances. Elle ciblera en particulier les problèmes de portabilité, gestion mémoire et composition de noyaux que pose la programmation GPGPU. Elle présentera la solution que nous proposons pour répondre aux objectifs que nous avons fixés. Elle s organise en trois chapitres. Les deux premiers s attachent au développement de langages et outils pour la programmation GPGPU, tandis que le troisième s appuie sur les outils construits précédemment pour proposer des abstractions de plus haut niveau pour la programmation GPGPU. Le chapitre 2 présentera le langage que nous avons conçu pour manipuler les GPGPU et qui s appuie comme Cuda et OpenCL sur le Stream Processing. Afin de décrire précisément le comporte-

IV. PLAN DE LA THÈSE 37 ment des programmes écrits avec ce langage, nous proposerons sa sémantique opérationnelle. Nous nous attacherons d abord à la partie hôte, exécutée par le CPU et chargée d organiser les transferts mémoires et l exécution des noyaux de calcul. Nous détaillerons précisément le mécanisme mis en place pour permettre une gestion automatique des transferts de données entre les mémoires CPU et GPGPU. Nous présenterons ensuite la partie dédiée aux noyaux de calcul, et à travers elle nous détaillerons le comportement général d un noyau de calcul GPGPU. En particulier, nous préciserons le comportement de chaque threads au sein de la grille virtuelle projetée sur les unités de calcul du GPGPU. Par ailleurs, nous détaillerons le mécanisme de lancement des noyaux de calcul qui fait le lien entre la partie hôte et les noyaux. Le chapitre 3 présentera notre implantation de ce langage avec le langage OCaml et les raisons de ce choix. Nous présenterons d abord la bibliothèque Stream Processing with OCaml (SPOC) pour OCaml qui correspond à la partie hôte du langage présenté en partie I. Nous présenterons par la suite Stream ARchitectures Extensible Kernels (Sarek), un langage intégré à OCaml, dédié aux noyaux de calcul. Nous présenterons d abord SPOC, comme Sarek, du point de vue de l utilisateur avant de développer certains détails d implantation. Par la suite nous montrerons les performances obtenues par ces outils à travers différents programmes de tests. En particulier, nous vérifierons les performances atteintes sur différents GPGPU mais aussi sur des architectures multi-gpgpu parfois très hétérogènes. Enfin, nous présenterons un cas d utilisation réaliste de nos outils à travers le portage d une application de calcul numérique haute performance connue depuis Fortran et Cuda vers OCaml avec nos outils. Le chapitre 4 s attachera au développement de squelettes algorithmiques parallèles dédiés à la programmation GPGPU à l aide de nos outils. Ainsi, nous vérifierons leur capacité d extensions et montrerons comment ils permettent d offrir des constructions de plus haut niveau d abstractions qui simplifient la description d algorithmes complexes tout en offrant de nouvelles optimisations automatiques. L utilisation de ces squelettes sera présentée à travers un exemple de programme simple qui servira en fin de chapitre pour la mesure des performances. Enfin, nous conclurons cette thèse en revenant sur l ensemble des objectifs fixés dans cette introduction, avant de discuter des perspectives qu elle apporter par nos travaux et par l évolution logicielle et matérielle des GPGPU et accélérateurs de calcul.

CHAPITRE 2 Langages pour la programmation GPGPU : sémantique opérationnelle et typage «A beginning is the time for taking the most delicate care that the balances are correct.» F. Herbert (Dune) Sommaire I Un langage pour le programme hôte : SPML...................... 41 I.1 Grammaire de SPML................................ 41 I.2 Quelles propriétés pour SPML?......................... 42 I.3 Sémantique opérationnelle de SPML...................... 44 I.4 Propriétés de SPML................................ 58 II Un langage pour les noyaux de calcul.......................... 59 II.1 Quelles propriétés pour Sarek?.......................... 59 II.2 Grammaire de Sarek................................ 60 II.3 Sémantique opérationnelle de Sarek...................... 62 II.4 Propriétés de Sarek................................. 72 III Système de types....................................... 73 III.1 Environnements et notations........................... 73 III.2 Typage des expressions.............................. 74 III.3 Liaison entre SPML et Sarek........................... 77 IV Conclusion.......................................... 78 39

40 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Résumé Ce chapitre définit un ensemble de langages de programmation pour la programmation GPGPU. Basé sur le Stream Processing, il est constitué d un langage hôte, Stream Processing mini-ml (SPML) et d un langage pour l expression des noyaux de calcul, Sarek. Ce chapitre présente d abord le langage SPML à travers sa sémantique opérationnelle. SPML est un langage basé sur un mini-ml auquel nous avons ajouté un ensemble de primitives dédiées à la programmation GPGPU. En particulier, il est capable de transférer les données automatiquement, à la demande, entre les mémoires CPU et GPGPU. Par la suite, nous présentons le langage Sarek, qui se base sur les sous-ensembles du C fourni avec les systèmes Cuda et OpenCL, mais offre une syntaxe à la ML, et un système de types fort. À travers la sémantique opérationnelle de Sarek, nous détaillons le comportement d un noyau de calcul GPGPU et les interactions entre les différents threads qui le composent.

I. UN LANGAGE POUR LE PROGRAMME HÔTE : SPML 41 DANS CE CHAPITRE, nous présentons un ensemble de langages de programmation, destinés à la programmation GPGPU. Comme avec les systèmes Cuda et OpenCL, nous nous appuyons sur le modèle du Stream Processing qui demande de distinguer le programme hôte, qui est exécuté par le CPU, des noyaux de calcul qui s exécutent sur les dispositifs GPGPU. Pour chaque type de programme, hôte ou noyau, nous proposons un langage dédié. Nous spécifierons les deux langages, à travers leur sémantique opérationnelle. Dans une première section, nous décrirons le langage SPML (Stream Processing ML) qui s attache au programme hôte. Ensuite, nous présenterons le langage Sarek (Stream ARchitectures Extensible Kernels) qui cible plus particulièrement l expression des noyaux de calcul GPGPU. Nous étendrons ensuite Sarek avec un système de types afin de permettre une meilleure vérification statique des programmes qu avec les outils de bas niveau actuels. Notre objectif dans ce chapitre est de décrire, exactement, les caractéristiques principales de ces langages qui serviront de support au reste de la thèse. Nous définirons ainsi les propriétés de ces langages, en particulier les garanties apportées sur la position des données (qui sont transférées entre les mémoires de l hôte et des GPGPU) et sur les transferts. Ces propriétés, présentées en section I.4 et II.4 de ce chapitre, en plus d offrir un socle formel sur lequel s appuieront les chapitres suivants, pourront servir de guide pour l implantation de langages pour la programmation GPGPU. Toutefois, la lecture de l ensemble des règles de sémantique décrites dans ce chapitre n est pas indispensable à la compréhension des chapitres suivants, dont le chapitre 3 qui illustre l implantation de ces langages avec le langage OCaml. Nous y détaillerons l utilisation de ces langages du point de vue de l utilisateur en précisant certains détails d implantation. Pour aider la lecture des règles dans ce chapitre, on pourra utiliser la feuille de notations accompagnant ce manuscrit et disponible en copie page 165. I Un langage pour le programme hôte : SPML Dans cette section, nous décrivons SPML, un langage de la famille ML, fonctionnel et impératif, auquel on ajoute des primitives de manipulation de données transférables sur les dispositifs GPGPU ainsi que des primitives de manipulation de noyaux de calcul. SPML est dédié à l expression du programme hôte et est exécuté par le CPU. C est afin d offrir un langage suffisamment expressif, mais aussi simple à décrire et à implanter que notre choix s est porté sur un langage fonctionnel de la famille ML proche du lambda calcul. I.1 Grammaire de SPML Afin de simplifier la description, nous décrivons, ici, uniquement le cœur de SPML. La grammaire, présentée en figure 2.1, résume les expressions du langage. Le langage reste particulièrement simple, son principal objectif étant de présenter une base sur laquelle nous proposons des extensions pour la manipulation des données et calculs sur les dispositifs GPGPU. La grammaire en figure 2.2 présente l extension du langage pour la programmation GPGPU, c està-dire l ensemble des expressions de SPML dédiées à la manipulation des GPGPU. Le langage SPML complet est donc constitué du cœur fonctionnel associé à l extension GPGPU.

42 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Expressions expr = x Variable c Constante expr ; expr Séquence if cond then expr [else expr ]? Alternative for x = expr to expr do expr done Boucle For fun x > expr Abstraction let x = expr in expr Définition let rec x = fun x > expr in expr Définition récursive expr bi nop expr Opération Binaire ref expr Référence! expr Déréférencement expr = expr Modification d une référence expr expr Application FIGURE 2.1 : Cœur de SPML Expressions expr = K Noyau g Dispositif GPGPU v Vecteur transférable v. [< expr >] Lecture dans un vecteur v. [< expr >] <- expr Ecriture dans un vecteur run K g [expr ] Exécution d un noyau mkvec expr expr Création de vecteur await g Attente d un GPGPU FIGURE 2.2 : Partie GPGPU de SPML Celle-ci s articule, principalement autour de trois éléments, les noyaux de calcul, K ; les dispositifs GPGPU, g ; et les vecteurs transférables, vt. Les vecteurs représentent les données transférables sur les dispositifs GPGPU. Ce sont des ensembles monomorphes de données alignées en mémoire. On différentiera le bloc stocké en mémoire CPU ou GPGPU du vecteur transférable. Le vecteur transférable sera considéré dans le reste de ce chapitre comme une référence sur un bloc. On ajoute à cela des primitives concernant l exécution d un noyau sur un dispositif, ainsi que pour la création d un nouveau vecteur transférable. Les noyaux sont des éléments qui seront décrits dans la section suivante. I.2 Quelles propriétés pour SPML? SPML a pour objectif de définir un langage hôte pour la programmation GPGPU. En particulier, il vise à offrir une solution pour simplifier la gestion des transferts mémoires entre CPU et dispositifs GPGPU. C est un langage qui permet de décrire des programmes au comportement prédictif, sans

I. UN LANGAGE POUR LE PROGRAMME HÔTE : SPML 43 demander à l utilisateur de focaliser son attention sur la synchronisation des données entre le CPU et les GPGPU. En pratique, cela implique d offrir des transferts automatiques entre les mémoires. Pour des raisons de performances et d implantation, les transferts seront asynchrones et non bloquants, afin de profiter des capacités des GPGPU à recouvrir ces transferts par du calcul via des solutions de double buffering. Ce type de solution permet de recouvrir les temps de transfert par l exécution des noyaux et donc d accélérer l exécution globale des programmes. Avec des transferts asynchrones et non bloquants, il est difficile de prédire la position des vecteurs transférables, et donc d ordonnancer correctement le programme. Il est alors nécessaire d apporter des garanties sur les transferts. 1. Un vecteur transférable ne peut subir qu un transfert à la fois. 2. Un transfert supprime la source 3. Un vecteur transférable ne peut être présent que dans un espace mémoire à la fois, c est-à-dire en mémoire CPU ou dans la mémoire d un GPGPU. C est l unicité des vecteurs transférables qui permet d assurer la prédiction du comportement du programme. Les transferts étant asynchrones, on considèrera les vecteurs transférables comme des valeurs dont le contenu sera défini plus tard lors de l exécution, à la manière des futures de Lisp[80] et Scheme[81] ou Java[82, 83]. Les transferts étant automatiques, on considèrera leur utilisation implicite, l accès à un vecteur en lectureécriture déclenchant la synchronisation et l attente du calcul de sa valeur (si nécessaire). Du point de vue de la gestion des noyaux de calcul, SPML assure uniquement que l ensemble des vecteurs transférables utilisés par un noyau sont bien présents sur le GPGPU avant le lancement du noyau. Ainsi, il est possible de décrire un programme simple utilisant un noyau de calcul prédéfini K avec un dispositif GPGPU g comme présenté dans la figure 2.3. Ici, nous créons trois vecteurs transférables d entiers en mémoire CPU et nous les initialisons. Cette opération est réalisée par le CPU. Ensuite, nous exécutons notre noyau de calcul K sur le dispositif GPGPU g en lui passant en paramètre ces trois vecteurs. Comme nous le détaillerons par la suite, les trois vecteurs sont automatiquement transférés dans la mémoire du GPGPU g avant que le noyau ne soit exécuté. Enfin, nous renvoyons la première valeur du vecteur de sortie. Cette opération est réalisée par le CPU et le vecteur est donc transféré automatiquement de la mémoire de g vers la mémoire CPU. C est ce mécanisme de transferts automatiques que nous allons détailler par la suite. Code SPML. l e t vecteur_in1 = mkvec 1024 0 in l e t vecteur_in2 = mkvec 1024 0 in l e t vecteur_out = mkvec 1024 0 in for i = 0 to 1023 do vecteur_in1[ <i>] < i; vecteur_in2[ <i>] < i; done ; run K g vecteur_in1 vecteur_in2 vecteur_out; vecteur_out.[ <0 >] FIGURE 2.3 : Exemple : programme SPML

44 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE I.3 Sémantique opérationnelle de SPML Dans cette section, nous présentons un extrait de la sémantique opérationnelle, à grands pas, de SPML, c est-à-dire, comment un programme SPML s évalue. Nous nous attachons ici aux expressions directement impliquées dans la programmation GPGPU (présentées en figure 2.2). La sémantique opérationnelle du langage SPML complet est décrite en annexe A. I.3.a Environnement d évaluation Un programme SPML peut modifier l état de la mémoire hôte, mais aussi l état des mémoires des dispositifs GPGPU. Les opérations exécutées par les GPGPU sont ordonnées par l hôte, mais sont asynchrones, on maintient donc une file de commandes liée à chaque dispositif GPGPU à jour au cours du programme. Les expressions du langage sont donc évaluées dans trois types d environnements d évaluation : Les environnements lexicaux E, E 0, E 1, etc. associent des variables à des valeurs. Les mémoires M associent des adresses à des valeurs. On distinguera la mémoire du CPU hôte M h de la mémoire de différents dispositifs GPGPU connectés M g1, M g2, etc. On utilisera la notation M 0 h, M 1 h,...,m n h et M 0 g, M 1 g,...,m n g pour signifier les modifications de l état mémoire. Les associations entre adresses et valeurs sont notées (@v, v). La file de commandes (command queue en anglais) d un GPGPU g, est notée Q g. À chaque commande lancée sur un GPGPU, un élément est ajouté à cette file, chaque fois qu une commande se termine elle est retirée de la file. On considèrera que l ajout d une commande dans une file est une section critique et qu il ne peut y avoir de commandes simultanées. On s attache ici uniquement à la partie CPU de la programmation GPGU. C est elle qui est responsable des lancements de noyaux de calcul, mais aussi des transferts vers et depuis la mémoire des GPGPU. Les dispositifs GPGPU possèdent trois niveaux de mémoire : la mémoire locale, qui n est, elle, pas accessible par le CPU, la mémoire partagée, qui est principalement utilisée à des fins d optimisation et simulable par des espaces de mémoire globale, la mémoire globale. Le CPU ne peut modifier que la mémoire globale des GPGPU, on ne considèrera donc ici que celle-ci. Les autres mémoires seront utilisées par les noyaux de calcul que nous détaillerons dans la section suivante. La file d exécution des GPGPU a un comportement FIFO et possède trois types de commandes : un transfert depuis l hôte qu on notera Trvec, un transfert vers l hôte qu on notera Tr vec, l exécution d un noyau de calcul K qu on notera R K. Dans le cas des transferts, on précisera via l indication vec la valeur transférée. Pour simplifier la description, on considèrera que chaque GPGPU ne possède qu une seule file de commandes. En pratique, pour autoriser l exécution parallèle de différents noyaux de calcul ou pour permettre l exécution de transferts en parallèle des calculs et ainsi rendre possible la description d algorithmes de multi-buffering, certains GPGPU permettent l exécution simultanée de plusieurs files de commandes. Pour un système multi-gpgpu, on notera Q g 1, Q g 2, etc., la file de chacun des GPGPU. On notera C une commande quelconque.

I. UN LANGAGE POUR LE PROGRAMME HÔTE : SPML 45 I.3.b Notations Nous utiliserons les notations suivantes. Les valeurs sont notées v, v 0, v 1, v n, etc. On utilisera parfois la constante correspondant au résultat de l évaluation d une expression e en une valeur v, on notera alors cette constante v. Les valeurs sont spécifiées dans la sémantique par l ensemble suivant : V al = immedi ates integ er f loat adresse immedi ates = {true, false, ( )} integ er = Z f loat = R vecteur = N adresse = N Les vecteurs transférables sont des vecteurs dont la valeur peut changer en fonction de leur position courante. On les représentera par une référence sur l adresse courante du bloc de données correspondant au vecteur. Cette adresse pourra être présente en mémoire hôte, en mémoire GPGPU ou, pendant un transfert, être indéterminée. On notera @v 1 l adresse en mémoire hôte d une valeur v 1 et @ g v 2 l adresse dans la mémoire du GPGPU g d une valeur v 2. Pour un transfert, d une position p1 vers une position p2, on utilisera la notation @ p1 p2 v 3. On notera @v 1 [n] l accès direct au champ n du vecteur v 1 à l adresse @v 1 et @v 1 [n] v 2 l écriture de la valeur v 2 dans ce champ. Attention, cette notation est à différentier des expressions v 1.[< n >] et v 1.[< n >] < v 2 qui s attachent aux vecteurs transférables et peuvent introduire des transferts. Les booléens sont notés true ou false. Les expressions du langage sont notées e, e 0, e 1, e n, etc. Un environnement vide est noté [] On notera E 1 E 2 l union disjointe entre deux environnements. Ainsi, on décomposera parfois un environnement en plusieurs sous-environnements, pour préciser une modification de l environnement. Par exemple, on utilisera la notation E 2 = (x, v) E 1 pour préciser que (x, v) existe dans E 2. Cette notation pourra aussi préciser une modification apportée à un environnement. I.3.c État initial L environnement lexical, E initial (voir table 2.1) contient les primitives de base du langage, en particulier, les opérateurs arithmétiques de base, mais aussi un ensemble de fonctions de bas niveau inaccessibles à l utilisateur définies dans la section I.3.e. Les mémoires initiales de l hôte comme des dispositifs GPGPU sont vides. Les files d exécutions des GPGPU seront considérées comme initialement vides. I.3.d Évaluation L évaluation d une opération du langage modifie les environnements d évaluation.

46 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Accessibles à l utilisateur Bibliothèque d exécution E ini t = [(+, prim) ; (-, prim) ; (*, prim) ; (size, prim) ; (alloc h,prim) ; (, prim) ; (length, prim) ; (alloc g, prim) ; (copy, prim) ; (trans hg, prim) ; (trans g h, prim) ; (trans g g, prim) ; Atransfer hg, prim) ; Atransfer g h, prim) ; (Atransfer g g, prim) ; ( Gtransfer,prim) ; (has_error, prim) pr im indique qu il s agit d une valeur fournie au langage. TABLE 2.1 : Primitives de SPML L environnement lexical est modifié par les expressions de liaison let in et let rec in. Leur évaluation n est pas décrite dans ce chapitre, car on ne présente ici que l extension dédiée à la programmation GPGPU. L ajout d une commande C dans la file Q 0 g d un GPGPU g s exprimera Q1 g = C Q0 g L évaluation d une expression rend sa valeur et l état mémoire après son évaluation sous la forme (v M). On notera E 0, M 0 h, M g 0,Q0 g e v E 1, M 1 h, M g 1,Q1 g l évaluation d une expression e qui, à partir des états E 0, M 0 h, M g 0,Q0 g, rend la valeur v et modifie les états en E 1, M 1 h, M g 1,Q1 g. On précisera parfois la composition des environnements. Par exemple, E 0,(@v, v) M 0 h, M g 0,Q0 g e v E 1, M 1 h, M g 1,Q1 g indiquera que la mémoire M h de l environnement initial contenait l asso- h ciation (@v, v). Pour simplifier la lecture, on notera parfois E le triplet M h, M g,q g. Ainsi E 0 correspond au triplet M 0 h, M 0 g,q0 g. Pour vérifier la position courante d un vecteur transférable, on vérifiera son adresse courante en le déréférençant. Ainsi, l évaluation M h, M g!vt @v M h, M g indiquera que le vecteur est actuellement en mémoire hôte et M h, M g!vt @ g v M h M g qu il est en mémoire GPGPU. I.3.e Bibliothèque d exécution Il est nécessaire de décrire les primitives de bas niveau qui constituent la bibliothèque d exécution qui sera utilisée par la suite pour la gestion des GPGPU (en section I.3.f). On différentiera les primitives de très bas niveau associées à la gestion mémoire des primitives de transfert. La table 2.2 présente les opérations de manipulation mémoire à travers leur type et leur fonction. Les opérations sur la mémoire sont utilisées pour construire les fonctions de transfert pour lesquelles nous présentons la sémantique opérationnelle. Afin de simplifier la description, nous considèrerons d abord les transferts synchrones et bloquants puis en utilisant la définition de ces transferts, nous présenterons une version asynchrone et non bloquante. Pour les fonctions, qui s appuient sur la bibliothèque d exécution de SPML, et afin de simplifier la lecture au lecteur peu familier avec la sémantique opérationnelle, nous présentons leur construction sous forme de pseudo-code en plus de leur sémantique opérationnelle.

I. UN LANGAGE POUR LE PROGRAMME HÔTE : SPML 47 Type alloc h int a adresse alloc g device int a adresse copy adresse adresse int uni t size vecteur int has_error ker nel device bool Fonction alloue en mémoire de l hôte h alloue en mémoire globale d un GPGPU g copie entre deux vecteurs renvoie le nombre d éléments d un vecteur vérifie si un noyau a échoué TABLE 2.2 : Opérations mémoire de SPML Références et blocs. On distingue les blocs des vecteurs transférables. En pratique, un vecteur transférable sera une référence sur l adresse d un bloc de données en mémoire ce qui nous permettra d exprimer plus simplement les transferts. Pour exprimer exactement la sémantique opérationnelle des opérations sur les vecteurs transférables, il est nécessaire de préciser la sémantique des références et des accès aux blocs de données dans SPML. Les références sont des valeurs mutables. Une référence pointe sur un espace mémoire dans lequel est stockée la valeur référencée. Ainsi, à la création d une référence, un espace mémoire est alloué pour stocker la valeur à référencer et la référence prend pour valeur l adresse de ce nouvel espace. La mémoire est donc mise à jour pour contenir cette nouvelle liaison (adresse,valeur) nouvellement créée. Ben sûr l évaluation de l expression e peut modifier la mémoire du GPGPU et la file de commandes du GPGPU. La règle suivante se lit ainsi : Si dans l environnement E 0, l expression e s évalue en une valeur v en modifiant l environnement vers E 1, alors l évaluation de l expression ref e renvoie la valeur @v dans un nouvel environnement, correspondant à l environnement E 1 dans lequel la mémoire hôte a été enrichie de la liaison (adresse, valeur), (@v, v). Ref E 0 e v E 1 E 0 ref e @v (@v, v) M 1 h, M g 1,Q1 g Le déréférencement donne accès à la valeur référencée. On renvoie donc cette valeur, en utilisant l adresse stockée dans la référence. À nouveau, l évaluation de l expression e peut modifier l état de la mémoire hôte comme de la mémoire GPGPU et de la file de commandes du GPGPU. Bang E 0 e @v E 1 (@v, v) M 1 h E 0!e v E 1 La mise à jour d une référence remplace la liaison (adresse, valeur_initale) dans la mémoire par une liaison (adresse, nouvelle_valeur). De même qu auparavant, chaque évaluation d une expression peut entraîner des changements d état mémoire, aussi bien sur l hôte que sur un GPGPU.

48 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Update E 0 e 1 @v 1 E 1 E 1 e 2 v 2 E 2 M 2 h = (@v 1, v 1 ) M h E 0 e 1 := e 2 ( ) (@v 1, v 2 ) M h, Mg 2,Q2 g Les vecteurs transférables sont des références sur des blocs de données. Un bloc peut être présent en mémoire hôte ou en mémoire GPGPU. Notre bibliothèque d exécution propose deux fonctions alloc h et alloc g pour allouer l espace mémoire nécessaire pour stocker un bloc. Le bloc est considéré comme contigu en mémoire. L allocation renvoie l adresse du bloc nouvellement créé, c est-à-dire, l adresse de départ du bloc. À l allocation on initialise toutes les valeurs du bloc avec la valeur passée en paramètre. Alloc_h E 0 e 1 v 1 E 1 E 1 e 2 v 2 E 2 E 0 alloc h e 1 e2 @v 3 (@v 3 [0], v 2 ) (@v 3 [1], v 2 )... (@v 3 [v 1 1], v 2 ) M 2 h, M 2 g,q2 g Dans le cas d une allocation sur un GPGPU g, l adresse retournée n est pas présente en mémoire hôte, on note alors @ g a l adresse retournée. Alloc_g E 0 e 1 v 1 E 1 E 1 e 2 v 2 E 2 E 0 alloc g e 1 e2 @ g v 3 M 2 h,(@ g v 3 [0], v 2 ) (@ g v 3 [1], v 2 )... (@ g v 3 [v 1 1], v 2 ) M 2 g,q2 g L accès en lecture à un bloc par l hôte n est réalisable que s il est présent en mémoire hôte. Pour lire la valeur stockée au champ i du bloc, il suffit d accéder à la valeur associée à l adresse du champ i du bloc. Pour l écriture, on agit de même en écrasant directement la valeur à l adresse du champ à modifier. Get E 0 e 1 @v 1 E 1 E 1 e 2 v 2 E 2 M 2 h (@v 1[v 2 ]) = v 3 E 0 e 1 [e 2 ] v 3 M 2 h, M 2 g,q2 g Set E 0 e 1 @v 1 E 1 E 1 e 2 v 2 E 2 E 2 e 3 v 3 E 3 M 4 h (@v 1[v 2 ]) = v 3 E 0 e 1 [e 2 ] e 3 ( ) M 4 h, M g 3,Q3 g Vecteurs transférables. Les fonctions d allocation d un bloc ne sont pas accessibles au programmeur. Elles sont utilisées par les fonctions de transfert que nous décrirons par la suite et lors de la création d un vecteur transférable. L utilisation de mk_vec construit un bloc en allouant l espace nécessaire dans la mémoire

I. UN LANGAGE POUR LE PROGRAMME HÔTE : SPML 49 CPU et associe ce bloc à un nouveau vecteur transférable vt. À sa création un vecteur transférable est toujours alloué en mémoire hôte de la taille passée en paramètre et avec une valeur par défaut. Ici, on passe en paramètre l expression e 1 qui détermine la taille du vecteur et l expression e 2 qui définit la valeur par défaut de tous les champs du vecteur. On commence donc par évaluer e 1 et e 2 qui peuvent chacune modifier les environnements d évaluations. On utilise alors alloc h pour créer un bloc de données en mémoire hôte. Enfin, on renvoie le vecteur transférable nouvellement créé, c est-à-dire une référence sur le bloc dernièrement alloué. Mkvec E 0 e 1 v 1 E 1 E 1 e 2 v 2 E 2 v 2 > 0 M 2 h alloc h v 1 v 2 @v 3 M 3 h M 3 h ref @v 3 @v 4 M 4 h @v 4 = @@v 3 E 0 mkvec e 1 e 2 @v 4 M 4 h, M 2 g,q2 g Pseudo-code. ( * mk_vec : int > a > a vecteur * ) l e t mk_vec n d = l e t a = alloc_h n d in r e f a Transferts synchrones et bloquants. Aansfert depuis l hôte vers un GPGPU. de la bibliothèque d exécution de SPML. Ce transfert est réalisé en plusieurs étapes via les fonctions 1. On commence par allouer la mémoire nécessaire sur le GPGPU : M 1 g alloc g g s d @ g v g M 2 g 2. Puis, on copie les données depuis l adresse @v h du bloc en mémoire hôte vers le nouvel espace alloué en mémoire GPGPU. Cette opération modifie la mémoire globale du GPGPU : Mg 2 copy @v h @ g v g s ( ) M 3 g 3. Enfin on met à jour la position courante du vecteur transférable vt : M 1 h vt := @ g v g ( ) M 2 h La mémoire hôte est donc modifiée, ainsi que la mémoire globale du dispositif GPGPU. TransferHG E 0 e vt = ref @v h E 1 s = size vt Mg 1 alloc g g s @ g v g Mg 2 Mg 2 copy @v h @ g v g s ( ) Mg 3 M 1 h vt := @ g v g ( ) M 2 h E 0 trans hg e g ( ) M 2 h, M g 3,Q1 g

50 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Pseudo-code. ( * transfer_hg : a vecteur > device > unit * ) l e t transfer_hg vt g = l e t s = size vt in l e t adr_g = alloc_g g s vt.[ <0 >] in l e t adr_h =!vt in copy adr_h adr_g s; vt := adr_g Transfert depuis un GPGPU vers l hôte. Le mécanisme est similaire au transfert précédent. 1. On commence par allouer la mémoire nécessaire en mémoire hôte : M 1 h alloc h s d @v h M 2 h 2. Puis, on copie les données depuis le bloc en mémoire GPGPU vers le nouvel espace alloué. Cette opération modifie la mémoire hôte : M 2 h copy @ g v g @v h s ( ) M 3 h 3. Enfin, on met à jour la position courante du vecteur transférable vt : M 3 h vt := @v h ( ) M 4 h TransferGH E 0 e vt = ref @ g v g E 1 s = size vt M 1 h alloc h s @v h M 2 h M 2 h copy @ g v g @v h s ( ) M 3 h M 3 h vt := @v h ( ) E 0 trans gh e ( ) M 4 h, M g 1,Q1 g M 4 h Pseudo-code. ( * transfer_gh : a vecteur > unit * ) l e t transfer_gh vt = l e t s = size vt in l e t adr_h = alloc_h s vt.[ <0 >] in l e t adr_g =!vt in copy adr_g adr_h s; vt := adr_h Transfert entre deux GPGPU. Dans le cas général, un transfert entre deux GPGPU g 0 et g 1 consistera en un transfert de g 0 vers l hôte puis en un transfert de l hôte vers g 1. Dans l implantation, si les deux GPGPU sont compatibles avec le même système (Cuda ou OpenCL), il sera possible d éviter le passage par l hôte et de profiter d optimisations supplémentaires. TransferGG M 0 h, M g 0 1, Mg 0 2,Qg 0 1,Q0 g 2 M e vt 1 h, M g 1 1, Mg 1 2,Qg 1 1,Q1 g 2 M 1 h!vt ref @ g 1v g1 M 1 h M 1 h trans gh vt ( ) M 2 h Mg 1 2 trans hg g 2 vt ( ) M 0 h, M g 0 1, Mg 0 2,Qg 0 1,Q0 g 2 trans gg e g 2 ( ) M 2 h, M g 1 1, Mg 2 2,Qg 1 1,Q1 g 2 M 2 g 2

I. UN LANGAGE POUR LE PROGRAMME HÔTE : SPML 51 Pseudo-code. l e t transfer_gg vec g1 = transfer_gh vec; transfer_hg vec g1

52 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Transferts asynchrones et non bloquants. Nous avons commencé par présenter les transferts comme synchrones et bloquants, mais dans SPML, les transferts sont asynchrones et non bloquants. Ils s effectuent en parallèle du programme, dès qu ils le peuvent. Lancer un transfert asynchrone implique d ajouter une commande de transfert au GPGPU concerné. Les mémoires ne sont pas considérées comme modifiées tant que le transfert n est pas terminé. L instruction await permet d attendre la fin d une file, nous verrons en détail son comportement par la suite. On met tout de même à jour l environnement lexical pour préciser qu un transfert est en cours. Transfert depuis l hôte vers un GPGPU. Comme le transfert est asynchrone et non bloquant, son lancement n assure ni son exécution directe ni sa terminaison. Le vecteur transféré ne peut donc être considéré comme déplacé de l hôte h vers le GPGPU g. Il faut donc mettre à jour le vecteur pour indiquer qu il est à une position indéfinie. On utilise pour cela la valeur spécifique : @ h g v. Par ailleurs, on ajoute une commande de transfert à la file de commandes du GPGPU concerné (Tr vt Q g ). Async_TransferHG E 0 e vt E 1 M 1 h!vt ref @v h M 1 h M 1 h vt := @ h g v ( ) E 0 Atransfer hg e g ( ) M 2 h, M g 1,Tr vt Qg 1 M 2 h Transfert depuis un GPGPU vers l hôte. De la même manière que pour un transfert depuis l hôte vers un GPGPU, on utilisera une valeur spécifique pour préciser l état du vecteur en cours de transfert : @ g h v. Async_TransferGH E 0 e vt E 1 M 1 h!vt ref @ g v g M 1 h M 1 h vt :=@ g hv ( ) E 0 Atransfer gh e g ( ) M 2 h, M g 1,Tr vt Qg 1 M 2 h Transfert entre GPGPU. Comme pour les transferts synchrones, un transfert entre GPGPU consiste en un transfert depuis le premier GPGPU vers l hôte suivi d un transfert depuis l hôte vers le GPGPU. Afin d empêcher un mauvais ordonnancement des transferts, on utilisera un transfert synchrone et bloquant pour le premier transfert si la file de commande du GPGPU source est vide. Sinon, on réalisera une attente sur la file de commande du GPGPU source avant de retenter le transfert. Async_TransferGG1 M 0 h, M g 0 1, Mg 0 2,Qg 0 1,Q0 g 2 M e vt 1 h, M g 1 1, Mg 1 2,Qg 1 1,Q1 g 2 Qg 1 1 = [] M 1 h!vt ref @ g 1v g1 M 1 h M 1 h trans gh vt ( ) M 2 h M 2 h,q1 g 2 Atrans hg g 2 vt ( ) M 2 h,q2 g 2 M 0 h, M g 0 1, Mg 0 2,Qg 0 1,Q0 g 2 Atransfer gg e g 2 ( ) M 2 h, M g 1 1, Mg 1 2,Qg 1 1,Q2 g 2

I. UN LANGAGE POUR LE PROGRAMME HÔTE : SPML 53 Async_TransferGG2 M 0 h, M g 0 1, Mg 0 2,Qg 0 1,Q0 g 2 M e vt 1 h, M g 1 1, Mg 1 2,Qg 1 1,Q1 g 2 Qg 1 1 [] M 1 h, M g 1 1, Mg 1 2,Qg 1 1,Q1 g 2 M await g 1 ( ) 2 h, M g 2 1, Mg 2 2,Qg 2 1,Q2 g 2 M 2 h, M g 2 1, Mg 2 2,Qg 2 1,Q2 g 2 Atransfer gg e g 2 ( ) M 3 h, M g 3 1, Mg 3 2,Qg 3 1,Q3 g 2 M 0 h, M g 0 1, Mg 0 2,Qg 0 1,Q0 g 2 Atransfer gg e g 2 ( ) M 3 h, M g 3 1, Mg 3 2,Qg 3 1,Q3 g 2 Cas général. On définit une fonction de transfert vers les GPGPU générique qui spécifiera son comportement en fonction de la position du vecteur à transférer. Dans le cas où le vecteur est en mémoire hôte, on effectue un transfert asynchrone simple : E 0 e vt E 1 Gen_TransferHG1 M 1 h!vt ref @v h M 1 h E 1 Atransfer hg vt g ( ) M 2 h, M g 1,Q2 g E 0 Gtransfer e g ( ) M 2 h, M g 1,Q2 g Dans le cas où le vecteur est déjà dans la mémoire du GPGPU, on ne le transfère pas : Gen_TransferHG2 E 0 e ref @ g v g E 1 E 0 Gtransfer e g ( ) Dans le cas où le vecteur est sur un autre GPGPU, on effectue un transfert simple entre deux GPGPU : Gen_TransferHG3 M 0 h, M g 0 1, Mg 0 2,Qg 0 1,Qg 0 2 e vt M 1 h, M g 1 1, Mg 1 2,Qg 1 1,Qg 1 2 M 1 h!vt ref @ g 2 v g1 M 1 h M 1 h,q1 g 2 Atransfer gg vt g 2 ( ) M 2 h,q2 g 2 M 0 h, M g 0 1, Mg 0 2,Qg 0 1,Qg 0 2 Gtransfer e g 2 ( ) M 2 h, M g 1 1, Mg 2 2,Qg 1 1,Qg 2 2 Dans le cas où le vecteur est en cours de transfert vers ou depuis le GPGPU concerné, on termine la file de commandes du GPGPU et l on vérifie à nouveau la position du vecteur. On utilise pour cela la primitive await qui sera détaillée par la suite page 54. Gen_TransferHG4 E 0 e vt E 1 M 1 h!vt ref @ h g v M 1 h E 1 await g ( ) E 2 E 2 Gtransfer vt g ( ) E 0 Gtransfer e g ( ) E 3 Gen_TransferHG5 E 0 e vt E 1 M 1 h!vt ref @ g hv M 1 h E 1 await g ( ) E 2 E 2 Gtransfer vt g ( ) E 0 Gtransfer e g ( ) E 3 E 1 E 3 E 3

54 I.3.f Gestion des GPGPU CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Exécution de noyau L exécution d un noyau K sur un GPGPU g est asynchrone, elle modifie donc la file d exécution du GPGPU chargé d exécuter le noyau, mais ne modifie pas nécessairement les autres environnements d évaluation. C est à la suite de l attente de cette file d exécution quils sont effectivement mis à jour. Afin de pouvoir exécuter correctement le noyau, la fonction run prend en paramètre le noyau K, mais aussi l ensemble des vecteurs nécessaires au calcul sur le GPGPU. On ajoutera donc le transfert de ces vecteurs à la file de commandes avant d y ajouter la commande d exécution du noyau R K. Nous ne considérons pour l instant que la partie hôte du programme GPGPU, nous étudierons la définition des noyaux de calcul dans la section suivante. Run E 0 e 1 vt 1 E 1... E n 1 e n vt n E n E n Gtransfer vt 1 g ( ) E n+1... E 2n 1 Gtransfer vt n g ( ) M 0 h, M g 0,Q0 g run K g e1... e n ( ), M g 2n K Qg 2n M 2n h E 2n L attente sur une file L attente sur une file vide ne modifie aucun environnement. Wait1 M 0 h, M 0 g,[] await g ( ) M 0 h, M 0 g,[] Il existe quatre cas de file non vide, (i)avec un noyau en cours d exécution, (ii)avec un transfert vers l hôte, (iii)avec un transfert vers le GPU et (iv)le cas général où la file contient plusieurs éléments : Exécution d un noyau de calcul. Les noyaux de calcul travaillent par effet de bord, la mémoire du GPGPU est donc potentiellement modifiée par l exécution du noyau. Nous verrons le comportement des noyaux de calcul et leur gestion interne des erreurs dans la section suivante. Par ailleurs, à chaque lancement de noyau, un espace est réservé sur le GPGPU pour stocker des informations sur d éventuelles erreurs lors de l exécution du noyau. Cet espace est donc copié et analysé par l hôte. Wait2 M 1 h M has_error K g false 1 h M 0 h, M g 0,[R K ] await g ( ) M 1 h, M g 1,[] En cas d erreur, l exécution du programme est interrompue. En pratique, dans l implantation, pour offrir davantage de flexibilité, on pourra utiliser un mécanisme d exceptions pour la gestion des erreurs. Wait2-Err M 1 h M has_error K g true 1 h M 0 h, M g 0,[R K ] await g Er r or M 1 h, M g 1,[]

I. UN LANGAGE POUR LE PROGRAMME HÔTE : SPML 55 Dans le cas d un transfert, l attente assure que les effets du transfert ont eu lieu comme dans le cas d un transfert synchrone bloquant. C est-à-dire que la mémoire de destination est bien modifiée pour contenir le vecteur, ainsi que la valeur courante du vecteur, qui est mise à jour pour tenir compte de sa nouvelle position. Transfert depuis l hôte. Wait3 M 0 h!vt @ h g v M 0 h M 1 h!vt @ g v g M 1 h M 0 h, M 0 g,[tr vt ] await g ( ) M 1 h, M 1 g,[] Transfert vers l hôte. Wait4 M 0 h!vt @ g hv M 0 h M 1 h!vt @v h M 1 h M 0 h, M 0 g,[tr vt ] await g ( ) M 1 h, M 1 g,[] Dans le cas où la file contient plusieurs commandes quelconques C, on évalue l attente de chaque commande dans l ordre jusqu à vider la file. Wait5 Q g 1 = [C1]... Qn g = [Cn] M 0 h, M g 0,Q1 g awaitg ( ) M 1 h, M g 0 n 1,[]... Mh, Mg n 1,Qg n await g ( ) M n h, M g n,[] M 0 h, M g 0,[C 1;...;C n ] await g ( ) M n h, M g n,[] I.3.g Lectureécriture dans un vecteur transférable En s appuyant sur les primitives de la bibliothèque d exécution, il est possible de définir les fonctions de lecture et écriture dans un vecteur transférable. En particulier, comme ces opérations sont ici réalisées sur l hôte, on s assurera toujours que les données correspondant au vecteur transférable sont bien présentes dans la mémoire hôte avant d en autoriser l accès. Lecture. On s attache pour l instant à la partie hôte et donc à la lecture dans un vecteur v.[< n >] par le CPU. Si l on tente d accéder à une donnée en dehors du vecteur, l exécution est interrompue. Là encore, on pourra utiliser des exceptions dans l implantation de SPML. VecGet0 E 0 e 1 vt E 1 s = size vt E 1 e 2 v 2 E 2 v 2 [0, s 1] E 0 e 1.[< e 2 >] Erreur E 2 Si l indice appartient bien au vecteur, il existe cinq cas de lecture possible : Si le vecteur est présent en mémoire CPU, on renvoie la valeur de l indice n du vecteur.

56 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE VecGet1 E 1 e 2 v 2 E 2 E 0 e 1 vt E 1 s = size vt v 2 [0, s 1] M 2 h!vt @v, M 2 h E 0 e 1.[< e 2 >] v 3 E 2 M 2 h @v[v 2] v 3 M 2 h si le vecteur est sur un dispositif GPGPU et si la file de commandes du GPGPU est vide, alors, on transfère le vecteur en mémoire hôte (avec les fonctions de transfert synchrones bloquantes). VecGet2 E 0 e 1 vt E 1 E 1 e 2 v 2 E 2 s = size vt v 2 [0, s 1] M 2 h!vt @ g v g M 2 h Qg 2 = [] M 2 h trans gh vt ( ) M 3 h!vt @v h M 3 h M 3 h @v h[v 2 ] v 3 M 3 h M 3 h E 0 e 1.[< e 2 >] v 3 M 3 h, M 2 g,q2 g Si le vecteur transférable est présent en mémoire GPGPU et si la file de commandes est non vide (VecGet3) ou s il est en cours de transfert vers un GPGPU (VecGet4), ou en cours de transfert vers la mémoire hôte (VecGet5), on réalise une attente sur la file de commandes du GPGPU avant de retenter la lecture. VecGet3 E 0 e 1 vt E 1 E 1 e 2 v 2 E 2 s = size vt v 2 [0, s 1] M 2 h!vt @ g v g M 2 h Qg 2 = [C] E 2 await g ( ) E 3 E 3 vt.[< v 2 >] v 3 E 4 E 0 e 1.[< e 2 >] v 3 E 4 VecGet4 E 0 e 1 vt E 1 E 1 e 2 v 2 E 2 s = size vt v 2 [0, s 1] M 2 h!vt @ h g v M 2 h E 2 await g ( ) E 3 E 3 vt.[< v 2 >] v 3 E 0 e 1.[< e 2 >] v 3 E 4 E 4 VecGet5 E 0 e 1 vt E 1 E 1 e 2 v 2 E 2 s = size vt v 2 [0, s 1] M 2 h!vt @ g hv M 2 h E 2 await g ( ) E 3 E 3 vt.[< v 2 >] v 3 E 0 e 1.[< e 2 >] v 3 E 4 E 4 le vecteur ne peut être en cours de transfert entre deux GPGPU g 0 et g 1. En effet ce type de transfert se décompose en un transfert de g 0 vers l hôte h, une attente sur la file de commandes de g 0 et enfin un transfert de h vers g 0.

I. UN LANGAGE POUR LE PROGRAMME HÔTE : SPML 57 Ecriture. Comme pour l écriture, on s attache à la partie hôte et donc à la lecture par le CPU. Le fonctionnement est donc similaire avec quatre possibilités : l erreur, si on tente d écrire dans un espace n appartenant pas au vecteur VecSet0 E 0 e 1 vt E 1 s = size vt E 1 e 2 v 2 E 2 v 2 [0, s 1] E 0 e 1.[< e 2 >] < e 3 Erreur E 2 l écriture sur un vecteur déjà en mémoire CPU VecSet1 E 0 e 1 vt E 1 E 1 e 2 v 2 E 2 s = size vt v 2 [0, s 1] E 2 e 2 v 3 E 3 M 3 h!vt[v 2] e 3 ( ) E 0 e 1.[< e 2 >] <- e 3 ( ) M 4 h, M g 3,Q3 g M 4 h Si le vecteur est sur un dispositif GPGPU et si la file de commandes du GPGPU est vide, alors, on transfère le vecteur en mémoire hôte (avec les fonctions de transfert synchrones bloquantes). VecSet2 E 0 e 1 vt E 1 E 1 e 2 v 2 E 2 s = size vt v 2 [0, s 1] E 2 e 3 v 3 E 3 M 3 h!vt @ g v g M 3 h Qg 2 = [] M 3 h trans gh vt ( ) M 4 h M 4 h!v t [v 2 ] v 3 ( ) M 5 h E 0 e 1.[< e 2 >] <- e 3 ( ) M 5 h, M g 3,Q3 g l écriture sur un vecteur en mémoire GPGPU (VecSet3), en cours de transfert vers un GPGPU (VecSet4) ou en cours de transfert vers l hôte (VecSet5). VecSet3 E 0 e 1 vt E 1 E 1 e 2 v 2 E 2 s = size vt v 2 [0, s 1] E 2 e 3 v 3 E 3 M 3 h!vt @ g v g M 3 h Qg 3 = [C] E 3 await g ( ) E 4 E 4 vt.[< v 2 >] <- v 3 ( ) E 5 E 0 e 1.[< e 2 >] <-e 3 ( ) E 5 VecSet4 E 0 e 1 vt E 1 E 1 e 2 v 2 E 2 s = size vt v 2 [0, s 1] E 2 e 3 v 3 E 3 M 3 h!vt @ h g v Qg 3 = [C] E 3 await g ( ) E 4 E 4 vt.[< v 2 >] <- v 3 ( ) E 0 e 1.[< e 2 >] <-e 3 ( ) E 5 M 3 h E 5

58 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE VecSet5 E 0 e 1 vt E 1 E 1 e 2 v 2 E 2 s = size vt v 2 [0, s 1] E 2 e 3 v 3 E 3 E 3 = M 3 h, M g 3,Q3 g M 3 h!vt @ g hv Qg 3 = [C] E 3 await g ( ) E 4 E 4 vt.[< v 2 >] <- v 3 ( ) E 5 E 0 e 1.[< e 2 >] <-e 3 ( ) E 5 M 3 h I.4 Propriétés de SPML Nous avons décrit le langage SPML. Celui-ci s attache à l expression du programme hôte dans le cadre de la programmation GPGPU. En particulier, il permet de manipuler noyaux de calcul et vecteurs. Les vecteurs sont transférés automatiquement entre les mémoires de l hôte et des dispositifs GPGPU, abstrayant la notion de transfert. Ceci libère déjà le programmeur de leur ordonnancement et l affranchit ainsi d une tâche complexe, mais primordiale dans l écriture d un programme GPGPU. En particulier, les règles présentées ici permettent de définir certaines propriétés du langage. Propriété 1. Un vecteur transférable ne peut être présent dans plusieurs espaces mémoire à la fois. Cette propriété est imposée par les règles présentées, car l utilisateur n a pas accès directement au bloc de données en mémoire, mais uniquement à sa référence, le vecteur transférable. En effet, le vecteur transférable est une référence qui ne peut, par définition, avoir qu une valeur à la fois, et donc ne peut pointer que sur une seule adresse. De plus, les transferts assurent que la référence est toujours mise à jour pour ne pointer que sur le dernier bloc de données alloué. Propriété 2. Un noyau de calcul ne peut s exécuter que si les vecteurs composant ses arguments sont sur le GPGPU. Là encore, la règle du lancement des noyaux ajoute, à la file de commandes du GPGPU, les transferts de chaque vecteur-argument suivis de l exécution du noyau. Ainsi, comme la file est FIFO, et qu il n y a qu une seule file par GPGPU, il est impossible qu un vecteur soit déplacé hors de la mémoire du GPGPU avant l exécution du noyau. Pour garantir cette propriété dans un environnement parallèle (ou plusieurs threads CPU s exécutent au sein du programme hôte), il suffit de placer l accès à la file de commande de chaque GPGPU (lors d une attente comme lors d un ajout d une nouvelle commande) dans une section critique. Propriété 3. L hôte ne peut modifier les données utilisées par un noyau de calcul en cours d exécution Cette propriété se vérifie simplement. SPML ne peut modifier le contenu d un vecteur qu à travers les opérations définies par les règles VecSet. Celles-ci n autorisent la modification du vecteur que s il est présent en mémoire hôte. Dans le cas contraire, elles réalisent une attente sur le GPGPU qui possède le vecteur. Cette attente (définie dans les règles Wait1 à Wait5) vide complètement la file de commande du GPGPU, et assure que toute opération en cours sur le GPGPU est bien terminée. Ainsi, si l exécution d un noyau de calcul est prévue dans la file, on s assurera de sa terminaison avant de réaliser tout nouveau transfert. Propriété 4. Si un noyau de calcul termine, alors, les données transférées sont accessibles par l hôte.

II. UN LANGAGE POUR LES NOYAUX DE CALCUL 59 Cette propriété nécessitera d avoir présenté le langage de description des noyaux. En particulier, elle demandera de vérifier que, depuis le noyau de calcul, les adresses des blocs de données en mémoire GPGPU ne peuvent être modifiées (voir page 73). Ces propriétés permettent de garantir le comportement des programmes décrits avec le langage SPML et ainsi de proposer une solution qui automatise les transferts et simplifie donc la programmation GPGPU, tout en offrant efficacité (grâce aux transferts asynchrones) et prédictibilité grâce à l assurance que l ordonnancement des transferts et calculs sera respecté. La section suivante va présenter le langage de description des noyaux de calcul pour avoir un ensemble complet et cohérent pour la programmation GPGPU. II Un langage pour les noyaux de calcul Nous nous plaçons dans le modèle du Stream Processing. SPML, que nous avons décrit, s attache au programme hôte, qui manipule des noyaux de calcul GPGPU et qui réalise les transferts mémoires nécessaires. Dans cette section, nous décrivons Sarek, le langage dédié a l expression des noyaux de calcul GPGPU. Il s agit d un langage impératif, monomorphe, qui s appuie sur les sous-ensembles du C proposés par OpenCL ou Cuda. À travers la sémantique opérationnelle de ce langage, nous présentons le comportement des programmes exécutés par le GPU. Ces programmes sont hyper-parallèles, décrivant l exécution de milliers de threads, et restreints par le modèle Stream Processing. Cette description vise à établir les propriétés du langage et à travers elles à proposer une représentation du comportement des GPU qui représentent la cible principale des travaux de cette thèse. II.1 Quelles propriétés pour Sarek? Sarek est dédié à l expression des noyaux de calcul. Il se base sur le Stream Processing. Ainsi, un programme écrit en Sarek décrit l ensemble d instructions élémentaires qui seront exécutées par un thread unique lors de l exécution du noyau. Comme nous l avons vu dans le chapitre précédent, les noyaux de calcul sont exécutés, en parallèle, sur une grille, en trois dimensions, contenant des blocs, eux-mêmes en trois dimensions, de plusieurs threads. Chaque thread de la grille exécute exactement les mêmes instructions. C est un langage impératif, monomorphe avec une syntaxe à la ML pour plus de cohérence avec SPML. Par ailleurs, Sarek est un langage fortement et statiquement typé avec de l inférence de type. Sarek permet de décrire le comportement des noyaux de calcul en exprimant uniquement le code exécuté par un thread du noyau. Ce code sera par la suite projeté sur l ensemble des threads composant le noyau. Pour permettre des traitements particuliers pour chaque thread il est donc nécessaire d avoir accès à certaines informations sur celui-ci, comme son identifiant et son placement au sein d un bloc ou au sein de la grille totale. Grilles et blocs étant considérés en trois dimensions, il sera nécessaire de préciser dans quelle dimension (x, y ou z) est l indice recherché. Pour avoir l indice global, il sera nécessaire d utiliser les fonctions spécialisées ou de réaliser une opération à partir des indices pour chaque dimension. Pour accéder à ces informations, Sarek offre un ensemble de globales qui à l exécution prendront une valeur particulière pour chaque thread. La table 2.3 présente ces globales ainsi que leur équivalent Cuda et OpenCL quand il existe.

60 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Sarek Cuda OpenCL thread_idx_x threadidx.x get_local_id (0) indice du thread_idx_y threadidx.y get_local_id (1) thread dans thread_idx_z threadidx.z get_local_id (2) son bloc global_thread_id pas d équivalent get_global_id indice global dans la grille block_idx_x blockidx.x get_group_id (0) indice du block_idx_y blociidx.y get_group_id (1) bloc dans block_idx_z blockidx.z get_group_id (2) la grille block_dim_x blockdim.x get_local_size (0) dimension block_dim_y blockdim.y get_local_size (1) du bloc block_dim_z blockdim.z get_local_size (2) grid_dim_x griddim.x get_num_groups (0) dimension grid_dim_y griddim.y get_num_groups (1) de la grid_dim_z griddim.z get_num_groups (2) grille TABLE 2.3 : Variables globales de Sarek dédiées à la programmation GPGPU II.2 Grammaire de Sarek Kernel K = kern x -> expr noyau de calcul Expressions expr = expr ; expr Séquence let open Mod in expr Module local if cond then expr [else expr ]? Alternative for x = expr to expr do expr done Boucle For let [mutable]?x = expr in expr Définition while cond do expr done Boucle While x. [< expr >] Lecture dans un vecteur expr bi nop expr Opération binaire x := expr Affectation x. [< expr >] <- expr Ecriture dans un vecteur barrier Barrière x expr... expr Appel de fonction x Variable c Constante Ident x = [a z A Z 0 9_]+ Nom de variable [A Z ][a z A Z 0 9_] Nom de module [A Z ][a z A Z 0 9_]. x Accès dans un module FIGURE 2.4 : Grammaire de Sarek La grammaire en figure 2.4 présente l ensemble des expressions du langage. Bien que plus proche

II. UN LANGAGE POUR LES NOYAUX DE CALCUL 61 d un mini-pascal ou d un mini-c, Sarek conserve une syntaxe à la ML pour plus de cohérence avec SPML. On définit par x une variable ; par c, une constante et par expr, une expression. Module. Sarek module Float32 : sig val add : float32 > float32 > float32 val minus : float32 > float32 > float32 val mul : float32 > float32 > float32 val div : float32 > float32 > float32 val pow : float32 > float32 > float32 val sqrt : float32 > float32 ( *... * ) val acos : float32 > float32 val cos : float32 > float32 val zero : float32 val one : float32 ( *... * ) val of_int32 : int32 > float32 val of_int64 : int64 > float32 val of_float64 : float64 > float32 end FIGURE 2.5 : Exemple : module Sarek Le langage ne permet pas de définir de fonctions, cependant il est possible d utiliser des fonctions prédéfinies associées à différents modules du langage. En particulier, Sarek étant fortement typé ces fonctions incluent des opérations de transtypage et des fonctions mathématiques. La figure 2.5 présente un exemple de module prédéfini pour Sarek. Ici, le module Float32 contient les opérations mathématiques dédiées à ce type de données. Pour simplifier la sémantique opérationnelle et plus tard la description du système de types de Sarek, on ne distinguera pas, ici les entiers et flottant 32 ou 64bits. Cela sera, cependant, nécessaire dans une implantation du langage, pour offrir des performances élevées et prédictibles. Type length vecteur int save_error uni t uni t Fonction renvoie le nombre d éléments d un vecteur stocke une information d erreur en mémoire globale TABLE 2.4 : Bibliothèque d exécution de Sarek On ajoute à cela une bibliothèque d exécution (table 2.4) qui permet d assurer la vérification des bornes des vecteurs lors d un accès à ceux-ci. En particulier, on ajoute une fonction qui renvoie la taille des vecteurs, et une autre qui stocke l identifiant global du thread courant dans un espace réservé de la mémoire globale en cas d erreur à l exécution. Ainsi, il est possible de définir un noyau de calcul réalisant l addition de deux vecteurs comme présenté dans la figure 2.6. Comme nous l avons dit, dans un noyau de calcul, on ne décrit que le comportement d un seul thread. Ici, il sera chargé de l addition d un seul élément des vecteurs a et b, correspondant à l identifiant global du thread. Le calcul se fait par effet de bord, le vecteur c est modifié pour contenir le résultat. Il suffira de lancer une grille contenant exactement autant de

62 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Code. Sarek l e t add_vector = kern a b c > l e t id = global_thread_id in c.[ <id>] < a.[ <id>] + b.[ <id>] FIGURE 2.6 : Exemple : noyau de calcul en Sarek threads qu il y a d éléments dans les vecteurs à additionner pour réaliser l addition complète. Nous reviendrons sur le lancement des noyaux et la liaison entre SPML et Sarek à la fin de cette section. II.3 Sémantique opérationnelle de Sarek Dans cette section, nous présentons la sémantique opérationnelle de Sarek, c est-à-dire, comment s évalue un programme écrit en Sarek. Il est important de rappeler que le programme Sarek est évalué par un thread unique, mais que plusieurs threads effectueront cette évaluation en parallèle. II.3.a Notations Le même type de notations que pour SPML sera utilisé. Les variables sont notées x, x 0, x 1, x n, etc. Les valeurs sont notées v, v 0, v 1, v n, etc. Les vecteurs sont notés vt, vt 0, vt 1, vt n, etc. Les booléens sont notés true ou false. Les expressions du langage sont notées e, e 0, e 1, e n, etc. On notera @v l adresse d une valeur v. Contrairement à SPML, Sarek ne peut manipuler que des adresses en mémoire globale du GPGPU exécutant le noyau. On ne précisera donc pas avec la notation @ g la position en mémoire globale du GPGPU de ces adresses. On utilisera le même domaine sémantique pour Sarek que pour SPML. Les valeurs sont ainsi spécifiées dans la sémantique par l ensemble suivant : V al = immedi ates integ er f loat adresse immedi ates = {true, false, ( )} integ er = Z f loat = R vecteur = N adresse = N II.3.b Environnement d évaluation Cette fois, on s attache à la programmation des noyaux de calcul. Les GPGPU sont considérés comme des unités de calculs isolées de l hôte et le noyau comme un programme isolé du programme hôte. Les noyaux ne peuvent modifier l état de la mémoire de l hôte et ne peuvent pas se donner de nouvelles tâches à réaliser. On ne considèrera donc pas la file d exécution du dispositif GPGPU, qui ne peut être modifié que par le programme hôte.

II. UN LANGAGE POUR LES NOYAUX DE CALCUL 63 Les expressions sont évaluées dans deux types d environnements d évaluation. Le premier est l environnement lexical E T qui associe variables et valeurs. Le second est la mémoire M. Cette foisci, comme on s attache à la partie GPU des calculs, on ne considèrera pas la mémoire de l hôte, qui n est pas accessible. Le GPGPU ne peut en effet accéder qu à sa mémoire propre, c est-à-dire sa mémoire globale, sa mémoire partagée et la mémoire locale de chaque unité de calcul. Comme précédemment, on ne considèrera pas la mémoire partagée qui est principalement utilisée à des fins d optimisation et qui est simulable par des espaces de mémoire globale. On distinguera cependant la mémoire globale du dispositif GPGPU M g de la mémoire locale accessible à chaque thread exécutant le noyau M T, M T1,... Comme pour SPML, on notera E 0 expr v E 1 l évaluation d une expression expr à partir d un environnement E 0 qui rend la valeur v dans un nouvel environnement E 1. Comme nous l avons présenté en section II.3, les noyaux de calcul décrivent une opération élémentaire qui sera exécutée par un thread appartenant à un bloc, lui-même inclus dans une grille. Le noyau de calcul sera donc évalué à trois niveaux, au niveau le plus général de la grille, au niveau des blocs et au niveau le plus fin des threads. Nous présenterons donc les trois niveaux d évaluation. Nous commencerons par l évaluation de chaque thread, puis nous étudierons l évaluation par un bloc et enfin par la grille complète. Nous reviendrons alors sur le lancement et l exécution des noyaux. Cette partie du programme fait le lien entre SPML et Sarek et entre les environnements d exécution du CPU hôte et du dispositif GPGPU. II.3.c Evaluation par un thread Les threads évaluent localement un noyau, en calculant séquentiellement la suite d opérations élémentaires qu il décrit. Ils ont accès à la mémoire globale du dispositif GPGPU, qu ils partagent, ainsi qu à leur mémoire locale. Chaque thread possède son propre environnement lexical. Sarek étant un langage impératif très simple, l évaluation locale est triviale pour la plupart des opérations. Cependant, les unités de calcul matérielles regroupées dans un même bloc (qu on appelle un warp) doivent toujours exécuter la même opération. De ce fait, l utilisation de branchement (via les constructions if-then-else ou les boucles) peut introduire de la divergence. Dans ce cas, le comportement local d un thread peut être influencé par le comportement des autres threads de son bloc. L évaluation d une constante ne modifie pas les environnements d évaluation. Const M T c c M T L évaluation d une variable x renvoie la valeur E T (x). Le système de type garantit qu il ne peut y avoir de variable introuvable lors de l exécution du noyau. Var E T, M T x E T (x) M T L évaluation d une définition locale associe une valeur à une variable. L utilisation du mot clef mutable permet de définir une liaison entre une variable mutable et une valeur. L environnement

64 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE lexical ne fait pas la différence entre une variable mutable et une variable non mutable, c est le système de type qui permet d empêcher une modification d une variable non mutable. Let-in1 E 0 T, M 0 g, M 0 T e 1 v 1 E 1 T, M 1 g, M 1 T (x, v 1 ) E 1 T, M 1 g, M 1 T e 2 v 2 E 2 T, M 2 g, M 2 T E 0 T, M 0 g, M 0 T let x = e 1 in e 2 v 2 E 2 T, M 2 g, M 2 T Let-in2 E 0 T, M 0 g, M 0 T e 1 v 1 E 1 T, M 1 g, M 1 T (x, v 1 ) E 1 T, M 1 g, M 1 T e 2 v 2 E 2 T, M 2 g, M 2 T E 0 T, M 0 g, M 0 T let mutable x = e 1 in e 2 v 2 E 2 T, M 2 g, M 2 T L ouverture locale à une expression expr d un module Mod ajoute, dans l environnement lexical, l ensemble des environnements associés au module. Ils sont supprimés de l environnement d évaluation après évaluation de l expression expr en une valeur v. L évaluation de l expression peut cependant, elle-même, modifier les environnements d évaluation du programme. Let-open Mod = E x T, M x g, M x T E 0 T, M 0 g, M 0 T e v Mod E 1 T, M 1 g, M 1 T E 0 T, M 0 g, M 0 T let open Mod in e v E 1 T, M 1 g, M 1 T L affectation d une variable mutable met à jour l environnement lexical avec la nouvelle valeur. Elle peut être utilisée dans toute séquence ce qui indique que toute expression peut modifier l environnement lexical du noyau. Affect E 0 T, M g 0, M 0 T e v 2 E 1 T, M g 1, M 1 T E 1 T (x) = v 2 E 0 T, M 0 g, M 0 T x := e ( ) E 1 T, M 1 g, M 1 T La séquence évalue la première expression que le système de type assure d évaluer en ( ). Elle évalue ensuite la seconde expression et rend sa valeur. Seq E 0 T, M g 0, M 0 T e 1 ( ) E 1 T, M g 1, M 1 T E 1 T, M g 1, M 1 T e 2 v 2 E 2 T, M g 2, M 2 T E 0 T, M 0 g, M 0 T e 1 ; e 2 v 2 E 2 T, M 2 g, M 2 T L appel de fonction rend le résultat de l exécution de la fonction appliquée à ses paramètres. L évaluation de chaque paramètre, et l exécution de la fonction peuvent modifier les environnements d évaluation. Call E 0 T, M g 0, M 0 T e 1 v 1 E 1 T, M g 1, M 1 T... E n 1 T, Mg n 1, M n 1 T e n v n E n T, M g n, M n T E n T, M g n, M n T x v 1... v n v n+1 E n+1 T, Mg n+1, M n+1 T E 0 T, M g 0, M 0 T x e 1... e n v n+1 E n+1 T, Mg n+1, M n+1 T

II. UN LANGAGE POUR LES NOYAUX DE CALCUL 65 Vecteurs Les vecteurs sont stockés en mémoire globale du GPGPU. Contrairement à SPML, aucune vérification n est faite pour vérifier la présence du vecteur. En effet, SPML nous garantit que lors de l exécution d un noyau de calcul, et jusqu au terme de cette exécution, les vecteurs nécessaires au calcul sont bien présents en mémoire globale du GPGPU exécutant le noyau. L accès à un vecteur évalue l expression associée à l indice du vecteur. Cette évaluation peut modifier l environnement d évaluation. L accès au vecteur rend alors directement la valeur lue en mémoire globale. VecAcc E 0 T (x) = @v E 0 T, M 0 g, M 0 T e 1 v 1 E 1 T, M 1 g, M 1 T v 1 [0; length(x) 1] M 1 h (@v[v 1]) = v 2 E 0 T, M 0 g, M 0 T x.[<e 1>] v 2 E 1 T, M 1 g, M 1 T Au lancement des noyaux, depuis SPML, on a associé un espace mémoire pour chaque bloc, qu on utilisera pour stocker les informations d exécution. En cas d accès en dehors des bornes du vecteur, Sarek synchronise l ensemble des threads du bloc. Nous verrons par la suite que ceci peut entraîner une modification de la mémoire globale du GPGPU par les autres threads du noyau. Ensuite, on sauvegarde dans l espace associé au bloc l identifiant du thread responsable de l erreur et on interrompt l exécution du thread. On ne sauvegardera donc qu un seul identifiant par bloc, si plusieurs threads ont rencontré cette erreur, l hôte n en sera pas informé. Ainsi, lors d une prochaine attente sur ce GPGPU, SPML vérifiera l état de l espace spécifique aux exceptions associé au noyau et pourra renvoyer une exception comme présentée dans la section précédente en page 54. VecAcc-Err E 0 T (x) = @v E 0 T, M g 0, M 0 T e 1 v 1 E 1 T, M g 1, M 1 T v 1 [0; length(x) 1] E 1 T, M g 1, M 1 T E barrier ( ); ( ) 1 T, M g 2, M 1 T Mg 2 save_error ( ); ( ) Mg 3 M 1 T exit ( ); EXIT M 1 T E 0 T, M g 0, M 0 T x.[<e 1>] EXIT E 1 T, M g 3, M 2 T L écriture dans un vecteur peut produire une erreur de la même manière que la lecture. Par ailleurs, les vecteurs sont stockés en mémoire globale du GPGPU et la modification d un champ d un vecteur modifie donc la mémoire globale. Sarek n offre aucune garantie sur l ordre d exécution des threads d un noyau, c est au programmeur de s assurer de la correction du code et du bon ordonnancement des accès à la mémoire globale. VecAff E 0 T (x) = @v E 0 T, M g 0, M 0 T e 1 v 1 E 1 T, M g 1, M 1 T v 1 [0; length(x) 1] E 1 T, M g 1, M 1 T e 2 v 2 E 2 T, M g 2, M 2 T Mg 3 (@v[v 1]) = v 2 E 0 T, M g 0, M 0 T x.[<e 1>] < e 2 ( ) E 2 T, M g 3, M 2 T

66 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE VecAff-Err E 0 T (x) = @v E 0 T, M g 0, M 0 T e 1 v 1 E 1 T, M g 1, M 1 T v 1 [0; length(x) 1] E 1 T, M g 1, M 1 T E barrier ( ); ( ) 1 T, M g 2, M 1 T Mg 2 save_error ( ); ( ) Mg 3 M 1 T exit ( ); EXIT M 1 T E 0 T, M g 0, M 0 T x.[<e 1>] EXIT E 1 T, M g 3, M 2 T Tests et SIMT Les noyaux de calcul suivent le modèle Single Instruction Multiple Threads (SIMT) (Single Instruction Multiple Thread) décrit au chapitre 4 de [84]. C est-à-dire que tous les threads d un même bloc, exécutent les mêmes instructions de manière synchronisée. Lors d un branchement, les threads peuvent avoir à évaluer différentes branches. Le modèle SIMT considère alors deux états pour les threads : passifs ou actifs. Un thread actif évalue la branche courante. Un thread passif est en attente. Lors d un branchement, si plusieurs threads d un même bloc évaluent différentes branches, alors l évaluation de chaque branche se fera séquentiellement. Dans l exemple simple, if e1 then e2 else e3 si au moins un thread du bloc évalue e 1 à vrai, et au moins un autre à faux, alors l évaluation de la conditionnelle se fera séquentiellement. Dabord tous les threads évaluant e1 à vrai seront actifs et tous les autres passifs. Puis, inversement, tous les threads évaluant e1 à vrai deviendront passif et les autres actifs. Les flots de contrôle des différents threads du bloc ont divergé, ils convergent à nouveau à la fin de la conditionnelle. Ce mécanisme sera précisé en page 67 lors de la présentation de l évaluation des noyaux au niveau des blocs. Conditionnelle La conditionnelle est composée de trois expressions la condition e 1, la conséquence e 2 et l alternance e 3. Sarek autorise les conditionnelles sans alternance, elles sont considérées comme des conditionnelles, dont la conséquence, est (). Dans le cas où le thread évalue e 1 à true, il évaluera activement e 2, mais il est possible que d autres threads évaluent e 3 en modifiant la mémoire globale. If1 E 0 T, M g 0, M 0 T e 1 true E 1 T, M g 1, M 1 T E 1 T, M g 1, M 1 T e 2 v 2 E 2 T, M g 2, M 2 T E 0 T, M g 0, M 0 T if e 1 then e 2 else e 3 v 2 E 2 T, M g 2, M 2 T Et inversement dans le cas où le thread évalue e1 à false. If2 E 0 T, M g 0, M 0 T e 1 false E 1 T, M g 1, M 1 T E 1 T, M g 1, M 1 T e 3 v 3 E 2 T, M g 2, M 2 T E 0 T, M g 0, M 0 T if e 1 then e 2 else e 3 v 3 E 2 T, M g 2, M 2 T Boucles La divergence peut aussi avoir lieu dans le cas des boucles, en effet, si un seul thread évalue le corps d une boucle, alors tous le bloc l évaluera aussi, activement ou passivement (attendant la sortie de boucle pour redevenir actif).

II. UN LANGAGE POUR LES NOYAUX DE CALCUL 67 La boucle bornée for x = e1 to e2 do e3 done est composée de trois composantes e1, e2 et e3. On évalue dans l ordre e1 en v1 puis e2 en v2. Ces évaluations peuvent modifier l ensemble des environnements d évaluation. Le résultat de la comparaison v1 > v2 permet de définir le comportement de la boucle. Si v1 > v2, alors on renvoie directement ( ). Sinon, on évalue le corps de la boucle, e3, qui peut lui aussi modifier les environnements d évaluation du noyau. Enfin, on réévalue la boucle en remplaçant e 1 et e 2 par les expressions associées aux valeurs évaluées : (v 1 + 1) et v 2. For1 E 0 T, M g 0, M 0 T e 1 v 1 E 1 T, M g 1, M 1 T E 1 T, M g 1, M 1 T e 2 v 2 E 2 T, M g 2, M 2 T v 1 v 2 (x, v 1 ) E 2 T, M g 2, M 2 T e 3 v 3 E 3 T, M g 3, M 3 T E 3 T, M g 3, M 3 T for x = (v 1 + 1) to v 2 do e 3 done ( ) E 4 T, M g 4, M 4 T E 0 T, M g 0, M 0 T for x = e 1 to e 2 do e 3 done ( ) E 4 T, M g 4, M 4 T Si le thread évalue v1 > v2 à true, il n évalue pas le corps de la boucle. Cependant, il se peut qu au moins un autre thread du bloc l évalue à false et modifie la mémoire globale. For2 E 0 T, M g 0, M 0 T e 1 v 1 E 1 T, M g 1, M 1 T E 1 T, M g 1, M 1 T e 2 v 2 E 2 T, M g 2, M 2 T v 1 > v 2 E 0 T, M g 0, M 0 T for x = e 1 to e 2 do e 3 done ( ) E 2 T, M g 2, M 2 T La boucle non bornée while e1 do e2 done s évalue tant que la condition d évaluation e1 est évaluée à true. À chaque tour de boucle, e2 est réévalué ce qui peut modifier les environnements d évaluation. While1 E 0 T, M g 0, M 0 T e 1 true E 1 T, M g 1, M 1 T E 1 T, M g 1, M 1 T e 2 ( ) E 2 T, M g 2, M 2 T E 2 T, M g 2, M 2 T while e1 do e2 done ( ) E 3 T, M g 3, M 3 T E 0 T, M g 0, M 0 T E while e1 do e2 done ( ) 3 T, M g 3, M 3 T De même que pour la boucle non bornée, la mémoire globale peut être modifiée par un autre thread du noyau. While2 E 0 T, M g 0, M 0 T e 1 false E 1 T, M g 1, M 1 T E 0 T, M g 0, M 0 T E while e1 do e2 done ( ) 1 T, M g 1, M 1 T II.3.d Évaluation par un bloc La programmation GPGPU demande la description d un noyau de calcul élémentaire. Celui-ci est exécuté en parallèle par plusieurs threads. Les threads sont groupés en blocs et les blocs sont groupés dans une grille. Lors du lancement d un noyau de calcul, la grille est projetée sur les multiprocesseurs du dispositif GPGPU. En particulier, on va associer un bloc à un ou plusieurs multiprocesseurs. En

68 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE effet, il est possible de définir des blocs contenant plus de threads que les multiprocesseurs n ont d unités de calcul ce qui implique l utilisation de plusieurs multiprocesseurs pour un seul bloc. De même, dans le cas où il n y a pas assez de multiprocesseurs, le système va exécuter autant de threads que possible sur les multiprocesseurs disponibles, puis va réitérer l opération séquentiellement jusqu à avoir exécuté l ensemble des threads du bloc. Le programmeur ne peut modifier le comportement d un bloc, il n a accès qu au code élémentaire exécuté par les threads. Cependant, certaines opérations peuvent, comme nous l avons vu, influencer le comportement de tous les threads du bloc. C est en particulier le cas de la fonction de synchronisation barrier, mais aussi des conditionnelles et boucles. En effet, celles-ci peuvent entraîner une attente, dans le cas d une divergence du code à exécuter. Notation. On notera E 0, M T 0 p... E n, M T n p un bloc de n+1 threads évaluant un programme p. On ne fait pas ici la distinction entre bloc matériel (warp) et bloc virtuel. On considèrera donc tout thread d un bloc comme faisant partie du même warp, c est-à-dire comme susceptible de faire diverger le bloc. Nous représentons un bloc comme un ensemble de threads qui évaluent un programme p en un résultat r, et l évaluation par un bloc, comme le résultat de l ensemble des évaluations. On notera l évaluation d un bloc comme suit : M 0 g, E 0 0, M 0 T 0 p... E 0 n, M 0 T n p M 1 g, r 0 E 1 0, M 1 T 0 B... r n En 1, M 1 T n Attention, même si tous les threads évaluent le même programme, comme les globales qui leurs sont associées (placement dans la grille etc.) diffèrent, le résultat de l évaluation de chaque thread pourra varier. Évaluation des expressions. L évaluation d expression par les threads d un bloc n influe généralement pas sur le comportement des autres threads du bloc. Seules la barrière, et les branchements (alternativesboucles) peuvent avoir un effet global. La synchronisation, barrier, permet d assurer que tous les threads d un bloc ont atteint un point du code avant de continuer l exécution. En pratique, si un thread atteint une instruction barrier, il informe le bloc et attend l autorisation de continuer l exécution. L implantation de la barrière est matérielle et dépendra du type de GPGPU utilisé. Ici, on modélisera une barrière par l utilisation d un compteur, bar, au niveau du bloc. Lors de l appel à barrier, par chaque thread, le bloc informé incrémente le compteur. Une fois que le compteur est égal au nombre de threads exécutés par le bloc, celui-ci autorise chaque thread à reprendre son exécution.

II. UN LANGAGE POUR LES NOYAUX DE CALCUL 69 Barrier bar < n bar, M G,... L i, M Ti barrier; p i... n+1 B bar + 1, M G,... L i, M Ti,wait; p i... n+1 bar = n n, M G,... L i, M Ti wait; p i... n+1 B 0, M G,... L i, M Ti, p i... n+1 Dans le cas des conditionnelles et boucles, les threads peuvent diverger (évaluer différentes branches) et introduire des ralentissements dans l exécution du bloc complet. En effet, dans le cas ou un thread évalue une condition à true, tous les threads du bloc évalueront la conséquence, activement s il ont aussi évalué la condition à true passivement sinon. C est-à-dire qu ils seront en attente d être réactivés. On notera p une attente d un thread passif. On considèrera ici que la conséquence est évaluée avant l alternant, mais en pratique, ceci dépendra de l implantation et du matériel utilisé. L évaluation complète de la conditionnelle sera alors composée d une séquence de deux évaluations successives, l une pour la branche true, l autre pour la branche false, ce qui entraînera une réduction du parallélisme pour l ensemble du noyau. M G,... E i, M Ti if true then p tr ue else p f al se Li+1, M Ti+1 if false then p tr ue else p f al se... n+1 M G,... B E i, M Ti, p tr ue ; p f al se Li, M Ti, p tr ue ; p f al se... Ce comportement peut largement influencer les performances des noyaux de calcul. Il est donc important d essayer d éviter la divergence.. n+1 cpt.[ <0 >] < 0 in while cpt.[ <0 >] < global_thread_id do ( ) done ; cpt.[ <0 >] < cpt.[ <0 >] + 1 FIGURE 2.7 : Exemple : Programme divergent qui ne termine pas Prenons l exemple du programme décrit figure 2.7. Ici, chaque thread à accès à un compteur global stocké dans un vecteur en mémoire globale cpt.[< 0 >]. Chaque thread lance une boucle.

70 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Dans un programme multithreadé classique (sans SIMT), Le premier thread, avec l identifiant global 0 sort de la boucle et modifie le compteur. Ainsi le second thread sortira à son tour de la boucle pour permettre aux threads suivants de continuer l exécution du programme. Dans un programme mutlthreadé classique, ce programme terminera avec le compteur égal au nombre de threads lancé. En SIMT tous les threads entreront dans l attente de la boucle infinie avant de modifier le compteur présent en mémoire globale. Le thread avec l identifiant 0 sera lui inactif jusqu a la sortie de la boucle. Le compteur ne sera jamais mis à jour. Ce programme ne terminera pas. II.3.e Évaluation par une grille Un noyau de calcul est exécuté par plusieurs threads en parallèle. Ceux-ci sont regroupés en blocs. Les blocs sont eux-mêmes regroupés dans une grille qui réalise l ensemble du calcul. De la même manière que pour les blocs, on considère que l évaluation par une grille correspond au résultat de l ensemble des évaluations par les blocs qu elle contient. } M G, { B 0... B n n n n+1 M G, { B 0 n G... } B n n n+1 Il n est pas possible de synchroniser les blocs entre eux. C est la fin d un noyau de calcul qui assure la synchronisation entre les blocs. On ne peut, par ailleurs, garantir une évaluation déterministe d une grille de blocs de threads, compte tenu des accès parallèles des différents threads à la mémoire globale. À la fin du calcul d un noyau, tous les threads de tous les blocs ont terminé leur évaluation. Un noyau de calcul travaille par effet de bord sur la mémoire globale du dispositif GPGPU. On peut représenter l exécution complète d un noyau de calcul, qui décrit l exécution parallèle du programme élémentaire p, ainsi : { M G, E 1, M T 1 p... E n, M T n p n... E 1, M T 1 p }... E n, M T n p n n M G { (),... () n G }... ()... () n n II.3.f Retour sur les lancements des noyaux de calcul Définition de noyau. Dans la partie CPU, avec SPML, il est possible de définir des noyaux de calcul. Un noyau Sarek est défini avec la construction let x = ker n x 1... x n > e s, ou e s est une expression Sarek, non évaluable par le programme hôte. On définit ainsi les paramètres d un noyau et le calcul à réaliser. On associera donc un noyau à ces paramètres, afin de pouvoir, lors du lancement d un noyau, créer une liaison pour chaque vecteur paramètre du noyau, entre nom et adresse en mémoire globale GPGPU dans l environnement lexical E de chaque thread du noyau. Un noyau K sera alors considéré comme un couple associant une liste de variables (les paramètres du noyau), à un programme à exécuter (le

II. UN LANGAGE POUR LES NOYAUX DE CALCUL 71 corps du noyau, c est-à-dire une expression Sarek). On notera donc K = ([x 1 ;... ; x n ], e) s le noyau créé à partir de l expression précédente. Description de la grille de blocs de threads. Avec SPML et Sarek, il est possible de manipuler des données transférables entre les mémoires du CPU et des GPGPU. Sarek permet d exprimer les calculs à effectuer sur la carte graphique comme avec les outils actuels de la programmation GPGPU, c est-à-dire via la description de l opération élémentaire à faire exécuter par différents threads sur la carte graphique. Cependant, actuellement, ni SPML ni Sarek ne permettent de décrire le dispositif virtuel qui va être projeté sur le dispositif physique qui permet de définir le nombre de threads à lancer sur la carte graphique ainsi que leur association en blocs. Pour résoudre ce problème, nous proposons, comme avec les outils actuels, Cuda et OpenCL, de définir les blocs et grilles de threads virtuels depuis le langage hôte et ajouter cette information au lancement d un noyau. Liaison entre SPML et Sarek. La liaison entre les deux parties du système se fait lors du lancement d un noyau de calcul. En effet, c est à ce moment qu est défini l état initial des environnements d évaluation du noyau sur le dispositif GPGPU. En particulier, la mémoire globale est mise à jour pour contenir les paramètres du noyau et ceux-ci sont associés à l environnement lexical de chaque thread. Ici l opération run est exécutée par le CPU, avec SPML, mais nous nous attachons à la modification de l environnement d évaluation du côté GPU. Comme nous l avons présenté dans la section précédente, à l issue de l appel à la fonction Run, l ensemble des arguments du noyau a été transféré en mémoire globale du dispositif GPGPU. SPML réservera aussi, en mémoire globale GPGPU, l espace associé à la gestion d erreurs pour le noyau de calcul. Nous ajoutons alors les paramètres de définition de la grille et des blocs à l appel à la fonction run. Ces paramètres représentent la taille d un bloc de la grille (c est-à-dire le nombre de threads de chaque bloc) et le nombre de blocs de celle-ci. Celle-ci va alors créer une grille en construisant blocs et threads et en associant la mémoire globale à l environnement lexical de chaque thread. Côté CPU, l appel à la fonction run s évalue comme présenté précédemment. Run-CPU M 0 h, M g 0,Q0 g run K (g rd, blc) g e1... e n ( ) M 1 h, M g 1,R K Qg 1 Côté GPU, on commence par créer la grille de blocs. On créé ainsi le nombre de blocs définis dans la description de la grille passée en paramètre. On n associe pour l instant aucun thread, et aucun calcul aux blocs ainsi créés. Run-GPU1 K = ([x 1 ;... x n ], e) M G run K (g rd, blc) g e1... e n M G, { B 1 blc... B g rd On associe maintenant des threads à chaque bloc. Pour chaque thread, on créé un environnement lexical initial. Celui-ci contient l ensemble des variables globales, c est-à-dire les arguments blc } g rd

72 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE passés au noyau lors de son lancement depuis le CPU, mais aussi des globales définies pour chaque thread. Celles-ci correspondent aux informations nécessaires pour situer le thread dans son bloc et dans la grille afin de permettre des traitements spécifiques à chaque thread. Ces globales (voir page 60) permettent d accéder à l identifiant du thread dans son bloc, l identifiant du bloc du thread dans la grille, la taille du bloc et la taille de la grille. On ajoute aussi dans l environnement lexical de chaque thread du bloc l association entre les paramètres du noyau et les adresses des vecteurs dans la mémoire globale du GPGPU. Pour chaque paramètre, on ajoute une liaison (variable, valeur), ou la variable correspond au nom du paramètre défini lors de la description du noyau de calcul (let k = kern x 1... x n ) et la valeur correspond au bloc de données, présent en mémoire GPGPU, associé au vecteur passé en paramètre. Ces valeurs sont considérées globales dans le noyau de calcul, tous les threads du noyau peuvent y lireécrire. A chaque thread est alors associé un calcul à réaliser, c est l expression Sarek e s contenue dans le noyau. Ainsi, l évaluation du noyau s effectuera comme décrite précédemment. Run-GPU2 K = ([x 1 ;... x n ], e s ) M 0 h [(@ g v 1, v 1 );...;(@ g v n, v n )] E 0 T = [(x 1, @v 1 );...; (x n, @v n )] M G run K (g rd,blc) g v 1... v n M G, E 0 T g lobs 1, e s... E 0 T g lobs blc, e s blc En conclusion, côté GPU, le lancement d un noyau de calcul correspond à l association des règles Run-GPU1 et Run-GPU2. Le lancement d un noyau de calcul ne garantit pas son exécution immédiate, seule l attente, depuis l hôte sur le GPGPU pourra assurer que l exécution à bien eu lieu et s est bien terminée sans erreur. Run-GPU { M G run K (g rd,blc) ar g s g M G, E 0 T g lobs 11, e s... E 0 T g lobs blc 1, e s blc... { } E 0 T g lobs 1g rd, e s... E 0 T g lobs blc g rd, e s blc g rd II.4 Propriétés de Sarek Comme pour SPML, la sémantique opérationnelle de Sarek nous permet de définir certaines propriétés du langage et ainsi de garantir le comportement des programmes. Propriété 5. Les noyaux de calcul ne peuvent accéder à la mémoire hôte. Cette propriété est directement issue des définitions des règles de sémantique opérationnelle de Sarek. On peut ainsi assurer que les noyaux de calcul ne modifieront pas l exécution parallèle du programme hôte. De même, on peut assurer que les valeurs transférables resteront accessibles par le programme hôte.

III. SYSTÈME DE TYPES 73 Propriété 6. Les noyaux de calcul ne peuvent modifier l adresse d un vecteur transférable. Ces deux propriétés permettent de vérifier la propriété 4 de SPML. En effet, lors de l exécution d un noyau, le programme hôte, décrit avec SPML, ne peut accéder ou modifier un vecteur transférable. Sarek peut modifier les valeurs stockées dans ce vecteur transférable mais pas son adresse. Ainsi, à la terminaison d un noyau, il est possible pour SPML de récupérer les données du vecteur transférable. III Système de types Afin d apporter une vérification statique sur les programmes, nous enrichissons nos langages d un système de type. Ce système permet de vérifier la compatibilité entre les différentes constructions du langage formant le programme. En particulier, il permet de rejeter des programmes mal formés. On s attachera dans cette section au système de types de Sarek. En effet, SPML s appuie principalement sur une version simplifiée d OCaml et son système de types a été décrit dans [85] et [86]. L extension apportée pour manipuler les données et lancer les noyaux de calcul ne modifie pas le système de types. III.1 Environnements et notations Les types associés aux expressions sont décrits dans la figure 2.8. Tous ces types sont concrets, il n y a pas de polymorphisme. Le type des fonctions associe l ensemble des types des paramètres de la fonction au type de retour. L environnement de typage Γ contient les associations (variable type). Base b = Int Entier F loat Flottant B ool Booléen Uni t Unité Vecteur v = b vect or Vecteur Mutable v = b mut abl e Mutable Type t = b v Fonction f = (t1,..., tn) t Fonctions Environnement de Γ = [] Environnement vide typage (v, t) Γ Enrichissement FIGURE 2.8 : Sarek : Types et environnements de typage

74 III.2 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Typage des expressions On note de la manière suivante qu une expression e a pour type t dans l environnement Γ Γ e t Le type des constantes ne dépend pas de l environnement de typage, ce sont toujours des entiers, des flottants ou des booléens. CST-Int CST-Float CST-Bool Γ i Int Γ d F loat Γ b Bool La liaison d une variable à une valeur enrichit l environnement de typage. On distingue le type des valeurs mutables des valeurs non mutables. Cette distinction n est faite que lors de l affectation, pour toute autre opération une valeur mutable sera considérée comme une valeur non mutable. Dans la suite, pour simplifier la description, nous ne distinguerons pas, sauf si c est nécessaire les types mutables des types non mutables. La mutabilité n est induite que si la variable créée est déclarée mutable. Si l expression affectée à la valeur mutable (à gauche) est elle-même de type mutable, c est sa valeur courante qui sera affectée. Attention, il faut bien différentier les types mutables des références (telles que décrites pour SPML). Let Let Γ e b Γ = (x,b) Γ Γ let x = e Γ Let-Mutable1 Γ e b Γ = (x,b mut able) Γ Γ let mutable x = e Γ Γ e b mut able Γ = (x,b) Γ Γ let x = e Γ Let-Mutable2 Γ e b mut able Γ = (x,b mut able) Γ Γ let mutable x = e Γ Un vecteur est de type b vector ou b est le type de chacun des éléments du vecteur. Vect i [0;n 1],Γ e i b Γ [< e 0 ;...;e n 1 >] b vector Pour l accès à une valeur dans un vecteur, on vérifie que l index est de type Int. Le vecteur doit être de type b vector et l élément du tableau est de type b. Vect-Acc Γ e Int Γ vec b vector Γ vec.[< e >] b Pour typer l affectation, on s assure que la variable à affecter (à gauche) est de type mutable, et que l expression (à droite) est de type compatible (mutable ou non). L affectation est, elle, de type Unit.

III. SYSTÈME DE TYPES 75 Affectation1 Γ e b Γ x b mut able Γ x = e Uni t Affectation2 Γ e b mut able Γ x b mut able Γ x = e Uni t De même, pour l affectation dans un vecteur. On vérifie le type du vecteur et de l expression à y affecter. On s assure aussi que l indice est bien de type Int. Là aussi, l affectation est de type Unit. Vector Affectation Γ e 1 Int Γ e 2 b Γ vec b vector Γ vec.[< e 1 >] < e 2 Uni t Vector Affectation Γ e 1 Int Γ e 2 b mut able Γ vec b vector Γ vec.[< e 1 >] < e 2 Uni t La barrière est de type Unit. Barrier Γ barrier Uni t L ouverture d un module enrichit l environnement de typage. Open Module Γ open Mod Mod Γ Mod Γ e t Γ let open Mod in e b La conditionnelle est bien typée si sa condition est de type Bool et si ses sous-expressions sont bien typées. En particulier, dans le cas d une conditionnelle simple (sans alternative), le type de la conséquence doit être Unit. Dans le cas d une conditionnelle avec alternative, les types de la conséquence et de l alternative doivent être les mêmes. If1 Γ e1 Bool Γ e2 Uni t Γ if e1 then e2 Uni t If2 Γ e1 Bool Γ e2 b Γ e3 b Γ if e1 then e2 else e3 b

76 CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE La boucle bornée (for) est bien typée si son corps est bien typé (c est-à-dire de type Unit) et si les expressions qui définissent ses bornes sont bien de type Int. For Γ e1 Int Γ e2 Int Γ e3 Uni t Γ for x = e1 to e2 do e3 done Uni t La boucle non bornée (while) est bien typée si sa condition est de type Bool et son corps de type Unit. While Γ e1 Bool Γ e2 Uni t Γ while e1 do e2 done Uni t La séquence est bien typée si la première expression est de type Unit. Son type est le type de sa seconde expression. Seq Γ e1 Uni t Γ e2 b Γ e1 ; e2 b Les opérateurs sont monomorphes. On distinguera les opérateurs capables de manipuler des entiers, de ceux capables de manipuler des flottants ou des booléens. Op-Int {+,,, } Γ e1 Int Γ e2 Int Γ e1 e2 Int Op-Float {+.,.,.,.} Γ e1 F loat Γ e2 F loat Γ e1 e2 F loat Op-Bool {&&, } Γ e1 Bool Γ e2 Bool Γ e1 e2 Bool Les opérateurs de comparaison sont monomorphes et renvoient toujours une valeur booléenne. On ne peut cependant comparer que des Int, des Bool ou des F loat. Sarek ne permet pas de comparer les valeurs non constructibles comme les vecteurs ou les fonctions. Comp-Int {=,<>,<,>} Γ e1 Int Γ e2 Int Γ e1 e2 Bool

III. SYSTÈME DE TYPES 77 Comp-Bool {=.,<>.,<.,>.} Γ e1 Bool Γ e2 Bool Γ e1 e2 Bool Comp-Float {=,<> } Γ e1 F loat Γ e2 F loat Γ e1 e2 Bool Pour l appel de fonction, on vérifie d abord que le type de la fonction est bien un type fonctionnel. Puis, on vérifie que le type de chacun des arguments de la fonction correspond bien au type attendu. Le type de l appel est égal au type de retour de la fonction. Function-call Γ e (t 0,... t n 1 ) t i [0;n 1],Γ e i t i Γ e e 0... e n 1 t Ce système de type permet de définir l ensemble des règles à respecter pour écrire un code Sarek correct. Il sera donc possible de vérifier, automatiquement, que l ensemble de ces règles est respecté par un programme écrit en Sarek, et donc de rejeter des programmes erronés. De la même manière, le système de types étant relativement simple, il sera aisé de proposer de l inférence de type pour Sarek, en s appuyant uniquement sur les constructions syntaxiques. III.3 Liaison entre SPML et Sarek Les systèmes de type de SPML et Sarek sont différents. Chacun est vérifié séparément. Cependant, le type des données partagées entre les deux domaines de la programmation GPGPU, les vecteurs, est commun. C est à nouveau la fonction de lancement des noyaux de calcul qui permet la liaison entre SPML et Sarek. Cette fois, au lancement d un noyau, les types des paramètres passés au noyau doivent être équivalents à ceux des arguments décrits lors de la définition du noyau. Lors de la définition d un noyau de calcul, une valeur SPML est créée qui associe le corps du noyau, le calcul à évaluer, et ses arguments, et donc leur type. Le corps du noyau est typé par le système de types de Sarek décrit précédemment. Kernel-decl i [0;n 1], Γ x i b i vector Γ let k = kern x 0... x n 1 -> ker nel_bod y (b 0 vector,..., b n 1 vector ) ker nel Lors du typage du corps du noyau Sarek, les paramètres sont considérés comme des variables globales. Ainsi, l environnement de typage initial du noyau Sarek contient les liaisons (variable, type) associées aux paramètres du noyau. Γ ini t = (x 0, b 0 vector )... (x n 1, b n 1 vector ) De même, du côté de SPML, c est la fonction d exécution d un noyau, qui permet que le type des paramètres soit vérifié.

78 IV Conclusion CHAPITRE 2. LANGAGES POUR LA PROGRAMMATION GPGPU : SÉMANTIQUE OPÉRATIONNELLE ET TYPAGE Nous avons décrit un langage, SPML, pour l expression des programmes hôtes, capable de manipuler données transférables entre la mémoire hôte et la mémoire des dispositifs GPGPU. Ces transferts se font automatiquement, à la demande. Leur exécution est asynchrone et implicite via l utilisation des expressions de lectureécriture dans un vecteur ou bien lors du lancement d un noyau de calcul. Par ailleurs, nous avons décrit Sarek, un langage pour la description des noyaux de calcul. Ce langage permet d opérer des calculs sur les données en mémoire GPGPU et de les modifier, par effet de bord. Il permet aussi de décrire l architecture virtuelle que l on souhaitera utiliser pour chaque noyau. Ces deux langages, associés, permettent de décrire l ensemble d un programme GPGPU, à la fois la partie hôte et la partie destinée aux dispositifs invités. Nous avons enrichi Sarek d un système de types. Celui-ci permet de détecter de nombreuses erreurs de programmation et donc de rejeter des programmes erronés, dès la phase de compilation, avant leur exécution.

CHAPITRE 3 Programmation GPGPU : implantation avec OCaml «FORTRAN, the infantile disorder, by now nearly 20 years old, is hopelessly inadequate for whatever computer application you have in mind today : it is now too clumsy, too risky, and too expensive to use.» E. W. Dijkstra (1975) Sommaire I Choix d implantation.................................... 82 I.1 Pourquoi OCaml?................................. 82 I.2 Pourquoi une bibliothèque?........................... 82 II Présentation de la bibliothèque SPOC.......................... 83 II.1 Unification des systèmes OpenCL et Cuda................... 83 II.2 Transferts de données............................... 86 II.3 Exemple de programme SPOC.......................... 90 III Noyaux de calcul....................................... 92 III.1 Noyaux de calcul externes............................. 92 III.2 Un langage intégré dédié aux noyaux : Sarek.................. 94 III.3 Différences avec le formalisme.......................... 98 IV Tests de performance.................................... 99 IV.1 Petits exemples................................... 100 IV.2 Bibliothèques optimisées............................. 102 IV.3 Comparaison avec d autres langages de haut niveau............. 103 IV.4 Calcul multi-gpu................................. 105 V Cas d utilisation : portage du programme PROP.................... 107 V.1 Présentation du programme étudié....................... 108 V.2 Portage avec SPOC................................. 110 V.3 Résultats et performances............................ 111 VI Conclusion.......................................... 113 79

80 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML Résumé Ce chapitre présente une implantation des langages SPML et Sarek décrits au chapitre précédent en utilisant le langage OCaml. SPML est implanté sous la forme d une bibliothèque pour OCaml, SPOC, dédiée à la programmation GPGPU et à l automatisation des transferts. Sarek est un langage dédié à l expression des noyaux de calcul, intégré à OCaml. Pour SPOC, comme pour Sarek, nous présentons d abord les outils du point de vue de l utilisateur avant de préciser certains choix d implantation. Cette thèse a pour objet l étude d abstraction performante pour les cartes graphiques. Pour vérifier le niveau de performance, nous proposons l évaluation des performances obtenues sur une série de tests simples. Ceux-ci sont exécutés sur différentes architectures, parfois très hétérogènes (avec plusieurs accélérateurs de calculs différents). Pour aller plus loin, nous proposons, en fin de chapitre, un retour d expérience sur le portage d une application numérique hautes-performances depuis Fortran et Cuda vers OCaml avec les outils (bibliothèque et langage) que nous avons implantés. Cette évaluation permet de vérifier que nos outils permettent d atteindre de hautes performances tout en simplifiant la programmation GPGPU et qu ils peuvent ainsi être utilisés, à la fois par le programmeur OCaml cherchant à exploiter les GPGPU, mais aussi par la communauté numérique haute-performance pour simplifier l expression de programmes complexes.

81 LA PROGRAMMATION GPGPU est complexe. Elle implique l utilisation de solutions de bas niveau qui demandent la manipulation explicite des dispositifs GPGPU, mais aussi des transferts mémoires. Ceci accroît les difficultés de programmation et augmente le risque de bogues. De plus, les deux systèmes, Cuda et OpenCL, sont incompatibles. Même OpenCL, qui est un standard, et a priori portable, propose nombres d extensions spécifiques à certaines implantations ou architectures. Il est donc difficile d écrire du code GPGPU, qui plus est du code portable et performant. Dans le chapitre précédent, nous avons présenté deux langages, SPML et Sarek, pour la programmation GPGPU. SPML s attache à la partie hôte, c est-à-dire, la partie du programme exécuté par le CPU. Sarek, lui, s attache à l expression des noyaux de calcul GPGPU. Dans le but de proposer un outil performant et qui simplifie la programmation GPGPU, nous proposons une implantation de ces deux langages, à l aide du langage OCaml. Pour résoudre les problèmes de portabilité et abstraire certains aspects liés au matériel et au modèle de bas niveau utilisé dans la programmation GPGPU, nous avons implanté SPML à travers une bibliothèque pour le langage OCaml. Nos motivations sont de simplifier la manipulation des noyaux de calcul GPGPU et de la mémoire, tout en maintenant un haut niveau de performance. En particulier, nous souhaitons faciliter la portabilité des programmes, que ce soit entre architectures matérielles, mais aussi entre les systèmes logiciels Cuda et OpenCL. Afin de permettre une manipulation plus naturelle des GPGPU, nous souhaitons aussi libérer les programmeurs de la gestion explicite des transferts de données entre la mémoire du CPU hôte et celle des dispositifs GPGPU invités. Bien que visant à accroître le niveau d abstraction, nous avons fait l ensemble de nos choix en ciblant la conservation des performances, c est-à-dire dans l objectif d offrir une solution capable d atteindre des niveaux de performances proches ou équivalents à ceux offerts avec les outils de bas niveau. Ce chapitre présente la bibliothèque SPOC[87, 88] dédiée à la programmation GPGPU avec OCaml. Nous présenterons en section I les raisons qui nous ont poussés à choisir le langage OCaml, à travers les spécificités du langage utilisées dans l implantation de la bibliothèque. La section II présentera la bibliothèque SPOC du point de vue de l utilisateur en décrivant l ensemble de ses fonctionnalités, mais détaillera aussi son implantation. Nous discuterons particulièrement de trois aspects de cette bibliothèque, d abord pour la portabilité, puis en nous attachant aux transferts de données et enfin à la manipulation des noyaux de calcul. Par ailleurs, nous souhaitons permettre l utilisation de noyaux de calcul externes. Ceci permet d exploiter directement les nombreuses bibliothèques optimisées existantes. Pour ce faire, nous proposons une extension à OCaml pour manipuler des noyaux de calcul décrits avec les outils de bas niveaux de Cuda et OpenCL. Cette extension et son utilisation sont décrites en section III.1. Pour l expression des noyaux de calcul, nous proposons une implantation de Sarek sous la forme d un langage dédié, intégré à OCaml. La section III.2 présente cette implantation et la liaison entre SPOC et Sarek. Enfin, nous proposerons en section IV différents tests de performance pour valider notre approche. Nous présenterons d abord, en section IV.1 des exemples simples pour vérifier les gains obtenus en comparaison avec une exécution séquentielle avec OCaml. Nous vérifierons aussi que SPOC permet bien de faciliter portabilité et utilisation de multiples dispositifs GPGPU en parallèle. Nous comparerons notre approche à différentes solutions de haut niveau pour la programmation GPGPU. Afin de vérifier le niveau de performance obtenu et la validité d une utilisation de SPOC pour la programmation haute-performance, nous présenterons en section V un portage d une application HPC reconnue dans le domaine de la physique depuis Fortran vers OCaml et discuterons des performances obtenues.

82 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML I Choix d implantation Cette section discutera des choix d implantation ainsi que des bénéfices et difficultés qu ils apportent. En particulier, notre choix s est porté sur un langage d implantation de haut niveau d abstraction, OCaml, et sur le développement d une bibliothèque plutôt que d un compilateur ou d un nouveau langage. I.1 Pourquoi OCaml? OCaml [89, 90] est un langage de programmation de haut niveau généraliste, développé par IN- RIA. C est un langage multiparadigme : fonctionnel, impératif et orienté objet. Il est fortement et statiquement typé avec inférence de types. Il offre une gestion automatique de la mémoire par le biais d un ramasse-miettes (Garbage Collector (GC)) efficace. La distribution OCaml dispose de deux compilateurs. L un, qui produit du code-octet portable (il existe des machines virtuelles OCaml pour beaucoup d architectures), tandis que l autre produit du code natif efficace. OCaml est entièrement interopérable avec le langage C. SPOC bénéficie directement de plusieurs de ces aspects. En utilisant les bibliothèques C issues des systèmes Cuda et OpenCL, elle exploite évidemment l interopérabilité entre OCaml et C. SPOC cible la programmation GPGPU et les hautes performances, cela implique d utiliser un langage de programmation déjà efficace pour les calculs séquentiels, ce que permettent les compilateurs natifs d OCaml. En outre, avec SPOC, nous visons à faciliter la programmation GPGPU tout en améliorant la fiabilité des logiciels et la productivité. La vérification statique des types améliore la fiabilité en détectant de nombreuses erreurs de programmation dès la compilation, tandis que le gestionnaire automatique de mémoire garantit la cohérence des données (pas de fuite de mémoire ou de pointeurs fous) et limite l utilisation de la mémoire lors de l exécution. SPOC bénéficie aussi des multiples paradigmes d OCaml, car il utilise la programmation modulaire, fonctionnelle et séquentielle, mais aussi la programmation orientée objet pour gérer les noyaux. Ceci fera l objet d une description plus précise dans la suite de cette section. Enfin, OCaml est un langage extensible. En effet, il existe plusieurs préprocesseurs comme Camlp4 ou Camlp5 pour OCaml qui permettent d étendre la syntaxe du langage. Ces outils sont particulièrement utiles pour le développement de sous-langages intégrés à OCaml. Camlp4 fait partie de la distribution actuelle (4.01) d OCaml et nous l avons utilisé pour l implantation de Sarek[91]. I.2 Pourquoi une bibliothèque? Pour offrir une solution de haut niveau pour la programmation GPGPU, nous avions deux principales solutions : développer un langage etou un compilateur pour la manipulation des GPU, ou développer une bibliothèque pour un langage existant. Le développement d un langage dédié offre un contrôle total sur l ensemble de la chaîne de compilation, et permet d analyser l ensemble du code pour produire du code compatible avec les GPGPU. Le développement d une bibliothèque permet de toucher une communauté déjà large et de profiter des optimisations et constructions de haut niveau déjà présentes dans le langage. De plus, cela donne accès aux outils déjà existants pour ce langage. Nous avons choisi de développer une bibliothèque pour proposer rapidement une solution efficace, en nous appuyant sur le langage de programmation de haut niveau, OCaml. De plus, il per-

II. PRÉSENTATION DE LA BIBLIOTHÈQUE SPOC 83 met d offrir une solution portable, au moins pour la partie hôte. Le développement d une bibliothèque pour un langage connu et utilisé semble aussi une solution plus pérenne. Notre bibliothèque s appuie uniquement sur des outils intégrés dans la distribution standard du langage pour limiter la dépendance à des outils tiers qui pourraient devenir incompatibles avec OCaml à l avenir. Par ailleurs, OCaml est distribué avec le préprocesseur Camlp4. Celui-ci permet d étendre le langage via des constructions dédiées, sans modifier le compilateur OCaml. Il nous permet donc d ajouter les extensions nécessaires à la prise en charge des GPGPU par OCaml. Le choix d une bibliothèque pour OCaml simplifie donc le développement d une solution portable, optimisée, pérenne et facilement utilisable par les développeurs. II Présentation de la bibliothèque SPOC SPOC 1 (Stream Processing with OCaml) consiste en une extension à OCaml associée à une bibliothèque d exécution. L extension permet la déclaration de noyaux GPGPU externes utilisables depuis un programme OCaml, tandis que la bibliothèque permet de manipuler ces noyaux ainsi que les données nécessaires à leur exécution. En effet, elle implante le système décrit dans SPML qui permet des transferts automatiques entre la mémoire hôte et celles des dispositifs GPGPU. SPOC offre une abstraction supplémentaire en unifiant les deux environnements de développement GPGPU en une même bibliothèque. C est-à-dire en permettant l écriture d un seul et même code qui spécifiera son comportement en fonction de types de GPGPU (compatibles Cuda ou OpenCL) rencontrés à l exécution. Dans cette section, nous présenterons d abord ce système d unification de Cuda et OpenCL. Ensuite, nous présenterons l implantation de l extension présentée dans SPML pour la gestion automatique des transferts. Cette implantation s appuie sur l introduction d un type de données spécifique, les vecteurs. II.1 Unification des systèmes OpenCL et Cuda Comme différents environnements existent pour la programmation GPGPU, il peut être difficile d obtenir portabilité et efficacité. La bibliothèque SPOC détecte, lors de son initialisation, l ensemble des GPGPU compatibles avec Cuda ou OpenCL et les rend exploitables par le programme OCaml. Elle unifie aussi les deux environnements en s adaptant lors de l exécution en fonction du matériel utilisé et de l environnement (Cuda ou OpenCL) correspondant. Les outils actuels sont proposés par les constructeurs et ciblent uniquement leur matériel (voir figure 3.1). NVidia par exemple propose Cuda ainsi qu une implantation d OpenCL pour ses GPU tandis qu AMD ou Intel proposent des implantations d OpenCL différentes pour leurs CPU ou leurs GPU. SPOC s appuie sur tous ces outils pour offrir une compatibilité totale vers toutes les architectures compatibles avec au moins l un des deux systèmes, Cuda ou OpenCL (voir figure 3.2). Ceci permet l exécution d un même programme sur différentes architectures, mais aussi d utiliser de multiples GPGPU (y compris avec les deux environnements incompatibles) conjointement dans des systèmes hétérogènes. 1. http:algo-prog.infospoc

84 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML OpenCL GPU CPU NVidia. Intel OpenCL Cuda GPU GPU CPU CPU AMD OpenCL IBM OpenCL GPU Accelerator FIGURE 3.1 : Compatibilité des systèmes GPGPU implantés par différents constructeurs AMD CPU Intel IBM... OpenCL GPU AMD NVidia Intel ARM... SPOC. Accelerator IBM Altera Parallela... Cuda GPU NVidia FIGURE 3.2 : Compatibilité système et matérielle de SPOC II.1.a Pour l utilisateur Afin de simplifier l utilisation de la bibliothèque, nous avons cherché à en limiter les dépendances. En particulier, elle doit être utilisable sur des machines ne disposant ni de Cuda ni d OpenCL, et donc sur des architectures ne disposant d aucun système ou dispositif GPGU. Pour cela, nous avons implanté SPOC avec une liaison dynamique vers les bibliothèques OpenCL et Cuda. En effet,

II. PRÉSENTATION DE LA BIBLIOTHÈQUE SPOC 85 l utilisateur peut compiler des programmes utilisant SPOC sur toute architecture compatible avec OCaml, c est lors de l exécution du programme compilé que SPOC recherchera des systèmes (Cuda etou OpenCL) et des dispositifs GPGPU. Afin de lancer la détection automatique, l utilisateur doit initialiser la bibliothèque d exécution (fig. 3.3). Cette initialisation renvoie un tableau contenant tous les dispositifs GPGPU manipulables avec SPOC. Si le système ne trouve aucun dispositif ou environnement sur le système, il renverra un tableau vide.. val init :?only : specificlibrary > unit > device array FIGURE 3.3 : Type OCaml de la fonction d initialisation L ensemble des fonctions de la bibliothèque SPOC prend en paramètre un device. Celui-ci permet en fonction des spécificités de l architecture ciblée (compatibilité CudaOpenCL, architecture GPUCPU, etc.) à la bibliothèque d exécution de faire les traitements nécessaires automatiquement. Si SPOC n a pas détecté de système Cuda ou OpenCL installé, ou s il n a pas trouvé de dispositifs GPGPU associés, le tableau retourné par la fonction d initialisation sera vide. Le programme pourra s exécuter séquentiellement, mais, les fonctions GPGPU de SPOC, qui demandent un device en paramètre ne pourront être utilisées. Il conviendra donc, pour l utilisateur de vérifier que le résultat de l initialisation contient bien des dispositifs GPGPU.. type generalinfo = { name : string; ( * * nom du d i s p o s i t i f * ) totalglobalmem : int; ( * * quantité t o t a l e de mémoire globale * ) localmemsize : int; ( * * quantité t o t a l e de mémoire locale * ) clockrate : int; ( * * fréquence du d i s p o s i t i f * ) multiprocessorcount : int; ( * * nombre de multi processeurs * ) eccenabled : bool; ( * * le code de correction d erreur est i l activé? * ) id : int; ( * * id du d i s p o s i t i f * ) ctx : context; ( * * contexte associé * ) } type specificinfo = CudaInfo of cudainfo OpenCLInfo of openclinfo type device = { general_info : generalinfo; specific_info : specificinfo; } FIGURE 3.4 : Type OCaml d un device Un device (voir figure 3.4)contient les informations générales concernant le dispositif qui sont disponibles avec Cuda, comme avec OpenCL. Il contient aussi des informations spécifiques au système utilisé. Celles-ci peuvent être très nombreuses et dépendent à la fois du système concerné (ver-

86 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML sion de Cuda ou d OpenCL prise en charge, etc.) et dans le cas d OpenCL du type de dispositif. Un GPU, un CPU ou un accélérateur n auront pas les mêmes informations spécifiques. Toutes ces informations sont accessibles au programmeur, lui permettant, s il le souhaite d optimiser son programme pour des architectures précises. II.1.b Implantation Afin de permettre l utilisation de SPOC sur toutes architectures et de n effectuer la détection qu à l exécution, nous avons utilisé la liaison dynamique. En effet, SPOC se lie dynamiquement avec la bibliothèque Driver (de plus bas niveau) de Cuda et avec la bibliothèque OpenCL. Cuda est fourni avec deux API, la bibliothèque Driver et la bibliothèque Runtime. SPOC s appuie sur la bibliothèque Driver qui ne demande aucune installation supplémentaire pour fonctionner, mais uniquement un driver NVidia chargé lors de l exécution. La bibliothèque Runtime de Cuda, de plus haut niveau, demande l installation des outils Cuda sur le système pour pouvoir être utilisée. De plus, la bibliothèque Driver donne un accès plus fin au système Cuda et permet donc de proposer une bibliothèque aux fonctionnalités plus spécifiques qu avec la bibliothèque Runtime. Il n était, par exemple pas possible, avec les premières versions de Cuda de manipuler un même device depuis plusieurs threads CPU avec la bibliothèque Runtime, alors que c était possible avec la bibliothèque Driver. Un autre avantage vient du fait que la bibliothèque Driver de bas niveau est très proche de la bibliothèque fournie par OpenCL. Ceci simplifie l implantation en permettant une plus grande réutilisation du code. Cette étape, réalisée à l initialisation, permet d associer les systèmes Cuda et OpenCL à SPOC. SPOC va alors détecter les dispositifs, compatibles, et construire le tableau renvoyé par la fonction d initialisation. Pour chaque fonction de la bibliothèque SPOC, deux versions existent, l une pour OpenCL, l autre pour Cuda. Le device passé en paramètre, est lié à un système ce qui permet de choisir automatiquement le code qui convient. Nous avons appliqué cette approche à Cuda et OpenCL, mais il serait de la même manière possible de cibler d autres systèmes proches comme DirectCompute ou même le sous-ensemble dédié au shading language d OpenGL. II.2 Transferts de données Comme présenté dans les deux chapitres précédents, la programmation GPGPU implique des transferts mémoire, entre le CPU hôte, et les GPGPU invités. Cuda et OpenCL demandent des transferts explicites à travers des API de bas niveau. De plus, pour obtenir de hautes performances pour des programmes complexes, il est important d optimiser l ordonnancement de ces transferts. Nous avons donc implanté l extension GPGPU de SPML pour OCaml afin d offrir des transferts automatiques dans SPOC. Ce système présente aussi l avantage de limiter les transferts. En effet, un modèle de programmation GPGPU basique consiste à transférer toutes les données nécessaires à un calcul sur le dispositif concerné, puis, après avoir réalisé le calcul, à rapatrier toutes les données. En effet, il est difficile de savoir à l avance quelles données ont été modifiées par le noyau de calcul, et cette solution assure de rapatrier les données modifiées. Elle présente cependant deux inconvénients : elle introduit des transferts inutiles en rapatriant des données qui ne seront plus utilisées par le CPU et elle risque d introduire d autres transferts dans le cas d une réutilisation des données sur le dispositif GPGPU. En ne transférant les données que par nécessité (comme décrit dans SPML), c est-à-dire uniquement lorsqu elles sont utilisées, on évite ces deux problèmes et on réduit considérablement le

II. PRÉSENTATION DE LA BIBLIOTHÈQUE SPOC 87 nombre de transferts en ne conservant que les transferts réellement utiles. Nous avons, en particulier, utilisé le gestionnaire mémoire d OCaml pour implanter ce mécanisme. Pour cela, SPOC introduit un type d ensemble de données appelé vectors. À l exécution, lors d un accès à un vector (par le CPU ou lors du lancement d un noyau), SPOC vérifie la position courante du vector et le déplace si nécessaire. Les vectors sont gérés par le gestionnaire mémoire d OCaml qui les allouelibère automatiquement, CPU hôte ou des GPGPU invités. En particulier, si un transfert vers un GPGPU échoue, car il n y à pas assez d espace mémoire disponible sur celui-ci, SPOC va déclencher le ramasse-miettes d OCaml. Celui-ci va automatiquement terminer l ensemble des calculs et transferts en cours sur le GPGPU puis va tenter de libérer la mémoire du GPGPU en supprimant les vecteurs désormais inutiles avant de réessayer de réaliser le transfert. En cas de nouvel échec, SPOC va renvoyer une exception OCaml signalant le manque de mémoire sur le GPGPU. Ceci libère le programmeur de la gestion des transferts mémoires qui sont une part importante, complexe et source de bogues de la programmation GPGPU calculs. II.2.a Pour l utilisateur SPOC permet de définir des ensembles de données transférables sur un GPGPU : les vecteurs. En particulier, il est possible de créer deux types de vecteurs, des vecteurs contenant des types prédéfinis et des vecteurs paramétrables pour définir des types de données personnalisés. S appuyant sur le module Bigarray d OCaml, les vecteurs prédéfinis permettent de créer des ensembles d entiers, flottants, caractères et booléens transférables et utilisables avec des noyaux GPGPU. Le module Bigarray d OCaml est généralement utilisé pour l expression de grands ensembles de données, mais aussi pour simplifier l interopérabilité avec du code numérique C ou Fortran. En basant les vecteurs dessus, SPOC simplifie aussi l interopérabilité avec les codes numériques existants (en OCaml, aussi bien qu en C ou Fortran). La création d un vecteur (voir figure 3.5) demande de préciser le type des données qu il contient (flottant ou entiers de 32 ou 64 bits) ainsi que le nombre d éléments qui le composent. Ainsi SPOC pourra réserver les espaces mémoire nécessaires dans la mémoire hôte, mais aussi dans la mémoire des GPGPU. L argument optionnel (?dev Devices.device) permet de définir un vecteur dont la position initiale est sur un dispositif GPGPU.. val create : ( a, b) kind >?dev :Devices.device > int > ( a, b) vector type ( a, b) kind = Float32 of ( a, b) Bigarray.kind Char of ( a, b) Bigarray.kind Float64 of ( a, b) Bigarray.kind Int32 of ( a, b) Bigarray.kind Int64 of ( a, b) Bigarray.kind Complex32 of ( a, b) Bigarray.kind Custom of a custom FIGURE 3.5 : Type OCaml de la fonction de création de vecteurs

88 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML Les vecteurs paramétrables permettent de construire des ensembles de données complexes en décrivant l ensemble des caractéristiques nécessaires à la construction de données transférables sur GPGPU. En particulier, ils demandent de décrire les opérations de transformation entre le type de données, utilisable en OCaml, et le type de données correspondant (en langage C), utilisable dans les noyaux GPGPU. Le type a custom (voir figure 3.6) permet de décrire un élément d un vecteur paramétré. Celui-ci demande la description de l élément OCaml (elt), une évaluation de sa taille pour pouvoir allouer l espace nécessaire pour le transfert sur le périphérique GPGPU et des fonctions de transformation (g et et set) pour intégrer un élément OCaml dans un tableau décrit en C (représenté en OCaml par le type abstrait cust omar r a y). C est effectivement ce tableau C qui sera transféré et accessible depuis les noyaux GPGPU. La fonction g et permet de lire un élément du tableau C et de renvoyer une valeur OCaml correspondante. La fonction set réalise l opération inverse et traduit une valeur OCaml en valeur C avant de la stocker dans le tableau.. type a custom ={ elt : a; size : int ; get : customarray > int > a; set : customarray > int > a > unit} FIGURE 3.6 : Type OCaml des vecteurs paramétrables La figure 3.7 présente un exemple d utilisation de vecteurs paramétrés. Ici on définit, en C les fonctions nécessaires au calcul de la taille d un vecteur, pour l allocation mémoire sur les dispositifs GPGPU et pour réaliser les transferts. On décrit aussi comment traduire un élément du vecteur OCaml en un élément compatible avec Cuda ou OpenCL. Cet ensemble de fonctions C est ensuite directement utilisé dans la déclaration du type des éléments du vecteur. Le vecteur OCaml d éléments de type pointml est automatiquement projeté sur un vecteur C d éléments de type pointc. SPOC met alors à jour les fonctions d accès en lectureécriture au vecteur OCaml ainsi que les fonctions de transfert en utilisant les fonctions externes définies dans le type custompoint. Les transferts se font par défaut automatiquement. Cependant, il est possible de forcer les transferts pour en optimiser l ordonnancement si nécessaire. Ceci s effectue par l utilisation de deux fonctions : to_device pour transférer sur un GPGPU et to_cpu pour rapatrier les données en mémoire CPU (voir figure 3.8. Il est aussi possible de désactiver complètement les transferts automatiques (réduisant un peu le nombre de tests sur la localité des vecteurs à l exécution). Cela demande alors de réaliser explicitement les transferts. Afin de permettre plus de flexibilité dans l écriture des programmes, mais aussi de simplifier la description de programmes multi-gpgpu, SPOC permet de définir des sous-vecteurs. Ceux-ci ont la particularité de partager l espace mémoire de leur parent en mémoire CPU et pas en mémoire GPGPU. Cela permet de simplifier le découpage de certains problèmes en dédiant certaines portions des vecteurs à des noyaux et dispositifs GPGPU particuliers. Chaque noyau travaillera sur un sousvecteur, en réduisant la quantité de données à transférer pour ce calcul. Cependant, le vecteur parent, s il sert de résultat sur le GPU, sera automatiquement mis à jour avec les valeurs calculées sur ses sous-vecteurs.

II. PRÉSENTATION DE LA BIBLIOTHÈQUE SPOC 89 Code OCaml. type pointml = { mutable x : float; mutable y : float; } ( * associe des fonctions OCaml à des fonctions C * ) external getsizeofpoint : unit > int = "custom_getsizeofpoint" external extget : customarray > int > pointml = "custom_extget" external extset : customarray > int > pointml > unit = "custom_extset" l e t custompoint = {elt = { x = 0. ; y = 0. } ; ( * élément OCaml par défaut * ) size = getsizeofpoint( ) ; ( * correspond à la fonction C custom_getsizeofpoint * ) get = extget; ( * correspond à la fonction C custom_extget * ) set = extset; ( * correspond à la fonction C custom_extset * ) } Code. C struct pointc{ float x; float y; } ; renvoie la valeur OCaml correspondant à la t a i l l e d un élément du vecteur CAMLprim value custom_getsizeofpoint( ) { CAMLparam0( ) ; CAMLreturn(Val_int( sizeof ( struct pointc) ) ) ; } renvoie une copie d un élément du vecteur dans une valeur OCaml CAMLprim value custom_extget (value customarray, value idx) { CAMLparam2(customArray, idx) ; CAMLlocal1(mlPoint) ; struct pointc * pt; pt = ( ( struct pointc * ) (Field(customArray, 1) ) ) +(Int_val(idx) ) ; mlpoint = caml_alloc( 2, 0) ; Store_double_field(mlPoint, 0,( float ) (pt >x) ) ; Store_double_field(mlPoint, 1, ( float ) (pt >y) ) ; CAMLreturn(mlPoint) ; } copie une valeur OCaml dans un élément du vecteur CAMLprim value custom_extset (value customarray, value idx, value v) { CAMLparam3(customArray, idx, v) ; struct pointc * pt; pt = ( ( struct pointc * ) (Field(customArray, 1) ) )+Int_val(idx) ; pt >x= ( float )Double_field(v, 0) ; pt >y= ( float )Double_field(v, 1) ; CAMLreturn(Val_unit) ; } FIGURE 3.7 : Exemple : déclaration d un vecteur paramétré avec OCaml et C. val to_device : ( a, b) Vector.vector > Devices.device > unit val to_cpu : ( a, b) Vector.vector > unit val auto_transfers : bool > unit FIGURE 3.8 : Types OCaml des fonctions de transferts explicites

90 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML II.2.b Implantation Notre implantation de SPOC suit la sémantique de SPML décrite au chapitre 2. Ainsi, SPOC permet de réaliser des transferts, automatiques, via les vecteurs. Ceux-ci sont des structures pointant vers différents espaces mémoire. Lors de la création d un vecteur, un espace est alloué en mémoire CPU. Lors d un transfert vers un périphérique GPGPU, un espace mémoire est alloué sur le GPGPU. L espace mémoire CPU n est pas libéré lors d un transfert, pour permettre d assurer le rapatriement des données sur CPU afin qu un vecteur reste accessible tout au long de sa vie. Ceci permet en effet d éviter de tenter l accès, par le CPU, à un vecteur présent sur un GPGPU alors qu il n y a plus d espace disponible en mémoire CPU pour réaliser le transfert. Cependant, comme décrit dans SPML, SPOC interdit l ubiquité des vecteurs. À chaque transfert, l information de position est mise à jour et le système considère le vecteur comme uniquement disponible à cette position. Même si aucune modification n est réalisée sur un vecteur transféré sur un GPGPU, un accès à ce même vecteur depuis le CPU entraînera un transfert. SPOC vérifie la position des vecteurs à trois occasions : lors d une lecture du vecteur depuis le CPU, lors d une écriture depuis le CPU ou lors du lancement d un noyau de calcul. SPOC transfère les vecteurs en entier, ainsi, effectuer une lecture d une valeur d un vecteur présent sur un GPGPU entraîne donc le transfert du vecteur complet. Les sous vecteurs peuvent réduire ce problème. Les deux environnements, Cuda et OpenCL étant incompatibles, un transfert d un vecteur présent sur un dispositif manipulé par SPOC via l un vers un dispositif manipulé via l autre nécessite un passage par la mémoire CPU. Pour les transferts entre dispositifs compatibles, une amélioration de notre implantation pourra profiter des éventuelles optimisations offertes par les bibliothèques Cuda et OpenCL en déclenchant un transfert direct entre eux, sans passer par la mémoire CPU. Les transferts de données sont asynchrones, il est donc difficile de prévoir quand il sera possible de libérer l espace occupé par un vecteur sur un dispositif GPGPU. La première solution implantée consistait à attendre l accès par l hôte (ou le lancement d un nouveau noyau) pour libérer le vecteur, car c est le seul moment où l on est sûr d avoir terminé le transfert vers la mémoire hôte. En associant chaque transfert à un événement Cuda ou OpenCL, nous avons pu optimiser ce mécanisme. Les événements Cuda ou OpenCL sont des structures qu on peut associer aux commandes passées dans la file de commandes d un GPGPU (stream en Cuda et command_queue en OpenCL). À chaque événement est associé une fonction de rappel. Lors de la terminaison d une commande, cette fonction est appelée. Nous avons donc associé les transferts vers la mémoire CPU avec une fonction qui libère l espace mémoire sur le dispositif GPGPU. Ceci permet de libérer l espace au plus tôt. Dès qu un transfert asynchrone se termine, la fonction de rappel associée est appelée et l espace mémoire source est libéré. II.3 Exemple de programme SPOC La figure 3.9 présente un exemple de programme OCaml utilisant SPOC. Elle décrit l intégralité du programme hôte nécessaire au lancement d un noyau de calcul qui réalise l addition de deux vecteurs. À droite du code, on peut voir la position des trois vecteurs, a, b et c en mémoire CPU ou dans la mémoire du dispositif GPGPU invité. Le programme commence par initialiser le système et détecter l ensemble des dispositifs compatibles avec SPOC (ligne 4).

II. PRÉSENTATION DE LA BIBLIOTHÈQUE SPOC 91 1 l e t example ( ) = 2 ( * I n i t i a l i s a t i o n * ) 3 ( * renvoie un tableau contenant l ensemble des d i s p o s i t i f s compatibles * ) 4 l e t devs = Spoc.Devices.init ( ) in 5 l e t a = Spoc.Vector.create Spoc.Vector.float32 1024 CPU Vector location a b c 6 and b = Spoc.Vector.create Spoc.Vector.float32 1024 CPU CPU 7 and c = Spoc.Vector.create Spoc.Vector.float32 1024 in 8 ( * l e s vecteurs sont remplis de valeurs a l é a t o i r e s 9 fill_vectors [a; b; c ] ; * ) 10 ( * description de la g r i l l e de blocs à p r o j e t t e r 11 l e t blk = {Spoc.Kernel.blockX = 256 ; * ) 12 Spoc.Kernel.blockY = 1 ; 13 Spoc.Kernel.blockZ = 1 ; } 14 and grd = {Spoc.Kernel.gridX = 4 ; 15 Spoc.Kernel.gridY = 1 ; 16 Spoc.Kernel.gridZ = 1 ; } in 17 ( * l e noyau e s t lancé sur l e premier GPGPU du système * ) 18 Spoc.Kernel.run devs. ( 0 ) (blk,grd) vector_add (a, b, c, 1024) ; 19 ( * l e s r é s u l t a t s sont a f f i c h é s * ) 20 for i = 0 to 1023 do 21 Printf.printf "%g\n" (c.[ <i>]) 22 done ; ; CPU CPU CPU GPU GPU GPU GPU GPU CPU FIGURE 3.9 : Exemple : programme hôte avec transferts automatiques via OCaml et SPOC On initialise ensuite trois vecteurs de flottants (lignes 5 à 7). Ces vecteurs sont de type prédéfini et s appuient sur les Bigarrays d OCaml. On constate, en particulier qu on distingue les flottants simple précision (32bit) des flottants double précision (64bit). En effet, OCaml manipule uniquement des flottants double précision, mais il est important, pour des raisons de performance de permettre l utilisation de flottants simple précision sur les dispositifs GPGPU. En effet, comme on a pu le voir en introduction, de nombreux GPGPU ne sont pas capables de réaliser des calculs en double précision, ou alors avec des performances très faibles. On initialise alors les valeurs des vecteurs aléatoirement (ligne 9). On décrit alors la grille de blocs de threads qui sera utilisée pour projeter le noyau sur les unités de calcul du GPGPU (lignes 11 à 16). On lance alors le noyau de calcul (ligne 18). Ceci déclenche le transfert des vecteurs utilisés par ce noyau (a,b et c) vers la mémoire du dispositif GPGPU chargé de réaliser le calcul. On imprime ensuite le contenu du vecteur c (lignes 20 à 22). Cette opération est réalisée par le CPU. Elle implique donc de transférer le vecteur en mémoire CPU. Celui-ci est utilisé par le noyau de calcul. SPOC attend donc la fin du calcul sur le GPGPU puis réalise le transfert du vecteur c en mémoire CPU avant de permettre la lecture des éléments du vecteur. Les vecteurs a et b ne sont pas utilisés par l hôte, ils restent donc en mémoire GPGPU jusqu à ce qu ils soient à nouveau utilisés ou libérés par le ramasse-miettes d OCaml. Cet exemple montre bien la gestion automatique des transferts telle que décrite dans SPML. En comparaison d un programme décrit avec les API CC++ pour Cuda ou OpenCL, l utilisation d OCaml offre une vérification statique des types pour l ensemble du programme hôte avec une gestion automatique de la mémoire. La bibliothèque SPOC et les transferts automatiques permettent aussi de libérer le programmeur de la verbosité introduite par Cuda et OpenCL. Le programme hôte est alors plus court, plus sûr et plus simple. L annexe B propose une comparaison entre Cuda Run-

92 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML time, Cuda Driver, OpenCL et SPOC pour l écriture du programme hôte. Elle pourra permettre de mieux apprécier les gains offerts par l utilisation de la bibliothèque SPOC et du langage OCaml. III Noyaux de calcul Comme différents environnements existent pour la programmation GPGPU, il peut être difficile d obtenir portabilité et efficacité. SPOC détecte, lors de son initialisation, l ensemble des GPGPU compatibles avec Cuda ou OpenCL et les rend exploitables par le programme OCaml. Elle unifie aussi les deux environnements en s adaptant lors de l exécution en fonction du matériel utilisé et de l environnement (Cuda ou OpenCL) correspondant. Ceci permet l exécution d un même programme sur différentes architectures, mais aussi d utiliser de multiples GPGPU (y compris avec les deux environnements incompatibles) conjointement. SPOC permet de manipuler des vecteurs de données transférables ainsi que des noyaux de calcul GPGPU. Pour exprimer ces noyaux, nous proposons deux solutions. La première consiste en l utilisation des outils existants via la déclaration de noyaux de calcul externes. La seconde consiste en une implantation de Sarek, décrit au chapitre précédent. III.1 Noyaux de calcul externes Noyaux. SPOC permet l utilisation de noyaux externes écrits en assembleur Cuda ou en OpenCL C99 (le sous-ensemble du C fourni avec OpenCL pour la description des noyaux de calcul). Elle offre une extension à OCaml pour déclarer ces noyaux, d une manière similaire à celle qu OCaml propose pour déclarer des fonctions C externes. Cette extension permet la vérification statique des types des arguments du noyau réduisant les risques d erreurs difficiles à déboguer lors de l exécution. Cependant, le corps des noyaux de calcul est décrit en dehors d OCaml qui ne pourra donc pas vérifier leur correction. III.1.a Pour l utilisateur Comme lors de l utilisation d externals C, l utilisateur exprime les noyaux de calcul dans le langage de bas niveau Cuda ou OpenCL et les déclare dans le code OCaml pour pouvoir les utiliser. La figure 3.10 présente un exemple simple de noyau OpenCL et comment le déclarer dans le code OCaml pour l utiliser avec SPOC, comme dans le programme présenté en figure 3.9. Le noyau vec_add, prend trois vecteurs ( global float *) stockés dans la mémoire globale du GPGPU en paramètre ainsi qu un entier représentant la taille des vecteurs à additionner. Chaque unité de calcul (dont l identifiant est obtenu via get_global_id(0)) va calculer l addition d un seul élément de chaque vecteur. Le code OCaml associé permet l utilisation d un tel noyau dans un programme OCaml. Il consiste en la déclaration du nom du noyau suivi de son type et de deux chaînes de caractères. Les deux chaînes permettent d identifier le fichier externe contenant le noyau ainsi que la fonction dans ce fichier correspondant au noyau. Ici, nous pouvons voir que les types des paramètres doivent être traduits pour être compatibles avec SPOC et OCaml de global float* vers Spoc.Vector.float32.

III. NOYAUX DE CALCUL 93 Code OpenCL. kernel void vec_add( global const float * a, global const float * b, global float * c, int vector_size) { int nindex = get_global_id( 0) ; i f (nindex < vector_size) c[nindex] = a[nindex] + b[nindex ] ; } Code OCaml. kernel vector_add : Spoc.Vector.vfloat32 > Spoc.Vector.vfloat32 > Spoc.Vector.vfloat32 > int > unit = "kernel_file" "vec_add" FIGURE 3.10 : Exemple : déclaration d un noyau GPGPU avec SPOC III.1.b Implantation L implantation se base sur l utilisation d une extension Camlp4 pour OCaml. Celle-ci permet de traduire la déclaration en une structure manipulable en OCaml. En effet, cette déclaration est transformée en la déclaration d une classe OCaml qui est par la suite instanciée en un objet OCaml. À chaque noyau correspondent une classe et un objet. Cette classe contient les fonctions d accès au code source du noyau ainsi que des fonctions de compilation et exécution de ce code.. class [ a, b] spoc_kernel : string > string > object ( * * compiled binaries * ) val binaries : (Devices.device, kernel) Hashtbl. t ( * * cuda source code of the kernel * ) val mutable cuda_sources : string list ( * * opencl source code of the kernel * ) val mutable opencl_sources : string list ( * * compiles a kernel for a device * ) method compile :?debug : bool > Devices.device > unit ( * * compiles and run a device for a kernel * ) method compile_and_run : a > block * grid > int > Devices.device > unit method exec : a > block * grid > int > Devices.device > kernel > unit ( * * runs the kernel on a device * ) method run : a > block * grid > int > Devices.device > unit end FIGURE 3.11 : Type OCaml de la classe générée pour chaque noyau La classe générée est sous type d une classe plus générale dont un extrait est présenté en figure 3.11. La fonction d exécution est polymorphe et dépend du type des paramètres du noyau. On bénéficie donc du polymorphisme d inclusion qui permet d utiliser indifféremment plusieurs noyaux avec des fonctions d exécution différentes. La classe [ a, b] spoc_ker nel est définie en fonction des types a et b qui correspondent aux types des paramètres du noyau sous deux formats différents. Le type a correspond aux paramètres

94 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML tel que le programmeur les utilisera lors de l exécution d un noyau, alors que le type b correspond à une traduction de ces paramètres pour les manipuler via des fonctions C des APIs Cuda et OpenCL. La traduction est réalisée automatiquement par SPOC, les fonctions de traduction étant générées par l extension Camlp4 avec pour chaque type de vecteur OCaml un type correspondant du côté C. Un objet noyau contient les méthodes nécessaires à la compilation et à l exécution du code source du noyau. SPOC ne compile les noyaux de calcul qu une fois par dispositif (les binaires sont incompatibles entre différents dispositifs), sauf si le programmeur le demande explicitement. Ceci permet d éviter une recompilation coûteuse d un noyau utilisé plusieurs fois dans le programme. De la même manière, SPOC ne relira par défaut pas le code source. Spoc.Kernel.run dev (block,grid) vector_add. (a,b,c, 1024) ; Système associé à dev OpenCL Cuda Fichier source du noyau kernel void vec_add (... ) {... } kernels.cl.entry vec_add (... ) {... } kernels.ptx for i = 0 to 1023 do Printf.printf "%g" (c.[<i>]) done ;; FIGURE 3.12 : Compilation dynamique d un noyau GPGPU externe La figure 3.12 décrit la phase de compilation et d exécution d un noyau de calcul externe. À l exécution, lors de l appel de la fonction Kernel.run, SPOC recherche le système de bas niveau (Cuda ou OpenCL) qui gère le dispositif chargé d exécuter le noyau. En fonction du système, SPOC recherche le fichier (assembleur Cuda ou OpenCL c99) associé au noyau, et dans ce fichier la fonction qui définit le noyau. Une fois la fonction chargée, SPOC la compile et l exécute sur le dispositif GPGPU. L exécution est asynchrone. Une fois celle-ci lancée, SPOC rend la main au programme OCaml. III.2 Un langage intégré dédié aux noyaux : Sarek Afin de permettre de décrire des noyaux internes, c est-à-dire, depuis le programme OCaml, et d offrir des constructions de plus haut niveau sur les noyaux de calcul, nous avons implanté Sarek sous la forme d un langage dédié, intégré à OCaml. Là aussi, nous avons utilisé une extension Camlp4. Celle-ci se charge de réaliser une première phase de compilation vers un code intermédiaire qui est embarqué dans le code OCaml préprocessé. Nous avons implanté un module de compilation de ce code intermédiaire, utilisable depuis le code OCaml. Ce module génère le code Cuda et OpenCL, à l exécution. Ce code est alors utilisé comme dans le cas d un noyau externe.

III. NOYAUX DE CALCUL 95 III.2.a Pour l utilisateur Le programmeur doit décrire un noyau de calcul en utilisant le langage Sarek tel que décrit au chapitre précédent. Il doit par la suite utiliser le module Kirc (Kernel Internal Representation Compiler) à la place du module Kernel (pour les noyaux externes) pour compiler et exécuter le noyau. open Spoc l e t vec_add = kern a b c n > ( * l e module Std donne accès aux globales GPGPU * ) l e t open Std in ( * i d e n t i f i a n t global du thread dans la g r i l l e * ) l e t idx = global_thread_id in ( * pour é v i t e r l e s accès hors des bornes * ) i f idx < n then c.[ <idx>] < a.[ <idx>] + b.[ <idx>] l e t dev = Devices.init ( ) l e t n = 1_000_000 l e t v1 = Vector.create Vector.float64 n l e t v2 = Vector.create Vector.float64 n l e t v3 = Vector.create Vector.float64 n. Kernel Code (Sarek) l e t block = {blockx = 1024 ; blocky = 1 ; blockz = 1} l e t grid={gridx=(n+1024 1)1024 ; gridy=1 ; gridz=1} l e t main ( ) = random_fill v1; random_fill v2; Kirc.gen vec_add ; Kirc.run vec_add (v1, v2, v3, n) (block,grid) dev. ( 0 ) ; for i = 0 to Vector.length v3 1 do Printf.printf "res[%d] = %f ; " i v3.[ <i>] done ; Host Code (SPOC) FIGURE 3.13 : Addition de vecteur avec Sarek La figure 3.13 présente le code complet du programme d addition de vecteurs utilisant Sarek. Il est donc possible avec Sarek de décrire l ensemble du programme GPGPU dans un seul fichier avec une syntaxe proche de celle d OCaml. De plus, l extension CamlP4 permet de réaliser l inférence des types dans le noyau de calcul et réaliser une vérification statique des types qui produit, si nécessaire, des erreurs dès la phase de compilation du code OCaml. Ceci permet de détecter de nombreuses erreurs dès la phase de compilation, là où l utilisation de noyaux externes ne vérifie que les types des paramètres déclarés. En effet, avec les noyaux externes, il faut attendre la compilation dynamique pour vérifier le code, et ce avec les outils de bas niveau qui s appuient sur le langage C et son système de types plus faible que celui de Sarek. Sarek permet donc une plus grande cohérence dans l écriture du code, mais assure aussi une meilleure vérification des programmes, dès la phase de compilation du code OCaml. Sarek permet, par ailleurs, d utiliser certaines valeurs du programme hôte. En effet, il est possible d accéder à des variables globales du programme hôte de types int, float ou bool dans le code du noyau Sarek. Celles-ci ne seront liées au noyau que lors de la génération du code OpenCL ou Cuda qui a lieu à l exécution du programme.

96 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML III.2.b Implantation L implantation de Sarek s appuie sur une extension Camlp4. Celle-ci définit la grammaire du langage, mais aussi le vérificateur de types et la première phase de compilation. En effet, Sarek est d abord compilé statiquement vers un code intermédiaire qui est par la suite compilé, dynamiquement vers du code OpenCL ou Cuda. Compilation statique. La figure 3.14 présente la phase de compilation statique. On commence par parcourir le noyau Sarek pour générer une représentation intermédiaire (sous forme d Abstract Syntax Tree (arbre de syntaxe abstraite) (AST) du programme (voir table 3.1). Cet AST est alors utilisé pour inférer les types et les vérifier. C est lors de cette phase que l on peut produire des erreurs de types et interrompre la compilation. À partir de l AST typé, on génère alors trois codes différents. D abord un code OCaml qui correspond au contenu du noyau de calcul, c est-à-dire l opération élémentaire qu il décrit. Ce code vise à être utilisé dans un simulateur de dispositif GPGPU écrit en OCaml. Il sera par ailleurs vérifié par le compilateur OCaml qui assurera que notre vérification n a pas laissé passer d erreur. Sarek. kern a let idx = Std.global_thread_id () in a.[< idx >] 0 I R Bind( (Id 0), (ModuleAccess((Std), (global_thread_id)), (VecSet(VecAcc...)))) Typage Génération de code OCaml typed I R Génération du spoc_kernel OCaml fun a > let idx = Std.global_thread_id () in a.[< idx >] < 0l Génération de Kir Kir spoc_kernel Kern Params class spoc_class1 VecVar 0 method run =... VecVar 1 method compile =...... end new spoc_class1 FIGURE 3.14 : Compilation statique de Sarek On produit ensuite une représentation intermédiaire du noyau de calcul qu on nomme Kir (Kernel Internal Representation). Celui-ci sera intégré au code OCaml et sera compilé dynamiquement vers du code Cuda ou OpenCL. Enfin, on produit une classe et un objet OCaml comparables à ceux

III. NOYAUX DE CALCUL 97 Sarek IR E 1 kern id 1... id n > e Kern (Params [f(id 1 );...; f(id n )], f(e)), E 2 E 1 let open M in e f(e), E 2 E 1 if e 1 then e 2 else e 3 If (f(e 1 ), f(e 2 ), f(e 3 )), E 2 E 1 for id = e 1 to e 2 doe 3 done For (f(id), f(e 1 ), f(e 2 )), E 2 E 1 while e 1 do e 2 done While (f(e 1 ), f(e 2 )), E 1 E 1 let id = e 1 in e 2 Bind (g(id), f(e 1 ), f(e 2 )), E 2 E 1 e 1 e 2 BinOp (f(e 1 ), f(e 2 )), E 2 e 1.[<e 2 >] VecGet (f(e 1 ), f(e)), E 2 E 1 e 1.[<e 2 >] <- e 3 VecSet (f(e 1 ), f(e 2 ), f(e 3 )), E 2 E 1 barrier Barrier, E 1 E 1 id e 1... e n Call (f(id), [f(e 1 );...;f(e n )), E 2 E 1 id Id id, E 1 E 1 c Cons c, E 1 E 1 : environnement de compilation f : fonction qui produit le code intermédiaire g : introduction d une variable fraîche dans l environnement E 1 e A,E 2 : compilation de l expression e depuis l environnement E 1 vers l AST A et l environnement E 2 TABLE 3.1 : Schéma de compilation de Sarek vers IR produits par l extension destinée aux noyaux externes. La principale différence entre les deux vient de la liaison de l objet avec le code Kir, plutôt qu avec un fichier source externe. Compilation dynamique. Lors de l exécution, le code Kir est compilé vers du code source Cuda et OpenCL (voir figure 3.15). La table 3.2 présente un extrait des transformations possibles. Sarek, Cuda et OpenCL sont très proches, on ne détaillera donc pas les transformations qui se font directement. On ne réalise aucune optimisation sur les programmes Cuda et OpenCL. Ceci pourra faire l objet de travaux futurs.. let my_kernel = kern... >...... ;; Kirc.gen my_kernel ; Compile vers un fichier source Cuda Compile vers nvcc -O3 -ptx... Kirc.run my_kernel dev (block,grid) ; OpenCL C99 GPGPU OpenCL Cuda Assembleur ptx Cuda noyau OpenCL C99 Cuda ptx assembly Compilation et Exécution Retour à l exécution du code OCaml FIGURE 3.15 : Compilation dynamique de Sarek

98 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML Kir Cuda OpenCL # i f d e f cplusplus extern "C" { #endif Kern (args,body) global void dummy ( Kirc (args) ) { Kirc (body) } kernel void dummy ( Kirc (args) ) { Kirc (body) } # i f d e f cplusplus } #endif Var (t,i) t : Int int spoc_var_i; int spoc_var_i; t : Float f l o a t spoc_var_i; f l o a t spoc_var_i; VecVar (t,i) t : Int int * spoc_var_i; global int * spoc_var_i; t : Float f l o a t * spoc_var_i; global f l o a t * spoc_var_i; Set (var, value) Kirc (var) = Kirc (value) Kirc (var) = Kirc (value) For (e1, e2, e3, e4) for ( Kirc (e1) = Kirc (e2) ; Kirc (e1) <= Kirc (e3) ; Kirc (e1) ++) { Kirc (e4) } for ( Kirc (e1) = Kirc (e2) ; Kirc (e1) <= Kirc (e3) ; Kirc (e1) ++) { Kirc (e4) } Barrier syncthreads( ) barrier( ) Kirc est la fonction de génération de code CudaOpenCL TABLE 3.2 : Extrait de la génération de code Cuda et OpenCL Le code source Cuda n est pas utilisable directement, il faut le transformer en assembleur Cuda, pour cela nous utilisons le compilateur nvcc de Nvidia. Une optimisation future pourra consister à générer non pas du Cuda C, mais de l assembleur directement. Ce code est ensuite utilisé comme pour un noyau externe, à la place du code source chargé dans un fichier. La figure 3.16 montre le code généré à partir du noyau Sarek décrivant l addition de deux vecteurs. III.3 Différences avec le formalisme Dans le chapitre précédent, nous avons formalisé les extensions dédiées à la programmation GPGPU implantées ici. Cette implantation s appuie sur le formalisme et ajoute quelques optimisations particulières. Par exemple, SPOC supprime les données sources lors d un transfert sans attendre que le ramasse-miettes d OCaml ne se déclenche, pour éviter un déclenchement inutile qui ralentirait l exécution générale du programme. D un autre côté, certaines propriétés de SPML et Sarek ne sont pas implantées ici. En particulier, le mécanisme d exceptions qui demande au noyau de calcul de vérifier les dépassements de bornes lors d un accès à un vecteur n ont pas été intégrés à Sarek. En effet, pour faire cette vérification, il faut introduire de nombreuses alternatives qui peuvent faire diverger chaque thread d un même bloc. Néanmoins, si le calcul est correct, c est-à-dire qu on accède bien à une donnée du vecteur, tous ces tests devraient entrer dans la même branche de l alternative et réduire la divergence. Cependant, le simple fait d ajouter ces instructions de tests supplémentaires

IV. TESTS DE PERFORMANCE 99 Code OpenCL. kernel void spoc_cl_0 ( global int * spoc_var0, global int * spoc_var1, global int * spoc_var2 ) { int spoc_var4; spoc_var4 = get_global_id ( 0) ; } spoc_var2[spoc_var4] = spoc_var0[spoc_var4] + spoc_var1[spoc_var4 ] ; Code. Cuda global void spoc_cu_0 ( int * spoc_var0, int * spoc_var1, int * spoc_var2 ) { int spoc_var4; spoc_var4 = blockidx.x * blockdim.x +threadidx.x; spoc_var2[spoc_var4] = (spoc_var0[spoc_var4] + spoc_var1[spoc_var4] ) ; } FIGURE 3.16 : Code CudaOpenCL généré depuis un noyau Sarek (fig.3.13) peut déjà impacter les performances. Par ailleurs, pour que le programme hôte soit informé de l erreur et puisse lever une exception, il faut réaliser un transfert supplémentaire pour chaque transfert. Dans le cas de programmes ou l on lance plusieurs noyaux de calcul à la suite sur un même dispositif sans rendre la main au CPU, attendre entre chaque noyau pour effectuer ce transfert réduira encore les performances. Cette vérification et le mécanisme d exception présenté au chapitre 2 pourront être implantés dans une version debug du compilateur dynamique de Sarek qui produira un code moins performant. IV Tests de performance Afin de vérifier le niveau de performance obtenu avec nos outils, nous proposons dans cette section de les mesurer à l aide de différents types d exemples. Tout d abord, nous présenterons les performances obtenues avec des programmes simples et classiques, souvent utilisés comme tests de performance dans la littérature. Nous étudierons ici les performances obtenues en utilisant à la fois des noyaux de calcul externes, et des noyaux de calcul décrits avec Sarek. Pour certains d entre eux, nous comparerons notre implantation à d autres outils de haut niveau pour la programmation GPGPU. Afin d être le plus exhaustif possible, nous montrerons aussi les résultats obtenus sur certains programmes manipulant plusieurs dispositifs GPGPU en même temps, y compris des dispositifs utilisant des systèmes différents. En section V, nous présenterons un test de performance s appuyant sur un code de physique optimisé pour atteindre de hautes performances. En effet, afin de s assurer que nos outils puissent être utilisés aussi bien pour accélérer les programmes OCaml, que pour simplifier la programmation haute performance classique, nous avons effectué le portage d un programme HPC connu depuis Fortran et Cuda vers OCaml. Ce programme nommé PROP sera présenté ainsi que son portage et les performances obtenues.

100 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML IV.1 Petits exemples Dans cette section, nous allons présenter les résultats obtenus avec cinq programmes de test. Chacun d entre eux a été écrit en OCaml et exploite les GPGPU avec SPOC. Chaque exemple est proposé en deux versions, l une qui s appuie sur des noyaux de calcul externes (décrits en Cuda ou OpenCL) et une version qui utilise Sarek. Des comparaisons seront effectuées avec des programmes OCaml compilés avec le compilateur natif ocamlopt et exécutés séquentiellement par le CPU seul. Nous les comparerons aussi avec des programmes équivalents, écrits avec d autres outils de haut niveau pour la programmation GPGPU : Copperhead, ScalaCL, Obsidian, Accelerate et RustGPU. Les exemples utilisés sont des exemples classiques de la programmation parallèle qui comportent différentes propriétés : Mandelbrot calcule la fractale représentant l ensemble de Mandelbrot. Elle est définie comme Z 0 = 1 l ensemble des points c du plan complexe pour lesquels la suite définie par Z n+1 = Zn 2 + c ne diverge pas. Cet exemple est facilement parallélisable. En effet, chaque point du plan complexe est indépendant des autres et on peut donc assigner une unité de calcul au traitement d un de ces points. Par ailleurs, la quantité de calcul peut varier pour chaque point, certains nécessitant plus ou moins d itérations pour converger (ou non). Cependant, les points d une même zone du plan, auront tendance à se comporter d une manière équivalente, en dédiant un bloc de threads au traitement d une zone du plan, on pourra maximiser l utilisation des unités de calcul des dispositifs GPGPU. De plus, on peut faire varier le critère de divergence, et donc le nombre d itérations maximal pour chaque point, ceci permet de facilement faire varier la quantité de calcul et donc de mesurer efficacement l impact des transferts (qui restent constant), en fonction des calculs. Power est un programme qui élève naïvement l ensemble des éléments d un vecteur à une puissance donnée en utilisant une boucle. En faisant varier la puissance, on fait varier la quantité de calcul. Vector Addition correspond à l exemple présenté dans le chapitre 3. Cet exemple est facilement parallélisable, car là aussi il n y a pas de dépendance entre les éléments à calculer. Matrix Multiplication possède les mêmes propriétés que l addition de vecteurs. Cependant, cet exemple a un ratio calcultransfert plus important et permet donc de mesurer l impact des temps de transfert sur les programmes. Game of Life est une boucle qui évalue à chaque itération l état courant d un environnement à partir de l état précédent. Il n y a pas de dépendance entre les données à calculer, mais il y a une dépendance entre générations. Ceci implique une gestion différente de la mémoire. Dans cet exemple, nous affichons le résultat obtenu (via le module Graphics d OCaml). Cet affichage est pris en charge par le CPU ce qui demande un transfert de l état à afficher vers le CPU. Ainsi, on pourra, en modifiant le nombre d itérations entre chaque affichage faire varier la quantité de transferts. C est un modèle simple proche des codes de différences finies avec snapshot utilisés dans la modélisation de nombreux phénomènes physiques. Ces exemples sont relativement simples et permettent de mesurer les performances obtenues avec SPOC sur différentes architectures matérielles. Cela permet aussi d évaluer les types de pro-

IV. TESTS DE PERFORMANCE 101 grammes qui pourront bénéficier des dispositifs GPGPU, en fonction du rapport calculdonnées. IV.1.a Comparaison avec OCaml GPGPU OCaml C2070 GTX 680 6950HD i7-3770 Test CPU - 1 Coeur Cuda Cuda OpenCL OpenCL Mandelbrot ext 474.5s 5.9 ( 80.4) 4.0 ( 118.6) 4.9 ( 96.8) 6.0 ( 79.1) Mandelbrot Sarek 7.0 ( 67.8) 4.8 ( 98.8) 5.6 ( 84.7) 7.2 ( 65.9) Power ext 25.30s 4.5 ( 5.6) 4.3 ( 5.9) 4.6 ( 5.5) 4.5 ( 5.6) Power Sarek 5.9 ( 4.3) 5.1 ( 5.0) 6.0 ( 4.2) 5.8 ( 4.4) VecAdd ext 83.05s 8.6 ( 9.7) 7.8 ( 10.6) 8.3 ( 10.0) 13.7 ( 6.1) VecAdd Sarek 10.5 ( 7.9) 9.0 ( 9.2) 9.4 ( 8.8) 14.6 ( 5.7) Matmult ext 85.0s 1.3 ( 65.4) 1.7 ( 50.0) 2.5 ( 34.0) 4.8 ( 17.7) Matmult Sarek 1.7 ( 50.0) 2.1 ( 40.5) 2.6 ( 32.7) 6.2 ( 13.7) Life ext 209.66s 11.0 ( 19.1) 9.8 ( 21.4) 10.3 ( 20.4) 13.6 ( 15.4) Life Sarek 13.6 ( 15.4) 11.4 ( 18.4) 12.6 ( 16.6) 15.5 ( 13.5) Les accélérations sont entre parenthèses TABLE 3.3 : Mesure de performances : temps (en s) et accélérations La table 3.3 présente les résultats obtenus pour les différents programmes présentés avec différentes architectures : une carte graphique dédiée à la programmation haute performance, la Tesla C2070 de NVidia, exploitée via le système Cuda. une carte graphique haut de gamme grand public, plutôt dédiée aux jeux vidéo, la Geforce GTX 680 de NVidia, exploitée via le système Cuda. une carte graphique grand public du constructeur AMD, une AMD 6950HD, exploitée via l implantation OpenCL de AMD. un processeur multicœurs (4 cœurs) classique, le Intel Core-i7 3770, exploité via l implantation OpenCL d Intel. Ce processeur est aussi utilisé pour la mesure du temps de la version OCaml exécutée séquentiellement sur un cœur. Ces architectures représentent les principales architectures GPGPU. Elles possèdent par ailleurs des propriétés différentes. Par exemple, la 6950 HD demande d utiliser les fonctions SIMD vectorielles des GPGPU pour offrir davantage de performances, les deux cartes NVidia préféreront une programmation scalaire, enfin le CPU multicœur profitera d une programmation vectorielle, mais l implantation OpenCL d Intel contient un compilateur optimisant permettant une vectorisation automatique performante. On constate donc que, quel que soit le dispositif utilisé, les performances sont meilleures qu avec une utilisation classique d OCaml. Tous ces programmes sont très parallélisables et bénéficient donc largement des nombreuses unités de calcul des cartes graphiques. L utilisation du processeur via OpenCL permet aussi d accroître les performances. En effet, cela permet d abord d exploiter les quatre cœurs du CPU, mais aussi, grâce au compilateur fourni par Intel (qui compile dynamiquement le

102 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML noyau GPGPU) des extensions vectorielles de ce CPU (ici les extensions SSE et AVX). On constate de plus que logiquement, ce sont les programmes au plus fort rapport meilleures accélérations. calcul s tr ans f er t s qui offrent les D une manière générale les cartes graphiques offrent de meilleures performances sur ce type de programmes que les CPU cependant, on constate que lorsque le programme a un faible rapport calcul s tr ans f er t s les performances sont très proches sur CPU et cartes graphiques. De même, pour les exemples avec de nombreux transferts ou de nombreux affichages graphiques (gérés par le CPU) les performances CPU sont similaires à celles obtenues avec les GPU. IV.2 Bibliothèques optimisées Afin d offrir les meilleures performances possible aux programmeurs, nous avons conçu SPOC pour qu il puisse facilement permettre l utilisation de bibliothèques optimisées. En particulier, il permet de manipuler explicitement les transferts mémoires (sans s appuyer sur les transferts automatiques à la demande) si besoin et autorise le lancement de noyaux de calcul externes. Ainsi, il est possible d organiser les transferts manuellement depuis le code OCaml pour s interfacer avec de bibliothèques précompilées manipulables depuis du code C, aussi bien que d exploiter directement depuis le programme SPOC des noyaux de calcul optimisés. Afin de vérifier le niveau de performance atteint avec de telles bibliothèques, nous avons comparé notre implantation naïve de la multiplication de matrices avec une version utilisant la fonction sgemm issue des bibliothèques Cublas et Magma ainsi qu avec une version du programme écrite en C utilisant ces mêmes bibliothèques. Pour cet exemple, nous n avons utilisé que la carte Tesla C2070. Temps de calcul (s) Accélération par rapport à OCaml (85s) 1.3 654 607 654 654. 0.13 0.14 0.13 0.13 65.4.. SPOC SPOC+Cublas SPOC+Magma C+Cublas C+Magma FIGURE 3.17 : Mesure de performances : Bibliothèques optimisées - Multiplication de matrices La figure 3.17 présente les résultats obtenus. On constate que logiquement, l utilisation de bibliothèques optimisées permet d obtenir des performances nettement supérieures à notre implantation naïve (les versions optimisées vont autour de 10 plus vite). On constate aussi que les deux bibliothèques offrent des performances similaires, mais aussi que notre implantation avec OCaml s appuyant sur SPOC offre les mêmes performances que la version C. Notre implantation permet donc d obtenir un très haut niveau de performance lors de l utilisation de noyaux de calcul très optimisés.

IV. TESTS DE PERFORMANCE 103 IV.3 Comparaison avec d autres langages de haut niveau La programmation GPGPU avec SPOC permet d accroître les performances des programmes que nous avons présentées, en comparaison d une exécution classique et séquentielle sur un CPU. Ces calcul s programmes étant fortement parallélisables et offrant de bons rapports tr ans f er t s. Afin de situer les performances apportées par nos solutions, nous avons souhaité les comparer avec d autres solutions de haut niveau pour la programmation GPGPU. En particulier, nous avons comparé certains exemples avec des programmes équivalents écrits avec les outils suivants : Accelerate[92, 93] est une bibliothèque pour Haskell qui définit un langage pour décrire des opérations sur des tableaux d une dimension exécutables sur des dispositifs GPGPU. Accelerate propose aussi un ensemble de squelettes paramétrables pour la manipulation de structures régulières multidimensionnelles (map, zip, filter, etc.). Accelerate compile dynamiquement ces opérations vers Cuda, OpenCL ou la bibliothèque Repa[94] pour l exécution parallèle sur les architectures multicœurs. Comme SPOC, Accelerate transfère les données automatiquement entre les mémoires des dispositifs. Contrairement à SPOC, Accelerate ne permet pas d utiliser des noyaux externes et donc des bibliothèques de calcul optimisées existantes. Accelerate permet effectivement d écrire simplement des programmes exploitant les dispositifs GPGPU, mais il peut être difficile d exprimer certains types de calcul difficilement décomposables à l aide des squelettes fournis. Aparapi[95] est une bibliothèque pour Java développée à l origine par AMD. Elle permet de définir des noyaux de calcul en Java. Aparapi propose une classe Kernel qu il convient d étendre en surchargeant la méthode run pour définir le noyau de calcul. Celui-ci peut ensuite être exécuté directement sur un dispositif GPGPU. Aparapi compile alors le bytecode Java associé au dispositif sélectionné vers OpenCL. Le langage disponible pour l expression du corps de la méthode run est très proche des sous-ensembles du C proposés par Cuda et OpenCL, et donc très proche de Sarek. Comme SPOC, Aparapi transfère automatiquement les données entre dispositifs. Aparapi n offre pas de squelettes particulier et ne permet pas de transformer les noyaux depuis le code hôte Java, au contraire de Sarek qui donne accès à l AST des noyaux depuis le code hôte OCaml. RustGPU[96] est une modification du compilateur Rust pour cibler l écriture de noyaux de calcul. Rust[97] est un langage de haut niveau développé par la fondation Mozilla. Rust est compilé nativement via le compilateur LLVM et il existe un backend LLVM pour Cuda. En s appuyant sur celui-ci, RustGPU produit du code assembleur Cuda (PTX). RustGPU s appuie par ailleurs sur la bibliothèque Rust-OpenCL[98] pour la manipulation des données et l exécution des noyaux. Rust-OpenCL est un binding d OpenCL vers Rust. RustGPU manipule donc noyaux et données via OpenCL et exécute les calculs via la compilation du noyau Cuda PTX généré via LLVM. Ceci implique que bien que se basant sur OpenCL, RustGPU n est utilisable qu avec les dispositifs GPGPU compatibles à la fois avec Cuda et OpenCL, c est-à-dire les dispositifs du constructeur NVidia. Cependant en ciblant l assembleur PTX, RustGPU propose de décrire des structures complexes dans le code Rust qui sont compilées vers le noyau de calcul. RustGPU ne propose aucune abstraction sur la gestion des données ou des noyaux de calcul et ne propose pas de squelettes prédéfinis. Il n existe pas d assembleur OpenCL standard (celui-ci est dépendant de chaque architecture) et il serait donc difficile de proposer une implantation de RustGPU portable via OpenCL. Pour comparer ces langages et outils, nous avons utilisé deux exemples : l addition de vecteurs et

104 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML la multiplication de matrices. Ces deux exemples permettent de mesurer les performances de ces ou- calcul s tils pour des ratios t ailledesdonnes différents. Nous avons fait varier la taille des données et mesuré le temps d exécution moyen du calcul. SPOC, Accelerate et Aparapi gèrent automatiquement les transferts et maintiennent les données d entrée en mémoire GPGPU entre chaque itération. Nous avons donc mesuré ici le temps d exécution moyen du noyau de calcul associé au transfert des données de sortie. Nous avons utilisé un GPU GTX-680 avec le framework Cuda pour l exécution des noyaux. Temps de calcul (ms) 120 100 80 60 40 20 0 Addition de vecteurs SPOC (OCaml) Aparapi (Java) Accelerate (Haskell) 20 40 60 80 100 120 140 Taille des vecteurs (x10^6) FIGURE 3.18 : Mesure de performances : Comparaison - Addition de vecteurs La figure 3.18 présente les temps d exécution en fonction de la taille des vecteurs pour l addition de deux vecteurs. Il nous a été impossible d écrire un programme avec RustGPU qui permette de maintenir en mémoire les vecteurs d entrée pour cet exemple. De ce fait, les temps mesurés incluaient des transferts supplémentaires et donc des performances plus faibles. On constate par ailleurs que les trois autres outils se placent au même niveau de performance. Ceci est principalement dû au fait que tous compilent leurs noyaux dynamiquement vers du Cuda ou de l OpenCL et maintiennent en mémoire le noyau compilé. La mesure effectuée correspond donc à l exécution d un noyau GPUGPU Cuda ou OpenCL associé à un transfert. Les très faibles différences mesurées viennent donc des bibliothèques d exécution de chaque système et des opérations supplémentaires qu elles réalisent. Il est cependant intéressant de remarquer que nous n avons pas réussi à exécuter les tests manipulant de très grands jeux de données avec Accelerate. La figure 3.19 présente les temps d exécution en fonction de la taille N des matrices pour la multiplication de deux matrices N N. Nous ne proposons pas ici les résultats obtenus avec Accelerate, car il nous a été difficile de décrire efficacement la multiplication de matrices avec les squelettes fournis dans le langage. Comme pour l exemple précédent, on constate que les performances sont très proches entre les outils, pour les mêmes raisons. De même, on observe qu Aparapi et Rust-GPU refusent les données de très grande taille. Ceci peut s expliquer du fait que Aparapi et Rust-GPU s appuient sur OpenCL pour les transferts et la gestion mémoire sur les GPGPU tandis que SPOC s appuie ici sur Cuda qui permet d accéder à davantage de mémoire sur les GPGPU.

IV. TESTS DE PERFORMANCE 105 IV.4 Calcul multi-gpu SPOC permet d utiliser indifféremment tous les dispositifs GPGPU compatibles avec OpenCL ou Cuda présents sur le système à l exécution du programme. En particulier, cela permet d utiliser ces dispositifs conjointement pour accélérer les programmes. Nous présentons ici les résultats obtenus lors de l exécution de programmes modifiés pour exploiter différents dispositifs GPGPU en parallèle sur plusieurs systèmes. GPGPU InfoSystème Portable Bureau, haut de gamme GPU Pro CPU Multicœur GTX-560M GTX-460 GTX-680 Q2000 i7-2670qm i7-3770 NVidia NVidia NVidia NVidia Intel Intel # unités de calcul 192 336 1536 192 4 4 Mémoire associée (GB) 1,5 1 2 1 12 12 Bande passante (GBs) 60 96.2 192.2 41.6 21.3 25.6 GFLOPS (SP) 595.2 907.2 3090 480 140.8 224 Ordinateur 1 2 Ordinateur 2 2 1 Ordinateur 3 2 1 Ordinateur 4 1 1 Ordinateur 5 1 1 2 Ordinateur 6 1 1 2 1 TABLE 3.4 : Meusre de performances : Systèmes Multi-GPU utilisés Les ordinateurs utilisés pour ce test sont présentés dans la table 3.4. Les 2 premiers systèmes proposent des architectures portables donc moins performantes que les autres, mais plus économes en énergie. Les cartes graphiques NVidia seront utilisées avec Cuda et les CPU Intel avec OpenCL. Les cartes graphiques proposées ont des architectures différentes et sont associées en divers systèmes très hétérogènes. Le Système 6 est particulièrement hétérogène, car il associe quatre GPU dont trois FIGURE 3.19 : Mesure de performances : Comparaison - Multiplication de matrices

106 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML différents et un CPU multicœur, le tout manipulé avec deux frameworks : Cuda et l implantation d OpenCL d Intel pour les CPU multicœurs. Test Ordinateur OCaml séquentiel GPU Seul Tesla C2070 Ordinateur 1 2 GT X 560M Ordinateur 2 2 GT X 560M + 1 i7 2670QM Ordinateur 3 2 GT X 680 + 1 i7 3770 Ordinateur 4 1 GT X 680 + 1 GT X 460 Ordinateur 5 1 GT X 680 + 1 GT X 460+ 2 Quadr oq2000 Ordinateur 6 1 GT X 680 + 1 GT X 460+ 2 Quadr oq2000 + 1 i7 3770 MatMult temps (s) accélération 85 1 1.23 65.4 1.70 50 1.98 42.92 0.68 125 0.73 116.44 0.44 193.18 0.80 106.25 TABLE 3.5 : Mesure de performances : Multi-GPU - Temps (en s) et accélérations La table 3.5 présente les temps de calcul de la multiplication de matrices sur ces systèmes avec des noyaux de calcul externes. On constate que SPOC permet efficacement de tirer parti des différents dispositifs GPGPU sur le système. Par exemple, l ordinateur 3 qui associe 2 GTX680 et 1 CPU i7-3770 va plus de deux fois plus vite que la version exploitant uniquement 1 GTX680 présentée en figure 3.3 (qui réalisait le calcul en 1.7s pour une accélération de 50 contre ici 0.68s et une accélération de 125 par rapport au temps séquentiel). Par ailleurs, lors de grand déséquilibre entre les dispositifs, l ajout d un dispositif particulièrement lent peut ralentir l ensemble du système. C est par exemple le cas de l ordinateur 2 ou l ajout du CPU mobile par rapport au système 1 réduit les performances du système. Il faut par ailleurs ajouter que l ordonnancement des tâches est réalisé à l aide de threads OCaml qui ne peuvent s exécuter en parallèle. Seules les opérations de la bibliothèque SPOC (en particulier l exécution des noyaux de calcul) sont réalisées en parallèle. De plus, l utilisation du CPU est difficile à évaluer, car celui-ci est utilisé pour l ordonnancement des noyaux et des transferts sur les différents dispositifs et en parallèle pour le calcul via OpenCL. La figure 3.20 montre la distribution dynamique des tâches entre chaque dispositif GPGPU pour les 6 ordinateurs. Les ordinateurs 5 et 6 sont les plus hétérogènes. La figure 3.21 présente le rapport du nombre de tâches réalisées par chaque dispositif de ces ordinateurs avec ses performances théoriques. On constate ainsi qu au regard de ces performances, chaque dispositif réalise bien une quantité de tâches équivalente. Ceci montre que notre équilibrage est plutôt bon et donc que dans notre exemple les tâches sont bien réparties entre les dispositifs. On peut donc en conclure que SPOC et Sarek offrent une utilisation simplifiée de systèmes très hétérogènes. De plus, qu ils permettent de profiter de l ensemble des dispositifs d accélération des calculs de ce type de systèmes pour augmenter les performances.

V. CAS D UTILISATION : PORTAGE DU PROGRAMME PROP 107 80 Ordinateur 1 Ordinateur 2 % tâches réalisées 60 40 20 50 50 50 49 0.. GTX560M-A GTX560M-B. 1 GTX560M-A GTX560M-B i7-2670qm 80 Ordinateur 3 Ordinateur 4 67 % tâches réalisées 60 40 20 49 48 33 0. 3. GTX680-A GTX680-B i7-3770 GTX680 GTX460 80 Ordinateur 5 Ordinateur 6 60 52 51 % tâches réalisées 40 20 0. 25 12 11. 23 11 11 4 GTX680 GTX460 Q2000-A Q2000-B GTX680 GTX460 Q2000-A Q2000-B i7-3770 FIGURE 3.20 : Répartition des tâches sur systèmes multi-gpus V Cas d utilisation : portage du programme PROP Afin de vérifier sur un cas réaliste les bénéfices qu elle apporte en abstraction et en maintenabilité, nous avons ici expérimenté son utilisation dans le cadre du portage d une application de calcul physique exploitant déjà les GPU via l environnement Cuda[99]. Cette expérience nous a permis

108 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML 10 2 Ordinateur 5 10 2 Ordinateur 6 tâches réaliséesperformances théorique 3 2 1 0.. 1.7 10 2 GTX680 2.7 10 2 GTX460 2.5 10 2 Q2000-A 2.3 10 2 Q2000-B 2.5 10 2 2.3 10 2 2.3 10 2 1.6 10 2 1.8 10 2. GTX680 GTX460 Q2000-A Q2000-B i7-3770 FIGURE 3.21 : Répartition des tâches et puissance théorique sur systèmes multi-gpus d évaluer l utilisation d outils de haut niveau pour les GPU, du point de vue de l écriture d un programme haute performance réaliste. Ainsi, nous avons observé les gains en abstraction et en sûreté offerts au programmeur tout en vérifiant la conservation de l efficacité. Nous présenterons d abord le programme étudié et son implantation. Puis, nous détaillerons le portage et la description des bénéfices eux-mêmes liés au passage à un langage de haut niveau. Enfin, nous exposerons les résultats obtenus en matière de performances avant de conclure sur cette expérience et les perspectives qu elle offre. V.1 Présentation du programme étudié PROP. PROP est un programme de la suite 2DRMP [100] qui modélise la diffusion des électrons dans des atomes hydrogénoïdes et des ions à des énergies intermédiaires. L objectif principal de PROP est de propager une R-matrice [101, 102], R, dans un espace de configuration à deux électrons. La propagation est réalisée pour chaque énergie de diffusion. Celles-ci sont indépendantes les unes des autres. Chaque propagation est calculée par des équations dont les éléments principaux sont des multiplications de matrices, impliquant des sous-matrices de R. Au cours du programme, R est modifiée : en particulier, sa taille augmente au cours de la propagation. PROP se situe au cœur de la suite 2DRMP. Il prend en entrée des fichiers produits par un autre programme de la suite et produit d autres fichiers exploitables par le programme suivant. Ceci implique des étapes de lectureécriture et de préparation des données en plus de l étape de calcul. Le programme PROP réalise la résolution successive de quatre équations. On note la R-matrice globale d entrée R I et la R-matrice globale de sortie R O sous la forme : R I = ( R I I I R I X I R I I X R I X X ), R O = ( R O OO R O XO RO OX RO X X ) et les R-matrices locales sous la forme : ( ) ( ) R11 R 12 R13 R 14 r I I =, r IO = R 21 R 22 R 23 R 24

V. CAS D UTILISATION : PORTAGE DU PROGRAMME PROP 109 ( ) ( ) R31 R 32 R33 R 34 r OI =, r OO = R 41 R 42 R 43 R 44 Les quatre équations à résoudre du programme sont les suivantes : R O OO = r OO r OI (r I I +R I I I ) 1 r IO R O OX = r OI (r I I +R I I I ) 1 R I I X R O XO = RI X I (r I I +R I I I ) 1 r IO R O X X = RI X X RI X I (r I I +R I I I ) 1 R I I X Pour les résoudre, la version séquentielle du programme fait appel aux bibliothèques d algèbre linéaire BLAS[32, 103, 104] et LAPACK[33]. Parallélisation et exploitation GPGPU. La suite 2DRMP fonctionne sur des machines séquentielles, sur des clusters hautes performances ou encore sur des super calculateurs. 2DRMP a fait l objet d optimisations pour les architectures parallèles à mémoire partagée, mais aussi à mémoire distribuée[105]. PROP a, lui, par la suite, été modifié à plusieurs reprises pour profiter des dispositifs GPGPU. La section suivante présente la programmation GPGPU puis l implantation GPGPU du programme PROP. Implantation. PROP est un programme écrit en Fortran utilisant les bibliothèques BLAS et LA- PACK pour les calculs de propagation de la R-Matrice. CAPS-Entreprise a apporté une première modification à PROP dans le cadre d un appel d offres GENCI visant au portage d applications. PROP avait été retenu pour un portage avec le compilateur HMPP[106]. Lors de cette étude, l équation de propagation a été remaniée pour manipuler des matrices plus grandes. En effet, pour des produits de matrices, les performances GPGPU s accroissent avec la taille des matrices, diminuant la surcharge induite par le transfert des données. Les quatre équations présentées ci-dessus ont été remplacées par une unique équation : R O = ( R I I I R I X I R I I X R I X X ) = ( roo 0 0 R I X X ) + ( roi R I X I ) (r I I +R I I I ) 1 (r IO R I I X ) ) Pour résoudre cette équation, des matrices intermédiaires ont été utilisées : A = r I I +R I I I V = (r I O R I I X ) ) U = ( rio R I X I g lobalout putrmatr i x = ( ) roo 0 0 R I X X

110 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML Le programme utilise alors les solveurs dgetrf et dgetrs de la bibliothèque LAPACK pour résoudre l équation : V = A 1 V La fonction BLAS dgemm est utilisée pour effectuer la multiplication de matrices dans l équation : R 0 = g lobalout putrmatr i x +UV En utilisant le compilateur HMPP, CAPS-Entreprise a développé une version de PROP dans laquelle les produits de matrices s effectuent sur GPGPU (via Cuda), et non plus via la fonction dgemm de la bibliothèque BLAS[107]. Une seconde modification réalisée au Laboratoire d Informatique de Paris 6 (LIP6) a eu lieu pour limiter les transferts entre CPU et GPU en localisant l ensemble des calculs sur les dispositifs GPGPU [108]. Ces précédents travaux ont permis de développer une version du programme PROP effectuant l ensemble des calculs de propagation sur GPGPU. PROP effectue une série de calculs sur différentes sections de la R-matrice d entrée. Les données nécessaires au calcul sont lues dans des fichiers lors d une phase de préparation des données. La préparation a lieu pour chaque section à évaluer. L implantation étudiée utilise un système de double buffering pour permettre, pour chaque section, au calcul sur GPGPU de recouvrir les temps de lecture des données et de préparation du calcul de la section suivante. Elle utilise la bibliothèque Cublas[45] pour la multiplication de matrice via la fonction cublas_dgemm et la bibliothèque MAGMA[109] qui contient les fonctions LAPACK dgetrf et dgetrs optimisées pour les dispositifs GPGPU. L implantation GPGPU de PROP utilise une couche de code C pour faire la liaison entre le code Fortran et la bibliothèque Cuda. Le programme final associe 11400 lignes de Fortran avec 2800 lignes de C et 300 lignes de noyaux GPGPU Cuda. C est à partir de cette version que nous avons réalisé le portage vers OCaml avec SPOC et Sarek. V.2 Portage avec SPOC Démarche. Le programme PROP porté se divise en 3 grandes parties : la première, qui lit les données d entrée dans des fichiers, prépare les tâches à accomplir et les lance, la seconde qui effectue le traitement sur GPGPU et la troisième qui correspond effectivement aux noyaux de calcul Cuda exécutés. PROP utilise à la fois des noyaux de calcul simples écrits en Cuda pour l initialisation ou la copie de données, mais aussi des fonctions issues des bibliothèques Cublas et Magma afin d effectuer les calculs. Nous avons concentré notre travail sur le portage des calculs (et non des lecturesécritures) depuis les langages FortranC et Cuda vers OCaml et Cuda en utilisant la bibliothèque SPOC. Pour réaliser ce travail, nous avons d abord effectué un binding des bibliothèques Cublas (V1) et Magma vers OCaml en nous appuyant sur SPOC pour la gestion des structures de données et des transferts entre CPU et GPGPU. Le programme une fois porté comprend alors une part de Fortran (initialisation et entrées- sorties), une part de C (glue entre Fortran et OCaml), une part d OCaml (composition des calculs sur GPGPU avec SPOC) et une part de Cuda (noyaux de calcul).

V. CAS D UTILISATION : PORTAGE DU PROGRAMME PROP 111 Bénéfices du passage à un langage de haut niveau. L utilisation d un langage de haut niveau permet d accroître la productivité et de faciliter le développement en libérant le programmeur de la verbosité des langages de bas niveau. L utilisation de la bibliothèque SPOC a une incidence directe sur la façon d écrire le programme et en particulier de traiter les données à transférer sur les GPGPU. Les allocationslibérations et transferts de mémoire étant gérés automatiquement, une importante charge de travail, qui de plus est source de bogues, est déportée du programme final vers la bibliothèque. En pratique, cela permet de n avoir qu à créer des vecteurs et à les utiliser soit pour du calcul sur CPU soit avec des noyaux GPGPU. PROP nécessite du calcul double précision pour offrir des résultats satisfaisants, là aussi l utilisation d OCaml et de sa sûreté de typage nous assure de ne pas insérer des calculs en simple précision au sein du programme. Le portage réduit la taille du code de 31% des 3000+ lignes de code (hors commentaires) du calcul GPGPU, principalement par la suppression du code lié aux transferts. PROP ayant déjà fait l objet d importants efforts d optimisation et s appuyant principalement sur des bibliothèques de calcul optimisées, nous n avons pas particulièrement profité d OCaml comme langage de composition des calculs afin de simplifier l expression des algorithmes employés. Dans le cas de l écriture de nouveaux programmes, les fonctions de haut niveau et l aspect multiparadigme d OCaml permettront, néanmoins l expression simplifiée d algorithmes complexes. V.3 Résultats et performances Nous avons comparé notre implantation utilisant OCaml et SPOC avec des implantations précédentes de PROP. Pour cela nous avons utilisé un système Linux (3.5.4-1) Fedora 17 64-bits avec un processeur Intel(R) Core(TM) i7-3770 CPU à 3.40GHz (Quad-Core + Hyperthreading), avec 8Go de mémoire DDR3, associé à un GPU Nvidia Tesla C2070 (driver 304.37 et Cuda_5.0.24) avec 6Go de mémoire, en exploitant les bibliothèques Intel MKL 2013.0.079 et Magma 1.2.1 et les compilateurs gcc-4.5.3 et OCaml-4.00.0. Jeu de données Taille de la Taille de la Nombre d énergies R-matrice locale R-matrice globale finale de diffusion Petit 90x90 360x360 6 Moyen 90x90 360x360 64 Grand 383x383 7660x7660 6 TABLE 3.6 : Mesure de performances : PROP - Caractéristiques des jeux de données Afin de vérifier les performances obtenues avec SPOC et OCaml, nous avons comparé notre implantation GPGPU avec l implantation précédente en Fortran (sur CPU et GPGPU avec Cuda) sur différents jeux de données (décrits table 3.6). Temps d exécution. Les tables 3.7 et 3.8 présentent les temps obtenus pour les différents cas étudiés. La première figure montre les temps obtenus en utilisant SPOC et Sarek pour l expression des noyaux de calcul GPGPU pour les cas Petit et Moyen. La seconde figure compare les temps d exécution du programme PROP en utilisant OCaml avec Sarek ou avec des noyaux de calcul externes. Nous avons ici

112 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML Jeu de tests Fortran Temps d exécution (s) Temps OCaml Accélération Fortran Petit 1,49s 3,36s 0.44 Moyen 10,70s 26,58s 0.40 TABLE 3.7 : Mesure de performances : PROP - Cas Petit et Moyen Langage et machine Temps de calcul Accélération Fortran Fortran CPU 1 core 4271s (71m11s) 1.00 Fortran CPU 4 core 2178s (36m18s) 1.96 Fortran GPU 951s (15m51s) 4.49 OCaml GPU 1018s (16m58s) 4.20 OCaml (+ Sarek) GPU 1195s (19m55s) 3.57 TABLE 3.8 : Mesure de performances : PROP - Cas Grand comparé nos versions du programme utilisant en OCaml aux versions séquentielles et parallèles du programme Fortran sur CPU ainsi qu à la version GPGPU qui nous a servi de base de travail. Lors du développement de la version d origine du programme, l utilisation des GPGPU permettait d obtenir une accélération de 15 comparée à la version n utilisant qu un cœur du CPU. Elle est ici de, 4.49 car le CPU utilisé a évolué entre les tests (passant d un Intel Q8200 à un Core i7-3770) alors que le GPGPU est resté de même génération. On peut supposer qu en exploitant des GPGPU de la récente génération Kepler, les accélérations seront plus importantes. Notre implantation avec OCaml offre pour les cas Petit et Moyen des performances plus faibles que pour le cas Grand en comparaison avec la version d origine. De plus, ces performances diminuent en passant du cas Petit au cas Moyen. Ceci s explique principalement par le fait qu entre ces deux cas, seul le nombre d énergies varie. Pour chaque énergie, les mêmes calculs seront réalisés. Ceci indique que le coût du lancement des noyaux avec SPOC peut influer pour des quantités de calcul GPGPU faibles. Ceci est confirmé avec le cas Grand qui avec moins d énergies et d importantes quantités de calcul voit notre implantation atteindre 80% des performances du programme d origine avec Sarek et 93% en utilisant des noyaux de calcul externes. On constate alors que SPOC offre de très bonnes performances. Plus précisément, avec Sarek pour décrire les noyaux, notre version du programme atteint 80% des performances de la version GPGPU en Fortran. Si on utilise des noyaux externes un peu plus optimisés que nos noyaux compilés depuis Sarek, et qu on évite la phase de compilation dynamique, les performances de notre version OCaml atteignent alors 93% des performances du programme Fortran. Ces très bons résultats permettent de confirmer nos choix qui ciblaient à la fois l apport d abstractions, mais aussi la performance. En effet, notre version de PROP est plus courte, mais aussi plus sûre que la version d origine grâce à l utilisation d OCaml, de son gestionnaire de mémoire automatique et du typage statique. De plus, avec SPOC, le développeur est libéré de la contrainte qu impose la gestion manuelle des transferts. Le tout pour des performances très proches de celles de la version Fortran. Par

VI. CONCLUSION 113 ailleurs, le programme OCaml utilise SPOC et est donc déjà compatible pour sa plus grande partie vers OpenCL, il manque un binding vers des bibliothèques équivalentes à Cublas et Magma pour OpenCL, pour assurer une portabilité complète. Cette expérience montre que l utilisation d un langage de haut niveau, qui simplifie l exploitation des GPGPU et l expression du programme, permet d obtenir des performances proches de celles obtenues après de nombreux travaux d optimisation avec un langage de programmation de bas niveau. Occupation mémoire. Afin de mesurer l efficacité du gestionnaire mémoire d OCaml et de son utilisation par la bibliothèque SPOC, nous avons aussi étudié pour le cas Grand l occupation mémoire à la fois sur GPGPU et CPU au cours de l exécution du programme. La figure 3.22 présente, en haut, les résultats obtenus avec le programme d origine et, en bas, ceux du programme porté. On peut y voir que l occupation mémoire GPU varie entre 1Go et 1,8Go sur le programme d origine et entre 1Go et 2,2Go avec le programme OCaml. La consommation mémoire CPU est stable avec le programme d origine et augmente au cours de l exécution avec le programme OCaml pour atteindre 1Go. On constate que le GC d OCaml libère efficacement la mémoire sur le GPGPU, l amplitude d occupation mémoire étant similaire pour les deux versions du programme. Cependant, la mémoire CPU occupée est plus importante avec la version OCaml du programme qu avec la version Fortran. En effet, le programme originel utilise de nombreuses matrices temporaires pour les calculs. Cellesci ne sont pas utilisées pour le résultat final, et peuvent donc, être allouées uniquement sur le GPU où elles resteront le temps du calcul avant d être désallouées. SPOC ne permet que d exprimer des vecteurs, cest-à-dire des ensembles de données transférables. Ces vecteurs sont, par défaut, alloués en mémoire CPU. Une option (à préciser lors de la création d un vecteur) permet de l allouer directement en mémoire GPGPU (et donc d éviter un transfert coûteux et inutile dans le cas de vecteurs utilisés uniquement sur GPGPU). Cependant, SPOC réserve un espace en mémoire CPU pour tout vecteur créé afin de permettre son rapatriement automatique, lors d un accès par le CPU à ce vecteur, ou lors d une libération de la mémoire GPGPU par le GC d OCaml. Ceci implique que pour chaque matrice temporaire uniquement allouée en mémoire GPGPU dans le programme d origine, nous avons, avec SPOC, une allocation de mémoire supplémentaire côté CPU. Celle-ci pourra disparaitre via des abstractions permettant la composition de calculs, optimisant les transferts et placements de données automatiquement. Le chapitre suivant montre comment SPOC et Sarek permettent de construire des squelettes algorithmiques dédiés à la programmation GPGPU qui peuvent remplir ce rôle. VI Conclusion Dans ce chapitre, nous avons présenté l implantation de la bibliothèque SPOC et du langage Sarek. SPOC s appuie sur SPML, décrit au chapitre précédent pour permettre de manipuler les dispositifs GPGPU et les données qui leur sont associées avec le langage OCaml. En particulier, comme dans SPML, SPOC introduit un type d ensemble de données, les vecteurs qui sont automatiquement transférables entre la mémoire hôte et celles des dispositifs GPGPU. De plus, SPOC unifie les

114 CHAPITRE 3. PROGRAMMATION GPGPU : IMPLANTATION AVEC OCAML 2500 GPU Fortran CPU Fortran 2000 Occupation mˆ'moire (Mo) 1500 1000 500 0 0 100 200 300 400 500 600 700 800 Temps d execution (s) 2500 GPU OCaml CPU OCaml 2000 1500 1000 500 0 0 200 400 600 800 1000 1200 1400 Temps d execution (s) FIGURE 3.22 : Mesure de performances : PROP - Occupation mémoire cas Grand deux systèmes Cuda et OpenCL en une seule bibliothèque, ce qui permet d exploiter différents types d architectures indifféremment, mais aussi conjointement. Enfin, SPOC permet de manipuler des noyaux de calcul externes décrits avec les outils fournis avec Cuda ou OpenCL, mais aussi des noyaux de calcul décrits avec le langage Sarek. Sarek est implanté, avec Camlp4, sous la forme d un langage dédié, intégré à OCaml. Il apporte plus de cohérence grâce à sa syntaxe proche de celle d OCaml. Par ailleurs, il offre une vérification statique des types et permet ainsi d apporter une vérification supplémentaire, au plus tôt sur les noyaux de calcul. Enfin, Sarek simplifie la portabilité en générant automatiquement du Cuda aussi bien que de l OpenCL. À travers un ensemble de petits programmes, nous avons montré que SPOC permet d accroître les performances des programmes OCaml, qu on

VI. CONCLUSION 115 utilise un dispositif compatible Cuda, OpenCL ou une combinaison de divers dispositifs GPGPU. Enfin, à travers le portage d un programme hautes-performances réaliste depuis Fortran et Cuda vers OCaml et SPOC, nous avons montré que notre implantation offre de très bonnes performances tout en simplifiant la programmation et en apportant plus de sûreté.

CHAPITRE 4 Squelettes et composition «The key to performance is elegance...» J. Bentley & D. McIlroy Sommaire I Squelettes parallèles pour la programmation GPGPU................. 119 II Implantation avec des noyaux externes......................... 123 III Implantation avec Sarek.................................. 126 IV Exemple d utilisation.................................... 127 V Tests de performance.................................... 128 VI Conclusion.......................................... 130 117

118 CHAPITRE 4. SQUELETTES ET COMPOSITION Résumé Ce chapitre présente l implantation des squelettes algorithmiques MapReduce et des compositions PipePar pour la programmation GPGPU en s appuyant sur la bibliothèque SPOC et le langage Sarek introduits au chapitre précédent. Map et Reduce sont des squelettes qui permettent de réaliser un même calcul sur un ensemble de données uniformes, tandis que Pipe et Par permettent d ordonnancer différentes tâches. À travers ces squelettes classiques, ce chapitre décrit les capacités d extensions introduites à SPOC grâce à l utilisation du langage OCaml ainsi que les possibilités de transformations dynamiques des noyaux de calcul offertes par Sarek.Ce chapitre s appuie sur un exemple simple, l algorithme de la puissance itérée qui permet d approcher la plus grande valeur propre d une matrice, pour exposer l utilisation des squelettes. En fin de chapitre, cet exemple est utilisé pour réaliser des mesures de performances et vérifier l apport des nouvelles optimisations automatiques introduites par les squelettes de programmation MapReduce et la composition Pipe qui s appuient sur la bibliothèque SPOC.

I. SQUELETTES PARALLÈLES POUR LA PROGRAMMATION GPGPU 119 LA PROGRAMMATION GPGPU demande de manipuler des dispositifs spécialisés, hyper-parallèles (c est-à-dire avec plusieurs centaines voir plusieurs milliers d unités de calcul), avec leur modèle de programmation propre. Pour cela, elle implique d utiliser deux types de langages de programmation, l un pour la description du programme hôte, l autre pour la description du programme GPGPU lui-même. Dans le chapitre précédent, nous avons présenté l implantation de la bibliothèque SPOC pour manipuler les GPGPU et les données qu ils utilisent. Nous avons aussi présenté comment SPOC permet d utiliser des noyaux de calcul externes (écrits en Cuda ou OpenCL), ou décrits à l aide d un langage dédié intégré, Sarek. Sarek permet d apporter plus de cohérence dans l écriture des programmes, via une syntaxe proche de celle du programme hôte écrit en OCaml. Il permet aussi d offrir une vérification statique des types et donc d assurer plus de sûreté dans l exécution des programmes, dès la phase de compilation. SPOC de son côté permet de décrire des programmes manipulant des noyaux, en s abstrayant de nombreux aspects matériels. En particulier, cette bibliothèque unifie Cuda et OpenCL et permet de s affranchir des transferts mémoires via l utilisation de vecteurs automatiquement transférables. Ceci permet de simplifier la programmation GPGPU. Cependant, dans le cas de programmes complexes, il reste difficile de décrire des programmes portables et performants. Par exemple, la gestion efficace de la projection du noyau sur une grille reste complexe et dépend du matériel utilisé. En effet, en fonction de l architecture du GPGPU, de son nombre de multiprocesseurs et du nombre d unités de calcul de chacun d entre eux, il sera important d optimiser le nombre et l organisation des threads à lancer. D un autre côté, une optimisation classique de la programmation parallèle consiste à recouvrir les temps de transferts par du calcul. Comme nous l avons vu, les transferts sont une part importante des programmes GPGPU, ils peuvent considérablement ralentir l ensemble d un programme. En pratique, les GPGPU peuvent exécuter un noyau de calcul tout en réalisant des transferts. Il convient alors, lorsque c est possible, de réaliser les transferts nécessaires à l exécution d un noyau de calcul, au plus tôt, pendant l exécution d un précédent noyau de calcul. Ceci permet d exploiter au mieux les dispositifs GPGPU, en réduisant l attente entre chaque exécution d un noyau. Afin de généraliser ce type d optimisation et de simplifier certaines descriptions dépendantes du matériel (comme la description de la grille de threads), il paraît nécessaire d offrir des constructions automatiques, paramétrables. Dans ce chapitre, nous allons décrire l implantation, et l utilisation d une bibliothèque de squelettes algorithmiques, dédiés à la programmation GPGPU, basée sur SPOC. Dans la section I, nous décrirons les squelettes de parallélisme implantés. En section II, nous verrons comment nous avons implanté ces squelettes, avec des noyaux de calcul externes. Dans la section III, nous présenterons une implantation basée sur Sarek. À travers un exemple simple, nous décrirons en section IV l utilisation et les apports de ce type de bibliothèque pour la programmation GPGPU. Enfin nous utiliserons cet exemple en section V pour effectuer des tests de performance. I Squelettes parallèles pour la programmation GPGPU Squelettes. Pour simplifier la programmation, il est commun d utiliser des «recettes» connues. Celles-ci, souvent nommées Design Patterns (ou patron de conception en français) peuvent parfois être proposées sous la forme de constructions algorithmiques prédéfinies : des squelettes, qui cachent une

120 CHAPITRE 4. SQUELETTES ET COMPOSITION grande partie de la complexité des programmes parallèles. En utilisant certains paramètres, ils gèrent automatiquement les synchronisations ainsi que l ordonnancement des tâches, pour le programmeur. La programmation parallèle est un domaine complexe pour lequel de nombreux squelettes ont été proposés. Ils automatisent des algorithmes parallèles connus en cachant la complexité associée à la gestion efficace du parallélisme et aux synchronisations que la programmation parallèle impose. Ils sont souvent regroupés en bibliothèques qui proposent des fonctions de composition de squelettes permettant de décrire de nouveaux algorithmes, plus riches, à partir d un jeu de squelettes limité. De nombreux squelettes permettent de manipuler des ensembles de données pour leur appliquer un traitement particulier, c est par exemple le cas des squelettes Map, Reduce, Scan ou Filter. Map prend une opération et un vecteur en paramètres, et retourne un vecteur. Chaque élément du vecteur retourné est le résultat de l application de l opération à l élément correspondant du vecteur d entrée. Reduce prend aussi une opération et un vecteur comme paramètres. Cette fois-ci, il s agit d une opération binaire. Reduce renvoie un vecteur contenant une unique valeur. Cette valeur est calculée en combinant récursivement, les éléments du vecteur d entrée en utilisant l opération binaire pour réaliser la combinaison. Une réduction parallèle demande de nombreuses synchronisations, il est donc difficile de tirer pleinement parti de toutes les unités de calcul des dispositifs GPGPU pour ce type de calcul. Scan est très proche de Reduce. Comme lui il combine les éléments d un vecteur avec une opération binaire. La différence est qu il retourne un vecteur contenant les résultats issus des combinaisons successives. Filter est un squelette qui permet, à partir d un vecteur et d un prédicat de retourner le vecteur qui contient tous les éléments du vecteur précédent qui satisfont le prédicat. De la même manière, il existe des squelettes chargés d organiser l ordonnancement de différentes tâches, de composer différents squelettes entre eux. Ils peuvent servir à automatiser la parallélisation de tâches ou bien à optimiser le placement des calculs et données. On les appellera ici des compositions. Il existe par exemple les compositions Pipe, Par, Farm ou For. Pipe prend deux squelettes en paramètre et renvoit un squelette qui effectuera le calcul de chacun des deux squelettes passés en paramètre séquentiellement, en utilisant le résultat du premier comme entrée du second. Par prend deux squelettes et renvoi un squelette chargé de les exécuter en parallèle. Farm prend en paramètre un squelette est un ensemble d unités de calcul et projette ce squelette sur chacune des unités. For prend un squelette en paramètre et l exécute séquentiellement un nombre de fois déterminé. Bien sûr les compositions produisent des squelettes et sont donc normalement elles-mêmes composables. La programmation GPGPU est issue de la programmation parallèle et des bibliothèques de squelettes ont été développées pour elle. C est le cas de la bibliothèque Thrust[61] fournie avec Cuda qui offre des opérations prédéfinies du type MapReduce. On pourra aussi noter les bibliothèques

I. SQUELETTES PARALLÈLES POUR LA PROGRAMMATION GPGPU 121 SkelCL[63] et SkePU[62] qui fournissent des constructions similaires. La bibliothèque Accelerate[92] pour le langage Haskell offre, elle aussi, des squelettes de données pour la programmation GPGPU. Du côté des squelettes de tâches, une des principales bibliothèques dédiées à la programmation GPGPU est la bibliothèque C++ StarPU[65]. Celle-ci permet d ordonnancer automatiquement différents noyaux de calcul aussi bien sur des systèmes avec un seul GPGPU que sur des systèmes hétérogènes ou distribués. En particulier, elle permet de paramétrer chaque tâche par une évaluation de son coût et ainsi d automatiquement optimiser l équilibrage des tâches sur les différents dispositifs de calcul disponibles. De même [110] propose de compiler des squelettes algorithmiques décrits dans un langage de haut niveau vers des graphes de flot de données (Macro Data Flow (MDF) graph en anglais) qui peuvent être exécutés sur des systèmes hétérogènes (CPU-GPU) en automatisant l ordonnancement des calculs et transferts ainsi que le placement des données. OCaml est un langage de haut niveau qui permet de facilement proposer des constructions de haut niveau comme des squelettes. Il a été utilisé pour le développement de différentes bibliothèques de squelettes pour la programmation parallèle, en particulier la bibliothèque Sklml[111] et la bibliothèque CamlP3L[112] sur laquelle Sklml se base. Toutes deux modifient le compilateur OCaml pour offrir un ensemble de squelettes et compositions pour la programmation parallèle et distribuée. De même, la bibliothèque ParMap[113] permet de profiter des architectures parallèles avec le langage OCaml. D autres outils existent, comme la bibliothèque BSML[114, 115], pour OCaml qui permet, à partir d une représentation du système chargé d exécuter le calcul (basée sur le modèle BSP[116]), de prédire le coût d un calcul et le niveau de performance qu il atteindra et ainsi d en optimiser l ordonnancement et le placement sur différentes unités de calcul. BSML a de plus été utilisé pour proposer des bibliothèques de squelettes pour la programmation parallèle, et les vérifier formellement[117]. À l aide des propriétés de haut niveau d OCaml, nous proposons d utiliser SPOC pour construire des squelettes afin de simplifier la conception de logiciels et d offrir davantage d optimisations automatiques. Pour cela nous présentons deux squelettes classiques qui permettent de tirer parti des GPGPU tout en simplifiant la programmation, Map et Reduce. Ils pourront servir de base à la construction d autres squelettes. Ici l opération appliquée aux éléments du vecteur est remplacée par un noyau. Pour Map ce noyau doit manipuler des éléments de type a et retourner des éléments de types b (on le note alors ici ( a > b)ker nel). Ainsi, à partir d un vecteur d élément de type a, le squelette pourra projeter le noyau de calcul sur l ensemble du vecteur pour produire un vecteur d éléments de type b.. map : ( a > b) kernel > a vector > ( a > b) skeleton Pour Reduce l opération binaire est appliquée récursivement aux éléments du vecteur. Le noyau doit donc être de type ( a > a > a) pour, à partir de deux éléments de vecteur, produire un nouvel élément lui-même réutilisable par le noyau. Le résultat final sera donc lui aussi un élément unique de type a.. reduce : ( a > a > a) kernel > a vector > ( a > a) skeleton

122 CHAPITRE 4. SQUELETTES ET COMPOSITION Composition de squelettes. À partir de ces squelettes classiques, il est désormais possible de proposer de fonctions de compositions sur les squelettes. Nous proposons dans ce cadre deux compositions :Pipe et Par. Comme nous l avons dit précédemment, Pipe prend deux squelettes quelconques et utilise la sortie du premier comme entrée du second pour produire un nouveau squelette. Ainsi, à partir d un squelette de type ( a > b)skeleton et d un squelette de type ( b > v)skeleton il pourra utiliser la sortie du premier, de type b comme entrée du second pour produire un squelette de type ( a > c)skeleton. pipe : ( a > b)skeleton > ( b > c) skeleton > ( a > c) skeleton Le squelette Par tente d exécuter en parallèle deux squelettes. Les entrées et sorties du squelette généré sont donc une combinaison des entrées et sorties des deux squelettes utilisés.. par : ( a > b)skeleton > ( c > d) skeleton > ( a > c >( b * d) ) skeleton Optimisations automatiques. L utilisation de squelettes, et de compositions permet d exprimer simplement des calculs complexes. En effet, à travers les squelettes Map et Reduce, nous proposons d automatiser la description de la grille en fonction de la taille du vecteur et des propriétés du dispositif matériel chargé d exécuter le calcul. Pour simplifier, nous considérons que l utilisation d un squelette Map ou Reduce donne accès à l ensemble des unités de calcul du GPGPU pour le calcul du noyau du squelette. De même, nous proposons avec la composition Pipe d automatiser le recouvrement d une partie des transferts par du calcul. En effet, si les squelettes le permettent, nous pouvons automatiser le lancement des transferts des données nécessaires au calcul du second squelette, en parallèle de l exécution du premier. Par permet de mieux répartir l utilisation des unités de calcul d un même dispositif GPGPU en automatisant la description des grilles pour les deux squelettes à la fois. Par divise par deux le nombre d unités de calcul disponibles pour chaque squelette. Il est alors possible d imbriquer les compositions Par pour privilégier, ou non certains squelettes. Ne pouvant actuellement pas mesurer le coût d un noyau de calcul, en particulier dans le cas de l utilisation de noyaux externes, on pourra aussi permettre de paramétrer les squelettes par une évaluation du coût de calcul afin de mieux équilibrer la charge de chacun des squelettes. Dans la section suivante, nous présenterons comment, nous proposons d exprimer les squelettes parallèles avec SPOC en utilisant des noyaux externes[118]. Ensuite, nous verrons comment Sarek permet de les exprimer. Enfin, nous présenterons un exemple de programme utilisant des squelettes de programmation afin de discuter de leur utilité et des bénéfices qu ils apportent dans le cadre de la programmation GPGPU.

II. IMPLANTATION AVEC DES NOYAUX EXTERNES 123 II Implantation avec des noyaux externes Squelettes. Comme nous l avons décrit en section III.1, SPOC permet d utiliser des noyaux de calcul externes, écrits en Cuda C ou OpenCL C99. Ces noyaux prennent des paramètres qu ils modifient à travers des effets de bord. Pour permettre la composition, et profiter d une gestion automatisée des noyaux et des données, il est nécessaire de définir des entrées et sorties aux squelettes. Pour cela, nous proposons de décrire un squelette comme l association, d un noyau de calcul, d un environnement d exécution, qui correspond aux paramètres du noyau d une entrée, qui appartient à l environnement et d une sortie, qui appartient à l environnement. Les noyaux étant représentés dans SPOC par des objets OCaml (voir chapitre III.1.b), nous avons conservé ce modèle de programmation pour les squelettes. Chaque type de squelette est défini par un objet qui définit les fonctions d exécutions du squelette. Chaque squelette est sous-type de la classe plus générale, skeleton. Skeleton. class type [ a, b, c] skeleton = object constraint c = ( d, e) Vector.vector val env : a val ker : ( a, b) Kernel.spoc_kernel method env : unit > a method ker : unit > ( a, b) Kernel.spoc_kernel method par_run : Devices.device list > ( f, g) Vector.vector > c method run : Devices.device > ( f, g) Vector.vector > c end FIGURE 4.1 : Définition OCaml de la classe skeleton La figure 4.1 présente le type de la classe skeleton. Elle s appuie sur les noyaux de calcul externes et est définie par trois types. Les deux premiers correspondent au type des paramètres du noyau, qui représentent aussi l environnement d exécution du squelette. Comme pour les noyaux, les paramètres sont fournis dans deux formats définis par les types a et b, l un correspondant à la représentation OCaml du paramètre, l autre à sa représentation C, manipulable par Cuda ou OpenCL. Le troisième type, c, correspond à la valeur de retour du squelette, ici un vecteur. La valeur d entrée d un squelette n est pas fournie à la création, mais à l exécution, via les méthodes r un et par _r un. On décrira donc un squelette via un noyau de calcul, un vecteur qui correspondra à la valeur de sortie du squelette et un environnement d exécution. Lors du lancement du squelette, on lui passera en paramètre un vecteur d entrée. Par exemple, dans le cas de la description d un noyau qui incrémente tous les éléments d un vecteur, on pourra définir un squelette Map comme présenté dans la figure 4.2. Dans cet exemple, on utilise le noyau de calcul dans un squelette en définissant son environnement d exécution et sa valeur de sortie. On constate que contrairement à l utilisation classique des noyaux de calcul (décrite au chapitre précédent), il n est pas nécessaire de définir la grille de threads

124 CHAPITRE 4. SQUELETTES ET COMPOSITION Code OpenCL. kernel void vec_incr( global const float * a, global const float * b, int vector_size) { int nindex = get_global_id( 0) ; i f (nindex < vector_size) b[nindex] = a[nindex] + 1 ; } Code OCaml. kernel vec_incr : Spoc.Vector.int32 > Spoc.Vector.int32 > int > unit = file vec_incr l e t n = 10_000 l e t vec1 = Vector.create Vector.int32 n l e t vec2 = Vector.create Vector.int32 n... l e t res = l e t map_incr = new map vec_incr vec2 (vec1,vec2, n) in map_incr#run device vec1 FIGURE 4.2 : Définition OCaml d un squelette Map avec un noyau de calcul externe virtuelle qui va exécuter le noyau. En effet, à l exécution du squelette, en fonction des paramètres d entrée, et du dispositif chargé de réaliser le calcul, on va automatiquement définir cette grille pour maximiser le parallélisme.. val run : ( a, b, ( c, d) Vector.vector) skeleton > Devices.device > ( e, f) Vector.vector > ( c, d) Vector.vector val par_run : ( a, b, ( c, d) Vector.vector) skeleton > Devices.device list > ( e, f) Vector.vector > ( c, d) Vector.vector val map : ( a, ( b, c) Kernel.kernelArgs array) Kernel.spoc_kernel > ( b, c) Vector.vector > a > ( a, ( b, c) Kernel.kernelArgs array, ( b, c) Vector.vector) map val reduce : ( a, ( b, c) Kernel.kernelArgs array) Kernel.spoc_kernel > ( d, e) Vector.vector > a > ( a, ( b, c) Kernel.kernelArgs array, ( d, e) Vector.vector) reduce FIGURE 4.3 : Types OCaml : Fonctions de manipulation des squelettes Bien sûr, il est possible d utiliser des constructions fonctionnelles pour masquer le paradigme objet (voir figure 4.3). La fonction map permet de définir un objet map, les fonctions r un et par _r un exécutent les méthodes r un et par _r un du squelette passé en paramètre. Ceci permet d écrire l exemple précédent comme dans la figure 4.4. Les fonctions r un et par _r un sont équivalentes, elles exécutent le noyau de calcul et renvoient le résultat définit à la création du squelette. r un exécute simplement le noyau sur un dispositif GPGPU.

II. IMPLANTATION AVEC DES NOYAUX EXTERNES 125.... l e t res = run (map vec_incr vec2 (vec1,vec2, n) ) device vec1 FIGURE 4.4 : Exemple : utilisation de Map avec des constructions fonctionnelles par _r un tente de paralléliser l exécution du noyau en utilisant plusieurs dispositifs GPGPU. Son comportement dépend du type de squelette. Par exemple, dans le cas d un squelette Map, elle va découper le vecteur d entrée en n sous-parties et ainsi produire n sous parties du vecteur de sortie, chacune des parties étant utilisée par un calcul sur un dispositif différent. Ceci permet de facilement exploiter plusieurs dispositifs GPGPU. Cependant, aucune analyse n est réalisée sur le reste de l environnement d exécution du noyau qui est copié sur chaque dispositif. Si celui-ci est trop important, il pourra s avérer coûteux d utiliser par _r un. Composition. La composition de squelettes s organise de la même façon. Une composition correspond à une classe, sous-type de skeleton, avec ses fonctions d exécution propres. À chaque classe est par la suite associée une fonction (voir figure 4.5).. val pipe : ( a, ( b, c) Kernel.kernelArgs array, ( d, e) Vector.vector) skeleton > ( f, ( g, h) Kernel.kernelArgs array, ( i, j) Vector.vector) skeleton > ( a, ( b, c) Kernel.kernelArgs array, ( i, j) Vector.vector) pipe FIGURE 4.5 : Type OCaml : Fonction de compositions Le squelette Pipe permet d associer deux squelettes en utilisant la sortie du premier comme entrée du second. Les méthodes d exécution du squelette généré vont rechercher les vecteurs de l environnement d exécution du second squelette non présent dans l environnement d exécution du premier pour les transférer pendant le calcul du premier squelette. Ainsi, le temps de ces transferts sera recouvert par le calcul du premier squelette, améliorant les performances générales du programme. L utilisation de squelettes basés sur des noyaux de calcul externes limite nos possibilités, car on ne peut manipuler directement le noyau. En effet, le corps d un noyau de calcul est vu par OCaml comme la chaîne de caractères correspondant au code du noyau Cuda ou OpenCL. OCaml, comme les bibliothèques Cuda et OpenCL manipulent donc les noyaux comme des chaînes de caractères ce qui rend difficile l analyse et la transformation. Cependant, les squelettes aident déjà les programmeurs en permettant une manipulation en gérant automatiquement la définition de la grille de blocs de threads qui peut être difficile à définir en fonction du matériel utilisé. La composition de son côté permet d exprimer les relations entre noyaux, mais aussi d apporter des optimisations supplémentaires, comme le recouvrement de certains transferts par du calcul. Afin d apporter davantage de flexibilité et pour permettre d aller plus loin dans la définition des squelettes, nous présentons, dans la prochaine section comment nous proposons d utiliser Sarek

126 CHAPITRE 4. SQUELETTES ET COMPOSITION pour définir les squelettes. III Implantation avec Sarek Comme nous l avons présenté dans la section précédente, utiliser des noyaux de calcul externes pour construire des squelettes et les composer est rendu difficile par plusieurs propriétés des noyaux externes comme le fait qu ils ne fonctionnent qu à travers des effets de bord et qu ils ne peuvent pas facilement être modifiés (comme ils sont principalement issus de code source externe). En utilisant Sarek, nous pouvons résoudre ces problèmes. En effet, la compilation prend en partie place à l exécution, il est ainsi possible de transformer la représentation interne du noyau (le code Kir) pour associer un noyau Sarek à un squelette. D abord, pour permettre de manipuler des noyaux de calcul avec une valeur de retour, nous avons ajouté, lors de la phase d inférence des types du code Sarek, une évaluation du type de retour d un noyau. Il devient alors possible de générer un code complet associant du code hôte construisant un vecteur qui sera utilisé par le noyau de calcul pour contenir le résultat et qui sera renvoyé comme valeur de retour du noyau Sarek. Pour offrir les squelettes Map et Reduce, nous avons construit deux fonctions qui transforment le code Kir avant de le compiler en code CudaOpenCL, à l exécution. Par exemple, Map transforme du code, travaillant sur un élément unique en code réalisant du calcul sur un vecteur entier. Map transforme chaque accès au paramètre scalaire en un accès à un champ de vecteur. Le corps du noyau reste globalement inchangé, on utilise la mémoire locale au thread pour calculer l élément scalaire à retourner en le copiant, à la fin du calcul, dans le vecteur résultat.. l e t res = (map (kern a > a +1) device vec1) FIGURE 4.6 : Exemple : Squelettes avec Sarek val map : ( a > b) kirc_kernel > spoc_kernel > spoc_kernel * ( ( a, d) vector > ( b, c) vector) kirc_kernel FIGURE 4.7 : Type OCaml : transformation de noyaux Sarek avec map La figure 4.7 présente le type de la fonction de transformation Map. Elle prend en paramètre un noyau généré via Sarek, un spoc_kernel (qui correspond à un objet instancié depuis une classe générée par l extension de SPOC pour les noyaux de calcul externes), et un noyau Sarek (qui associe le code OCaml généré avec l AST Kir). Elle renvoie un nouveau couple de noyaux. Les calculs scalaires ( a > b) sont transformés en calculs vectoriels (( a, d)vector > ( b, c)vector ). Les types vectoriels (( a, d) et ( b, c)) sont définis en couplant le type OCaml des éléments du vecteur avec le type abstrait correspondant à leur type C (utilisé par les fonctions C des APIs CudaOpenCL). Comme pour les squelettes basés sur les noyaux externes, la grille de threads est générée automatiquement. Le code de la figure 4.6 présente un exemple d utilisation de squelettes avec Sarek.

. IV. EXEMPLE D UTILISATION 127. l e t res = (map (kern a > a +1) device vec1) Évaluation du type de retour Transformation scalaires -> vecteurs. (kern a b > l e t i = Std.global_thread_id in b.[ <i>] < a.[ <i>] + 1). Création du vecteur de sortie et exécution du noyau l e t vec_out = Vector.create return_type (Vector.length vec1) in run (kern a b > l e t i = Std.global_thread_id in b.[ <i>] < a.[ <i>] + 1) (vec1, vec_out) device; vec_out FIGURE 4.8 : Transformation de noyaux de calcul Sarek avec des squelettes La figure 4.8 présente les différentes étapes nécessaires pour générer le code vectoriel associé au code scalaire du noyau. Pour définir cette fonction, nous avons tout d abord ajouté l évaluation du type de retour d un noyau Sarek lors de la phase d inférence de type (statiquement). Ce type est alors propagé dans l AST Kir et permet de créer le vecteur résultat lors de la phase de compilation dynamique. La fonction map associe alors un calcul élémentaire à un thread, utilisant un élément de vecteur comme valeur d entrée et copiant le résultat dans le champ correspondant du vecteur résultat. Avec Sarek, il devient possible de modifier le code des noyaux exécutés sur les GPGPU. Plutôt que de manipuler une chaîne de caractères, il est possible d écrire des fonctions de transformation de l AST correspondant au noyau. Cet AST transformé génèrera un nouveau noyau lors de la phase de compilation dynamique décrite au chapitre précédent. Ceci permet d aller plus loin qu avec les noyaux externes et simplifie nettement l utilisation des squelettes en autorisant une manipulation plus proche des fonctions classiques (map etc) d OCaml. De plus, comme la transformation est dynamique, et s effectue au niveau du code OCaml hôte, il est désormais possible d intégrer de nouvelles constructions de haut niveau, basées sur Sarek, nécessitant uniquement d écrire une fonction OCaml transformant l AST Kir. IV Exemple d utilisation Dans cette section, nous présentons un exemple simple de programme qui profite des squelettes présentés dans les sections précédentes. Ainsi nous discuterons de leur utilisation, ainsi que des bénéfices qu ils apportent, en général. La figure 4.9 présente trois versions de ce programme. Il s agit de la boucle itérative d un algorithme de puissance itérée (qui calcule une valeur approchée de la plus grande valeur propre d une matrice donnée).

128 CHAPITRE 4. SQUELETTES ET COMPOSITION Cet algorithme est décrit par l itération b k+1 = A b k A b k À chaque itération, le vecteur b k est multiplié par la matrice A et normalisé. Dans cet exemple, nous utiliserons des squelettes basés sur des noyaux externes. Il s agit de trois noyaux de calcul très simples : kern_init qui calcule la multiplication matrice-vecteur kern_divide qui divise les éléments d un jeu de données par un scalaire kern_norm qui calcule la norme d un vecteur passé en paramètre Le code en figure (a) implante l algorithme de la puissance itérée en utilisant les trois noyaux de calcul avec SPOC pour gérer les noyaux et les transferts. On constate par ailleurs que ces trois noyaux de calcul peuvent être utilisés à travers des squelettes parallèles. La multiplication matrice-vecteur peut être considérée comme un map, de même pour la division vecteur-scalaire ou la norme. On peut aussi utiliser une réduction pour calculer le maximum d un vecteur. Le code de la figure (b) correspond à l utilisation de squelettes. Ceci permet de simplifier le code en supprimant la description explicite des blocs et grilles de threads utilisés par les noyaux. On constate alors que certaines valeurs calculées via un squelette sont utilisées en entrée du squelette suivant. On utilisera alors la composition Pipe pour explicitement décrire cette relation entre les squelettes et profiter du recouvrement automatique de transferts par du calcul. Le code correspondant à l utilisation de la composition de squelettes. La section suivante discute des gains de performance apportés par les optimisations automatiques offertes par les squelettes et leur composition. V Tests de performance Afin de vérifier les gains de performance apportés par l utilisation de squelettes parallèles et par leur composition, nous avons mesuré les temps d exécution des trois versions du programme présenté en section IV. La table 4.1 présente les résultats obtenus avec les trois versions du programme pour 10.000 itérations et les compare à une version séquentielle en OCaml. La première version (table 4.1.(a)) utilise SPOC avec des noyaux de calcul externes pour décrire l algorithme de calcul de la puissance itérée. La seconde version (table 4.1.(b)) remplace l utilisation directe de noyaux de calcul par l utilisation des squelettes Map et Reduce. La troisième version (table 4.1.(c)) utilise la composition Pipe quand c est possible pour composer les squelettes entre eux. Comme nous l avons expliqué, l utilisation des squelettes Map et Reduce permet d automatiser la représentation virtuelle de la grille de blocs de threads utilisée pour le calcul en s appuyant sur la taille du vecteur d entrée (qui correspond directement au nombre d opérations à réaliser pour le calcul) et sur les propriétés du dispositif GPGPU chargé de réaliser le calcul. En particulier, on cherche à maximiser le parallélisme pour profiter des unités de calcul présentes sur le dispositif GPGPU. La composition Pipe, permet de son côté d automatiser le recouvrement d une partie des transferts par du calcul. En effet, elle permet de lancer les transferts nécessaires au calcul du second squelette pendant le calcul du premier. On limite ainsi l impact des transferts sur le temps d exécution global de la composition. Les résultats montrent que l utilisation de squelettes et leur composition permettent d améliorer les performances des programmes. En effet, l utilisation de noyaux de calcul permet déjà d améliorer

V. TESTS DE PERFORMANCE 129 ( * *** Programme OCaml u t i l i s a n t la bibliothèque SPOC *** * ) open Kernel... l e t block = {blockx = 256 ; blocky = 1 ; blockz = 1 ; } and grid = {gridx = (Vector.length vn) 256 ; gridy = 1 ; gridz = 1 ; } while (! norm > eps &&!iter < max_iter) do incr iter; maximum.[ <0 >] < 0. ; run dev (block,grid) kern_init (vn, v, a, n) ; ( * Tools. f o l d _ l e f t max 0. v r e c u r s i v e l y computes the * ) ( * maximum of the vector v ( computes on CPU) * ) maximum.[ <0 >] < Tools.fold_left max 0. vn; run dev (block, grid) kern_divide (vn, maximum, n) ; run dev (block, grid) kern_norm (vn, v, v_norm, n) ; norm := Tools.fold_left max 0. v_norm; done ; (a) Puissance itérée ( * *** Modifications pour u t i l i s e r des s q u e l e t t e s *** * ) open Compose... while (! norm > eps &&!iter < max_iter) do incr iter; maximum.[ <0 >] < 0. ; vn := (run (map kern_init vn (vn, v, a, n ) ) dev vn) ; maximum := (run ( reduce spoc_max maximum (vn, maximum, n) ) dev vn) ; vn := (run (map kern_divide vn (vn, maximum, n) ) dev vn) ; v_norm := (run (map kern_norm v_norm (vn, v, v_norm, n) ) dev vn) ; max2 := (run ( reduce spoc_max max2 (v_norm, max2, n) ) dev v_norm ) ; norm := max2.[ <0 >] ; done ; (b) Puissance itérée avec squelettes ( * *** Modifications pour u t i l i s e r la composition de s q u e l e t t e s *** * ) open Compose... while (! norme > eps &&!iter < max_iter) do incr iter; max := (run ( pipe (map k_init vn (vn, v, a, n ) ) ( reduce spoc_max max (vn, max, n) ) ) dev vn) ; norm := (run ( pipe ( pipe (map k_divide vn (vn, max, n) ) (map k_norm v_norm (vn, v, v_norm, n) ) ) ( reduce spoc_max max2 (v_norm!, max2, n) ) ) dev vn ).[ <0 >] ; done ; (c) Puissance itérée avec composition de squelettes FIGURE 4.9 : Exemples : Puissance itérée les performances du programme en comparaison à une exécution séquentielle. L utilisation de squelettes améliore les performances, principalement car la réduction a lieu sur le GPU ce qui évite des transferts inutiles. Par ailleurs, bien qu elle améliore un peu les performances, la troisième version du programme ne bénéficie que peu de la composition Pipe. En effet, le recouvrement des transferts par le calcul ne concerne ici qu une seule valeur. De plus, l algorithme de réduction employé reste très simple et peu parallélisé et n améliore donc que peu les performances du programme.

130 CHAPITRE 4. SQUELETTES ET COMPOSITION (a) Résultats avec SPOC. SPOC GFLOPS (DP) Système Temps (s) Accélérations Dispositif Théoriques i7-3770 32 (1 core) OCaml (1 cœur) 637 1 Tesla C2070 515 Cuda 150 4.24 Radeon HD 6950 562.5 OpenCL 101 6.31 (b) Résultats avec squelettes. Squelettes Système Temps (s) Accélérations Dispositif OCaml SPOC (a) i7-3770 OCaml 637 - - Tesla C2070 Cuda 135 4.72 1.11 Radeon HD 6950 OpenCL 81 7.86 1.25 (c) Résultats avec composition de squelettes. Composition Systèmes Temps (s) Accélérations Dispositif OCaml SPOC (a) Squelettes (b) i7-3770 OCaml 637 - - - Tesla C2070 Cuda 133 4.79 1.13 1.02 Radeon HD 6950 OpenCL 74 8.61 1.36 1.09 TABLE 4.1 : Mesure de performances : Puissance itérée VI Conclusion Dans ce chapitre, nous avons présenté l utilisation de SPOC et Sarek pour offrir des abstractions supplémentaires pour la programmation GPGPU. Afin de permettre de décrire les relations entre les noyaux de calcul et les données qu ils exploitent, nous avons développé des squelettes de programmation. Ce sont des constructions algorithmiques qui automatisent la parallélisation et les synchronisations et qui permettent d exprimer simplement des calculs complexes. En particulier, nous avons présenté les squelettes Map et Reduce. Ces squelettes simplifient la description de calculs parallèles à effectuer sur des vecteurs. Afin de composer ces squelettes et ainsi de décrire les liens entre squelettes, nous avons présenté la composition Pipe. En plus de faciliter la description d algorithmes complexes, squelettes et composition nous permettent d offrir des optimisations supplémentaires automatiquement. Nous avons décrit l implantation de squelettes avec SPOC en utilisant des noyaux de calcul externes, mais aussi avec Sarek. Nous avons ainsi pu voir que Sarek permet de définir des squelettes qui modifient directement le code de noyaux de calcul à exécuter sur les GPGPU afin de permettre une utilisation plus naturelle pour le programmeur OCaml. Enfin, nous avons pu vérifier que l utilisation de squelettes parallèles permet effectivement d apporter des optimisations automatiques supplémentaires, et ainsi d accroître les performances des programmes.

CHAPITRE 5 Conclusion générale et perspectives «Computer, compute to the last digit the value of pi» Spock (Star Trek : The Original Series) Sommaire I Conclusion générale.................................... 133 II Perspectives......................................... 136 II.1 Rapprocher Sarek d OCaml............................ 137 II.2 Squelettes et extensions.............................. 137 II.3 Portage vers d autres langages.......................... 138 III Perspectives offertes par l évolution du matériel et des systèmes.......... 138 III.1 Évolution matérielle................................ 138 III.2 Cuda 5.5, OpenCL 2.0 : l avenir de la programmation GPGPU....... 140 IV Vers une programmation GPGPU unifiée........................ 141 131

132 CHAPITRE 5. CONCLUSION GÉNÉRALE ET PERSPECTIVES

I. CONCLUSION GÉNÉRALE 133 I Conclusion générale En introduction, nous avons décrit la programmation GPGPU et les principales difficultés qu elle représente. En particulier, elle demande une gestion manuelle et complexe des dispositifs GPGPU à travers l écriture de sous-programmes dédiés et la manipulation explicite des transferts mémoires entre CPU et GPGPU. De plus, les dispositifs GPGPU sont constitués de nombreuses unités de calculs et pour en obtenir le maximum de performances, il est nécessaire d écrire des programmes hautement parallèles. Nous avons ainsi pu identifier plusieurs problèmes de la programmation GPGPU auxquels nous souhaitions répondre dans cette thèse. Premièrement nous avons constaté un problème de portabilité, posé par l utilisation de deux systèmes incompatibles, Cuda et OpenCL, et d architectures matérielles parfois très différentes (CPU multicoeurs, GPU,... ) qui demandent des optimisations spécifiques. Nous voulions aussi apporter une solution au problème des transferts explicites qui alourdissent la tâche du programmeur, mais qu il est important d optimiser pour ne pas perdre de performances. Enfin, la programmation GPGPU étant complexe de par sa nature hybride et hyper parallèle, nous avons montré le besoin d apporter des abstractions de haut niveau pour la rendre plus accessible sans limiter les performances. En particulier, nous proposons dans les chapitres suivants une solution unifiée pour la description du programme hôte et des noyaux GPGPU, ainsi que des squelettes algorithmiques pour simplifier la description de programmes complexes. Dans le chapitre 2, nous avons présenté le modèle de programmation retenu pour notre approche à travers la description du langage hôte et du langage pour l expression des noyaux. Ces descriptions ont été proposées à travers la sémantique opérationnelle de ces langages. Ainsi, nous avons montré comment, grâce à sa bibliothèque d exécution, notre langage hôte offre des transferts automatiques entre la mémoire CPU et la mémoire des dispositifs GPGPU. Nous avons aussi présenté l évaluation d un noyau de calcul et le comportement de chaque thread qu il contient, à travers la relation entre threads et blocs de la grille qu il représente. Ceci a permis de souligner certains aspects du comportement des noyaux de calcul comme la synchronisation des threads et le besoin de limiter la divergence dans les programmes GPGPU. En effet, le paradigme du Stream Processing, imposé par la programmation GPGPU, demande de regrouper les threads par blocs, et le modèle SIMT utilisé pour l exécution des noyaux de calcul force tous les threads d un même bloc à exécuter la même instruction de manière synchronisée. Dans le cas d une alternative, le code à exécuter par les threads d un bloc peut diverger et introduire une attente entre les différents threads. Cette attente réduit, par définition, les performances du noyau. Enfin, nous avons présenté le système de types associé au langage de description des noyaux qui permet d offrir une vérification statique des noyaux et donc de détecter au plus tôt de nombreuses erreurs de programmation. Dans le chapitre 3, nous avons présenté notre implantation de ces deux langages à travers une bibliothèque pour le langage OCaml, SPOC (Stream Processing with OCaml), associée à un langage dédié intégré à OCaml via une extension du langage, Sarek. Nous avons donc décrit différents choix d implantation ainsi que différents tests de performance qui ont souligné les très bonnes performances apportées par nos solutions. En particulier, nous avons montré que notre solution est portable et permet d offrir un haut niveau de performances avec différents types de dispositifs GPGPU, qu il soit utilisé seul ou dans des systèmes multi-gpu. De plus, nous avons montré que même sur des systèmes très hétérogènes, c est-à-dire avec plusieurs dispositifs très différents et parfois même

134 CHAPITRE 5. CONCLUSION GÉNÉRALE ET PERSPECTIVES incluants des dispositifs compatibles avec Cuda et d autres avec OpenCL, notre implantation permet de profiter des performances de l ensemble du système. Enfin, nous avons présenté le portage d une application HPC reconnue écrite en Fortran et Cuda vers OCaml avec SPOC et Sarek. Cette expérience a montré que nos outils offrent un niveau de performance presque équivalent à celui qu on peut obtenir avec des outils de bas niveau en exploitant explicitement les ressources matérielles du système (en particulier, les transferts mémoires). Ces différents tests de performance ont montré que notre solution offre un très haut niveau de performance, associé à une simplification de la programmation GPGPU grâce à des transferts automatiques et la possibilité d utiliser un langage de haut niveau multiparadigme pour l écriture du programme hôte. Du côté des noyaux de calcul, Sarek offre principalement davantage de sûreté à travers sa vérification statique des types et davantage de portabilité grâce à sa compilation dynamique vers Cuda et OpenCL. Dans le chapitre 4, nous avons montré comment nos solutions peuvent servir de base à l élaboration d abstractions de plus haut niveau pour la programmation GPGPU. En particulier, nous avons présenté l implantation d une bibliothèque de squelettes algorithmiques pour la programmation GPGPU basée sur SPOC. Ces squelettes permettent d abstraire différents aspects de la programmation GPGPU et apportent des optimisations automatiques supplémentaires. À travers un exemple, nous avons évalué les performances de cette bibliothèque et ainsi vérifié que les différentes optimisations apportées permettent effectivement d accroître les performances tout en simplifiant la description de programmes complexes. Dans ce dernier chapitre, nous conclurons l approche présentée dans cette thèse en présentant les perspectives d évolution de notre solution et les domaines d application qu ils peuvent permettre de développer. Par ailleurs, nous discuterons des évolutions actuelles et à venir de la programmation GPGPU en général, aussi bien à travers l apparition de nouvelles architectures matérielles et l évolution des architectures actuelles qu à travers l évolution des systèmes Cuda et OpenCL. Enfin, nous verrons comment ces évolutions pourraient permettre le développement d une solution unifiant davantage la programmation GPGPU hôte et les noyaux de calcul. D autres domaines d applications pour SPOC et Sarek Au cours de cette thèse, nous avons décrit les langages SPML et Sarek et leur implantation avec le langage OCaml à travers la bibliothèque SPOC et un langage dédié intégré à OCaml. Ces outils ont principalement été présentés dans le cadre d une utilisation avec des cartes graphiques et pour la recherche des hautes performances. D autres domaines d applications peuvent profiter des solutions développées ici. Dans cette section, nous allons présenter comment les outils implantés peuvent permettre l accélération de programmes OCaml sur des systèmes multicœurs sans accélérateur de calcul particulier. De même, ils peuvent être intégrés à des outils existants pour la programmation d application web performante. Enfin nous présenterons comment le modèle de programmation présenté ici peut s adapter à d autres domaines d application comme celui du cloud computing. SPOC pour les architectures multicœurs. OCaml est un langage de programmation qui permet l expression de la concurrence, mais n autorise pas l exécution de threads en parallèle. Ainsi, il est difficile de profiter des différents cœurs

I. CONCLUSION GÉNÉRALE 135 des CPU modernes pour augmenter les performances des programmes. Pour pallier cette limitation, quelques solutions ont été proposées dans la littérature, comme le projet OCaml For Multicore[119] (OC4MC) qui a fourni une modification de la bibliothèque d exécution du langage et du gestionnaire de mémoire d OCaml pour autoriser l utilisation d une bibliothèque de threads parallèles la bibliothèque ParMap[113] qui offre des squelettes algorithmiques très spécifiques capables de répartir des calculs OCaml sur différents cœurs Dans ce cadre, SPOC et Sarek peuvent aussi présenter une solution alternative, car ils permettent via OpenCL de profiter des architectures multicœurs et peuvent ainsi accroître, simplement, les performances des programmes OCaml même dans des ordinateurs sans carte graphique dédiée au calcul. OCaml ParMap OC4MC SPOC + Sarek Power 11s14 3s30 - <1s Matmul 85s - 28s 6.2s TABLE 5.1 : Mesure de performances : Comparaison - architecture multicœur La table 5.1 présente des tests de performance réalisés sur un CPU multicœur, Intel Core i7-3770 à quatre cœurs, cadencés à 3.40GHz. Les deux programmes utilisés sont Power et Matmul présentés au chapitre 3. Ici est comparée dans le cas de Power une implantation séquentielle classique en OCaml avec une implantation utilisant ParMap pour profiter du parallélisme et une version exploitant SPOC et Sarek. ParMap permet de profiter d une parallélisation automatique des programmes exploitant les constructions (des squelettes) map ou fold d OCaml. Par ailleurs, OC4MC limite la taille des vecteurs allouables et ne permet pas d exécuter ce test. Pour Matmul nous avons comparé la version OCaml séquentielle avec une version compilée avec les outils proposés par OCaml For Multicore (OC4MC) et une version utilisant SPOC et Sarek. Il était, ici difficile de définir la multiplication de matrices en utilisant ParMap et sans introduire de copie- synchronisation supplémentaire. Du point de vue des performances, on constate que pour ce type de programmes, très dataparallel, les trois outils offrent de bonnes accélérations et permettent de profiter des architectures multicœurs. Cependant, SPOC et Sarek permettent d obtenir un niveau de performance bien meilleure. Ceci s explique principalement par l utilisation avec SPOC et Sarek du Stream Processing qui demande de définir les programmes tel qu ils soient facilement déployables sur les nombreuses unités de calculs d un GPU. Ici, les unités de calcul principales (les cœurs) sont moins nombreuses, mais le programme est facilement vectorisable, par les compilateurs inclus dans les drivers OpenCL. Ainsi, le compilateur d Intel utilisé ici, produit du code exploitant les différentes extensions SIMD de notre CPU et en particulier les extensions AVX. C est cette utilisation des extensions SIMD, offerte par la description singulière du calcul apporté par le Stream Processing qui permet d accroître encore les performances. Du point de vue de la programmation, chacun de ses outils est très différent. ParMap et SPOC sont plutôt dédiés à des applications data-parallel, tandis que OC4MC permet de décrire tout type de programme parallèle. SPOC et Sarek permettent aussi de décrire des programmes parallèles plus

136 CHAPITRE 5. CONCLUSION GÉNÉRALE ET PERSPECTIVES généraux, mais il sera alors plus difficile pour les compilateurs de générer du code vectoriel SIMD efficace. Par ailleurs, ParMap est une bibliothèque qui offre des fonctions très proches de celles existantes dans la bibliothèque standard d OCaml, son utilisation demande donc très peu de modifications de la part des programmeurs. De même, OC4MC est totalement compatible avec la bibliothèque de threads d OCaml. Ainsi elle permet, via une simple recompilation, de profiter du parallélisme avec des programmes existants. Cependant, OC4MC est toujours expérimental comme l a montré sa limitation de la taille de création de vecteurs. Bien sûr, SPOC et Sarek demandent une programmation particulière et donc de modifier les programmes existants, cependant, l introduction de squelettes proches de ceux existants dans la bibliothèque du langage pourra permettre une utilisation proche de celle offerte par un outil comme ParMAp tout en autorisant plus de flexibilité via l écriture manuelle de noyaux de calcul. Applications web et cloud computing Parallèlement au développement des GPU et des accélérateurs de calcul, le développement web a changé pour permettre d utiliser directement les ressources de la machine cliente. En particulier, avec l apparition des API WebGL et WebCL pour JavaScript, il est devenu possible d utiliser directement le GPU de la machine cliente depuis le navigateur, et donc avec une application web. WebCL permet la programmation GPGPU de façon similaire à OpenCL. Les outils que nous avons développés ici pourraient alors s adapter à un environnement web pour simplifier la programmation d applications accélérées par l utilisation des GPGPU. Les deux environnements principaux qui pourraient intégrer SPOC et Sarek sont la machine virtuelle OCaml écrite en JavaScript pour navigateur web : Obrowser[120], et le compilateur js_of_ocaml[121] qui permet de compiler du code OCaml vers du code JavaScript exécutable dans un navigateur. De la même manière, le cloud computing s appuie sur l utilisation de serveurs de calcul distants pour accélérer certains programmes. Une fois les sections de programme à accélérer sélectionnées, les programmeurs doivent écrire le programme distribué qui pourra réaliser cette opération les serveurs (le cloud). Évidemment, pour alimenter le cloud, il est nécessaire d opérer des transferts de données. Ce modèle peut s apparenter à celui utilisé pour la programmation GPGPU en remplaçant les dispositifs GPGPU par le cloud et les transferts via le bus PCI-express par des échanges entre le client et le cloud. Le programme exécuté par le cloud peut être considéré comme un noyau de calcul. Cette vision orientée accélération des calculs n est pas le seul mode d utilisation possible du cloud, cependant, pour ce type d utilisation, le modèle présenté dans cette thèse pour abstraire les transferts et simplifier la programmation pourra s adapter. De même, il sera possible de définir un langage dédié à l expression des calculs distribués sur le cloud. Bien sûr il est possible d utiliser la programmation GPGPU via le cloud dans le cas ou les serveurs du cloud possèdent des GPGPU. Ceci ajoute un niveau de parallélisme et pourra largement bénéficier de l introduction de squelettes algorithmiques dédiés. II Perspectives SPOC et Sarek permettent déjà de profiter des hautes performances des GPGPU avec le langage OCaml. Dans cette thèse, nous avons montré comment SPOC permet d automatiser la détection des

II. PERSPECTIVES 137 GPGPU et unifie la gestion de tout type de GPGPU. De plus, SPOC, en implantant le langage SPML offre des transferts mémoires transparents et automatiques tout en maintenant un haut niveau de performance. Le langage intégré Sarek permet de décrire les noyaux de calcul depuis le programme OCaml avec une syntaxe cohérente avec celle du langage hôte tout en offrant du typage statique et une extensibilité simplifiée. Enfin en s appuyant sur SPOC et Sarek, nous avons présenté, comment il est possible de définir de nouvelles extensions pour la programmation GPGPU à travers le développement de squelettes algorithmiques optimisés, dédiés à la programmation GPGPU. Les principales perspectives autour de ce travail s axent autour de trois points. D abord, le langage Sarek pourra être étendu pour y intégrer des traits du langage OCaml. Ensuite en s appuyant sur les capacités d extensions de Sarek, il sera intéressant de développer davantage de squelettes algorithmiques optimisés. Enfin, comme nous l avons fait pour OCaml, il sera possible d utiliser les spécifications des langages SPML et Sarek décrites au chapitre 2 pour développer des extensions GPGPU pour d autres langages de haut niveau. II.1 Rapprocher Sarek d OCaml. Comme nous l avons vu, Sarek est un langage avec une syntaxe à la ML, mais qui reste très proche des extensions du langage C offertes par les systèmes Cuda et OpenCL. Une perspective évidente sera d ajouter des traits de langage à Sarek pour le rendre plus proche d OCaml. En particulier, il sera important de permettre la définition de types et de fonctions utilisables dans les noyaux de calcul. Ceci pourra s implanter via inlining en faisant attention à la récursion. Pour rapprocher davantage Sarek, d OCaml, il sera intéressant d y permettre le filtrage par motif (pattern matching en anglais). Ceci permettra de manipuler des valeurs de types complexes avec simplicité. Cependant, le filtrage par motif risque d entraîner la génération de programmes avec de nombreux branchements qui peuvent très largement influencer les performances des noyaux de calcul. Sarek est actuellement un langage monomorphe, il sera intéressant de permettre la définition de noyau polymorphe. L implantation pourra s appuyer sur la compilation dynamique des noyaux en spécialisant un noyau polymorphe pour générer un noyau monomorphe en fonction du type des paramètres du noyau. II.2 Squelettes et extensions. Sarek permet d accéder directement à l AST du noyau de calcul depuis le programme hôte et ainsi de le modifier dynamiquement. Ainsi, il est possible de définir des constructions qui transforment les noyaux. Nous avons présenté ainsi comment définir quelques squelettes algorithmiques et compositions. Une perspective logique sera de profiter de SPOC et Sarek pour proposer une bibliothèque de squelettes algorithmiques complète dédiée à la programmation GPGPU avec OCaml. Comme nous l avons montré, il sera possible de définir des squelettes de données, capables d automatiser un traitement parallèle sur une grande quantité de données. Comme pour la composition Pipe, il sera possible d organiser les noyaux de calcul pour offrir davantage d optimisations. Pour aller plus loin, on pourra introduire à Sarek un modèle de coût capable d évaluer le coût d exécution d un noyau de calcul en fonction des paramètres du dispositif chargé de l exécuter. Grâce à ce modèle, il sera alors possible de définir des squelettes de tâches dédiées à la programmation hétérogène, capable de répartir données et calculs entre différents dispositifs GPGPU pour maximiser

138 CHAPITRE 5. CONCLUSION GÉNÉRALE ET PERSPECTIVES les performances. On pourra en particulier s inspirer du modèle BSP[116] et de son implantation dans la bibliothèque OCaml, BSML[115]. II.3 Portage vers d autres langages Dans cette thèse, nous avons choisi d utiliser le langage OCaml pour développer la bibliothèque SPOC et le langage dédié aux noyaux Sarek. Cependant, les travaux réalisés ici pourraient être adaptés vers d autres langages de programmation. En effet, le chapitre 2 de cette thèse décrit les propriétés des extensions GPGPU pour permettre d automatiser les transferts. Cette extension, que nous avons implantée sous la forme d une bibliothèque pour OCaml pourra être implantée avec un langage de programmation offrant des propriétés similaires. En particulier, il sera possible d utiliser tout langage, facilement extensible, permettant de s interfacer avec les bibliothèques Cuda ou OpenCL. Un langage possédant un gestionnaire de mémoire automatique avec ramasse-miettes permettra d offrir les mêmes abstraction et optimisations que nous avons pu implanter avec OCaml. Pour un portage de ce type, on peut évidemment penser aux langages proches d OCaml, comme F# mais des langages comme Java, C++ ou Objective-C possèdent aussi les propriétés nécessaires à une implantation efficace de SPML. De même, nous avons dans cette étude ciblé les deux systèmes Cuda et OpenCL, une extension de SPML pourra en cibler d autres. En particulier, on pourra intégrer l utilisation de shaders d OpenGL comme des noyaux de calcul et ainsi accroître la compatibilité de SPOC et Sarek à toute architecture compatible avec OpenGL. Ceci pourra permettre de cibler, par exemple, les smartphones et tablettes qui ne sont pas toujours compatibles avec Cuda ou OpenCL. III Perspectives offertes par l évolution du matériel et des systèmes Comme nous l avons décrit dans la section précédente, SPOC et Sarek pourront être étendus pour offrir davantage d abstractions pour la programmation GPGPU. Parallèlement, les architectures matérielles et les systèmes Cuda et OpenCL continuent d évoluer et ces évolutions pourront permettre d étendre encore davantage nos solutions. D abord grâce aux évolutions matérielles qui permettent désormais davantage de flexibilité dans l exécution des noyaux de calcul. Enfin grâce à l évolution des systèmes Cuda et OpenCL qui d un côté s adaptent aux évolutions matérielles et de l autre visent eux aussi à apporter d un côté davantage d abstractions pour simplifier leur utilisation et de l autre davantage de standardisation (en particulier du côté d OpenCL) pour simplifier le développement d applications portables et hétérogènes. III.1 Évolution matérielle Au cours de cette thèse, les architectures matérielles des GPU ont considérablement évolué. Par exemple, alors que peu de GPU grand public permettait le calcul double précision au départ, c est devenu une norme par la suite (même si les performances restent très en dessous de celles offertes lors de calcul simple précision). D autres évolutions ont eu lieu, comme lors de l introduction des cartes graphiques Kepler, et certaines architectures nouvelles sont apparues ou se sont généralisées comme les System On Chip (SOC) hybrides et certains accélérateurs de calculs.

III. PERSPECTIVES OFFERTES PAR L ÉVOLUTION DU MATÉRIEL ET DES SYSTÈMES 139 Évolution des GPU. Avec l architecture Kepler[122], NVidia a proposé un GPU toujours performant pour la partie graphique et le jeu vidéo, mais davantage tourné vers le calcul. En effet, cette architecture propose davantage d unité de calcul double précision, et donc des performances plus proches de celles obtenues en simple précision, mais aussi davantage d unités de branchement et la possibilité d y déployer des noyaux de calcul récursifs. En effet, avec Kepler, Nvidia a introduit la possibilité, pour un noyau de calcul d exécuter un autre noyau de calcul avec sa grille de blocs de threads propre. Il est ainsi devenu possible de développer des algorithmes récursifs qui demandaient jusqu alors une intervention du CPU entre chaque exécution et éventuellement le transfert de données pour informer le CPU de l état actuel du calcul. Bien que cette architecture soit encore peu répandue 1, ces multiples améliorations ont déjà permis d accélérer de nombreux programmes et algorithmes, en particulier les algorithmes de tri récursifs[123, 124, 125, 126]. SOC hybrides. Afin de permettre un dégagement de chaleur plus faible, et une introduction dans des systèmes miniaturisés (en particulier les smartphones), les constructeurs ont associé CPU et GPU sur une même puce, un SOC. Ces systèmes ont souvent une mémoire partagée par le CPU et le GPU. Bien que l espace d adressage soit souvent différent, il devient tout de même possible de partager les données sans transfert via une traduction des pointeurs ou une simple copie mémoire. De plus, comme ce sera le cas pour les futurs CPU Kaveri d AMD, l espace d adressage sera bientôt partagé par le CPU et le GPU pour permettre un partage total des données. Accélérateurs. Parallèlement au développement des GPU et à leur introduction directement au sein de la même puce que le CPU, des accélérateurs dédiés au calcul ont continué à être développés. C est en particulier le cas de l architecture MIC (Many Integrated Cores) d Intel qui équipe l accélérateur Xeon Phi. Ce système se présente sous la forme d une carte d extension, connectable au port PCI-Express d un ordinateur, comme un GPU dédié, mais cette fois-ci composé de nombreux cœurs x86. Ces coeurs «classiques», sont manipulables via les outils standards comme les bibliothèques de threads posix ou les bibliothèques parallèles OpenMP ou MPI. Ceci permet de définir des programmes parallèles complexes ou chaque unité de calcul peut réaliser les mêmes types d opérations que le CPU hôte, sans les restrictions habituelles des GPU. De plus, les outils de programmation étant communs entre le programme hôte et le programme dédié à l accélérateur, la programmation s en retrouve simplifée. Il devrait normalement être possible, par exemple, d utiliser n importe quel langage de programmation compatible x86 avec un Xeon Phi. De plus, cette architecture est fournie avec une implantation d OpenCL ce qui permet d y porter facilement des programmes GPGPU. L évolution des architectures matérielles tend à faire converger CPU et GPU vers des architectures hybrides avec un GPU de plus en plus indépendant et avec des transferts de moins en moins nécessaires. De même, les outils de programmation GPGPU, et en particulier OpenCL, semblent s installer comme outils standard pour les architectures hétérogènes avec accélérateurs de calculs. 1. septembre 2013

140 CHAPITRE 5. CONCLUSION GÉNÉRALE ET PERSPECTIVES III.2 Cuda 5.5, OpenCL 2.0 : l avenir de la programmation GPGPU Les travaux de cette thèse ont été principalement réalisés en s appuyant sur Cuda 3, Cuda 4 et OpenCL 1.1. Depuis, de nouvelles versions de ces systèmes ont été proposées et certaines sont en cours d élaboration. L évolution du matériel et de la demande des utilisateurs ont imposé le développement de nouvelles fonctionnalités. En s appuyant sur les prochaines versions de ces systèmes, on peut prévoir leur évolution. On s appuiera pour cela sur les nouveautés apportées par Cuda 4 puis Cuda 5 ainsi que sur celles apportées par OpenCL 1.2, mais aussi sur les fonctionnalités attendues dans Cuda 5.5 et OpenCL 2.0. Tout d abord, pour simplifier la programmation GPGPU, les deux systèmes proposeront une mémoire virtuelle unifiée qui permettra de ne plus effectuer explicitement les transferts. Elle permettra aussi de définir des structures complexes, comme des listes chaînées, qui contiennent des pointeurs et qui seront automatiquement manipulables par le CPU hôte ou les noyaux GPGPU. La mémoire unifiée sera aussi utilisable entre différents dispositifs GPGPU et son implantation permettra de ne plus avoir à réaliser un transfert intermédiaire vers la mémoire CPU. Une autre évolution concerne les noyaux qui seront désormais capables de lancer eux-mêmes d autres noyaux. Cette nouveauté introduite dans Cuda avec l architecture Kepler devrait intégrer OpenCL 2.0 et être compatible avec de nouvelles architectures. En particulier, il sera possible pour un noyau de définir un nombre de threads et leur organisation, qui peut différer de la sienne, pour lancer un nouveau noyau. Ceci permettra de ne plus avoir à passer par le CPU pour organiser certains algorithmes récursifs dont le parallélisme varie dynamiquement. Par exemple, une réduction simple pourra se faire directement depuis le GPU en diminuant le nombre d opérations restantes à effectuer, et donc le nombre de threads à lancer, à chaque étape de la réduction. Actuellement, chaque étape correspond à un lancement de noyau depuis le CPU, à l avenir ceci pourra être représenté par un appel récursif du noyau effectuant la récursion. Enfin, l utilisation, d un langage cible proche du C complexifie l implantation de compilateurs de langages de haut niveau vers des noyaux de calcul. Cuda offre depuis le départ le langage assembleur PTX[127] et sa spécification, mais du côté d OpenCL, chaque constructeur définit son propre langage assembleur. Afin de simplifier le développement d outils et compilateurs complexes, un langage intermédiaire est en cours de spécification pour OpenCL, SPIR[128]. Pour celui-ci, comme pour PTX, il existe (ou existera) un backend pour la suite d outils de compilation LLVM qui permet de simplifier la compilation de langage déjà basé sur LLVM. L ensemble de ces nouvelles propriétés montre d abord une volonté de simplifier la programmation GPGPU en réduisant le contrôle par le CPU et en simplifiant la gestion des données. Par ailleurs, cela simplifie aussi le développement d outils en simplifiant l aspect hybride de la programmation GPGPU d un côté et en donnant l accès à des fonctionnalités de très bas niveau (via les langages intermédiaires PTX et SPIR) pour construire des solutions plus complexes. De plus, à l instar d OpenMP, différents jeux de directives de compilations pour CC++ et Fortran sont développés spécifiquement pour la programmation GPGPU. On citera principalement le standard OpenACC[129, 130, 131] et les directives du compilateur HMPP[106] qui permettent de définir des portions de code à transformer en noyaux GPGPU ainsi que de spécifier l ordonnancement des transferts (pour par exemple, les réaliser au plus tôt dans l exécution du programme). Ces directives permettent de transformer des programmes séquentiels pour y introduire du calcul GPGPU sans mo-

IV. VERS UNE PROGRAMMATION GPGPU UNIFIÉE 141 difier le code source séquentiel. Ainsi, elles pourront simplifier le développement de bibliothèques portables, compatibles avec les architectures séquentielles, mais aussi avec les systèmes GPGPU. IV Vers une programmation GPGPU unifiée Les évolutions technologiques, matérielles et logicielles, ouvrent des perspectives supplémentaires sur l unification de la programmation GPGPU. Actuellement, la partie hôte et les noyaux de calcul ont des spécificités et donc des rôles bien distincts. L évolution matérielle offre aux calculateurs que sont les dispositifs GPGPU des fonctionnalités de contrôle de plus en plus avancées et les outils logiciels tendent à simplifier la liaison entre l hôte et ses invités. En s appuyant sur ces nouvelles propriétés, il semble possible de proposer des évolutions conséquentes de notre solution. En effet, l utilisation de mémoire unifiée simplifie les transferts automatiques proposés par SPOC. Par ailleurs, cette mémoire unifiée permet l accès à des structures complexes, définies dans le programme hôte, depuis les noyaux de calcul. Ceci permet d utiliser directement des éléments du langage hôte, et donc des éléments OCaml sans passer par une phase de traduction supplémentaire. Ainsi, les types de données manipulables avec SPOC ne se limiteront plus aux types réguliers et monomorphes que sont les vecteurs. Du côté de Sarek, comme nous l avons présenté dans la section précédente, nous souhaitons permettre de manipuler des types OCaml directement. Ceci sera donc simplifié si la représentation mémoire est la même du côté GPGPU que du côté hôte. Par ailleurs, la phase de compilation dynamique de Sarek cible actuellement les extensions du langage C pour Cuda et OpenCL. Il sera plus efficace d implanter les fonctionnalités de haut niveau du langage en ciblant les représentations intermédiaires proposées par les langages PTX et SPIR. Le développement d un langage pour les noyaux de calcul proche de celui du langage hôte et son utilisation à travers une bibliothèque de squelettes permet de masquer davantage l aspect hybride de la programmation GPGPU. Il devient alors envisageable d approcher un modèle de programmation unifié pour la programmation GPGPU, c est-à-dire un modèle de programmation ne différentiant plus le programme hôte et ses données, des noyaux de calcul.

ANNEXE A Sémantique opérationnelle de SPML La section I du chapitre 2 de cette thèse présente la sémantique opérationnelle de l extension GPGPU de SPML. Ici, nous présentons la sémantique opérationnelle du cœur fonctionnel et impératif du langage. I Grammaire du cœur de SPML SPML est un mini-ml dont la grammaire est présentée en figure A.1. Expressions expr = x Variable c Constante expr ; expr Séquence if cond then expr [else expr ]? Alternative for x = expr to expr do expr done Boucle For fun x > expr Abstraction let x = expr in expr Définition let rec x = fun x > expr in expr Définition récursive expr expr Opération Binaire ref expr Référence! expr Déréférencement expr = expr Modification d une référence expr expr Application Constante c = i nombre entier f nombre flottant false Booléen faux true Booléen vrai ( ) Unit Entiers i = [0 9]+ Flottants f = [0 9] +.[0 9] Ident x = [a z A Z 0 9_]+ Nom de variable FIGURE A.1 : Cœur de SPML 143

144 ANNEXE A. SÉMANTIQUE OPÉRATIONNELLE DE SPML II Notations On reprend ici les notations utilisées dans le chapitre 2. On pourra, à nouveau, s appuyer sur la feuille de notations disponible avec ce manuscrit et en copie page 165 pour aider à la lecture des règles. III Sémantique opérationnelle L évaluation d une variable rend sa valeur E(x). Var E,E x E(x) E,E L évaluation d une constante ne modifie pas les environnements d évaluation. Const E,E c c E,E L évaluation de l abstraction construit une fermeture contenant le corps de l abstraction ainsi que l environnement courant. Abstraction E,E fun x -> e x,e,e,e E,E L application entre deux expressions n est possible que si la première s évalue en une fermeture. L application peut entrainer des mutations de l environnement. E 0,E 0 e 1 x,e 3,E,E Application E 1,E 1 E 1,E 1 e 2 v 2 E 2,E 2 E 0,E 0 e 1 e 2 v E 3,E 3 (x, v 2 ) E 2,E 2 e 3 v E 3,E 3 La définition locale associe une valeur à une variable. Let-in E 0,E 0 e 1 v 1 E 1,E 1 (x, v 1 ) E 1,E 1 e 2 v 2 E 2,E 2 E 0,E 0 let x = e 1 in e 2 v 2 E 2,E 2 La définition récursive est limitée à la définition de fonctions. Rec-in (x 1, x 1,e 1,E ) E 0,E 0 e 2 v E 1,E 1 E 0,E 0 let rec x 1 = x 2 -> e 1 in e 2 v E 1,E 1

III. SÉMANTIQUE OPÉRATIONNELLE 145 La séquence évalue la première expression que le système de type assure d évaluer en ( ). Elle évalue ensuite la seconde expression et rend sa valeur. Seq E 0 e 1 ( ) E 1 E 1 e 2 v 2 E 0 e 1 ; e 2 v 2 E 2 La conditionnelle est composée de trois expressions la condition e 1, la conséquence e 2 et l alternance e 3. SPML autorise les conditionnelles sans alternance, elles sont considérées comme des conditionnelles donc la conséquence est (). Dans le cas où on évalue e 1 à true, on évaluera e 2. Dans le cas contraire, on évaluera e 3. If1 E 0 e 1 true E 1 E 1 e 2 v 2 E 0 if e 1 then e 2 else e 3 v 2 E 2 If2 E 0 e 1 false E 1 E 1 e 3 v 3 E 0 if e 1 then e 2 else e 3 v 3 E 2 La boucle bornée for x = e1 to e2 do e3 done est composée de trois composantes e1, e2 et e3. On évalue dans l ordre e1 en v1 puis e2 en v2. Ces évaluations peuvent modifier l ensemble des environnements d évaluation. Le résultat de la comparaison v1 > v2 permet de définir le comportement de la boucle. Si v1 > v2, alors on renvoie directement ( ). Sinon, on évalue le corps de la boucle, e3, qui peut lui aussi modifier les environnements d évaluation. Enfin, on réévalue la boucle en remplaçant e 1 et e 2 par les expressions associées aux valeurs évaluées : (v 1 + 1) et v 2. For1 E 0 e 1 v 1 E 1 E 1 e 2 v 2 E 2 v 1 v 2 (x, v 1 ) E 2 e 3 v 3 E 3 E 3 for x = (v 1 + 1) to v 2 do e 3 done ( ) E 0 for x = e 1 to e 2 do e 3 done ( ) E 4 Si on évalue v1 > v2 à true, on n évalue alors pas le corps de la boucle. For2 E 2 E 2 E 2 E 0 e 1 v 1 E 1 E 1 e 2 v 2 E 2 v 1 > v 2 E 0 for x = e 1 to e 2 do e 3 done ( ) E 2 E 4

146 ANNEXE A. SÉMANTIQUE OPÉRATIONNELLE DE SPML Les opérations binaires renvoient directement la valeur calculée a partir de l opération appliquée aux valeurs résultant de l évaluation des deux expressions associées à l opération. BinOp E 0 e 1 v 1 E 1 E 1 e 2 v 2 E 2 v 1 v 2 = v 3 E 0 e 1 e 2 v 3 E 2 Par souci de complétion, la sémantique opérationnelle des références présentée au chapitre 2 est ici recopiée. Les références sont des valeurs mutables. Une référence pointe sur un espace mémoire dans lequel est stockée la valeur référencée. Ainsi, à la création d une référence, un espace mémoire est alloué pour stocker la valeur à référencer et la référence prend pour valeur l adresse de ce nouvel espace. La mémoire est donc mise à jour pour contenir cette nouvelle liaison (adresse,valeur) nouvellement créée. Ben sûr l évaluation de l expression e peut modifier la mémoire du GPGPU et la file de commandes du GPGPU. Ref E 0 e v E 1 E 0 ref e @v (@v, v) M 1 h, M g 1,Q1 g Le déréférencement donne accès à la valeur référencée. On renvoie donc cette valeur, en utilisant l adresse stockée dans la référence. À nouveau, l évaluation de l expression e peut modifier l état de la mémoire hôte comme de la mémoire GPGPU et de la file de commandes du GPGPU. Bang E 0 e @v E 1 (@v, v) M 1 h E 0!e v E 1 La mise à jour d une référence remplace la liaison (adresse, valeur_initale) dans la mémoire par une liaison (adresse, nouvelle_valeur). De même qu auparavant, chaque évaluation d une expression peut entrainer des changements d état mémoire, aussi bien sur l hôte que sur un GPGPU. Update E 0 e 1 @v 1 E 1 E 1 e 2 v 2 E 2 M 2 h = (@v 1, v 1 ) M h E 0 e 1 := e 2 ( ) (@v 1, v 2 ) M h, Mg 2,Q2 g

ANNEXE B Comparaison : Cuda, OpenCL et SPOC Dans cette annexe, nous comparons les différentes API de programmation GPGPU utilisée dans cette thèse : l API Runtime de Cuda l API Driver de Cuda l API d OpenCL la bibliothèque SPOC Nous n entrerons pas dans les détails de chaque API et ne définirons pas explicitement chaque fonction utilisée. L objectif ici est, à travers cette comparaison, de présenter les gains en expressivité et en abstraction offerts par la bibliothèque SPOC. On ne traitera ici que les programmes hôtes, Sarek étant comparé avec les noyaux Cuda et OpenCL dans le chapitre 3 de cette thèse. Un programme GPGPU simple (qui n exécute qu un seul noyau de calcul) peut se diviser en plusieurs étapes : 1. l initialisation de la bibliothèque, du système qu on va utiliser 2. la détection des cartes graphiques du système 3. l initialisation des données 4. la préparation du noyau de calcul 5. l allocation d espace sur le dispositif GPGPU 6. le transfert des données depuis l hôte vers le GPGPU 7. l exécution du noyau de calcul 8. le transfert des valeurs de retour depuis le GPGPU vers l hôte 9. la vérification ou l utilisation des données par l hôte 147

148 ANNEXE B. COMPARAISON : CUDA, OPENCL ET SPOC I Initialisation Cuda Runtime automatique Cuda Driver OpenCL cuinit(0) ; automatique SPOC let devs = Devices.init () Cuda Runtime et OpenCL vont automatiquement s initialiser dès l appel d une fonction de la bibliothèque. Cuda Driver demande l appel à la fonction cuinit(0). Le paramètre de cuinit doit obligatoirement être 0. SPOC demande l appel à la fonction Devices.init () qui initialise l ensemble de systèmes disponible à l exécution et recherche l ensemble des dispositifs compatibles. Devices.init () renvoit un tableau contenant l ensemble des dispositifs trouvés sur le système. II Détection des dispositifs compatibles Cuda comme OpenCL demandent de manipuler les dispositifs GPGPU à travers des context. Il sera alors nécessaire de détecter chaque dispositif et d associer un context à chacun d entre eux. Avec Cuda Runtime, il suffit de connaître le nombre de dispositifs compatible. On pourra par la suite via CudaSetDevice(int dev_id) sélectionner un dispositif. Cuda Runtime ne permet que d avoir un dispositif actif par thread de l hôte à la fois. En contrepartie, comme nous n avons qu un GPGPU actif à la fois, il n est pas nécessaire de construire un context pour celui-ci. Cuda Driver permet de référencer tous les dispositifs via cudeviceget. Il est ainsi beaucoup plus simple de définir des programmes exploitant plusieurs GPGPU à la fois. OpenCL est très proche de Cuda Driver et permet lui aussi de référencer l ensemble des dispositifs du système. La principale différence vient de la notion de plateforme. Chaque distribution d OpenCL correspond à une plateforme. Par exemple, dans le cas d un système avec des GPU Nvidia et un processeur Intel. Si le système OpenCL de chaque constructeur est bien présent, alors le programme devrait détecter deux plateformes avec chacune ses dispositifs respectifs. Pour SPOC, tout a été fait automatiquement lors de l appel à Devices.init (). SPOC a déjà recherché tous les dispositifs compatibles avec Cuda et OpenCL sur le système.

II. DÉTECTION DES DISPOSITIFS COMPATIBLES 149 Cuda Runtime int cudadevicecount ; récupère le nombre de dispositifs compatible avec Cuda sur le système cudagetdevicecount (&cudadevicecount) ; Cuda Driver int cudevicecount ; récupère le nombre de dispositifs compatible avec Cuda sur le système cudevicegetcount (&cudevicecount) ; CUdevice cudevices[cudevicecount] ; CUcontext cucontexts[cudevicecount] ; place chaque dispositif dans le tableau cudevices for (int i = 0 ; i < cudevicecount ; i++){ } cudeviceget(&cudevices[i], i) ; associe un context a chaque GPGPU cuctxcreate(&cucontext[i], 0, cudevices[i]) ; cl_uint platformcount ; cl_platform_id * platforms ; cl_uint devicecount = 0 ; cl_device_id * devices ; cl_context * contexts ; int tmp ; récupérer toutes les plateformes clgetplatformids(0, NULL, &platformcount) ; platforms = (cl_platform_id * ) malloc(sizeof(cl_platform_id) * platformcount) ; clgetplatformids(platformcount, platforms, NULL) ; OpenCL for (i = 0 ; i < platformcount ; i++) { } récupérer tous les dispositifs clgetdeviceids(platforms[i], CL_DEVICE_TYPE_ALL, 0, NULL, &tmp) ; devicecount += tmp ; devices = (cl_device_id * ) malloc(sizeof(cl_device_id) * devicecount) ; contexts = (cl_context * ) malloc(sizeof(cl_context) * devicecount) ; int d = 0 ; for (i = 0 ; i < platformcount ; i++) { clgetdeviceids(platforms[i], CL_DEVICE_TYPE_ALL, 0, NULL, &tmp) ; for (j = 0 ; j < tmp ; j++ ){ clgetdeviceids(platforms[j], CL_DEVICE_TYPE_ALL, 1, &devices[d], NULL) ; on associe un context à chaque device contexts[d] = clcreatecontext(0, 1, &devices[d], NULL, NULL, NULL) ; d++ ; } } SPOC déjà fait par Devices.init()

150 ANNEXE B. COMPARAISON : CUDA, OPENCL ET SPOC III Initialisation des données Cuda Runtime Cuda Driver float* host_array = (float*)malloc(vector_size * (sizeof(float))) ; OpenCL let vec = Vector.create Vector.float32 vector_size SPOC ou let vec = Vector.of_bigarray_shr Vector.float32 bigarray L initialisation des données ne dépend pas du système utilisé, mais plutôt du langage employé. Avec Cuda et OpenCL, on pourra construire des tableaux C classiques qui seront stockés en mémoire hôte. Avec SPOC, il faudra utiliser des vecteurs. Ceux-ci seront automatiquement alloués en mémoire hôte. On utilisera pour cela la fonction create du module Vector. Afin de simplifier l utilisation de SPOC dans une base de code numérique existante, la bibliothèque propose aussi des fonctions de conversion de BigArray vers Vector. En effet, la bibliothèque BigArray est très utilisée, en OCaml, pour les programmes numériques, mais aussi, plus généralement pour sa facilité à s interfacer avec du code C ou Fortran. Cette fonction ne copie pas l ensemble du bigarray dans un vecteur, mais permet d associer celui-ci à un vecteur SPOC : Vector.of_bigarray_shr. Il faudra donc faire attention aux effets de bord qu elle peut entraîner. IV Préparation du noyau de calcul Les noyaux de calculs doivent être compilés avant d être exécutés. Cuda permet de précompiler les noyaux et de les lier au reste du programme dans la phase de compilation. Cependant, ceci demande de spécifier à l avance la cible (GPGPU) et ses spécificités. Pour plus de portabilité et pour comparer avec OpenCL et SPOC, nous présentons ici le cas de la compilation dynamique de noyaux de calcul.

IV. PRÉPARATION DU NOYAU DE CALCUL 151 Cuda Runtime impossible de compiler un noyau dynamiquement CUmodule module ; CUfunction * kernel = malloc(sizeof(cufunction)) ; jitoptions[0] = CU_JIT_INFO_LOG_BUFFER_SIZE_BYTES ; jitlogbuffersize = 1024 ; jitoptvals[0] = (void *)(size_t)jitlogbuffersize ; Cuda Driver jitoptions[1] = CU_JIT_INFO_LOG_BUFFER ; jitlogbuffer = malloc(sizeof(char)*jitlogbuffersize) ; jitoptvals[1] = jitlogbuffer ; jitoptions[2] = CU_JIT_MAX_REGISTERS ; jitoptvals[2] = (void *)(size_t)jitregcount ; jitoptions[3] = CU_JIT_TARGET_FROM_CUCONTEXT ; cumoduleloaddataex(&module, ptx_source, jitnumoptions, jitoptions, (void **) jitoptvals) ; cumodulegetfunction(kernel, module, functionn) ; OpenCL functionn = String_val(function_name) ; cl_source = String_val(moduleSrc) ; clcreateprogramwithsource(ctx, 1, (const char**)&cl_source, 0, &opencl_error) ; clbuildprogram(hprogram, 1, &device_id, 0, NULL, NULL) ; cl_kernel kernel = clcreatekernel(hprogram, functionn, NULL) ; SPOC automatique Cuda Runtime ne permet pas de compiler dynamiquement un noyau de calcul. Il faut pour cela utiliser Cuda Driver. Cuda Driver demande de préciser les options de compilation. Ici nous préparons un espace pour stocker les messages du compilateur JIT et nous laissons le compilateur sélectionner des optimisations compatibles avec notre dispositif. La fonction cumoduleloaddataex permet de charger le fichier contenant le noyau de calcul tandis que la fonction cumodulegetfunction récupère la fonction (compilée) correspondant au noyau de calcul à exécuter. Ici, functionn correspond au nom de la fonction (c est un char*) et ptx_source au code source du noyau (en assembleur PTX, sous la forme d un char*). Comme pour Cuda Driver, avec OpenCL, on commence par charger le code source va clcreate- ProgramWithSource, puis on compile le programme et la fonction correspondant au noyau dans ce programme. SPOC, de son côté, compilera automatiquement le noyau lors de sa première utilisation.

152 ANNEXE B. COMPARAISON : CUDA, OPENCL ET SPOC V Allocation sur le dispositif Cuda Runtime float* dev_array ; cudamalloc((void **)&dev_array, size) ; Cuda Driver float* dev_array ; cumemalloc(&dev_array, size) ; OpenCL SPOC cl_mem dev_array = clcreatebuffer(context, CL_MEM_READ_WRITE, sizeof( cl_float) * size, NULL, &cierr1) ; automatique Avant de pouvoir réaliser le transfert, Cuda et OpenCL demandent tous les deux d allouer l espace mémoire nécessaire sur le GPGPU. SPOC réalisera automatiquement cette tâche si un transfert est déclenché. VI Transfert de données depuis l hôte vers le GPGPU Il existe de multiples fonctions pour transférer les données, selon qu on veut un transfert synchrone ou asynchrone et avec ou sans options particulières. L objectif ici étant de montrer un aperçu des différentes API, nous présenterons uniquement les fonctions de transfert les plus simples, c està-dire synchrones et sans option. Cuda Runtime cudamemcpy(dev_array, host_array, size * sizeof(float), cudamemcpyhosttodevice) ; Cuda Driver cumemcpyhtod(dev_array, host_array, size * sizeof(float)) ; OpenCL SPOC clenqueuewritebuffer(clcommandqueue, dev_array, CL_TRUE, 0, sizeof(cl_float) * size, host_array, 0, NULL, NULL) ; automatique Les deux API Cuda proposent des fonctions similaires. Du côté d OpenCL, on constate que la fonction clenqueuewritebuffer possède quelques paramètres de plus. clcommandqueue est une file de commande, il est possible d en créer plusieurs pour ordonnancer transferts et exécution de noyaux de calcul. Les fonctions de transfert asynchrones de Cuda permettent là même chose en associant les transferts à des CUstream.

VII. EXÉCUTION DU NOYAU DE CALCUL 153 Du côté de SPOC, les transferts seront réalisés automatiquement lors de l utilisation d un vecteur sur un GPGPU (par un noyau de calcul). VII Exécution du noyau de calcul Cuda Runtime int threadsperblock = 256 ; int blockspergrid =(numelements + threadsperblock 1) threadsperblock ; vectoradd<<<blockspergrid, threadsperblock>>>(dev_array1, dev_array2, dev_array3, numelements) ; int threadsperblock = 256 ; int blockspergrid = (numelements+ threadsperblock 1) threadsperblock ; Cuda Driver void *args[] = { &dev_array1, &dev_array2, &ddev_array3, &numelements } ; Launch the CUDA kernel culaunchkernel(vecadd_kernel, blockspergrid, 1, 1, threadsperblock, 1, 1, 0, NULL, args, NULL) ; int localworksize = 256 ; int globalworksize = shrroundup((int)szlocalworksize, inumelements) ; rounded up to the nearest multiple of the LocalWorkSize OpenCL clsetkernelarg(kernel, 0, sizeof(cl_mem), (void*)&dev_array1) ; clsetkernelarg(kernel, 1, sizeof(cl_mem), (void*)&dev_array1) ; clsetkernelarg(kernel, 2, sizeof(cl_mem), (void*)&dev_array1) ; clsetkernelarg(kernel, 3, sizeof(cl_int), (void*)&numelements) ; Launch kernel clenqueuendrangekernel(clcommandqueue, kernel, 1, NULL, &globalworksize, & localworksize, 0, NULL, NULL) ; SPOC let threads_per_block = 256 in let blockspergrid = (numelements + threadsperblock 1) threadsperblock in let block = { Spoc.Kernel.blockX = threadsperblock ; Spoc.Kernel.blockY = 1 ; Spoc.Kernel.blockZ = 1 ;} in let grid = { Spoc.Kernel.gridX = blockspergrid ; Spoc.Kernel.gridY = 1 ; Spoc. Kernel.gridZ = 1 ;} in Kernel.run!dev (block, grid) vec_add (dev_array1, dev_array2, dev_array3, numelements) ; Lest quatre versions sont similaires. Il faut d abord préciser la configuration de la grille de blocs de threads qui va exécuter le noyau. On notera cependant que Cuda Runtime offre une syntaxe courte pour l exécution d un noyau qui permet entre autres de ne pas préciser, via un appel de fonction, chaque paramètre du noyau. SPOC, réaliser automatiquement l association de chaque vecteur avec

154 ANNEXE B. COMPARAISON : CUDA, OPENCL ET SPOC le noyau. C est à ce moment-là que SPOC déclenchera, si besoin, des transferts pour assurer la présence des données nécessaires à l exécution du noyau en mémoire GPGPU. VIII Transfert des valeurs de retour vers l hôte Cuda Runtime cudamemcpy(host_array, dev_array, size * sizeof(float), cudamemcpydevicetohost) ; Cuda Driver cumemcpydtoh(host_array, dev_array, size * sizeof(float)) ; OpenCL SPOC clenqueuereadbuffer(clcommandqueue, host_array, CL_TRUE, 0, sizeof(cl_float) * size, dev_array, 0, NULL, NULL) ; automatique Une fois le calcul réalisé, il est possible de rapatrier les données en mémoire CPU. Tout se passe comme pour le transfert vers le GPGPU, en utilisant les fonctionsparamètres inverses. IX Utilisation des résultats par l hôte Il est maintenant possible d utiliser les données normalement depuis le CPU. Celui-ci accèdera aux données dans l espace alloué à l étape 3. Dans le cas de SPOC les données sont toujours présentes uniquement en mémoire GPGPU. C est lors d un accès à un vecteur par l hôte qu un transfert se déclenchera pour rapatrier ce vecteur. Il est désormais possible de libérer les données en mémoire GPGPU, sauf pour SPOC qui réalisera la libération automatiquement.

Bibliographie [1] CHENG, HARRY H : Ch : A CC++ Interpreter for Script Computing. CC++ Users Journal, 24(1) :6, 2006. [2] BELLARD, FABRICE : Tiny C Compiler, 2008. http:tinycc.org. [3] Manuel SERRANO et Pierre WEIS : Bigloo : A Portable and Optimizing Compiler for Strict Functional Languages. In Static Analysis, pages 366 381. Springer, 1995. [4] Stefan BEHNEL, Robert BRADSHAW, Craig CITRO, Lisandro DALCIN, Dag Sverre SELJEBOTN et Kurt SMITH : Cython : The Best of Both Worlds. IEE Computing in Science & Engineering, 13(2) :31 39, 2011. [5] Rajib NATH, Stanimire TOMOV et Jack DONGARRA : An Improved Magma Gemm For Fermi Graphics Processing Units. International Journal of High Performance Computing Applications, 24(4) :511 515, novembre 2010. [6] Guangming TAN, Linchuan LI, Sean TRIECHLE, Everett PHILLIPS, Yungang BAO et Ninghui SUN : Fast Implementation of DGEMM on Fermi GPU. In Proceedings of 2011 International Conference for High Performance Computing, Networking, Storage and Analysis (SC). ACM, 2011. [7] Jakub KURZAK, Stanimire TOMOV et Jack DONGARRA : Autotuning GEMM Kernels for the Fermi GPU. IEEE Transactions Parallel and Distriuted Systems, 23(11) :2045 2057, 2012. [8] Marc SNIR, Steve W OTTO, David W WALKER, Jack DONGARRA et Steven HUSS-LEDERMAN : MPI : The Complete Reference. MIT press, 1995. [9] Leonardo DAGUM et Ramesh MENON : OpenMP : An Industry Standard API for Shared-Memory Programming. Computational Science & Engineering, IEEE, 5(1) :46 55, 1998. [10] Murray COLE : Algorithmic Skeletons : Structured Management of Parallel Computation. Pitman, 1989. [11] Murray COLE : Bringing Skeletons out of the Closet : A Pragmatic Manifesto for Skeletal Parallel Programming. Parallel computing, 30(3) :389 406, 2004. [12] Susanna PELAGATTI : Structured Development of Parallel Programs. Taylor & Francis Abington, 1998. 155

156 BIBLIOGRAPHIE [13] Fethi A RABHI et Sergej GORLAČ : Patterns and Skeletons for Parallel and Distributed Computing. Springer, 2003. [14] Herbert KUCHEN et Murray COLE : The Integration of Task and Data Parallel Skeletons. Parallel Processing Letters, 12(02) :141 155, 2002. [15] Pixar Animation STUDIOS : RenderMan Interface Specification, Version 3.0, 1988. http: renderman.pixar.com. [16] Michael ONEPPO : HLSL Shader Model 4.0. In Proceedings of the Special Interest Group in Graphics (SIGGRAPH), pages 112 152. ACM, 2007. [17] Randi Kessenich John ROST et Dava BALDWIN : The Open GL Shading Language, 2012. http: www.opengl.orgdocumentationglsl. [18] William R. MARK, R. Steven GLANVILLE, Kurt AKELEY et Mark J. KILGARD : Cg : A System for Programming Graphics Hardware in a C-like Language. ACM Transactions on Graphics, 22(3) : 896 907, 2003. [19] Ian BUCK, Tim FOLEY, Daniel Reiter HORN, Jeremy SUGERMAN, Kayvon FATAHALIAN, Mike HOUSTON et Pat HANRAHAN : Brook for GPUs : Stream Computing on Graphics Hardware. ACM Transactions on Graphics, 23(3) :777 786, 2004. [20] Justin HENSLEY : Close to the Metal. In Proceedings of the Special Interest Group in Graphics (SIGGRAPH), pages 120 130, 2007. [21] AMD INC : AMD Stream SDK, 2008. http:developer.amd.com tools-and-sdksheterogeneous-computing. [22] NVIDIA : Cuda C Programming guide, 2012. [23] Khronos OpenCL Working GROUP : OpenCL 1.2 specifications, 2012. [24] Chas BOYD : The DirectX 11 compute shader. In Proceedings of the Special Interest Group in Graphics (SIGGRAPH), 2008. [25] Gordon E MOORE : Progress in Digital Integrated Electronics. In Electron Devices Meeting (IEDM), International, volume 21, pages 11 13. IEEE, 1975. [26] Michael UPTON : The Intel Pentium 4 Processor, 2000. [27] Freescale SEMICONDUCTOR : AltiVec Technology Programming Interface Manual, 1999. [28] Nadeem FIRASTA, Mark BUXTON, Paula JINBO, Kaveh NASRI et Shihjong KUO : Intel AVX : New Frontiers in Performance Improvements and Energy Efficiency. Intel white paper, 2008. [29] SCHNETTER ERIK : Vecmathlib, 2013. https:bitbucket.orgeschnett vecmathlib. [30] Joël FALCOU et Jocelyn SÉROT : CamlG4 : une bibliothèque de calcul parallèle pour Objective Caml. In Journées Francophones des Langages Applicatifs (JFLA), pages 139 152, 2003. [31] Aart JC BIK : The Software Vectorization Handbook. Intel Press, 2004.

BIBLIOGRAPHIE 157 [32] Chuck L LAWSON, Richard J. HANSON, David R KINCAID et Fred T. KROGH : Basic Linear Zlgebra Subprograms for Fortran Usage. ACM Transactions on Mathematical Software (TOMS), 5(3) : 308 323, 1979. [33] Edward ANDERSON, Zhaojun BAI, Christian Heinrich BISCHOF, Laura Susan BLACKFORD, James Weldon DEMMEL, Jack DONGARRA, Jeremy DU CROZ, Anne GREENBAUM, Sven HAM- MARLING, Alan MCKENNEY et Danny SORENSEN : LAPACK Users Guide. Society for Industrial and Applied Mathematics, troisième édition, 1999. [34] Dac PHAM, Shigehiro ASANO, Mark BOLLIGER, Michael N DAY, H Peter HOFSTEE, C JOHNS, J KAHLE, Atsushi KAMEYAMA, John KEATY, Yoshio MASUBUCHI et al. : The Design and Implementation of a First-Generation CELL Processor. In International Solid-State Circuits Conference (ISSCC), pages 184 592. IEEE, 2005. [35] Kevin J. BARKER, Kei DAVIS, Adolfy HOISIE, Darren J. KERBYSON, Mike LANG, Scott PAKIN et Jose C. SANCHO : Entering the Petaflop Era : The Architecture and Performance of Roadrunner. In Proceedings of the 2008 ACMIEEE conference on Supercomputing (SC), pages 1 :1 1 :11. IEEE Press, 2008. [36] Ian KUON, Russell TESSIER et Jonathan ROSE : FPGA Architecture. Now Publishers Inc., 2008. [37] Arthur S BLAND, Ricky A KENDALL, Douglas B KOTHE, James H ROGERS et Galen M SHIPMAN : Jaguar : The World s Most Powerful Computer. Memory (TB), 300(62) :362, 2009. [38] Jack J DONGARRA, Hans W MEUER et Erich STROHMAIER : TOP500 Supercomputer Sites, 2012. http:www.top500.orglist201211. [39] Tarik SAIDANI, Joel FALCOU, Claude TADONKI, Lionel LACASSAGNE et Daniel ETIEMBLE : Algorithmic skeletons within an embedded domain specific language for the cell processor. In Parallel Architectures and Compilation Techniques, 2009. PACT 09. 18th International Conference on, pages 67 76. IEEE, 2009. [40] Alfredo BUTTARI, Piotr LUSZCZEK, Jakub KURZAK, Jack DONGARRA et George BOSILCA : A Rough Guide to Scientific Computing on the PlayStation 3. Innovative Computing Laboratory, University of Tennessee, Knoxville, Tech. Rep. UT-CS-07-595, 2007. [41] Donald E THOMAS et Philip R MOORBY : The Verilog Hardware Description Language, volume 2. Springer, 2002. [42] Peter J ASHENDEN : The Designer s Guide to VHDL, volume 3. Morgan Kaufmann, 2010. [43] Stan LIAO, Grant MARTIN, Stuart SWAN et Thorsten GRÖTKER : System Design with SystemC. Kluwer Academic Pub, 2002. [44] Jocelyn SÉROT, François BERRY et Sameer AHMED : Implementing Stream-Processing Applications on FPGAs : A DSL-Based Approach. In Proceedings of the 21st International Conference on Field Programmable Logic and Applications (FPL), pages 130 137. IEEE, 2011. [45] NVIDIA : Cublas Library, 2012. http:developer.nvidia.comcublas. [46] John R. HUMPHREY, Danniel K. PRICE, Kyle E. SPAGNOLI, Aaron L. PAOLINI et Eric J. KELMELIS : CULA : Hybrid GPU Accelerated Linear Algebra Routines. In Society of Photo-Optical Instrumentation Engineers (SPIE) Conference Series, volume 7705, page 1, 2010.

158 BIBLIOGRAPHIE [47] Stanimire TOMOV, Rajib NATH, Peng DU et Jack DONGARRA : MAGMA Users Guide. Innovative Computing Laboratory, University of Tennessee, Knoxville, 2011. [48] AMD INC : Bolt C++ Template Library, 2013. http:developer.amd.comtools. [49] NVIDIA : CUDA-GDB, 2012. http:developer.nvidia.comcuda-gdb. [50] Graphic REMEDY : gdebugger, 2010. www.gremedy.com. [51] PGI : PGI CUDA-x86 : CUDA Programming for Multi-core CPUs. http:www.pgroup. comresourcescuda-x86.htm. [52] Gregory Frederick DIAMOS, Andrew Robert KERR, Sudhakar YALAMANCHILI et Nathan CLARK : Ocelot : A Dynamic Optimization Framework for Bulk-Synchronous Applications in Heterogeneous Systems. In Proceedings of the 19th International Conference on Parallel Architectures and Compilation Techniques (PACT), pages 353 364. ACM, 2010. [53] Gabriel MARTINEZ, Mark GARDNER et Wu-chun FENG : CU2CL : A CUDA-to-OpenCL Translator for Multi- and Many-Core Architectures. In Proceedings of the 2011 IEEE 17th International Conference on Parallel and Distributed Systems (ICPADS), pages 300 307. IEEE Computer Society, 2011. [54] AMD INC : Accelerated Parallel Processing Math Libraries, 2012. http:developer. amd.comtoolsheterogeneous-computing. [55] Karl RUPP, Florian RUDOLF et Josef WEINBUB : ViennaCL - A High Level Linear Algebra Library for GPUs and Multi-Core CPUs. In International Workshop on GPUs and Scientific Applications (GPUScA), pages 51 56, 2010. [56] Karl RUPP, Josef WEINBUB et Florian RUDOLF : Automatic Performance Optimization in ViennaCL for GPUs. In Proceedings of the 9th Workshop on ParallelHigh-Performance Object- Oriented Scientific Computing (POOSC), pages 6 :1 6 :6. ACM, 2010. [57] Yannick ALLUSSE, Patrick HORAIN, Ankit AGARWAL et Cindula SAIPRIYADARSHAN : GpuCV : An OpenSource GPU-Accelerated Framework for Image Processing and Computer Vision. In Proceedings of the 16th ACM International Conference on Multimedia (MM), pages 1089 1092. ACM, 2008. [58] TUNACODE : CUVILib, 2012. http:cuvilib.com. [59] Panagiotis D VOUZIS et Nikolaos V SAHINIDIS : GPU-BLAST : Using Graphics Processors to Accelerate Protein Sequence Alignment. Bioinformatics, 27(2) :182 188, 2011. [60] Jacek BLAZEWICZ, Wojciech FROHMBERG, Michal KIERZYNKA et Pawel WOJCIECHOWSKI : G- MSA GPU-based, Fast and Accurate Algorithm for Multiple Sequence Alignment. Journal of Parallel and Distributed Computing, pages 32 41, 2012. [61] Jared HOBEROCK et Nathan BELL : Thrust : C++ Template Library for CUDA, 2009. http: code.google.compthrust. [62] Johan ENMYREN et Christoph W. KESSLER : SkePU : A Multi-Backend Skeleton Programming Library for Multi-GPU Systems. In Proceedings of the Fourth International Workshop on Highlevel Parallel Programming and Applications (HLPP), pages 5 14. ACM, 2010.

BIBLIOGRAPHIE 159 [63] Michel STEUWER, Philipp KEGEL et Sergei GORLATCH : SkelCL - A Portable Skeleton Library for High-Level GPU Programming. In Proceedings of the 2011 IEEE International Symposium on Parallel and Distributed Processing Workshops and PhD Forum (IPDPSW), pages 1176 1182. IEEE Computer Society, 2011. [64] Cédric AUGONNET : Scheduling Tasks over Multicore machines enhanced with Accelerators : A Runtime System s Perspective. Thèse de doctorat, Université Bordeaux 1, 2011. [65] Cédric AUGONNET, Samuel THIBAULT, Raymond NAMYST et Pierre-André WACRENIER : StarPU : A Unified Platform for Task Scheduling on Heterogeneous Multicore Architectures. Concurrency and Computation : Practice and Experience, Special Issue : Euro-Par 2009, 23 :187 198, 2009. [66] Jocelyn SÉROT et Dominique GINHAC : Skeletons for Parallel Image Processing : An Overview of the SKIPPER Project. Parallel computing, 28(12) :1685 1708, 2002. [67] Coullon HÉLÈNE, Le MINH-HOANG et Limet SÉBASTIEN : Parallelization of Shallow-Water Equations with the Algorithmic Skeleton Library SkelGIS. Procedia Computer Science, 18 :591 600, 2013. [68] Guodong LI : Formal Verification of Programs and their Transformations. Thèse de doctorat, The University of Utah, 2010. [69] Leslie LAMPORT : The Temporal Logic of Actions. ACM Transactions on Programming Languages and Systems (TOPLAS), 16(3) :872 923, 1994. [70] Kaustuv CHAUDHURI, Damien DOLIGEZ, Leslie LAMPORT et Stephan MERZ : The TLA+ Proof System : Building a Heterogeneous Verification Platform. In Proceedings of the 7th International colloquium conference on Theoretical aspects of computing (ICTAC). Springer-Verlag, 2010. [71] Adam BETTS, Nathan CHONG, Alastair DONALDSON, Shaz QADEER et Paul THOMSON : GPUVerify : A Verifier for GPU Kernels. SIGPLAN Not., 47(10) :113 132, 2012. [72] Axel HABERMAIER : The Model of Computation of CUDA and its Formal Semantics. Rapport technique, Technical Report 2011-14, University of Augsburg, 2011. [73] Andreas KLÖCKNER, Nicolas PINTO, Yunsup LEE, Bryan CATANZARO, Paul IVANOV et Ahmed FASIH : PyCUDA and PyOpenCL : A Scripting-Based Approach to GPU Run-time Code Generation. Parallel Computing, 38(3) :157 174, 2012. [74] Yonghong YAN, Max GROSSMAN et Vivek SARKAR : JCUDA : A Programmer-Friendly Interface for Accelerating Java Programs with CUDA. In Euro-Par 2009 Parallel Processing, pages 887 899. Springer, 2009. [75] Joel SVENSSON : Obsidian : GPU Kernel Programming in Haskell. Rapport technique 77L, Computer Science and Enginering, Chalmers University of Technology and Gothenburg University, 2011. [76] Reidar BECK, Helge Willum LARSEN, Tommy JENSEN et Bent THOMSEN : Extending Scala with General Purpose GPU Programming, 2011.

160 BIBLIOGRAPHIE [77] David TARDITI, Sidd PURI et Jose OGLESBY : Accelerator : Using Data Parallelism to Program GPUs for General-Purpose Uses. In John Paul SHEN et Margaret MARTONOSI, éditeurs : AS- PLOS, pages 325 335. ACM, 2006. [78] QUANTALEA : Alea.Cubase, 2012. http:quantalea.net. [79] BRAHMA : Brahma : Shader Meta-Programming Framework for.net, 2012. http: brahma.ananthonline.net. [80] Robert H HALSTEAD JR : Multilisp : A Language for Concurrent Symbolic Computation. ACM Transactions on Programming Languages and Systems (TOPLAS), 7(4) :501 538, 1985. [81] Cormac FLANAGAN et Matthias FELLEISEN : The Semantics of Future and an Application. Journal of Functional Programming, 9(1) :1 31, 1999. [82] Polyvios PRATIKAKIS, Jaime SPACCO et Michael HICKS : Transparent Proxies for Java Futures. ACM SIGPLAN Notices, 39(10) :206 223, 2004. [83] Adam WELC, Suresh JAGANNATHAN et Antony L. HOSKING : Safe Futures for Java. In Ralph JOHNSON et Richard P. GABRIEL, éditeurs : Object-Oriented Programming, Systems, Languages & Applications (OOPSLA), pages 439 453. ACM, 2005. [84] NVIDIA : Cuda C Programming Guide, 2012. http:docs.nvidia.comcudaindex. html. [85] Andrew K WRIGHT et Matthias FELLEISEN : A Syntactic Approach to Type Soundness. Information and computation, 115(1) :38 94, 1994. [86] Francois POTTIER et Didier RÉMY : The Essence of ML Type Inference. In Benjamin C. PIERCE, éditeur : Advanced Topics in Types and Programming Languages, chapitre 10, pages 389 489. MIT Press, 2005. [87] Mathias BOURGOIN, Emmanuel CHAILLOUX et Jean Luc LAMOTTE : SPOC : GPGPU Programming through Stream Processing with OCaml. Parallel Processing Letters, 22(2), 2012. [88] Mathias BOURGOIN, Emmanuel CHAILLOUX et Jean Luc LAMOTTE : SPOC : Stream Processing with OCaml, 2012. http:www.algo-prog.infospoc. [89] Xavier LEROY, Damien DOLIGEZ, Alain FRISCH, Jacques GARRIGUE, Didier Remy RÉMY et Jérôme VOUILLON : The OCaml System Release 4.00 : Documentation and User s Manual, 2012. http:caml.inria.fr. [90] Emmanuel CHAILLOUX, Pascal MANOURY et Bruno PAGANO : Développement d applications avec Objective CAML (avec CD-ROM). O Reilly Editions, 2000. [91] Mathias BOURGOIN, Emmanuel CHAILLOUX et Jean-Luc LAMOTTE : Efficient Abstractions for GPGPU Programming. International Journal of Parallel Programming, 2013. [92] Manuel MT CHAKRAVARTY, Gabriele KELLER, Sean LEE, Trevor L MCDONELL et Vinod GROVER : Accelerating Haskell Array Codes with Multicore GPUs. In Proceedings of the sixth workshop on Declarative Aspects of Multicore Programming (DAMP), pages 3 14. ACM, 2011.

BIBLIOGRAPHIE 161 [93] Simon MARLOW : Parallel and Concurrent Programming in Haskell : Techniques for Multicore and Multithreaded Programming. " O Reilly Media, Inc.", 2013. [94] The DPH TEAM : High Performance, Regular, Shape Polymorphic Parallel Arrays. http: hackage.haskell.orgpackagerepa. [95] AMD INC : Aparapi, 2013. http:code.google.compaparapi. [96] Eric HOLK, Milinda PATHIRAGE, Arun CHAUHAN, Andrew LUMSDAINE et Nicholas D. MATSA- KIS : GPU Programming in Rust : Implementing High-Level Abstractions in a Systems-Level Language. In Proceedings of the 2013 IEEE 27th International Symposium on Parallel and Distributed Processing Workshops and PhD Forum (IPDPSW), pages 315 324. IEEE Computer Society, 2013. [97] MOZILLA : Rust Reference Manual, 2013. http:www.rust-lang.org. [98] OpenCL Bindings for Rust. http:github.comluqmanarust-opencl. [99] Mathias BOURGOIN, Emmanuel CHAILLOUX et Jean-Luc LAMOTTE : Retour d expérience : portage d une application haute-performance vers un langage de haut niveau. In la Conférence d informatique en Parallélisme, Architecture et Système (ComPas), 2013. [100] N. Stan SCOTT, M. Penny SCOTT, Phil G. BURKE, Timothy STITT, V. FARO-MAZA, Christophe DE- NIS et A. MANIOPOULOU : 2DRMP : A Suite of Two-Dimensional R-matrix Propagation Codes. Computer Physics Communications, 180(12) :2424 2449, 2009. [101] A.M. LANE et R.G. THOMAS : R-Matrix Theory of Nuclear Reactions. Reviews of Modern Physics, 30(2) :257, 1958. [102] Phil G. BURKE, C.J. NOBLE et Penny SCOTT : R-Matrix Theory of Electron Scattering at Intermediate Energies. Proceedings of the Royal Society of London. Applied Mathematical and Physical Sciences, 410(1839) :289 310, 1987. [103] L Susan BLACKFORD, Antoine PETITET, Roldan POZO, Karin REMINGTON, R Clint WHALEY, James DEMMEL, Jack DONGARRA, Iain DUFF, Sven HAMMARLING, Greg HENRY et al. : An Updated Set of Basic Linear Algebra Subprograms (BLAS). ACM Transactions on Mathematical Software, 28(2) :135 151, 2002. [104] National Science FOUNDATION et Department of ENERGY : BLAS. http :www.netlib.orgblas, 2010. http :www.netlib.orgblas. [105] Timothy STITT, N Stan SCOTT, M Penny SCOTT et Phil G BURKE : 2-D R-Matrix Propagation : A Large Scale Electron Scattering Simulation Dominated by the Multiplication of Dynamically Changing Matrices. In High Performance Computing for Computational Science VECPAR 2002, pages 354 367. Springer, 2003. [106] Romain DOLBEAU, Stéphane BIHAN et François BODIN : HMPP : A Hybrid Multi-core Parallel Programming Environment. In Proceedings of the First Workshop on General Purpose Processing on Graphics Processing Units (GPGPU), 2007. [107] CAPS-ENTREPRISE : Rapport de portage HMPP de PROP, 2010.

162 BIBLIOGRAPHIE [108] Pierre FORTIN, Rachid HABEL, Fabienne JEZEQUEL, Jean-Luc LAMOTTE et N Stan SCOTT : Deployment on GPUs of an Application in Computational Atomic Physics. In Parallel and Distributed Processing Workshops and Phd Forum (IPDPSW), pages 1359 1366. IEEE, 2011. [109] Stanimire TOMOV, Jack DONGARRA, Vasily VOLKOV et James DEMMEL : MAGMA Library, 2009. http:icl.utk.edumagma. [110] Marco ALDINUCCI, Marco DANELUTTO, Peter KILPATRICK et Massimo TORQUATI : Targeting Heterogeneous Architectures Via Macro Data Flow. Parallel Processing Letters, 22(02), 2012. [111] Quentin CARBONNEAUX, François CLÉMENT et Pierre WEIS : Sklml : Functional Parallel Programming - User Manual, 2011. http:sklml.inria.fr. [112] Roberto DI COSMO, Zheng LI, Marco DANELUTTO, Susanna PELAGATTI, Xavier LEROY, Pierre WEIS et François CLÉMENT : CamlP3l 1.0 : User Manual, 2010. http:camlp3l.inria. fr. [113] Marco DANELUTTO et Roberto DI COSMO : A «Minimal Disruption» Skeleton Experiment : Seamless Map & Reduce Embedding in OCaml. Procedia Computer Science, 9 :1837 1846, 2012. [114] Louis GESBERT, Frédéric GAVA, Frédéric LOULERGUE et Frédéric DABROWSKI : Bulk Synchronous Parallel {ML} with Exceptions. Future Generation Computer Systems, 26(3) :486 490, 2010. [115] Olivier BALLEREAU, Frédéric LOULERGUE et Gaétan HAINS : High Level BSP Programming : BSML and BSlambda. In Philip W. TRINDER, Greg MICHAELSON et Hans-Wolfgang LOIDL, éditeurs : Scottish Functional Programming Workshop (SFP), volume 1 de Trends in Functional Programming, pages 29 40. Intellect, 1999. [116] Leslie G VALIANT : A Bridging Model for Parallel Computation. Communications of the ACM, 33(8) :103 111, 1990. [117] Wadoud BOUSDIRA, Frédéric LOULERGUE et Julien TESSON : A Verified Library of Algorithmic Skeletons on Evenly Distributed Arrays. In Yang XIANG, Ivan STOJMENOVIC, BernadyO. APDU- HAN, Guojun WANG, Koji NAKANO et Albert ZOMAYA, éditeurs : Algorithms and Architectures for Parallel Processing, volume 7439 de Lecture Notes in Computer Science, pages 218 232. Springer, 2012. [118] Mathias BOURGOIN, Emmanuel CHAILLOUX et Jean-Luc LAMOTTE : High Level GPGPU Programming with Parallel Skeletons. In Patterns for parallel programming on GPUs. Saxe-Coburg Publications, To appear. [119] Mathias BOURGOIN, Benjamin CANOU, Emmanuel CHAILLOUX, Adrien JONQUET, Philippe WANG et al. : OC4MC : Objective Caml for Multicore Architectures. In Draft Proceedings of the 21st Symposium on Implementation and Application of Functional Languages (IFL), 2009. [120] Benjamin CANOU, Vincent BALAT et Emmanuel CHAILLOUX : O browser : Objective Caml on Browsers. In Proceedings of the 2008 ACM SIGPLAN Workshop on ML, pages 69 78. ACM, 2008. [121] Jérôme VOUILLON et Vincent BALAT : From Bytecode to JavaScript : The js_of_ocaml Compiler. Software : Practice and Experience, 2013.

BIBLIOGRAPHIE 163 [122] NVIDIA : Whitepaper : NVIDIA s Next Generation, CUDA Compute Architecture : Kepler GK110,The Fastest, Most Efficient HPC Architecture Ever Built, 2013. http:www. nvidia.comobjectnvidia-kepler.html. [123] Nhat-Phuong TRAN, Myungho LEE, Sugwon HONG et DongHoon CHOI : Multi-Stream Parallel String Matching on Kepler Architecture. In James J. (Jong Hyuk) PARK, Hojjat ADELI, Namje PARK et Isaac WOUNGANG, éditeurs : Mobile, Ubiquitous, and Intelligent Computing, volume 274 de Lecture Notes in Electrical Engineering, pages 307 313. Springer, 2014. [124] Fei WANG, Jianqiang DONG et Bo YUAN : Graph-Based Substructure Pattern Mining Using CUDA Dynamic Parallelism. In Proceedings of the 14th International Conference on Intelligent Data Engineering and Automated Learning (IDEAL), pages 410 417. Springer, 2013. [125] Changjun WU et Ananth KALYANARAMAN : GPU-Accelerated Protein Family Identification for Metagenomics. In Proceedings of the 2013 IEEE 27th International Symposium on Parallel and Distributed Processing Workshops and PhD Forum (IPDPSW), pages 559 568. IEEE Computer Society, 2013. [126] Jianqiang DONG, Fei WANG et Bo YUAN : Accelerating BIRCH for Clustering Large Scale Streaming Data Using CUDA Dynamic Parallelism. In Proceedings of the 14th International Conference on Intelligent Data Engineering and Automated Learning (IDEAL), pages 410 417. Springer, 2013. [127] NVIDIA : Parallel Thread Execution ISA, 2013. http:docs.nvidia.comcudapdf. [128] KHRONOS OPENCL WORKING GROUP - SPIR SUBGROUP : SPIR 1.2 Specification for OpenCL, 2013. http:www.khronos.orgregistrycl. [129] CAPS Enterprise Nvidia CRAY INC. et The Portland GROUP : OpenACC 1.0 Specification, 2011. www.openacc.org. [130] Ruymán REYES, Iván LÓPEZ-RODRÍGUEZ, Juan J. FUMERO et Francisco de SANDE : A Preliminary Evaluation of OpenACC Implementations. The Journal of Supercomputing, 65(3) :1063 1075, 2013. [131] Sandra WIENKE, Paul L. SPRINGER, Christian TERBOVEN et Dieter an MEY : OpenACC - First Experiences with Real-World Applications. In Christos KAKLAMANIS, Theodore S. PAPATHEO- DOROU et Paul G. SPIRAKIS, éditeurs : Euro-Par, volume 7484 de Lecture Notes in Computer Science, pages 859 870. Springer, 2012.

Grammaires et Notations Expressions expr = K Noyau SPML g Dispositif GPGPU v Vecteur transférable v. [< expr >] Lecture dans un vecteur v. [< expr >] <- expr Ecriture dans un vecteur run K g [expr ] Exécution d un noyau mkvec expr expr Création de vecteur await g Attente d un GPGPU Kernel K = kern x -> expr noyau de calcul Expressions expr = expr ; expr Séquence Sarek let open Mod in expr Module local if cond then expr [else expr ]? Alternative for x = expr to expr do expr done Boucle For let [mutable]?x = expr in expr Définition while cond do expr done Boucle While x. [< expr >] Lecture dans un vecteur expr binop expr Opération binaire x := expr Affectation x. [< expr >] <- expr Ecriture dans un vecteur barrier Barrière x expr... expr Appel de fonction x Variable c Constante Ident x = [a z A Z 0 9_]+ Nom de variable [A Z ][a z A Z 0 9_] Nom de module [A Z ][a z A Z 0 9_]. x Accès dans un module Expressions et valeurs e, e 1, e n expression v, v 1, v n valeur v, v 1, v n expression constante associée à une valeur calculée précedemment @v, @v 1, @v n adresse d une valeur en mémoire hôte @ g v, @ g v 1, @ g v n adresse d une valeur en mémoire du GPGPU g @ p1 p2 v 1 adresse prédéfinie pour une valeur en cours de transfert entre p1 et p2 @v n [n] lecture directe du champ i du vecteur v n @v n [n] v écriture directe de la valeur v dans le champ i du vecteur v n Trvec Trvec R K C Commandes GPGPU transfert depuis l hôte transfert vers l hôte exécution du noyau de calcul K commande quelconque Environnements SPML E, E 1, E n environnement lexical associe des laisons (variables, valeur) M h, M 1 h, M n h M g, M 1 g, M n g Q g, Q 1 g, Qn g E mémoire hôte associe des laisons (adresse, valeur) mémoire GPGPU associe des laisons (adresse, valeur) file de commande du GPGPU g contient une liste de commandes correspond au triplet M h, M g,q g E T, E 1 T, E n T M T, M 1 T, M n T M g, Mg 1, M g n E 1 E 2 Environnements Sarek environnement lexical local à un thread associe des laisons (variables, valeur) mémoire locale à un thread associe des laisons (adresse, valeur) mémoire globale du GPGPU associe des laisons (adresse, valeur) union disjointe entre deux environnements E 1 E 2 union disjointe entre deux environnements Évaluation Q 1 g C Q 0 g E 0, M 0 h, M 0 g,q0 g e v E 1, M 1 h, M 1 g,q1 g E 0,(@v, v) M 0 h, M g 0,Q0 g e v E 1, M 1 h, M g 1,Q1 g M h, M g!vt @v M h, M g M h, M g!vt @ g v M h, M g Ajout d une commande dans la file Qg 0 évaluation d une expression e qui rend la valeur v indique que la mémoire de l environnement initial M h contenait la liaison (@v, v) indique que le transférable vt est actuellement en mémoire hôte indique que le transférable vt est actuellement en mémoire GPGPU

Résumé Les cartes graphiques (GPU) sont des dispositifs performants et spécialisés dotés de nombreuses unités de calcul, dédiés à l affichage et au traitement 3D. Les systèmes Cuda et OpenCL permettent d en détourner l usage pour réaliser des calculs généralistes, normalement effectués par le CPU : la programmation GPGPU (General Purpose GPU). De très bas niveau d abstraction, ils demandent de manipuler explicitement de nombreux paramètres matériels comme la mémoire ou le placement des calculs sur les différentes unités. Le but de cette thèse est l étude de solutions de plus haut niveau d abstraction pour la programmation GPGPU, afin de la rendre à la fois plus accessible et plus sûre. Nous introduisons deux langages de programmation dédiés à la programmation GPGPU, SPML et Sarek ainsi que leur sémantique opérationnelle, et les garanties qu ils apportent. Nous présentons ensuite une implantation de ces langages, en OCaml, à travers la bibliothèque SPOC et le langage dédié intégré, Sarek. Des tests montrent que notre solution permet d atteindre un haut niveau de performance, pour des exemples simples, comme pour le portage d une application numérique réaliste depuis Fortran et Cuda, vers OCaml. Nous montrons alors comment notre solution permet de définir des squelettes de programmation offrant davantage d abstractions. À travers un exemple, nous présentons comment ils simplifient la programmation GPGPU et autorisent le développement d optimisations supplémentaires. Enfin, nous discutons les possibilités offertes par l évolution des systèmes matériels et logiciels pour offrir une solution unifiée pour la programmation GPGPU. Abstract Graphics Processing Units (GPUs) are complex devices with many computation units. Dedicated to display management and 3D processing, they are very efficient, but also highly specialized. Since recent years, it is possible to divert their use to enable them to perform general computations normally performed by the CPU of the computer. This programming model, GPGPU (General Purpose GPU) programming is mainly based on two frameworks : Cuda and OpenCL. Both are very low-level and demands explicit management of hardware parameters such as the memory or the placement of computations on the various computation units. The goal of this thesis is the study of solutions of higher level of abstraction for GPGPU programming, in order to make it more accessible and safer. After an introduction to the context of GPGPU programming, we presnet two programming languages dedicated to GPGPU programming, SPML and Sarek. Through their operationnal semantics, we discuss their properties and the guarantees they offer. Then, we present an implementation of these languages with OCaml through the SPOC library and the domain specific language, Sarek. Performance tests show that our solution achieves a high level of performance for simple examples, as well as with the translation of a realistic numerical application from Fortran and Cuda, to OCaml. We also show how our solutions allow to define algorithmic skeletons that offer more abstractions. Through an example, we present how these skeletons eases GPGPU programming and offers additional automatic optimizations. Finally we discuss how the current hardware and software evolution can help providing a unified solution for GPGPU programming.