Proposition d'un modèle pour l'optimisation automatique de boucles dans le compilateur Tiramisu : cas d'optimisation de déroulage

07/29/2019
by   Asma Balamane, et al.
0

Computer architectures become more and more complex. It requires more effort to develop techniques that improve the programs of performance and allow to exploit material resources efficiently. As a result, many transformations are applied on various levels of code abstraction. The first level is the high level, where the representation is close to the high level language. The second one is the low level, where the presentation is close to the machine code. Those transformations are called code optimizations. Optimizing programs requires deep expertise. On one hand, it is a tedious task, because it requires a lot of tests to find out the best combination of optimizations to apply with their best factors. On the other hand, this task is critical, because it may degrade the performance of the program instead of improving it. The automatization of this task can deal with this problem and permit to obtain good results. Our end of study project consists on proposing a novel approach based on neural networks to automatically optimize loops in Tiramisu. Tiramisu is a new language to create a code of high performance. It allows to separate between the algorithm and its optimizations. We have chosen loop unrolling as a study case. Our contribution aims to automate the choice of the best loop unrolling factor for a program written in Tiramisu.

READ FULL TEXT VIEW PDF

Authors

page 1

page 2

page 3

page 4

11/10/2019

Using Deep Neural Networks for Estimating Loop Unrolling Factor

Optimizing programs requires deep expertise. On one hand, it is a tediou...
06/09/2020

Guiding Optimizations with Meliora: A Deep Walk down Memory Lane

Performance models can be very useful for understanding the behavior of ...
04/12/2021

AI Powered Compiler Techniques for DL Code Optimization

Creating high performance implementations of deep learning primitives on...
05/12/2021

Breaking the Computation and Communication Abstraction Barrier in Distributed Machine Learning Workloads

Recent trend towards increasing large machine learning models require bo...
05/22/2019

A Quick Introduction to Functional Verification of Array-Intensive Programs

Array-intensive programs are often amenable to parallelization across ma...
02/03/2022

HECO: Automatic Code Optimizations for Efficient Fully Homomorphic Encryption

In recent years, Fully Homomorphic Encryption (FHE) has undergone severa...
08/07/2020

Optimizing Program Size Using Multi-result Supercompilation

Supercompilation is a powerful program transformation technique with num...
This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

Résumé

Les architectures des ordinateurs deviennent de plus en plus complexes. Ceci nécessite des efforts pour développer des techniques d’amélioration de programmes permettant une exploitation efficace des ressources matérielles. De ce fait, des transformations sont appliquées sur divers niveaux d’abstraction de code, à savoir le haut niveau, où la représentation du code est proche du langage haut niveau, et le bas niveau, où la représentation est proche du code machine. Ces transformations s’appellent les optimisations de code.

L’optimisation des programmes requiert une expertise profonde. D’une part, elle représente une tâche fastidieuse, car elle nécessite plusieurs tests pour trouver les meilleures combinaisons d’optimisations à appliquer avec leurs bons paramètres. D’une autre part, cette tâche est critique, car elle risque de dégrader les performances du programme au lieu de les améliorer. L’automatisation de l’optimisation de programmes permet de faciliter cette tâche et d’obtenir de bons résultats.

Notre projet de fin d’étude consiste à concevoir et développer un modèle pour l’optimisation automatique de boucles dans Tiramisu. Ce dernier est un nouveau langage pour créer des codes de haute performance. Il permet de séparer entre le programme et ses optimisations. Nous avons pris comme cas d’étude l’optimisation de déroulage (loop unrooling

). Notre contribution vise à automatiser le choix du meilleur facteur de l’optimisation de déroulage de boucles pour un programme écrit en Tiramisu. La solution proposée se base sur les réseaux de neurones profonds.

Pour évaluer notre solution, nous avons d’abord comparé le modèle proposé avec les autres algorithmes du machine learning (K plus proches voisin et les arbres de décision) utilisés dans les travaux précédents. Notre modèle présente une précision compétitive à ces deux méthodes. Une seconde évaluation est effectuée sur un ensemble de benchmarks. Nous avons comparé entre les facteurs prédits par le modèle et ceux trouvés exhaustivement. Le modèle a pu prédire correctement le meilleur facteur de déroulage dans des instances. Nous avons également évalué l’accélération en temps d’exécution des benchmarks sans application de déroulage et avec l’application du déroulage dont le facteur est prédit par notre modèle. Notre méthode a pu améliorer le temps d’exécution dans des instances. Ce résultat confirme que les réseaux de neurones profonds peuvent être utilisés pour résoudre le problème de choix du facteur de déroulage. Le modèle apprend et donne une précision supérieure à la prédiction aléatoire.

Mots clés :

Optimisation automatique de boucles, sélection de paramètres d’optimisation, déroulage de boucle, Tiramisu, approche d’optimisation automatique.

Abstract

Computer architectures become more and more complex. It requires more effort to develop techniques that improve programs’ performance and allow to exploit material resources efficiently. As a result, many transformations are applied on various levels of code abstraction. The first level is the high level, where the representation is close to the high level language. The second one is the low level, where the presentation is close to the machine code. Those transformations are called code optimizations.

Optimizing programs requires deep expertise. On one hand, it is a tedious task, because it requires a lot of tests to find out the best combination of optimizations to apply with their best factors. On the other hand, this task is critical, because it may degrade the program’ performance instead of improving it. The automatization of this task can deal with this problem and permit to obtain good results.

Our end of study project consists on proposing a novel approach based on neural networks to automatically optimize loops in Tiramisu. Tiramisu is a new language to create a code of high performance. It allows to separate between the algorithm and its optimizations. We have chosen loop unrolling as a study case. Our contribution aims to automate the choice of the best loop unrolling factor for a program written in Tiramisu.

Initially, we have compared our model with other machine learning algorithms ( KNN and decesion treee) from previous work. Our model was competitive to those two methods. A second evaluation has been performed on a set of five benchmarks. We compared between the factor predicted by our model and the best one found exhaustively. Our model has been able to predict correctly the best unrolling factor in

of the instances. We have also evaluated the acceleration in the execution time of the benchmarks without applying loop unrolling optimization and with the application of loop unrolling with predicted unrolling factor. Our method could improve execution time in of the instances. This result confirms that deep neural networks can be used to solve the problem of choosing the best unrolling factor. The model has been able to learn and it gives greater precision than random prediction.

Key words :

Automatic optimization of loops, selection of the best factor of optimizations, loop unrolling, Tiramisu, automatic optimization approaches.

See pages - of resumezina.pdf

Table des matières
Table des figures

Introduction générale

Afin d’atteindre des niveaux de performances avancés, les architectures des ordinateurs deviennent de plus en plus complexes. Dans certains domaines, les applications requièrent une exploitation efficace des ressources matérielles. Les développeurs doivent introduire des transformations (optimisations) sur divers niveaux d’abstraction de codes. Ces optimisations visent à améliorer certaines métriques notamment l’utilisation de la mémoire et le temps d’exécution.

L’optimisation des programmes n’est pas une tâche simple. Elle requiert une expertise profonde afin de donner les bonnes optimisations avec leurs meilleurs facteurs à appliquer. Il s’agit d’une tâche qui est d’une part fastidieuse, car elle nécessite plusieurs tests pour trouver les meilleures combinaisons d’optimisations. D’une autre part, c’est une tâche critique, car elle risque de dégrader les performances du programme au lieu de les améliorer. En effet, L’optimisation dépend de plusieurs contraintes, à savoir les caractéristiques matérielles de la machine d’exécution, les dépendances entre les instructions du programme et les interactions entre les optimisations de codes à appliquer. Ces interdépendances agissent significativement sur l’effet des optimisations.

L’automatisation de l’optimisation des programmes permet de faciliter cette tâche et d’obtenir de bons résultats compétitifs aux optimisations soigneusement établies par des experts. Plusieurs approches ont été proposées afin d’améliorer l’automatisation de l’optimisation de codes donnant naissance à des nouvelles techniques intégrées aux compilateurs. Il existe plusieurs problèmes ouverts dans le domaine d’optimisation automatique : la sélection des optimisations bénéfiques à appliquer, l’estimation des bons paramètres des optimisations et la définition de l’ordre d’application des optimisations pour avoir les meilleures performances.

L’optimisation du code touche principalement deux niveaux, à savoir l’optimisation haut niveau, où la représentation du code est proche du langage haut niveau, et l’optimisation bas niveau où la représentation est proche du code machine. L’optimisation haut niveau du code s’avère primordiale avant toute optimisation bas niveau notamment dans certains langages dit spécifiques au domaine (LSD). Ces langages fournissent des fonctionnalités de programmation spéciales qui ne sont pas facilement offertes par les autres langages de programmation à objectifs généraux TouatiAdvancedBackendOptim.

Tiramisu est un nouveau langage et compilateur qui permet de générer des codes très rapides et de cibler différentes architectures matérielles (multicore, GPU, FPGA et systèmes distribués). Il a été lancé en 2018 par l’équipe COMMIT de CSAIL111

Computer Science and Artificial Intelligence Laboratory.

du MIT, elle travaille principalement sur le développement de nouveaux compilateurs qui facilitent l’écriture des codes optimisés. Tiramisu offre la particularité de séparer l’algorithme et les optimisations à appliquer sur le code dans une partie appelée Schedule. Cette partie contient des commandes d’optimisation introduites par le programmeur manuellement, c’est-à-dire que le programmeur doit choisir parmi les optimisations, celles qui rendent son code plus rapide. Or, l’optimisation manuelle est compliquée, le programmeur risque de ne pas choisir les meilleures combinaisons d’optimisations.

Dans le cadre de notre projet de fin d’étude, nous visons à contribuer dans l’optimisation automatique de boucles dans Tiramisu. Nous prenons comme cas d’étude l’optimisation de déroulage (loop unrooling). Le problème de sélection du meilleur facteur de déroulage représente le problème principal ciblé par notre projet de fin d’étude.
Concrètement, les objectifs de notre contribution sont :

  • [label=–]

  • Concevoir un modèle pour automatiser le choix du meilleur facteur de l’optimisation de déroulage de boucles pour un programme écrit en Tiramisu (un programme déjà optimisé ou non optimisé), et ce, pour des architectures matérielles à base de CPU.

  • Explorer une nouvelle approche de sélection automatique du facteur de déroulage. La solution proposée doit présenter une précision qui dépasse la sélection aléatoire (random selection) et qui est compétitive aux travaux précédents.

Ce document est organisé comme suit : dans la première partie, nous commençons d’abord par une étude théorique répartie en trois chapitres. Dans le chapitre I nous introduisons l’optimisation du code en donnant une explication sur le fonctionnement des optimisations haut niveau du code les plus utilisées. Le chapitre II présente les approches et les techniques adoptées par les compilateurs pour optimiser automatiquement les programmes. Dans le chapitre III, nous exposons le compilateur Tiramisu ciblé par notre projet. Dans la deuxième partie, nous détaillons notre contribution tout au long de trois chapitre. Dans le chapitre IV, nous expliquons les phases de conception du système proposé en détaillant notre modèle de prédiction du meilleur facteur de déroulage. Dans le chapitre IV.4, nous expliquons les étapes de réalisation du système tout en justifiant les différents choix technologiques considérés.
Pour clore ce mémoire, nous exposons dans le chapitre LABEL:ch:tests, les résultats de comparaison du modèle avec deux autres algorithmes de machine learning (K plus proches voisin et les arbres de décision) utilisés dans les travaux précédents pour le problème de sélection du facteur de déroulage. Nous exposons également dans le dernier chapitre l’évaluation effectuée sur des benchmarks implémentés sur Tiramisu.

Introduction

Dans certains domaines d’application, il ne suffit pas d’écrire des codes qui répondent aux spécifications, mais aussi des codes qui exploitent efficacement les ressources matérielles avec un temps d’exécution minimal Knijnenburg2002.

Les optimisations des programmes sont des transformations appliquées sur la structure du code dans le but d’améliorer ses performances notamment le temps d’exécution. Cependant, dans certains cas, l’application des optimisations peut les dégrader. En effet, déterminer les transformations susceptibles d’améliorer les performances dépend de plusieurs facteurs parfois incontrôlables par le programmeur, à savoir la gestion des caractéristiques de l’architecture de la machine . D’autre part, la connaissance de l’impact d’application de chaque optimisation à part et celui de sa combinaison avec d’autres est très compliquée ce qui fait de l’optimisation du code une tâche assez complexe et nécessite une expertise profonde.

Dans ce chapitre, nous allons exposer les optimisations de codes utilisées dans le compilateur Tiramisu, expliquer leurs objectifs et déterminer les paramètres à considérer pour les appliquer. Enfin, nous allons clôturer le chapitre par une explication des difficultés rencontrées afin de choisir les bonnes combinaisons d’optimisations à appliquer.

i.1 Niveaux d’optimisation du code

Un compilateur est un programme qui traite les instructions écrites dans un langage de programmation donné pour les traduire en langage machine, utilisé par le processeur d’un ordinateur. Les algorithmes peuvent être exprimés sous divers langages de programmation, les codes seront ensuite transformés en une représentation intermédiaire ou un code machine. De ce fait, un code peut avoir une représentation haut niveau qui est proche aux langages de programmation haut niveau, et une représentation bas niveau proche aux instructions du processeur.

les optimisations de code représentent l’ensemble des techniques utilisées afin d’améliorer les performances d’un programme, à savoir le temps d’exécution, l’utilisation de la mémoire ou encore la consommation de ressources. L’optimisation des programmes s’avère primordiale dans certains domaines d’application nécessitant un calcul immense et donc une grande consommation des ressources. La théorie de la compilation définit des frontières entre l’optimisation haut niveau et l’optimisation bas niveau, cette classification est liée aux niveaux de la représentation de code, à savoir la représentation haut niveau et la représentation bas niveau.

- Optimisation bas niveau (backend optimization): il s’agit de l’ensemble des transformations appliquées sur le code sous sa représentation finale proche aux instructions destinées aux processeurs comme les instructions assembleur et le code à trois adresses. Les métriques optimisées à ce niveau d’abstraction sont généralement liées à l’architecture du processeur : le nombre des instructions générées, la taille du code, l’ordonnancement des instructions, l’allocation et l’affectation des registres, l’optimisation du cache, l’optimisation du mode d’adressage, etc.

- Optimisation haut niveau (front-end optimization): elle représente l’ensemble des transformations appliquées sur le code écrit dans le langage haut niveau directement ou sous sa représentation intermédiaire proche au langage haut niveau, cette représentation contient des structures syntaxiques sophistiquées telles que les boucles et les structures de contrôles ainsi que les structures de données complexes. Parmi ces optimisations se trouvent les transformations des nids de boucles, l’analyse de dépendance de données et de procédures, la parallélisation des instructions et des blocs et l’analyse des alias. L’optimisation haut niveau de code est généralement effectuée par le programmeur, qui gère les entités abstraites et tient compte du fonctionnement du programme en général pour optimiser la conception du système. L’analyse et l’optimisation à ce niveau d’abstraction du programme permettent d’améliorer des métriques indépendamment des architectures d’exécution TouatiAdvancedBackendOptim.

Ce travail s’intéresse à l’optimisation haut niveau des codes qui représente la phase basique de l’optimisation de codes. Dans des langages spécifiques aux domaines (LSD) comme Halide Kelly2013Halide, Tiramisu Baghdadi2018TiramisuV3, PolyMage PolyMage qui sont utilisés dans le domaine de la programmation de hautes performances, l’optimisation haut niveau est primordiale avant toute optimisation bas niveau. En effet, les LSD fournissent des fonctionnalités spéciales de programmation optimisée qui ne sont pas fournies par les autres langages de programmation comme C, Java, etc.

i.2 Optimisations de boucles

Les outils de profilage111Code profiling consiste à analyser l’exécution d’un programme pour connaître son comportement à l’exécution. de code ainsi que les démonstrations basées sur la théorie de la complexité des algorithmes ont affirmé que les programmes passent plus de 90% du temps d’exécution au niveau des boucles, il est donc plus intéressant de focaliser sur l’optimisation des nids de boucles. De plus, ils utilisent voracement les ressources matérielles, tel que la mémoire cache et les unités de calcul Yaroub2013BclnestOptm.

i.2.1 Nids de boucles

Une "boucle" est considérée comme étant une structure itérative d’un programme fini. La structure algorithmique est schématisée dans la figure I.1 Ciorba2008ParallelLops.

Figure I.1: Structure générale d’une boucle.

Un nid de boucles est une imbrication d’un nombre fini n (n>1) de boucles (voir figure I.2). Il est dit "parfait" si toutes les instructions appartiennent à la boucle la plus interne.

Figure I.2: Structure générale d’un nid de boucle.

Une grande partie du temps d’exécution est consommée au niveau des boucles. De ce fait, il est très utile d’optimiser ces parties de codes, ceci permet d’améliorer efficacement les performances globales des programmes dans nombreuses applications.

Différentes techniques sont utilisées pour augmenter les performances de boucles. Cependant, les optimisations ne sont pas toujours valides et certaines peuvent détériorer les performances du programme ou même fausser ses résultats. L’analyse de dépendance est une des théories qui permettent de décider la possibilité d’appliquer une optimisation sur un programme sans avoir changé sa logique. Nous allons aborder ici les optimisations de boucles les plus importantes et les plus utilisées, à savoir la parallélisation, la vectorisation, la fusion de boucles, l’interversion de boucles, le déroulage, tuilage, etc.

i.2.2 Parallélisation (Parallelization)

Le parallélisme de boucle est un type très courant de parallélisme du code. Il a comme principal objectif de détecter et d’exploiter le parallélisme dans des programmes séquentiels. Cette transformation consiste à affecter chaque itération indépendante à des threads parallèles puis lancer l’exécution de ces derniers de manière asynchrone (voir figure I.3). Cela nécessite de trouver la bonne transformation de boucle qui permet de lancer le parallélisme.

Figure I.3: Parallélisation des itérations d’une boucle imbriquée sur deux threads.

Pour effectuer cette transformation d’une manière efficace, il faut prendre en considération les paramètres de la machine. En effet, plus que la machine dispose d’unités fonctionnelles plus la parallélisation est meilleure, de ce fait, la machine doit avoir au moins deux unités de traitement pour pouvoir exécuter les instructions en parallèle.

Il est intéressant de noter que la gestion des threads peut engendrer un coût supplémentaire. Donc, pour que l’optimisation soit vraiment utile, il faut que le temps perdu dans la gestion des threads soit strictement inférieur au temps gagné par la parallélisation. D’autre part, pour que la parallélisation soit valide, il faut s’assurer qu’elle n’inverse pas le sens de dépendance222Il y a une dépendance de données entre deux instructions, S1 et S2, lorsque elles tentent d’accéder à la même localité mémoire M, et au moins l’une de ces deux instructions tente de modifier M. entre les itérations Allen2002Optim.

i.2.3 Vectorisation (Vectorization)

La vectorisation est le processus permettant de transformer un programme qui opère sur des données élémentaires à un programme qui opère sur des vecteurs de données chacun porte sur deux éléments ou plus Byunghyun2010DataTransformations.

Dans une boucle, cette transformation consiste à regrouper un ensemble de éléments qui subissent le même traitement à travers les itérations pour les rediriger vers des registres vectoriels afin d’effectuer un traitement commun Allen2002Optim. La figure I.4 montre un exemple de vectorisation de boucles.

Figure I.4: Vectorisation de la boucle y avec un facteur de quatre.

L’objectif de cette transformation est d’utiliser efficacement les ressources matérielles notamment les registres. La vectorisation est une technique très utile pour exploiter le parallélisme au niveau de données. La taille des registres vectoriels affecte le choix du facteur dans la transformation de vectorisation, car plus la taille des registres est grande plus il est possible d’appliquer la vectorisation pour gagner en temps d’exécution Byunghyun2010DataTransformations.

