Vous êtes sur la page 1sur 131

Mémoire de fin d’études

Pour l’obtention du diplôme d’Ingénieur d’État en Informatique


Option : Systèmes Informatiques

Thème
Optimisation automatique d’une classe de programmes
écrits en Halide.

Mémoire de projet de fin d’études

Encadré par : Réalisé par :


Dr. BAGHDADI Riyadh (MIT) MANSERI Ikram
Pr. BENATCHBA Karima (ESI)

Soutenu le :
02 JUILLET 2018

Devant le jury composé de :


KERMI Adel
BOUMAZOUZA Daoud
BOUZAR Lydia

Promotion : 2017/2018
Remerciement

Je remercie dieu de m’avoir donné la patiente et le courage à faire ce travail et


l’accomplir dans de bonnes conditions.

Je tiens à remercier mon promoteur M. Riyadh BAGHDADI pour son implication


dans ce projet, son suivi permanent et pour toutes les recommandations et les conseils
précieux qu’il m’a donnés pour la réalisation de ce travail.

Mes sincères remerciements vont à Mme. Karima BENATCHBA, mon encadrante à


l’Ecole nationale Supérieure d’Informatique, pour son soutien, son accueil, sa gentillesse
et ses conseils irremplaçables.

Je remercie tous les membres du jury d’avoir accepté d’examiner et de juger mon
modeste travail.

Je tiens à remercier Mme. AIT ALI YAHIA Dahbia, pour tous ses efforts fournis afin
d’assurer le bon déroulement des stages de fin d’étude.

Pour terminer, je remercie toute personne qui a contribué de loin ou de près à la


réalisation de ce présent travail.
Dédicace

Je dédie ce modeste travail aux membres de ma petite famille : mon cher papa, ma
chère maman, mes frères Amine et Chemss Eddine et ma petite sœur Meriem. Vous, qui
avez toujours cru en moi et à mes compétences, vous m’avez toujours soutenu et poussé
à aller toujours plus loin. Je vous remercie de m’avoir procuré l’environnement convivial
pour vivre, étudier, continuer et avancer dans ma vie.
Autant faire simple, je vous aime plus que tout dans ma vie ...

Je dédie aussi ce travail aux membres de ma grande famille, spécialement mon oncle
Omar pour son support et les suggestions qu’il me fournissait quant à mon parcours aca-
démique et professionnel.

A toute personne qui a lu mon rapport et qui m’a aidé à corriger ses éventuelles
erreurs : Zizou, mon oncle Omar, mon grand-père et Mme. Ghizlaine.

A tous mes amis pour tous les encouragements qu’ils m’ont donné durant l’année :
Zakaria, Fahima, Amine, Habiba, Mariem, Nesrine, Mouna, Kawthar, Amina et Besma
...

Aux membres de mon club GDG Algiers. Avec lesquels j’ai passé des moments inou-
bliables, et j’ai appris beaucoup de choses. Grâce au GDG, j’ai pu améliorer mes compé-
tences, booster mes connaissances, et même former une nouvelle famille. Je vous remercie
tous d’avoir fait partie de mon aventure : Tina, Asmaa, Manel, Amel, Azzeddine, Smail,
Milissa, Amine, Afaf et tous les autres.

Ikram
Résumé
L’accroissement des performances des architectures matérielles et leur hétérogénéité
ont rendu le développement d’application de plus en plus complexe. En effet, les appli-
cations doivent être transformées et optimisées pour exploiter au mieux les ressources
matérielles de la machine. Le processus d’optimisation d’un programme n’est pas simple,
qu’il soit mené par le programmeur ou par le compilateur ; car les transformations réali-
sées sur un programme peuvent être bénéfiques 1 dans certains cas, et mauvaises 2 dans
d’autres cas, tout dépend de plusieurs paramètres parmi lesquels les caractéristiques de
l’architecture d’exécution.

Notre projet de fin d’étude consiste à optimiser de façon automatique la classe des pro-
grammes implémentant les réseaux de neurones convolutifs (RNC) écrite dans le langage
Halide. Ce dernier est un nouveau langage et compilateur qui distingue entre l’algorithme
et les différentes optimisations qui lui sont appliquées. La classe des programme implé-
mentant les RNC est gourmande en termes de temps d’exécution surtout lorsqu’il s’agit
de l’exécuter sur une machine à base de CPU. L’objectif de notre travail est d’optimiser
un sous-ensemble des programmes de cette classe en un temps inférieur à 24 heures, et de
produire un code optimisé proche de celui développé à la main par les experts en matière
d’optimisation de programmes.

Notre méthode d’optimisation automatique est une hybridation de l’approche ana-


lytique et l’approche exploratrice pour l’optimisation des programmes implémentant les
RNC. Nous avons testé notre méthode sur un ensemble de sept benchmarks, et nous avons
comparé le temps d’exécution du meilleur programme construit par notre méthode et ce-
lui optimisé à la main par les experts. La méthode proposée génère des programmes qui
sont non seulement compétitifs mais dont le temps d’exécution est plus petit que celui
des programmes développés à la main dans 96% des cas testés.

Mots clés : Halide, optimisations de code, optimisation automatique, RNC

1. améliore son temps d’exécution.


2. dégrade son temps d’exécution.

IV
Abstract
With the increase in hardware architectures performance, the development of powerful
applications is becoming increasingly difficult as they must be transformed and optimized
to take advantage of the machine material qualities.

The process of optimizing a program is not simple, whether it is done by the program-
mer or the compiler ; because the transformations carried out can be beneficial in some
cases, and bad in other ones, all depends on several parameters including the characteris-
tics of the execution’s architecture.

Our end-of-study project consists in automatically optimizing a set of convolutional


neural network kernels that are written in Halide. Halide is a new compiler and program-
ming language that distinguishes between the algorithm and the different optimizations
that are applied on it. The class of programs implementing CNN is greedy in terms of
execution time especially when it comes to running it on a CPU-based machine. The
goal of our work is to optimize, for CPU-based machine, a subset of CNN programs in
less than 24 hours, and produce an optimized code similar to that developed by program
optimization experts.

In this report, we are generally interested in compilers used techniques for automatic
optimization and in particular those techniques developed for Halide. Indeed, there are
already three automatic optimization techniques for Halide programs, two of which are
based on an empirical autotuning approach and the third is based on an analytic approach.

In this thesis, we present our automatic optimization method which is based on both
analytic and exploratory approach for CNN kernels optimization. We tested our method
on a set of seven benchmarks 3 , and we compared the execution time of the best program
built by our method and that optimized by experts. The suggested method generates
programs that are not only competitive but whose execution time is better than that of
programs optimized by hand in 96% of the tested cases.

Keywords : Halide, Code optimizations, Automatic optimization, CNN.

3. Benchmark is a CNN kernel implemented in Halide.

V
‫ملخص‬

‫التطور الذي شهدته تصاميم الحواسيب الجديدة واختالفها عن بعضها البعض أدى إلى خلق‬
‫صعوبات أمام تطوير تطبيقات ذات سرعة عالية‪ .‬حيث أصبح المبرمجون ملزمين بتطوير‬
‫نفس البرنامج وتحسينه بطريقة مختلفة من أجل كل تصميم للحصول على برنامج يستغل‬
‫كافة خصائص ذلك التصميم‪.‬‬

‫عملية تحسين البرامج ليست باألمر السهل ألن التحسينات المطبقة ال تزيد بالضرورة من‬
‫أداء البرنامج بل يمكنها أن تقلل من ادائه‪ .‬ان مردود كل تحسين يتأثر بالعديد من العوامل‬
‫منها مواصفات التصميم‪.‬‬

‫يقوم مشروعنا على التحسين لصنف من البرامج المكتوبة بلغة البرمجة هاليد دورها‬
‫األساسي هو برمجة طبقات الشبكات العصبونية االلتفافية‪ .‬هاليد عبارة عن لغة برمجة‬
‫ومترجم جديد له خاصية الفصل بين البرنامج والتحسينات المطبقة عليه‪ .‬إن برامج الشبكة‬
‫العصبونية االلتفافية تستغرق وقتا ً طويالً عند تنفيذها خاصةً عندما تنفذ على مستوى وحدات‬
‫المعالجة المركزية‪ .‬الهدف من مشروعنا هو التحسين التلقائي لهذا النوع من البرامج في‬
‫غضون مدة زمنية ال تتجاوز ‪ 24‬ساعة وإنتاج برامج محسنة أفضل من قريناتها‪.‬‬

‫من خالل هذه المذكرة سنتطرق بصفة عامة إلى المناهج المعتمدة من طرف المترجمين‬
‫للتحسين التلقائي للبرامج وبصفة خاصة إلى المناهج المعتمدة في هاليد‪ .‬في الواقع ‪ 3‬تقنيات‬
‫طورت من أجل التحسين التلقائي لبرامج هاليد من بينهن اثنتان تقومان على منهج‬
‫االستكشاف أما االخرى فتتركز على المنهج التحليلي‪.‬‬

‫في هذه المذكرة نقترح تقنية جديدة للتحسين التلقائي لبرامج هاليد التي تعتمد على المنهج‬
‫االستكشافي والتحليلي في آن واحد‪ .‬تقنيتنا تختص في تحسين برامج الشبكات العصبونية‬
‫االلتفافية‪ .‬اختبرنا صحة وفعالية تقنيتنا بتطبيقها على مجموعة من سبعة برامج مرجعية‬
‫مختلفة‪ .‬وكانت النتائج جد مرضية حيث أن البرامج المحسنة تلقائيا ً بواسطة تقنيتنا لها أوقات‬
‫تنفيذ تقارب تلك المحسنة من قبل خبراء في عالم تحسين البرمجيات‪.‬‬

‫الكلمات المفتاحية‪ :‬التحسينات‪ ،‬التحسين التلقائي‪ ،‬هاليد‪.‬‬


Table des matières

Remerciement II
Dédicace III
Résumé IV
Abstract V
Table des figures XI
Liste des tableaux XIV
Introduction générale 1
Etude théorique 3
I Optimisations de programmes 4
I.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
I.2 Optimisations de boucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
I.2.1 Parallélisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
I.2.2 Vectorisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
I.2.3 Découpage en bandes . . . . . . . . . . . . . . . . . . . . . . . . . 6
I.2.4 Déroulage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
I.2.5 Interversion de boucles . . . . . . . . . . . . . . . . . . . . . . . . . 7
I.2.6 Coalescence de boucles . . . . . . . . . . . . . . . . . . . . . . . . . 8
I.2.7 Tuilage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
I.3 Autres optimisations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
I.3.1 Calcul redondant . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
I.3.2 Réorganisation de calcul . . . . . . . . . . . . . . . . . . . . . . . . 9
I.4 Difficulté du choix des bonnes optimisations pour un programme . . . . . . 10
I.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
II Approches d’optimisation automatique pour les compilateurs. 12
II.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
II.2 Approche exploratrice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
II.2.1 Stratégies d’exploration d’espace . . . . . . . . . . . . . . . . . . . 13
II.2.2 Modes d’utilisation de l’approche exploratrice . . . . . . . . . . . . 14
II.2.3 Challenges des techniques basées sur l’approche exploratrice . . . . 14
II.3 Approche analytique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
II.3.1 Objectifs d’un modèle analytique . . . . . . . . . . . . . . . . . . . 15
II.3.2 Paramètres en entrée du modèle . . . . . . . . . . . . . . . . . . . . 15
II.3.3 Challenges des techniques basées sur l’approche analytique . . . . . 15
II.4 Approche prédictive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
II.5 Approche basée sur l’apprentissage automatique . . . . . . . . . . . . . . . 16

VII
II.5.1 Caractérisation des programmes . . . . . . . . . . . . . . . . . . . . 18
II.5.2 Algorithme d’apprentissage . . . . . . . . . . . . . . . . . . . . . . 19
II.5.3 Challenges des techniques basées sur l’apprentissage automatique . 20
II.6 Comparaison entre les approches d’optimisation automatique . . . . . . . . 20
II.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
III Halide et ses techniques d’optimisation automatique. 22
III.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
III.2 Algorithme dans Halide . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
III.3 Schedule dans Halide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
III.3.1 Optimisations à un seul étage . . . . . . . . . . . . . . . . . . . . . 24
III.3.2 Optimisations à deux étages . . . . . . . . . . . . . . . . . . . . . . 26
III.4 Techniques exploratrices pour l’optimisation automatique des programmes
Halide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
III.4.1 Technique basée sur l’algorithme génétique . . . . . . . . . . . . . . 27
III.4.2 Technique basée sur OpenTuner . . . . . . . . . . . . . . . . . . . . 29
III.4.3 Comparaison entre les deux techniques exploratrices . . . . . . . . . 30
III.5 Technique analytique pour l’optimisation des programmes Halide . . . . . 31
III.6 Comparaison entre les techniques de l’approche exploratrice et la technique
de l’approche analytique . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
III.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Contributions 36
IV Conception 37
IV.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
IV.2 Description du problème . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
IV.2.1 Codage du schedule . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
IV.2.2 Fonction objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
IV.3 Conception globale du système . . . . . . . . . . . . . . . . . . . . . . . . . 39
IV.3.1 Architecture globale du système . . . . . . . . . . . . . . . . . . . . 39
IV.3.2 Caractérisation des composantes du problème . . . . . . . . . . . . 41
IV.3.3 Fichier d’annotation . . . . . . . . . . . . . . . . . . . . . . . . . . 42
IV.3.4 Base de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
IV.3.5 Construction des schedules . . . . . . . . . . . . . . . . . . . . . . . 44
IV.4 Conception détaillée de la stratégie de construction des schedules . . . . . 46
IV.4.1 Recherche exhaustive . . . . . . . . . . . . . . . . . . . . . . . . . . 46
IV.4.2 Méthode Reorder-explore . . . . . . . . . . . . . . . . . . . . . . . . 47
IV.4.3 Méthode Reorder-analytique . . . . . . . . . . . . . . . . . . . . . . 50
IV.4.4 HalideAutotuner pour l’optimisation automatique des programmes
Halide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
IV.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
V Réalisation 59
V.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
V.2 Architecture du système . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
V.2.1 Programmes de la classe des RNC . . . . . . . . . . . . . . . . . . . 59
V.2.2 Système d’optimisation automatique pour les programmes de la
classe des RNC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
V.3 Technologies et bibliothèques utilisées . . . . . . . . . . . . . . . . . . . . . 61
V.4 Fonctionnalités du système . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
V.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
VI Tests et évaluations 64
VI.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
VI.2 Vue globale sur la phase d’évaluation . . . . . . . . . . . . . . . . . . . . . 64
VI.2.1 Caractéristiques des benchmarks . . . . . . . . . . . . . . . . . . . 64
VI.2.2 Optimisation automatique des benchmarks . . . . . . . . . . . . . . 65
VI.2.3 Architecture matérielle de test . . . . . . . . . . . . . . . . . . . . . 66
VI.3 Benchmarks de test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
VI.3.1 Benchmark de convolution . . . . . . . . . . . . . . . . . . . . . . . 67
VI.3.2 Benchmark ReLU . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
VI.3.3 Benchmark Convolution-ReLU . . . . . . . . . . . . . . . . . . . . . 74
VI.3.4 Benchmark MaxPool . . . . . . . . . . . . . . . . . . . . . . . . . . 76
VI.3.5 Benchmark de la multiplication de matrices . . . . . . . . . . . . . 77
VI.3.6 Benchmark de la multiplication matricielle par lots . . . . . . . . . 79
VI.3.7 Benchmark de la multiplication matricielle transposée par lots . . . 80
VI.4 Synthèse des tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
VI.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Conclusion et perspectives 83
Annexes 86
A Analyse de dépendance 87
A.1 Validité d’une optimisation . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
A.2 Identification des dépendances . . . . . . . . . . . . . . . . . . . . . . . . . 88
B Détails sur quelques optimisations 90
B.1 Augmentation du taux de parallélisme par le déroulage . . . . . . . . . . . 90
B.2 Tuilage pour la localité des données . . . . . . . . . . . . . . . . . . . . . . 91
B.3 Difficulté du choix des optimisations pour un programme . . . . . . . . . . 93
C Le framework OpenTuner 96
C.1 Architecture logicielle d’OpenTuner . . . . . . . . . . . . . . . . . . . . . . 96
C.2 Types de paramètres dans OpenTuner . . . . . . . . . . . . . . . . . . . . 96
D Optimisation automatique des programmes Halide 99
D.1 Calcul du taux de réutilisation des données pour une fonction. . . . . . . . 99
D.2 Schedules raisonnables pour la population initiale de l’auto-scheduler . . . 100
D.3 Coût arithmétique d’une fonction pour l’auto-scheduler de Halide . . . . . 101
E Choix de conception 102
E.1 Diagramme de classe pour la recherche exhaustive des schedules Halide . . 102
E.2 Choix de conception pour la méthode HalideAutotuner . . . . . . . . . . . 104
E.2.1 Diagramme de classe du système . . . . . . . . . . . . . . . . . . . 104
E.2.2 Diagramme d’activité résumant l’exploration des optimisations . . . 106
F Modèle analytique pour l’optimisation d’interversion de boucle 107
G Réseaux de neurones et réseaux de neurones convolutifs 110
G.1 Réseaux de neurones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
G.1.1 Structure d’un neurone . . . . . . . . . . . . . . . . . . . . . . . . . 111
G.2 Réseau de neurones convolutif . . . . . . . . . . . . . . . . . . . . . . . . . 112
Bibliographie 114
Table des figures

1 Parallélisation des itérations d’une boucle imbriquée sur trois threads. . . 5


2 Le code sur la gauche est vectorisé et il est équivalent à celui de la droite 5
3 La boucle de gauche a été découpée et a produit les deux boucles imbri-
quées sur la droite. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4 Le déroulage d’une boucle avec un facteur de déroulage k=3 . . . . . . . 7
5 Interversion de deux boucles de dimension i et j [Bacon et al., 1994]. . . . 7
6 La coalescence fusionne les deux boucles imbriquées sur la gauche pour
former une seule boucle : celle qui est sur la droite . . . . . . . . . . . . . 8
7 Un tuilage avec facteurs : n*m (les étendues des deux boucles internes).
A[i,j] et B[j,i] sont des données multidimensionnelles. A[i,j] est accessible
ligne par ligne, mais B[j,i] est accessible colonne par colonne. Après le
tuilage, A et B sont accessibles en blocs de taille n*m. . . . . . . . . . . . 9
8 Le parcours exhaustif des combinaisons de quatre optimisations pour un
programme donné. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
9 La variation de la performance du programme de multiplication de ma-
trices en fonction de la taille de tuilage appliquée au programme (sur deux
architectures matérielles de test : Sparc et SGI). . . . . . . . . . . . . . . 17
10 Utilisation du modèle basé sur l’apprentissage automatique pour l’opti-
misation automatique de programmes. . . . . . . . . . . . . . . . . . . . 17
11 La base de données d’apprentissage pour l’optimisation automatique des
programmes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
12 L’ensemble des propriétés qui caractérisent les programmes de l’ensemble
d’apprentissage dans la technique d’Agakov [Agakov et al., 2006]. . . . . 18
13 L’ensemble des optimisations appliquées aux programmes de l’ensemble
d’apprentissage, dans la technique d’Agakov [Agakov et al., 2006]. . . . . 18
14 Pseudo-code équivalent à l’appel F(x,y) = x+y dans Halide . . . . . . . . 23
15 Code Halide pour flouter une image . . . . . . . . . . . . . . . . . . . . . 23
16 Pipeline équivalent au code de la figure 15 . . . . . . . . . . . . . . . . . 23
17 Optimisations dans Halide (le nom des optimisations et leur équivalent
dans le langage Halide). . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
18 Pseudocode équivalent à l’application de l’optimisation « compute_at »
à granularité d’un point sur un programme Halide . . . . . . . . . . . . . 26
19 Individu, chromosome et population dans l’Autotuner de Halide. . . . . . 28
20 Entrées et sorties de l’Auto-scheduler pour la génération automatique du
schedule optimal pour un algorithme Halide en entrée . . . . . . . . . . . 31
21 Pipeline de traitement d’images dont les étages sont groupés par l’Auto-
scheduler. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
22 Un algorithme Halide A auquel on applique deux schedules différents. . . 38
23 Schéma global de la solution . . . . . . . . . . . . . . . . . . . . . . . . . 39

XI
24 Test de performance d’un schedule construit sur le programme Halide en
entrée. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
25 Exemple d’algorithme Halide . . . . . . . . . . . . . . . . . . . . . . . . . 42
26 Annotation de l’algorithme ci-dessus. . . . . . . . . . . . . . . . . . . . . 43
27 Schéma de la base de données pour la sauvegarde des programmes et les
schedules testés. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
28 Méthodes conçues pour l’optimisation automatique des programmes Halide 45
29 Schéma résumant la construction des schedules dans l’exploration exhaus-
tive. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
30 Courbe montrant la différence entre l’application d’une bonne et d’une
mauvaise interversion de boucle sur les schedules renvoyés une fois l’in-
terversion fixée. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
31 Exploration en deux étapes dans la méthode Reorder-explore . . . . . . . 49
32 Introduction d’un modèle analytique pour le choix de l’optimisation d’in-
terversion de boucle dans Reorder-Analytique . . . . . . . . . . . . . . . 50
33 Evolution de la performance d’un schedule en faisant varier uniquement
l’optimisation compute_at. . . . . . . . . . . . . . . . . . . . . . . . . . . 53
34 Variante du Hill Climbing pour trouver une optimisation de granularité
de calcul (compute_at) de bonne qualité . . . . . . . . . . . . . . . . . . 54
35 Variante du Hill Climbing pour la recherche de bons facteurs de découpage
en bandes et de tuilage . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
36 Stratégie d’exploration d’espace adoptée dans HalideAutotuner . . . . . . 57
37 Architecture globale du système. . . . . . . . . . . . . . . . . . . . . . . . 60
38 Ligne de commande pour lancer la méthode de construction des schedules
HalideAutotuner. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
39 Optimisation automatique des programmes de test. . . . . . . . . . . . . 66
40 Optimisation parallèle des instances de test sur le cluster . . . . . . . . . 67
41 Principe de fonctionnement du traitement de convolution. . . . . . . . . . 67
42 Fichier d’annotation pour le programme de convolution pour des entrées
de 32 filtres de taille 5*5*16 et 32 images de taille 68*68*16 chacune. . . 68
43 Tableau comparatif entre les schedules construits automatiquement et
manuellement en fonction de la taille des entrée pour le programme de
convolution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
44 Fichier d’annotation pour le programme de relu avec des images de taille
64*64*32*32. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
45 Fichier d’annotation pour le programme de Convolution-ReLU avec des
images de taille 68*68*16*32 et des filtres de taille 5*5*16*32. . . . . . . 75
46 Principe du maxpooling sur une image. . . . . . . . . . . . . . . . . . . . 76
47 S2 dépend de S1 (S2 ne peut pas s’exécuter en parallèle avec S1) . . . . . 87
48 S2 ne dépend pas de S1. (S1 et S2 s’exécutent en parallèle). . . . . . . . . 87
49 Boucle qui manipule une donnée multidimensionnelle A. . . . . . . . . . . 88
50 Boucle imbriquée qui manipule une donnée multidimensionnelle A. . . . . 88
51 Déroulage d’une boucle avec facteur k = 3 . . . . . . . . . . . . . . . . . 90
52 Code assembleur équivalent à celui de la boucle non déroulée de la figure
51 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
53 Code assembleur équivalent au programme déroulé de la figure 51 . . . . 91
54 Tuilage avec facteurs : n*m (les étendues des deux boucles internes). A[i,j]
et B[j,i] sont des données multidimensionnelles. A[i,j] est accessible ligne
par ligne, mais B[j,i] est accessible colonne par colonne. Après le tuilage,
A et B sont accessibles en blocs de taille n*m. . . . . . . . . . . . . . . . 92
55 Nombre de défauts de cache pour la version tuilée de la boucle, et pour
la version sans tuilage. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
56 Implémentation naïve du programme à 3 tableaux : input, Blurx et Blury 94
57 Implémentation de la fenêtre coulissante sur le programme à 3 tableaux :
input, Blurx, Blury . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
58 Implémentation de la fusion totale sur le programme à trois tableaux :
input, Blurx et Blury. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
59 Schéma démontrant le flux de calcul des données dans l’implémentation
de la fenêtre coulissante sur le programme à trois tableaux. . . . . . . . . 95
60 Architecture globale d’OpenTuner. α, β, γ présentent des coefficients af-
fectés de la méta-heuristique du bandit multi-armé aux différentes tech-
niques de recherche, où celle qui dispose du plus grand coefficient sera
exécutée plus que les autres. . . . . . . . . . . . . . . . . . . . . . . . . . 97
61 Paramètres dans OpenTuner . . . . . . . . . . . . . . . . . . . . . . . . . 97
62 Exemple de déclaration d’un paramètre Halide à autorégler . . . . . . . . 98
63 Le corps de la fonction Blurx en Halide, extrait du corps de la fonction
de troublement d’une image. . . . . . . . . . . . . . . . . . . . . . . . . . 99
64 Code équivalent du parcours ligne par ligne de la fonction Blurx. . . . . . 99
65 Code équivalent du parcours colonne par colonne de la fonction Blurx. . . 100
66 Rectangle minimal qui englobe les valeurs de la fonction productrice f
nécessaires au calcul d’une valeur de la fonction consommatrice g. . . . . 100
67 Diagramme de classe pour la recherche exhaustive. . . . . . . . . . . . . . 103
68 Diagramme de classe pour la description des entités du système. . . . . . 105
69 Diagramme d’activité illustrant le fonctionnement de la génération de
schedules guidée par les restrictions. . . . . . . . . . . . . . . . . . . . . . 106
70 Boucle imbriquée de profondeur 2 d’étendues égales à N*M, qui manipule
les deux tableaux A et B (accès colonne par colonne) [Allen and Kennedy,
2002]. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
71 Défauts de cache engendrés lors des premiers accès au tableau A 3*4. . . 108
72 Boucle imbriquée de profondeur 2 d’étendues égales à N*M, qui manipule
les deux tableaux A et B (accès ligne par ligne). . . . . . . . . . . . . . . 108
73 Schéma d’un réseau de neurones. . . . . . . . . . . . . . . . . . . . . . . 110
74 Structure d’un neurone artificiel. . . . . . . . . . . . . . . . . . . . . . . . 111
75 Exemple d’un réseau de neurones convolutifs. . . . . . . . . . . . . . . . 113
Liste des tableaux

I Tableau comparatif entre les approches d’optimisation automatique uti-


lisées dans les compilateurs . . . . . . . . . . . . . . . . . . . . . . . . . . 20
II Tableau récapitulant les optimisations Halide qui sont appliquées sur un
seul étage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
III Benchmarks optimisés par l’autotuner et les résultats obtenus sur une
machine de type quad core Xeon W3520 x86 CPU . . . . . . . . . . 29
IV Tableau récapitulant les paramètres OpenTuner correspondants aux op-
timisations Halide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
V Benchmarks optimisés dans OpenTuner et les résultats obtenus sur chaque
architecture d’exécution . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
VI Tableau comparatif entre l’algorithme génétique et la méthode basée sur
OpenTuner pour la génération automatique de schedules pour un pro-
gramme Halide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
VII Tableau comparatif entre les méthodes des deux approches : analytique
et exploratrice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
VIII Modélisation de l’entité Schedule par un tableau d’entités, où chaque
entité représente un type d’optimisation appliquée sur l’algorithme A. . . 38
IX Informations du fichier d’annotation. . . . . . . . . . . . . . . . . . . . . 42
X Caractéristiques des benchmarks de test . . . . . . . . . . . . . . . . . . 65
XI Caractéristiques de l’architecture d’exécution . . . . . . . . . . . . . . . . 66
XII Caractéristiques des fonctions consommatrices du benchmark . . . . . . . 68
XIII Statistiques à propos de l’optimisation automatique du programme de
convolution en utilisant HalideAutotuner. . . . . . . . . . . . . . . . . . . 69
XIV Statistiques à propos de l’optimisation automatique du programme ReLU
en utilisant HalideAutotuner. . . . . . . . . . . . . . . . . . . . . . . . . 73
XV Caractéristiques des fonctions consommatrices du benchmark Convolution-
ReLU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
XVI Statistiques sur l’optimisation automatique du programme Convolution-
ReLU par HalideAutotuner . . . . . . . . . . . . . . . . . . . . . . . . . . 75
XVII Statistiques à propos de l’optimisation automatique du programme de
MaxPool en utilisant HalideAutotuner . . . . . . . . . . . . . . . . . . . 77
XVIII Statistiques sur l’optimisation automatique de l’algorithme de la multi-
plication de matrices par HalideAutotuner . . . . . . . . . . . . . . . . . 78
XIX Statistiques sur l’optimisation automatique de l’algorithme de la multi-
plication de matrices par lots . . . . . . . . . . . . . . . . . . . . . . . . . 80
XX Statistiques sur l’optimisation automatique de l’algorithme de la multi-
plication matricielle transposée par lots . . . . . . . . . . . . . . . . . . . 81

XIV
Table des sigles et abréviations

AA Apprentissage Automatique
API Application Programmable Interface
ATLAS Automatically Tuned Linear Algebra Software
CISC Complex Instruction Set Computing
CNN Convolutional Neural Networks
CPU Central Processing Unit
GPU Graphics Processing Unit
JSON JavaScript Object Notation
ML Machine Learning
OCP Open Closed Principle
PFE Projet de Fin d’Etude
RISC Reduced Instruction Set Computing
RNC Réseaux de Neurones Convolutifs
SVM Support Vector Machine
VLIW Very Long Instruction Word

XV
Introduction générale

L’accroissement des performances des architectures matérielles a rendu le développe-


ment d’applications de plus en plus complexe. En effet, une fois que les applications sont
fonctionnelles, elles doivent être optimisées pour exploiter efficacement les ressources ma-
térielles de la machine.

Ce processus d’optimisation n’est pas simple car les optimisations appliquées sur le
programme peuvent être invalides. Dans le cas où elles sont valides, elles peuvent être
bénéfiques (réduisent le temps d’exécution du programme) ou mauvaises (augmentent le
temps d’exécution du programme). Cela dépend de plusieurs paramètres dont : les carac-
téristiques matérielles de la machine qui est cencée exécuter le programme, la structure
et la dépendance entre les instructions du programme et les optimisations qui y sont déjà
appliquées.

Afin d’alléger la tâche d’optimisation aux programmeurs, les compilateurs ont été
conçus pour optimiser les programmes de façon automatique. Néanmoins, l’optimisation
automatique des programmes écrits dans des langages tels que C, C++ ou Java s’avère
complexe. En effet, un langage comme le C qui intégre les doubles pointeurs pour adres-
ser les données, n’est pas simple à optimiser car la théorie qui permet de décider si une
optimisation est valide ou pas devient indécidable.

L’objectif de l’équipe de recherche sur les compilateurs, Commit du MIT, est de déve-
lopper de nouveaux langages qui permettent d’exprimer des programmes de façon simple
et de pouvoir les optimiser automatiquement. L’un des langages développés par cette
équipe est Halide. Ce nouveau langage et compilateur a la particularité de séparer entre
le code et les optimisations qui y sont appliquées pour simplifier le passage d’une combi-
naison d’optimisations à une autre et cela sans alterner le code et son fonctionnement.

Notre projet de fin d’étude vise à contribuer dans l’optimisation automatique des pro-
grammes écrits dans Halide. Concrètement, notre objectif est d’élaborer un système qui
reçoit en entrée un programme Halide est renvoie en sortie les meilleures optimisations
à appliquer pour ce programme. Mais, ce n’est pas n’importe quel programme et pour
n’importe quelle architecture d’exécution. Notre projet se restreint à la classe des pro-
grammes implémentant les réseaux de neurones convolutifs, destinés à s’exécuter sur une
architecture matérielle à base de CPU. Le système élaboré doit prendre au minimum 24
heures pour aboutir à une combinaison d’optimisations qui minimise le temps d’exécution
du programme et qui est compétitive à celle développée par des experts.

Pour apporter une solution à notre problème, nous avons soulevé plusieurs questions :
Quelles sont les optimisations de programme concernées par notre projet et quelles sont

1
leurs caractéristiques et impact sur le programme ? Comment un compilateur optimise-t-il
un programme ? Quelles sont les méthodes d’optimisation automatique déjà développées
pour l’optimisation des programmes Halide ? Comment développer une nouvelle méthode
qui est censée donner des programmes Halide optimisés proches de ceux optimisés à la
main par les experts ?