i.2.4 Interversion de boucles (Loop reordering)

Cette transformation permet de changer l’ordre d’exécution de niveaux de boucle dans une boucles imbriquée i.e la boucle la plus interne devient la plus externe et vice versa (voir la figure I.5). L’interversion de boucles permet d’exploiter le parallélisme et d’améliorer la localité de données David1994Compilertransformations et par conséquent, d’utiliser le cache efficacement et d’améliorer l’accès aux données.

Cette transformation permet d’excellentes améliorations, mais elle n’est pas toujours légale. Pour effectuer cette transformation de manière à préserver le sens original du programme, il faut s’assurer qu’elle soit valide à appliquer, ceci est fait en analysant la dépendance entre les instructions des boucles à intervertir.

Figure I.5: Interversion de deux boucles.
i.2.5 Fusion de boucles (Loop fusion)

Cette transformation consiste à fusionner deux boucles adjacentes en une seule boucle (voir la figure I.6) afin de réduire la surcharge de la boucle et d’améliorer les performances d’exécution.

Il est important de noter que cette transformation n’améliore pas toujours le temps d’exécution, car cela dépend des contraintes architecturales de la machine. Par exemple, l’architecture de la mémoire peut offrir de meilleures performances si les deux tableaux sont initialisés dans des boucles séparées, plutôt que de les initialiser simultanément dans une seule boucle. D’autre part, cette transformation n’est pas toujours légale, elle doit être soumise aux conditions de validité en utilisant l’analyse de dépendance.

Figure I.6: Fusion de deux boucles adjacentes en une seule boucle.
i.2.6 Découpage en bandes (Loop splitting)

Le découpage en bandes est l’une des transformations de boucles les plus puissantes qui consiste à découper la dimension par un facteur pour créer de nouvelles dimensions en divisant la variable d’itération en sous variables internes et externes. Ce facteur est connu sous le nom de "facteur de découpage". Il transforme la boucle ayant par exemple l’indice i et la dimension N en une boucle imbriquée : la boucle la plus interne avec la dimension k et la boucle externe avec la dimension qui est le résultat de la division de l’étendue de sur le facteur de découpage k (voir la figure I.7). Après le découpage, les références à l’index d’origine deviennent : () Jonathan2012Decouplingalgorithms.

Figure I.7: Découpage en bandes d’une boucle avec un facteur de 16.

Cette transformation est utilisée pour les nids de boucles qui manipulent les données multidimensionnelles (tableaux à 2 dimensions ou à 3 dimensions). L’objectif principal de cette transformation est de séparer les itérations indépendantes de la boucle principales, ce qui permet de les exécuter en parallèle.

L’optimisation de découpage en bandes ne change pas l’ordre d’évaluation des instructions. Cette transformation ne fait rien en elle-même, mais cela ouvre d’autres possibilités d’optimisations comme le déroulage lorsqu’il est combiné avec d’autres transformations. La vectorisation et le déroulage l’utilisent souvent, ils sont précédés par une division de la dimension de boucles par le facteur de vectorisation ou de déroulage. Ensuite, la vectorisation ou le de déroulage de la boucle interne est effectué.

i.2.7 Déroulage de boucle (Loop unrolling)

L’optimisation de déroulage (loop unrolling) est une transformation qui consiste à répliquer les instructions de la boucle la plus interne une ou plusieurs fois, selon le facteur de déroulage k donné. Ce qui permet de réduire le nombre d’itérations. Par exemple, si alors le nombre d’itérations (initialement égal à ) est réduit à (voir la figure I.8). Le déroulage est appliqué lorsque plusieurs itérations de boucles rechargent les mêmes valeurs.

Figure I.8: Déroulage d’une boucle avec un facteur de déroulage de quatre.

La transformation de déroulage permet la réutilisation des registres, si les instructions déroulées sont indépendantes entre elles, il est possible de les lancer en parallèle à condition que la machine dispose d’un nombre suffisant de registres pour appliquer la parallélisation. Un autre avantage de cette transformation est qu’elle peut être appliquée sur n’importe quelle boucle sans restriction sur le langage source. Le déroulage améliore les performances presque dans tous les cas où il est appliqué d’une manière significative David1994Compilertransformations.

Il est recommandé d’appliquer cette transformation sur la boucle la plus interne, car le fait de l’appliquer sur la boucle externe risque d’augmenter la surcharge du contrôle de la boucle au lieu de la diminuer (à cause de la réplication de la boucle interne). Le déroulage de la boucle interne dans le cas des boucles partageant des données qui se chevauchent réduit le nombre total de chargements (load). Il permet aussi de réduire le nombre de tests d’instruction de branchements : conditions d’arrêt seront testées fois, les pénalités de branchements seront réduites d’un facteur de dans les architectures à base de pipeline.

Le facteur de déroulage doit être bien choisi en prenant en considération les contraintes architecturales de la machine : si est grand, le chargement de toutes les instructions dans le cache à la fois ne sera pas possible, ainsi le taux de défauts de cache augmente et de même le temps d’exécution. L’optimisation de déroulage peut être intégrée automatiquement dans les compilateurs grâce à sa simplicité David1994Compilertransformations. Pour plus de détails sur cette optimisation voir l’annexe A.

i.2.8 Tuilage de boucles (Loop tiling)

Le tuilage ou encore le tiling ou le blocking est une combinaison de découpage en bandes et d’interversion de boucles. Tout d’abord, nous découpons et avec un facteur de et respectivement ( est appelé le facteur de tuilage). Ensuite, nous réordonnons les variables pour exprimer le tuilage (voir la figure I.9). Le tuilage est une transformation qui permet de découper la matrice en petits blocs rectangulaires : il itère à l’extérieur sur les blocs, et à l’intérieur, il itère sur les instructions de chaque bloc Baghdadi2018TiramisuV3.

Figure I.9: Un tuilage avec facteur : . Après le tuilage, A est accessible en blocs de taille .

Le tuilage est la généralisation multidimensionnelle de découpage en bandes. Le tuilage est principalement utilisé pour améliorer la réutilisation du cache en divisant l’espace d’itération333L’ensemble de toutes les instances d’exécution d’une instruction. en blocs David1994Compilertransformations. Le tuilage permet aussi de maximiser la localité de données (localité spatiale et temporelle). Il permet de créer de nouvelles boucles internes de sorte que les données accessibles dans les boucles internes s’insèrent dans le cache. Le tuilage peut augmenter le taux de parallélisme, car si les données des blocs ne dépendent pas l’une des autres, il est possible de les paralléliser. Cette transformation s’avère indispensable pour atteindre des hautes performances dans les calculs de multiplication sur les matrices denses ou encore dans l’algèbre linéaire. Elle constitue aussi un facteur clé pour améliorer les performances des réseaux de neurones convolutifs444Les CNN estun type de réseaux de neurones connus par l’opération convolution. Ils s’adaptent bien pour des données organisées en grille telles que les images..

Le tuilage est une transformation complexe. Le facteur de tuilage doit être choisi soigneusement. D’une part, il doit être assez petit pour s’assurer que le bloc peut être chargé entièrement dans le cache et pour que la parallélisation soit bénéfique. D’une autre part, il doit être assez grand pour bien exploiter l’espace du cache. Il n’est pas toujours possible d’appliquer cette transformation comme elle se base sur la transformation de l’interversion de boucles qui n’est pas toujours légale. Elle doit être soumise au processus de validation en utilisant l’analyse de dépendance avant son application. Pour plus de détails sur cette optimisation voir l’annexe A.

i.2.9 Torsion de boucles (Loop skewing)

la torsion de boucle ou

Loop skewing

est une transformation qui redéfinit l’espace d’itération pour permettre d’exploiter la parallélisation par la suite. Elle est principalement utile en combinaison avec la transformation d’interversion de boucles David1994Compilertransformations.

Cette optimisation est effectuée en ajoutant l’index de la boucle externe multipliée par le facteur d’inclinaison , aux bornes de la variable de la boucle interne, puis soustraire la même valeur pour chaque utilisation de la variable de la boucle interne à l’intérieur de la boucle (voir la figure I.10). Elle change les bornes de la boucle et change aussi l’utilisation des index correspondants pour garder le même sens que le programme original. Cette transformation ne change pas le sens du programme et elle est toujours légaleDavid1994Compilertransformations.

Figure I.10: Exemple de loop skewing, J sert à une nouvelle variable pour remplacer j avec  David1994Compilertransformations.

Les boucles qui résultent de la transformation d’inclinaison sont extrêmement trapézoïdales555Trapézoïdal est un adjectif qui désigne quelque chose en forme de trapèze, c’est-à-dire avec quatre côtés dont deux sont parallèles. ce qui produit des bornes complexes. Les pénalités résultant d’une telle boucle irrégulière sont plus sévères sur les machines vectorielles mais moins graves sur les multi-processeurs asynchrones. Cependant, même sur ces architectures, l’inclinaison de boucle doit être appliquée avec précaution pour éviter des déséquilibres de charges significatifs Allen2002Optim.

i.2.10 Calcul redondant

Cette transformation permet de calculer la donnée à chaque fois que c’est nécessaire au lieu d’aller la charger à partir de la mémoire. En effet, la donnée doit être recalculée d’une manière redondante à chaque fois qu’elle est utilisée même si le résultat existe déjà dans la mémoire. Cette optimisation est très utile lorsque le coût arithmétique (le temps pris) de calcul est inférieur par rapport au coût de son chargement à partir de la mémoire Menaceri2008HalideAuto.

i.2.11 Réorganisation de calcul

Cette transformation permet de réordonner les instructions du programme David1994Compilertransformations. Elle consiste à rapprocher les instructions consommatrices (celles qui utilisent la donnée produite par d’autres instructions) des productrices (celle qui produit ces données). Cette transformation assure que la donnée consommée se trouve toujours dans le cache de données, ce qui permet d’améliorer la localité des données par conséquent Menaceri2008HalideAuto.

i.3 Difficultés du choix des bonnes optimisations

Plus les architectures des processeurs deviennent complexes, plus le nombre de transformations possibles à appliquer augmente et donc le processus de sélection des bonnes optimisations devient très compliqué aussi David1994Compilertransformations.

Ainsi, la structure de l’architecture matérielle est un facteur déterminant pour le choix des optimisations à effectuer, car il faut maximiser l’utilisation des ressources, à savoir les processeurs, les unités fonctionnelles et les registres, etc. D’autre part, il faut minimiser le nombre d’opérations effectuées, les défauts de cache et la taille mémoire requise.

Parfois, ces objectifs ne peuvent pas être réunis. Considérons la figure I.11(a), supposons que la machine stocke les tableaux colonne par colonne, l’accès à chaque élément de la matrice "a" nécessite de charger entièrement la colonne contenant cet élément à chaque itération de la boucle interne. Par contre, en inversant l’ordre des niveaux de la boucle, les éléments seront accessibles séquentiellement colonne par colonne ce qui correspond à l’ordre de stockage défini par la machine. Ceci permet de réduire la distance entre deux accès successifs de la mémoire et donc, de minimiser les défauts de cache. D’autre part, la version (a) du programme exprimé dans la figure I.11 permet au tableau "total[i]" d’être chargé dans le registre une seule fois pour chaque ligne de la matrice "a". Mais, la version optimisée (b) augmente le nombre des opérations de chargement/stockage de (de à ) car l’élément est chargé dans le registre pour chaque itération de la boucle interne de la matrice David1994Compilertransformations.

Dans la Figure I.11

(a), la boucle interne accède aux éléments du tableau a avec un stride

666Le nombre de locations mémoires entre deux éléments successifs dans un tableau.-n. En inversant l’ordre des boucles, l’accès devient en stride-1, comme le montre la figure I.11 (b).

Figure I.11: Exemple de l’interversion de boucle pour montrer la difficulté de choix de la meilleure optimisation David1994Compilertransformations.

Une autre difficulté peut se poser aussi, la transformation peut être illégale, donc il faut d’abord étudier la validité d’appliquer cette transformation, une tâche qui n’est pas simple.

Nous avons vu que le choix d’une seule optimisation est une tâche assez complexe. La difficulté se multiplie énormément lorsqu’on veut avoir une bonne combinaison d’optimisations.
Considérons la multiplication généralisée de matrice (gemm), qui calcule (avec , et sont des matrice, et sont des scalaires). Elle représente un bloc de construction de nombreux algorithmes, notamment les RNCs (Réseau de Neurones Convolutifs). Les implémentations hautement optimisées nécessitent la fusion des boucles de multiplication et d’addition, ainsi que l’application du tuilage de boucles à deux niveaux, de la vectorisation, du déroulage de la boucle, etc. Baghdadi2018TiramisuV3.

Générer un code de telle complexité est très difficile. En effet, pour la plupart des programmeurs, il est difficile d’obtenir ce niveau de performance, car l’effort requis pour explorer l’espace d’implémentations possibles est insoluble lors du codage manuel de transformations de code complexe. L’optimisation du code nécessite des connaissances très approfondies dans le domaine ainsi que de l’expertise. À titre d’exemple chez Google, il y a plus de 150 ingénieurs qui écrivent des algorithmes en langage Halide777Halide Kelly2013Halide est un nouveau compilateur et langage spécifique au domaine de traitement d’images, la principale particularité de ce language est la séparation entre les optimisations (schedule) et l’algorithme. non optimisés alors qu’il y a uniquement deux personnes qui disposent de l’expertise nécessaire pour les optimiser. Baghdadi2018TiramisuV3.

Par conséquent, le choix des optimisations à appliquer sur un programme est une tâche très fastidieuse qui nécessite la connaissance de l’architecture matérielle, le recensement des avantages et des inconvénients de chaque optimisation. Elle nécessite également des tests sur l’effet de combiner les optimisations et s’assurer que la logique de l’algorithme reste correcte après l’application des optimisations.

Conclusion

Dans ce chapitre, nous avons abordé les principales optimisations de boucles qui permettent d’améliorer les performances et de réduire le temps d’exécution. Nous avons exposé principalement les optimisations de boucles les plus importantes dans l’optimisation de code.

Il est important de noter qu’il ne faut pas appliquer les optimisations sans avoir effectué une étude au préalable, car elles peuvent détériorer les performances. Il faut effectivement prendre plusieurs contraintes en considération comme l’impact de l’utilisation de chaque optimisation à part et l’effet de les combiner. Il faut aussi prendre en considération les propriétés de l’architecture d’exécution (nombres d’unité de traitements, la taille des registres, etc). Quelques optimisations n’ont aucun effet sur le programme mais s’avère très utiles comme prétraitement pour appliquer d’autres. Ainsi, il se trouve que l’optimisation du programme n’est pas une tâche simple. C’est une tâche très complexe et fastidieuse pour les programmeurs. Elle nécessite beaucoup de temps pour trouver la meilleure combinaison d’optimisations qui utilise efficacement les ressources matérielles et améliore davantage le temps d’exécution. C’est pour cette raison, des compilateurs qui permettent d’optimiser le code d’une manière automatique sont apparus. Dans le chapitre suivant, nous allons aborder les approches et les techniques utilisées par les compilateurs pour optimiser le code d’une manière automatique.

Introduction

Dans le chapitre précèdent, nous avons exposé les différentes optimisations de codes qui permettent d’améliorer ses performances et d’exploiter efficacement les caractéristiques qu’offrent les architectures matérielles. La tâche de sélection de bonnes optimisations à appliquer est très complexe, elle nécessite une compréhension profonde des optimisations, de leurs effets sur le code ainsi que leurs interactions. Ce qui rend cette tâche très difficile voire infaisable manuellement pour des programmes assez complexes. Des techniques d’optimisation automatique ont été intégrées dans les compilateurs afin d’alléger cette tâche et de trouver les meilleures combinaisons d’optimisations à appliquer sur les programmes. Cependant, l’automatisation mène à résoudre un problème NP-difficile où l’espace de recherche est immense.

Dans ce chapitre, nous allons exposer les différentes approches d’optimisation automatique en comparant les avantages et les défis de chacune d’elle. Nous allons également cité quelques techniques de pointes basées sur ces approches.

ii.1 Motivation pour adopter l’optimisation automatique

Pour bien illustrer l’effort nécessaire lors de l’optimisation manuelle par un développeur expert comparativement à l’optimisation automatique, Mullapudi2016HalidAutoScheduler ont recruté deux développeurs professionnels dans le langage Halide Kelly2013Halide pour mettre en défi l’Auto-scheduler de Halide. Les experts ont sélectionné trois benchmarks qu’ils n’ont jamais optimisés auparavant111LENSBlur , NLMeans et MAXFilter. et ils ont implémenté les algorithmes originaux de ces benchmarks sur Halide. Chaque expert a défini des optimisations qu’il a jugées optimales pour chaque benchmark. Tout au long du processus d’optimisation manuelle, les experts ont pris des mesures de performances de leurs optimisations actuelles. Ensuite, ils ont comparé les performances de leur code optimisé manuellement à celui optimisé automatiquement par l’Auto-scheduler de Halide222Le test est effectué sur une machine de 4 cœurs d’Intel E5-2690 proposée par les deux experts..
Les résultats de la comparaison sont illustrés par la figure II.1. L’axe des dans chaque graphe indique le temps de développement (en minutes) des optimisations manuelles. L’axe des montre les performances des benchmarks333Mesuré en tant que débit en pixels.. La ligne horizontale correspond aux performances des optimisations générées par l’Auto-scheduler (en secondes). Les lignes jaune et grise correspondent chacune au progrès de chaque programmeur.

Figure II.1: Comparaison entre l’optimisation automatique et manuelle effectuée sur trois benchmarks Mullapudi2016HalidAutoScheduler.

Pour deux benchmarks parmi trois, même après presque une heure de travail, l’optimisation manuelle donne des résultats moins performants que ceux obtenus par l’optimisation automatique (générés en quelques secondes). L’optimisation automatique de codes est compétitive et parfois dépasse l’optimisation manuelle en termes de performances.

ii.2 Défis de l’optimisation automatique du code

Le défi majeur du choix des bonnes combinaisons d’optimisations réside essentiellement dans leurs dépendances aux langages de programmation, à la logique des algorithmes et aux architectures matérielles. De plus, les optimisations peuvent dégrader les performances du code dans certains cas. Comprendre le comportement de ces optimisations, leurs effets sur le code source et leurs interactions est un problème de modélisation très complexe qui donne naissance à des thématiques de recherche de pointe. En effet, il existe plusieurs problèmes ouverts dans le domaine d’optimisation des compilateurs : la sélection des optimisations bénéfiques à appliquer, l’estimation des bons paramètres des optimisations (le facteur de tuilage, facteur de déroulage de boucles, etc.) et la définition de l’ordre d’application des optimisations pour avoir les meilleures performances.

ii.2.1 Problème de sélection des bonnes optimisations

Le problème est défini en se limitant à la sélection des optimisations bénéfiques pour un programme donné tout en ignorant l’ordre d’application de ces optimisations. Les chercheurs ont montré que l’interdépendance et l’interaction entre l’activation et la désactivation des optimisations dans une combinaison peuvent altérer considérablement les performances du programme en cours d’exécution, même en ignorant leur ordre d’application Ashouri2018Survey.

L’espace de recherche : l’ensemble des optimisations peut être modélisé sous forme d’un vecteur booléen (soit ). Un élément d’optimisation peut avoir deux valeurs : si l’optimisation est activée, sinon () quand elle est désactivée. L’espace de toutes les combinaisons d’optimisations est (voir la figure II.2).

Figure II.2: Exemple d’espace de recherche d’une combinaison de quatre optimisations Menaceri2008HalideAuto.