Ce document commence par une étude théorique répartie en trois chapitres : dans
le premier, nous abordons les optimisations de programmes, le second énumère les ap-
proches et techniques utilisées par le compilateur pour optimiser les programmes, et dans
le troisième, nous présentons le langage Halide et les différentes méthodes d’optimisation
automatique développées pour ce compilateur. En second lieu, nous entamons la par-
tie contribution qui est répartie en trois chapitres : dans le premier, nous expliquons la
conception de notre méthode d’optimisation automatique appelée HalideAutotuner ainsi
que le système global, dans le second, nous expliquons nos différents choix technologiques
pour l’élaboration du système et dans le troisième, nous clôturons ce mémoire par les
résultats de l’optimisation automatique faite par HalideAutotuner sur sept programmes
implémentant les RNC.

2
Première partie
Etude théorique
Chapitre I

Optimisations de programmes

I.1 Introduction
Un programme doit subir plusieurs optimisations, pour pouvoir utiliser de façon effi-
cace les ressources matérielles de la machine et s’exécuter dans les temps impartis.

Une optimisation peut être bénéfique (réduit le temps d’exécution du programme) ou


mauvaise (augmente le temps d’exécution du programme) pour un programme. Cela dé-
pend de plusieurs facteurs que nous exposerons dans ce premier chapitre. De même, une
optimisation peut être bénéfique pour un programme mais une fois combinée à d’autres,
elle peut dégrader les performances du programme. En effet, la décision des optimisations
à appliquer pour un programme est une tâche complexe, qui exige une connaissance sur
l’impact de chaque optimisation à part, et une compréhension de la nature de relation
entre toute paire d’optimisation combinées et appliquées sur le programme.

Au cours de ce chapitre, nous nous limitons aux optimisations utilisées dans le com-
pilateur Halide (les optimisations de boucles). Nous expliquerons le principe de chaque
optimisation abordée, son objectif et les ressources matérielles affectées par cette opti-
misation. Au final, nous clorons le chapitre avec un aperçu sur les compromis entre les
différentes optimisations abordées.

I.2 Optimisations de boucles


Une transformation d’optimisation peut être appliquée sur différentes parties du pro-
gramme. Nous allons nous intéresser en particulier aux optimisations de boucles. Evidem-
ment, il est plus bénéfique de se focaliser sur le segment de code qui est fréquemment
exécuté et de l’optimiser pour gagner considérablement en performance. En effet, il est
reconnu que la plupart des programmes passent 90% de leur temps à exécuter uniquement
10% du code : ces 10% représentent les boucles critiques du programme [Davidson and Jin-
turkar, 1996]. On souligne que certaines des optimisations que nous allons aborder ne sont
pas toujours valides car elles peuvent alterner le fonctionnement du programme. L’analyse
de dépendance est l’une des théories qui permet de décider de la justesse d’application
d’une optimisation sur le code (voir annexe A). Nous allons aborder maintenant les op-
timisations de boucles les plus récurrentes et celles utilisées dans Halide : parallélisation,
vectorisation, déroulage, interversion de boucles, tuilage, découpage en bandes,.

4
Chapitre I. Optimisations de programmes

I.2.1 Parallélisation
La parallélisation consiste à lancer en parallèle l’exécution d’un ensemble d’instruc-
tions séquentielles textuellement et qui sont indépendantes. La parallélisation de boucles
consiste à attribuer une ou plusieurs itérations d’une boucle à un thread à part, et les
exécuter toutes en parallèle et d’une façon asynchrone (voir figure 1). Cette optimisation
se base essentiellement sur les propriétés de l’architecture matérielle d’exécution. En effet,
ces machines doivent être équipées de plusieurs unités de traitement pour pouvoir exécu-
ter les instructions en parallèle (au moins deux).

Figure 1: Parallélisation des itérations d’une boucle imbriquée sur trois


threads.

L’utilisation du parallélisme implique un surcoût pour la création, la gestion et l’or-


donnancement des threads [Allen and Kennedy, 2002]. Par conséquent, il faut que le temps
gagné par la parallélisation dépasse le temps perdu pour la gestion des threads.

Notons que le nombre d’unités fonctionnelles d’exécution dont dispose la machine est
un paramètre important pour l’optimisation de parallélisation. Car plus le nombre d’unités
fonctionnelles est grand, plus le traitement peut être parallélisé.

I.2.2 Vectorisation
La vectorisation d’une boucle consiste à repérer un ensemble de k éléments d’une don-
née multi-dimensionnelle qui subissent le même traitement à travers les itérations, à les
regrouper, à les rediriger vers les registres vectoriels, où ils subissent le traitement com-
mun [Allen and Kennedy, 2002]. L’exemple de la figure 2 illustre une boucle qui peut être
vectorisée.

Figure 2: Le code sur la gauche est vectorisé et il est équivalent à celui


de la droite

Comme pour la parallélisation, l’utilisation des registres vectoriels entraîne une perte
de temps pour leur gestion. Par conséquent, il est plus judicieux d’utiliser la vectorisation

5
Chapitre I. Optimisations de programmes

lorsque le calcul vectorisé est important. La taille du registre vectoriel est un paramètre
matériel déterminant pour l’optimisation de vectorisation, car plus le registre est large,
plus on peut vectoriser et gagner en temps d’exécution.

I.2.3 Découpage en bandes


C’est une opération qui consiste à transformer une boucle d’indice i et dont l’étendue 1
est égale à n, en une boucle imbriquée, avec un facteur de découpage égal à k [Bacon et al.,
1994]. La boucle la plus interne aura une étendue égale à k, tandis que la boucle externe
aura comme étendue n/k, (voir figure 3). Le découpage en bandes est généralement réalisé
sur les boucles manipulant une donnée multi-dimensionnelle comme un tableau à 2D ou
3D, où les résultats d’une itération qui manipulent une ou plusieurs champs de la donnée
multi-dimensionnelle dépendent des résultats des itérations précédentes. L’objectif prin-
cipal est de séparer les itérations indépendantes de la boucle principale, qui peuvent à
leur tour être exécutées en parallèle. Sur le code de la figure 3 (gauche), on remarque que
l’itération i+1 ne dépend pas de l’itération i, mais qu’elle dépend de l’itération i-1. Alors
il est conseillé de procéder à un découpage de facteur égal à 2, qui renvoie les itérations
indépendantes (i et i+1) vers la boucle interne.

Figure 3: La boucle de gauche a été découpée et a produit les deux boucles


imbriquées sur la droite.

L’optimisation de découpage en bandes peut-être vue comme un pré-traitement pour


appliquer d’autres optimisations (elle n’améliore pas le temps d’exécution du programme
ni le dégrade), comme la parallélisation, la vectorisation et le déroulage.

I.2.4 Déroulage
C’est une transformation qui permet de réduire le nombre d’itérations d’une boucle
en répliquant son corps k fois tel que k <= n, où n représente le nombre d’itérations de la
boucle [Bacon et al., 1994]. La figure 4, illustre le déroulage d’une boucle avec un facteur
de déroulage k = 3. En effet, on transforme la boucle de dimension y, ayant un pas = 1, et
un nombre d’itérations = n+1, en une boucle, avec un pas = 3, et un nombre d’itérations
réduit à n+1 /3.

Le déroulage est utile car il réduit le nombre de tests de l’instruction de branche-


ment. Il a la particularité de s’appliquer sur n’importe quelle boucle. Cependant, pour les
boucles imbriquées, il est recommandé de l’appliquer sur la boucle la plus interne. Son
application sur la boucle externe provoquerait la réplication du corps des boucles internes,
ce qui augmenterait le surcoût du contrôle de fin de boucle au lieu de le baisser [Bacon
et al., 1994]. Le facteur de déroulage k doit être choisi judicieusement : si k est grand,
on risque de ne pas pouvoir charger à la fois toutes les instructions de la boucle dans le

1. Nombre d’itérations de la boucle.

6
Chapitre I. Optimisations de programmes

Figure 4: Le déroulage d’une boucle avec un facteur de déroulage k=3

cache d’instructions et ainsi de produire des défauts de cache. Par conséquent, cela va
augmenter le temps d’exécution.

Le nombre de registres est important pour l’optimisation de déroulage, car les données
des instructions déroulées vont allouer des regitres différents pour pouvoir s’exécuter en
parallèle. Donc, il faut prendre en considération le nombre de registres disponibles dans
la machine, pour pouvoir décider du facteur de déroulage (voir annexe B.1).

I.2.5 Interversion de boucles


C’est une transformation qui agit sur une boucle imbriquée et qui consiste à interver-
tir l’ordre des boucles : la boucle la plus interne devient externe et vice versa (voir figure
5) [Bacon et al., 1994].

Figure 5: Interversion de deux boucles de dimension i et j [Bacon et al.,


1994].

L’interversion permet d’appliquer l’optimisation de vectorisation (resp. parallélisation)


sur la boucle la plus interne (resp. la plus externe) suite à une interversion de boucle qui
positionne la boucle indépendante 2 dans le niveau le plus intérieur de la boucle (resp. le
plus externe) [Bacon et al., 1994].

De même, l’interversion peut améliorer la localité des données et réduire la distance


entre deux accès mémoire [Allen and Kennedy, 2002]. Sur l’exemple de la figure 5, pour
accéder à un seul élément de la matrice B, il faut charger toute une ligne de B à chaque
itération de la boucle interne 3 . Par contre, après l’interversion, les éléments de la matrice
B sont accessibles dans un ordre séquentiel tel qu’ils sont stockés par la machine, car deux
éléments successifs de la boucle interne se retrouvent au niveau de la même ligne. Ce-
pendant, tous ces avantages que nous avons cités sont mutuellement exclusifs, c’est-à-dire
qu’on ne peut pas les satisfaire tous en même temps.

Notons que l’opération d’interversion n’est pas toujours légale, elle est soumise à des
contraintes de validité, comme nous l’avons souligné dans l’annexe A qui expose la théorie
2. Celle dont les résultats de l’itération i ne dépend pas des résultats de l’itération précédentes.
3. Si la machine stocke les tableaux ligne par ligne

7
Chapitre I. Optimisations de programmes

de l’analyse de dépendances.

I.2.6 Coalescence de boucles


C’est une technique d’optimisation qui consiste à fusionner deux boucles parfaitement
imbriquées en une seule boucle (voir figure 6). La boucle résultante aura une étendue plus
large, qui est égale à la multiplication des étendues des deux boucles fusionnées (n*n, sur
l’exemple de la figure 6). La coalescence est toujours possible, car elle n’entraîne aucun
changement sur les dépendances entre les instructions, et le parcours des données reste le
même.

Figure 6: La coalescence fusionne les deux boucles imbriquées sur la gauche pour
former une seule boucle : celle qui est sur la droite

Si on veut paralléliser la boucle la plus externe et que son étendue n’est pas assez
large alors le surcoût de la gestion des threads va affecter le temps d’exécution lors de
l’application de la parallélisation. Alors on procède à une coalescence de boucles, pour
produire une nouvelle boucle avec une étendue plus large, et ensuite la paralléliser 4 .
Néanmoins, la coalescence de boucles, peut nuire au parallélisme et à la vectorisation, car
elle risque de fusionner deux boucles dont l’une est dépendante et l’autre indépendante
et produire une boucle dépendante qui ne peut être ni parallélisée ni vectorisée.

I.2.7 Tuilage
Il est connu sous le nom de tiling ou loop blocking. C’est une optimisation qui est
obtenue en combinant trois optimisations : deux découpages en bandes pour deux boucles
imbriquées (chacune avec un facteur de découpage n et m) suivis d’une réorganisation de
boucles, dans ce cas n*m sont dits les facteurs de tuilage. La transformation de tuilage
est réalisée sur les boucles qui manipulent des données multi-dimensionnelles indexées en
fonction des indices de boucles. Suite à l’application du tuilage, ces données vont être
accédées tuile par tuile de taille n*m au lieu qu’elles soient accédées ligne par ligne ou
colonne par colonne et cela afin d’améliorer leur localité de données (voir figure 7).

L’optimisation de tuilage permet d’améliorer la localité des données et utiliser effica-


cement le cache (voir annexe B.2 pour un exemple détaillé sur l’effet du tuilage).

Le tuilage peut augmenter le taux de parallélisme car si les données des tuiles (les
données manipulées dans les deux boucles les plus internes) sont indépendantes les unes
des autres, on peut paralléliser l’exécution du programme sur les tuiles de données. Néan-
moins, cela peut détériorer les performances quand il n’y a pas de localité de données,
car au lieu de tirer bénéfice du chargement « ligne par ligne » établi par le prélecteur,
4. Si elle reste indépendante.

8
Chapitre I. Optimisations de programmes

Figure 7: Un tuilage avec facteurs : n*m (les étendues des deux boucles internes).
A[i,j] et B[j,i] sont des données multidimensionnelles. A[i,j] est accessible ligne par
ligne, mais B[j,i] est accessible colonne par colonne. Après le tuilage, A et B sont
accessibles en blocs de taille n*m.

on abandonne cet avantage pour passer à d’autres données qui ne sont pas nécessaires au
calcul.

Les facteurs de tuilage doivent être bien choisis : assez petits pour permettre une
grande granularité de parallélisme, et pour assurer qu’une tuile peut être chargée entiè-
rement dans le cache. De même, ils doivent être assez grands pour exploiter l’espace du
cache de façon efficace. La transformation de tuilage n’est pas toujours possible. En effet,
c’est une transformation qui combine le découpage en bandes et l’interversion de boucles.
Comme la transformation d’interversion de boucles n’est pas toujours légale, et qu’elle
est soumise à des conditions de validité (voir l’annexe A pour plus de détails sur l’analyse
de dépendance : la théorie qui permet de décider de la validité d’une optimisation sur le
code), le tuilage est ainsi soumis aux mêmes conditions.

I.3 Autres optimisations


Dans cette partie, nous présentons d’autres optimisations, qui sont utilisées dans Ha-
lide et que nous avons jugées utiles pour la compréhension des autres parties du mémoire.

I.3.1 Calcul redondant


C’est une optimisation qui favorise le recalcul d’une donnée par rapport à son char-
gement à partir de la mémoire. En effet, si une donnée est déjà calculée et stockée suite
à l’exécution d’une instruction, on la recalcule à chaque fois qu’elle est sollicitée par une
autre instruction. Cette optimisation est bénéfique lorsque le temps pris par le calcul d’une
donnée est plus petit par rapport au temps pris pour son chargement.

I.3.2 Réorganisation de calcul


Cette optimisation consiste à réordonner les instructions du programme [Bacon et al.,
1994]. Le rapprochement des instructions consommatrices (celles qui utilisent la donnée
produite par d’autres instructions) des instructions productrices (celles qui produisent une
donnée consommée par d’autres instructions) améliore la localité des données et assure
que la donnée consommée se trouve toujours dans le cache de données.

9
Chapitre I. Optimisations de programmes

I.4 Difficulté du choix des bonnes optimisations pour


un programme
Les architectures matérielles ont connu un grand développement au cours des dernières
années pour offrir plus de performances calculatoires. En effet, elles deviennent de plus
en plus complexes et hétérogènes : plusieurs niveaux de cache, des registres vectoriels de
grande taille, diverses stratégies de remplacement dans les caches, plusieurs unités de cal-
cul ...etc. Cette hétérogénéité a rendu le développement d’applications performantes de
plus en plus difficile. En effet, pour produire une application performante qui utilise effica-
cement les ressources de la machine, il faudrait développer une implémentation spéciale à
chaque type d’architecture : l’effet d’une optimisation sur un code varie d’une architecture
à une autre [Allen and Kennedy, 2002].

Plusieurs implémentations sont possibles pour exécuter un programme et aboutir au


même résultat, mais quelle est l’implémentation la plus efficace et qui prend le moins de
temps ? Nous avons tendance à vouloir paralléliser notre code, mais est-il toujours efficace
de paralléliser ? qu’en est-t-il pour la localité des données ?

L’amélioration de la localité des données dans les boucles implique le rapproche-


ment des instructions productrices et consommatrices les unes des autres, pour assurer la
consommation des données produites tant qu’elles sont dans le cache. Cet objectif est as-
suré par plusieurs optimisations de boucle à l’instar du tuilage. Cependant, le parallélisme
de boucles implique le groupement des instructions indépendantes, pour pouvoir affecter
chacune à un thread séparé et les exécuter toutes ensemble. Ces deux objectifs : l’amélio-
ration de la localité des données et l’amélioration du taux de parallélisme, ne peuvent pas
coexister dans la même proportion de code, sans qu’une redondance de calcul n’apparaisse.

Effectivement, la parallélisation lance les instructions indépendantes en parallèle, qui


à leur tour chargent et déchargent le cache par les différentes données utilisées et stockées
en mémoire. Une fois que tous les threads ont terminé, et quand c’est le tour d’exécuter
les instructions qui dépendent de celles qui sont exécutées en parallèle, on a de fortes
chances que les résultats de ces dernières ne se retrouvent plus dans le cache. C’est ce
qui va nuire à la localité de données du programme. Si on veut paralléliser et en même
temps gagner en localité mémoire, on va procéder à des modifications supplémentaires.
En effet, une fois l’exécution parallèle terminée, au lieu de charger les données produites
par les instructions parallèles contenues dans la mémoire principale, on va les recalculer
de nouveau. Cependant, cette optimisation implique aussi des coûts, qui sont les coûts
calculatoires du recalcul, et une question qui se pose : est-ce que le coût calculatoire est
plus petit que le coût du chargement de la donnée à partir de la mémoire principale ? Sans
oublier le sacrifice de perdre une donnée d’un registre. Les objectifs phares de l’optimisa-
tion de programme, et qui sont antagonistes, sont l’augmentation du taux de parallélisme,
l’amélioration de la localité mémoire et la diminution du taux de calcul redondant.

Par conséquent, le choix des optimisations à appliquer sur un programme est complexe
et nécessite une connaissance préalable des propriétés de l’architecture matérielle : la taille
du registre vectoriel et la taille du cache. Ainsi, une connaissance des avantages et des
inconvénients de chaque optimisation est primordiale pour atteindre un bon niveau de

10
Chapitre I. Optimisations de programmes

performance et réduire le temps d’exécution de l’application (voir l’annexe B.3 pour un


exemple détaillé qui montre la tension entre les trois objectifs de l’optimisation de code).

I.5 Conclusion
Nous avons abordé dans ce chapitre quelques optimisations de programmes nécessaires
pour améliorer les performances des applications et réduire leur temps d’exécution. Nous
nous sommes focalisés particulièrement sur les optimisations de boucles, auxquelles nous
avons rajouté l’optimisation du calcul redondant et l’optimisation de réorganisation de
code, car elles sont utilisées dans le compilateur Halide.

De cette étude, il ressort qu’une optimisation n’est pas nécessairement bénéfique pour
un programme ou pour un segment de programme, et donc son application doit être réflé-
chie. L’utilisation d’une optimisation est soumise à plusieurs contraintes qui prennent en
considération : les propriétés de l’architecture d’exécution (la taille du registre vectoriel, le
nombre de processeurs), l’impact de chaque optimisation de façon générale, et la relation
entre les différentes optimisations. En outre, il existe des optimisations qui ne sont pas
censées améliorer directement les performances, mais elles représentent un prétraitement
pour l’application d’autres optimisations.

De ce fait, il apparait distinctement que l’optimisation de programmes n’est pas une


tâche simple. C’est un travail qui consomme énormément de temps pour le programmeur
afin d’arriver à la bonne combinaison d’optimisations, celle qui utilise efficacement les pro-
priétés de l’architecture matérielle d’exécution et qui par ailleurs diminue au maximum
le temps d’exécution.

Pour faciliter la tâche des programmeurs, les compilateurs ont été conçus pour opti-
miser de façon automatique les programmes développés. Dans le chapitre suivant, nous
examinerons des approches utilisées par les compilateurs pour automatiser le processus
d’optimisation de programmes.

11
Chapitre II

Approches d’optimisation automatique


pour les compilateurs.

II.1 Introduction
Dans le chapitre précédent, nous avons abordé quelques optimisations récurrentes et
qui peuvent être appliquées à un programme, dans le but d’améliorer son temps d’exé-
cution. Nous avons constaté que la tâche d’optimisation d’un programme n’est pas facile
dû à l’interdépendance entre les instructions du programme, l’ordre et l’organisation des
instructions ainsi que les propriétés de l’architecture matérielle d’exécution.

Pour soulager les programmeurs de la tâche d’optimisation, il a été intégré aux compi-
lateurs des techniques qui cherchent la meilleure combinaison d’optimisations à appliquer
pour un programme. Mais cette tâche constitue un problème NP-difficile car l’espace de
recherche des optimisations pouvant être appliquées à un programme est immense, et le
test de toutes les combinaisons d’optimisations prend un temps exponentiel [Stephenson
et al., 2003].

Dans la littérature, il existe six approches pour l’optimisation automatique de pro-


grammes. Dans ce chapitre nous présentons les quatre approches que nous jugons utiles
pour notre travail : l’approche exploratrice, l’approche analytique, l’approche prédictive
et l’approche par apprentissage automatique. Les deux approches non abordées sont :
l’approche basée sur les directives et l’approche basée sur la programmation linéaire.

II.2 Approche exploratrice


En compilation, cette approche est connue sous le nom d’Autotuning empirique. Elle
consiste à parcourir un espace d’implémentations pour un même programme, tester cha-
cune à part, mesurer leurs temps d’exécution, pour enfin choisir l’implémentation qui
minimise le temps d’exécution [Bergstra et al., 2012].

L’espace des implémentations est régi par l’ensemble des optimisations que le compi-
lateur peut appliquer sur un programme, ainsi que les paramètres de chacune des optimi-
sations. Par exemple : la taille de tuilage, le facteur de déroulage, le facteur de vectori-
sation [Hall, 2011]. Ces paramètres auront plusieurs valeurs possibles, et chaque type de

12
Chapitre II. Approches d’optimisation automatique pour les compilateurs.

paramètre se verra combiner avec d’autres pour former les différentes combinaisons d’op-
timisation. Ces combinaisons seront, ensuite, appliquées sur le programme à optimiser,
donnant naissance à des implémentations (des variantes pour le même programme) [Park
et al., 2011]. Pour simplifier, on peut considérer qu’une combinaison d’optimisations est
un vecteur à éléments binaires O=({0,1}n), où chaque oi , appartenant à O, fait référence
à une optimisation i qui peut être appliquée au programme. Si oi = 1, alors l’optimisa-
tion i est effectivement appliquée au programme. Sinon, elle ne l’est pas. Par conséquent,
l’espace de toutes les combinaisons d’optimisations est de 2n (voir figure 8).

Figure 8: Le parcours exhaustif des combinaisons de quatre optimisations pour un programme


donné.

II.2.1 Stratégies d’exploration d’espace


Plusieurs stratégies d’exploration d’espace des optimisations ont été implémentées,
parmi lesquelles il y a la recherche exhaustive, les algorithmes génétiques et la recherche
aléatoire.

Recherche exhaustive : ATLAS est une bibliothèque spécialisée dans l’optimisa-


tion automatique du programme de multiplication de matrices de type : C = a*A*B +
b*C. L’autotuning appliqué par ATLAS vise à trouver un certain nombre de paramètres
pré-élaborés qui sont susceptibles d’améliorer la multiplication de matrices sur l’architec-
ture d’exécution cible. Pour ce faire, ATLAS procède à une recherche exhaustive dans
des plages de valeurs pour tous ses paramètres d’optimisation pré-élaborés, et cela pour
différentes tailles de matrices A et B [Whaley and Dongarra, 1998].

Algorithme génétique : Il a été utilisé dans [Ragan-Kelley et al., 2013] pour l’optimi-
sation automatique des programmes écrits dans le langage Halide. En effet, dans [Ragan-
Kelley et al., 2013], on considère qu’une combinaison d’optimisations, applicable au pro-
gramme à optimiser, est un individu dont la fitness est mesurée par le temps d’exécution
du programme auquel on applique cette combinaison d’optimisations. L’algorithme d’op-
timisation automatique commence par générer une population aléatoire d’individus sur
laquelle il applique les différents opérateurs de l’algorithme génétique 1 pendant N itéra-
1. mutation, croisement, sélection ...etc.

13
Chapitre II. Approches d’optimisation automatique pour les compilateurs.

tions et renvoie à la fin la meilleure combinaison d’optimisations explorée.

II.2.2 Modes d’utilisation de l’approche exploratrice


L’approche exploratrice peut être utilisée suivant l’un des deux modes : le mode en
ligne et le mode hors ligne [Tiwari et al., 2009].

Mode en ligne : On est sur ce mode lorsque l’exploration des combinaisons d’op-
timisations se fait à l’exécution du programme, c’est-à-dire, au moment où toutes les
informations sur le programme sont disponibles.

Mode hors ligne : On dit que l’Autotuning est appliqué en mode hors ligne, lorsqu’il
se fait en temps de compilation où on ne dispose pas de toutes les données utilisées au
cours de l’exécution.

Evidemment, l’autotuning en ligne arrive à des programmes optimisés meilleurs que


ceux obtenus par l’autotuning hors ligne, car toutes les informations sur le programme
sont disponibles. Hors, dans le mode en ligne, l’exécution du programme va être retardée
jusqu’à trouver les bonnes optimisations à appliquer. Contrairement à l’autotuning hors
ligne qui ne dispose pas de toutes les informations du programme mais qui est exécuté à
la compilation. Donc au moment de l’exécution de ce dernier, c’est sa version optimisée
(déjà prête) qui va être exécutée et aucun temps supplémentaire ne sera consommé à part
le temps d’exécution du programme optimisé [De Mesmay et al., 2010].

II.2.3 Challenges des techniques basées sur l’approche explora-


trice
L’approche exploratrice est soumise à un grand espace de recherche représenté par
les différentes optimisations applicables au programme. Cet espace de recherche explosif
entraine un temps de traitement énorme qui fait que la recherche exhaustive devienne
prohibitive. Pour pouvoir y remédier, on fait appel à des techniques d’exploration d’es-
pace intelligentes, qui n’explorent pas la totalité des combinaisons d’optimisations. Ces
stratégies, connues sous le nom d’heuristique, font de sorte à approcher les combinaisons
d’optimisations susceptibles d’améliorer le temps d’exécution du programme à optimi-
ser [Ansel et al., 2014].

II.3 Approche analytique


L’approche analytique dans le domaine de l’optimisation implique la mise en place d’un
modèle représenté sous forme d’un algorithme exacte ou une fonction mathématique. Cette
fonction vise à estimer le bénéfice d’appliquer chaque optimisation sur un programme en
entrée, en essayant de prendre en considération plusieurs paramètres, comme les carac-
téristiques du programme, les choix d’optimisation déjà établis et les caractéristiques de
l’architecture matérielle d’exécution [Park et al., 2011]. Contrairement à l’approche explo-
ratrice, l’approche analytique est jugée rapide car elle n’exécute pas les variantes du code
(le code avec différentes combinaisons d’optimisations) pour mesurer leur performance,

14
Chapitre II. Approches d’optimisation automatique pour les compilateurs.

mais cette dernière est estimée par le modèle.

II.3.1 Objectifs d’un modèle analytique


Les modèles analytiques visent parfois à déterminer une bonne valeur pour une seule
optimisation ou un petit sous-ensemble d’optimisations pour maîtriser les différentes in-
terdépendances 2 [Allen and Kennedy, 2002]. Mais, il existe d’autres modèles analytiques
qui considèrent plusieurs optimisations à la fois et visent à trouver la bonne combinaison
qui optimise au mieux le programme [Mullapudi et al., 2016]. Au fait, la conception d’un
modèle analytique reflète la compréhension du concepteur de l’objectif de l’optimisation,
son impact sur le programme, et les interdépendances qu’elle présente avec les autres op-
timisations [Epshteyn et al., 2005].

II.3.2 Paramètres en entrée du modèle


La technique d’optimisation automatique développée dans [Mullapudi et al., 2016] se
base sur un modèle qui prend en entrée plusieurs informations sur l’architecture d’exé-
cution (temps d’accès à la mémoire, taille du cache, taille du registre vectoriel et le seuil
de parallélisme) pour générer une combinaison d’optimisations performante pour un pro-
gramme écrit dans le langage Halide. Le modèle utilise ces entrées pour conduire des
estimations sur la profitabilité d’une optimisation et décider de celles à appliquer sur un
programme Halide. Ces optimisations concernent principalement les transformations de
boucles que nous avons exposées dans le premier chapitre (cette technique d’optimisation
sera présentée en détails au niveau de la section III.5).

Plusieurs modèles analytiques ont été proposés dans [Allen and Kennedy, 2002] afin
de choisir une bonne valeur pour une seule optimisation uniquement : un modèle pour
le choix d’une bonne taille de tuilage, un autre pour le choix d’une bonne optimisation
d’interversion de boucles ...etc. Selon [Allen and Kennedy, 2002] l’optimisation d’inter-
version de boucle est une optimisation qui a pour objectif de maximiser l’utilisation de
la mémoire cache et des registres. Donc, le modèle s’est basé sur l’estimation du taux de
défaut de cache et ne reçoit en entrée que la taille d’une ligne de cache (voir l’annexe F
pour une explication détaillée du modèle d’interversion de boucle).

II.3.3 Challenges des techniques basées sur l’approche analytique


Les compilateurs de nos jours sont développés pour tourner sur des machines dont
les architectures matérielles varient intrinsèquement. En effet, certaines architectures
comptent plusieurs niveaux de cache : L1, L2 et L3, alors que d’autres n’en disposent
même pas. De même, les machines diffèrent de par leur jeu d’instructions et de par leur
type de processeur (processeurs CISC pour les jeux d’instructions complexes, les proces-
seurs RISC pour les jeux d’instructions réduits et les processeurs VLIW pour les jeux
d’instructions longs). Le développement d’un modèle qui décide des optimisations à ap-
pliquer exige une maîtrise à propos de l’interdépendance entre les choix des optimisations,

2. les interdépendances entre les optimisations, l’architecture d’exécution et la structure du code

15
Chapitre II. Approches d’optimisation automatique pour les compilateurs.

l’effet de chacune d’elles, et son impact sur les autres. Par conséquent, un modèle ana-
lytique robuste doit être complexe, car il prend nécessairement un grand ensemble de
paramètres en considération.

II.4 Approche prédictive


Cette approche combine entre les deux approches abordées ci-dessus, l’analytique et
l’exploratrice, dans le but de mener des recherches efficaces dans l’espace des optimisa-
tions. L’approche prédictive a le même principe que celui de l’approche exploratrice : elle
parcourt un espace de combinaisons d’optimisations, évalue et mesure la performance de
chacune pour enfin renvoyer la meilleure trouvée. Mais au lieu d’explorer aveuglement
l’espace de recherche, cette approche a été enrichie par un modèle, qui dirige et concentre
l’espace de recherche autour des optimisations opportunes. Cependant, la conception du
modèle est une tâche qui n’est pas simple, car elle exige des connaissances approfondies
sur le problème en question.

Une technique basée sur l’approche prédictive a été conçue pour l’optimisation auto-
matique de la multiplication de matrices, afin de réduire le temps pris par ATLAS 3 durant
les décisions d’optimisation. Cette technique combine entre l’approche exploratrice d’AT-
LAS et l’approche analytique développée dans [Yotov et al., 2003] pour l’optimisation
automatique de la multiplication de matrices. L’approche exploratrice implémentée dans
ATLAS cherche de façon presque exhaustive des paramètres pré-élaborés pour l’optimi-
sation : taille du tuilage de premier niveau, taille du tuilage de second niveau et le facteur
de déroulage. Ce qui prend énormément de temps pour choisir les bons paramètres d’op-
timisation, c’est le fait de tester toutes les combinaisons de ces paramètres, les compiler
et les exécuter. L’approche prédictive a été conçue pour utiliser les facteurs estimés par le
modèle analytique de [Yotov et al., 2003] et les tester sur le programme de la multiplica-
tion de matrices. Ensuite, cette technique prédictive procède à un ensemble de tests (sur
un espace beaucoup plus restreint que celui d’ATLAS) en choisissant d’autres facteurs, et
tentant d’atteindre le meilleur facteur de tuilage. La figure 9 montre la courbe de variation
de la performance du programme de multiplication de matrice en fonction des tailles de
tuilage adjacentes à celle retournée par le modèle de [Yotov et al., 2003].
Les challenges de cette technique combinent entre ceux de la technique exploratrice
et ceux de la technique analytique. La difficulté de cette technique réside dans la qualité
du modèle car si le modèle n’est pas bien étudié, il peut centrer l’espace de recherche
autour des combinaisons d’optimisations de mauvaise qualité, ou donner un sous-espace
à explorer qui reste toujours assez grand.

II.5 Approche basée sur l’apprentissage automatique


Cette approche a vu le jour pour remédier aux problèmes de l’approche exploratrice,
qui prend beaucoup de temps pour décider des optimisations à appliquer sur un pro-
gramme. Les techniques d’optimisation basées sur l’apprentissage automatique sont fon-
dées sur l’idée de construction d’un modèle pour optimiser de façon automatique les