L’espace d’optimisation du problème de sélection est un espace exponentiel. Si les facteurs que peut prendre chaque optimisation sont pris en considération, la version étendue de l’espace d’optimisation dépasse le choix binaire (entre activer/désactiver l’optimisation) à une variante à choix multiples pour chaque optimisation. De plus, certaines optimisations peuvent prendre plusieurs paramètres. Pour simplifier, si nous considérons comme étant le nombre total des options d’optimisations, l’espace devient .

ii.2.2 Problème du choix de l’ordre des optimisations (phase-ordering problem)

L’ordre dans lequel les optimisations sont appliquées influence sur l’accélération apportée : le choix d’un ordre d’optimisations prédéfini peut dégrader certaines optimisations qui auraient pu être plus efficaces si un autre ordre a été choisi.

L’espace de recherche : l’espace des choix d’ordre possibles des optimisations est factoriel ( permutations) tel que est le nombre d’optimisations à appliquer. Si de plus, le cas d’une taille variable d’une séquence d’optimisation est pris en considération444Possible de considérer cinq optimisations ou se restreindre à trois ou encore deux, de plus, une optimisation peut être réappliquée plusieurs fois, donc le nombre des optimisations dans la séquence est variable., le cardinal de l’espace devient alors , où représente le nombre d’optimisations possibles et représente la taille maximale que peut prendre une séquence d’optimisations. Même avec des valeurs raisonnables de et de , l’espace de recherche d’optimisations formé est immense. Par exemple, en supposant que et , l’espace de recherche d’optimisations est formé de plus de 11 billions séquences d’optimisations différentes. Le problème de trouver le bon ordre d’optimisations est non déterministe étant donné que la taille d’une séquence d’optimisation est non bornée.

Le problème du choix du bon ordre d’optimisations reste un problème ouvert dans le domaine d’optimisation des compilateurs. L’incapacité des chercheurs à résoudre complètement le problème les conduit à concentrer leurs efforts sur le problème de la sélection de l’ensemble des bonnes optimisations sans prendre en considération le problème d’ordre Ashouri2018Survey.

ii.3 Approches d’optimisation automatique du code

L’optimisation automatique du code traite de la génération automatique des codes optimisés en utilisant différents scénarios et architectures. Elle vise à choisir différents facteurs de code qui influencent en maximisant ou en minimisant une fonction objectif Ashouri2018Survey. Différentes approches ont été proposées, chaque approche traite une problématique d’optimisation de code pour répondre à la fonction objectif en favorisant certaines contraintes par rapport à d’autres. Cependant, elles reposent toutes sur deux principales notions : l’espace des optimisations à appliquer et le coût d’application des optimisations. Nous avons donné une classification des approches d’optimisation de code en se basant sur la problématique traitée par chaque classe. La figure II.3 donne une vision globale sur les approches d’optimisation automatique du code.

Figure II.3: Approches d’optimisation automatique de code.

L’espace des optimisations à appliquer représente les différentes optimisations de code connues, chacune vise à améliorer certaines caractéristiques de performance définies au niveau de la fonction objectif, certaines optimisations nécessitent des paramètres à définir. Cet espace de transformations crée de nombreuses versions du programme. L’exploration de cet espace des optimisations et leurs paramètres peut être coûteuse ce qui mène à définir des approches dédiées pour une exploration plus efficace. D’autre part, le coût d’application d’un sous-ensemble d’optimisations , représenté sous forme d’une instance mesurée de la fonction objectif, permet de décider l’efficacité de et aide à orienter l’exploration.

ii.3.1 Approches d’exploration d’espace d’optimisations

L’espace d’optimisations est constitué de différentes optimisations de boucles pouvant améliorer les performances d’un programme si une "bonne combinaison" a été choisie.
L’espace est très souvent grand ce qui impose de définir des approches d’exploration permettant de trouver la solution en un temps raisonnable.
Les approches exposées sont : (1) l’approche exhaustive, (2) l’approche basée sur les heuristiques et les métaheuristiques et (3) l’approche basée sur l’apprentissage automatique.

ii.3.1.1 Approche exhaustive

Cette méthode consiste à explorer tout l’espace de recherche en essayant toutes les optimisations possibles afin de trouver la meilleure combinaison. Par exemple, ATLAS une bibliothèque spécialisée dans l’optimisation automatique des programmes de multiplication de matrices. Elle utilise la méthode exhaustive pour trouver certains paramètres d’optimisation pré-élaborés ce qui permet d’améliorer la multiplication de matrices sur l’architecture d’exécution cible. ATLAS explore exhaustivement des plages de valeurs des paramètres d’optimisations pour différentes tailles de matrices. Ensuite, cette bibliothèque sauvegarde les meilleurs modèles d’implémentation du programme pour les différentes tailles de matrices afin de les utiliser ultérieurement pendant l’exécution Atlasref.

Cette approche permet donc d’essayer toutes les possibilités pour choisir la meilleure avec précision. Cependant, elle requière un temps de traitement énorme exponentiellement lié à la dimension de l’espace d’exploration. Cette approche est utilisée alors pour des espaces restreints qui ont été déjà réduits grâce à un certain mécanise de présélection.

ii.3.1.2 Approches d’heuristiques et de métaheuristiques

Plusieurs algorithmes basés sur des heuristiques555Les heuristiques sont des règles empiriques simples basées sur l’expérience. Elles ne donnent pas forcément la solution la plus optimale mais plutôt des solutions assez proches de l’optimale en un temps raisonnable. Elles sont définies pour un type de problème d’optimisation. et des métaheuristiques666Des stratégies d’exploration d’espace de recherche (qui peut être très grand). Elles permettent d’explorer cet espace d’une manière efficace pour se rapprocher de la solution optimale à des problèmes d’optimisation plus généraux. ont été proposées pour résoudre les problèmes d’optimisation. Dans le cas où l’espace de recherche est vaste, la recherche exhaustive, les méthodes itératives ou les méthodes heuristiques simples ne sont pas assez pratiques, alors que les métaheuristiques permettent souvent de trouver de bonnes solutions avec moins d’effort de calcul. L’algorithme génétique (AG) est l’une des métaheuristiques les plus utilisées dans les problèmes d’optimisation automatique du code. Les méthodes d’optimisation automatique basées sur l’algorithme génétique génèrent d’abord une population aléatoire d’individus. Ensuite, les différents opérateurs de l’algorithme génétique (mutation, croisement, sélection, etc.) sont appliqués sur ces individus pendant itérations. La sortie de cet algorithme est la meilleure combinaison d’optimisations explorées.

Cooper1999AG ont utilisé l’algorithme génétique pour la sélection des optimisations avec la compilation itérative. iterativeCompilationFactors ont aussi proposé une technique itérative où ils ont utilisé l’algorithme génétique pour la sélection du facteur de tuilage et de déroulage des boucles et ils ont pu montrer que leur méthode peut fonctionner sur plusieurs différentes architectures. Jonathan2012Decouplingalgorithms ont aussi utilisé l’algorithme génétique pour permettre l’optimisation automatique des programmes écrits dans le langage Halide.

Cependant, les méthodes approchées peuvent dans certains cas donner des solutions qui ne sont pas proches de la solution optimale. Ceci est dû souvent au mauvais choix des hyperparamètres caractérisant les heuristiques et les métaheuristiques notamment. D’autre part, le choix de la méthode la plus appropriée au problème est souvent difficile.

ii.3.1.3 Approche d’apprentissage automatique

L’apprentissage automatique est utilisé pour concevoir des modèles de résolution aux principaux problèmes d’optimisation de code : le choix de la meilleure optimisation à appliquer, l’estimation des paramètres pour les optimisations sélectionnées et le choix de l’ordre d’application des optimisations. Les méthodes basées sur cette approche consistent à construire des modèles de prédiction utilisant différentes classes d’algorithmes d’apprentissage automatique. Elles prennent en entrée les caractéristiques du code à optimiser et donnent en sortie la prédiction associée Park2011IterativeCompilation.

A Caractéristiques du programme (program features)

Pour construire le modèle d’apprentissage, le concepteur doit décider les caractéristiques les plus représentatives du programme et qui aident le modèle à apprendre mieux pour donner des bonnes estimations. Les caractéristiques sont représentées sous forme d’une structure (vecteurs, graphes) identifiant distinctement les programmes : plus les caractéristiques sont précises et détaillées plus elles sont représentatives. La représentation sous forme de graphe permet de mettre en relief les dépendances entre les instructions. Par exemple KosekiGraphe ont utilisé les graphes comme structure de représentation pour trouver les bons facteurs de déroulage à appliquer sur les boucles. Or, la construction d’une structure volumineuse des caractéristiques est inefficace et peut ralentir les processus ML. Plusieurs projets basés sur l’apprentissage automatique utilisent des techniques d’extraction des caractéristiques des programmes : l’analyse statique des caractéristiques, l’analyse dynamique et l’analyse hybride Ashouri2018Survey.

a) Extraction statique : il s’agit d’extraire les caractéristiques d’un programme à partir de son code source seulement, sans prendre en considération les fonctionnalités du programme en cours de son exécution. Ces caractéristiques peuvent être simples comme le nom de la fonction où plus complexes telles que le nombre des opérations de chargement/stockage en mémoire dans les boucles. Il existe de nombreux extracteurs de caractéristiques de code source utilisés. Par exemple, le framework Milepost GCC milepost est utilisé en tant qu’un plugin pour le compilateur GCC pour extraire les caractéristiques du code source Ashouri2018Survey.

Agakov2006 focalisent sur l’optimisation de boucles, ils proposent 33 caractéristiques statiques pour décrire les boucles à optimiser (voir figure II.6 (a)), dans le but de sélectionner les cinq meilleures optimisations parmi un ensemble d’optimisations proposé (voir figure II.6 (b)).

Cependant, la caractérisation statique ne décrit pas suffisamment le programme, certains détails relatifs à l’exécution du programme ne sont pas pris en considération alors qu’ils influencent sur le choix des optimisations. L’utilisation de la caractérisation statique génère des modèles qui ne sont pas très précis GrapheModeling.

(a) Ensemble des caractéristiques
(b) Ensemble des optimisations
Figure II.6: Ensemble des optimisations et des caractéristiques utilisées dans la technique de Agakov2006.

b) Extraction dynamique : cette analyse consiste à collecter des informations sur le programme en cours d’exécution ce qui permet de caractériser son comportement dynamique sur l’architecture d’exécution. Elle est aussi utilisée pour déterminer comment plusieurs ressources du système sont utilisées. Les machines actuelles offrent des registres pour extraire les caractéristiques des programmes au cours d’exécution : le comportement du cache (nombre de défauts de cache, nombre de succès de cache), le nombre d’erreurs de prédiction de branchement, etc. Pour pouvoir aboutir à cette analyse, il faut exécuter le programme plusieurs fois pour extraire les caractéristiques nécessaires ce qui est gourmand en termes de temps d’exécution. D’autre part, la nature des registres qui diffèrent d’une machine à une autre rend cette analyse non portable entre les plateformes d’exécution. De ce fait, des chercheurs ont proposé d’autres façons de collection dynamique qui peut être portable sur plusieurs plateformes à condition qu’elles disposent de la même structure de jeu d’instructions. Cette nouvelle façon de caractérisation s’appelle l’instrumentation et elle est réalisée à l’aide des outils d’analyse dynamique de programmes Ashouri2018Survey.

Cavazos2007PerformanceCounters ont proposé un modèle basé sur ML (un modèle basé sur l’apprentissage automatique hors ligne777Il se fait en temps de compilation.) qui peut être utilisé pour prédire la bonne séquence d’optimisations. Leur méthode utilise cette analyse pour construire le vecteur de caractéristiques des programmes en entrée du modèle.

c) Extraction hybride : la caractérisation hybride consiste à combiner les deux techniques précédentes ce qui permet d’extraire plus d’informations sur le programme au cours d’exécution. Elle s’avère très intéressante car elle prend en considération les différents niveaux de caractérisation. GrapheModeling ont utilisé la caractérisation hybride pour construire un modèle de prédiction qui aide à choisir efficacement une bonne combinaison d’optimisations. Ashouri2018Survey ont aussi utilisé le framework MilePost milepost pour extraire les caractéristiques statiques et MICA888MICA est un outil pour l’extraction des caractéristiques dynamiques des programmes indépendantes de la machine comme la moyenne d’accès aux registres par instruction, la prédiction de branchement. pour extraire les caractéristiques dynamiques.

Cependant, il n’est pas évident de connaître les caractéristiques à retenir et qui aident le plus pour choisir des bonnes optimisations. Parfois, des algorithmes d’apprentissage automatique (tel que l’analyse en composantes principales ) sont utilisés pour réduire la dimension des caractéristiques et se restreindre aux caractéristiques fondamentales pour la construction du modèle Ashouri2018Survey.

B Algorithmes d’apprentissage automatique

Plusieurs algorithmes d’apprentissage automatique sont utilisés pour concevoir les modèles de prédiction des bonnes combinaisons d’optimisations. Ils sont classés en trois grandes catégories : apprentissage supervisé, apprentissage non supervisé et autres méthodes (y compris l’apprentissage par renforcement, techniques basées sur les graphes et méthodes statiques). Dans cette section nous allons exposer brièvement l’utilisation de certains algorithmes dde l’apprentissage automatique.

a) Apprentissage supervisé

L’apprentissage supervisé permet l’apprentissage d’une fonction à partir des données étiquetées de l’ensemble d’apprentissage : le modèle reçoit un ensemble d’exemples étiquetés en tant que données d’apprentissage pour apprendre à déterminer les étiquettes de classe pour les nouvelles instances non vues (unseen points). Les modèles linéaires et les SVMs999

SVM (Support Vector Machine ou Machine à vecteurs de support) appartient à la catégorie des classificateurs linéaires (qui utilisent une séparation linéaire des données).

ainsi que les arbres de décision et les forêts aléatoires sont les algorithmes les plus adoptés pour les problèmes de classification et de régression modélisant l’optimisation automatique du code.

  • [label=–]

  • Modèles linéaires et SVMs : les modèles linéaires, à savoir la régression linéaire, l’algorithme des K-plus proches voisins, etc. représentent les méthodes d’apprentissage supervisé les plus populaires. Ils sont généralement très stables i.e les sorties n’enregistrent pas des fluctuations importantes par rapport aux modifications mineures apportées à l’ensemble d’apprentissage. D’autre part, les SVMs utilisent une fonction noyau pour assurer une transformation non linéaire des données vers un espace intermédiaire (feature space). Ceci permet d’appliquer une classification linéaire qui sépare les points vers les différentes classes. Ils sont utilisés dans la construction des modèles pour estimer si une optimisation est bénéfique pour le programme, dans ce cas la sortie appartient à la classe "vrai". Dans le cas contraire la sortie sera "faux" Ashouri2018Survey.

  • Arbres de décision et forêts aléatoires (Decision Trees and Random Forests) : il s’agit de trouver un partitionnement des individus à représenter sous la forme d’un arbre de décision. L’objectif est de produire des groupes d’individus les plus homogènes possibles du point de vue de la variable à prédire qui prend une valeur binaire.

    Les forêts aléatoires d’arbres décisionnels désignent une famille de méthodes de classification, composée de différents algorithmes d’induction d’ensemble d’arbres de décision. La méthode commence par construire de nombreux arbres de décision au moment de l’apprentissage et fournit en sortie la classe correspondante dans le cas de la classification ou encore, la moyenne des classes dans le cas de la régression. Les forêts aléatoires viennent pour corriger le problème de surapprentissage des arbres de décisions. Monsifrot2002 ont utilisé des arbres de décision pour suivre le comportement de l’optimisation de déroulage afin de décider si l’application de l’optimisation de déroulage de boucles est bénéfique sur une architecture donnée101010Sur les machines UlraSPARC et IA-64..

  • Réseaux de neurones artificiels : les réseaux de neurones artificiels (RNA), ou Artificial Neural Network en anglais, sont des réseaux constituent d’un ensemble de couches dont chacune est constitue de nombreuses unités élémentaires appelées les neurones. Chaque couche calcule une sortie sur la base des informations qu’elle reçoit. Les réseaux de neurones artificiels constituent, entre autre, une alternative intéressante aux statistiques traditionnelles pour le traitement des données. Les réseaux de neurones sont une façon de construire des modèles paramétriques, c’est-à-dire pour lesquels la fonction objectif est explicite. Contrairement à d’autres algorithmes paramétriques comme la régression linéaire, ils permettent de construire facilement des modèles très complexes et non linéaires.

    Les réseaux de neurones sont souvent utilisés pour la construction de modèles de prédiction des optimisations de code. Kulkarni2012phaseOrderingANN ont proposé une technique qui utilise la neuro-évolution (NEAT)111111NEAT est un algorithme génétique pour la génération de réseaux de neurones artificiels (ANNs). Il représentent des modèles puissants pour l’apprentissage des problèmes complexes, car ils sont capables de changer la topologie du réseau et les paramètres de pondération pour trouver la fonction de coût (fitness) la plus équilibrée. pour construire un réseau de neurones artificiel capable de prédire un ordre des optimisations bénéfique pour une partie de code en cours d’optimisation.

b) Apprentissage non supervisé

L’apprentissage non supervisé regroupe l’ensemble des algorithmes d’apprentissage automatique permettant d’identifier des groupes d’objets ou d’individus similaires (clusters) à partir d’un ensemble de données sans en connaître au préalable la structure (à partir de données non étiquetées) ce qui les distinguent des algorithmes d’apprentissage supervisé.
Les méthodes de partitionnement de données (clustering) représentent la classe des algorithmes les plus utilisés pour l’apprentissage non supervisé, elles permettent de construire des classes automatiquement en se basant sur un critère de similarité entre les données. Le clustering est utilisé pour regrouper les nids de boucles indépendantes afin de préparer le programme à la phase d’optimisation. Il est également utilisé pour réduire l’espace d’optimisation. Par exemple, Martins2016Clustering proposent une technique basée sur le clustering, elle consiste à regrouper les fonctions d’un programme pour réduire l’espace d’exploration des combinaisons d’optimisations, pour chaque groupe, l’espace contient les optimisations précédemment suggérées pour les fonctions qu’il inclut.

Comme toutes les approches déjà exposées, les modèles basés sur le ML présentent des défis relatifs. D’une part, à leurs implémentations car la précision du modèle implique sa complexité. D’une autre part, des défis relatifs à la complexité de la phase d’apprentissage (training) des données, à savoir le type et la masse importante des données et les problèmes du mauvais apprentissage (le surapprentissage121212Le modèle s’adapte bien aux données de traitement (Training Set) et se généralisera mal pour d’autres données non déjà vues. ou encore le sous-apprentissage131313Le modèle s’adapte mal aux données de traitement (Training Set) et n’arrive même pas à capturer ses corrélations : le coût d’erreur en phase d’apprentissage reste grand. du modèle).

ii.3.1.4 Comparaison entre les approches d’exploration d’espace des optimisations

Dans le tableau I, nous comparons entre les différentes approches en se basant sur leurs degrés de précision et le temps de calcule pris. Cette comparaison vise principalement à mettre en relief les avantages et les inconvénients de chaque approche.

max width= Critère /Approche Degré de précision Temps de calcul Exhaustive Précision la plus élevée Exponentiel Basée sur les heuristiques et les métaheuristiques Précision moyenne comparée à l’exhaustive et dépend de la complexité du modèle Le temps est réduit comparativement à l’approche exhaustive et dépend de la taille du problème Basé sur le ML Précision moyenne comparée à l’exhaustive et dépend de la complexité de l’algorithme et les hyperparamètres du modèle assez réduit

Table I: Tableau comparatif entre les approches d’exploration d’espace d’optimisations.