3. La bibliothèque d’optimisation automatique qui se base sur la recherche exhaustive afin d’optimiser
le programme de multiplication de matrices.

16
Chapitre II. Approches d’optimisation automatique pour les compilateurs.

Figure 9: La variation de la performance du programme de multiplication de matrices en fonction


de la taille de tuilage appliquée au programme (sur deux architectures matérielles de test : Sparc
et SGI).

programmes. Une fois le modèle construit, il doit pouvoir généraliser et décider des op-
timisations à appliquer pour un nouveau programme en entrée (qui n’appartient pas à
l’ensemble d’apprentissage). Cela consiste à analyser les caractéristiques du programme à
optimiser et à les corréler avec celles des programmes de l’ensemble d’apprentissage, pour
enfin lui appliquer les optimisations correspondantes (voir figure 10).

Figure 10: Utilisation du modèle basé sur l’apprentissage automatique pour l’optimisation auto-
matique de programmes.

Le modèle est établi à partir d’une base de données d’apprentissage constituée de


programmes pré-optimisés par les meilleures combinaisons d’optimisations, qu’on définit
par un ensemble de caractéristiques et auxquelles on rajoute les optimisations qui leur
sont appliquées (voir figure 11).

Figure 11: La base de données d’apprentissage pour l’optimisation automatique des programmes.

17
Chapitre II. Approches d’optimisation automatique pour les compilateurs.

II.5.1 Caractérisation des programmes


Le choix des propriétés qui caractérisent les programmes constitue l’un des problèmes
majeurs lorsque l’apprentissage automatique est appliqué. En effet, il existe trois carac-
térisation pour le programme.

Caractérisation statique repose sur l’extraction d’information à partir du code


source du programme. Par exemple, [Agakov et al., 2006] se concentre particulièrement
sur l’optimisation de boucle. Il utilise 33 caractéristiques statiques qui décrivent la boucle
à optimiser : le nombre d’instructions arithmétiques dans la boucle, la profondeur de la
boucle, l’étendue de la boucle ...etc (voir figure 12 pour le reste des caractéristiques). [Aga-
kov et al., 2006] veut distinguer les cinq bonnes optimisations parmi celles exposées dans
la figure 13 pour chaque programme optimisé.

Figure 12: L’ensemble des propriétés qui caractérisent les programmes de l’ensemble d’apprentis-
sage dans la technique d’Agakov [Agakov et al., 2006].

Figure 13: L’ensemble des optimisations appliquées aux programmes de l’ensemble d’apprentis-
sage, dans la technique d’Agakov [Agakov et al., 2006].

18
Chapitre II. Approches d’optimisation automatique pour les compilateurs.

Caractérisation dynamique collecte des informations sur le programme en cours


d’exécution, pour caractériser son comportement dynamique sur l’architecture d’exécution
(ces informations sont dites compteurs de performance) [Park et al., 2011]. Les caracté-
ristiques utilisées généralement sont : le nombre d’erreurs de prédiction de branchement,
le nombre de défauts de cache, le nombre de succès cache ... etc. L’utilisation de cette ca-
ractérisation nécessite d’exécuter le programme (et l’exécuter plusieurs fois pour extraire
les caractéristiques qu’il faut) et c’est ce qui fait perdre beaucoup de temps.

Caractérisation hybride s’apprête mieux pour caractériser un programme car elle


utilise les deux types de caractérisation à la fois. De plus, son utilisation a donné de bonnes
performances dans plusieurs travaux d’optimisation automatique. Par exemple, [Ashouri
et al., 2016] utilise une caractérisation hybride où les caractéristiques statiques sont ex-
traites à partir du framework MilePost [MilePost, 2018] et les caractéristiques dynamiques
du programme sont extraites à partir du framework MICA. MICA est un outil utilisé pour
la caractérisation dynamique des programmes, il se restreint aux caracatéristiques indé-
pendantes de la machine comme : la moyenne d’accès aux registres par instruction, la
prédiction de branchement et d’autres informations [Hoste and Eeckhout, 2007].

II.5.2 Algorithme d’apprentissage


Plusieurs algorithmes d’apprentissage automatique et d’apprentissage profond ont été
utilisés pour l’établissement du modèle d’optimisation automatique de programmes.

Les SVM (Machines à vecteur de support) ont été employées pour établir un modèle
qui décide de la pertinence d’application d’une optimisation oi sur le programme P [Park
et al., 2011]. La sortie de ce modèle est l’une des deux classes "vrai" ou "faux" : "vrai"
pour dire que l’optimisation oi est bénéfique pour P, "faux" sinon.

La régression linéaire a été utilisée pour prédire l’accélération d’un programme P


suite à son optimisation par un ensemble de n optimisations. Le modèle reçoit en entrée
les caractéristiques dynamiques du programme P et l’ensemble des optimisations qu’on
veut lui appliquer, et donne en sortie l’accélération du programme optimisé par rapport
à sa version naïve.

Les réseaux de neurones ont été utilisés pour prédire le temps d’exécution d’un
programme C/C++ optimisé par un tuilage à 3 niveaux au maximum. Chaque facteur de
tuilage peut prendre une valeur parmi 22 facteurs de tuilage différents, donc l’espace des
optimisations est de : 223 = 10648 combinaisons. Parmi ces combinaisons, un ensemble
de 530 combinaisons des facteurs de tuilage sont choisies aléatoirement, dont 90% sont
utilisées pour l’apprentissage et 10% pour le test de validité du modèle. Pour chaque
programme à optimiser, on génère un modèle à base de réseau de neurones. Le réseau
de neurones développé est constitué de 3 couches seulement : la couche d’entrée avec 3
neurones, la couche de sortie avec un seul neurone et une couche intermédiaire avec 30
neurones. Le modèle reçoit en entrée 3 entiers qui sont les facteurs de tuilage (T1, T2,
T3) à appliquer sur le programe P et il renvoit en sortie le temps d’exécution estimé de
ce programme optimisé par (T1, T2, T3) [Rahman et al., 2010].

19
Chapitre II. Approches d’optimisation automatique pour les compilateurs.

II.5.3 Challenges des techniques basées sur l’apprentissage auto-


matique
Plus les programmes de l’ensemble d’apprentissage sont de même type (domaine d’uti-
lisation, nombre de fonctions utilisées, complexité des fonctions utilisées) que ceux de l’en-
semble de tests mieux c’est. Dans le cas contraire, on peut tomber sur des optimisations qui
n’améliorent en aucun cas notre programme. Les modèles utilisés dans l’approche basée
sur l’apprentissage automatique peuvent être soumis au phénomène de sur-apprentissage,
où le modèle apprend parfaitement des programmes pré-optimisés. Par conséquent, le
modèle ne peut plus généraliser pour de nouveaux programmes [Stephenson et al., 2003].

II.6 Comparaison entre les approches d’optimisation


automatique
Dans le tableau VI, nous avons rassemblé les différentes approches d’optimisation
automatique abordées dans ce chapitre. Nous les avons comparé selon leur niveau d’auto-
matisation, leur complexité de conception et le temps de calcul pris par chacune d’elles.
Le niveau d’automatisation indique le degré d’implication du programmeur dans la dé-
signation des optimisations à appliquer pour le programme. Quant à la complexité de
conception, elle indique l’effort conceptuel fait par le développeur de l’approche d’optimi-
sation.

Tableau I: Tableau comparatif entre les approches d’optimisation automatique utilisées dans les
compilateurs

Critère /
Automatisation Complexité de conception Temps de calcul
Approche
Approche
Automatique Facile Exponentiel
exploratrice
Approche
Automatique Difficile Très réduit
analytique
Plus petit que celui
Moins difficile
Approche prédictive Automatique de l’exploratrice plus grand
que l’analytique
que celui de l’analytique.
tout dépend de la
Moins difficile que celle
Approche AA Automatique taille de l’ensemble
de l’analytique
d’apprentissage

L’approche exploratrice consiste à parcourir l’ensemble des optimisations applicables


au programme, tester et mesurer chacune d’elles pour enfin renvoyer la meilleure. Ce-
pendant, vu les contraintes de temps d’exécution qu’elle impose, les chercheurs se sont
penchés sur de nouvelles approches, comme l’approche analytique. Cette dernière se base
sur un modèle qui prend en considération plusieurs paramètres avant de déterminer les
optimisations à appliquer. Néanmoins, cette approche n’est pas simple, car le modèle doit
être complexe pour donner de bons résultats. Afin de tirer bénéfice de ces deux approches,
elles ont été fusionnées pour former l’approche prédictive. Cette approche se base sur un
modèle, afin d’orienter la recherche vers un espace de combinaisons d’optimisations op-
portun pour ensuite l’explorer.

20
Chapitre II. Approches d’optimisation automatique pour les compilateurs.

Le modèle reste toujours difficile à concevoir ; c’est alors que l’approche basée sur
l’apprentissage automatique a vu le jour. Cette approche cherche à générer le modèle
de façon automatique à partir d’une base de données d’apprentissage constituée de pro-
grammes pré-optimisés. Le modèle généré sera capable ensuite d’optimiser de nouveaux
programmes ne figurant pas dans la base de données d’apprentissage. Mais, cette approche
a ses propres contraintes parmi lesquelles on trouve la difficulté du choix des caractéris-
tiques du programmes, et l’algorithme d’apprentissage automatique appliqué.

II.7 Conclusion
Nous avons abordé dans ce chapitre les différentes approches utilisées pour l’automa-
tisation du processus d’optimisation dans les compilateurs, en exposant le principe de
chacune d’elles, et énumérant les challenges rencontrés à l’emploi de chaque approche.

L’objectif de ce travail est de réaliser une méthode d’optimisation automatique pour


les programmes Halide. Donc, nous présentons dans le chapitre suivant, le langage de
programmation et compilateur Halide ainsi que les techniques d’optimisation automatique
conçues pour ce compilateur.

21
Chapitre III

Halide et ses techniques d’optimisation


automatique.

III.1 Introduction
Halide est un nouveau langage et compilateur apparu en 2012, embarqué sur le C++ et
spécifique au domaine du traitement d’images, qui a découplé la définition de l’algorithme,
de ses optimisations (schedule) pour produire un code lisible dans lequel les optimisations
n’affectent pas le fonctionnement et les sorties de l’algorithme [Ragan-Kelley et al., 2012].
L’application des optimisations sur les programmes écrits dans un langage généraliste,
comme le C ou Java, peut nuire à son fonctionnement, le rendre illisible et difficilement
maintenable. Contrairement à Halide, où on peut parcourir plusieurs combinaisons d’op-
timisations sans perturber le fonctionnement du code.

Dans ce chapitre, nous introduisons le compilateur Halide en commençant par présen-


ter la manière dont ses algorithmes sont définis, ensuite nous passons aux optimisations
qui sont présentes et pré-implémentées dans ce compilateur. En dernier, nous nous inté-
ressons aux méthodes d’optimisation automatique développées pour ce compilateur qui
visent à générer de façon automatique le schedule adéquat pour un algorithme Halide.

III.2 Algorithme dans Halide


Dans la partie algorithme, le programmeur implémente les fonctionnalités de son pro-
gramme à travers une suite d’instructions Halide qui manipulent des fonctions. Une fonc-
tion Halide est équivalente à une boucle imbriquée de profondeur égale (ou dépassant) le
nombre de variables manipulées par la fonction, où chaque niveau de boucle est indexé
par une des variables de la fonction 1 .

Par exemple, F(x,y) = x+y est une instruction Halide qui manipule une seule fonction
F et qui prend en paramètre les variables x et y. F est équivalente à une boucle imbriquée
de profondeur 2 indexée par les variables x et y (voir le pseudo-code de la figure 14).
Les algorithmes de traitement d’images sont caractérisés par des pipelines profonds
où chaque étage (fonction) implémente un traitement d’images spécifique. En effet, un
algorithme de traitement d’images en Halide n’est pas constitué d’une seule fonction mais

1. Dans ce rapport, la variable et le niveau de boucle d’une fonction Halide veulent dire la même chose.

22
Chapitre III. Halide et ses techniques d’optimisation automatique.

Figure 14: Pseudo-code équivalent à l’appel F(x,y) = x+y dans Halide

d’un ensemble de fonctions où chacune implémente un étage du pipeline et peut servir


d’entrée pour les autres étages (fonctions). Les étages d’un pipeline sont consommateurs
ou producteurs et peuvent être à la fois producteurs et consommateurs 2 .

Par exemple, le traitement qui consiste à flouter une image nécessite trois fonctions
Halide pour l’implémenter : la fonction input pour référencer l’image en entrée, la fonction
Blurx pour référencer l’image après la première opération de floutage et en dernier la
fonction Blury pour référencer l’image après la seconde opération de floutage. L’exemple
de la figure 15 montre le code Halide équivalent au traitement de floutage.

Figure 15: Code Halide pour flouter une image

La première ligne du code calcule la fonction Blurx qui présente la moyenne de chaque
trois pixels adjacents de l’image input. Quant à la seconde ligne du code, elle calcule la
fonction Blury qui est à son tour définie par la moyenne de chaque trois pixels adjacents
de l’image Blurx. Input est productrice pour Blurx qui est à son tour consommatrice et
Blurx est productrice pour Blury qui est à son tour consommatrice. L’algorithme Halide
de la figure 15 implémente le pipeline de traitement d’images exposé dans la figure 16.

Figure 16: Pipeline équivalent au code de la figure 15

La partie algorithme ne décrit que les traitements qui doivent être réalisés par le
programme, et ne spécifie aucun ordre sur l’évaluation de chaque fonction du pipeline. On
parle alors d’une implémentation naïve, où le calcul des valeurs des fonctions se fait en
mode inline (calcul par substitution de fonctions) et aucune optimisation n’est appliquée
sur le programme. Pour optimiser un programme Halide, l’utilisateur doit introduire des
optimisations au niveau de la partie schedule 3 .

2. Un étage producteur est utilisé pour le calcul d’un autre étage qu’on appelle consommateur à son
tour.
3. Planning en français, nous optons pour l’appellation schedule tout au long de ce rapport.

23
Chapitre III. Halide et ses techniques d’optimisation automatique.

III.3 Schedule dans Halide


Après avoir exprimé l’ensemble des fonctions du pipeline, l’utilisateur doit insérer des
optimisations au niveau de la section schedule pour améliorer le temps d’exécution de son
programme [Ragan-Kelley et al., 2013].
Les optimisations dans Halide sont de deux types : des optimisations à un seul étage
(pour une seule fonction) et des optimisations à deux étages (une fonction productrice
par rapport à une de ses fonctions consommatrices) (voir figure 17).

Figure 17: Optimisations dans Halide (le nom des optimisations et leur équivalent dans le langage
Halide).

III.3.1 Optimisations à un seul étage


Ces optimisations sont appliquées sur une seule fonction pour organiser le calcul de
ses données. Elles sont en nombre de sept (elles représentent un sous-ensemble des opti-
misations abordées dans le chapitre I).

Le tableau II présente la majorité des optimisations Halide applicables au niveau d’un


seul étage. Dans ce tableau, nous avons montré la syntaxe d’application de chaque op-
timisation, ses paramètres et son principe, tout en indiquant le nom de l’optimisation
dans le jargon de l’optimisation de code. Par exemple, pour appliquer l’optimisation d’in-
terversion de boucle sur la fonction f, il faut saisir dans la partie schedule l’instruction
f.reorder(x,y). Cette dernière a pour but d’intervertir les deux niveaux de boucle x et y de
la fonction f, pour positionner le niveau de boucle x dans le niveau le plus interne et le ni-
veau y dans le niveau de boucle le plus externe. On remarque que toutes les optimisations
commencent pas le nom de la fonction (étage) sur laquelle l’optimisation est appliquée.
Mais, elles diffèrent de par les valeurs qu’elles prennent en paramètres, car chacune a son
propre objectif.

24
Tableau II: Tableau récapitulant les optimisations Halide qui sont appliquées sur un seul étage

Optimisation
Paramètres Signication Equivalent
Halide
dans le jargon
Changer l’ordre des niveaux
- f : la fonction de boucle de f en commen-
f.reorder(x,y) - x et y : deux niveaux çant par la boucle la plus Interversion de
de boucle de f. interne x ensuite la plus boucle.
externe y.
- f : la fonction Paralléliser le niveau de bou-
f.parallel(y) Parallélisation
- y : un niveau de boucle de f. cle de f, dont l’indice est y.
de boucle.
- f : la fonction Vectoriser le niveau de bou-
f.vectorize(x) Vectorisation de
- x : un niveau de boucle de f. cle de f, dont l’indice est x.
boucle.
- f : la fonction Dérouler le niveau de boucle
f.unroll(x) Déroulage de
- x : un niveau de boucle de f. de f, dont l’indice est x.
boucle.
Fusionner les deux niveaux
- f : la fonction
de boucle de f, dont les indi-
- x, y : deux niveaux de boucle
f.fuse(x,y,t) ces sont x et y, pour former Coalescence de
de f.
une nouvelle boucle dont boucle.
- t : nouveau niveau de boucle
l’indice est t.
Éliminer le niveau de boucle
- f : la fonction.
x et produire deux nouveaux
- x : niveau de boucle de f.
f.split(x,xo,xi,f ) niveaux de boucle dans cet Découpage en
- xi, xo : deux nouveaux ni-
ordre : xo, xi. Où l’étendue bande.
veaux de boucle pour f.
de xi est f1.
- f : la fonction. Éliminer x et y et produire
- x, y : deux niveaux de boucle de nouveaux niveaux de
f.tile(x,y,xo,yo,xi,yi,f1,f2) de f. boucle dans cet order : xo,yo Tuilage de
- xi, yi, xo, yo : les indices xi,yi. Où l’étendue de xi est boucle.

25
Chapitre III. Halide et ses techniques d’optimisation automatique.

des nouvelles boucles. f1 et l’étendue de yi est f2.


Chapitre III. Halide et ses techniques d’optimisation automatique.

III.3.2 Optimisations à deux étages


Ce type d’optimisation est appliqué à un étage producteur par rapport à l’un de
ses consommateurs, afin d’organiser le calcul et le stockage des valeurs de la fonction
productrice par rapport aux valeurs de ses fonctions consommatrices. Ces optimisations
sont au nombre de deux : la granularité de stockage et la granularité de calcul.

III.3.2.1 Granularité de calcul


Elle permet de définir l’ensemble des valeurs de f calculées avant le calcul d’une va-
leur de g (g est consommatrice de f). Cette opération est faite dans le but d’éviter le
phénomène de : breadth first où la totalité des valeurs de la fonction productrice f sont
calculées et stockées puis rechargées pour qu’elles soient consommées par g. En effet, dans
ce cas de figure, la mémoire cache est rapidement inondée de valeurs de f qui ne seront pas
utilisées rapidement pour le calcul de g et risquent d’être effacées du cache avant d’être
utilisées [Ragan-Kelley, 2014].

Pour spécifier la granularité de calcul d’une fonction productrice f par rapport à une
fonction consommatrice g qui a comme variables : x et y, on choisit entre : f.compute_at(g,x),
f.compute_at(g,y), f.compute_root().

– f.compute_at(g,x) : Soit E(g,x=i) = {ensemble des valeurs de g calculées pendant


l’itération i du niveau de boucle x}. En utilisant f.compute_at(g,x), toutes les valeurs
de f nécessaires au calcul de E(g,x=i) sont calculées avant chaque itération i de la
boucle x de g (voir figure 18).

– f.compute_root() : Organise le calcul en breadth first.

Figure 18: Pseudocode équivalent à l’application de l’optimisation « compute_at » à


granularité d’un point sur un programme Halide

III.3.2.2 Granularité de stockage


C’est une optimisation qui spécifie la plage des valeurs de f à stocker, pour qu’elles
soient réutilisées sans pour autant les recalculer. Par exemple : f.store_at(g,x), signifie
qu’au niveau de la boucle x de g, un buffer est alloué pour sauvegarder toutes les valeurs
de f nécessaires au calcul de E(g,x=i) pour toute itération i de la boucle x [Ragan-Kelley,
2014].

D’une part, plus la granularité de stockage est fine pour une fonction productrice f
par rapport à sa fonction consommatrice g, plus le programme aura tendance à faire du

26
Chapitre III. Halide et ses techniques d’optimisation automatique.

calcul redondant. En effet, les valeurs de f qui ont été déjà calculées mais non stockées
seront recalculées une fois que g en aura besoin une autre fois. Par contre, une granularité
de stockage fine assure que le buffer qui contient les valeurs de f tient dans le cache (et
c’est ce qui améliore la localité). D’autre part, plus la granularité de stockage est grande,
moins le programme fera de calcul redondant car les anciennes valeurs de f sont toujours
présentes dans le buffer alloué. Cependant, en allouant un buffer de taille importante,
il risque de ne pas tenir dans le cache (on risque de faire plusieurs transferts mémoire
principale - mémoire cache pour récupérer les données stockées) [Allen and Kennedy,
2002, Ragan-Kelley, 2014].

III.4 Techniques exploratrices pour l’optimisation au-


tomatique des programmes Halide
Deux techniques exploratrices ont été proposées pour l’optimisation automatique des
programmes Halide, dont l’une repose sur l’utilisation d’un algorithme génétique et l’autre
se base sur l’utilisation d’une combinaison de trois méta-heuristiques.

III.4.1 Technique basée sur l’algorithme génétique


C’est une technique exploratrice connue sous le nom d’AutoTuner Halide, elle fait appel
à un algorithme génétique pour trouver le schedule qui minimise le temps d’exécution du
programme Halide à optimiser. Les opérateurs de cet algorithme génétique emploient
des connaissances sur le domaine du traitement d’images afin de conduire des schedules
efficaces plus rapidement 4 . De plus, cette technique repose sur les hypothèses suivantes
pour réduire l’espace des schedules à explorer [Ragan-Kelley et al., 2013] :

– L’optimisation de fusion n’est pas considérée car elle n’est pas aussi bénéfique pour
le programme.

– Les étendues des boucles du programme et la taille des blocs ont été restreint à
des multiples de deux et générés aléatoirement car elles ont été considérées plus
bénéfiques pour le programme.

– Les niveaux de boucle de petite étendue ne sont pas tuilés car il a été considéré que
le tuilage n’était pas bénéfique pour les boucles qui ont déjà une petite étendue.

– Les niveaux de boucle dont l’étendue est inconnue se voient affectés des bornes
maximales et minimales aléatoires pour ne pas tester toutes les étendues de boucle
possibles pour chaque fonction du programme.

Opérateurs de l’algorithme génétique

– L’individu : est considéré comme un schedule dont la fitness est son temps d’exé-
cution une fois qu’il est appliqué sur le programme. Un individu est une séquence
de chromosomes représentée par l’ensemble des optimisations qui figurent dans le
schedule (voir figure 19).

4. Des optimisations qui donnent souvent des programmes performants.

27
Chapitre III. Halide et ses techniques d’optimisation automatique.

Figure 19: Individu, chromosome et population dans l’Autotuner de Halide.

– La population initiale est générée en affectant des schedules dits raisonnables (dans
l’annexe D.2 nous expliquons ce qu’est un schedule raisonnable et comment le trou-
ver), de façon stochastique à toutes les fonctions de l’algorithme.

– La sélection des parents se fait une fois que la population initiale générée. Deux
schedules de la population, dits schedules parents sont sélectionnés en utilisant la
stratégie de sélection par tournoi.

– Le croisement de deux parents est effectué pour générer deux nouveaux individus
(schedules), qu’on appelle : schedules enfants. Au cours du croisement, deux points
de chaque schedule parent sont choisis aléatoirement, on leur échange les optimi-
sations (chromosomes) qui se trouvent entre ces deux points, pour former les deux
individus enfants.

– La mutation : Une opération appliquée sur les nouveaux individus afin de les diver-
sifier. En premier une fonction f est choisie aléatoirement à partir du programme.
Ensuite, une opération de mutation parmi huit possibilités de mutation est appliquée
sur cette fonction.

Pour juger de son efficacité, cet autotuner a été appliqué sur un ensemble de pro-
grammes de traitement d’images, afin de les optimiser de façon automatique (voir le ta-
bleau III). Ensuite, le temps d’exécution de chaque programme optimisé par l’autotuner
est comparé avec celui du programme optimisé à la main par des experts. La comparaison
se fait en calculant l’accélération (le speedup) qui est donnée par la formule :

temps d’exécution du programme optimisé à la main


accélération = (III.1)
temps d’exécution du programme optimisé par l’autotuner.
Si l’accélération est supérieure à 1, alors l’autotuner a amélioré le temps d’exécution du
programme optimisé à la main. Sinon, la version optimisé à la main reste la meilleure.
Au niveau de tous les programmes de test, l’autotuner a pu construire des schedules
compétitifs par rapport à ceux construits par des experts en optimisation de programmes
et dans un temps assez court. Malgré ces résultats satisfaisants, cet autotuner qui était
intégré dans Halide a été abandonné dans ses prochaines versions à cause de sa complexité
de maintenance [Ansel, 2014].

28
Chapitre III. Halide et ses techniques d’optimisation automatique.

Tableau III: Benchmarks optimisés par l’autotuner et les résultats obtenus sur une machine de
type quad core Xeon W3520 x86 CPU

Nom du Nombre d’étages Temps pris par


Accélération
Benchmark du benchmark l’autotuner
Blur 2 1.2 11ms
Bilateral grid 7 4.4 36ms
Camera pipe 32 3.4 14ms
Interpolate 49 1.7 32ms
Local laplacian 99 1.7 113ms

III.4.2 Technique basée sur OpenTuner


« OpenTuner est un framework open source générique, pour la création d’autotuners
de programmes, multi-objectifs et spécifiques au domaine » [Ansel et al., 2014]. Il est dit
générique car il peut être utilisé comme générateur d’autotuners pour différents projets
d’optimisation, où il faut définir un espace de recherche et explorer ses configurations pour
trouver la combinaison optimale. Les autotuners qu’il génère sont dits multi-objectifs, car
la solution optimale recherchée peut satisfaire une condition d’optimalité à plusieurs ob-
jectifs comme la minimisation du temps d’exécution et la réduction de la consommation
en énergie ou d’autres combinaisons d’objectifs.

L’utilisateur doit donner en entrée pour OpenTuner : l’espace de recherche qui est mo-
délisé par un ensemble de paramètres, une fonction qui calcule la fonction objectif pour
toute solution candidate et les techniques d’exploration utilisées pour explorer l’espace de
recherche.

Autotuning de Halide dans OpenTuner


Pour autotuner les programmes Halide dans OpenTuner, il faut définir l’espace de
recherche des combinaisons d’optimisations. Cela est fait en associant à chaque type d’op-
timisation un type de paramètre OpenTuner (voir tableau IV) (voir annexe C.2 pour se
renseigner des différents paramètres prédéfinis dans OpenTuner et la signification de cha-
cun d’eux).

Ensuite, l’utilisateur introduit sa fonction objectif. Dans le cas de l’optimisation des


programmes Halide, l’objectif est de trouver le schedule qui minimise le temps d’exécu-
tion du programme à optimiser. En effet, pour chaque schedule (solution) candidat une
procédure particulière applique ce schedule sur le programme à optimiser et mesure son
temps d’exécution.

En dernier, l’utilisateur choisit les stratégies d’exploration d’espace des combinai-


sons d’optimisations. Dans OpenTuner, il existe plusieurs stratégies d’exploration pré-
implémentées qui travaillent en collaboration pour trouver le meilleur candidat dans l’es-
pace de recherche (voir annexe C.1 pour la collaboration entre les stratégies d’exploration
d’espace dans OpenTuner). Dans [Ansel, 2014], il apparaît que les stratégies d’explora-
tion utilisées pour le projet d’optimisation automatique des programmes Halide sont un
ensemble de 3 méta-heuristiques : la mutation avare, l’évolution différentielle et deux va-
riantes du grimpeur (Hill Climbing).

29
Chapitre III. Halide et ses techniques d’optimisation automatique.

Tableau IV: Tableau récapitulant les paramètres OpenTuner correspondants aux optimisations
Halide

Optimisation Paramètre OpenTuner


Explication
Halide correspondant
Au lieu de lui attribuer le paramètre
PermutationParameter, on lui
attribue ScheduleParameter car c’est
f.reorder(y,x, . . . ) ScheduleParameter une permutation avec la contrainte
de ne pas intervertir deux niveaux
de boucle issus d’un découpage
en bandes.
f.vectorize(x, fx) / Le facteur fx de vectorisation de
f.unroll(x, fx) / PowerOfTwoParameter déroulage et de découpage en bandes
f.split(x,xo,xi,fx) est une puissance de 2.
Cette optimisation prend la valeur
"vrai" si la parallélisation est
f.parallel(y) BooleanParameter
appliquée sur le niveau de boucle y
de la fonction f, "faux" sinon.
Nouveau paramètre Les plus complexes à modéliser, car
f.store_at(g,x) /
qui hérite de soumises à des contraintes
f.compute_at(g,x)
ScheduleParameter de dépendance.

La technique d’optimisation automatique des programmes Halide sur OpenTuner a été


testée sur trois benchmarks, où chacun d’eux a été testé sur une architecture d’exécution
différente. Le tableau V résume les résultats des trois benchmarks testés.
Tableau V: Benchmarks optimisés dans OpenTuner et les résultats obtenus sur chaque architecture
d’exécution

Nom du Temps pris Architecture


Nb. étages Accélération
Benchmark par OpenTuner d’exécution
Blur 2 1< <1.5 500s Core i5-3550
Wavelet 2 1< <1.5 1500s Core i7-3770
12 Cores of a du-
Bilateral 7 1< <1.5 20000s
al Socket Xeon

La technique a accéléré les benchmarks optimisés à la main avec une accélération entre
1 et 1.5 seulement mais cela est valable sur 3 différentes architectures. Contrairement, à
la première technique basée sur l’autotuner génétique qui n’a été exécutée que sur une
seule architecture d’exécution. Mais qu’en est-il pour les autres architectures d’exécution ?
peut-être que l’autotuner génétique ne pourra pas générer d’aussi bons résultats que ceux
qu’il a généré sur l’architecture de test.

III.4.3 Comparaison entre les deux techniques exploratrices


Le tableau VI présente les principaux points communs ainsi que les dissimilarités entre
la technique développée sous OpenTuner et l’algorithme génétique.

Vu que l’espace de recherche des schedules applicables à un programme Halide est


immense (impossible à l’explorer exhaustivement), les deux techniques restreignent l’en-
semble des optimisations appliquées au programme et emploient des méthodes approchées

30
Chapitre III. Halide et ses techniques d’optimisation automatique.

pour la recherche de bons schedule.

Contrairement à la première technique qui se base uniquement sur l’algorithme géné-


tique, celle basée sur OpenTuner utilise un ensemble de méta-heuristiques qui travaillent
en collaboration pour trouver un schedule de bonne qualité. Dans OpenTuner, il est pos-
sible de définir de nouvelles méta-heuristiques et les utiliser pour explorer l’espace des
schedules pour un programme, ce qui n’est pas le cas avec la première technique qui n’est
pas extensible.

Tableau VI: Tableau comparatif entre l’algorithme génétique et la méthode basée sur
OpenTuner pour la génération automatique de schedules pour un programme Halide

La technique basée La technique basée


sur l’algorithme génétique sur OpenTuner
Approche exploratrice
empirique
Limite l’espace de recherche
par des hypothèses
Approche basée sur l’utilisation des méthodes
approchées pour la résolution
La collaboration de
Un seul algorithme génétique
plusieurs techniques d’exploration
Une exploration orientée par des
Une exploration aveugle.
connaissances issues du domaine
Pas extensible. Facilement extensible

III.5 Technique analytique pour l’optimisation des pro-


grammes Halide
Devant le problème de l’explosion de l’espace de recherche, et le temps perdu lors de la
compilation et exécution de chaque schedule candidat, une technique basée sur l’approche
analytique a été proposée : Auto-scheduler. L’Auto-scheduler se base sur l’estimation
des performances de chaque décision d’optimisation (voir figure 20). Pour pouvoir faire
cela, il reçoit en entrée des informations sur l’architecture de la machine (taille du cache,
taille du registre vectoriel, coût d’un chargement mémoire) ainsi, que des informations
supplémentaires sur le programme (taille estimée des images en entrée et étendue des
boucles de chaque fonction) [Mullapudi et al., 2016].

Figure 20: Entrées et sorties de l’Auto-scheduler pour la génération automatique du schedule


optimal pour un algorithme Halide en entrée

31
Chapitre III. Halide et ses techniques d’optimisation automatique.

Le modèle de l’Auto-scheduler : Le modèle de l’Auto-scheduler repose sur l’idée


de former des groupes de fonctions où chaque groupe est optimisé à part afin d’augmenter
la localité producteur-consommateur et maximiser la réutilisation de données au sein de
chaque groupe. Quant à l’exécution entre les groupes, elle se fait en breadth first. Les
grandes phases du modèle sont décrites dans le pseudo-algorithme 1.

Algorithm 1 Pseudo-algorithme de l’Auto-scheduler


1: G = {}
2: for fonction f i dans P rogramme do
3: Tuiler fSi et la mettre dans un groupe singleton gi.
4: G = G gi
5: while Il existe des groupements potentiels avec un gain dans G do
6: Soit (g1, g2) le groupement potentiel avec le plus petit coût (plus grand gain).
7: Cout_fusion = le coût de la fusion de g1 et g2.
8: Cout_Non_fusion = le coût lors de l’absence de fusion entre g1 et g2.
9: if Cout_fusion > Cout_Non_fusion then
10: Ne pas fusionner g1 et g2
11: else
12: Fusionner g1 et g2.
13: Mettre à jour G avec g1 et g2 dans le même groupe.
14: for groupe gi dans G do
15: Soit fi la fonction de sortie de gi.
16: Choisir la meilleure interversion de boucle pour fi : celle qui améliore la réutilisation
des données dans fi (voir annexe D.1).
17: Dérouler et vectoriser les deux niveaux de boucle internes de petite étendue de fi.
18: Paralléliser le niveau de boucle externe de fi.

On doit définir ce qu’est un groupe, comment estimer le gain en performance produit


par une fusion potentielle de groupe et quels sont les facteurs de tuilage utilisés pour une
fonction.
– Un ensemble de fonctions peuvent appartenir au même groupe, si toutes les fonctions
de ce groupe servent à calculer une seule fonction, dite fonction de sortie du groupe.
Dans la figure 21, les fonctions C, D et E peuvent appartenir au même groupe car
une seule fonction de sortie les rassemble qui est E.

Figure 21: Pipeline de traitement d’images dont les étages sont groupés par
l’Auto-scheduler.

– La fusion de deux groupes g1 et g2, où g1 est producteur de g2, implique de trouver


une bonne taille de tuilage pour g2 et un bon choix des optimisations de granularité
de calcul/ stockage (les deux optimisations qui concrétisent le concept de fusion)
pour positionner g1 dans g2.

32
Chapitre III. Halide et ses techniques d’optimisation automatique.

– Une bonne taille de tuilage : le facteur de tuilage interne doit être assez petit et
multiple de la taille des registres vectoriels pour vectoriser. Le facteur de tuilage
externe est assez grand pour bénéficier du parallélisme et assez petit pour qu’une
tuile puisse tenir dans le cache.

– L’estimation du gain d’une fusion potentielle : Soient deux groupes (g1, g2) où g1
est fusionné potentiellement dans g2 avec f la fonction de sortie de g2. Le coût de
la fusion est donné par la formule :

Cout_F usion(g1, g2) = (C + coût_chargement_memoire ∗ D) ∗ N (III.2)

Où C représente le coût arithmétique de la fonction f et N son nombre de tuiles


(puisqu’elle est tuilée) 5 . D est le nombre de données consommées lors du calcul
d’une tuile de f.

– L’estimation du gain lors de l’absence de fusion : Le coût de l’absence de fusion


entre deux groupes g1 et g2 est donné par la formule :

Cout_N on_F usion(g1, g2) = C1+C2+coût_chargement_memoire∗Dg (III.3)

Où C1 et C2 sont les coûts arithmétiques associés à g1 et g2 respectivement et Dg


est l’ensemble des valeurs de g1 nécessaires au calcul de g2.

L’Auto-scheduler a été testé et validé avec un ensemble de 14 benchmarks sur la même


architecture d’exécution : Intel Xeon E5-2620 v3 CPU. Les benchmarks utilisés sont de
différents types : des benchmarks de traitement d’images, un benchmark de multiplication
de matrices et deux benchmarks implémentant un réseau de neurones profond.

L’optimisation automatique de ces benchmarks par l’Auto-scheduler a été comparée


avec leur optimisation à la main, avec leur implémentation naïve (sans optimisations),
avec deux implémentations issue chacune d’une variante d’autotuning et avec une pro-
cédure d’optimisation appelée PolyMage. Dans 8/14 des benchmarks, il apparaît que la
version optimisée à la main est mieux que celle optimisée par l’Auto-scheduler (mais reste
compétitive). De plus, dans tous les cas, les implémentations issues de l’autotuning sont
meilleures que celles issues de l’Auto-scheduler. Mais, d’après les tests, l’Auto-scheduler ar-
rive à construire des implémentations compétitifs dans un temps très court (dans quelques
secondes ou quelques millisecondes) comparant à ce que nécessite les techniques d’auto-
tuning.

III.6 Comparaison entre les techniques de l’approche


exploratrice et la technique de l’approche analy-
tique
Nous rassemblons dans le tableau VII, les différences constatées durant notre étude
entre les techniques d’optimisation automatique pour le compilateur Halide.

5. voir annexe D.3 pour plus de détails sur comment calculer le coût arithmétique d’une fonction.

33
Chapitre III. Halide et ses techniques d’optimisation automatique.

Tableau VII: Tableau comparatif entre les méthodes des deux approches : analytique et exploratrice

Approche / Critère Approche exploratrice Approche analytique

Temps de calcul Très grand Relativement court

N’importe quel
Environnement d’exécution Le même que celui du programme
environnement d’exécution
Exécute et calcule le temps Estime la performance de
Choix d’optimisation d’exécution du programme l’optimisation sur le
avec l’optimisation programme.
Efficace lors d’une recherche
Efficacité du modèle
Efficacité du système exhaustive ou une heuri-
implique sa complexité
stique bien paramétrée
Plus petit que celui de
Espace de recherche Généralement très grand l’approche exploratrice, car
il est guidé par le modèle.

La méthode analytique peut être exécutée sur une architecture A afin d’élaborer des
schedules optimaux pour une architecture B. En effet, il suffit d’introduire les paramètres
de l’architecture B en entrée du modèle au lieu des caractéristiques de l’architecture A.
Donc, la méthode n’est pas forcément exécutée sur le même environnement d’exécution
que celui pour lequel on optimise les programmes. Contrairement aux deux techniques ex-
ploratrices, qui compilent et exécutent les combinaisons d’optimisations sur l’architecture
d’exécution, elles doivent s’exécuter sur la même architecture pour laquelle on optimise
les programmes.

La technique analytique de l’Auto-scheduler est plus rapide en termes de temps d’exé-


cution par rapport aux deux techniques exploratrices (celle basée sur OpenTuner, et celle
basée sur l’algorithme génétique), car elle estime le bénéfice de chaque alternative d’opti-
misation sans l’exécuter. Néanmoins, le modèle basé sur l’approche analytique est moins
susceptible de donner de bonnes combinaisons d’optimisations, car il suit le même rai-
sonnement pour tous les programmes et toutes les architectures d’exécution. Les deux
techniques basées sur l’approche exploratrice sont stochastiques et peuvent donner de
bons schedules si elles s’exécutent pour longtemps.

III.7 Conclusion
Halide est un nouveau langage qui commence à gagner de l’ampleur grâce à sa sépara-
tion entre l’algorithme et ses optimisations et la simplicité d’expression des optimisations.

Dans ce chapitre, nous avons établi un état de l’art sur les travaux d’optimisation au-
tomatique pour le compilateur Halide. En effet, il existe trois travaux dans la littérature,
dont deux reposent sur l’approche exploratrice et une technique repose sur l’approche
analytique. Ces techniques ont été testées de façon particulière sur des programmes de
traitement d’images. Néanmoins, la technique analytique a été appliquée sur divers pro-
grammes comme la multiplication de matrice, la convolution et d’autres.

34
Chapitre III. Halide et ses techniques d’optimisation automatique.

Dans le chapitre suivant, nous arrivons à la partie contribution. Dans cette partie,
nous expliquons la méthode proposée pour l’optimisation automatique d’une classe des
programmes Halide.

35
Deuxième partie
Contributions
Chapitre IV

Conception

IV.1 Introduction
Ce projet de fin d’études fait partie des projets initiés par l’équipe de recherche Com-
mit du MIT. Le but global des projets de cette équipe est de développer des langages
qui permettent d’exprimer les programmes de façon simple et de pouvoir les optimiser
de façon automatique sur tout type d’architecture. L’un des compilateurs développés par
Commit est le compilateur Halide.

L’objectif de ce PFE est de développer, en utilisant une approche d’autotuning, un op-


timiseur automatique pour une classe de programmes Halide destinée à s’exécuter sur une
architecture à base de CPU. L’approche d’autotuning n’est pas pratique (prend beaucoup
de temps) lorsqu’elle est appliquée pour l’optimisation d’un programme de grande taille
(au-dela de 6 fonctions consommatrices). De ce fait, notre projet se limite à l’optimisa-
tion d’une classe de programmes ayant chacun un petit nombre de fonctions : la classe des
programmes implémentant les RNC 1 . En effet, cette classe est attrayante à optimiser car
elle regroupe un ensemble de programmes de petite taille, qui sont gourmands en termes
de temps d’exécution et qui deviennent de plus en plus utilisés grâce à la vulgarisation de
l’apprentissage automatique.

Notre travail consiste à mettre en place un système qui génére pour tous les pro-
grammes de la classe des RNC, un schedule compétitif à celui trouvé par un expert en
matière d’optimisation de programmes et cela dans un délai inférieur à 24 heures.

Dans ce chapitre, après avoir décrit le problème, nous présentons notre solution. Cette
solution est répartie en deux sections : la conception globale du système et la conception
détaillée de la stratégie de construction des schedules.

IV.2 Description du problème


Un programme écrit dans Halide est réparti en deux sections : la section algorithme
et la section schedule qui consiste en un ensemble d’optimisations à appliquer à l’algo-
rithme. La section algorithme est fixe, invariante et dépend uniquement du traitement
que le programmeur veut réaliser. Mais, la section schedule est variable. Elle inclut les

1. réseaux de neurones convolutifs, voir annexe G pour plus d’informations sur les réseaux de neurones.

37
Chapitre IV. Conception

différentes optimisations à appliquer au programme et représente la partie critique qui


détermine la performance de l’algorithme. La figure 22 illustre le même algorithme Halide
auquel on applique deux schedule différents, schedule 1 et schedule 2, donnant naissance
à deux programmes différents. Ces deux programmes renvoient les mêmes résultats mais
leurs temps d’exécution sont différents car les optimisations appliquées sur chacun d’eux
sont différentes.

Figure 22: Un algorithme Halide A auquel on applique deux schedules différents.

Afin d’optimiser un algorithme Halide A pour une architecture matérielle, il faut


construire un schedule S qui réduit au mieux le temps d’exécution de A sur l’architecture
matérielle en question. A ce stade, les questions qui se posent sont : comment évaluer un
schedule ? quelle est la fonction objectif associé à ce problème d’optimisation ? comment
générer et faire varier les optimisations pour un algorithme Halide en entrée ? et comment
aboutir à un bon schedule ?

IV.2.1 Codage du schedule


Un schedule est une entité qui rassemble toutes les optimisations, notées O, appliquées
sur l’algorithme A. Etant donné que nous nous sommes restreints au choix des meilleures
optimisations et non à leur ordre d’application, nous avons fixé l’ordre d’application des
optimisations dans le schedule. En effet, après des discussions avec des experts en matière
d’optimisation de programmes, nous avons opté pour l’ordre suivant : les tuilages, les
découpages en bandes, les interversions de boucle, les parallélisations, les vectorisations,
puis les déroulages et enfin les granularités de calcul et de stockage (voir figure VIII).
Durant la phase de construction des différents schedules, nous varions les valeurs que
peut prendre chaque optimisation, selon son type.

Tableau VIII: Modélisation de l’entité Schedule par un tableau d’entités, où chaque entité repré-
sente un type d’optimisation appliquée sur l’algorithme A.
T ile1 ... T ilei Split1 ... Splitj Reorder ... Fuse ... Parallel ... Vectorize ... Unroll ... compute_at ... store_at ...

38
Chapitre IV. Conception

IV.2.2 Fonction objectif


Un schedule S est associé à un algorithme Halide A. Pour évaluer un schedule S, on
doit appliquer S sur A afin de produire un algorithme optimisé A’. L’évaluation de S sera
donc le temps d’exécution de A’.

Ce problème d’optimisation consiste à trouver le schedule S associé à un algorithme A,


tel que S minimise le temps d’exécution de A sur une architecture d’exécution. Il n’existe
pas de contraintes liées aux schedules construits, mais, des contraintes liées à la technique
de résolution. En effet, notre système doit construire des schedules compétitifs à ceux
trouvés par les experts dans un laps de temps inférieur à 24 heures, et cela pour tous les
programmes de la classe concernée par l’optimisation automatique.

IV.3 Conception globale du système


Dans cette section, nous allons présenter l’architecture globale du système en expli-
quant ses différents composants.

IV.3.1 Architecture globale du système


L’architecture globale du système est présentée dans la figure 23. Notre système
consiste à trouver le meilleur schedule pour un programme Halide.

Figure 23: Schéma global de la solution

Le système utilise une méthode qui explore un certain espace de schedules pour un
algorithme Halide et qui renvoie à la fin du traitement le meilleur schedule rencontré.
Pour ce faire, l’utilisateur introduit d’abord le code source de l’algorithme Halide, ainsi

39
Chapitre IV. Conception

qu’un fichier d’annotation pour annoter l’algorithme et renseigner son contenu comme :
fonctions, variables, constantes . . . etc.
Selon la stratégie d’exploration de l’espace des optimisations, nous construisons alors
un sous-ensemble de schedules candidats. Chaque schedule S construit doit être évalué.
Suite à l’évaluation de S, nous le stockons ainsi que sa valeur d’évaluation dans une
base de données. La sauvegarde dans la base de données est une préparation pour la
phase d’apprentissage automatique où elle (la base de données) va constituer une base
d’apprentissage pour la construction d’un modèle qui optimise les programmes de façon
automatique. Lorsque la méthode s’arrête, elle renvoie le meilleur schedule trouvé ainsi
que son temps d’exécution 2 .

Afin de réaliser ces traitements, le système est décomposé en plusieurs modules, où


chacun a sa propre fonction :

– Extraction des informations du programme : Le premier module du système


consiste à lire le fichier d’annotation, introduit par l’utilisateur, pour récupérer un
ensemble d’informations comme le nom de l’algorithme, la taille et le type de son
buffer de sortie, les fonctions de l’algorithme, les variables de chaque fonction, leurs
étendues et d’autres informations que nous présentons dans la section IV.3.3.

– Construction des schedules : Une fois que les caractéristiques du programme


sont analysées, nous commençons par explorer des combinaisons d’optimisations
applicables au programme puis construire des schedules pour améliorer son temps
d’exécution.

– Test de performance : Lorsqu’un schedule S est construit, nous l’appliquons sur


l’algorithme A. Pour cela nous créons un nouveau fichier source, nous insérons de-
dans le code source de l’algorithme A et le code source du schedule S. Ensuite,
nous faisons appel au compilateur Halide pour compiler ce code source et générer
son exécutable. Nous lançons l’exécution de ce dernier et nous mesurons son temps
d’exécution (voir figure 24).

Figure 24: Test de performance d’un schedule construit sur le programme Halide en entrée.

2. Le temps d’exécution du programme à optimiser auquel est appliqué ce schedule.

40
Chapitre IV. Conception

– Sauvegarde dans la base de données : Après avoir mesuré le temps d’exécution


du schedule, nous insérons S ainsi que son temps d’exécution dans la base de données.

IV.3.2 Caractérisation des composantes du problème


Nous avons proposé une caractérisation des composantes de notre problème qui par-
ticipent à l’élaboration de la solution.

Un programme est une entité caractérisée par :


– son nom,

– la liste des fonctions qui figurent dans le programme,

– le type de son buffer de sortie,

– la taille de son buffer de sortie,

– la liste des constantes du programme avec leurs valeurs respectives,

– la liste des variables de réduction.


Une fonction est une entité caractérisée par :
– son nom,

– la liste de ses variables,

– la liste des fonctions auxquelles elle fait appel (ses fonctions productrices).
Une variable est une entité caractérisée par :
– son nom,

– son étendue,

– son type.
Une optimisation dispose de ses propres caractéristiques (qui dépendent de sa nature).
Néanmoins, chaque optimisation est appliquée sur une certaine fonction du programme,
donc chacune est caractérisée par au moins la fonction sur laquelle elle est appliquée.
Ci-dessous les caractéristiques de chaque type d’optimisation :

i. Un(e) parallélisation/vectorisation / déroulage est caractérisé(e) par la variable sur


laquelle l’optimisation est appliquée.

ii. Un découpage en bandes est caractérisé par la variable sur laquelle l’optimisation
est appliquée, auquel s’ajoute le facteur de découpage en bande utilisé.

iii. Une interversion de boucle est caractérisée par la liste des variables sur lesquelles
elle est appliquée.

iv. Un tuilage est caractérisé par les deux variables sur lesquelles il est appliqué, et les
deux facteurs de tuilage utilisés.

41
Chapitre IV. Conception

v. Une fusion est caractérisée par les deux variables sur lesquelles l’optimisation est
appliquée.
vi. Une granularité de calcul/ stockage est caractérisée par la fonction consommatrice
dans laquelle la fonction productrice est calculée/ stockée et le niveau de boucle
(la variable) de la fonction consommatrice où le calcul/ stockage de la fonction
productrice est réalisé.
Cette caractérisation nous a permis de définir la structure du fichier d’annotation que
l’utilisateur introduit à l’entrée du système.

IV.3.3 Fichier d’annotation


Le fichier d’annotation introduit par l’utilisateur doit comporter toutes les informa-
tions qui décrivent la structure de l’algorithme à optimiser. Pour cela nous avons déter-
miné un format précis pour le fichier d’annotation et nous avons fixé les champs qu’il doit
contenir. Donc, l’utilisateur doit respecter ce format afin que son programme puisse être
optimisé par notre méthode d’optimisation automatique (voir tableau IX).

Tableau IX: Informations du fichier d’annotation.

Nom du champ Son contenu


name_program Nom du programme à optimiser
output_size Taille du buffer de sortie
RVars Liste des variables de réduction
constantes Dictionnaire des constantes
functions Liste des fonctions
Pour chaque fonction f du programme :
name Nom de f
vars Liste ordonnée des variables de f
estime Les étendues de toutes les variables de f
legal_vectorize Niveau de boucle vectorisable dans f
reuse Niveaux de boucle où il y a réutilisation de données dans f
instruction Instruction définissant f

Par exemple, pour l’algorithme Halide A de la figure 25, nous devons avoir le fichier
d’annotation exposé dans la figure 26.

Figure 25: Exemple d’algorithme Halide

Tout fichier d’annotation doit respecter cette syntaxe et doit être rempli rigoureuse-
ment. Un fichier d’annotation qui respecte cette syntaxe mais qui est a été rempli par
de fausses informations va fausser les schedules construits car la stratégie de construction
des schedules ne saura pas la vraie structure de l’algorithme à optimiser.

42
Chapitre IV. Conception

Figure 26: Annotation de l’algorithme ci-dessus.

IV.3.4 Base de données


La base de données permet de garder trace de tous les schedules construits et testés
pour tous les programmes optimisés.

Nous présentons dans la figure 27 un schéma rassemblant les différentes entités sau-
vegardées et luers relations. Nous avons opté pour une base de données non-relationnelle
(NoSQL) car son schéma est facilement extensible, qui nous permet de sauvegarder des
tableaux et des entités imbriquées dans la même table.

Tous ces avantages nous les avons exploités pour pouvoir sauvegarder différents types
d’optimisations dans la même table, et étendre le schéma de la table des optimisations à
chaque nouvelle optimisation traitée. De plus, nous avons pu sauvegarder des champs de
type tableau dans les entités Programme et Schedule pour sauvegarder les fonctions qui
apparaissent dans le programme et les optimisations manipulées par le schedule respecti-
vement.

Chaque table de la base de données sauvegarde une entité du programme, avec ses
propres caractéristiques :

– Optimisation : est une table qui contient toutes les optimisations manipulées.

– _id : identifiant de l’optimisation.


– type_optimisation : type de l’optimisation 3 .
– id_fonction : identifiant de la fonction concernée par l’optimisation.
– facteur_split : facteur de découpage (s’il s’agit d’une optimisation de découpage
en bandes).
– facteur_tile_in : premier facteur de tuilage (s’il s’agit d’une optimisation de
tuilage).
– facteur_tile_out : deuxième facteur de tuilage (s’il s’agit d’une optimisation
de tuilage).
– id_fonction_consommatrice : identifiant de la fonction consommatrice (s’il
s’agit d’une optimisation de granularité de calcul/ stockage).
– id_variable_fusion : identifiant de la nouvelle variable produite suite à une
fusion (s’il s’agit d’une optimisation de fusion).
3. découpage en bandes, tuilage, parallélisation ...etc.

43
Chapitre IV. Conception

– id_variables : tableau contenant les identifiant des variables concernées par une
interversion de boucle (s’il s’agit d’une optimisation d’interversion de boucles).

Figure 27: Schéma de la base de données pour la sauvegarde des pro-


grammes et les schedules testés.

– Schedule : est une table dont chaque tuple représente un tableau d’optimisations.

– _id : identifiant du schedule.


– id_optimisations : tableau rassemblant les identifiants des optimisations qui
apparaissent dans le schedule en question.
– id_program : identifiant du programme pour lequel le schedule a été construit.
– temps_execution : temps d’exécution du schedule en question une fois appliqué
sur le programme.

– Fonction : est une table qui contient toutes les informations sur les fonctions mani-
pulées dans le programme

– Variable : est une table qui contient toutes les variables manipulées dans une fonc-
tion.

– Programme : est une table qui contient toutes les informations qui concernent le
programme ainsi que la liste des _id de ses fonctions.

IV.3.5 Construction des schedules


Le développement de ce module était spiral, car nous ne disposions pas de connais-
sances sur les variations de l’espace des optimisations.Afin d’élaborer la méthode de
construction des schedules HalideAutotuner, nous sommes passés par trois différentes
méthodes, où l’une améliore celle qui la précède (voir figure 28).

Nous montrons le cheminement qui nous a mené vers HalideAutotuner :

44
Chapitre IV. Conception

Figure 28: Méthodes conçues pour l’optimisation automatique des programmes Halide

1. Recherche exhaustive : Nous avons commencé par développer une méthode qui
explore toutes les combinaisons d’optimisations applicables à un programme. Mais
cette méthode s’est rapidement avérée prohibitive car le nombre de combinaisons à
explorer était trop grand.
2. Méthode Reorder-explore : A travers plusieurs exécutions de la méthode ex-
haustive, nous avons dégagé un comportement particulier qui concerne l’optimisa-
tion d’interversion de boucle. Ce comportement nous a permis de réduire le nombre
de combinaisons à tester et de décomposer l’étage d’exploration des optimisations
en deux. Donc au lieu d’explorer toutes les optimisations possibles en même temps,
la méthode Reorder-explore agit en deux étapes d’exploration. Premièrement, elle
explore différentes valeurs pour l’optimisation d’interversion de boucle, sans inclure
aucune des autres optimisations et à la fin de cette étape elle retient la meilleure
interversion parcourue. Deuxièment, elle explore de façon exhaustive les autres op-
timisations sauf l’optimisation d’interversion de boucle (qui n’est pas explorée car
nous lui affectons l’interversion qui a été retenue lors de la première étape). Cette
nouvelle stratégie a réduit considérablement l’espace des schedules à explorer, mais
il reste toujours grand et prohibitf à explorer.
3. Méthode Reorder-analytique : La méthode Reorder-analytique reprend les mêmes
étapes de Reorder-explore avec une modification sur la première étape d’exploration.
Lors de la première étape, on explore plusieurs possibilités d’interversions pour dé-
cider de la meilleure. L’exploration des possibilités d’interversion s’avère assez coû-
teuse, par exemple, pour une fonction à 5 variables, on a 5 ! = 120 possibilités
d’interversion de boucle. Dans le but de réduire ce nombre de tests, la méthode
Reorder-analytique utilise le modèle analytique exposé dans [Allen and Kennedy,
2002] pour le choix d’une bonne interversion de boucle. Ce modèle vient rempla-
cer la première étape d’exploration présentée dans Reorder-explore. Mais il demeure
prohibitif d’explorer les autres combinaisons d’optimisations de façon exhaustive (la
seconde étape de la méthode Reorder-explore).
4. Méthode HalideAutotuner : La méthode HalideAutotuner représente la der-
nière stratégie de construction de schedules que nous proposons pour l’optimisation
automatique des programmes Halide. Elle utilise plusieurs stratégies d’exploration
d’espace et un modèle pour la construction des schedules de bonne qualité et dans
les temps impartis. En effet, elle utilise le modèle analytique de [Allen and Kennedy,
2002] pour le choix d’une bonne interversion de boucle. Elle explore l’optimisation
de déroulage de façon exhaustive. Elle ne considère pas les optimisations de coa-
lescence de boucle et celle de la granularité de stockage. Elle applique toujours les
optimisations de vectorisation et de parallélisation. En dernier, elle utilise deux va-
riantes du Hill Climbing pour les optimisations de type granularité de calcul, tuilage
et découpage en bandes.
Dans la section suivante, nous présentons en détails le processus que nous avons suivi
pour l’optimisation automatique des programmes Halide.

45
Chapitre IV. Conception

IV.4 Conception détaillée de la stratégie de construc-


tion des schedules
Dans cette partie, nous allons parcourir les méthodes de construction de schedules
développées, en justifiant la conception de chacune d’entre elles et les différents choix pris
pour la réduction de l’espace des optimisations.

IV.4.1 Recherche exhaustive


Comme les programmes que nous voulons optimiser sont de petite taille (contiennent
peu de fonctions), nous avons commencé par développer une recherche exhaustive dans
tout l’espace des optimisations possibles pour retrouver le meilleur schedule.

Nous avons développé pour chaque type d’optimisation une méthode exhaustive basée
sur le backtracking récursif pour construire toutes ses combinaisons possibles. A chaque
fois qu’une combinaison d’une certaine optimisation est construite, nous passons à la
seconde optimisation et ainsi de suite (voir figure 29). Lorsque nous arrivons à la der-
nière optimisation, nous mesurons le temps d’exécution du schedule (voir l’annexe E.1
qui montre le diagramme de classe établi pour la recherche exhaustive).

Figure 29: Schéma résumant la construction des schedules dans l’exploration exhaustive.

IV.4.1.1 Améliorations apportées à la recherche exhaustive


Cette recherche exhaustive était prohibitive car l’espace des combinaisons d’optimisa-
tions était très grand et impossible à explorer. Dans le but de réduire l’espace des combi-

46
Chapitre IV. Conception

naisons d’optimisations, nous avons eu plusieurs discussions avec des experts pour nous
indiquer les optimisations qui sont généralement bénéfiques et celles qui sont mauvaises
pour un programme. Nous sommes arrivés aux constatations suivantes :

– Le découpage en bande est appliqué uniquement pour les niveaux de boucle à vec-
toriser ou dérouler.

– Les facteurs de découpage en bande et ceux du tuilage sont des puissances de deux
uniquement.

– L’application du tuilage se fait uniquement sur les niveaux de boucle où il y a une


réutilisation de données.

– La parallélisation et la vectorisation sont toujours appliquées sur la boucle externe


et la boucle légalement vectorisable respectivement.

– La fusion peut ne pas être considérée du tout.

– Le déroulage peut être appliqué sur les deux boucles les plus internes ou la plus
interne uniquement.

Pour adapter notre système à ces constatations, nous avons introduit la notion de
restriction. Par le biais d’une restriction, nous pouvons désactiver une optimisation lors
de l’exploration ou restreindre les valeurs qu’elle peut prendre (à l’exemple des facteurs
de tuilage et de découpage en bandes), ou même fixer sa valeur à une valeur prédéfinie.

Suite aux restrictions présentées ci-dessus, et pour une fonction à 2 niveaux de boucle
dont chacun a une étendue de 32, nous comptons une réduction de l’ordre de 99.80% pour
l’espace des combinaisions d’optimisations.

IV.4.1.2 Critiques de la recherche exhaustive


Pour montrer que cette recherche exhaustive reste prohibitive (malgré les restrictions
introduites), nous avons calculé le nombre de schedules construits par cette recherche
pour le programme de convolution (un programme de la classe des RNC que nous devons
optimiser). Pour des images de taille 68*68*16*32 et des filtres de taille 5*5*16*32, la
recherche exhaustive explore 11*1012 schedules possibles. Sachant que le temps de compi-
lation d’un programme Halide prend de 5 à 6 secondes, et en ignorant le temps d’exécution
de chaque programme optimisé et le temps de vérification de validité du schedule, le temps
d’exécution de la recherche exhaustive serait de 11*1012 * 5s = 16742 siècles.

IV.4.2 Méthode Reorder-explore


Nous avons utilisé le mécanisme de restriction afin d’examiner le comportement de
quelques optimisations par rapport à d’autres sur un petit ensemble de schedules. Nous
avons introduit à chaque fois différentes restrictions lors de l’exploration des combinaisons
d’optimisations et nous avons examiné la qualité des schedules renvoyés. Tout cela a pour
but de dégager une connaissance sur l’impact d’une optimisation particulière ou d’une
combinaison d’optimisations sur le temps d’exécution des programmes de test.

47
Chapitre IV. Conception

Après plusieurs exécutions de la recherche exhaustive avec restrictions sur le pro-


gramme de convolution (un programme de la classe des RNC) nous sommes parvenus
à distinguer un comportement relatif à l’optimisation d’interversion de boucle. En effet,
nous avons constaté que pour deux schedules (S1 , S2 ) et deux interversions de boucle (r1 ,
r2 ) tels que S1 ne contient que r1 , S2 ne contient que r2 et S1 est meilleur que S2 alors
∀O le même ensemble des optimisations rajoutées à S1 et S2 , le temps d’exécution de
S1 sera plus petit que celui de S2 . Cela veut dire que si une optimisation d’interversion
testée individuellement (sans aucune autre optimisation) sur un programme donne un bon
temps d’exécution par rapport aux autres interversions donc elle peut être maintenue pour
l’exploration des autres optimisations, car elle donnera des schedules meilleurs que ceux
obtenus avec d’autres interversions de boucles. Cette constation peut ne pas être géné-
ral à tous les programmes, mais elle l’était au moins pour le programme de la convolution.

Pour le benchmark de convolution, nous avons varié l’ensemble des optimisations O,


rajouté à S1 et S2 en même temps 4 , et nous mesurons le temps d’exécution de S1 et S2 .
La figure 30 présente la variation du temps d’exécution de S1 (courbe jaune) et celui de S2
(courbe bleue). Il est clair à travers la figure 30 que la qualité des schedules avec l’inter-
version r1 est meilleure et converge rapidement vers des schedules qui minimise le temps
d’exécution du programme. Quant à ceux construits avec l’interversion r2 , ils donnent de
bons schedules suite à l’application des autres optimisations mais ils convergent lentement
et ils ne sont pas meilleurs que ceux qui contiennent l’interversion r1 .

Figure 30: Courbe montrant la différence entre l’application d’une bonne et d’une
mauvaise interversion de boucle sur les schedules renvoyés une fois l’interversion
fixée.

Suite à cette constation, nous avons proposé de décomposer l’exploration des opti-
misations en deux étapes. Premièrement, elle explore différentes valeurs pour l’optimisa-
tion d’interversion de boucle, sans inclure aucune des autres optimisations, et retient la
meilleure interversion parcourue. Deuxièment, elle explore de façon exhaustive les autres
optimisations sauf l’optimisation d’interversion de boucle qui est fixée à celle retenue lors
de la première étape (voir figure 31 et algorithme 2).

4. S1 contient la bonne optimisation d’interversion de boucle r1 et S2 contient la mauvaise r2

48
Chapitre IV. Conception

Figure 31: Exploration en deux étapes dans la méthode Reorder-explore

Algorithm 2 Pseudo-algorithme de la méthode Reorder-explore


1: P rogramme ← extraire_info_programme(source.cpp, source.settings)
2: restriction_reorders = []
3: for fonction fi in P rogramme.f onctions() do
4: if fi est consommatrice then
5: Explorer toutes les interversions possibles pour fi
6: Choisir la meilleure interversion explorée ri S
7: restriction_reorders = restriction_reorders [fi , ri ]
8: Explorer toutes les autres optimisations sauf l’interversion de boucle qui est fixée grâce
à la restriction restriction_reorders
9: Renvoyer le meilleur schedule exploré

IV.4.2.1 Améliorations apportées par la méthode Reorder-explore