L’approche exhaustive parcourt tout l’espace de recherche pour avoir la solution. Elle est simple, mais vu qu’elle prend beaucoup de temps, d’autres approches sont apparues. Les méthodes rapprochées (basées sur les heuristiques et les métaheuristiques) permettent de réduire l’espace de recherche par rapport à l’approche exhaustive. Cependant, elles sont lentes et ne garantissent pas de trouver la solution la plus optimale. L’approche basée sur l’apprentissage automatique vient pour permettre la génération des modèles d’une manière automatique grâce à l’apprentissage sur un grand ensemble de programmes pour assurer davantage de précision.

Il est à noter que l’hybridation entre les approches permet de bénéficier à la fois de plusieurs avantages des approches Agakov2006. Très souvent, il s’agit d’utiliser une approche pour résoudre une partie du problème comme la sélection ou l’ordre des optimisations, combinée avec une autre approche pour résoudre d’autres parties du problème comme l’estimation des bons paramètres pour les optimisations sélectionnées.

ii.3.2 Approches de sélection des bons paramètres des optimisations

Certaines optimisations de boucles nécessitent un ou plusieurs paramètres qui influencent sur l’efficacité de l’optimisation. La définition de ces paramètres dépend de différents critères, à savoir la hiérarchie mémoire, la complexité du cache, la taille et le nombre des registres vectoriels, etc. D’autre part, l’interdépendance et l’interaction entre les optimisations peuvent être renforcées à cause des paramètres choisis, la figure II.7141414Pour la multiplication de matrices sur une architecture Pentium II et UltraSPARC. montre qu’un léger écart par rapport aux "bons" paramètres des optimisations de tuilage de boucles et de déroulement peut entraîner une augmentation considérable du temps d’exécution voire même un ralentissement par rapport au programme initial iterativeCompilationFactors.

Figure II.7: Variation du temps d’exécution en fonction des paramètres de tuilage et du déroulement de boucles iterativeCompilationFactors.

L’espace des paramètres des optimisations peut être intégré dans l’espace des optimisations, la technique d’optimisation automatique effectue alors une exploration générale de l’espace résultant, cette approche prend en considération l’interaction entre les paramètres des différentes optimisations. Cependant, l’espace résultant est considérablement grand. La technique risque d’être complexe et lente. De ce fait, les recherches de pointe se sont orientées vers une approche "séparatrice" entre l’espace des optimisations et l’espace de leurs paramètres, traitant ainsi séparément le problème de sélection/ordre des optimisations et le problème d’estimation des bons paramètres pour les optimisations sélectionnées.

Des modèles ont été conçus afin de prédire le meilleur paramètre à utiliser pour une ou plusieurs optimisations en entrée, Les approches adoptées sont principalement : (1) l’approche empirique (estimation par exécution), (2) l’approche statique basée sur un modèle analytique et (3) l’approche basée sur l’apprentissage automatique.

ii.3.2.1 Approche empirique (estimation par exécution)

Dans cette approche, une exploration de l’espace des paramètres est effectuée et pour chaque solution une version du programme est générée, ensuite exécutée. Le paramètre permettant d’enregistrer le meilleur temps d’exécution est sélectionné. La bibliothèque d’optimisation automatique des programmes d’algèbre linéaire (ATLAS) effectue une recherche empirique afin d’obtenir le meilleur paramètre dans des plages de valeurs pour des optimisations pré-élaborées.

iterativeCompilationFactors ont utilisé la compilation itérative afin de choisir le meilleur facteur de tuilage et de déroulage de boucles. Cependant, la taille de l’espace de recherche impose des limites sur cette approche. Par exemple, il est impossible de l’utiliser pour trouver le paramètre de tuilage sur des espaces rectangulaires car l’espace de recherche pour le tuilage de boucle rectangulaire est exponentiellement plus grand que celui de tuilage carré Yuki.

ii.3.2.2 Approche statique basée sur un modèle analytique

Le modèle analytique est conçu en se basant sur l’architecture matérielle et de certains détails sur le comportement d’un ensemble de programmes. Ce modèle statique préconçu donne directement en sortie le paramètre d’une optimisation appliquée sur un programme donné pour une combinaison (architecture, compilateur) spécifique Yuki.

Les modèles visent principalement à améliorer l’utilisation de la mémoire cache et les registres. Par exemple, SarkarTileAnalytic ont présenté un algorithme à temps constant qui estime le facteur de tuilage le plus optimal pour les nids de boucles. Ils ont formulé une fonction du coût sous forme d’une équation quadratique qui dépend de la taille du problème et de la taille du cache. Ils ont pu essayer tous les candidats pour les minima locaux de cette fonction en un temps constant.

L’inconvénient majeur de cette approche est la nature statique des modèles analytiques. Les modèles analytiques sont développés sur la base d’une analyse détaillée du programme et de l’architecture. Lorsqu’un nouveau facteur de l’architecture est ajouté, les modèles doivent être mis à jour suite à une analyse exhaustive de ce nouveau facteur. Ceci est très coûteux, car cela nécessite des connaissances spécialisées et éventuellement change toute la conception du modèle Yuki.

ii.3.2.3 Approche basée sur l’apprentissage automatique

Dans cette approche, un modèle est conçu grâce à des algorithmes d’apprentissage automatique. Cette approche permet de mettre en place plusieurs techniques de pointe d’estimation des paramètres d’optimisations.

Des modèles basés sur la classification utilisant l’algorithme des K-plus proches voisins (KNN) et les SVMs ont été proposés comme outils d’estimation du meilleur facteur de déroulage de boucles, le modèle considère un sous ensemble des paramètres après une restriction en se basant sur les caractéristiques du programme en entrée. Yuki ont utilisé l’apprentissage supervisé, à savoir un réseau de neurones artificiel pour prédire le meilleur facteur de l’optimisation de tuilage. La technique prend en entrée un vecteur de caractéristiques du programme et donne en sortie l’estimation du meilleur paramètre du tuilage de boucles (voir la section II.4.2). Xiaoming2008Matrix ont proposé un système de classification basé sur le machine learning pour optimiser la multiplication de matrices. Leur système permet de déterminer le nombre de niveaux de tuilage et la taille de tuilage à chaque niveau selon la plateforme ciblée.

ii.3.3 Approches d’estimation de coût

L’estimation du niveau de performance atteinte, suite à l’application des optimisations, est un facteur décisif pour mesurer l’efficacité de ces optimisations sur le programme. Le coût d’application d’une combinaison d’optimisations est relatif aux objectifs151515Autres objectifs tels que la taille du code ou la consommation d’énergie. visés notamment le temps d’exécution du programme optimisé qui représente le principal objectif définissant le coût de l’application des optimisations. De ce fait, tout au long de cette rubrique, le coût des optimisations fera référence exclusivement au temps d’exécution du programme optimisé.

Pour estimer le coût, plusieurs approches ont été proposées, il n’existe pas de meilleure approche puisque chacune adopte un compromis entre certaines contraintes, à savoir la précision, le temps d’exécution et la complexité de l’approche d’estimation mendis2018ithemal.

ii.3.3.1 Estimation par exécution

Cette approche permet d’estimer le coût en exécutant réellement le programme. Elle est souvent utilisée dans des domaines exigeant une précision assez élevée. Le principe de cette approche est simple : chaque instance de programme optimisé générée161616Code avec ses optimisations. par l’unité d’exploration d’espace des optimisations, sera compilée et exécutée pour mesurer réellement son temps d’exécution qui sera renvoyé vers une unité de comparaison et de décision afin de classer cette instance par rapport aux autres. Ce processus est répété autant de fois qu’une nouvelle instance est générée. Le critère d’arrêt peut être après un nombre prédéfini d’itérations ou après avoir atteint un coût d’optimisation prédéfini Knijnenburg2002. La figure II.8 illustre le principe de cette approche.

Figure II.8: Principe général de l’approche d’estimation par exécution.

En théorie, cette approche peut être utilisée pour n’importe quel ensemble d’optimisations de compilateur. Cependant, il est recommandé de l’utiliser pour des espaces d’exploration qui ont été déjà réduits. Plusieurs techniques d’optimisation automatique adoptent cette approche telles que les techniques de compilation itérative171717Les transformations successives d’optimisation sont appliquées sur un programme, leurs coûts sont déterminés par l’exécution réelle du code résultant. Plusieurs versions différentes du programme sont générées et exécutées, la version la plus optimale en termes de temps d’exécution est sélectionnée. Knijnenburg2002. Elle est aussi très utilisée pour estimer le coût pour des parcours partiels d’espace de paramètres pour certaines optimisations, notamment les facteurs de tuilage de boucles et du déroulage de boucles iterativeCompilationFactors. Elle est également utile dans les cas où l’architecture visée change, car cette stratégie n’impose pas de connaissances sur les plateformes d’exécution.

Cependant, cette approche impose des restrictions parfois bloquantes dans la pratique, les espaces de recherche peuvent être extrêmement grands, d’ailleurs c’est le cas le plus fréquent, ce qui impose un prétraitement de sélection pour restreindre l’espace de recherche. De ce fait, l’efficacité de cette approche dépend étroitement de la technique d’exploration de l’espace des optimisations.

ii.3.3.2 Estimation basée sur des méthodes analytiques

Cette approche est basée sur la conception d’un modèle analytique qui peut être un algorithme ou une fonction mathématique capable de prédire automatiquement le coup d’application d’une optimisation sans avoir besoin de l’exécuter.

Le principe général d’un prédicteur analytique consiste à construire une fonction prenant en entrée un tuple , où est le vecteur caractérisant le programme (soit ) à optimiser, représente une des combinaisons d’optimisations possible à appliquer sur . La sortie du modèle est le coût prévu que la séquence T devrait apporter suite à son application sur le programme Ashouri2018Survey. La figure II.9 résume le fonctionnement général de cette approche.

Figure II.9: Principe général de l’approche d’estimation du coût utilisant le modèle analytique.

Le modèle est synthétisé par des expressions mesurant le coût en se basant sur les caractéristiques statiques ou dynamiques du code en entrée (voir section II.3.1.3 (A)) ainsi que la taille des données qui influence considérablement le modèle. Le temps d’exécution total est décomposé en général comme suit :

est le temps de calcul passé par le processeur lui-même, est le temps nécessaire pour accéder aux hiérarchies de la mémoire, est le temps de communication interprocessus (les threads) et est le temps écoulé pour effectuer les . Chaque terme de cette équation constitue une formule mathématique exprimée en fonction de valeurs d’entrée du programme, de caractéristiques du programme et éventuellement de quelques paramètres caractérisant la machine cible en se basant, très souvent, sur la documentation du fournisseur Cascaval2000AnalyticTimeEstimation.

Chaque sous-système de la formule principale est traité en détail. En effet, pour estimer , le sous-système compte le nombre d’opérations exécutées dans les unités fonctionnelles du CPU. Les opérations ensuite sont regroupées en classes (blocs) dont l’estimation du coût d’exécution est définie selon les modèles proposés des architectures d’exécution. Le terme est estimé en comptant le nombre des accès pour chaque niveau de la hiérarchie mémoire dont la pénalité est calculée selon différents modèles. Ces derniers prennent en considération les caractéristiques de chaque niveau de la hiérarchie mémoire et se basent essentiellement sur le microbenchmarking181818Des programmes benchmarks utilisés pour estimer les performances du compilateur et de la plateforme d’exécution. Smith1995MemoryTimeMesuring.

Par ailleurs, les architectures modernes ont des organisations internes très complexes. Les modèles de machine simplifiés, qui prennent en compte des abstractions des architectures réelles, fournissent des estimations de performances souvent très approximatives. En effet, plusieurs contraintes s’opposent à l’approche analytique :

  • [label=–]

  • Extension des micro-opérations : chaque instruction est étendue aux micro-opérations dans le processeur. Ainsi, les pipelines, les dépendances et la gestion des ressources se produisent au niveau des micro-opérations, ce qui représente un niveau plus granulaire que les instructions considérées dans les modèles analytiques.

  • Exécution dans le désordre et les architectures superscalaires : les processeurs qui adoptent une exécution dans le désordre191919En anglais out of order execution consiste à réorganiser l’ordre dans lequel les instructions vont s’exécuter dans le processeur. Ces instructions ne sont alors pas forcément exécutées dans l’ordre dans lequel elles apparaissent dans le programme. exploitent la structure de dépendance des données d’un bloc de base pour mapper son ordre d’exécution optimisant le parallélisme des micro-opérations au niveau des instructions. Les unités d’exécution superscalaires202020Un processeur est dit superscalaire s’il est capable d’exécuter plusieurs instructions simultanément parmi une suite d’instructions. Pour cela, il comporte plusieurs unités de calcul et il est capable de détecter l’absence de dépendances entre instructions. permettent l’exécution simultanée de plusieurs instructions d’une même opération. Le problème d’estimation du coût qui en résulte est donc non linéaire.

  • Caractéristiques non spécifiées : la documentation vague de certaines caractéristiques micro-architecturales pose un défi supplémentaire. Par exemple, lorsque l’exécution dans le désordre est spécifiée, la taille du tampon de réorganisation peut ne pas l’être. Ce qui mène à considérer des estimations vagues pour les modèles proposés mendis2018ithemal.

ii.3.3.3 Estimation basée sur l’apprentissage automatique

Cette approche permet de prédire le coût des optimisations sans exécution du programme optimisé. En général, l’approche prend en entrée un tuple est une structure caractérisant le programme (voir section II.3.1.3 (A)) et est l’une des séquences possibles d’optimisations à appliquer, la sortie ( soit ) est le coût du tuple estimé grâce à un ou plusieurs algorithmes de l’apprentissage automatique.

Figure II.10: Principe général de l’approche d’estimation basée sur l’apprentissage automatique.

Pour concevoir l’estimateur, plusieurs algorithmes du machine learning ont été utilisés. Nous exposons une approche basée sur les réseaux de neurones artificiels qui ont donné de bon résultats d’estimation dans plusieurs techniques de pointe.

Les modèles basés sur les réseaux de neurones exploitent les avantages qu’ils offrent. En effet, les réseaux de neurones permettent de prédire le temps d’exécution d’un programme en entrée dont les caractéristiques sont modélisées sous forme d’une structure (vecteur, graphe, etc.). La phase d’apprentissage (training) permet au réseau d’apprendre en corrigeant ses différents coefficients. L’ensemble de données d’apprentissage (DataSet) est composé des couples de (caractéristiques de , temps d’exécution de ) avec un programme généré aléatoirement (pour éviter le surapprentissage ou encore le sous-apprentissage du modèle).

Le passage d’une couche du réseau à une autre se fait grâce à des fonctions dont le choix influence sur les performances du modèle, la profondeur et le nombre des nœuds dans chaque couche présentent aussi des hyperparamètres critiques, ces deniers peuvent être réglés grâce à des tests de performance du modèle effectué pour chaque hyper-paramètre. La figure II.11 résume les principales composantes du modèle.

Figure II.11: Vue d’ensemble sur le modèle d’estimation du coût basé sur les réseaux de neurones.

Rahmani2010TSS, dans leur technique de sélection du meilleur paramètre de l’optimisation du tuilage de boucles (TSS), ont utilisé les réseaux de neurones pour estimer le temps d’exécution des programmes. Le modèle de prédiction est constitué de trois couches. Il reçoit en entrée les caractéristiques du programme ainsi que les trois facteurs de tuilage et donne en sortie le temps d’exécution prédit du programme pour chaque facteur. Cette estimation permet donc de sélectionner le meilleur facteur de tuilage.

D’autres techniques de pointe basées sur l’utilisation des réseaux de neurones utilisent les réseaux de neurones récurrents (RNNs) pour prédire le débit d’un ensemble d’instructions de bloc de base (voir la section II.4.3). Le bloc d’instruction est représenté par un graphe orienté acyclique (DAG212121Directed Acyclic Graph un graphe orienté qui ne possède pas de circuit. Un tel graphe peut être vu comme une hiérarchie.). Ce mécanisme de représentation est inspiré des techniques de traitement de langage naturel.

Les modèles basés sur l’apprentissage automatique ont pu atteindre des performances remarquables. Cependant, ces modèles sont très sensibles aux hyperparamètres qu’ils utilisent, les ensembles de données d’apprentissage (problème de sous- apprentissage et surapprentissage) ainsi que la définition des caractéristiques représentatives du programme ( Features engineering). La conception de ces modèles doit être méticuleusement étudiée au niveau de chaque phase.

ii.3.3.4 Comparaison entre les approches d’estimation de coût

Afin de comparer les trois approches d’estimation du coût, il faut préciser les critères à considérer. Il s’agit de la précision, le temps d’exécution et la complexité de l’estimateur.

  • [label=–]

  • La précision : l’estimateur du coût doit être précis. Tout le processus du choix des optimisations dépend de cette estimation. En effet, à une échelle d’ordre aussi petite, l’erreur doit être minimale car si l’estimateur présente un taux d’erreur important, la prédiction des bonnes optimisations sera erronée et la méthode d’automatisation sera par la suite fausse.

  • Temps d’exécution de l’estimateur : L’estimateur doit être rapide. En effet, un estimateur fait partie de tout un processus d’automatisation. L’exploration d’espace de recherche, qui est assez grand, risque de prendre un temps important. Si de plus, l’exécution de l’estimateur prend un temps considérable, la méthode d’automatisation sera trop lente et inutile.

  • Complexité de l’estimateur : l’estimateur ne doit pas être trop complexe, c’est-à-dire que l’estimateur ne doit pas dépendre de plusieurs prétraitements et paramètres qui risquent d’influencer sa précision. D’autre part, la complexité d’un estimateur implique l’intervention de plusieurs contributeurs et nécessite très souvent des outils plus complexes.

Pour concevoir une méthode d’optimisation automatique, le choix de l’approche d’estimation du coût se fait selon les contraintes favorisées. La taille de l’espace des optimisations à appliquer et leurs paramètres peuvent orienter le concepteur. Ceci impose des limitations sur la précision de l’approche. De ce fait, plusieurs travaux ont adopté une phase de présélection pour réduire l’espace de recherche OptimSpace. Le tableau II résume la comparaison entre les trois approches d’estimation de coût.

max width= Approche / critère La précision Temps d’exécution complexité Par exécution Très élevée et présente un référentiel pour les autres approches Très élevé dans le cas de grands espaces d’optimisation Simple à concevoir Basée sur des méthodes analytique Moins élevée et dépend de la complexité de la méthode et la définition des modèles d’architectures Très réduit Plus le modèle est précis plus il est complexe Basée sur l’apprentissage automatique Elevée mais dépend de la complexité du modèle réduit Complexe(dépendance aux hyperparamètres, de données d’apprentissage, etc.)

Table II: Comparaison entre les trois approches d’estimation du coût.

ii.4 Exemples de techniques d’optimisation automatique

Plusieurs techniques d’optimisation automatique ont été intégrées dans les compilateurs. Elles ont permis d’apporter des améliorations remarquables sur leurs fonctionnements et ce, sur différents niveaux, à savoir l’accélération de l’exploration d’espace des optimisations, la sélection des meilleurs paramètres des optimisations et l’estimation du coût des programmes optimisés. Dans cette section, nous allons exposer trois techniques. Chacune propose une solution pour automatiser un de ces trois niveaux.

ii.4.1 Autoscheduler de Halide basé sur l’approche analytique

L’auto-scheduler Mullapudi2016HalidAutoScheduler est une technique de génération automatique des Schedules222222Équivalent à "Planning" en français, ce terme représente l’ensemble des optimisations à appliquer sur le code Halide.. Elle permet de résoudre le problème du temps perdu lors de la compilation et d’exécution de chaque schedule candidat pour un espace de recherche immense. Il s’agit alors d’une estimation des performances de chaque schedule. En entrée, l’auto-scheduler reçoit le programme à optimiser, des informations complémentaires sur le programme tel que l’étendue des boucles de chaque fonction et la taille estimée des images ainsi que l’architecture de la machine (taille du registre vectoriel, coût du chargement mémoire) (voir la figure II.12).