Nous nous sommes attaqués à l’optimisation qui fait exploser le temps d’exploration ;
qui est l’optimisation d’interversion de boucle. Le nombre de combinaisons d’optimisations
pour le programme de convolution, qui était de 11* 1012, devient 63628 combinaisons. En
effet, on se retrouve avec une nouvelle réduction de 99.99% de l’espace de recherche par
rapport à la recherche exhaustive avec restrictions.

IV.4.2.2 Critiques de la méthode Reorder-explore


La première étape de la méthode Reorder-explore parcourt toutes les possibilités d’in-
terversion de boucle pour choisir la meilleure et cela pour chaque fonction du programme.
Par exemple, le programme de convolution compte deux fonctions ; une avec 4 niveaux
de boucles et l’autre avec 7 niveaux de boucles. Donc, la première étape d’exploration
de Reorder-explore coûte une exploration de 7 !+4 ! = 5064 combinaisons et qui prend au
minimum 5064*5s = 7 heures. Donc la méthode d’optimisation automatique va perdre 7
heures juste pour décider de l’optimisation d’interversion de boucles, alors qu’il lui reste
d’autres optimisations à explorer.

49
Chapitre IV. Conception

IV.4.3 Méthode Reorder-analytique


Devant ce problème, où il fallait réduire l’espace des interversions à explorer et ap-
procher une bonne interversion de boucle, nous avons sélectionné un modèle analytique
pour le choix d’une bonne optimisation d’interversion de boucle. Nous avons introduit le
modèle de [Allen and Kennedy, 2002] pour avoir rapidement une bonne interversion de
boucle pour chaque fonction du programme (voir figure 32 et algorithme 3).

Figure 32: Introduction d’un modèle analytique pour le choix de l’optimisation d’interversion de
boucle dans Reorder-Analytique

Ce modèle vise à trouver une bonne interversion de boucle à appliquer pour une boucle
imbriquée B = {B1 , B2 , ..., BN }, en estimant le nombre de défauts de cache engendrés
(le modèle est expliqué en détails au niveau de l’annexe F). Ce modèle donne en sortie
B’ ; un nouvel ordre aux boucles de B. Dans le but de tirer la meilleure optimisation
d’interversion pour une fonction Halide, nous avons adapté ce modèle analytique à notre
problème : chaque fonction est équivalente à une boucle imbriquée B, et les niveaux de
boucle Bi sont les variables de cette fonction.

Néanmoins, le modèle proposé par [Allen and Kennedy, 2002] peut construire une
optimisation d’interversion de boucle B’ qui n’est pas valide. Dans ce cas, il propose une
stratégie de correction pour construire une autre optimisation d’interversion de boucle
B" qui est proche de B’ mais qui est valide. Cette stratégie consiste à appliquer des
permutations sur les niveaux de boucle de B, en essayant d’approcher l’interversion B’ sans
causer d’invalidité. Au cours de ces permutations, le modèle de [Allen and Kennedy, 2002]
vérifie la validité de chaque permutation en utilisant la théorie de l’analyse de dépendance.
Dans notre cas, nous avons intégré cette stratégie de correction, mais comme Halide
n’implémente pas les algorithmes d’analyse de dépendance, nous avons testé la validité
de chaque permutation en compilant et exécutant le code avec l’interversion permutée.
Ensuite chaque interversion permutée est sauvegardée avec son temps d’exécution dans un
tableau de candidats. Lorsque la stratégie de correction arrive à sa fin, nous choisissons la
permutation avec le plus petit temps d’exécution parmi celles qui figurent dans le tableau
des candidats.

50
Chapitre IV. Conception

Algorithm 3 Pseudo-algorithme de la méthode Reorder-analytique


1: P rogramme ← extraire_info_programme(source.cpp, source.settings)
2: restriction_reorders = []
3: for fonction fi in P rogramme.f onctions() do
4: if fi est consommatrice then
5: Récupérer la meilleure interversion ri pour la fonction
S fi à partir du modèle
6: restriction_reorders = restriction_reorders [fi , ri ]
7: Explorer toutes les autres optimisations sauf l’interversion de boucle qui est fixée grâce
à la restriction restriction_reorders
8: Renvoyer le meilleur schedule exploré

IV.4.3.1 Améliorations apportées par la méthode Reorder-analytique


Nous avons implémenté le modèle analytique tel qu’il a été présenté dans [Allen and
Kennedy, 2002], mais nous avons rajouté à l’optimisation d’interversion testée l’optimisa-
tion de parallélisation pour avoir des interversions qui améliorent non seulement la localité
des données du programme mais qui visent aussi à augmenter son taux de parallélisme.
Donc, après avoir récupéré l’optimisation d’interversion du modèle, nous lui rajoutons
l’optimisation de parallélisation sur le niveau de boucle le plus externe, et nous appli-
quons ce schedule sur le programme à optimiser. Ensuite, nous procédons à l’évaluation
de ce schedule pour récupérer son temps d’exécution et les erreurs produites dans le cas
où il n’est pas valide.

IV.4.3.2 Critiques de la méthode Reorder-analytique


En négligeant le temps consacré par le modèle analytique pour retrouver les bonnes op-
timisations d’interversion de boucle pour chaque fonction du programme de convolution,
le nombre de combinaisons d’optimisations qui restent à tester est de 58564 combinaisons.
En prenant en considération uniquement le temps de compilation du schedule qui est de
5s, le temps épuisé par la méthode sera de 58564*5s = 3.38 jours.

Pour un programme de test aussi petit que le programme de convolution, la technique


prend au minimum 3 jours pour terminer, ceci fait que la technique Reorder-analytique
reste inacceptable.

51
Chapitre IV. Conception

IV.4.4 HalideAutotuner pour l’optimisation automatique des pro-


grammes Halide
Les optimisations sur lesquelles il reste du travail sont le tuilage, le découpage en
bandes et les deux optimisations inter-étages (compute_at et store_at). Nous sommes
revenus aux restrictions pour explorer le comportement de ces optimisations sur des en-
sembles de schedules restreint. Ces dernières analyses nous ont permis de réduire considé-
rablement l’espace des schedules explorés et arriver à une technique pratique qui renvoie
des schedules de bonne qualité dans moins de 24 heures. La technique finale de construc-
tion des schedules nommée HalideAutotuner est expliquée en détails dans la section sui-
vante.

Dans HalideAutotuner, nous avons combiné plusieurs techniques d’exploration d’es-


pace et un modèle analytique pour arriver à une méthode qui génère de bons schedules
dans les temps impartis. Nous avons utilisé :

– Le modèle analytique de [Allen and Kennedy, 2002] pour le choix d’une bonne
interversion de boucle.
– Une exploration exhaustive pour l’optimisation de déroulage.
– Une exploration en Hill Climbing pour l’optimisation de granularité de calcul (com-
pute_at).
– Une exploration en Hill Climbing pour les facteurs de tuilage et de découpage en
bandes.
Les optimisations de parallélisation et de vectorisation sont directement appliquées
à tous les schedules testés. Quant à l’optimisation de fusion et celle de la granularité
de stockage, elles n’ont pas été appliquées sur les schedules (voir annexe E.2 pour la
conception du diagramme de classe de HalideAutotuner).

IV.4.4.1 Explication des choix des stratégies d’exploration pour chaque type
d’optimisation
Chaque optimisation se distinguait des autres de par son comportement, et son impact
sur la performance du programme. De ce fait, nous avons utilisé une stratégie d’explora-
tion pour chaque optimisation. Dans cette partie, nous allons justifier le choix de chaque
stratégie d’exploration adoptée.
1. Modèle analytique pour l’optimisation reorder : Le choix de ce modèle a été
expliqué dans la section IV.4.3.
2. Absence d’exploration pour les optimisations parallel et vectorize : La
parallélisation et la vectorisation sont bénéfiques pour un programme, donc nous les
appliquons directement sur le schedule (information transmise par des experts en
optimisation de programmes).
3. Absence d’exploration pour les optimisations fuse et store_at : Après plu-
sieurs exécutions, nous avons constatés que ces deux optimisations n’impactent pas
considérablement la performance d’un programme, donc elles ne sont pas appliquées
sur les schedules.

52
Chapitre IV. Conception

4. Variante du Hill Climbing pour l’optimisation compute_at : L’optimisa-


tion compute_at est appliquée sur une fonction et prend en paramètre un niveau
de boucle de sa fonction consommatrice. Alors afin d’explorer le compute_at pour
une fonction il faut parcourir tous les niveaux de boucle de sa fonction consomma-
trice. Par exemple, pour une fonction f consommée par une fonction g, qui dispose
des niveaux de boucle d’indice x, y, z allant du plus interne au plus externe ; on
commence par tester f.compute_at(g,x) puis f.compute_at(g,y), f.compute_at(g,z)
et en dernier f.compute_root(). A l’exploration de l’optimisation compute_at nous
remarquons qu’il y a une amélioration dans les performances, et puis au bout d’une
certaine valeur les performances ne font que se dégrader (voir la figure 33).

Figure 33: Evolution de la performance d’un schedule en faisant varier uniquement l’optimisation
compute_at.

5. Variante du Hill Climbing pour les optimisations tile et split : Au départ,


nous avons appliqué les optimisations tile et split avec des facteurs par défaut, car
ils sont nécessaires pour les choix de l’optimisation compute_at. Ces deux optimi-
sations servent à augmenter le nombre de niveaux de boucle d’une fonction pour
pouvoir appliquer le compute_at sur plusieurs niveaux de boucle. Une fois que l’op-
timisation compute_at est explorée, nous avons constaté que le facteur par défaut
utilisé pour les deux optimisations tile et split ne donne pas des schedules de bonne
performance. Donc, nous avons dû explorer les valeurs que peuvent prendre ces deux
optimisations. Cependant, l’exploration exhaustive des facteurs pour ces deux op-
timisations est coûteuse en termes de temps d’exécution. Donc, nous avons opté
pour une exploration en Hill Climbing pour l’exploration des valeurs de ces deux
optimisations (split et tile).

IV.4.4.2 Variantes du Hill Climbing conçues pour les différentes optimisa-


tions
1. Variante du Hill Climbing pour l’optimisation de granularité de calcul : Nous avons
mis en place une variante du Hill Climbing pour nous faire converger rapidement
vers la bonne optimisation de type compute_at (voir la figure 34).

53
Chapitre IV. Conception

Figure 34: Variante du Hill Climbing pour trouver une optimisation de granularité de calcul
(compute_at) de bonne qualité

Projetons son fonctionnement sur un exemple. Si g a 5 niveaux de boucle d’indice


= {x1, x2, x3, x4, x5} du niveau le plus interne au plus externe alors l’optimisation
f.compute_at(g,...) peut prendre 6 valeurs. Au lieu d’explorer f.compute_at(g,x1)
puis f.compute_at(g,x2) ...etc 5 et de s’arrêter au bout d’une dégradation de perfor-
mances, comme cela devrait l’être dans la version originale du grimpeur, nous com-
mençons par explorer le niveau de boucle médian. Dans ce cas f.compute_at(g,x3)
est testé en premier ensuite f.compute_at(g,x2) (à gauche) et f.compute_at(g,x4) (à
droite) pour décider de la direction d’exploration et se diriger vers l’optimisation qui
améliore les performances. Si la performance du schedule avec f.compute_at(g,x4)
est meilleure que celle avec f.compute_at(g,x2) et f.compute_at(g,x3), alors la di-
rection serait à droite. Nous continuons ainsi jusqu’à ce qu’une dégradation de per-
formances apparaisse à gauche et à droite de l’optimisation courante. C’est ce niveau
de boucle qui va être choisi pour l’optimisation f.compute_at(g,...). Pour une fonc-
tion consommatrice à N niveaux de boucle, l’espace des optimisations exploré est
réduit de 50% au minimum pour chaque optimisation de type compute_at.

2. Variante du Hill Climbing pour les facteurs de tuilage et de découpage : Le choix des
facteurs de tuilage et de découpage en bandes affecte considérablement la qualité des
schedules. En effet, les facteurs de découpage sont critiques pour les optimisations de

5. jusqu’à arriver à compute_root().

54
Chapitre IV. Conception

déroulage et de vectorisation. Quant aux facteurs de tuilage, ils sont critiques pour
l’amélioration de l’utilisation de la mémoire cache. Une exploration exhaustive des
facteurs de ces deux optimisations n’est pas possible, car cela va engendrer un grand
espace de schedules à tester. Alors, nous avons mis en place une variante du grimpeur
(dont le principe ressemble à celui du Hill Climbing appliqué à compute_at) pour
retrouver rapidement des facteurs d’assez bonne qualité (voir figure 35).

Figure 35: Variante du Hill Climbing pour la recherche de bons facteurs de découpage en bandes
et de tuilage

En effet, nous commençons par les optimisations de découpage en bandes, en testant


la qualité du schedule avec le facteur par défaut m (qui est généralement de 16).
Ensuite, les deux autres facteurs testés sont : mg = m/2 (à gauche) et md = m × 2
(à droite), pour choisir le meilleur facteur afin qu’il soit à son tour le facteur m.
Ce procédé est arrêté une fois les facteurs de droite et de gauche n’améliorent plus
la performance du schedule. Une fois que le facteur adéquat est choisi, nous conti-
nuons avec les prochaines optimisations de type découpage en bandes, s’il en reste
en suivant le même procédé. Lorsque les facteurs de découpage en bandes sont déter-
minés, nous passons aux optimisations de tuilage, qui seront traitées en deux temps
car elles comptent deux facteurs différents. Cette variante du Hill Climbing nous a
permis de réduire de 50% l’espace des valeurs à explorer pour chaque optimisation
de type tuilage et découpage en bandes.

IV.4.4.3 Etapes de construction des schedules dans HalideAutotuner


Nous initialisons un schedule S vide. Pour chaque fonction du programme, nous com-
mençons par utiliser le modèle de [Allen and Kennedy, 2002] pour avoir la bonne optimi-
sation de type reorder (ligne 5 de l’algorithme 4). Une fois le reorder récupéré, nous le
rajoutons à S (ligne 5 et 6 de l’algorithme 4). Ensuite, nous appliquons sur S des optimi-
sations de type split (de la ligne 7 à la ligne 10 de l’algorithme 4) et des optimisations
de type tile (ligne 12 et 13 de l’algorithme 4) avec des facteurs par défaut qui donnent

55
Chapitre IV. Conception

généralement de bonnes performances (16 ou 32 par exemple). Rajoutant à ça, les opti-
misations parallel (ligne 14 et 15 de l’algorithme 4) et vectorize (ligne 11 de l’algorithme
4) qui sont appliquées sur toutes les fonctions car elles sont considérées bénéfiques.

Algorithme 4 Pseudo-algorithme de HalideAutotuner


1: P rogramme ← extraire_info_programme(source.cpp, source.settings)
2: Initialiser un schedule S vide
3: for fonction fi in P rogramme.f onctions() do
4: if fi est consommatrice then
5: Récupérer la bonne interversion de boucle ri pour fi à partir du modèle
6: Rajouter fi .reorder(ri ) à S
7: Soit vi le niveau de boucle vectorisable dans ri
8: Soit ui le niveau de boucle à dérouler dans ri
9: Rajouter fi .split(ui , 16) à S
10: Rajouter fi .split(vi , 16) à S
11: Rajouter fi .vectorize(vi ) à S
12: Soient t1i et t2i les niveaux de boucle à tuiler
13: Rajouter fi .tile(t1i , t2i , 16, 16) à S
14: Soit ei le niveau de boucle externe dans ri
15: Rajouter fi .parallel(ei ) à S
16: Explorer et tester les combinaisons de déroulage (recherche exhaustive) et de granu-
larité de calcul (Hill climbing) sur S
17: Garder les dix meilleurs schedules de l’exploration ci-dessus
18: for Schedule si parmi les 10 meilleurs schedules retenus do
19: Explorer les facteurs de split en Hill Climbing
20: Explorer les facteurs de tuilage en Hill Climbing
return Le meilleur schedule rencontré pendant les explorations

Suite à cette initialisation de S, nous enchaînons avec une exploration des optimisations
de type unroll et des optimisations de type compute_at avec une recherche exhaustive
et une exploration en Hill Climbing respectivement (ligne 16 de l’algorithme 4). Durant
cette exploration, nous allons être soumis à tester plusieurs schedules qui diffèrent uni-
quement selon les valeurs que prennent les optimisations unroll et compute_at. Une fois
cette exploration terminée, nous allons retenir les 10 meilleurs schedules explorés (ligne
17 de l’algorithme 4).

Les facteurs de tuilage et de découpage en bandes sont critiques pour la performance


d’un programme, et utiliser un facteur par défaut ne donne pas les meilleures perfor-
mances. Donc, pour chaque schedule retenu, nous explorons les facteurs de tuilage et de
découpage en bandes en utilisant la stratégie d’exploration Hill Climbing (ligne 19 et 20
de l’algorithme 4). La figure 36 résume les étapes de la méthode HalideAutotuner pour la
construction des schedules.

56
Chapitre IV. Conception

Figure 36: Stratégie d’exploration d’espace adoptée dans HalideAutotuner

IV.5 Conclusion
Dans la partie conception, nous avons commencé par décrire le problème et ses compo-
santes. En effet, ce travail consiste à optimiser automatiquement la classe des programmes
implémentant les RNC et produire des programmes dont le temps d’exécution est proche
de celui des programmes optimisés à la main. Cela doit être réalisé dans un délai qui ne
dépasse pas les 24 heures. En second lieu, nous avons présenté l’architecture globale de
notre système et ses différents modules qui sont répartis entre : identification de la struc-
ture du programme, construction des schedules, leur évaluation et la sauvegarde dans la
base de données.

En dernier, nous nous sommes focalisés sur la méthode de construction des schedules
développée HalideAutotuner. Mais avant d’arriver à HalideAutotuner et son principe de
fonctionnement, nous avons mis en relief tout le processus parcouru pour arriver à la
conception de cette méthode. En effet, nous avons commencé par une exploration exhaus-
tive dans tout l’espace des optimisations applicables à un programme. Au fur et à mesure,
nous avons réduit l’espace de recherche des combinaisons d’optimisations en introduisant
un modèle analytique et des stratégies d’exploration d’espace approchées (comme le Hill
Climbing) jusqu’à arriver à une méthode qui satisfait nos besoins qui sont : la construction
d’un schedule de bonne qualité dans un temps inférieur à 24 heures pour les programmes
implémentant les RNC.

Tous les résultats obtenus, à l’utilisation de la méthode HalideAutotuner sont mis en


relief dans la partie VI tests et évaluations. Mais avant de passer à cette partie, nous allons
abordé la partie réalisation, dans laquelle nous expliquons tous nos choix technologiques

57
Chapitre IV. Conception

pour le développement de notre système.

58
Chapitre V

Réalisation

V.1 Introduction
Dans le chapitre précédent, nous avons présenté notre heuristique et son principe de
fonctionnement en utilisant des diagrammes UML. Suite à une série de tests et évalua-
tions, nous avons identifié des techniques de recherche efficaces qui nous font converger
rapidement vers de bon schedules, de ce fait, nous sommes arrivés à proposer une bonne
méthode qui équilibre entre son temps d’exécution et la qualité des schedules qu’elle ren-
voit.

Dans cette partie - implémentation, nous allons décrire l’architecture globale du sys-
tème qui nous permet d’optimiser de façon automatique chaque programme de la classe
des RNC sur une architecture cible de type CPU, dans un temps relativement petit (moins
de 24 heures). De plus, nous allons cité toutes les technologiques, outils et bibliothèques
utilisées pour le développement de notre solution, en justifiant le choix de chacun d’eux.

V.2 Architecture du système


Notre système est conçu dans le but d’optimiser une classe de programmes destinés
à s’exécuter sur une architecture matérielle de type CPU. Ces programmes partagent
quelques caractéristiques : chacun est destiné à implémenter une des couches d’un ré-
seau de neurone, et ils sont tous composés d’un petit nombre de fonctions Halide. Avant
d’implémenter notre technique d’optimisation, il nous a fallu développer en amont les
programmes Halide que nous devrions optimiser.

V.2.1 Programmes de la classe des RNC


Cette classe regroupe sept programmes qui appartiennent au même domaine d’appli-
cation (les réseaux de neurones). Nous n’avons pas implémenter tous les programmes de
la bibliothèque car quelques uns étaient déjà disponibles sur GitHub, donc, nous les avons
directement intégrés à notre ensemble de test. Nous avons développé ces programmes en
suivant la syntaxe du langage Halide et celle du langage C++ (comme Halide est embar-
qué dans le C++).

59
Chapitre V. Réalisation

Pour pouvoir optimiser chaque programme de la bibliothèque, il fallait saisir un fichier


d’annotation pour chaque programme de la bibliothèque. Nous avons opté pour un fichier
caractéristique de type JSON (JavaScript Oriented Notation) pour sa lisibilité et sa faci-
lité de modification.

V.2.2 Système d’optimisation automatique pour les programmes


de la classe des RNC
Le système est implémenté en utilisant le langage Python. Notre choix s’est rapide-
ment porté sur ce langage car il est open source, riche en termes de bibliothèques, bien
documenté et derrière lequel il existe une grande communauté. Nous avons utilisé plusieurs
bibliothèques Python comme pymongo, numpy ...etc. dont nous allons justifier l’utilisa-
tion dans la partie technologies utilisées.

Lorsque HalideAutotuner construit un schedule pour le programme en entrée, nous


devons tester la performance de ce dernier (le temps d’exécution du programme optimisé
en utilisant ce schedule). Pour se faire, nous allons créer un fichier source et nous mettons
dessus le programme et le schedule généré ensuite nous lançons la compilation de ce fichier
en faisant appel au compilateur Halide. Une fois le programme est compilé, nous avons
un code exécutable prêt à être exécuté. Ensuite, on lance l’exécution de ce dernier et on
mesure son temps d’exécution. La sauvegarde du schedule et du programme est faite en
utilisant le SGBD NoSQL orienté document MongoDB. Nous avons choisi une base de
données de type NoSQL pour pouvoir sauvegarder les différentes optimisations traitées
au cours de ce travail et permettre une certaine agilité et flexibilité pour l’extension du
système dans ses prochaines versions.

Figure 37: Architecture globale du système.

60
Chapitre V. Réalisation

V.3 Technologies et bibliothèques utilisées


Au cours de ce travail, nous avons utilisées plusieurs technologies et bibliothèques que
nous citons ci-dessous.

– Python : Nous avons choisi le langage python comme un langage de programmation


open source et multiplateforme (MacOS, Windows, Linux), qui gagne de plus en plus
d’ampleur grâce à sa syntaxe qui est relativement facile à apprendre et à comprendre.
Il très riche en termes de bibliothèques spécialisées dans plusieurs domaines comme
le calcul matriciel/ vectoriel, manipulation des fonctions mathématiques, et c’est ce
qui a augmenté notre productivité lors du développement.

– Halide : L’utilisation du compilateur Halide est indispensable pour compiler les


divers programmes optimisés.

– JSON : Nous avons choisi JSON (JavaScript Object Notation) comme format pour
le fichier d’annotation. En effet, JSON est lisible, facilement interprété par la ma-
chine et par l’humain. Il se base sur des couples de données de type ’clé : valeur’, où
la valeur peut varier d’une simple donnée atomique à une liste de données ordonnée.

– MongoDB : C’est un système de gestion de bases de données NoSQL orienté do-


cument. Notre choix s’est rapidement posé sur le SGBD MongoDB, parce qu’il est
gratuit et derrière lequel il existe une grande communauté. MongoDB est orienté
document, c’est à dire qu’un document représente l’équivalent d’une ligne de table
en SQL. N’empêche que le document peut être plus riche qu’une ligne, par exemple,
un champs d’un document peut contenir une liste ou un document imbriqué ce qui
n’est pas le cas dans une ligne SQL.

– Pymongo : C’est une bibliothèque Python utilisée pour l’interfaçage du langage


de programmation Python avec le SGBD MongoDB. A partir d’un code Python,
Pymongo permet au programmeur de se connecter à un serveur MongoDB, de lui
envoyer des requêtes et de récupérer les documents qu’il renvoit.

– Numpy : C’est une bibliothèque Python qui permet de réaliser du calcul algébrique
(sur des vecteurs ou des matrices). Nous l’avons utilisé pour implémenter le modèle
analytique exposé dans IV.4.3 afin de calculer les estimations de défaut de cache.

– Scipy : C’est une bibliothèque Python qui est utilisée dans le calcul mathématique
et scientifique, qui utilise les vecteurs et matrices de type Numpy. Nous avons uti-
lisé Scipy pour la maximisation d’une fonction mathématique pour implémenter le
modèle analytique exposé dans IV.4.3 afin de calculer les estimations de défaut de
cache.

– Sympy : C’est une bibliothèque Python utilisée dans le calcul formel d’expression
mathématique. Nous avons utilisé Sympy pour traiter les expressions mathématiques
manipulées par les fonctions Halide.

61
Chapitre V. Réalisation

V.4 Fonctionnalités du système


Au cours de notre travail, nous avons développé trois différentes méthodes de construc-
tion de schedules : Recherche exhaustive, Reorder-explore, Reorder-analytique et HalideAu-
totuner. HalideAutotuner est une méthode d’optimisation qui réalise un bon compromis
entre le temps d’exécution du meilleur schedule retourné et le temps d’exécution de la
méthode. Les premières méthodes (Reorder-explore et Reorder-analytique) sont plus sus-
ceptibles de donner des schedules de bonne qualité mais elles demandent un temps d’exé-
cution qui dépasse les 7 jours pour certains benchmarks.
Nous donnons la main à l’utilisateur d’introduire plusieurs informations relatives au
fonctionnement de notre système à partir de la ligne de commande (voir figure 38).

Figure 38: Ligne de commande pour lancer la méthode de construction des schedules HalideAu-
totuner.

• - -heuristique : pour choisir la technique à exécuter (Reorder-explore, Reorder-analytique


ou HalideAutotuner ).

• - -trials : en optimisation de code, le temps d’exécution d’un programme n’est pas


déterminé en une seule exécution. Mais généralement, on exécute plusieurs fois le
même programme pour choisir la médiane entre ses temps d’exécution. Le paramètre
- -trials donne le nombre d’exécutions effectuées pour chaque schedule généré.

• - -time-limit : représente le temps limite pour le test d’un programme optimisé.


Autrement dit, l’évaluation d’un schedule est abandonnée lorsque son temps d’exé-
cution atteint les time-limit secondes.

• - -schedule-limit : donne le nombre maximal de schedules construit par la méthode.

• - -heuristique-limit : donne le temps maximal pris par l’heuristique. C’est à dire, dès
que l’heuristique dépasse les - -heuristique-limit secondes, elle renvoit le meilleur
schedule exploré et s’arrête.

• - -settings-file : pour indiquer le chemin vers le fichier d’annotation.

• L’argument de la commande est le chemin vers le code source de l’algorithme Halide


à optimiser.

V.5 Conclusion
Dans ce chapitre, nous avons argumenté tout choix technologique pour l’élaboration
des trois méthodes du système qui visent toutes à optimiser de façon automatique une
classe de programme qui implémentent les RNC écrite en Halide. Nous avons présenté

62
Chapitre V. Réalisation

l’architecture technique de notre solution, ainsi que l’environnement nécessaire pour le


fonctionnement du système. Dans le prochain chapitre, nous évaluons notre méthode
d’optimisation finale HalideAutotuner avec tous les programmes de la bibliothèque, en
mettant en relief le temps d’exécution de la méthode ainsi que le temps d’exécution du
meilleur schedule renvoyé par HalideAutotuner. Le temps d’exécution du meilleur schedule
renvoyé par HalideAutotuner est comparé avec celui du schedule développé à la main par
des experts en matière d’optimisation de code.

63
Chapitre VI

Tests et évaluations

VI.1 Introduction
Après avoir présenté notre système et détaillé son architecture technique, nous allons
l’évaluer. Rappelons que notre système consiste à optimiser de façon automatique la classe
des programmes implémentant les RNC sur une architecture à base de CPU. Cela est réa-
lisé dans l’exigence que le temps d’exécution des programmes optimisés automatiquement
(par HalideAutotuner) s’approche du temps d’exécution des programmes optimisés à la
main par des experts en matière d’optimisation de programmes.

L’évaluation de HalideAutotuner est réalisée sur un ensemble de sept benchmarks où


chacun est un programme écrit en Halide qui implémente l’une des couches d’un réseau de
neurones convolutif. Ces sept programmes ont différentes tailles d’entrée. Afin de juger de
la performance de HalideAutotuner, ces programmes sont optimisés manuellement (par
des experts en optimisation de programmes ou par nous-mêmes) et automatiquement
par HalideAutotuner. En effet, la méthode que nous proposons (HalideAutotuner) doit
prendre au maximum 24 heures pour optimiser chaque programme. De plus la déviation
entre son temps d’exécution et celui du programme optimisé manuellement ne doit pas
dépasser les 10%.

VI.2 Vue globale sur la phase d’évaluation


Les benchmarks de test sont en nombre de sept : ReLU, maxPool, convolution, convo-
lution -ReLU, multiplication matricielle, multiplication matricielle par lots et multiplica-
tion matricielle transposée par lots. Ces benchmarks sont tous destinés à être optimisés
de façon automatique par notre système d’optimisation automatique sur une architecture
à base de CPU.

VI.2.1 Caractéristiques des benchmarks


Le tableau X résume les caractéristiques importantes des benchmarks de test. Chaque
benchmark est caractérisé par le nombre total de ses fonctions, le nombre de ses fonctions
consommatrices et par qui il a été optimisé (par nous-mêmes ou par des experts). En
effet, certains benchmarks de test n’ayant pas été optimisés par des experts, nous avons
pris l’initiative de les optimiser manuellement.

64
Chapitre VI. Tests et évaluations

Tableau X: Caractéristiques des benchmarks de test

Nombre de Nombre de fonctions Optimisé manuelle-


Benchmark
fonctions consommatrices ment par
ReLU 2 1 nous-mêmes
MaxPool 2 1 nous-mêmes
Convolution 5 2 Patricia Suriana : ingénieur chez Google
Convolution-ReLU 6 3 Patricia Suriana : ingénieur chez Google
Multiplication matricielle 8 3 Andrew Adams : chercheur chez Facebook
Multiplication matricielle
8 4 nous-mêmes
par lots
Multiplication matricielle
8 4 nous-mêmes
transposée par lots

Les benchmarks sont classés du plus simple au plus complexe (voir tableau X), et
cela en fonction du nombre de fonctions qui apparaissent dans chaque benchmark. Plus le
nombre de fonctions d’un programme est grand, plus il est complexe à optimiser. En effet,
un programme qui renferme plus de fonctions qu’un autre aura plus de combinaisons d’op-
timisations applicables et donc un plus grand espace de recherche. Les deux benchmarks
les plus simples à optimiser sont ReLU et maxpool. Chacun ne compte que 2 fonctions
dont une seule est consommatrice. Cependant, le programme de la multiplication matri-
cielle par lots, qui est assez complexe à optimiser, renferme 8 différentes fonctions, dont
4 consommatrices.

VI.2.2 Optimisation automatique des benchmarks


Tous nos programmes (benchmarks) ont des données en entrée ; la taille de ces don-
nées est une information importante qui affecte le choix des optimisations à appliquer
sur le programme. De ce fait, nous avons choisi d’optimiser chaque benchmark avec 3
tailles d’entrée différentes (petite, moyenne et grande) pour distinguer les trois schedules
construits par notre système et essayer d’identifier une relation, si elle existe, entre ces
derniers.

Afin d’optimiser un programme de façon automatique dans notre système, et pour une
taille d’entrée particulière, il faut qu’il soit annoté. En effet, avant de procéder à l’optimi-
sation automatique de nos benchmarks, nous les avons tous annotés manuellement, selon
leurs tailles d’entrée, dans un fichier d’annotation qui contient toutes les informations
importantes sur le programme.

HalideAutotuner est exécuté sur le benchmark annoté pour explorer un espace de


schedules applicables au programme et renvoyer le meilleur schedule rencontré (voir figure
39). La performance de notre système est déterminée par deux paramètres :
1. TS : Il représente le temps pris par le système pour trouver la meilleure combinaison
d’optimisations (le meilleur schedule). TS doit être inférieur à 24 heures pour tous
les benchmarks optimisés.
2. Acc : L’accélération désigne le gain en temps d’exécution du meilleur schedule 1
trouvé par HalideAutotuner par rapport à celui développé manuellement. Le meilleur
1. Par abus de langage, on parle du temps d’exécution d’un schedule, qui veut dire le temps d’exécution
du programme optimisé par ce schedule.

65
Chapitre VI. Tests et évaluations

schedule trouvé par le système est de bonne qualité si son accélération est supérieure
ou égale à 90%. On rappelle la formule de l’accélération :
temps d’exécution du programme optimisé à la main
Acc = (VI.1)
temps d’exécution du programme optimisé par le système.

Figure 39: Optimisation automatique des programmes de test.

Tous les benchmarks ont été optimisés sur la même architecture à base de CPU. Cela
est réalisé afin de donner les bonnes combinaisons d’optimisations en se référant à une seule
architecture d’exécution, et pour pouvoir effectuer des comparaisons entre les différents
benchmarks.

VI.2.3 Architecture matérielle de test


Pour tester notre système d’optimisation automatique, nous avons eu un accès distant
au cluster du MIT pour réaliser nos différents tests. Ce cluster dispose d’un nombre im-
portant de machines (nœuds) qui ont toutes les mêmes performances (voir tableau XI).

Tableau XI: Caractéristiques de l’architecture d’exécution

Modèle Intel(R) Xeon(R) X7750


Architecture x86_64
Nombre de nœuds 48
Nombre de CPU par nœud 1
Nombre de cœurs par CPU 8
Fréquence de chaque cœur 2 GHz
Taille du cache de données 32 Ko
L1
Taille du cache d’instruc- 32 Ko
tions L1
Taille du cache L2 256 Ko
Taille du cache L3 18432 Ko
Taille de la RAM 128 Go
Système d’exploitation Ubuntu 14.04.5 LTS

Nous avons déployé notre système sur ce cluster, et nous l’avons utilisé pour optimi-
ser les 22 instances de test. L’utilisation du cluster nous a permis d’optimiser plusieurs
instances en même temps ; chaque programme avec une taille d’entrée particulière est

66
Chapitre VI. Tests et évaluations

optimisé sur une machine du cluster (voir figure 40).

Figure 40: Optimisation parallèle des instances de test sur le cluster

VI.3 Benchmarks de test


Dans cette section, nous allons présenter chaque benchmark de façon paticulière en
mettant l’accent sur son principe de fonctionnement, les propriétés de son algorithme
Halide, la taille de ses entrées, le schedule construit par notre système pour l’optimisation
de ce programme et celui développé à la main.

VI.3.1 Benchmark de convolution


Le programme de convolution implémente la couche de convolution dans les réseaux
de neurones convolutifs. L’opération de convolution consiste à parcourir une image (géné-
ralement une donnée multidimensionnelle) de taille H*W en utilisant un filtre (fenêtre) de
taille rectangulaire h*w. Ce filtre à son tour découpe l’image en zones rectangulaires de
taille h*w pour réaliser le même traitement sur les zones de l’image. L’image est modifiée
suivant les valeurs du filtre qui déterminent ainsi son rôle. La figure 41 résume le principe
de fonctionnement de la couche de convolution.

Figure 41: Principe de fonctionnement du traitement de convolution.

VI.3.1.1 Algorithme Halide de convolution et ses caractéristiques


L’algorithme 5 représente l’algorithme Halide qui calcule la couche de convolution
dans un réseau de neurones convolutif. Il inclut 5 fonctions : input, bias, infilter, conv et
conv.update() pour la seconde définition de la fonction conv.

67
Chapitre VI. Tests et évaluations

Algorithme 5 Algorithme Halide de la convolution


1: int h,w,c ;
2: Var x,y,z,n ;
3: RDom r(0,h,0,w,0,c) ;
4: Func input, bias, infilter, conv ;
5: conv(x, y, z, n) ← bias(z);
6: conv(x, y, z, n) ← conv(x, y, z, n)+input(x+r.x, y +r.y, r.z, n)∗infilter(r.x, r.y, r.z, n);

Cet algorithme n’inclut que deux fonctions consommatrices, conv et conv.update()


qu’on caractérise dans le tableau XII.
Tableau XII: Caractéristiques des fonctions consommatrices du benchmark

Nombre des ni-


Fonction consommatrice
veaux de boucle
conv 4
conv.update() 7

L’algorithme de convolution reçoit en entrée un nombre d’images N et un nombre de


filtres Z. Chaque filtre de l’ensemble des filtres est appliqué sur chaque image en entrée.
Les filtres ont tous la même taille h*w*c et les images aussi H*W*c. Les données en entrée
varient selon les paramètres Z*N*H*W*h*w*c, tels que : Z est le nombre de filtres, N le
nombre d’images, H la longueur d’une image, W sa largeur, h la longueur d’un filtre, w sa
largeur et c le nombre de ses canaux. L’ensemble des filtres et celui des images sont vus
comme deux tableaux à 4 dimensions de taille h*w*c*Z et H*W*c*N respectivement.

VI.3.1.2 Annotation de l’algorithme de convolution


Avant qu’un algorithme ne puisse être optimisé, il doit être annoté. Dans cette section
nous présentons le fichier d’annotation que nous avons saisi pour l’algorithme de convolu-
tion, et cela pour une seule taille d’entrée : un lot de filtres et un lot d’images de taille 32
chacun, avec des images de taille 68*68*16 et des filtres de taille 5*5*16 (voir figure 42).

Figure 42: Fichier d’annotation pour le programme de convolution pour des entrées de 32 filtres
de taille 5*5*16 et 32 images de taille 68*68*16 chacune.

La convolution calcule en sortie un tableau à 4 dimensions. Dans ce cas, il a une taille


de 64*64*32*32 (voir ligne 1 du fichier d’annotation) avec des données de type réel (voir

68
Chapitre VI. Tests et évaluations

ligne 3 du fichier d’annotation). Nous renseignons toutes les fonctions du programme,


bias, input, infilter, conv et conv.update(), avec l’étendue de chaque variable qui apparaît
dans la fonction. Par exemple, la fonction conv dispose de quatre variables ordonnées
dans vars, dont nous estimons l’étendue dans estime. La fonction conv fait appel à la
fonction bias (ligne 13 du fichier d’annotation), son niveau de boucle vectorisable est x et
les niveaux de boucles où il y a une réutilisation de données sont x et y.

VI.3.1.3 Evaluation des performances


Nous avons annoté et optimisé le programme de convolution pour quatre tailles d’en-
trée différentes. Nous avons choisi les tailles des images et des filtres les plus utilisées dans
les réseaux de neurones convolutifs, pour montrer la différence entre le temps d’exécu-
tion du programme optimisé à la main et celui optimisé par HalideAutotuner. Comme le
langage Halide ne supporte pas beaucoup de récursivité (sachant que le programme de
convolution est récursif) nous nous sommes limités à des images de taille inférieure ou
égale à 522*522*64*64.

Pour chaque taille d’entrée, nous avons mesuré le temps d’exécution du programme
sans optimisations (exe-Naïf), le temps d’exécution du programme optimisé à la main (exe-
Manuel), le temps d’exécution du programme optimisé en utilisant le schedule trouvé par
HalideAutotuner (exe-Autotuner) et le temps pris par le système pour l’optimisation du
programme (TS). Ensuite, nous calculons l’accélération (AccM ) du programme optimisé
par HalideAutotuner par rapport à celui optimisé manuellement et l’accélération (AccN )
du programme optimisé par notre système par rapport à celui qui n’a pas été optimisé
(voir tableau XIII).

Tableau XIII: Statistiques à propos de l’optimisation automatique du programme de convolution


en utilisant HalideAutotuner.

Nb.
Les entrées exe-Naïf exe-Manuel exe-Autotuner AccN AccM TS
schedules

- images : 68*68*16*32
1.6575 s 0.039635 s 0.018779 s 100.11 2.11 40 13min.
- filtres : 5*5*16*32

- images : 131*131*64*4
2.5581 s 0.075087 s 0.0236 s 108.39 3.18 44 43min.
- filtres : 3*3*64*64

- images : 260*260*16*32
22.7425 s 0.705296 s 0.193221 s 117.70 3.65 44 1h25min.
- filtres : 5*5*16*32

- images : 522*522*64*64
> 5000 s 51.6594 s 40.66 s > 132.53 1.24 69 6h03min.
- filtres : 11*11*64*64

HalideAutotuner arrive à construire des schedules qui améliorent considérablement le


temps d’exécution des programmes sans optimisations (AccN supérieure à 100) et dont la
performance dépasse celle des schedules développés par des experts (AccM supérieure à
1.2). La technique d’exploration des combinaisons d’optimisations se termine, et aboutit

69
Chapitre VI. Tests et évaluations

à un schedule de bonne qualité, avant les 24 heures pour toute instance de test du pro-
gramme de convolution. Nous remarquons que plus les tailles d’entrée du programme sont
larges, plus TS (temps pris par HalideAutotuner) est plus grand.

VI.3.1.4 Analyse des résultats


Notre système génère pour chaque taille d’entrée un schedule adéquat. Dans cette
section, nous allons essayer de distinguer les différences entre le schedule manuel et le
schedule construit pas notre système et cela pour chaque taille d’entrée (voir tableau 43).
Les schedules construits par notre système diffèrent mais ils ont tous la même structure,
car nous leur avons fixé l’ordre d’apparition des optimisations. De ce fait, ils sont plus
lisibles par rapport à ceux développés manuellement.

Il existe plusieurs différences entre le schedule construit automatiquement et celui dé-


veloppé à la main. En effet, nous avons constaté cela sur l’optimisation de tuilage de façon
particulière. Pour la fonction conv, le tuilage n’a pas été appliqué dans le schedule manuel
alors qu’il est appliqué dans tous les schedules que HalideAutotuner a construit. Tandis
que pour la fonction conv.update(), le tuilage est appliqué sur les niveaux de boucle z et y,
alors que dans les schedules que nous construisons, le tuilage est appliqué sur les variables
x et y. Nous ne pouvons rien conclure quant à l’effet de l’optimisation de tuilage sur les
schedules manuels par rapport à ceux construits par notre système.

On remarque que la vectorisation est appliquée sur les schedules automatiques avec
des facteurs plus grands que ceux appliqués dans les schedules manuels. En effet, il est
connu que plus le facteur de vectorisation est grand, plus le traitement est parallélisé (voir
section I.2.2), donc on peut estimer que les facteurs de vectorisation ont participé dans
l’amélioration du temps d’exécution du programme.

Le déroulage n’est pas toujours bénéfique pour une boucle, surtout si son étendue
est grand. En effet, nous remarquons qu’au niveau du schedule manuel, le déroulage est
toujours appliqué sur la fonction conv.update() avec un facteur égal à la longueur du
filtre. Cependant, les schedules construits par notre système applique l’optimisation de
déroulage lorsque la longueur du filtre manipulé est petite (le cas de 3 et 5) et il n’est
pas appliqué lorsque la longueur du filtre est grande (le cas de 11). Donc, on peut estimer
que les facteurs de déroulage ont participé aussi à l’amélioration du temps d’exécution du
programme à optimiser.

L’optimisation de fusion n’était pas considérée dans HalideAutotuner dû aux restric-


tions que nous avons mises en place. Cependant, nous avons constaté que HalideAutotu-
ner renvoyait des schedules moins bons que celui développé manuellement pour la seconde
taille d’entrée (131*131*64*4 - 3*3*64*64). Par conséquent, nous avons décidé de prendre
en considération cette optimisation pour les benchmarks qui renvoyait des schedules moins
bons. En effet, cette prise en considération de l’optimisation de fusion a amélioré, dans
ces cas, le temps d’exécution du programme à optimiser.

Nous avons remarqué également que les schedules renvoyés par HalideAutotuner ap-
pliquent parfois des optimisations de type découpage en bandes, qui n’ont pas d’effet sur
le schedule, car ils ne sont ni vectorisés ni déroulés par la suite.

70
Tableau 43: Tableau comparatif entre les schedules construits automatiquement et manuellement en fonction de la taille des entrée pour le programme
de convolution

71
Chapitre VI. Tests et évaluations
Chapitre VI. Tests et évaluations

VI.3.2 Benchmark ReLU


ReLU est une fonction d’activation utilisée dans les réseaux de neurones. Comme
d’autres fonctions d’activation, ReLU est appliquée à la sortie d’un neurone pour éliminer
la linéarité lors des calculs qui transitent sur les couches d’un réseau de neurones. ReLU
est définie par : ∀x ∈ R, ReLU(x) = maximum(0,x).

VI.3.2.1 Algorithme Halide de ReLU et ses caractéristiques


Dans les réseaux de neurones convolutifs, la fonction ReLU est appliquée sur tous les
neurones d’une couche donnée. L’algorithme 6 présente l’algorithme Halide qui implé-
mente la fonction ReLU.

Algorithme 6 Algorithme Halide ReLU


1: Var x,y,z,n ;
2: Func relu, input ;
3: relu(x, y, z, n) ← max(0, input(x,y,z,n));

Le programme ReLU reçoit en entrée un ensemble d’images de taille N, où chaque


image est de taille H*W*c. Il calcule et renvoie le même ensemble d’images, mais avec des
pixels de valeur >= 0. L’ensemble des images en entrée varie selon leur taille H*W*c*N :
H est la longueur de l’image, W sa largeur, c son nombre de canaux et N le nombre
d’images en entrée.

VI.3.2.2 Annotation de l’algorithme ReLU


Nous avons annoté le programme ReLU pour trois tailles d’entrée différentes : 64*64*32*
32, 128*128*64*4 et 256*256*32*32. Comme le programme ReLU inclut peu de fonctions
(et une seule fonction consommatrice) son fichier d’annotation était de petite taille. Le
fichier d’annotation est illustré dans la figure 44 avec la taille d’entrée 64*64*32*32.

Figure 44: Fichier d’annotation pour le programme de relu avec des images de taille 64*64*32*32.

ReLU calcule en sortie un tableau à 4 dimensions. Dans ce cas, ce tableau a une taille de
64*64*32*32 (voir ligne 1 du fichier d’annotation) avec des données de type réel (voir ligne
3 du fichier d’annotation). Nous renseignons toutes les fonctions du programme (input
et relu) avec l’étendue de chaque variable qui apparaît dans la fonction. Par exemple,
la fonction relu dispose de quatre variables ordonnées dans vars, dont nous estimons
l’étendue dans estime. La fonction relu fait appel à la fonction input (ligne 10 du fichier
d’annotation) et son niveau de boucle vectorisable est x.

72
Chapitre VI. Tests et évaluations

VI.3.2.3 Evaluation des performances


Nous avons annoté et optimisé automatiquement l’algorithme ReLU pour trois tailles
d’entrée différentes (voir tableau XIV). Nous avons choisi les tailles d’entrée utilisées
souvent dans les réseaux de neurones convolutifs et qui constituent les tailles de sortie
de l’étage de convolution (comme ReLU est appliquée souvent suite à une opération de
convolution). Cela est fait pour montrer la différence entre le temps d’exécution du pro-
gramme optimisé à la main et celui optimisé par HalideAutotuner.

Comme le programme ReLU n’est pas optimisé par des experts, nous l’avons nous-
mêmes optimisé manuellement. Pour aboutir à un schedule de bonne qualité, nous avons
suivi le raisonnement suivant :

– Nous n’avons pas appliqué le tuilage car il n’y avait pas de réutilisation de données.

– Comme l’accès aux données en mémoire était séquentiel pour les deux fonctions relu
et input, nous n’avons pas appliqué l’optimisation d’interversion de boucles.

– Nous avons testé la fusion plusieurs fois sur les deux niveaux de boucles les plus
externes.

– Nous avons appliqué la parallélisation sur le niveau de boucle le plus externe.

– Nous avons appliqué la vectorisation sur le niveau de boucle x et nous avons fait
varié le facteur de vectorisation plusieurs fois.

– Nous avons testé plusieurs fois le déroulage et avec différents facteurs.

En effet, cette optimisation manuelle nous a pris 25 minutes, entre construction de


chaque schedules si , compilation et exécution du programme optimisé par chaque schedule
si . Une fois trouvé un schedule d’assez bonne qualité, nous l’avons testé avec les autres
tailles d’entrée (voir tableau XIV).

Tableau XIV: Statistiques à propos de l’optimisation automatique du programme ReLU en utili-


sant HalideAutotuner.

Nb.
Les entrées exe-Naïf exe-Manuel exe-Autotuner AccN AccM TS
schedules

- 64*64*32*32 0.01144 s 0.009098 s 0.002927 s 3.91 3.11 18 5min.

- 128*128*64*4 0.01121 s 0.00916 s 0.00345 s 3.24 2.65 20 5min.

- 256*256*32*32 0.157481 s 0.0835 s 0.01561 s 10.08 5.35 24 20min.

Le temps pris par HalideAutotuner pour optimiser les instances de l’algorithme ReLU
est petit relativement à celui pris pour l’optimisation des programmes de convolution.
Cela est dû à la taille de l’algorithme ReLU qui ne compte que quelques fonctions, et
donc un plus petit espace de recherche des combinaisons d’optimisations par rapport à

73
Chapitre VI. Tests et évaluations

celui de la convolution. De plus, ReLU est moins complexe donc son temps d’exécution
est plus petit (que ceux de la convolution) même en lui appliquant des combinaisons d’op-
timisations médiocres. Par exemple, le pire temps d’exécution que nous avons eu lors de
l’optimisation automatique de l’algorithme ReLU et avec la plus grande taille d’entrée
’256*256*32*32’ était de 0.092 secondes.

La technique d’optimisation automatique arrive à des schedules meilleurs que ceux dé-
veloppés manuellement avec une accélération supérieure à 2×, et améliore les algorithmes
ReLU naïfs avec une accélération allant de 3× jusqu’à 10×.

VI.3.3 Benchmark Convolution-ReLU


Ce benchmark regroupe deux algorithmes, celui de la convolution et celui de ReLU,
pour former un pipeline de traitement plus long. Comme nous l’avons souligné plus-haut,
la fonction ReLU est généralement précédée par la fonction de convolution, donc leur
combinaison est judicieuse. En effet, l’optimisation de deux étages de traitement de façon
indépendante ne donne pas de bons résultats par rapport à l’optimisation globale du
pipeline, qui prend en considération l’échange de données entre les différents étages du
programme.

VI.3.3.1 Algorithme Halide de Convolution-ReLU et ses caractéristiques


L’algorithme 7 définit l’algorithme Halide du benchmark de Convolution-ReLU et le
tableau XV rassemble ses fonctions et le nombre de variables associées à chacune.

Algorithme 7 Algorithme Halide Convolution-ReLU


1: Var x,y,z,n ;
2: RDom r(0,max,0,max,0,max) ;
3: Func conv, infilter, input, relu ;
4: conv(x, y, z, n) ← 0;
5: conv(x, y, z, n) ← conv(x, y, z, n)+input(x+r.x, y +r.y, r.z, n)∗infilter(r.x, r.y, r.z, n);
6: relu(x, y, z, n) ← max(0, input(x,y,z,n));

Tableau XV: Caractéristiques des fonctions consommatrices du benchmark Convolution-ReLU

Nombre des ni-


Fonction consommatrice
veaux de boucle
conv 4
conv.update() 7
relu 4

Comme pour le programme de convolution, le programme Convolution-ReLU prend


en entrée des images de taille H*W*c*N et des filtres de taille h*w*c*Z. Chaque filtre
est appliqué sur les images en entrée pour l’opération de convolution, ensuite la sortie
est redirigée vers la fonction ReLU pour éliminer la linéarité créée par l’opération de
convolution.

74
Chapitre VI. Tests et évaluations

VI.3.3.2 Annotation du programme Convolution-ReLU


Comme le benchmark Convolution-ReLU était une composition entre les deux étages
de traitement de convolution et ReLU, alors son fichier d’annotation reprenait la même
annotation que celle du programme de convolution. La seule différence, c’est que nous
avons rajouté des informations sur la fonction relu, en indiquant qu’elle a fait appel à la
fonction conv (voir ligne 12 et 13 du fichier d’annotation présenté dans la figure 45).

Figure 45: Fichier d’annotation pour le programme de Convolution-ReLU avec des images de
taille 68*68*16*32 et des filtres de taille 5*5*16*32.

VI.3.3.3 Evaluation des performances


L’algorithme Halide qui implémente Convolution-ReLU a été développé et optimisé
à la main dans [Suriana, 2017]. Donc pour juger de la qualité de HalideAutotuner, nous
avons optimisé 3 instances de cet algorithme (avec trois tailles d’entrée différentes : petite,
moyenne et grande) et nous avons comparé leur temps d’exécution avec celui optimisé
dans [Suriana, 2017]. Le tableau XVI récapitule tous les résultats des tests.

Tableau XVI: Statistiques sur l’optimisation automatique du programme Convolution-ReLU par


HalideAutotuner

Nb.
Les entrées exe-Naïf exe-Manuel exe-Autotuner AccN AccM TS
schedules

- images : 68*68*16*32
1.67 s 0.050244 s 0.02663 s 62.98 1.88 386 2h05min.
- filtres : 5*5*16*32

- images : 131*131*64*4
2.55 s 0.081492 s 0.082745 s 30.91 0.98 460 2h25min.
- filtres : 3*3*64*64

- images : 260*260*16*32
23.04 s 0.587605 s 0.110015 s 209.46 5.34 1022 12h25min.
- filtres : 5*5*16*32

HalideAutotuner arrive à générer des schedules dont la qualité approche celle des
schedules développés par l’expert. Au niveau de la seconde ligne du tableau XVI, nous
remarquons que le schedule construit par HalideAutotuner est moins bon que celui dé-
veloppé par l’expert (accélération de 0.98 qui est inférieure à 1). Néanmoins, le schedule
développé par HalideAutotuner reste compétitif et proche de celui développé par l’expert.

75
Chapitre VI. Tests et évaluations

L’optimisation du benchmark Convolution-ReLU par HalideAutotuner a pris plus de


temps par rapport à ce qu’a pris l’optimisation automatique des programmes ReLU et de
convolution. De même, le temps pris pour optimiser Convolution-ReLU n’est pas égal au
temps pris pour l’optimisation de la convolution plus le temps pris pour l’optimisation
de ReLU. Pour la plus petite taille d’entrée (des images de 68*68*16*32 et des filtres de
5*5*16*32), le temps pris par HalideAutotuner pour optimiser ReLU était de 5 minutes
et celui pris pour optimiser la convolution était de 13 minutes, alors que l’optimisation
automatique de Convolution-ReLU a demandé un temps égal à 2 heures et 5 minutes.
En effet, l’une des raisons de cette différence est que la composition de ces deux étages
de traitement dans Convolution-ReLU, a créé la possibilité d’appliquer l’optimisation de
granularité de calcul qui n’avait pas lieu d’être appliquée ni dans la convolution ni dans
ReLU.

VI.3.4 Benchmark MaxPool


Le principe de MaxPool est de réduire la taille d’une donnée rectangulaire de taille X*Y
avec un facteur x*y. Pour une image, cela consiste à parcourir ses pixels en utilisant une
fenêtre de taille x*y. A chaque fois que cette fenêtre se positionne sur une zone de pixels de
taille x*y de l’image en entrée, elle prend le pixel maximal parmi ceux sélectionnés par la
fenêtre. La figure 46 illustre une image de taille 16*16, qui est parcourue en utilisant une
fenêtre de Maxpooling de taille 2*2. L’image résultante rassemble les valeurs maximales
de tout rectangle de pixels de taille 2*2 figurant dans l’image en entrée. Il existe d’autres
variantes de MaxPool comme le AveragePool qui calcule la moyenne au lieu du maximum.

Figure 46: Principe du maxpooling sur une image.

VI.3.4.1 Algorithme Halide de MaxPool


L’algorithme 8 présente l’implémentation Halide du programme MaxPool. Cet algo-
rithme compte uniquement deux fonctions de 4 niveaux de boucles chacune (input et
maxpool ) et dont une seule est consommatrice.

Algorithme 8 Algorithme Halide MaxPool


1: Var x,y,c,n ;
2: RDom rp(0, pool_size, 0, pool_size) ;
3: Func input, maxpool ;
4: maxpool(x, y, c, n) ← maximum(input(pool_size * x + rp.x, pool_size * y + rp.y, c, n));

MaxPool représente une des couches d’un réseau de neurone convolutif qui sert à
réduire le nombre de neurones qui apparaissent dans la première couche (un neurone est

76
Chapitre VI. Tests et évaluations

équivalent à un pixel d’une image). Il sert à réduire le nombre de paramètres appris par
le modèle en réduisant la dimensionnalité du réseau de neurone.

VI.3.4.2 Evaluation des performances


Le benchmark MaxPool est simple comme celui de ReLU car il ne contient qu’une
seule fonction à optimiser. Pour le test de MaxPool nous avons varié la taille des images
en entrée et la taille de la fenêtre pour donner naissance à trois instances de test. Le
tableau XVII récapitule la taille des entrée de chaque instance optimisée ainsi que son
temps d’exécution lorsqu’elle n’est pas optimisée, lorsqu’elle est optimisée manuellement
et lorsqu’elle est optimisée par HalideAutotuner.

Tableau XVII: Statistiques à propos de l’optimisation automatique du programme de MaxPool en


utilisant HalideAutotuner

Nb.
Les entrées exe-Naïf exe-Manuel exe-Autotuner AccN AccM TS
schedules

- images : 64*64*32*32
0.008967 s 0.007973 s 0.001407 s 6.37 5.66 16 4min.
- pool : 4*4

- images : 128*128*64*4
0.008385 s 0.005421 s 0.004015 s 2.08 1.35 21 6min.
- pool : 2*2

- images : 256*256*32*32
0.096084 s 0.033422 s 0.01 s 9.60 3.34 18 8min.
- pool : 4*4

L’algorithme MaxPool a été optimisé par nous-mêmes en suivant le même raisonne-


ment adopté lors de l’optimisation de l’algorithme ReLU. Cette optimisation nous a pris
15 minutes répartis entre : variation des combinaisons d’optimisations appliquées à l’al-
gorithme, compilation et exécution de l’algorithme sur lequel est appliqué la combinaison
d’optimisations.

Les schedules générés par HalideAutotuner sont meilleurs que ceux produits à la main.
On remarque une accélération AccM qui est supérieure à 1.35 pour toutes les instances
de test. Le temps pris par HalideAutotuner pour optimiser l’algorithme MaxPool est
relativement petit, car l’algorithme est simple à optimiser (il ne contient qu’une seule
fonction consommatrice).

VI.3.5 Benchmark de la multiplication de matrices


L’opération de convolution peut être implémentée comme une opération de multiplica-
tion de matrices [Karpathy, 2017]. La multiplication de matrices constitue un programme
gourmand en termes de temps d’exécution dû à ses contraintes de localité de données.
En effet, plusieurs méthodes ont été proposées pour optimiser de façon automatique le
programme de multiplication de matrices sur toute architecture d’exécution. Nous allons
à notre tour appliquer HalideAutotuner sur le programme de multiplication de matrices

77
Chapitre VI. Tests et évaluations

et comparer entre son optimisation automatique faite par HalideAutotuner et son opti-
misation manuelle.

VI.3.5.1 Algorithme Halide de la multiplication de matrices


L’algorithme 9 représente l’implémentation de la multiplication de matrice trouvée
dans [Adams, 2016]. Il inclut huit différentes fonctions (As, A, Atmp, B, prod, AB,
AB.update() et result_) dont 3 sont consommatrices (AB.update(), result_ et As).

Algorithme 9 Algorithme Halide - Multiplication de matrices


1: const int s ;
2: Var i,j,z,k ;
3: Func As, Atmp, A, B, result, prod, AB ;
4: RDom rv(0, max) ;
5: As(i, j, z) = Atmp(s ∗ z + i, j);
6: A(i, j) = As(i%s, j, is);
7: prod(k, i, j) = A(i, k) ∗ B(k, j);
8: AB(i, j)+ = prod(rv, i, j);
9: result( i, j) = AB(i, j);

VI.3.5.2 Evaluation des performances


Nous avons trouvé l’implémentation optimisée de la multiplication de matrice écrite
en Halide dans GitHub [Adams, 2016]. Nous avons extrait la partie algorithme de cette
implémentation et nous lui avons appliqué HalideAutotuner avec trois tailles d’entrée dif-
férentes. Les tailles d’entrée varient selon la taille de la matrice A et celle de la matrice
B.

Le tableau XVIII résume les informations relatives à l’utilisation de HalideAutotu-


ner pour l’optimisation automatique des instances de l’algorithme de multiplication de
matrices.
Tableau XVIII: Statistiques sur l’optimisation automatique de l’algorithme de la multiplication
de matrices par HalideAutotuner

Nb.
Les entrées exe-Naïf exe-Manuel exe-Autotuner AccN AccM TS
schedules

- A : 32*400
0.031409 s 0.003519 s 0.003431 s 9.15 1.02 232 1h18min.
- B : 400 * 1024

- A : 8*400
0.02055 s 0.142774 s 0.013699 s 1.50 10.42 87 3h25min.
- B : 400*4096

- A : 1000*500
2.0878 s 0.023954 s 0.04688 s 44.53 0.51 507 3h57min.
- B : 500 * 2000

78
Chapitre VI. Tests et évaluations

Il apparaît que la méthode HalideAutotuner atteint des optimisations de bonne qualité


pour le programme de multiplication de matrices et pour diverses tailles. Sauf pour une
multiplication de matrices de taille (1000*500) * (500*2000), nous avons eu des perfor-
mances 2 × moins bonnes que celles dans la version optimisée manuellement.

VI.3.6 Benchmark de la multiplication matricielle par lots


Ce benchmark consiste à calculer la multiplication d’un ensemble de matrices à la fois.
Donc au lieu de réaliser l’opération AB(i,j) = A(i,k) * B(k,j) pour tout i et j, on calcule
AB(i,j,n) = A(i,k,n) * B(k,j,n) pour tout i, j, M(i,j,n) : i pour l’indice de ligne, j pour
l’indice de colonne, n l’indice de la matrice calculée.

VI.3.6.1 Algorithme Halide de la multiplication matricielle par lots


L’algorithme de la multiplication de matrices par lots est le même que celui de la
multiplication de matrices, à la différence qu’une nouvelle variable n vient s’ajouter à
toutes les fonctions de l’algorithme pour indexer les matrices (voir algorithme 10).

Algorithme 10 Algorithme Halide - Multiplication de matrices par lots


1: const int s ;
2: Var i,j,z,k,n ;
3: Func As, Atmp, A, B, result, prod, AB ;
4: RDom rv(0, max) ;
5: As(i, j, z, n) = Atmp(s ∗ z + i, j, n);
6: A(i, j, n) = As(i%s, j, is, n);
7: prod(k, i, j, n) = A(i, k) ∗ B(k, j, n);
8: AB(i, j, n)+ = prod(rv, i, j, n);
9: result( i, j, n) = AB(i, j, n);

VI.3.6.2 Evaluation des performances


Les instances de la multiplication de matrices par lots varient selon la taille du lot
de matrices, auquel s’ajoute les tailles des matrices A et B à multiplier. Le tableau XIX
regroupe toutes les informations portant sur la comparaison entre le meilleur schedule op-
timisé à la main et le meilleur schedule optimisé par HalideAutotuner et cela pour chaque
instance de test.

L’algorithme de la multiplication matricielle par lots n’a pas été optimisé auparavant
par un expert, alors nous l’avons nous-mêmes optimisé. Pour ce faire, nous nous sommes
basés sur l’optimisation de la multiplication de matrice qui se trouve dans [Adams, 2016]
auquelle nous avons rajouté des optimisations de type parallélisation sur le niveau de
boucle externe de chaque fonction consommatrice (sur le niveau n), et nous avons exploré
plusieurs facteurs de tuilage et de découpage en bandes. On ne peut pas estimer le temps
qu’il nous a fallu pour l’optimisation manuelle car le schedule que nous avons proposé est
basé sur un autre qui existe déjà.

79
Chapitre VI. Tests et évaluations

Tableau XIX: Statistiques sur l’optimisation automatique de l’algorithme de la multiplication de


matrices par lots

Nb.
Les entrées exe-Naïf exe-Manuel exe-Autotuner AccN AccM TS
schedules
- matrice A : 512*100
- matrice B : 100*500 5.694 s 0.129501 s 0.115148 s 49.45 1.12 791 4h30min.
- lot : 100
- matrice A : 1000*500
- matrice B : 500*1000 34.814 s 1.021246 s 0.657695 s 52.93 1.55 819 8h06min.
- lot : 50
- matrice A : 256*500
- matrice B : 500*256 87.276 s 1.583942 s 0.869079 s 100.42 1.82 829 18h07min.
- lot : 1000

HalideAutotuner arrive à améliorer les algorithmes naïfs avec un facteur AccN su-
périeur à 49, et à construire des schedules qui sont non seulement compétitifs mais qui
améliorent encore le temps d’exécution de l’algorithme optimisé à la main (AccA supé-
rieur à 1.1). L’optimisation automatique des instances de test a été faite en prenant en
considération l’optimisation de fusion, pour améliorer la qualité des schedules. De ce fait,
HalideAutotuner explore un plus grand espace de recherche (un espace supérieur à 790
schedules) pour l’optimisation de la multiplication matricielle par lots.