Figure II.12: Les entrées et la sortie de l’Auto-Schedule de Halide.

Le processus de l’Auto-scheduler passe par quatre phases. D’abord, la phase de pré-calcul de chaque fonction, à savoir l’estimation de son coût arithmétique. Ensuite, l’Auto-scheduler forme des groupes de fonctions232323Une fonction Halide est équivalente à une boucle imbriquée dont la profondeur est supérieure ou égale au nombre de variables manipulées par cette fonction. liées par des relations producteur-consommateur242424Le résultat de calcul de la fonction productrice est utilisé dans le calcul de la fonction consommatrice., chaque groupe est optimisé indépendamment afin d’augmenter la localité et maximiser la réutilisation de données dans chaque groupe. Il s’agit d’appliquer des transformations de tuilage sur chaque fonction et de définir les points optimaux pour fusionner les fonctions producteur-consommateur. Enfin, il faut définir l’ordonnancement des groupes pour assurer une meilleure localité. Puis, dérouler les fonctions, les vectoriser et enfin paralléliser les boucles externes des fonctions dans chaque groupe. L’algorithme suivant résume les phases du modèle de l’auto-scheduler Mullapudi2016HalidAutoScheduler.

Résultat : Schedule du programme P
1 G = {};
2 pour (fonction dans ) faire
3       Tuiler et la mettre dans un groupe singleton ;
4       ;
5      
6 fin pour
7tant que (Il existe des groupements potentiels avec un gain dans ) faire
8       Soit le groupement potentiel avec le plus petit coût (plus grand gain);
9       coût _fusion le coût de la fusion de et ;
10       coût _Non_fusion le coût lors de l’absence de fusion entre et ;
11       si  (coût _fusion coût _Non_fusion)  alors
12             Ne pas fusionner et ;
13      sinon
14             Fusionner et ;
15       fin si
16      Mettre à jour avec et dans le même groupe;
17       pour  (groupe dans )  faire
18             Soit la fonction de sortie de ;
19             Choisir la meilleure interversion de boucle pour : celle qui améliore la réutilisation des données dans ;
20             Dérouler et vectoriser deux niveaux de boucles internes de petite étendue de ;
21             Paralléliser le niveau de boucle externe de ;
22            
23       fin pour
24      
25 fin tq
Algorithme 1 Pseudo algorithme de l’Auto-Scheduler
Figure II.13: Comparaison entre les temps d’exécution de quelques benchmarks optimisés manuellement et par Auto-Schedule de Halide Mullapudi2016HalidAutoScheduler.

L’Auto-Scheduler de Halide a été testé par un ensemble de 14 benchmarks différents sur la même architecture d’exécution

252525Intel Xeon E5-2620 v3 CPU.. Dans 8 parmi 14 des benchmarks, la version optimisée à la main a donné des performances meilleures que celle optimisée par l’Auto-Scheduler, mais les écarts entre les performances sont réduits, l’Auto-Scheduler reste très compétitif à l’optimisation manuelle (voir la figure II.13).

ii.4.2 Modèle automatique pour le problème de Tile Size Selection (Tss)

Une bonne estimation des paramètres des optimisations améliore profondément l’accélération qu’elles peuvent apporter. Yuki présentent une technique de création des modèles assez précis de prédiction des meilleurs paramètres pour l’optimisation de tuilage de boucles (TSS)262626Tile Size Selection est le problème de sélection du meilleur paramètre pour l’optimisation de tuilage des boucles. en se basant sur les réseaux de neurones artificiels. La technique a été conçue pour une classe de programmes spécifiques souvent utilisés dans l’algèbre linéaire (des nids de boucles d’une profondeur allant jusqu’à trois dimensions et des matrices de données à deux dimensions). Le principe de cette technique est résumé dans les deux sections suivantes.

ii.4.2.1 Caractéristiques du code considérées

Le tuilage de boucles vise principalement à maximiser la réutilisation des données et à éviter les accès mémoire coûteux. La technique prend en entrée des caractéristiques des programmes qui décrivent la localité temporelle et spatiale en s’appuyant sur l’aspect du pré-chargement (prefetching) qui consiste à récupérer les données avant la demande en parallèle avec le chargement des données voisines, ce qui permet de gagner un temps d’exécution considérable.

Les caractéristiques sont basées sur le nombre de références dans les instructions les plus internes du nid de boucle. Les références sont classées en trois types : les références pré-chargées272727(Prefetched references) qui sont les références bénéficiant de la localité spatiale créée grâce au prefetcher., les références non pré-chargées282828(Non-prefetched references.), à savoir les références qui ont besoin de la localité temporelle pour obtenir de bonnes performances. et les références constantes dans la boucle la plus interne (invariant), c’est-à-dire celles qui sont réutilisées pour toutes les itérations de la boucle la plus interne. Chaque type de référence est ensuite classé selon son mode d’accès : en lecture ou en écriture, ce qui fait un total de six types de caractéristiques.

ii.4.2.2 Phases de conception et principe de fonctionnement

La technique utilisée comporte les quatre étapes suivantes :

  • [label=–]

  • Génération aléatoire des programmes correspondant à la classe de programmes définie.

  • Collecte de données qui consiste à extraire les caractéristiques des programmes et les exécuter pour concevoir l’ensemble d’apprentissage, et ce, sur différentes architectures.

  • Entraînement du modèle TSS grâce à l’ensemble d’apprentissage en réduisant l’erreur de prédiction. Les entrées du réseau de neurones sont données à la première couche cachée et les sorties de chaque couche sont données à la couche suivante. Les sorties des couches cachées sont en fonction de la somme pondérée des entrées de cette couche, où la fonction est la tangente hyperbolique. La couche de sortie effectue la somme pondérée des sorties de la dernière couche cachée, mais n’applique pas la fonction de tangente hyperbolique. Compte tenu des pondérations (coefficients synaptiques) de la couche de sortie, de la dernière couche cachée , de la sortie souhaitée et du nombre de données d’apprentissage , chaque nœud de la couche de sortie tente à minimiser l’erreur calculée par de l’équation .

  • Une fois le modèle TSS conçu, il peut être utilisé en tant que partie du compilateur pour prédire les tailles de tuilage optimales sur le plan interne, ou en tant que partie d’une méthode d’exploration pour trouver les tailles de tuilage optimales.

ii.4.3 Ithemal, estimateur du coût

Ithemal (Instruction Throughput Estimator using Machine Learning) est un estimateur de débit d’un ensemble d’instructions de bloc de base à l’aide de l’apprentissage automatique, il peut être intégré dans les compilateurs afin d’estimer le temps d’exécution pour choisir les meilleures optimisations du code.

Ithemal utilise une nouvelle approche basée sur un réseau de neurones récurrents sous forme de graphe acyclique dirigé (DAG-RNN292929Recurrent Neural Networks. en anglais, c’est une classe de réseaux de neurones artificiels ayant des connexions récurrentes, qui dote le réseau de mémoire.). Il modélise l’estimation du débit à l’aide d’un réseau de neurones profonds (DNN). Il adopte donc une approche guidée par les données303030Data Driven Approach en anglais, regroupe les différentes approches de résolution qui se basent sur l’étude des données. ce qui permet de basculer facilement d’une microarchitecture à une autre avec un minimum de paramétrage manuel mendis2018ithemal.

ii.4.3.1 Architecture et fonctionnement d’ithemal

La technique permet d’estimer le débit d’un bloc de base d’instructions en assembleur. Les blocs passent par trois principales phases : la canonicalisation, le plongement et la prédiction (voir la figure II.14).

Figure II.14: Architecture globale d’Ithemal mendis2018ithemal.
A canonicalisation (canonicalization)

Dans cette phase, Ithemal prend un bloc en assembleur spécifié en tant que texte (syntaxe Intel) et le mappe en tant que texte aussi à une liste d’instructions. Chaque instruction consiste en un code opération, une liste d’opérandes de source et une liste d’opérandes de destination. La canonicalisation rend explicite les opérandes qui sont généralement implicites dans la représentation en code assembleur. La figure II.15 expose un exemple de cette transformation.

Figure II.15: La phase de la canonicalisation d’Ithemal mendis2018ithemal.
B Plongement (Embedding)

Cette phase prend un bloc de base canonique et produit une représentation du bloc de base compréhensible par un réseau de neurones. En effet, les réseaux de neurones prennent typiquement en entrée une séquence d’entrées à valeurs réelles. Dans les domaines structurés, tels que le texte ou, comme dans notre domaine, les programmes et les entrées sont de nature discrète (tels que les mots et les blocs de base). Il est donc nécessaire de mapper chaque entrée structurée sur une représentation pouvant être consommée par un réseau de neurones. Ithemal mappe un bloc de base sur un graphe acyclique dirigé avec des vecteurs à valeurs réelles comme contenu de chaque nœud. Chaque nœud du graphe correspond à une instruction du bloc de base. Chaque nœud est associé à un vecteur (à n dimensions) à valeur réelle. Un arc dirigé lie un nœud à un nœud si l’instruction de dépend de (déterminée grâce à l’analyse des opérandes de source de et des opérandes de destination de ). La création du vecteur à dimensions pour chaque nœud est basée sur la technique de traitement de langage naturel pour mapper une séquence de jetons textuels (en l’occurrence le code opération, les opérandes de source et les opérandes de destination) en une représentation détaillée sous la forme d’un vecteur mendis2018ithemal.

C Prédiction

La phase de prédiction prend en compte le graphe acyclique dirigé d’un bloc de base et prédit son débit. Les réseaux de neurones récurrents (DAG-RNN) sont les plus adéquats pour cette structure en entrée. Le DAG-RNN d’Ithemal parcourt le graphe dans l’ordre topologique, en calculant une représentation vectorielle profonde à valeur réelle de chaque sous-graphe connexe dans le graphe. Étant donné les vecteurs de chaque sous-graphe, Ithemal réduit ces vecteurs en un seul vecteur, puis effectue la prédiction à l’aide d’une régression linéaire.

ii.4.3.2 Utilisation du réseau de neurones récurrents (Rnn)

Un RNN prend en entrée une séquence de vecteurs et produit en sortie une séquence de vecteurs. Un RNN est dit récurrent si son exécution est définie de manière récursive tout au long de la séquence. Les nœuds interconnectés interagissent non-linéairement. Les unités sont reliées par des arcs (synapses) qui possèdent un poids. La sortie d’un neurone est une combinaison non linéaire de ses entrées. À chaque étape, le RNN applique une cellule

313131Cell en anglais est une fonction avec des paramètres internes apprenables qui, lorsqu’ils sont évalués à la position dans la séquence, consomment un vecteur d’entrée et un vecteur d’état caché , à partir de la position précédente, puis produisent un nouveau vecteur caché (vecteur d’état ). pour produire le vecteur de sortie de cette étape. Le calcul des cellules dépend du vecteur de sortie de l’étape précédente. Le dernier vecteur qu’un RNN calcule résume donc la séquence entière. Dans son implémentation, Ithemal utilise une cellule LSTM323232Long Short-Term Memory (LSTM) est un type de réseaux de neurones récurrents. LSTM qui mémorise de manière sélective les informations qui lui ont été transmises par la cellule précédente mendis2018ithemal.

La dernière étape d’Ithemal consiste à calculer l’estimation de débit à l’aide d’une régression linéaire. Etant donné un vecteur d’état caché réduit obtenu après la réduction de la sortie du RNN, Ithemal calcule la prédiction du débit par la formule , où est le vecteur de paramètres et est le biais.

Conclusion

Dans ce chapitre, nous avons expliqué les différentes approches adoptées pour résoudre les trois principaux problèmes liés à l’optimisation automatique, à savoir la sélection des meilleures combinaisons d’optimisations, l’estimation des meilleurs paramètres pour les optimisations sélectionnées et l’estimation du coût d’application des optimisations (le temps d’exécution en l’occurrence).

Chacune des approches présente des avantages et des défis. Une bonne définition des spécifications du problème donne plus de priorité à certaines contraintes, ce qui oriente le choix de l’approche à utiliser.

Les techniques d’optimisation automatique du code dépendent très souvent du compilateur visé. Dans la partie suivante, nous allons exposer le compilateur Tiramisu qui fera sujet de notre contribution par la suite.

Introduction

Générer des codes efficaces dédiés aux systèmes de hautes performances devient de plus en plus difficile. Ceci dû aux contraintes d’hétérogénéité des plateformes d’exécution d’une part et de la complexité des algorithmes utilisés d’une autre part.

Afin de remédier à ces difficultés, maints langages spécifiques au domaine (LSD) ont été proposés. Ces langages définissent des spécifications pour répondre en particulier aux contraintes d’un domaine d’application précis.

Tiramisu est un Langage Spécifiques au Domaine développé par l’équipe de recherche COMMIT de MIT Baghdadi2018TiramisuV3 capable de générer des codes optimisés dans lesquels les optimisations n’affectent pas le fonctionnement ni la lisibilité du code grâce à la séparation entre les algorithmes et les optimisations appliquées.

Dans ce chapitre nous allons présenter le langage et le compilateur Tiramisu en exposant ses avantages. Nous donnons une vue d’ensemble sur la compilation des codes en Tiramisu afin de mettre en relief l’effet de la représentation du code sur son optimisation et aussi sur sa portabilité. Ensuite, nous expliquons sa logique en exposant comment les algorithmes sont définis et comment les optimisations sur le code sont introduites.

iii.1 Tiramisu

Tiramisu est un LSD (Languages Spécifiques au Domaine) embarqué111Langage spécifique à un domaine défini en se basant sur un "langage hôte" plus puissant à usage général. il s’appuie sur l’infrastructure du langage hôte (analyse syntaxique, vérification typographique, modularité), le langage hôte peut être utilisé pour la métaprogrammation (l’écriture de programmes manipulant des programmes en DSL). sur le C++ permettant d’exprimer des algorithmes dits de données parallèles222Ces algorithmes sont appelés des algorithmes de données parallèles car leur parallélisme provient d’opérations simultanées sur de grands ensembles de données, plutôt que de plusieurs threads de contrôle. qui utilisent des tableaux denses et des nids de boucles. Ces algorithems sont souvent utilisés dans des systèmes de hautes performances, à savoir l’algèbre linéaire dense, l’algèbre tensorielle, le traitement d’images et les réseaux de neurones (RNCs). Tiramisu a été conçu afin de couvrir quatre principales caractéristiques d’optimisation de codes Malek2017ExtendingTiramiu qui sont :

  • [label=–]

  • Cibler différentes architectures matérielles.

  • Gérer la dépendance de données. En effet l’optimisation d’un programme est limitée par les dépendances entre les représentations des données en mémoire, notamment dans le cas des programmes ciblant différentes architectures.

  • Générer un code optimisé de hautes performances. En effet, les programmeurs doivent optimiser leurs codes manuellement ou encore automatiquement afin d’obtenir des résultats comparables aux codes optimisés soigneusement par des experts du domaine d’optimisation.

  • Assurer une représentation compréhensible et lisible du code optimisé. Très souvent, l’application des optimisations sur un programme risque d’affecter sa représentation, obombrer sa logique voire même la changer, d’où la nécessité d’une vérification du programme après optimisation.

iii.1.1 Modèle en couche de Tiramisu

Tiramisu est basé sur le modèle polyédrique333Permet d’avoir une représentation mathématique abstraite pour modéliser un programme. Chaque instruction du programme est représentée par 3 principales informations : domaine d’itération, relations d’accès (en lecture, écriture) et un Schedule. Pour davantage de détails voir PRADELLE2011polyedrique.polyhedral. Il utilise une représentation intermédiaire (RI)444Représentation intermédiaire que prend le code de haut niveau. multicouche assurant une séparation complète entre l’algorithme pur, les optimisations de code, le mappage et la structuration des données ainsi que la gestion de la communication et la synchronisation. L’avantage de cette représentation gît essentiellement dans la séparation entre les couches, ce qui définit un ordre spécifique dans lequel les optimisations sont appliquées. Cette représentation garantit que le compilateur passe d’une couche à une autre sans vérifier les modifications ou annuler des décisions déjà prises dans des couches précédentes.

En effet, l’optimisation de code pour différentes architectures est restreinte par certains facteurs liés notamment à la dépendance en mémoire. Toutes les opérations de synchronisation, de communication et de mappage des données vers les différentes hiérarchies mémoire ne doivent pas être effectuées avant l’application des optimisions de code. Pour illustrer cette complexité, prenons l’exemple du mappage des buffers vers la mémoire partagée et la mémoire cache des GPU, les quantités de données à transmettre et la synchronisation des opérations dépendent étroitement des optimisations de code appliquées comme les niveaux de l’optimisation de tuilage de boucles.

Tiramisu sépare la représentation du code en quatre couches que nous détaillons dans les sections suivantes.

iii.1.1.1 Couches d’algorithme abstrait

Dans cette première couche, l’algorithme pur est spécifié indépendamment de la localité temporelle et spatiale : aucun ordre des calculs n’est donné et aucune restriction sur le stockage des données n’est imposée. Les valeurs sont communiquées grâce à une relation producteur-consommateur.

iii.1.1.2 Couche de gestion des computations

Cette couche permet de définir l’ordre d’exécution des calculs, l’architecture d’exécution cible ainsi que les optimisations à appliquer sans pour autant préciser la structuration de données. Ceci, facilite considérablement l’application des différentes optimisations. En effet, les transformations requises pour l’application des optimisations sont plus souples puisque elles ne nécessitent pas des transformations complexes sur la structure de données.

iii.1.1.3 Couche de gestion de données

Au niveau de cette couche, les emplacements mémoire pour stocker des valeurs calculées sont définis grâce aux commandes de mappage des données, à savoir l’allocation/libération des tampons et les relations d’accès aux données en lecture ou en écriture pour chaque opération de calcul. Les mappages de données possibles en Tiramisu sont représentés par des structures de tableaux, des tableaux de structures et des tableaux multidimensionnels réduits en tableaux de dimensions minimales ou en scalaires.

iii.1.1.4 Couche de gestion de communication

Les commandes de synchronisation et de communication sont ajoutées au niveau de cette couche. Il s’agit d’annoter d’une dimension de temps la représentation obtenue après la couche de gestion de données. Ceci est réalisé grâce à des commandes de synchronisation spécifiées manuellement par l’utilisateur. La Figure III.1 donne une vue globale sur Tiramisu en mettant en relief le flux entre les différentes couches de la représentation intermédiaire en Tiramisu.

Figure III.1: Vue d’ensemble sur Tiramisu  Baghdadi2018TiramisuV3.
iii.1.2 Avantages de Tiramisu

Le compilateur Tiramisu permet une application plus souple des différentes optimisations de code et sur des niveaux de code séparés, spécifiques et ordonnés Malek2017ExtendingTiramiu. Ceci est grâce à la séparation entre l’algorithme et les optimisations à appliquer sur le code. De ce fait, Tiramisu offre la possibilité de tester les différentes combinaisons d’optimisations sur le même code ou encore d’automatiser cette exploration, ce qui n’est pas du tout facile dans d’autres langages comme le langage C.

En effet, la composition de deux optimisations nécessite la réécriture de l’optimisation résultante. Ceci s’avère complexe à fortiori dans le cas de plusieurs optimisations. Tiramisu permet une simple composition des optimisations dans une partie séparée complètement de l’algorithme sous forme de commandes d’optimisations. Cette séparation permet d’empêcher la réécriture du résultat de la composition des optimisations, la gestion se fait automatiquement par Tiramisu.