VI.3.7 Benchmark de la multiplication matricielle transposée par


lots
Ce benchmark consiste à calculer la multiplication transposée d’un ensemble de ma-
trices à la fois. Donc au lieu de réaliser l’opération AB(i,j,n) = A(i,k,n) * B(j,k,n) pour
tout i, j et n, on calcule AB(i,j,n) = A(i,k,n) * B(j,k,n). M(i,j,n) : i pour l’indice de ligne,
j pour l’indice de colonne, n l’indice de la matrice calculée.

VI.3.7.1 Algorithme Halide de la multiplication matricielle transposée par


lots
L’algorithme 11 présente l’implémentation Halide du programme de la multiplication
matricielle transposée par lots. Nous n’avons pas trouvé une implémentation Halide opti-
misée pour cet algorithme.

Algorithme 11 Algorithme Halide - Multiplication de matrices transposée par lots


1: const int s ;
2: Var i,j,z,k,n ;
3: Func As, Atmp, A, B, result, prod, AB ;
4: RDom rv(0, max) ;
5: As(i, j, z, n) = Atmp(s ∗ z + i, j, n);
6: A(i, j, n) = As(i%s, j, is, n);
7: prod(k, i, j, n) = A(i, k) ∗ B(j, k, n);
8: AB(i, j, n)+ = prod(rv, i, j, n);
9: result( i, j, n) = AB(i, j, n);

80
Chapitre VI. Tests et évaluations

Par conséquent, nous nous sommes basés sur l’optimisation de la multiplication de ma-
trice transposée exposée dans [Adams, 2016], et nous avons rajouté une nouvelle dimension
à toutes les fonctions manipulées dans le programme (cette dimension est rajoutée pour
indexer les matrices multipliées, car on multiplie un lots de matrices et non pas deux ma-
trices). Nous avons rajouté à ce schedule des optimisations de parallélisation pour chaque
fonction consommatrice qui manipule la nouvelle dimension.

VI.3.7.2 Evaluation des performances


Le tableau XX regroupe toutes les informations portant sur la comparaison entre le
meilleur schedule optimisé à la main et le meilleur schedule optimisé par HalideAutotuner.
Le temps d’exécution des programmes optimisés par HalideAutotuner sont 2× à 5× plus
rapides que ceux optimisés à la main.

Tableau XX: Statistiques sur l’optimisation automatique de l’algorithme de la multiplication ma-


tricielle transposée par lots

Nb.
Les entrées exe-Naïf exe-Manuel exe-Autotuner AccN AccM TS
schedules
- matrice A : 256*500
- matrice B : 500*256 173.34 s 1.076967 s 0.57198 s 303.05 1.88 281 8h16min.
- lot : 1000
- matrice A : 512*100
- matrice B : 100*500 5.9081 s 0.613993 s 0.120643 s 48.97 5.08 262 13h17min.
- lot : 100
- matrice A : 1000*500
- matrice B : 500*1000 83.893 s 4.21623 s 0.726127 s 115.53 5.80 245 13h53min.
- lot : 50

Le nombre de schedules explorés par HalideAutotuner est plus petit que celui exploré
pour l’optimisation de la multiplication de matrices par lots. Cela est dû à la désactiva-
tion de l’optimisation de fusion dans le cas de la multiplication matricielle transposée par
lots, ce qui a réduit l’espace à explorer. En effet, la prise en considération de l’optimi-
sation de fusion implique, un espace plus grand de 23 fois, comme il existe 3 fonctions
consommatrices dans le programme de la multiplication matricielle transposée par lots.

VI.4 Synthèse des tests


HalideAutotuner a été testé sur un ensemble de 22 instances, annotées manuellement,
où chaque instance représente un programme qui implémente une couche des RNC et
auquel est associé une certaine taille d’entrée. Ces tailles d’entrée choisies sont les tailles
les plus utilisées en pratique dans les différents projets implémentant un RNC.

HalideAutotuner a pris un temps d’exécution différent pour chaque instance de test.


En effet, les instances caractérisées par un nombre important de fonctions et de tailles
d’entrée plus grandes prenaient plus de temps lors de l’optimisation automatique par rap-
port aux autres instances. Cela revient à leur espace des combinaisons d’optimisations qui
est plus large et au temps d’exécution pris pour le test de chaque schedule. Néanmoins,
toutes les instances ont été optimisées dans un temps inférieur à 24 heures.

81
Chapitre VI. Tests et évaluations

HalideAutotuner a réduit le temps d’exécution des algorithmes naïfs (sans optimisa-


tions) de 1.5 × jusqu’à plus de 300 × selon l’instance optimisée. De même les algorithmes
optimisés par HalideAutotuner, dans 21/22 des cas, sont 1.02 × à 10 × meilleurs que ceux
optimisés manuellement par des experts.

HalideAutotuner générait parfois des schedules qui sont moins bons que ceux dévelop-
pés manuellement par des experts. Dans ces cas, nous avons repris les tests avec la prise
en considération de l’optimisation de fusion et nous avons remarqué que les schedules
se sont nettement optimisés. Cependant, pour les instances où les schedules générés par
HalideAutotuner était meilleurs que ceux développés manuellement, nous n’avons pas pris
en considération l’optimisation de fusion.

Même en activant l’optimisation de fusion, HalideAutotuner n’est pas arrivé à générer


un bon schedule pour l’optimisation de la multiplication de matrices avec une taille d’en-
trée de 1000*500 - 500*2000. En effet, le programme optimisé par HalideAutotuner était
2 × plus lent que le programme optimisé à la main par Andrew Adams 2 .

Suite aux analyses que nous avons faites sur les schedules construits par HalideAu-
totuner et ceux développés à la main, nous avons constatés plusieurs différences. Parfois
les optimisations sont appliquées sur la même fonction mais pas de la même façon : avec
des facteurs différents ou sur d’autres niveaux de boucles. Nous avons expliqué la diffé-
rence en termes de performances entre le schedule développé à la main et celui construit
par HalideAutotuner, par les facteurs de déroulage et de vectorisation utilisés, dans le
cas où ces deux optimisations sont appliquées sur la même fonction et avec des facteurs
différents. De même, on peut comparer entre les schedules suivant les facteurs de tuilage
choisis mais uniquement s’ils sont appliqués sur la même fonction et les mêmes niveaux
de boucles dans les deux schedules. Cependant, une optimisation peut ne pas apparaître
bénéfique pour un algorithme, mais elle peut l’être réellement une fois qu’elle est combinée
avec d’autres optimisations. De ce fait, on ne peut pas toujours expliquer la performance
d’un programme en fonction de ses optimisations.

VI.5 Conclusion
A travers ce chapitre, nous avons montrer les performances de la méthode HalideAu-
totuner sur des programmmes implémentant les réseaux de neurones convolutifs, et cela
pour différentes tailles d’entrée. Tous les résultats que nous avons présentés confirment
que HalideAutotuner est capable de générer des schedules compétitifs à ceux développés
à la main par des experts.

Les programmes optimisés à la main peuvent avoir une bonne performance, mais seule-
ment par rapport à un ensemble d’architectures qui partagent quelques caractéristiques
matérielles communes. Mais, lorsqu’un compilateur est utilisé pour tester les différentes
combinaisons d’optimisations, on peut approcher un schedule de bonne qualité sur n’im-
porte quelle architecture d’exécution.

2. Chercheur de chez Facebook

82
Conclusion et perspectives

Notre projet de fin d’étude consiste à concevoir et développer une méthode pratique
qui optimise une classe de programmes Halide destinée à s’exécuter sur une architecture
CPU. Cette méthode est dite pratique, car elle doit optimiser chaque programme de la
classe en un temps inférieur à 24 heures. Le programme optimisé automatiquement doit
avoir un temps d’exécution qui approche celui du programme optimisé à la main (par des
experts en matière d’optimisation de programmes). La classe de programmes à laquelle
nous nous sommes intéressée est utilisée dans divers domaines applicatifs, et elle est ca-
ractérisée par un temps d’exécution élevé. C’est la classe des programmes implémentant
les réseaux de neurones convolutifs.

Dans le premier chapitre, nous avons évoqué un nombre d’optimisations applicables


sur les boucles d’un programme. En effet, ce type d’optimisation est utile car il permet
de réduire le temps d’exécution de la partie la plus gourmande en termes de temps d’exé-
cution qui est la boucle. De plus, l’ensemble des optimisations auquel nous nous sommes
restreints est celui utilisé et pré-implémenté dans le compilateur Halide.

Nous avons insisté sur le fait qu’une optimisation n’est pas nécessairement bénéfique
pour un programme ; car d’une part, elle peut améliorer son temps d’exécution, et d’autre
part, elle peut le dégrader. L’effet d’une optimisation dépend de plusieurs paramètres dont
les caractéristiques matérielles de la machine qui exécute le programme, les optimisations
déjà appliquées sur le code et la structure de ce dernier. Tous ces facteurs ont rendu le
processus d’optimisation de programme complexe et non évident.

Afin d’alléger le travail des programmeurs, quelques compilateurs incluent des modules
pour l’optimisation automatique de leurs programmes. Afin d’optimiser les programmes,
les compilateurs se basent sur une ou une combinaison des quatre approches abordées
dans le second chapitre. Chaque approche a ses propres avantages et inconvénients en
termes de la qualité des optimisations renvoyées et le temps qu’elle prenne pour pouvoir
générer de bonnes optimisations. En effet, aucune approche n’est meilleure que les autres
dans l’absolu.

Dans le troisième chapitre, nous avons présenté le compilateur Halide et les diffé-
rentes méthodes d’optimisation automatique développées pour ce dernier. Halide étant
un langage dédié au domaine du traitement d’images, toutes les techniques d’optimisa-
tion automatique développées pour ce premier ont été testées sur des programmes de
traitement d’image.

Dans la quatrième partie, nous avons exposé la conception de notre système qui per-
met d’optimiser de façon automatique la classe des programmes Halide implémentant les

83
Chapitre VI. Tests et évaluations

réseaux de neurones convolutifs.

Dans la cinquième partie, nous avons cité et justifié nos différents choix technologiques
pour le développement de notre système pour l’optimisation automatique des programmes
Halide.

En dernier, nous avons testé la performance de notre système sur sept benchmarks
qui permettent d’implémenter un réseau de neurones convolutifs. Nous avons montré que
notre méthode d’optimisation automatique permet de générer des programmes optimisés
qui sont compétitifs à ceux optimisés à la main par des experts du domaine dans un délai
inférieur à 24 heures.

Les résultats exposés dans la partie tests et évaluations montre la robustesse de notre
méthode d’optimisation automatique. En effet, nous sommes parvenus à construire des
combinaisons d’optimisations qui donnent des programmes optimisés qui sont non seule-
ment compétitifs à ceux optimisés à la main par les experts, mais dont le temps d’exécution
est bien meilleur dans 96% des cas testés.

Malgré les résultats satisfaisants que nous avons eu, mais notre méthode HalideAutotu-
ner présente quelques limites. Afin d’optimiser un programme, l’utilisateur doit introduire
un ensemble d’informations pour décrire la structure du programme dans un fichier d’an-
notation ‘source.settings’. Plus le programme à optimiser contient de fonctions Halide,
plus le nombre d’informations introduites par l’utilisateur est important ; au bout de cinq
fonctions Halide, la saisie du fichier ‘source.settings’ devient très lourde et pas pratique.
De plus, l’utilisateur doit prédire l’étendue de toute variable qui figure dans le programme,
afin que notre méthode puisse générer des combinaisons d’optimisations valides. Pour y re-
médier, nous proposons d’automatiser la création du fichier d’annotation ’source.settings’.

La méthode d’optimisation automatique que nous avons proposée a généré de bonnes


combinaisons d’optimisation pour les différents programmes de test et pour des tailles
d’entrée différentes. Mais, lorsque nous avons procédé à l’optimisation automatique du
programme de multiplication de matrices avec des tailles d’entrée importantes : (5000*2500)
* (2500*5000), nous avons constaté que HalideAutotuner ne terminait pas au bout de 24
heures, en effet, elle prenait plus de temps. Ce fait est dû au temps d’exécution du pro-
gramme de multiplication de matrices qui prenait parfois une heure et demi juste pour
tester une combinaison d’optimisations. Donc, au bout d’une certaine taille d’entrée où
le temps d’exécution du programme augmente considérablement, l’optimisation automa-
tique de HalideAutotuner ne devient plus pratique.

Le choix des tailles de tuilage, de déroulage et de vectorisation explosent l’espace de


recherche de HalideAutotuner et nous font consommer énormément de temps lors de la
phase de recherche des bonnes optimisations. D’autant plus que le choix de ces facteurs
n’est pas aussi bien élaboré avec une technique de Hill Climbing. Alors, nous proposons
d’utiliser un modèle basé sur l’apprentissage automatique pour le choix de ces paramètres.

Afin de générer ce modèle, on a besoin d’une base de données d’apprentissage et d’un


algorithme d’apprentissage automatique. La base de données d’apprentissage contient gé-
néralement les caractéristiques du programme à optimiser. Néanmoins, nous croyons que

84
Chapitre VI. Tests et évaluations

la caractérisation d’un programme est complexe, et nous ne pouvons pas le représenter


fidèlement à travers un vecteur de caractéristiques. Par conséquent, nous proposons de
caractériser une fonction au lieu de tout un programme Halide. Pour construire la base
de données d’apprentissage, nous allons parcourir tous les programmes que nous avons
optimisé et qui sont stockés dans la base de données et pour chaque fonction déroulée/
vectorisée ou tuilée, nous allons extraire ses informations ainsi que les facteurs utilisés
pour le déroulage, la vectorisation et le tuilage et avec un booléen positionné à vrai, si les
facteurs améliorent le temps d’exécution du schedule, à faux sinon. Quant à la sortie du
modèle, elle sera de vrai ou faux : vrai, si les facteurs en entrée du modèle sont adéquats
pour la fonction à optimiser, faux sinon. Un modèle d’optimisation automatique généré
en utilisant les SVM à deux classes s’apprête bon pour cette mission.

Une fois que le modèle est entraîné, il sera utilisé dans notre système. En effet, pour de
nouveaux programmes, HalideAutotuner commence par décider de l’optimisation d’inter-
version de boucle en utilisant le modèle analytique de [Allen and Kennedy, 2002]. Ensuite,
il utilise la méta-heuristique du grimpeur pour décider de l’optimisation de granularité
de calcul. En dernier, il va utiliser le modèle entraîné ; ce dernier va avoir en entrée une
combinaison des facteurs de déroulage, vectorisation et tuilage et il est censé nous donner
une information sur est-ce que cette combinaison est bénéfique pour la fonction ou pas.
Nous allons tester plusieurs combinaisons de facteurs en entrée, et nous allons nous arrêter
à la première combinaison d’optimisations pour laquelle le modèle renvoie la valeur «vrai».

Cela n’était qu’une proposition pour réduire le temps d’exécution de la méthode Ha-
lideAutotuner. Dans le cas où le modèle proposé n’est pas précis (il n’a pas réussi à
prédire les bonnes optimisations pour une fonction), nous allons avoir des combinaisons
d’optimisations de qualité médiocre. En effet, le modèle doit être amélioré de façon itéra-
tive, en rajoutant d’autres caractéristiques ou en enlevant celles qui faussent ses décisions.

Le mode de la méthode d’optimisation automatique que nous avons proposé est offline,
ce qui veut dire que l’optimisation se fait en temps de compilation, où nous ne disposons
pas de toutes les informations (y compris la taille des entrées) sur notre programme à
optimiser. L’idéal serait de développer une méthode d’optimisation automatique online
qui dispose de toutes les informations nécessaires à l’exécution du programme et qui ne
prend pas beaucoup de temps pour s’exécuter. Pour ce faire, il faut développer un mo-
dèle ou une combinaison de modèles analytiques qui sont construits à partir d’une base
de données d’apprentissage en utilisant l’AA. Ces modèles seront capables de prédire les
bonnes optimisations pour un programme sans tester la performance d’aucune combinai-
son d’optimisations.

Pour conclure, ce projet de fin d’études nous a permis de combiner entre deux do-
maines informatiques attrayants qui sont l’optimisation de programmes et l’optimisation
combinatoire pour pouvoir optimiser de façon automatique la classe des programmes im-
plémentant les RNC dans le langage Halide.

85
Troisième partie
Annexes
Annexe A

Analyse de dépendance

L’analyse de dépendance est une théorie qui permet d’identifier toutes les contraintes
de l’ordre d’exécution entre chaque deux instructions du programme [Allen and Kennedy,
2002]. A titre d’exemple, si S1 et S2 sont deux instructions du programme, où S2 ap-
parait textuellement 1 après S1, la théorie d’analyse de dépendance sert à identifier les
contraintes de leur ordre d’exécution.

Dans l’exemple de la figure 47, l’instruction S2 dépend de la valeur de la variable A, qui


est modifiée par l’instruction S1. Donc, S2 doit attendre l’exécution de S1 pour récupérer
la bonne valeur de A. On dit que, S2 dépend de S1, donc S2 et S1 ne peuvent pas être
exécutées en parallèle. Quant à l’exemple de la figure 48, l’instruction S2 ne dépend pas
des résultats de l’instruction S1, car le calcul de B se fait en fonction d’autres variables
qui ne sont pas modifiées par le biais de S1. On dit que, S2 ne dépend pas de S1, donc S1
et S2 peuvent s’exécuter en parallèle.

Figure 47: S2 dépend de S1 (S2 ne peut pas s’exécuter en parallèle avec S1)

Figure 48: S2 ne dépend pas de S1. (S1 et S2 s’exécutent en parallèle).

1. Ecrites l’une après l’autre dans le code.

87
A.1 Validité d’une optimisation
Notons que la théorie de l’analyse de dépendance est nécessaire pour décider de la
justesse et de la validité d’application d’une optimisation sur le programme [Bacon et al.,
1994]. En effet, une transformation sur le programme est valide, si elle maintient toutes les
contraintes d’ordre d’exécution qui apparaissent dans la version originale du programme.

Tout d’abord, on doit identifier les dépendances dans un programme. En effet, il y a


dépendance de données lorsque deux instructions, S1 et S2, tentent d’accéder à la même
localité mémoire M, et qu’au moins l’une de ces deux instructions tente de modifier M.
Toute la complexité d’identification réside dans les instructions de boucles, qui mani-
pulent généralement des tableaux multi-dimensionnels, indexés en fonction des itérations
des boucles 2 .

A.2 Identification des dépendances


Pour identifier les dépendances entre les instructions de boucles et retrouver les itéra-
tions de boucle où la dépendance est manifestée, il faut introduire les concepts suivants.

– Les itérations d’une boucle imbriquée : Les itérations d’une boucle sont modélisées
par un ensemble { i1, i2, . . . , ik, . . . , in }, où ik[1 :n] est un vecteur qui contient les
valeurs de chaque index de boucle, organisées de la boucle la plus externe à la plus
interne, lors de l’itération k [Allen and Kennedy, 2002]. Soit la boucle de la figure
49, l’ensemble des itérations de cette boucle sont { 1, 2, 3,. . . , n }. Quant à la boucle
de la figure 50, l’ensemble de ses itérations sont : { (0,0), (0,1), (0,2), (1,0), (1,1),
(1,2), (2,0), (2,1), (2,2) }, où lors de l’itération i2 = (0,1), la boucle de i prend la
valeur 0 et la boucle de j prend la valeur 1.

Figure 49: Boucle qui manipule une donnée multidimensionnelle A.

Figure 50: Boucle imbriquée qui manipule une donnée multidimension-


nelle A.

2. A[i+j] est un champ du tableau A, on doit analyser les accès aux champs du tableau A à travers
l’expression i+j.

88
– La condition de dépendance dans les boucles : On dit que deux instructions S1 et S2
sont dépendantes s’il existe deux itérations i et j telles que i = j ou i précède j, où
S1 s’exécute à la i-ème itération et S2 s’exécute à la j-ème itération, et où S1 et S2
accèdent à la même localité mémoire M, avec au moins l’une d’entre elles qui tente
de modifier M [Allen and Kennedy, 2002].

– La caractérisation de la dépendance : Pour caractériser une dépendance entre deux


instructions pouvant appartenir à deux itérations de boucle différentes, on définit la
distance entre l’itération i = (i1, i2, . . . , in) de l’instruction S1 qui est la source de
dépendance et l’itération j= (j1, j2, . . . , jn) de l’instruction S2, par ce qu’on appelle
le vecteur de distance.

– Le vecteur de distance d(i,j) = {(j1, j2, . . . ., jn)-(i1,i2,. . . , in)} : Il définit la dis-


tance entre l’itération i et l’itération j [Allen and Kennedy, 2002]. Par exemple,
dans la figure 50, l’instruction S1 : A[1,2] = A[1,1] de l’itération j=(1,1) dépend de
l’instruction S2 (la même que S1 dans ce cas) de l’itération précédente i = (1,0),
alors la distance de dépendance entre les deux instructions S1 et S2 est définie par
le vecteur de distance d(i,j) = (1,1)-(1,0) = (0,1).

Il existe deux types de dépendances au niveau des boucles :

– Les dépendances indépendantes des boucles : C’est une dépendance qui apparait
entre deux instructions S1 et S2 qui s’exécutent pendant la même itération (i=j),
autrement dit, d(i,j)= (0, 0,. . . ,0) [Allen and Kennedy, 2002].

– La dépendance à travers les boucles : C’est une dépendance entre deux instructions
S1 et S2, où S1 s’exécute pendant l’itération i et S2 pendant l’itération j, telles
que i<j. Autrement dit, ce type de dépendance a lieu s’il existe un entier non nul
dans le vecteur de distance d(i,j) [Allen and Kennedy, 2002]. L’exemple de la figure
50, illustre une dépendance à travers les boucles, car d(i,j) = (0,1), où le premier
élément différent de 0 est égal à 1. L’indice du premier élément différent de 0 dans
d(i,j) est appelé niveau de la dépendance à travers les boucles [Allen and Kennedy,
2002]. Dans l’exemple de la figure 50, il est égal à 2.

Une transformation sur le programme est dite valide si elle maintient les mêmes dé-
pendances que celles qui se trouvaient dans la version originale du programme. Autrement
dit, tous les vecteurs de distance positifs 3 dans la version originale du programme doivent
être maintenus dans la version transformée du code.

3. Dont le premier élément non nul est positif.

89
Annexe B

Détails sur quelques optimisations

B.1 Augmentation du taux de parallélisme par le dé-


roulage
Pour plus de précision, nous allons donner le code intermédiaire équivalent à chacun
des segments de code de la figure 51, et montrer l’applicabilité du parallélisme sur le
second code et son inapplicabilité sur le premier.

Figure 51: Déroulage d’une boucle avec facteur k = 3

Figure 52: Code assembleur équivalent à celui de la boucle non déroulée de la figure 51

La figure 52, montre le code assembleur équivalent à la boucle de la figure 51 sans


déroulage. En effet, ce code montre l’utilisation de 6 registres pour réaliser les différentes
opérations. A travers, ce code nous pouvons déduire que les instructions des itérations ne

90
peuvent pas s’exécuter en parallèle malgré qu’elles soient indépendantes, car elles agissent
sur les mêmes registres, d’une itération à une autre. Par exemple, le registre R11 est des-
tiné à contenir B[i+1] et il ne peut pas contenir en même temps B[i+1] et B[i+2] pour la
prochaine itération.

Le déroulage se prête une bonne alternative pour assurer un parallélisme entre les
instructions, car elles vont agir sur des registres différents. Sur la figure 53, nous pouvons
charger simultanément les valeurs B[i+1], B[i+2] et B[i+3] car chacune d’elles s’appro-
priera un registre (dans le cas où la machine dispose d’assez de registres pour les contenir),
et nous pouvons dans ce cas-là charger toutes ces données dans leur endroit approprié qui
est A[i], A[i+1] et A[i+2].

Figure 53: Code assembleur équivalent au programme déroulé de la figure 51

B.2 Tuilage pour la localité des données


Le tuilage est une optimisation qui permet d’améliorer l’utilisation de la mémoire cache
lors de l’exécution d’une boucle imbriquée. Après avoir donné le principe du tuilage dans
la section I.2.7, nous allons illustrer ce dernier sur un exemple pour mieux le comprendre.
Dans la figure 54, on a deux matrices A et B ; B est accessible colonne par colonne et A
est accesible ligne par ligne (le code à gauche). Supposant que la taille de A et B sont de
taille 4*4 chacune.

La figure 55 illustre les défauts de cache et les succès cache lors de l’exécution du
programme de la figure 54 sans et avec tuilage (à facteurs 2*2). Nous nous sommes res-
treints à l’exécution des quatre premières itérations de la boucle imbriquée pour mieux
illustrer les choses dans la figure. Le prélecteur contient toutes les lignes de données qui
ont été préchargées de la mémoire principale vers la mémoire cache chronologiquement du
haut vers le bas, pour montrer les échanges entre ces deux mémoires. Quant à la mémoire

91
Figure 54: Tuilage avec facteurs : n*m (les étendues des deux boucles internes). A[i,j]
et B[j,i] sont des données multidimensionnelles. A[i,j] est accessible ligne par ligne,
mais B[j,i] est accessible colonne par colonne. Après le tuilage, A et B sont accessibles
en blocs de taille n*m.

cache, elle contient les données suite à toute l’exécution de la boucle imbriquée.

Figure 55: Nombre de défauts de cache pour la version tuilée de la boucle, et pour la
version sans tuilage.

Dans le cas où le tuilage n’est pas appliqué, le programme accède aux données A[1,1],
B[1,1], A[1,2], B[2,1], A[1,3], B[3,1], ensuite A[1,4] en dernier B[4,1]. Lors de l’accès à
A[1,1] toutes ses données adjacentes sont chargées avec elle (autant de données qu’une
ligne de cache peut supporter). De même, pour B[1,1]. Ensuite, la seconde itération accède
à A[1,2] et B[2,1]. A[1,2] est déjà dans le cache (succès cache) mais B[2,1] n y est pas donc
elle est chargée ainsi que ses données adjacentes (défaut de cache). L’accès à A[1,3] donne
un succès cache car la donnée y est toujours dans le cache, mais pour B[3,1] il faut la
charger car elle n y est pas dans le cache. Mais le cache est inondé, donc pour charger

92
B[3,1] il faut remplacer des données du cache par les données adjacentes à B[3,1]. Pour
cela, la ligne qui contient les données de A est supprimée et remplacée par B[3,1], B[3,2],
B[3,3] et B[3,4]. La prochaine itération fait référence à A[1,4] et B[4,1] qui produisent
toutes les deux un défaut de cache, car la première a été supprimée lors du remplacement
et la seconde n’a jamais été référencée. Pour l’exécution sans tuilage, on compte 6 défauts
de cache sur 8 accès mémoire.

Dans le cas où le tuilage est appliqué et avec un facteur de 2*2, le programme accède
aux données A[1,1], B[1,1], A[1,2], B[2,1], A[2,1], B[1,2], A[2,2], B[2,2] dans cet ordre. Lors
de l’accès à A[1,1] et B[1,1], toutes leurs données adjacentes sont chargées dans le cache.
Ensuite, les données A[1,2] et B[2,1], où A[1,2] se trouve déjà dans le cache (succès cache)
et B[2,1] n y est pas, donc elle doit être chargée. Lors de l’accès à A[2,1] et B[1,2], la
première produit un défaut de cache et ainsi elle et toutes ses données adjacentes sont
chargées en cache. Mais B[1,2] se trouve déjà dans le cache (succès cache). Enfin, l’accès à
A[2,2] et B[2,2] ne produit aucun défaut de cache, car les données ont été déjà préchargées.

B.3 Difficulté du choix des optimisations pour un pro-


gramme
Illustrons l’impact des optimisations sur un exemple pris de [Ragan-Kelley, 2014], pour
mieux comprendre la difficulté de choisir les bonnes optimisations pour un programme,
et les compromis qu’il y a entre les différents objectifs des optimisations.

Soient trois tableaux à deux dimensions chacun : input, Blurx et Blury.


– Le tableau input représente un tableau introduit par l’utilisateur du programme.
– Le tableau Blurx est un tableau dont la valeur de chaque case représente la moyenne
de 3 cases adjacentes horizontalement dans le tableau input : Blurx(x,y) = input[(x-
1,y)+input(x,y)+input(x+1,y)]/3.
– Le tableau Blury est un tableau dont la valeur de chaque case représente la moyenne
de 3 cases adjacentes verticalement dans le tableau Blurx : Blury(x,y) = [Blurx(x,y-
1)+Blurx(x,y)+Blurx(x,y+1)]/3.
La figure 56 illustre le programme naïf qui rassemble les trois tableaux plus haut.
Cette implémentation connue sous le nom de : largeur d’abord, impose le calcul de toutes
les valeurs du tableau Blurx, avant qu’aucune valeur de Blury ne soit calculée. En effet,
cette implémentation implique l’existence d’un buffer qui est aussi large que le tableau
Blurx pour le contenir, et pour le recharger lors du calcul de Blury.

Cette implémentation favorise le parallélisme. En effet, les itérations de la première


boucle peuvent être lancées en parallèle, de même pour la seconde boucle. Néanmoins,
les valeurs produites par Blurx ne sont pas directement consommées par Blury, mais leur
consommation est retardée jusqu’à la fin du calcul des valeurs de Blurx. Pour régler le
problème de la distance de consommation entre Blurx et Blury, une autre implémentation
est possible (voir figure 57).

93
Figure 56: Implémentation naïve du programme à 3 tableaux : input, Blurx et Blury

Figure 57: Implémentation de la fenêtre coulissante sur le programme à 3 tableaux :


input, Blurx, Blury

Cette implémentation est connue sous le nom de : fusion totale, car elle minimise au
maximum la distance entre l’instant où une donnée est produite et l’instant où elle est
consommée. En effet, cette implémentation utilise de façon efficace la mémoire cache, car
une fois la donnée produite, elle est rapidement consommée, et on a plus de probabilité de
la retrouver dans le cache. De plus, cette implémentation garde le même degré de parallé-
lisme que celui de la version naïve. Néanmoins, un recalcul de données est imposé, car pour
pouvoir paralléliser le calcul de Blury, on doit recalculer des valeurs de Blurx. Par exemple,
pour calculer Blury(x,y), on a besoin de Blurx(x,y), Blurx(x,y+1) et Blurx(x,y-1), et pour
calculer Blury(x,y+1) on aura besoin de Blurx(x,y) et Blurx(x,y+1) et Blurx(x,y+2). Par
conséquent, pour pouvoir paralléliser le calcul de Blury(x,y) et Blury(x,y+1) on a besoin
de calculer les valeurs de Blurx(x,y) et Blurx(x,y+1) deux fois.

Pour remédier au problème du recalcul, il existe une autre possibilité d’implémenta-


tion, qui porte le nom de : fenêtre coulissante. Dans cette alternative, au lieu de calculer
à chaque fois de nouveau toutes les valeurs de Blurx nécessaires au calcul d’une valeur
de Blury, on va plutôt calculer et sauvegarder l’ensemble des valeurs de Blurx nécessaires
au calcul d’un ensemble de valeurs de Blury, et ce dans le but de les réutiliser sans les
recalculer. Cette solution donne naissance à l’implémentation de la figure 58.

Figure 58: Implémentation de la fusion totale sur le programme à trois tableaux :


input, Blurx et Blury.

Dans l’implémentation ci-dessus, aucun recalcul de valeurs n’est aperçu. En effet,


chaque valeur de Blurx n’est calculée qu’une seule fois. Néanmoins, cette alternative ré-
duit le taux de parallélisme, car aucune valeur de Blury n’est calculée jusqu’au calcul de

94
toutes les valeurs du tableau Blurx[m,3] correspondant (voir figure 59).

Figure 59: Schéma démontrant le flux de calcul des données dans l’im-
plémentation de la fenêtre coulissante sur le programme à trois tableaux.

A travers les implémentations que nous avons expliquées en haut, nous avons pu mettre
l’accent sur trois concepts et facteurs qui sont en tension, et qui ne peuvent coexister dans
la même implémentation : le parallélisme, la localité des données et le recalcul des données.

95
Annexe C

Le framework OpenTuner

C.1 Architecture logicielle d’OpenTuner


OpenTuner a la spécificité de lancer en parallèle toutes les techniques d’exploration
d’espace explicitées par l’utilisateur, qui travaillent en collaboration pour trouver la solu-
tion optimale. En effet, chaque technique explore l’espace de recherche à sa façon, stocke
les combinaisons explorées et les performances mesurées respectives dans une base de don-
nées, dans le but qu’une technique ne réexplore pas une combinaison qui a été déjà visitée,
par une autre technique. Cependant, les configurations testées et enregistrées dans la base
de données peuvent être réexploitées par les techniques à base de populations, dans le cas
où elles sont utilisées, pour diversifier leurs populations.

Toutes les techniques sont coordonnées par une super technique appelée méta-technique,
qui est responsable de l’allocation du nombre de tests pour chaque technique d’exploration.
Cette méta-technique favorise la technique d’exploration qui éprouve des améliorations
(trouve des combinaisons de plus en plus performantes) en lui affectant plus de configura-
tions à tester, et néglige les techniques qui donnent des résultats médiocres, et peut même
arriver jusqu’à les désactiver (voir figure 60). La combinaison des techniques d’explora-
tion utilisées par défaut dans OpenTuner regroupe : l’évolution différentielle, la mutation
avare et deux variantes de l’escaladeur (Hill climber), orchestrées par une méta-technique.

C.2 Types de paramètres dans OpenTuner


L’utilisateur du framework OpenTuner définit les différentes configurations de l’espace
de recherche, en initialisant un objet dit Manipulateur de configurations. Ce manipulateur
regroupe les paramètres à autorégler (autotuner) et selon sa nature, l’utilisateur choisit
un type de paramètre parmi ceux prédéfinis dans OpenTuner (voir figure 61).

Les paramètres d’OpenTuner sont inspirés des types de paramètres à autorégler ren-
contrés dans les cas pratiques et les projets d’Autotuning. Nous retrouvons des paramètres
dits primitifs comme : les entiers, les puissances de deux, qui sont des paramètres variables
ayant une borne minimale et une borne maximale, où toute configuration de l’espace
de recherche prend une valeur entre ces bornes. Tandis que, les paramètres complexes,
comme : le paramètre BooleanParameter, qui prend soit la valeur 0 ou 1. Le paramètre

96
Figure 60: Architecture globale d’OpenTuner. α, β, γ présentent des coefficients affectés de la
méta-heuristique du bandit multi-armé aux différentes techniques de recherche, où celle qui dispose
du plus grand coefficient sera exécutée plus que les autres.

Figure 61: Paramètres dans OpenTuner

EnumParameter pour le choix d’un élément parmi une liste d’énumérations. Le paramètre
ScheduleParameter, qui consiste à organiser les éléments d’une liste avec des contraintes
de précédence.

Prenons l’exemple de la vectorisation d’un niveau de boucle x de la fonction f. Le


paramètre à autorégler est le facteur de vectorisation qui est un entier, de puissance de
deux, qui varie par exemple entre 1 et 32, où 1 signifie absence de vectorisation. Tout
d’abord, nous devons commencer par définir l’objet Manipulateur de Configuration qui
va contenir tous les paramètres à autorégler, ensuite, nous rajoutons à ce dernier, le
paramètre de vectorisation à autorégler qui est de type PowerOfTwoParameter, et qui
varie entre 1 et 32 (voir figure 62).

97
Figure 62: Exemple de déclaration d’un paramètre Halide à autorégler

98
Annexe D

Optimisation automatique des


programmes Halide

D.1 Calcul du taux de réutilisation des données pour


une fonction.
Le taux de réutilisation des données d’une boucle, représente le nombre de données
calculées à une itération de cette boucle et qui sont réutilisées dans la prochaine itération.
Nous cherchons toujours à favoriser le parcours de fonctions, qui détient le plus grand taux
de réutilisation des données. Dans la vision de réduire le taux de calcul pour la prochaine
itération et réduire le temps de chargement de la donnée car elle est déjà dans le cache.

Figure 63: Le corps de la fonction Blurx en Halide, extrait du corps de la fonction de troublement
d’une image.

Pour décider par exemple, de l’ordre de parcours de la fonction Blurx, on doit calculer
le taux de réutilisation garanti par chaque parcours : ligne par ligne (voir figure 64) et
colonne par colonne (voir figure 65) pour cette fonction.

Figure 64: Code équivalent du parcours ligne par ligne de la fonction Blurx.

Si le parcours de la fonction Blurx est de ligne par ligne, alors x représente la dimension
de la boucle la plus interne (voir 64) :

– Blurx nécessite le calcul de 3 valeurs d’input pour chaque itération de la boucle x.

99
– Le calcul d’une valeur de Blurx, réutilise deux valeurs d’input qui ont été déjà
calculées dans l’itération précédente : input(x-1,y) et input(x,y).
– Le taux de réutilisation = 2.

Figure 65: Code équivalent du parcours colonne par colonne de la fonction Blurx.

Si le parcours de la fonction Blurx est de colonne par colonne, alors y représente la


dimension de la boucle la plus interne (voir figure 65) :
– Blurx nécessite le calcul de 3 valeurs d’input pour chaque itération de la boucle y.
– Le calcul de la seconde valeur de Blurx, ne réutilise aucune valeur d’input qui est
calculée dans l’itération précédente.
– Le taux de réutilisation = 0.
Dans ce cas-là, il est préconisé de choisir le domaine de parcours ligne par ligne au
lieu de colonne par colonne pour assurer un niveau de réutilisation pour son code.

D.2 Schedules raisonnables pour la population initiale


de l’auto-scheduler
La technique de l’algorithme génétique, calcule l’empreinte rectangulaire de chaque
fonction productrice f. Cette quantité représente le bloc rectangulaire minimal, regrou-
pant les valeurs de f nécessaires au calcul d’une seule valeur de g (g consommatrice de
f). Dans la figure 66, l’empreinte rectangulaire de f par rapport à g est un bloc de taille 3*3.

Figure 66: Rectangle minimal qui englobe les valeurs de la fonction productrice f nécessaires au
calcul d’une valeur de la fonction consommatrice g.

Si l’empreinte rectangulaire = 1*1 :

100
– L’AutoTuner substitue l’appel à la fonction productrice, par son expression arith-
métique (son corps).

– Sinon, il choisit entre deux alternatives de transformations à appliquer pour la fonc-


tion productrice :

– Il tuile la fonction selon x et y en utilisant la taille de l’empreinte rectangulaire,


parallélise la boucle la plus externe et vectorise la plus interne.
– Ou, il parallélise la boucle la plus externe.

D.3 Coût arithmétique d’une fonction pour l’auto-scheduler


de Halide
Calculer le coût arithmétique d’une fonction, revient à estimer sa complexité de calcul
en termes des opérations arithmétiques qu’elle utilise. Prenant l’exemple suivant : F(x,y)
= g(x,y)+y*4+6. La technique associe à chaque opération un poids, selon sa complexité.
Par exemple : le coût de l’addition = 1 et le coût de la multiplication = 6. Par conséquent,
C(F) = 1+6+1 =8. C(f) : coût arithmétique de F.

101
Annexe E

Choix de conception

E.1 Diagramme de classe pour la recherche exhaustive


des schedules Halide
A cette étape, nous modélisons l’architecture de notre système par un diagramme de
classe, où nous faisons apparaitre les entités les plus importantes, leurs méthodes et leurs
attributs (voir figure 67).

La classe GenerateurOptimisation est une classe abstraite responsable de l’exploration


des optimisations. Toutes les classes qui héritent de cette dernière doivent implémenter la
méthode explore_optim de façon à explorer toutes les valeurs que peut prendre l’optimisa-
tion concernée. Par exemple, la classe SplitGenerateur redéfinit la méthode explore_optim
pour générer toutes les optimisations de type Split qui dépendent de la structure du pro-
gramme en entrée et des optimisations déjà contenues dans le schedule. Le Schedule est
une entité composée d’un ensemble d’optimisations. Ces optimisations peuvent être de
différents types : SplitOptim, TuilageOptim ...etc.

Chaque type d’optimisation dispose de ses propres attributs qui dépendent de la struc-
ture de l’optimisation concernée. Par exemple, l’optimisation SplitOptim a comme attri-
buts : split_facteur qui représente le facteur de découpage en bandes et split_var la
variable sur laquelle on applique le découpage en bandes. Toutes les optimisations, indé-
pendemment de leur type, sont appliquées sur une Fonction, qui est caractérisée par un
nom, une liste de type Variable pour retenir les variables manipulées par la fonction, et
une liste de type Fonction pour contenir ses fonctions productrices. Toutes les fonctions
sont regroupées dans l’entité Programme, qui rassemble d’autres informations comme le
nom du programme, le chemin vers le code source du programme Halide, les constantes
du programme ...etc.

Chaque fois qu’un schedule est construit, il est testé en utilisant la classe Execu-
tionSchedule qui prend le soin de transformer l’objet schedule en un code source, de le
fusionner avec le code source du programme en entrée, et de les mettre dans un même
fichier source. La méthode execute_source lance la compilation du code source puis son
exécution pour récupérer le temps d’exécution du programme optimisé.

Quant à l’entité StockageManager, elle gère les activités de sauvegarde et d’inter-

102
Figure 67: Diagramme de classe pour la recherche exhaustive.

rogation de la base de données. Par exemple, avant de commencer à générer les sche-

103
dules, l’entité Programme est sauvegardée dans la base données par la méthode sauvegar-
der_programme et chaque schedule testé est sauvegardé ainsi que son temps d’exécution
par le biais de la méthode sauvegarder_schedule.

E.2 Choix de conception pour la méthode HalideAuto-


tuner
E.2.1 Diagramme de classe du système
Par rapport au premier diagramme de classe établi dans l’annexe E.1, nous faisons ap-
paraitre l’entité Restriction qui représente une classe critique, qui nous a permi d’établir
les éventuelles restrictions sur les espaces de recherche de chaque optimisation (voir figure
68). De la classe abstraite Restriction héritent plusieurs types de restrictions, où chacune
est dédiée à un type d’optimisation. Une restriction sert à définir un nouvel espace à
explorer pour une optimisation précise.

Par exemple, la classe SplitFactorRes qui est une restriction de type SplitRestriction
permet de déterminer la plage de valeurs que peut prendre le facteur de découpage d’une
optimisation de type SplitOptimisation et la classe FixParallelRes permet de maintenir
ou d’éliminer l’optimisation de parallélisation pour une fonction lors de la construction
des schedules.

La fonction explore_optim de l’entité GenerateurOptimisation, qui est réimplémentée


dans les éventuelles classes qui héritent de GenerateurOptimisation, explore toutes les
valeurs que peut prendre une optimisation de type particulier. La fonction explore_optim
reçoit en paramètre le tableau de restrictions défini sur l’espace de recherche. Si au-
cune restriction du tableau ne correspond à l’optimisation courante, alors la fonction
explore_optim est exécutée normalement. Sinon, cette méthode appelle la fonction res-
trict définie dans la restriction issue du tableau de restrictions en entrée, pour lui déléguer
le travail de l’exploration. Cette restriction définit à son tour une nouvelle stratégie de re-
cherche pour l’optimisation en cours. Dans l’annexe E nous justifions davantage les choix
de notre conception et la structure de ce diagramme de classe.

Extensibilité du système : Dans le but de respecter le principe de la classe ouverte


à l’extension et fermée à la modification, à des fins d’extension du système, nous avons
déclaré les types de restrictions SplitRestriction, ParallelRestriction comme classes abs-
traites qui déclarent la méthode abstraite restrict. De ces classes héritent d’autres classes
qui reflètent le type de restriction imposée et qui redéfinissent la méthode restrict sensée
de redéfinir une nouvelle stratégie d’exploration pour l’optimisation en question. Du coup,
si le développeur veut rajouter une nouvelle restriction il aura simplement à déclarer une
nouvelle classe qui hérite d’une des restrictions (suivant l’optimisation concernée par la
restriction) et il doit redéfinir la méthode restrict pour donner un nouveau parcours pour
les combinaisons de l’optimisation en question.

Au faite, la définition d’une méthode de construction de schedules, dans notre système


se résume à l’utilisation d’une recherche exhaustive sur laquelle on définit un tableau d’en-

104
Figure 68: Diagramme de classe pour la description des entités du système.

105
tités de type Restriction. De part cette conception, le développeur peut facilement étendre
le système en définissant de nouvelles restrictions et en leur implémentant la méthode res-
trict pour définir la nouvelle stratégie d’exploration d’espace. Pareil, le développeur peut
développer de nouvelles méthodes d’exploration dans notre système en faisant uniquement
changer les restrictions définies sur l’espace de recherche.

E.2.2 Diagramme d’activité résumant l’exploration des optimisa-


tions
Le diagramme d’activité exposé dans la figure 69 résume le fonctionnement d’une re-
cherche exhaustive sur laquelle est défini un ensemble de restrictions. Pour chaque type
d’optimisation, nous vérifions s’il y a une restriction définie sur son espace d’exploration.
Dans le cas où il y a une restriction, nous explorons l’espace défini par cette dernière,
sinon, la totalité de l’espace des valeurs que peut prendre l’optimisation est exploré. A
chaque itération l’optimisation en question prend une valeur précise.
Après le parcours de toutes les optimisations, nous avons un schedule d’optimisation com-
plet 1 que nous pouvons appliquer sur le programme et mesurer son temps d’exécution.
Nous réexécutons le même processus pour que les optimisations puissent prendre de nou-
velles valeurs et former de nouveaux schedules à tester. En dernier, on s’arrête une fois
que nous avons exploré la totalité des schedules possibles.

Figure 69: Diagramme d’activité illustrant le fonctionnement de la génération de


schedules guidée par les restrictions.

1. Il contient toutes les optimisations

106
Annexe F

Modèle analytique pour l’optimisation


d’interversion de boucle

Ce modèle vise à trouver une bonne interversion de boucle pour toute boucle parfai-
tement imbriquée. Il consiste à pousser vers l’intérieur d’une boucle imbriquée le niveau
de boucle qui comporte le plus de réutilisation de données, pour profiter de l’accès rapide
aux données tant qu’elles sont dans le cache (principe de la localité temporelle 1 ). De
plus, elle vise aussi à rendre l’accès séquentiel aux différentes données manipulées dans
la boucle (principe de la localité spatiale 2 ). Par conséquent, le modèle analytique qu’il
propose pour le choix automatique d’une bonne optimisation d’interversion de boucle se
base sur l’estimation du nombre de défauts de cache engendrés lors du positionnement
d’un niveau de boucle comme le niveau le plus interne à une boucle imbriquée. Ce modèle
prend en entrée la boucle à optimiser et la taille d’une ligne de cache pour pouvoir estimer
le nombre de défauts de cache pour les interversions de boucle candidates.

Par exemple, la boucle imbriquée de la figure 70 de profondeur 2 manipule deux ta-


bleaux A et B. Supposant que le langage de programmation utilisé est un langage qui
stocke les tableaux en mémoire ligne par ligne. Si on déroule le programme exposé dans la
figure 70, on constate que les deux tableaux A et B sont accessibles colonne par colonne 3 ,
et c’est ce qui détériore les performances car la mémoire cache n’est pas utilisée de façon
efficace. Suivant le principe de la localité des données, après référencement de A[1,1], le
prélecteur (le prefetcher) charge les données adjacentes à A[1,1] qui sont A[1,2], A[1,3]
... 4 , dans la mémoire cache, mais ces données ne vont pas être référencées dans un futur
proche, car le prochain accès au tableau A sera A[2,1]. La figure 71 illustre le problème
de défaut de cache dans l’implémentation de la boucle imbriquée de la figure 70 avec
une matrice A de taille 3*4. Donc l’imbrication de boucles dans cet ordre (voir figure 70)
n’est pas bénéfique pour le programme et engendre un nombre de défauts de cache égal
à 2*N*M (c’est à dire pour chaque accès à un élément du tableau A et à un élément du
tableau B).

1. une donnée qui a été déjà référencée est susceptible d’être référencée encore une fois.
2. une donnée est susceptible d’être référencée si une donnée adjacente à elle vient d’être référencée
3. A[1,1] ensuite A[2,1], ..., A[N,1], A[2,1], A[2,2], ..., A[N, 2]...
4. celles susceptibles qu’elles soient référencées dans le futur proche

107
Figure 70: Boucle imbriquée de profondeur 2 d’étendues égales à N*M, qui manipule les deux
tableaux A et B (accès colonne par colonne) [Allen and Kennedy, 2002].

Figure 71: Défauts de cache engendrés lors des premiers accès au tableau A 3*4.

Si on intervertit les deux niveaux de boucle de la figure 72, l’accès aux éléments du
tableau A et à ceux du tableau B devient séquentiel (suivant leur ordre de stockage en
mémoire centrale), et on aura un nombre de défauts de cache égal à 2*N*M/b, tel que b
représente la taille d’une ligne de cache. En effet, le prélecteur charge autant de données
adjacentes à la donnée référencée à t=t0 que la taille d’une ligne de cache, alors, un défaut
de cache ne se produit qu’une fois on accède à une donnée qui n’a pas été chargée à t=t0
dans la ligne de cache.

Figure 72: Boucle imbriquée de profondeur 2 d’étendues égales à N*M, qui manipule les deux
tableaux A et B (accès ligne par ligne).

Le pseudo-algorithme 12 résume le fonctionnement du modèle exposé dans [Allen and


Kennedy, 2002] pour le choix automatique de l’optimisation d’interversion de boucle.

108
Algorithme 12 Pseudo-algorithme du modèle analytique de [Allen and Kennedy, 2002]
pour le choix automatique de l’optimisation d’interversion de boucle pour une boucle
imbriquée
1: Soit Bi un niveau de boucle de la boucle B.
2: Soit C(Bi) une estimation du nombre de défauts de cache lors du positionnement de
la boucle Bi comme le niveau de boucle le plus interne à B.
3: Soit Tx un tableau parmi les tableaux T de B.
4: Soit b la taille d’une ligne de cache (le nombre de données que peut contenir une ligne
de cache) ;
5: Soit c(Bi, Tx) une estimation du nombre de défauts de cache produit lors de l’appel
de Tx au niveau de la boucle Bi
6: for Niveau de boucle Bi dans B do
7: C(Bi) = 0
8: for Tableau T x dans T do
9: if Tx ne dépend pas de l’indice de boucle de Bi, c-à-d la réutilisation est assurée
then
10: c(Bi, T x) = 1
11: if Tx dépend de l’indice de boucle de Bi, et Tx est accessible sur un espace
d’adresse non contigu, c-à-d qu’à chaque accès à Tx provoque un défaut de cache.
then
12: c(Bi, T x) = N i
13: if Tx dépend de l’indice de boucle de Bi, et Tx est accessible sur un espace
d’adresse contigu, c-à-d après chaque b données de Tx il y a un défaut de cache, sinon
on est sur une réutilisation de données. then
14: c(Bi, T x) = N i/b
15: for Niveau de boucle Bj dans B do
16: if Bj 6= Bi then
17: if Tx ne varie pas avec l’indice de boucle de Bj then
18: c(Bi, T x) = c(Bi, T x) ∗ 1
19: if Tx varie avec l’indice de boucle de Bj then
20: c(Bi, T x) = c(Bi, T x) ∗ N j
21: C(Bi) = C(Bi) + c(Bi, T x)
22: B’ = Ordonner les boucles B dans l’ordre croissant de leur nombre de défauts de cache
estimé C(Bi).
23: Retourner B’

109
Annexe G

Réseaux de neurones et réseaux de


neurones convolutifs

G.1 Réseaux de neurones


Un réseau de neurones est schématisé par un graphe acyclique (voir figure 73) où
chaque nœud du graphe représente un neurone. Le réseau de neurones est représenté sous
forme de couches : une couche d’entrée, une couche de sortie et des couches intermédiaires,
où chaque couche est constituée d’un certain nombre de neurones. Chaque neurone de la
couche i est interconnecté avec tous les neurones de la couche i-1, et il est censé calculer
une valeur réelle, en combinant les valeurs qu’il reçoit des neurones de la couche i-1.

Figure 73: Schéma d’un réseau de neurones.

Le but d’un réseau de neurones est d’essayer d’approcher la valeur f(x) pour une valeur
x en entrée. Autrement dit, pour une valeur de x injectée au niveau des neurones d’entrée,
on s’apprête à avoir en sortie (au niveau du neurone de sortie) une valeur f* qui est proche
de f(x). Par exemple, si nous cherchons à détecter la présence d’un chien dans une image,
x = {l’ensemble des valeurs de pixels de l’image}, et f(x) = {0 si l’image ne renferme
pas de chien, 1 sinon}. Dans ce cas, chaque neurone d’entrée va contenir la valeur d’un
seul pixel de l’image, et après avoir terminé tous les calculs et transité à travers toutes
les couches intermédiaires, le neurone de la couche de sortie est censé renvoyer 0 ou 1. Le
réseau de neurones rassemble un certain nombre de paramètres dynamiques D, qui sont
ajustés au fur et à mesure du déroulage de l’algorithme : ils sont ajustés de telle façon à
réduire l’écart entre f(x) qui est la valeur effective et f*(x, D) qui est la valeur prédite en

110
fonction des paramètres dynamiques. Pour pouvoir comprendre comment cela peut-il se
faire, nous allons aborder : la structure du neurone et les différentes couches du réseau de
neurones.

G.1.1 Structure d’un neurone


Le neurone est la structure élémentaire d’un réseau de neurones, il est caractérisé
par un certain nombre de paramètres : les poids, le biais et la fonction d’activation. Un
neurone N calcule et renvoit une valeur S, telle que : S = φ (x1*w1j+ x2*w2j+ x3*w3j+
. . . + xn*wnj + θ) (voir figure 74). Autrement dit, le neurone N de la couche j doit :

– Calculer la combinaison linéaire de toutes les valeurs xi qu’il reçoit à partir de la


couche précédente, pondérées par les poids wij.

– Rajouter à cette combinaison une certaine valeur qui s’appelle le biais : θ.

– Enfin, appliquer la fonction d’activation φ à la somme précédente et renvoyer la


valeur S vers les neurones de la couche j+1.

Figure 74: Structure d’un neurone artificiel.

Les poids : Les poids représentent des nombres réels qui servent à pondérer la valeur
retournée par un neurone de la couche précédente : plus le poids est grand, plus la valeur
retournée par le neurone de la couche j-1 est significative lors du calcul de la valeur S.
Les valeurs des poids associés à un neurone, font partie des paramètres dynamiques du
réseau de neurones, et donc elles ne sont pas fixées au préalable, mais elles sont construites
et apprises au fur et à mesure de l’apprentissage : à chaque fois qu’une valeur de x est
introduite dans le réseau de neurones, les valeurs des poids subissent des changements,
pour que la valeur en sortie f* du réseau de neurones puisse atteindre ou approcher au
maximum la valeur effective f(x). Par ailleurs, les poids sont choisis de telle façon à réduire
la fonction perte, qui représente la distance entre la valeur de sortie effective f(x) et la
valeur de sortie estimée f*.

La fonction d’activation : Les problèmes du monde réel ne peuvent pas être tous
modélisés par une fonction linéaire, i.e. la sortie n’est pas toujours représentée par une

111
combinaison linéaire des valeurs en entrée. En effet, la fonction d’activation est appli-
quée sur la combinaison calculée au niveau de chaque neurone du réseau, pour ne pas
restreindre le réseau de neurones à l’apprentissage de modèles linéaires uniquement. Il
existe plusieurs fonctions d’activation standards qui sont utilisées en pratique : la fonc-
tion ReLU, la fonction Sigmoid, la fonction Tangente hyperbolique . . . etc. Par exemple,
la fonction ReLU(x) = max {0, x}, pour fonction d’activation linéaire rectifiée, est une
fonction très utilisée dans les réseaux de neurones, qui consiste à casser la linéarité, pas
de façon radicale, entre l’entrée et la sortie.

Le biais : Le biais ou le seuil est une valeur qui sert généralement à activer ou
désactiver un neurone : Dans le cas où nous voulons désactiver un neurone parce qu’il
revoit une valeur inférieure à un seuil θ, on rajoute à la combinaison linéaire calculée au
niveau de N, S = φ (x1*w1j+ x2*w2j+ x3*w3j+ . . . + xn*wnj + θ), la valeur θ. Ainsi,
lors de l’application de la fonction d’activation, ReLU par exemple, elle va renvoyer la
valeur 0 pour toute combinaison dont la valeur est inférieure au seuil θ, car ReLU(x) =
max{0, x}.

G.2 Réseau de neurones convolutif


Notre travail de PFE consiste à produire une bibliothèque développée en Halide, qui
rassemble un certain nombre de fonctions qui implémentent les différentes couches d’un
réseau de neurones convolutif. Les réseaux de neurones convolutifs, ou RN, constituent
un type particulier des réseaux de neurones, qui sont plus légers qu’un réseau de neurone
standard (les perceptrons multicouches) en termes d’espace, et plus rapides à entraîner.
C’est parce qu’ils disposent d’un ensemble de paramètres plus petit que celui des para-
mètres du réseau de neurones multicouches.

Comme son nom l’indique, un réseau de neurones convolutif emploie une opération
connue sous le nom de : convolution, cette technique consiste à détecter des patrons à
partir d’une donnée en entrée. Les réseaux de neurones convolutifs s’apprêtent bien pour
des données en entrée organisées en grille, à titre d’exemple, les images qui sont des don-
nées à base de grille à deux dimensions.

En effet, un réseau de neurones convolutif est une combinaison de couches, où chacune


est censée implémenter une fonctionnalité du réseau : couche de convolution, couche de
pooling et couche entièrement connectée.

La figure 75 illustre un exemple d’un réseau de neurone convolutif. Les neurones en en-
trée du réseau prennent comme valeur les pixels de l’image en entrée. La première couche
du réseau est celle de la convolution. C’est une couche caractérisée par un ensemble de k
filtres, où chacun extrait une caractéristique de l’image en entrée. Suite à cette couche de
convolution, on se retrouve avec k cartes de caractéristiques ; chaque carte i est le résultat
de l’application du filtre i sur l’image de la couche précédente.

La couche qui succède celle de la convolution est la couche de maxpool qui ne garde
que la valeur maximale parmis toutes les valeurs d’une proportion de taille n*m extraite
à partir de chaque carte de caractéristiques. Effectivement, la couche maxpool permet de

112
Figure 75: Exemple d’un réseau de neurones convolutifs.

réduire le nombre d’informations véhiculées par la couche précédente 1 . Maxpool est utile
pour la réduction de la dimension du réseau de neurone.

1. Surtout que la couche de convolution fait augmenter les cartes de caractéristiques et donc les données
à traiter par le réseau.

113
Bibliographie

[Adams, 2016] Adams, A. (2016). halide/halide. accessile à https ://gi-


thub.com /halide/Halide /blob/a3076e9c79be0a8e9ccffa f8eb28dea777bd838c/apps/ li-
near_algebra/src/blas_l3_generators.cpp.

[Agakov et al., 2006] Agakov, F., Bonilla, E., Cavazos, J., Franke, B., Fursin, G., OBoyle,
M., Thomson, J., Toussaint, M., and Williams, C. K. (2006). Using machine learning
to focus iterative optimization. In Proceedings of the International Symposium on Code
Generation and Optimization, pages 295–305. IEEE Computer Society.

[Allen and Kennedy, 2002] Allen, R. and Kennedy, K. (2002). Optimizing compilers for
modern architectures : a dependence-based approach, volume 1. Morgan Kaufmann San
Francisco.

[Ansel et al., 2014] Ansel, J., Kamil, S., Veeramachaneni, K., Ragan-Kelley, J., Bosboom,
J., O’Reilly, U.-M., and Amarasinghe, S. (2014). Opentuner : An extensible framework
for program autotuning. In Parallel Architecture and Compilation Techniques (PACT),
2014 23rd International Conference on, pages 303–315. IEEE.

[Ansel, 2014] Ansel, J. J. A. (2014). Autotuning programs with algorithmic choice. PhD
thesis, Massachusetts Institute of Technology.

[Ashouri et al., 2016] Ashouri, A. H., Mariani, G., Palermo, G., Park, E., Cavazos, J., and
Silvano, C. (2016). Cobayn : Compiler autotuning framework using bayesian networks.
ACM Transactions on Architecture and Code Optimization (TACO), 13(2) :21.

[Bacon et al., 1994] Bacon, D. F., Graham, S. L., and Sharp, O. J. (1994). Compiler
transformations for high-performance computing. ACM Computing Surveys (CSUR),
26(4) :345–420.

[Bergstra et al., 2012] Bergstra, J., Pinto, N., and Cox, D. (2012). Machine learning for
predictive auto-tuning with boosted regression trees. In Innovative Parallel Computing
(InPar), 2012, pages 1–9. IEEE.

[Davidson and Jinturkar, 1996] Davidson, J. W. and Jinturkar, S. (1996). Aggressive


loop unrolling in a retargetable, optimizing compiler. In International Conference on
Compiler Construction, pages 59–73. Springer.

[De Mesmay et al., 2010] De Mesmay, F., Voronenko, Y., and Puschel, M. (2010). Offline
library adaptation using automatically generated heuristics. In Parallel & Distributed
Processing (IPDPS), 2010 IEEE International Symposium on, pages 1–10. IEEE.

114
[Epshteyn et al., 2005] Epshteyn, A., Garzaran, M. J., DeJong, G., Padua, D., Ren, G.,
Li, X., Yotov, K., and Pingali, K. (2005). Analytic models and empirical search : A
hybrid approach to code optimization. In International Workshop on Languages and
Compilers for Parallel Computing, pages 259–273. Springer.

[Hall, 2011] Hall, M. (2011). Autotuning and its origins. accessible au


https ://goo.gl/asJjcP.

[Hoste and Eeckhout, 2007] Hoste, K. and Eeckhout, L. (2007). Microarchitecture-


independent workload characterization. IEEE Micro, 27(3).

[Karpathy, 2017] Karpathy, A. (2017). Convolutional neural networks (cnns / convnets).


accessible au http ://cs231n.github.io/convolutional-networks/.

[MilePost, 2018] MilePost (2018). Milepostgcc. accessible au http ://ctuning.org/wiki/


index.php/CTools :MilepostGCC : StaticFeatures :MILEPOST_V2.1.

[Mullapudi et al., 2016] Mullapudi, R. T., Adams, A., Sharlet, D., Ragan-Kelley, J., and
Fatahalian, K. (2016). Automatically scheduling halide image processing pipelines.
ACM Transactions on Graphics (TOG), 35(4) :83.

[Park et al., 2011] Park, E., Kulkarni, S., and Cavazos, J. (2011). An evaluation of dif-
ferent modeling techniques for iterative compilation. In Proceedings of the 14th inter-
national conference on Compilers, architectures and synthesis for embedded systems,
pages 65–74. ACM.

[Ragan-Kelley, 2014] Ragan-Kelley, J. (2014). Decoupling Algorithms from the Organiza-


tion of Computation for High Performance Image Processing. PhD thesis, Cambridge,
MA.

[Ragan-Kelley et al., 2012] Ragan-Kelley, J., Adams, A., Paris, S., Levoy, M., Amara-
singhe, S., and Durand, F. (2012). Decoupling algorithms from schedules for easy
optimization of image processing pipelines.

[Ragan-Kelley et al., 2013] Ragan-Kelley, J., Barnes, C., Adams, A., Paris, S., Durand,
F., and Amarasinghe, S. (2013). Halide : a language and compiler for optimizing pa-
rallelism, locality, and recomputation in image processing pipelines. ACM SIGPLAN
Notices, 48(6) :519–530.

[Rahman et al., 2010] Rahman, M., Pouchet, L.-N., and Sadayappan, P. (2010). Neural
network assisted tile size selection. In International Workshop on Automatic Perfor-
mance Tuning (IWAPT’2010). Berkeley, CA : Springer Verlag.

[Stephenson et al., 2003] Stephenson, M., Amarasinghe, S., Martin, M., and O’Reilly, U.-
M. (2003). Meta optimization : improving compiler heuristics with machine learning.
In ACM SIGPLAN Notices, volume 38, pages 77–90. ACM.

[Suriana, 2017] Suriana, P. (2017). Benchmark de convolution en halide. ac-


cessible au https ://github.com /halide /Halide/ blob/a3076e9c79be0a8e9cc
ffaf8eb28dea777bd838c /apps/ conv_layer/ conv_layer_generator.cpp.

115
[Tiwari et al., 2009] Tiwari, A., Chen, C., Chame, J., Hall, M., and Hollingsworth, J. K.
(2009). A scalable auto-tuning framework for compiler optimization. In Parallel &
Distributed Processing, 2009. IPDPS 2009. IEEE International Symposium on, pages
1–12. IEEE.

[Whaley and Dongarra, 1998] Whaley, R. C. and Dongarra, J. J. (1998). Automatically


tuned linear algebra software. In Proceedings of the 1998 ACM/IEEE conference on
Supercomputing, pages 1–27. IEEE Computer Society.

[Yotov et al., 2003] Yotov, K., Li, X., Ren, G., Cibulskis, M., DeJong, G., Garzaran, M.,
Padua, D., Pingali, K., Stodghill, P., and Wu, P. (2003). A comparison of empirical
and model-driven optimization. ACM SIGPLAN Notices, 38(5) :63–76.

116