Pratiquement, tous les autres compilateurs polyédriques imposent des restrictions afin d’assurer que les codes après optimisations sont justes. Tiramisu permet de vérifier la validité des optimisations appliquées grâce à l’analyse de dépendance. De ce fait, il permet d’utiliser sans restrictions des optimisations souvent considérées difficiles à appliquer notamment sur des espaces d’itération non rectangulaires, ou encore sur des graphes de flux de données cycliques.

Actuellement, Tiramisu est capable de générer des codes optimisés pour différentes architectures matérielles à savoir, CPU, GPU, FPGA ou encore des systèmes distribués. Ceci en utilisant la même syntaxe tout en s’assurant de tirer profit des avantages qu’offre chaque architecture.

iii.2 Programmer en Tiramisu

Tiramisu est un générateur de code. L’objectif d’un programme Tiramisu est de générer des codes censés être appelés à partir d’autres programmes (les programmes utilisateurs). Chaque programme Tiramisu commence par l’initialisation du compilateur Tiramisu. Cela permet aussi de définir le nom de la fonction. Cette fonction sera donc appelée dans un programme appelant (wrapper) qui peut être écrit en Tiramisu ou encore dans un autre langage Ray2018DistributedTiramiu.

Afin d’assurer une représentation compréhensible des programmes écrits en Tiramisu, il est recommandé d’organiser le code en deux principales sections correspondant à la forme générale du modèle en couches de la représentation intermédiaire. Dans la première étape, le programmeur définit l’algorithme pur. Dans la deuxième étape, (le Schedule555Équivalent à "Planning" en français, ce terme représente l’ensemble des optimisations à appliquer sur le code, Schedule est le terme à utiliser tout au long du rapport.) il décrit comment le code sera optimisé. Ensuite, il détermine les allocations et le stockage des résultats dans les buffers. Et pour clore chaque programme Tiramisu, il faut lancer la commande de génération du code. Pour décrire une vue globale, un code Tiramisu peut être représenté par le schéma de la figure III.2.

Figure III.2: Schéma global de code en Tiramisu.
iii.2.1 Algorithmes dans Tiramisu

Dans cette première partie, le programmeur décrit la logique de son algorithme grâce à des instructions particulières appelées computations. Ces dernières représentent des nids de boucles (voir la section I.2.1) dont la profondeur est égale au nombre des variables (itérateurs) qui lui ont été affectées. Chaque itérateur affecté à un niveau de boucle possède une borne fixée lors de sa déclaration.

Une computation peut être vue comme étant une expression associée à un domaine d’itération. L’expression représente le calcul à effectuer. Le domaine d’itération est défini grâce aux bornes affectées aux itérateurs du domaine Baghdadi2018TiramisuV3. Dans la figure III.5, la computation permet d’exprimer une boucle d’une profondeur de deux avec et comme itérateurs du domaine ayant la borne et respectivement.

(a) Code C
(b) Code Tiramisu équivalent
Figure III.5: Code Tiramisu équivalent à une boucle d’une profondeur de deux.

Tiramisu est destiné aux algorithmes de données parallèles qui manipulent les tableaux denses et les nids de boucles. Ainsi, les programmes Tiramisu sont constitués de plusieurs computations chacune assure des traitements particuliers dans une boucle "pour" (for) imbriquée, souvent de grande profondeur. Notons que seules les boucles "pour" et la structure conditionnelle peuvent être exprimées. Les boucles while et les goto ne sont pas encore utilisables en Tiramisu, ce qui oriente d’autant la spécialisation au domaine caractérisant les algorithmes exprimés en Tiramisu Baghdadi2018TiramisuV3.

L’ordre d’exécution des computations est indépendant de l’ordre de leurs déclarations. C’est au niveau de la partie Schedule où le véritable ordre est défini et donc les relations entre les computations sont prescrites. La définition de l’ordre des computations ne doit pas briser les relations producteur – consommateur qui existent entre les computations.

Par exemple, pour calculer le produit de deux matrices et soit , puis calculer la somme du résultat et la matrice D soit . les deux computations présentent une relation de producteur – consommateur, avec C comme producteur et E comme consommateur. La figure III.8 représente le code Tiramisu (partie algorithme) équivalent. Notons que pour initialiser les inputs, il faut créer des computations dédiées de type Input.

(a) Pseudo Algorithme
(b) Code Tiramisu équivalent
Figure III.8: Code Tiramisu d’un produit suivi d’une somme de deux matrices.

Dans cette première partie du code, seuls les traitements relatifs à la logique de l’algorithme sont définis, aucune spécification est donnée sur l’ordre d’exécution ni sur la structuration des données ni encore sur les optimisations à appliquer. Pour définir ces critères, l’utilisateur doit les préciser dans la partie Schedule Ray2018DistributedTiramiu.

iii.2.2 Schedule dans Tiramisu

Tiramisu propose un ensemble de commandes de scheduling666Commandes de planification en Français, nous optons pour le terme "scheduling" pour désigner Planification tout au long du rapport. de haut niveau pour définir l’ordre des computations et optimiser leurs exécutions. Les commandes d’optimisation permettent d’effectuer des transformations sur le domaine d’itération d’une façon transparente pour le programmeur. Cette opération facilite la combinaison de commandes d’optimisation. Le programmeur prescrit pour chaque computation, les commandes d’optimisation à appliquer avec les paramètres nécessaires (voir figureIII.9).

Figure III.9: Forme générale d’application des commandes de Scheduling sur une computation.

Les commandes de scheduling sont classées en quatre principaux types : les commandes de transformation des nids de boucles, les commandes pour mapper les niveaux des boucles sur l’architecture matérielle, les commandes de manipulation des données et les commandes de synchronisation Baghdadi2018TiramisuV3. Pour davantage de détails, nous proposons dans l’annexe B une description des types de commandes de scheduling. Le tableau LABEL:table_Commandes_Tiramisu dans l’annexe B expose quelques commandes de scheduling, en définissant pour chaque commande sa syntaxe d’application, ses paramètres, son principe et son type également.

iii.2.2.1 Amélioration de l’optimisation du code en Tiramisu

L’optimisation de code en Tiramisu est basée sur un ensemble de commandes paramétrables de haut niveau assurant la flexibilité et le contrôle total au programmeur. Or, la spécification d’un Schedule optimal manuellement nécessite d’avoir une bonne expertise et d’effectuer plusieurs tests sur les optimisations choisies pour différents paramètres.

La définition automatique du Schedule s’avère intéressante pour alléger cette tâche. D’ailleurs, plusieurs compilateurs des langages spécifiques au domaine proposent une gestion automatique des optimisations tel que Pencil Baghdadi2015Pencil et Halide Kelly2013Halide. Ce dernier, étant proche à Tiramisu puisque il sépare entre l’algorithme et le Schedule, propose un auto-scheduler générant automatiquement le Schedule susceptible d’être le meilleur. Le compilateur Tiramisu supporte la scalabilité de ses fonctionnalités et permet d’adopter des méthodes pour gérer la partie Schedule automatiquement.

Conclusion

Tiramisu est un nouveau langage qui offre l’avantage de séparer entre l’algorithme, les optimisations à appliquer et la structuration des données. Ceci permet de gérer des codes rapides visant plusieurs architectures matérielles.

Dans ce chapitre, nous avons présenté le compilateur Tiramisu, expliqué sa logique et ses fondements. Nous avons aussi exposé les principales notions relatives à la programmation en Tiramisu, à savoir la séparation entre les parties de l’algorithme et de Schedule.

La partie état de l’art est close. Nous entamons par la suite la partie contribution, dans laquelle nous allons expliquer en détails les différentes phases de conception, implémentation et tests du système proposé.

Introduction

Notre contribution s’inscrit dans le cadre des projets de recherche lancés par l’équipe COMMIT du laboratoire CSAIL111CSAIL : laboratoire d’informatique et d’intelligence artificielle (Computer Science and Artificial Intelligence Laboratory). à MIT visant à optimiser le compilateur Tiramisu.

L’objectif principal de notre travail est la réalisation d’un modèle de prédiction des meilleurs facteurs de l’optimisation de déroulage (loop unrolling) pour des programmes déjà optimisés (manuellement ou automatiquement) ou encore pour des programmes naïfs sans aucune optimisation préalable. Le modèle permet donc d’automatiser le choix du meilleur facteur de déroulage pour faciliter la tâche d’optimisation et améliorer le temps d’exécution du programme. La classe des programmes Tiramisu visée dans notre travail présente un taux assez élevé d’opérations de chargement mémoire considérées comme opérations gourmandes en temps d’exécution.

Dans ce chapitre, une description détaillée du problème traité est donnée suivie des détails sur les différentes phases de conception de notre solution.

iv.1 Description du problème

Dans le chapitre III, une explication détaillée de la structuration de programmes Tiramisu est donnée. Pour rappeler, les programmes en Tiramisu sont composés de deux principales parties : la première partie contient le code de l’algorithme et la deuxième partie (Schedule) contient l’ensemble des optimisations à appliquer sur le programme (voir la figure IV.1).

Figure IV.1: Structure d’un programme en Tiramisu.

Différentes combinaisons d’optimisations peuvent être appliquées dans la partie Schedule. Les optimisations appliquées peuvent prendre des facteurs comme l’optimisation de déroulage de boucles, l’optimisation de tuilage, etc. Le programmeur doit sélectionner les optimisations ainsi que les meilleurs facteurs à appliquer pour améliorer le temps d’exécution.

iv.1.1 Portée de la contribution

Notre contribution se focalise sur l’optimisation de déroulage de boucle (voir section I.2.7 et l’annexe A pour plus de détails sur l’optimisation de déroulage de boucles) qui améliore les performances dans pratiquement tous les cas si elle est appliquée d’une manière significative, à savoir appliquée avec un bon facteur David1994Compilertransformations.

Le tableau III montre la différence entre le temps d’exécution pour trois facteurs de déroulage (8, 16, 32) appliqués sur le même programme présenté dans la figure IV.1

facteur de déroulage sans unrolling 8 16 32 Temps d’exécution (ms) 43.2417 40.4118 32.4826 34.4535

Table III: Les temps d’exécution d’un programme optimisé avec trois différents facteurs (8, 16, 32) de la transformation de déroulage.

Actuellement, en Tiramisu, la définition du bon facteur de déroulage se fait manuellement. Le programmeur exécute le programme pour différents facteurs de déroulage et choisit le facteur qui donne un temps d’exécution meilleur. L’exécution du programme pour les différents facteurs coûte énormément de temps. Il s’agit d’explorer exhaustivement les différents facteurs et d’exécuter pour chaque facteur plusieurs exécutions afin de mesurer le temps pris pour chaque configuration (voir figure IV.2).

Figure IV.2: Processus du choix manuel du meilleur facteur de déroulage.

L’objectif principal de notre travail est d’automatiser le choix du meilleur facteur de déroulage et d’éviter l’exécution répétitive. La solution proposée doit permettre de prédire le meilleur facteur de déroulage pour un programme donné qui peut être déjà optimisé ou naïf sans aucune optimisation préalable. Pour atteindre cet objectif, il est nécessaire de définir une formalisation (codification) du problème ainsi qu’une fonction objectif.

iv.1.2 Formulation du problème

Considérer le problème traité comme étant un problème d’optimisation combinatoire222Les problèmes d’optimisation combinatoire traitent le choix d’une meilleure alternative dans un ensemble très grand mais fini d’alternatives dites solutions réalisables. La solution permet de satisfaire une fonction objectif. Une évaluation est associée à toute solution réalisable à l’aide d’une fonction dont la minimisation/maximisation définit la fonction objectif. permet de définir le cadre formel du problème. Soit le problème d’optimisation traité, caractérisé par un ensemble réalisable ou admissible non-vide et une fonction qui associe un scalaire dans à chaque élément (solution réalisable) pour un programme donné. Soit le programme à optimiser, est composé de deux partie : la partie qui constitue l’algorithme et la partie qui constitue les optimisations (Schedule). Le programme est représenté par un ensemble de caractéristiques .

est donc l’ensemble des facteurs de l’optimisation de déroulage de boucles (loop unrolling) du programme , tel que : . Les paramètres et dépendent de . Ils donnent les contraintes qui permettent de définir l’ensemble des solutions réalisables du problème.

La fonction associe à chaque solution réalisable pour le programme son temps d’exécution. Résoudre le problème revient à trouver parmi les solutions réalisables, une qui minimise i.e. trouver une solution telle que pour tout élément . Une telle solution est dite optimale, c’est la solution qui permet de minimiser le temps d’exécution de .

La fonction objectif est définie comme suit:

iv.1.3 Choix conceptuels étudiés

Afin de proposer une conception qui vérifie aux objectifs du projet, il est important de répondre à certaines questions pour mieux cerner nos choix conceptuels :

  • [noitemsep,topsep=0pt,parsep=0pt,partopsep=0pt, leftmargin=2.4mm]

  • Quel est l’espace de recherche à considérer ?

  • Quelle approche à adopter pour la prédiction du meilleur facteur de déroulage ?

  • Quel est le type de sortie à prédire ?

iv.1.3.1 Espace de recherche

Les facteurs de déroulage sont des valeurs discrètes (des entiers) qui doivent être des multiples de deux. L’optimisation de déroulage fait partie d’un Schedule regroupant plusieurs autres optimisations comme le tuilage (Loop tiling). Les facteurs de l’optimisation de tuilage appliqué dans Tiramisu sont impérativement des puissances de deux. Si nous appliquons le déroulage avec autres facteurs que les multiples de deux, des problèmes lors de la compilation dans Tiramisu peuvent apparaître. D’autre part, selon les experts, les facteurs de déroulage qui donnent les meilleures performances varient entre 2 et 64. Le facteur 64 est le plus grand facteur exploré manuellement jusqu’à maintenant.

Comme nous allons prédire automatiquement, nous pouvons aller au-delà de cette valeur. Cependant, d’après les tests que nous avons effectués, les valeurs qui dépassent 128 détériorent les performances, et risquent de créer des bugs dans certaines architectures d’exécutions.

En effet, l’optimisation de déroulage réplique le code plusieurs fois selon le facteur donné, elle crée autant d’instructions que le facteur de déroulage. Donc, de grands facteurs augmente la taille du code. Ainsi, les instructions ne peuvent pas être chargées d’une façon optimale dans le cache, les registres aussi ne seront pas exploités efficacement, ce qui détériore les performances au lieu de les améliorer (pour davantage de détails voire l’annexe A).

Conclusion : l’espace de recherche que nous devons explorer est représenté par les valeurs discrètes paires , avec : < < avec = 128  et = 0  ( 0 représente le cas où l’optimisation de déroulage n’est pas appliquée).

iv.1.3.2 Approche de prédiction du facteur de déroulage

La définition de l’approche d’exploration d’espace de recherche constitue une phase de conception cruciale. Dans la section II.3, plusieurs approches ont été exposées : la recherche exhaustive, les heuristiques et les métaheuristiques ainsi que les approches analytiques. Elles ont été déjà utilisées dans plusieurs travaux traitant ce problème. Chacune présente des avantages et des inconvénients. Le concepteur doit favoriser soit la précision ou le temps d’exécution de la méthode.

L’apprentissage automatique a prouvé son efficacité dans la résolution de plusieurs problèmes. Il propose un compromis entre la précision et le temps d’exécution de la méthode. La disponibilité d’un nombre de données suffisant et la puissance de calcul des machines actuelles ont permis d’intégrer ces techniques dans divers problèmes complexes d’optimisation dans les compilateurs.

En effet, supervised2005 et MLAforLoopUnrollingFactorPredictionInHighLevelSynthesis ont exploré les méthodes d’apprentissage automatique pour le choix du meilleur facteur de déroulage et ils ont pu atteindre une bonne précision (jusqu’à 60%). Les méthodes précédentes utilisent des techniques basées sur les réseaux de neurones peu profonds (Shallow Neural Networks) ou basées sur l’apprentissage automatique classique.

De notre part, nous voulons explorer un nouvel angle du problème de prédiction du meilleur facteur de déroulage par apprentissage automatique afin d’améliorer la précision. Nous avons opté pour les réseaux de neurones profonds qui ont fait preuve d’une bonne précision dans divers domaines. Nous souhaitons donc les explorer pour résoudre notre problème.

De plus, contrairement aux techniques classiques d’apprentissage automatique, les réseaux de neurones profonds ont la particularité de créer automatiquement des caractéristiques haut niveau (high-level features) à partir des caractéristiques bas niveau (low-level features) que nous allons utiliser pour représenter les programmes.

Conclusion : le modèle de prédiction du facteur de déroulage est appuyé sur l’approche basée sur l’apprentissage automatique, plus précisément les réseaux de neurones profonds (pour plus de détails sur les réseaux de neurones voir annexe C).

iv.1.3.3 Classe de réseau de neurones profond

Le théorème du no-free-lunch montre qu’aucun algorithme ou modèle résout parfaitement tous les problèmes. Dans notre cas, le choix du type de réseau de neurones dépend de plusieurs paramètres notamment la nature des contraintes du problème ainsi que les données d’apprentissage. La meilleure approche de sélection du type de réseaux de neurones est d’essayer d’identifier dans le modèle des contraintes présentes dans le problème traité, puis tester les types qui sont plus susceptibles de résoudre le problème. Nous avons effectué une étude comparative entre les trois classes de réseaux de neurones les plus stables et utilisées (voir annexe C), le tableau IV résume les résultats obtenus.

Classe Principales caractéristiques Type de données Réseaux de neurones multicouche (MLPs) [noitemsep,topsep=0pt,parsep=0pt,partopsep=0pt, leftmargin=2.4mm] Composés d’une ou plusieurs couches Chaque couche est composée d’un nombre variable de neurones Les entrées sont connectées aux sorties à travers des couches intermédiaires Réseau à propagation directe [noitemsep,topsep=0pt,parsep=0pt,partopsep=0pt, leftmargin=2.4mm] Données sous forme tabulaire. Entrées de taille fixe. Réseaux de neurones convolutifs (CNNs) [noitemsep,topsep=0pt,parsep=0pt,partopsep=0pt, leftmargin=2.4mm] Combinaison de couches, chacune représente une fonctionnalité du réseau : couche de convolution, couche de pooling et couche entièrement connectée [noitemsep,topsep=0pt,parsep=0pt,partopsep=0pt, leftmargin=2.4mm] Données matricielles. Données de type image. Données ayant des relations spatiales333Les points de données d’une unité de données sont liés de tel sorte qu’ils ne peuvent pas être séparés ni modifiés indépendamment car cela implique que l’unité de donné est corrompue(ex. données audio). Traitement d’images et d’audio Réseaux de neurones récursifs (RNNs) [noitemsep,topsep=0pt,parsep=0pt,partopsep=0pt, leftmargin=2.4mm] Constitués d’unités (neurones) interconnectées interagissant nonlinéairement qui présentent au moins un cycle dans la structure. Propagation de données dans les deux sens. [noitemsep,topsep=0pt,parsep=0pt,partopsep=0pt, leftmargin=2.4mm] Données séquentielles (ex. texte). Les entrées peuvent être de tailles variables. Ils ne sont pas appropriés aux données tabulaires ou de type image. Traitements de langage naturel (NLP).

Table IV: Comparaison entre les différentes classes de réseaux de neurones candidates.

Les entrées de notre modèle sont de tailles variables, si nous avons par exemple une boucle avec quatre niveaux et une autre avec deux niveaux, nous aurons au moins deux entées supplémentaires pour la boucle à quatre niveaux.

Les RNNs supportent les entrées de tailles variables, ils sont utilisés pour des données séquentielles, ce séquencement est défini par le temps. Les RNNs utilisent la sortie prédite par l’entrée précédente et l’entrée actuelle pour produire la sortie actuelle. Cependant nos données ne présentent aucun séquencement, chaque entrée de notre dataset est indépendante de l’autre. En effet, le facteur de déroulage prédit pour le programme ne sera pas utilisé avec les caractéristiques du programme pour prédire son meilleur facteur de déroulage. L’architecture des RNNs ne convient pas à notre problématique.

La taille variable des entrées peut être fixée à une taille maximale, en utilisant la technique du rembourrage où l’entrée est remplie par des valeurs bidons jusqu’à atteindre la taille maximale. Le rembourrage ou le padding permet de résoudre le problème de la taille variable des données en entrée des réseaux de neurones. Le zéro padding est la solution la plus reconnue pour la résolution du problème de la taille variable des entrées. Cependant, d’autres valeurs peuvent être utilisées. Généralement les deux valeurs "0" et le "-1". Le « -1 » peut remplacer le "0" si le "0" présente une valeur significative dans les données d’entrée du modèle.

Conclusion : selon le type de problème traité et la représentation des caractéristiques des programmes constituant les données utilisées, les Réseaux de neurones multicouche MLPs semblent être la solution la plus convenable.

iv.1.3.4 Type de sortie du modèle

Il existe plusieurs types de sorties possibles pour un modèle de prédiction de déroulage. Le modèle peut prédire le temps d’exécution des instances de programmes en prenant en entrée les caractéristiques du programme. Une fonction doit estimer le minimum des temps d’exécution pour les facteurs dans l’espace d’exploration. Le modèle doit aussi apprendre cette fonction. Cependant, dans le cas des réseaux de neurones, la fonction est difficile à entraîner et elle risque très souvent d’être piégée par les optimums locaux.

L’autre sortie possible est le facteur de déroulage optimal lui-même ; trouver le meilleur facteur de déroulage directement permet de remédier au problème des optimums locaux.

Conclusion : nous avons décidé de prédire directement le meilleur facteur de déroulage.

iv.1.3.5 Classification ou régression ?

Le modèle de prédiction du meilleur facteur de déroulage peut représenter un problème de classification ou un problème de régression.
a) Classer les programmes selon le facteur optimal de déroulage : la classification444Le problème de classification consiste à attribuer à chaque individu (objet) une classe ou une étiquette. est utilisée lorsque la variable de sortie est une valeur discrète qui représente une catégorie (les différents facteurs de déroulage : 2, 4, 6, 8, etc. dans notre cas). Cependant, la classification ne permet de classer les programmes que dans un ensemble prédéfini de classes. Il faut avoir suffisamment de programmes qui couvrent les différentes classes afin d’atteindre une bonne prédiction pour des nouveaux programmes.
b) Utiliser une fonction continue (Régression555Le problème de régression consiste à prédire une valeur réelle à partir d’un ensemble d’entrées.) : un problème de régression se pose lorsque la variable de sortie est une valeur réelle ou continue. Dans notre problème, le modèle de régression renvoie des valeurs réelles que nous devons arrondir en valeurs entières multiples de deux (les facteurs doivent être des entiers multiples de deux). Si la sortie est un nombre impair, il faut prendre soit son successeur ou son prédécesseur. En revanche, le nombre choisi risque de ne pas être la valeur la plus convenable pour le programme en entrée ce qui influence négativement la précision du modèle.

D’une autre part, la théorie des réseaux de neurones montre qu’ils sont aussi puissants dans la régression que dans la classification. Cependant, dans la pratique, il y a quelques différences en termes de précision. Dans la classification, il faut uniquement décider la classe convenable à l’entrée. Mais, les problèmes de régression sont plus difficiles car il s’agit de prévoir une certaine valeur pour chaque entrée.

Par conséquent, l’utilisation des réseaux de neurones pour les problèmes de régression peut être moins stable comparativement aux problèmes de classification. Il est généralement préférable de convertir les problèmes de régression en problèmes de classification lorsque cela est possible. Cependant, les réseaux de neurones restent toujours très puissants pour traiter des problèmes de régression, la difficulté serait dans l’optimisation des traitements du modèle. En effet, quelques techniques d’optimisation s’avèrent difficiles à appliquer dans les cas des modèles de régression (ex.dropout666Le décrochage, ou abandon est une technique de régularisation permet une suppression temporaire de neurones afin d’éviter le surapprentissage.).

Conclusion : La classification est la solution la plus convenable à notre problème, il faut juste s’assurer que l’ensemble de données d’apprentissage couvre toutes les valeurs de l’espace de recherche défini.

iv.2 Conception globale du système

Afin de donner une vue globale de la solution proposée, nous allons exposer l’architecture globale du système proposé, définir la classe des programmes visée et décrire les différents composants de la solution.

iv.2.1 Architechture globale du système

L’utilisateur définit la partie algorithme et éventuellement la partie Schedule contenant les différentes optimisations possibles hormis l’optimisation de déroulage de boucle (loop unrolling). L’utilisateur appelle le modèle de prédiction proposée, ce dernier analyse le code et décide le meilleur facteur de déroulage à appliquer. L’architecture globale du système est présentée dans la figure IV.3.

Figure IV.3: Architechture globale du système.
  • Module d’extraction des caractéristiques de programmes : ce module permet d’extraire les caractéristiques (features) du programme en entrée . L’ensemble des caractéristiques est constitué essentiellement des informations sur les niveaux de boucles, les opérations effectuées ainsi que les caractéristiques des optimisations (Schedule ) appliquées. Ceci permet de représenter les programmes Tiramisu distinctement. Plus de détails sont données dans la section IV.3.1.

  • Module de prédiction du facteur de déroulage: le module prend en entrée les caractéristiques extraites par le module précédent pour prédire le meilleur facteur de déroulage . Ce facteur est utilisé par le compilateur Tiramisu pour compléter le Schedule du programme.

iv.2.2 Classe de programmes visée

Tiramisu est dédié aux programmes dits de données parallèles qui utilisent des matrices denses et des nids de boucles. Notre équipe travaille sur la génération de programmes optimisés de calculs scientifiques comme l’algèbre linéaire dense. La classe des programmes visée est constituée de boucles à contrôle affine ACLs (Affine Contol loops), à savoir les bornes des nids de boucles et les adresses des accès en mémoire sont définies comme étant des fonctions affines des itérateurs de boucles et des paramètres constants.

Dans cette classe de programmes, nous avons visé des nids de boucles parfaitement imbriquées (voir section I.2.1) qui présentent des chargements de données en mémoire multiples et intenses. Nous focalisons notre travail sur les programmes composés d’une seule computation, à savoir un seul nids de boucles qui n’a pas été fusionné avec d’autres nids de boucles. La figure IV.4 donne un exemple de la classe des codes utilisés.

Le choix de cette classe de programmes est basé sur la nature des domaines visés par Tiramisu. En effet, cette classe présente des noyaux pour divers programmes dédiés aux calculs scientifiques. De plus, les chargements mémoire sont des opérations coûteuses en temps d’exécution, l’amélioration en temps d’exécution apportée grâce à l’application de déroulage de boucle avec un bon facteur est remarquable.

Figure IV.4: Exemple de programme Tiramisu appartenant à la classe de code visée.
iv.2.3 Caractéristiques des programmes

Un programme en Tiramisu représente un ensemble de nids de boucles parfaitement imbriqués appelés computations (voir chapitre III). Le module d’extraction des caractéristiques représente chaque computation par un ensemble de caractéristiques synthétiques . Dans certains travaux précédents, un nombre considérable des caractéristiques est utilisé. Ils utilisent des caractéristiques décrivant l’effet de l’architecture d’exécution sur les nids de boucles ce qui augmente le nombre des caractéristiques extraites. Cependant, un grand nombre de caractéristiques risque de nuire la prédiction du modèle. En effet, sélectionner les caractéristiques les plus significatives permet d’optimiser la prédiction du meilleur facteur de déroulage.

Initialement, nous avons opté pour un grand nombre de caractéristiques. Ensuite, nous avons gardé les caractéristiques qui influencent le plus la prédiction. L’abstraction adoptée résume les critères influençant le temps d’exécution d’une computation indépendamment de l’architecture d’exécution ce qui offre plus de portabilité au modèle. Elle décrit principalement la structure, les opérations et les optimisations appliquées sur le nid de boucles.

Les caractéristiques des optimisations appliquées sur une computation peuvent être classées en deux principales classes : optimisations locales et optimisations globales. Les optimisations locales agissent uniquement sur la structure du nid de boucles sur lequel elles sont appliquées telles que l’optimisation de tuilage de boucles, déroulage de boucles, inversion de boucles, parallélisation, etc. Les optimisations globales agissent sur l’ensemble des computations, elles mettent en relation plusieurs computations telles que la fusion de nids de boucles, ordonnancement d’ordre de calcul des opérations dans les nids de boucles (after(), before(), compute_at(), etc.).

Le tableau V donne un sous-ensemble des caractéristiques des programmes considérées.

Caractéristiques de la structure du nid de boucles Nombre de niveaux du nid de boucles. L’étendu de chaque niveau de boucle. Nombre de dépendances entre les niveaux du nid de boucles. Liste des dépendances entre les niveaux du nid de boucles.777Pour chaque niveau de boucle, exprimer les dépendances sous forme d’un vecteur d’itérateurs, si la définition du domaine d’itération (représentée par les contraintes sur les bornes de l’itérateur) présente des dépendances avec d’autres itérateurs. L’estimation de l’étendu de la boucle s’il n’est pas une valeur constante. La précédence du niveau de boucle par un prédicat de test (if statement). Caractéristiques des opérations du nid de boucles Le niveau de boucle dans lequel l’opération est effectuée. Le rang d’exécution de l’opération dans le niveau de boucle qui lui a été affecté. Nombre de variables/invariants utilisés dans les opérations. Histogramme des opérations par type de données.888L’histogramme des opérations représente le nombre d’opérations par type de données. C’est une matrice dont les lignes sont les types d’opérations (opérations arithmétiques, opérations d’accès mémoire, max, etc.) et les colonnes sont les types de données utilisés (Integer, Float et Boolean, avec les différentes modalités pour chaque type). Histogramme des chargement/sauvegarde en mémoire par type de données. Liste des niveaus de boucles définissant chaque accès mémoire.999Pour chaque opération d’accès mémoire, nous définissons la succession des itérateurs des niveaux de boucles utilisés. Par exemple, l’accès est représenté par la liste des itérateurs . Nombre d’unités de données (Quantité de données) chargées par niveau. Nombre des appels externes dans chaque niveau de boucle. Caractéristiques du Schedule (Optimisations) Les niveaux de boucle sur lesquels l’optimisation est appliquée. Facteurs utilisés pour chaque optimisation. Liste des computations dans le cas des optimisations globales.

Table V: Sous-ensemble des caractéristiques données par le module d’extraction des caractéristiques d’une computation.

iv.3 Conception détaillée

Dans cette section, nous allons exposer les détails de la conception des modules composant notre solution. En effet, la solution proposée est basée sur deux principaux modules : le module d’extraction des caractéristiques des programmes et le module de prédiction du facteur de déroulage.

iv.3.1 Module d’extraction des caractéristiques des programmes

Ce module permet d’extraire les caractéristiques des unités composant les programmes Tiramisu, à savoir les nids de boucles (computations). L’extraction des caractéristiques d’une computation passe par deux phases, la première phase consiste à extraire les caractéristiques décrivant la structure du nid de boucles, l’ensemble des opérations effectuées ainsi que les types des données. La deuxième phase consiste à enregistrer les caractéristiques des optimisations appliquées (schedule) sur le nid de boucles pour ensuite, les utiliser dans la mise à jour des caractéristiques de la structure du nid de boucles.

iv.3.1.1 Extraction initiale des caractéristiques de la computation

Afin d’extraire initialement les caractéristiques définissant la structure d’une computation, le module commence par parcourir la liste des itérateurs puis les expressions (opérations) Tiramisu associées au nid de boucles. L’expression associée à une computation en Tiramisu est un arbre n-aire dont les nœuds sont des expressions aussi (voir figure IV.5). Dans notre cas, les expressions qui nous intéressent sont de type opération (les opérations arithmétiques et les opérations d’accès mémoire). L’exploration de l’arbre qui représente l’expression permet d’extraire les caractéristiques des opérations effectuées dans le nid de boucles.

Figure IV.5: représentation d’une expression en Tiramisu.

L’optimisation de déroulage est fortement sensible à l’étendu de chaque niveau du nid de boucle, il influence également l’effet des opérations effectuées notamment les chargements mémoire101010Les chargements mémoire représentent la classe d’opérations la plus utilisée dans les programmes visés.. Pour accentuer cette relation nous avons défini des caractéristiques décrivant la quantité de donnée ("mots mémoire selon le type de données) chargée pour chaque variation d’un niveau de boucle. D’autre part, ces caractéristiques, décrivant les accès mémoire, dépendent de l’ordre des itérateurs lors des accès comparativement à l’ordre des niveaux de la computation(voir l’algorithme 2).

Résultat : définir la caractéristique de quantité de données pour chaque niveau de boucle de la en entrée
1 pour (iterator dans )  faire
2       pour (access_operation dans )  faire
3             ;
4             soit la liste des itérateurs de l’opération ;
5             soit la liste des itérateurs de la dont le niveau est supérieur au niveau de ;
6             pour (iterator dans )  faire
7                   si  ( )  alors
8                         ;
9                   fin si
10                  
11             fin pour
12            
13       fin pour
14      /* sauvegarder le nombre total d’accès de */ ;
15      
16 fin pour
Algorithme 2 Pseudo algorithme de l’extraction de la caractéristique de quantité de données chargées par niveau de boucle
iv.3.1.2 Mise à jour et exportation des caractéristiques de la computation

Suite à l’application de chaque optimisation dans la partie Schedule du programme, le module d’extraction doit enregistrer cette optimisation en gardant trace de son type, ses facteurs et les niveaux de boucles sur lesquels elle a été appliquée. L’optimisation modifie la structure du nid de boucles d’où la nécessité de mettre à jour les caractéristiques de la computation. Par exemple l’application de l’optimisation de tuilage de boucle change d’une part, les étendus des niveaux des boucles ainsi que leurs ordres. D’une autre part, elle modifie les caractéristiques relatives à l’opération d’accès.

Après la mise à jour des caractéristiques, le module les prépare sous un format utilisable par le réseau de neurones. Il s’agit d’une représentation vectorielle où chaque caractéristique unitaire (nombre de niveaux, nombre des opérandes, type de donnée, etc.) représente une entrée (input) au réseau de neurones.

Le module d’extraction est composé de trois sous modules : un module d’extraction initiale des caractéristiques de chaque computation, un second module pour l’extraction des caractéristiques du schedule et la mise à jour des caractéristiques de la computation après l’application du schedule et un troisième module pour l’exportation des caractéristiques sous le format utilisable par le réseau de neurones (voir figure IV.6)

Figure IV.6: Diagramme de classe du module d’extraction des caractéristiques des programmes.
iv.3.2 Modèle de prédiction du meilleur facteur de déroulage

Nous avons conçu notre modèle de prédiction d’une manière itérative. Nous avons commencé par un modèle de réseau de neurones de base, puis nous avons ajouté des améliorations à chaque itération afin d’atteindre une bonne précision. Les améliorations apportées ont été à base des tests et des résultats de la précision du modèle à chaque fois. Le processus d’amélioration est arrêté une fois la précision désirée est atteinte.
Dans cette section, nous allons présenter les principales étapes suivies.

iv.3.2.1 Architecture du réseau de neurones

Nous avons utilisé le réseau de neurones pour construire un modèle de classification supervisée. Il permet la prédiction du meilleur facteur de déroulage. Dans un modèle de classification, les sorties (les classes) sont prédéfinies. Il reçoit un ensemble de données d’apprentissage (training) étiquetées pour apprendre à classer les nouveaux programmes en entrée. L’architecture du réseau de neurones est basée sur l’architecture typique des réseaux de neurones multicouche (voir figure IV.7). Nous définissons dans les sections suivantes le nombre des couches cachés et le nombre de neurones dans chaque couche (voir IV.7).
Le modèle doit prédire la sortie parmi les classes définies qui représentent la plage de valeurs possibles du facteur de déroulage (voir section IV.1.3.1).

Figure IV.7: Architecture de base du modèle.

Dans les sections qui suivent, nous présentons les notions décrivant le fonctionnent du réseau de neurones proposé (voir l’annexe C pour plus de détails sur les réseaux de neurones). Nous détaillons les différentes étapes pour la génération du modèle de prédiction du facteur de déroulage.

iv.3.2.2 Propagation de l’information

La couche d’entrée du réseau est alimentée par les caractéristiques du programme (voir section IV.2.3), ses sorties sont attribuées à la première couche cachée qui, à son tour, passe ses sorties à la couche suivante et ainsi de suite. Ce processus est appelé la propagation de l’information. La sortie de chaque couche cachée est le résultat de l’application d’une fonction à la somme pondérée des sorties des couches précédentes à laquelle un biais est ajouté. Cette fonction est appelée la fonction d’activation (voir la figure IV.8).

Figure IV.8: La structure d’un neurone artificiel Goodfellow.

Essentiellement, les fonctions d’activation (ou fonctions de transfert) convertissent le signal d’entrée en un signal de sortie. Elles produisent une sortie non linéaire à partir d’une entrée linéaire. Ceci permet de représenter des fonctions plus complexes. Il existe différents types de fonctions d’activation, pour notre modèle nous considérons la fonction Relu comme fonction d’activation pour les couches cachées, elle retourne le max entre le et la valeur d’entrée ( ).

La sortie de chaque couche est donnée par . La sortie du réseau est fournie directement par , tel que sont les poids, sont les entrées et est le vecteur de biais associé.

Comme il s’agit d’un problème de classification en classes multiples (nombre de classes > 2), nous utilisons la fonction pour la couche de sortie. La fonction permet de calculer la distribution de probabilités des différentes classes. Initialement, le modèle effectue le traitement avec des valeurs de pondération aléatoires qui sont mises à jour à chaque itération afin de minimiser l’erreur.

La fonction de perte (loss function) permet de calculer l’erreur de prédiction. Le choix de cette fonction dépend de type du problème traité. Dans le problème de classification, nous utilisons Cross-Entropy111111Connue aussi sous le nom log loss., elle permet de mesurer l’erreur de probabilité dans le cas où les classes sont mutuellement exclusives. Cela signifie que chaque entrée appartient à une et une seule classe. Dans notre cas, l’optimisation de déroulage peut avoir un seul facteur optimal. L’erreur commise est généralement mesurée pour un ensemble de données (dataset), donc nous calculons la moyenne de l’erreur commise pour l’ensemble d’apprentissage fourni.

iv.3.2.3 Rétropropagation de l’erreur

Une fois le processus de propagation de l’information est terminé, le réseau de neurones procède à la correction des poids afin de minimiser l’erreur autant que possible. Le gradient de l’erreur calculée est rétro-propagé pour mettre à jour les poids en fonction du degré de leur contribution à l’erreur. Nous appelons ce processus la rétropropagation du gradient Goodfellow.

La valeur des poids mis à jour est contrôlée par un paramètre appelé le taux d’apprentissage (learning rate). La modification apportée aux poids du réseau pour une erreur donnée est souvent de l’ordre ou ou encore moins. Il existe plusieurs algorithmes utilisés pour mettre à jour les poids et donc minimiser l’erreur (les algorithmes d’optimisation) tels que l’algorithme de la descente de gradient stochastique (SGD), l’algorithme ADAM et l’algorithme RMSprop NNOptimizers. En fonction de l’algorithme choisi, certains paramètres doivent être ajustés tels que le taux d’apprentissage. Nous avons opté pour ADAM Optimiser AdamOptimizer comme algorithme d’optimisation, car il est connu pour sa robustesse et son efficacité. Le taux d’apprentissage pris pour cet algorithme est suite à plusieurs tests.

Les poids dans les réseaux de neurones peuvent être mis à jour à partir des erreurs calculées pour chaque ligne de données de traitements. Il s’agit de l’apprentissage en ligne. Cala permet de faire des mises à jour rapides mais parfois cause des effets chaotiques au réseau. Une autre alternative consiste à enregistrer l’erreur au niveau de tous les exemples du dataset d’entraînement. Ensuite, les mises à jour sont effectuées vers la fin. Il s’agit de l’apprentissage par lots (batch learning) qui est souvent plus stable. Étant donné que le nombre d’exemples dans le dataset est très grand, la taille du lot est réduite afin d’augmenter l’efficacité de calcul. Nous considérons des lots batch de taille de 100.

Le processus est répété pour toutes les données du dataset d’entraînement. Chaque passe sur l’intégralité de l’ensemble de données pour entraîner le réseau de neurones est appelée une itération (epoch). Le réseau de neurones peut être entraîné des dizaines, centaines, voire même des milliers d’itérations afin d’améliorer la précision. Le nombre d’itérations est un paramètre à choisir soigneusement. Nous considérons un nombre d’itérations pour lequel aucune amélioration n’est apportée au modèle.

iv.3.2.4 Génération et préparation de données

Le réseau de neurones est alimenté par un ensemble de données dont chaque élément représente les caractéristiques d’un programme donné et son facteur de déroulage optimal . Ce dernier est obtenu en exécutant le programme pour toutes les valeurs possibles dans l’espace d’exploration .

Cependant, le temps d’exécution d’un programme est influencé par plusieurs évènements externes relatifs au système et à l’architechture d’exécution (ordre d’instructions choisi par la machine, taille de cache alloué, les attentes entrées/sorties, etc.). Nous considérons alors une estimation de la moyenne du temps d’exécution pour chaque programme. Selon la loi des grands nombres121212La loi affirme que la moyenne empirique, calculée sur les valeurs d’un échantillon, converge vers l’espérance lorsque la taille de l’échantillon tend vers l’infini. Dans notre cas, ceci signifie que pour un grand nombre N d’exécutions, la moyenne des temps d’exécution enregistrés tend vers la moyenne réelle du temps d’exécution du programme, le nombre d’exécutions (soit ) doit être grand pour avoir une bonne estimation. Nous avons considéré la valeur minimale de qui permet d’avoir une stabilité du facteur de déroulage optimal . Après avoir effectué plusieurs tests, nous avons pris .

La génération des programmes se fait grâce à l’outil Tiramisu_Code_Generator (voir la section IV.6). Il s’agit de générer aléatoirement des programmes appartenant à la classe des programmes visée (voir section IV.2.2). Les schedules des programmes Tiramisu générés contiennent des combinaisons d’optimisations suivantes : tuilage de boucles (à deux et à trois niveaux), inversion de boucles et la parallélisation. Le module d’extraction des caractéristiques est ensuite utilisé pour extraire le vecteur de caractéristiques pour chaque programme généré.

Les données fournies aux réseau de neurones doivent être numériques. Elles peuvent être redimensionnées (normalisées) dans la plage comprise entre 0 et 1. Les données peuvent être aussi standardisées de tel sorte que chaque colonne soit centrée et réduite. Les données de notre dataset sont toutes numériques mais nécessitent une mise à l’échelle, nous allons donc les normaliser. Le Dataset est divisé en trois parties, chacune est utilisée pour une phase de création du modèle.

  • [label=–]

  • Dataset d’entraînement ou Training set ( de l’ensemble de données d’origine). Nous utilisons ce dataset pour entraîner notre modèle (trouver les bons poids et biais pour le modèle).

  • Dataset de validation ( de l’ensemble de données d’origine) : ce dataset est utilisé pour minimiser le surapprentissage. Il ne contribue pas directement dans la modification des poids, il permet uniquement de vérifier que toute augmentation de la précision par rapport à l’ensemble de données d’apprentissage entraîne une augmentation de la précision par rapport à un ensemble de données qui n’a pas encore été montré au réseau, ou au moins le réseau ne l’a pas encore utilisé pour s’entraîner. Si la précision sur l’ensemble de données d’entraînement augmente, mais la précision sur l’ensemble de données de validation reste la même ou diminue, cela implique que le modèle sur-apprend et donc nous devons arrêter l’entraînement.

  • Dataset de test ou Test set ( de l’ensemble de données d’origine) : nous l’utilisons une fois le modèle finit l’entraînement. Ce dataset est utilisé uniquement pour tester la solution finale afin de confirmer la précision réelle du réseau.

Il est fortement déconseillé que la phase de test soit ignorée. En effet, l’algorithme qui prédit bien pendant la phase de validation ne signifie pas forcément que c’est le modèle le plus convenable au problème traité. Au cours de la phase de test, le modèle final est utilisé pour prédire de données non déjà vue. Donc, si la précision du modèle est très mauvaise pendant le test, tout le processus de conception du modèle doit être remis en cause (voir la figure IV.9).

Figure IV.9: Utilisation des différents datasets pour entraîner le modèle final.
iv.3.3 Itérations de construction du modèle de prédiction

Nous avons suivi un processus itératif afin d’arriver à la conception finale de notre modèle. La conception passe par trois principales itérations.

iv.3.3.1 1ère itération : modèle de réseau de neurones de base

Le but de cette étape est de bien cerner les entrées et les sorties du modèle. Nous avons expliqué dans la section IV.2.3 qu’initialement, le nombre de caractéristiques décrivant chaque nid de boucle est considérable. Il faut définir les caractéristiques les plus significatives pour la prédiction. D’autre part, le nombre de classes de sortie du modèle est grand. Nous avons fait une étude à priori sur un échantillon de programmes pour définir les classes qui ne seront pas utiles dans notre problème (voir section IV.1.3.1).

Le premier entraînement du modèle a été effectué sur un dataset de 1500 éléments générés toute au long de deux semaines suite à une exploration exhaustive de tous les Schedules possibles pour 20 programmes(partie algorithme). Évidement, La précision enregistrée a été médiocre car le dataset considéré est très petit et ne présente que 20 fonctions avec leurs schedules. En revanche, nous avons pu constater des points très importants.

  • [label=–]

  • Quelques colonnes (features) du dataset ont la même valeur pour toutes les lignes comme la colonne qui représente le niveau d’application de l’optimisation de parallélisation, elle ne prend que la valeur ’1’131313La valeur ’1’ veut dire que la parallélisation est appliquée sur ce niveau de boucle sur toute la colonne ou que le ’0’141414La valeur ’0’ veut dire que la parallélisation n’est pas appliquée sur ce niveau car le niveau d’application de l’optimisation de parallélisation est le même (le niveau le plus profond). En effet, ce genre de colonnes ne porte aucune information utile au modèle, il faut donc les enlever.

  • Les valeurs de certaines colonnes présentent une distance considérable par rapport à d’autres (d’ordre de ). Par exemple, la colonne du nombre d’opération d’accès mémoire (unitaire) par rapport à celle de l’étendue de boucles. Même en effectuant la normalisation, la distance entre les colonnes reste la même. Pour remédier à ce problème, il faut redimensionner ces colonnes en les divisant par par exemple pour avoir des valeurs plus petites.

  • Pour chaque programme (partie algorithme), les lignes qui le représentent dans le dataset ont plusieurs colonnes communes. La différence entre ces lignes touche seulement les colonnes décrivant la partie schedule. Ce qui diminue la diversification dans le dataset et influence ainsi la précision du modèle.

  • Le nombre de classes est très grand (64 classes) par rapport aux données que nous pouvons générer. Plus le nombre de classe est grand, plus la taille du dataset nécessaire et qui couvre toutes les classes est grand. Générer un dataset aussi immense nécessite des super calculateurs. Ceci impose des contraintes sur le nombre des classe à considérer, a fortiori, si le modèle enregistre une mauvaise précision, il serait difficile de changer la stratégie et lancer encore la génération pour avoir un nombre suffisant de données.
    De ce fait, nous avons effectué une étude statistique afin de déterminer la marge de classes à éliminer. La figure IV.10 montre la distribution des données du dataset sur les classes.

Figure IV.10: Distribution des données du dataset sur les 64 classes.
iv.3.3.2 2ème itération : sélection de bons hyperparamètres du modèle

Les résultats de la première itération nous ont mené à remettre en question l’architecture du modèle ainsi que la nature de données à générer. Concernant le problème de diversification des données nous avons décidé de réduire le nombre de Schedules générés pour le même programme (partie algorithme) à 10 Schedules aléatoires.  Le nombre de classes pose également un grand problème par rapport au nombre de données possible à générer compte tenu du temps disponible. Nous avons décidé de restreindre le nombre de classes à sept classes. Il s’agit des puissances de deux 0, 2, 4, 8, 16, 32, 64. Ce sont les facteurs de déroulage les plus utilisés par les experts. Les tests que nous avons effectués dans la phase précédente le montre également (voir figure IV.10).

D’autre part, nous avons effectué une analyse des caractéristiques (features) les plus significatives en utilisant l’outil features_selctor (voir la section IV.6). L’outil propose la suppression de 5 colonnes telles que le nombre de constantes et le nombre de niveaux de boucle151515Il s’agit d’une information déductible à partir de certaines autres colonnes (le nombre de colonnes qui présentent les étendus des niveaux du nid de boucle). Nous avons effectué des tests pour s’assurer que la précision augmente après la suppression des 5 colonnes.

Dans cette itération, nous allons relancer les tests sur un Dataset plus large pour sélectionner les bons hyperparamètres du modèle, à savoir le nombre de couches, le nombre de neurones dans chaque couche, l’algorithme d’optimisation à utiliser, etc. (pour davantage de détails, voir l’annexe D). Pour chaque hyperparamètre, nous effectuons un ensemble de tests, la valeur qui donne la meilleure précision du modèle est maintenue.

Nous avons commencé par choisir le nombre de couches et le nombre de neurones dans chaque couche. Nous avons constaté que quatre couches cachées avec 500, 400, 250 et 100 neurones dans chaque couche respectivement, donne la meilleure précision. En effet, nous avons testé pour 12 couches au maximal161616Les tests effectués pour un nombre de couches supérieur à 12 a donné une précision très basse, et ce, pour les différents cas de nombre de neurones., puis nous diminuons le nombre de couches si la précision dégrade. Pour chaque couche nous avons testé dichotomiquement les cas du nombre de neurones. D’abord, nous avons défini une borne minimale et maximale au nombre de neurones. Ensuite, nous définissons la valeur divisant l’ensemble de cas de tests en deux sous ensembles. Nous continuons l’exploration du sous ensemble qui donne une précision meilleure.
Concernant les autres hyperparamètres du modèle, les tests effectués (voir section LABEL:ModelRealisation) donnent les résultats résumés dans le tableau VI.

Type hyperparamètre choisi Algorithme d’échelonnement Standardisation Fonction d’activation ReLu Algorithme d’optimisation ADAM Taux d’apprentissage Algorithme d’initialisation des poids Algorithme de Random_uniform Nombre d’itérations technique d’arrêt précoce (Early stopping) avec une patience de 10.

Table VI: hyperparamètres choisis pour le modèle.

Le choix des hyperparamètres est suivi par une phase d’optimisation (voir l’annexe D). Pour remédier au problème du sousapprentissage, nous avons utilisé la technique d’optimisation dropout avec les facteurs (0.12, 0.1, 0.04 et 0.07) respectivement. Quant à la régularisation, nous l’avons pas appliquée car elle a influencé négativement sur la précision du modèle.

Par contre, nous avons appliqué la technique de batch-normalization pour améliorer la vitesse d’entraînement (d’ordre de 10 fois), améliorer la précision171717L’application de la technique de batch-normalization a amélioré la précision de 5% et la stabilité du modèle.

iv.3.3.3 3ème itération : l’entraînement du modèle sur les données finales

Tout au long de deux mois, nous avons pu générer un dataset de taille réduite181818La taille du dataset est de éléments relativement aux tailles nécessaires des datasets utilisés pour l’apprentissage profond. En effet, pour chaque programme (élément du dataset), nous effectuons 30 exécutions pour avoir une estimation de la moyenne réelle du temps d’exécution du programme (voir section IV.3.2.4). Ceci consomme énormément du temps. Il faut presque une année de génération de donnée pour collecter autant de données que nécessite notre problème.

Toutefois, la précision du modèle augmente tout au long du processus de génération de données. L’augmentation du nombre de données générées à chaque fois n’est pas aussi grande pour donner des changements considérables, mais la précision du modèle s’améliore graduellement et elle enregistre une augmentation relativement remarquable. (voir la figure IV.11).

Figure IV.11: L’amélioration de la précision du modèle au cours de génération de données dans le cas de 7 classes à gauche et 4 classes à droite.

Nous avons constaté également que la distribution des données du dataset sur les classes du modèle n’est pas équilibrée (voir la figure IV.12). La distribution doit être uniforme pour permettre à toutes les classes d’avoir le même degré de contribution dans l’apprentissage. De ce fait, nous avons appliqué la technique d’équilibrage de classes en définissant un seuil minimal d’éléments dans chaque classe. Certes, la taille du dataset a diminué (devient 26670 éléments), or, la précision du modèle s’est améliorée.

Figure IV.12: Distribution de données dans le dataset final sur les différentes classes du modèle

Nous lançons l’entraînement du modèle sur le dataset final. Le but est d’extraire et de sauvegarder les paramètres finaux du modèle (derniers poids et biais avec l’architecture du modèle) et par la suite, l’intégrer dans Tiramisu.

iv.4 Synthèse de conception

Dans cette première partie de ce chapitre , nous avons détaillé les différentes étapes de conception de notre solution. D’abord, nous avons discuté les choix conceptuels concernant les entrées, les sorties et l’approche d’optimisation automatique à considérer pour concevoir la solution. L’architecture conceptuelle de notre solution est basée sur deux principaux modules : le module d’extraction des caractéristiques et le modèle de prédiction du meilleur facteur de déroulage. Ce dernier est basé sur les réseaux de neurones, nous avons expliqué et justifié tous les choix considérés pour le concevoir.

Dans cette deuxième partie, nous détaillons l’implémentation des choix conceptuels considérés dans les sections précédentes.

D’abord, nous exposons les phases de réalisation du système ainsi que son architecture technique. Ensuite, nous définissons l’environnement du développement en citant les technologies, les outils et les plateformes utilisés pour développer notre solution. Tous les choix techniques considérés seront également justifiés.

iv.5 Architecture technique du système

Notre système de prédiction du meilleur facteur de déroulage représente un composant de tout un système d’optimisation automatique dans le compilateur Tiramisu191919Le système d’optimisation automatique dans Tiramisu est un projet en cours de développement, notre contribution touche principalement l’optimisation de déroulage de boucle.. Ce dernier utilise une interface fournie permettant de gérer les appels à notre système prédicteur.

Figure IV.13: Architecture technique globale du système.

Lorsque l’utilisateur de Tiramisu lance la commande automatic_unrolling() pour automatiser le choix du facteur de déroulage, le compilateur fait appel à la première couche du système, à savoir l’extraction des caractéristiques du programme. Ces derniers sont ensuite passées au modèle de prédiction pour prédire le bon facteur de déroulage. Enfin, Tiramisu exécute le programme en considérant le facteur prédit (voir la figure IV.13).

iv.6 Environnement de développement

Nous avons choisi les technologies suivantes pour implémenter notre solution.

  • [label=–]

  • Tiramisu202020https://github.com/Tiramisu-Compiler/tiramisu : est le système hôte de notre solution : les modules implémentés sont intégrés comme étant des sous systèmes en Tiramisu. Il est utilisé pour compiler et exécuter les programmes.

  • Tiramisu_Code_Generator212121https://github.com/Tiramisu-Compiler/tiramisu/tree/master/utils/code_generator : afin de construire notre Dataset, nous avons généré aléatoirement des codes appartenant à la classe de programmes visée (voir section IV.2.2). Le générateur permet d’introduire des caractéristiques définissant certaines classes de programmes, il permet également de personnaliser la génération aléatoire de codes Tiramisu (parties algorithme) et les schedules associés.

  • TensorFlow222222https://www.tensorflow.org/

    : est un Framework utilisé pour développer le modèle de prédiction du bon facteur de déroulage. Il est open source, bien documenté et soutenu par une très grande communauté active et par le géant du développement Google. C’est l’outil de développement de solutions d’apprentissage automatique le plus utilisé. D’autre part, TensorFlow facilite l’exportation et le déploiement des modèles générés vers d’autres plateformes cibles, le C++ en l’occurrence (Tiramisu est embarqué sur le C++).

  • Keras232323https://keras.io/ : ce Framework haut niveau écrit en Python, facilite l’implémentation des algorithmes du machine learning. Nous l’avons utilisé pour comparer notre modèle implémenté sous TensorFlow notamment pour la sélection des bons hyperparamètres.

  • Feature_Selctor242424https://github.com/WillKoehrsen/feature-selector. : nous avons utilisé cet outil open source pour identifier les features qui ne contribuent pas significativement à l’apprentissage du modèle. Il se base sur plusieurs principes pour identifier les features à supprimer tels que la colinéarité des caractéristiques. cet outil offre la possibilité de visualiser les résultats sous forme de tableaux ou de graphes. Il donne également la liste directe des features à supprimer.

  • Pandas : une bibliothèque open source de Python qui permet de manipuler et d’analyser les données. En particulier, elle offre plusieurs structures de données (DataFrame) qui facilitent la manipulation des données numériques. Elle dispose également de nombreuses méthodes utilisées pour l’analyse de données, ce qui s’avère très utile lorsque nous travaillons sur des problèmes d’apprentissage automatique sur Python. Nous avons utilisé la structure DataFrame pour lire et manipuler notre dataset sauvgardé dans les fichiers CSV252525CSV signifie "valeurs séparées par des virgules". Nous l’avons choisi comme format de fichiers pour sauvegarder les données d’entraînement (caractéristiques des programmes)..

  • TensorBoard : est un outil de visualisation de TensorFlow. Il facilite la compréhension et l’optimisation des modèles implémentés sous TensorFlow. Il permet de visualiser le graphe d’exécution et les différents paramètres du modèle (les poids, les biais, fonction de perte, etc.).

  • Matplotlib : est une bibliothèque de visualisation rapide et facile à manipuler. Nous l’avons utilisée pour visualiser les différents résultats du modèle. Ceci nous a permis de comparer l’évolution de la fonction de perte pour différents types d’hyperparamètres (différents algorithmes d’optimisation, taux d’apprentissage, etc.).

  • scikit-learn

    : est une bibliothèque Python open source qui offre une solide implémentation de plusieurs algorithmes d’apprentissage automatique (SVMs, K-means, etc.). Nous l’avons utilisée principalement pour diviser notre

    Dataset en trois parties (entraînement, validation et test).

  • Lanka: le cluster est géré exclusivement par MIT. Il est composé de 24 machines. Il comporte au total 48 nœuds chacun dispose de 12 cœurs de 128GB de RAM. Nous avons utilisé ce cluster pour lancer les scripts de génération de codes et d’extraction des caractéristiques des programmes.

  • GoogleColab

    : ou Colaboratory est un service du Cloud totalement gratuit. C’est un environnement portable (qui ne nécessite aucune configuration) qui permet de développer des applications d’apprentissage automatique en utilisant les bibliothèques populaires telles que Keras, TensorFlow, PyTorch et OpenCV. Il facilite l’entraînement des modèles sans se soucier des problèmes d’installation et de puissance de calcul. Nous l’avons utilisé comme un IDE pour développer notre modèle afin de remédier au problème en puissance de calcul de nos machines.