Rapport de Stage Optimisation du Cell
Transcription
Rapport de Stage Optimisation du Cell
Rapport de Stage Optimisation du Cell Bertrand Putigny 7 septembre 2009 Sommaire 1 Introduction 1.1 Sujet . . . . . . . . . . . . . . . . . . . 1.2 Présentation du PRiSM et de l’équipe 1.3 Contexte . . . . . . . . . . . . . . . . . 1.4 Présentation de l’architecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 2 2 3 2 Élaboration du modèle de performances 2.1 Méthode d’évaluation des performances . . 2.2 Optimisation de plusieurs codes de blas . . 2.2.1 Différentes méthodes d’optimisation 2.2.2 Optimisation automatique . . . . . . 2.2.3 Optimisation au niveau source . . . 2.3 Benchmark des communications . . . . . . . 2.3.1 Communication par mailbox . . . . 2.3.2 Communication par DMA . . . . . . 2.3.3 Modèle de communication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 5 5 7 10 23 23 24 26 3 Modèle de performances 3.1 Énoncé . . . . . . . . . . . . . . . . . 3.2 Validation du modèle . . . . . . . . . 3.2.1 Produit matriciel multi-SPE . 3.2.2 Décomposition LU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 27 27 27 30 . . . . 33 33 33 33 34 4 Générateur de code 4.1 Description . . . . . . . 4.1.1 Fonctionnement . 4.1.2 Utilisation . . . . 4.2 Résultats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 1 1 1.1 Introduction Sujet Sujet : Optimisation iterative de code d’algebre lineaire sur machine heterogene de type Cell. La mise au point de bibliothèques hautes performances parallèles pour l’algebre lineaire est un sujet faisant l’objet de nombreux travaux de recherche (ATLAS ou plus recemment, Spiral par exemple) et d’effort de développements de la part de constructeurs (Intel ou IBM). Les architectures heterogenes comme le Cell d’IBM representent une difficulté supplémentaire pour la mise au point de ces bibliotheques. L’objectif de ce stage est de réaliser un générateur automatique de bibliothèque d’algebre lineaire (les BLAS et si possible d’autres expressions vectorielles/matricielles) sur architecture Cell, en utilisant les 8 co-processeurs vectoriels ainsi que le processeur central. Ce travail s’appuiera sur des microbenchmarks de l’architecture pour proposer un modele de performance, et notamment on réutilisera les microbenchmarks réalisés pendant l’etude de cas d’ARA. Le travail de ce stage sera en 3 etapes : – microbenchmarks pour la communication entre Synergistic Processing Elements (SPE) et Power Processor Element (PPE) sur le Cell, et proposition d’un modele de performances – proposition des transformations de code intéressantes à explorer sur Cell, et réalisation d’un générateur de code, a partir d’X language. Le but de cette etape est de pouvoir génerer de nombreuses versions de codes simples (codelets) servant à la construction de codes plus généraux. – proposition de modèle de performances a partir des performances mesurées des codelets et du modele de performance des communications. Benchmarks sur de vrais codes d’algèbre lineaire pour valider ces resultats. Environnement : programmation C, utilisation du langage de metacompilation X language et programmation prolog. 1.2 Présentation du PRiSM et de l’équipe Le PRiSM est le laboratoire de recherche en informatique de l’Université de Versailles Saint-Quentin-en-Yvelines. Les thèmes principalement étudier sont le Parallèlisme, les Réseaux, les Systèmes et de la Modélisation. L’équipe ou le stage c’est déroulé est l’équipe ARPA (Architecture et Parallèlisme). Les activités de recherche de cette équipe se situent à l’interface de trois domaines : architecture, analyse statique et optimisation de code. 1.3 Contexte L’équipe ARPA travaille sur un langage de haut niveau permettant d’exprimer des codes d’algèbre linéaire. Ce langage permet d’exprimer à la fois des codes d’algèbre linéaire simple, comme daxpy ou dgemm, mais également des noyaux plus complexes comme la décomposition LU ou bien des ”solvers”. Ce langage permet de générer des noyaux de calculs haute performance optimisés pour l’architecture cible ainsi qu’un graphe de dépendance de tâches. Le graphe de dépendance et les noyaux de calcul ainsi générés peuvent être utilisés par un ”runtime system” qui s’occupe de l’ordonnancement des tâches sur une machine hétérogène ou parallèle, comme StarPU. L’object du stage est de proposer un modèle de performance sur Cell, une des architecture ciblée par le langage. Ce modèle de performance doit permettre par la suite de générer des noyaux de calcul et des codelets performants sur Cell. 2 En outre le stage doit permettre de fournir des résultats montrant les performances qu’il est possible d’obtenir sur Cell en utilisant ce langage, ainsi que de réaliser un générateur de code pour Cell. État de l’art : Actuellement il n’existe pas de compilateur performant sur Cell. Certaines équipes comme celle de Jack Dongarra[?], ou encore une équipe de l’université de Dresden[?] en Allemagne arrivent à générer des codes performants sur Cell en écrivant le code directement en assembleur. De cette manière elles arrivent à obtenir de bonnes performances, par exemple plus de 99% des performances crêtes sur un code de multiplication matricelle. Mais il n’existe pas d’outils permettant de générer automatiquement des codes performants sur cette architecture. Même les blas d’IBM pour ce processeur ont des performances ”moyennes”. 1.4 Présentation de l’architecture La majorité des processeurs multi-cœur classiques sont composés de plusieurs cœurs identiques. Ce n’est pas le cas du processeur Cell car il est composé de deux type de cœurs différents. Il est composé d’un cœur principal, le PPE (PowerPC Processing Element) ainsi que de 8 SPE (Synergystic Processing Elements). Sur la PS3, plateforme sur laquelle la majorité des mesures ont été réalisées, seuls 7 SPE sont disponibles. Chaque SPU dispose d’un local storage de 256 Ko et est doté d’un jeu d’instructions de type SIMD. Il y a deux pipelines sur chaque SPU, l’un dédié aux calculs flottants et l’autre au calcul entier et aux accès mémoire (local store). Le processeur Cell est développé conjointement par IBM, Sony et Toshiba. Les performances crêtes de ce processeur sont de 205 Gflops (à 3,2 GHz) pour des calculs en simple précision, soit 25,6 Gflops par Synergystic Processing Unit (SPU). 3 2 Élaboration du modèle de performances 2.1 Méthode d’évaluation des performances Afin d’évaluer les performances, il est nécessaire de pouvoir mesurer précisément le temps d’exécution d’une portion de code. Pour ce faire deux fonctions sont disponibles sur les SPU : – La fonction ”spu write decrementer()” permet d’initialiser un compteur hardware du processeur qui est décrémenté régulièrement avec une fréquence appelée ”timebase”. La valeur de ce ”timebase” peut être récupérée dans le fichier ”/proc/cpuinfo”. – Une fois le compteur hardware initialisé, on peut récupérer sa valeur grâce à la fonction ”spu read decrementer” Ainsi pour calculer le temps d’exécution d’une portion de code, il suffit de calculer le nombre de fois que le compteur a été décrémenté durant l’exécution et de diviser ce nombre par la fréquence de mise à jour du compteur matériel : s p u _ w r i t e _ d e c r e m e n t e r (0 xFFFFFFFF ); t = s p u _ r e a d _ d e c r e m e n t e r (); /* * Code dont on veut mesurer le temps * d ’ execution */ t -= s p u _ r e a d _ d e c r e m e n t e r (); sec = t / timebase ; Un autre outil, nommé ”spu timing” est disponible pour cette plateforme. Il permet de simplifier l’analyse du code assembleur des SPE. En lui passant le paramètre ”-runningcount” il permet d’annoter le code assembleur avec des informations relatives à l’utilisation de chacun des deux pipelines. On peut y voir les instructions qui peuvent être lancées simultanément ainsi que les stalls. Voici un exemple de code assembleur annoté avec cet outil : 000020 000021 000022 000023 000024 000025 000026 000027 000028 000028 000029 000030 000032 000034 000035 0d 1d 1 1 1 1 1 1 0D 1D 0 0 0 0 0D 000035 1D 01 -123456 234567 345678 456789 567890 678901 789012 890123 890123 90 012345 -234567 -456789 5 5678 . L4 : ai lqx lqx lqx lqx lqx lqx lqx fma lqx ai fma fma fma nop . L9 : brnz $11 , $11 , -1 $28 , $10 , $13 $27 , $10 , $12 $26 , $10 , $20 $25 , $10 , $23 $7 , $10 , $19 $3 , $10 , $22 $24 , $10 , $18 $17 , $28 , $27 , $17 $4 , $10 , $21 $10 , $10 ,16 $15 , $26 , $25 , $15 $14 , $7 , $3 , $14 $16 , $24 , $4 , $16 127 $11 ,. L4 La première colonne représente le numéro du cycle où l’instruction est lancée (il ne faut pas en tenir compte car il est erroné dès lors qu’on a une boucle ou même un branchement dans le code). Le premier chiffre représente sur quel pipeline l’instruction est lancée. La lettre indique si l’instruction a put être lancée en simultané avec une autre. Un D majuscule montre que l’instruction a pu être lancée en même temps qu’une autre comme pour les instructions 9 et 10 de l’exemple. Si aucune lettre n’apparaı̂t c’est que l’instruction n’a pas pu être lancée en même temps qu’une autre. Un d minuscule, comme sur les deux premières instructions de l’exemple, montre qu’une dépendance due au prologue de la boucle a empêché les deux 4 instructions d’être lancées simultanément à la première itération de la boucle, mais que dans les itérations suivantes, elles pourront être lancées au même cycle. La troisième colonne représente l’utilisation du pipeline. Un signe moins (-) représente un stall, puis les chiffres représentent le pipeline, notamment la durée de chaque instruction. L’outil ”spu timing” permet d’évaluer les performances d’un code dans le sens où moins il y a de stalls et plus il y a d’instructions simultanées, plus le code est performant. Cet outil est surtout très utile pour optimiser les codes en repérant les points bloquants du programme. 2.2 Optimisation de plusieurs codes de blas L’objectif de cette section est d’optimiser plusieurs code d’algèbre linéaire afin d’avoir l’expertise suffisante pour élaborer un modèle de performance sur Cell. Dans cette section nous nous intéressons uniquement à l’optimisation de code sur un seul SPE, sans aucune communication. 2.2.1 Différentes méthodes d’optimisation Les SPE sont dotés d’un jeu d’instruction SIMD. Par conséquent l’optimisation la plus évidente qu’il est possible de faire sur les codes est la vectorisation afin d’utiliser les instructions vectorielles disponibles sur une telle architecture. Ce type d’instructions permet de faire une même opération sur plusieurs données simultanément avec une seule instruction. Le déroulage de boucle consiste à répliquer le corps d’une boucle et donc à faire une incrémentation avec stride de l’indice de boucle. Cette méthode a un double intérêt du point de vue de l’optimisation : d’une part cela réduit le nombre de branchement conditionnels puisque, dans le cadre d’un déroulage d’un facteur 2, cela divise par deux le nombre d’itérations à faire sur cette boucle et donc le nombre de branchement pris. D’autre part cela augmente le nombre d’instructions dans le corps de la boucle, laissant ainsi plus de place pour réordonner ces instructions afin de mieux utiliser le parallélisme d’instruction. Enfin le pipeline logiciel est une méthode d’optimisation de code qui consiste à utiliser le parallélisme d’instructions (i.e. l’utilisation simultanée des deux pipelines des SPE). Le principe est d’entrelacer les instructions de plusieurs itérations d’une boucle. Ainsi, si les instructions des deux itérations sont indépendantes, le processeur peut lancer deux instructions (une de chaque itération) simultanément par cycle d’horloge. Remarque En pratique, il peut quand même y avoir des dépendances entre les différentes itérations d’une boucle dans deux cas : si la boucle est une réduction, ou bien au niveau de l’utilisation des registres. 5 Illustration du pipeline logiciel sur cell : On considère le code C suivant : for ( i =0 ; i < N ; i ++) { a [ i ] = b [ i ] + c [ i ]; } Le code assembleur généré par le compilateur est le suivant : 000003 000004 000005 000011 000012 000017 000018 0d 1d 1 0 0d 1d 0D 34 -456789 567890 - - - - -123456 2 - - - - -789012 89 000018 1 D 8901 . L4 : ai lqx lqx fa nop stqx ai . L8 : brnz $6 , $6 , -1 $3 , $7 , $5 $8 , $7 , $4 $2 , $8 , $3 127 $2 , $7 , $5 $7 , $7 ,16 $6 ,. L4 Il n’y a aucun parallélisme d’instruction entre l’addition (fa) et les loads (lqx) ou bien les stores (stqx). Pour faire apparaitre un parallélisme d’instruction il faut décomposer le code C en instructions élémentaires comme suit : for ( i =0; i < n ; i ++) { a0 = b0 + c0 ; b0 = b [ i ]; c0 = c [ i ]; a [i -1] = a0 ; } // addition de l ’ iteration // load de l ’ iteration i // load de l ’ iteration i // store de l ’ iteration i -1 Le code assembleur généré pas le compilateur est le suivant : 000003 000004 000005 000005 000006 000009 000010 0d 1d 0D 1D 0d 1d 0D 345678 -456789 56 567890 67 - - -901234 01 000010 1 D 0123 . L4 : fa lqx ai lqx ai stqd ai . L8 : brnz $2 , $9 , $8 $9 , $7 , $4 $6 , $6 , -1 $8 , $7 , $5 $7 , $7 ,16 $2 , -16( $3 ) $3 , $3 ,16 $6 ,. L4 Cette fois il y a bien parallélisme d’instruction entre la somme et le premier load. Le compilateur spu-gcc ne réordonne pas de lui-même les instructions, il traduit simplement le code C qu’on lui fournit. Cela ne permet pas d’optimiser automatiquement les codes, mais cela permet d’ordonnancer les instructions assembleur directement à partir du code C. On peut schématiser le pipeline logiciel qu’on vient de réaliser ainsi : Cycle Cycle Cycle Cycle Cycle Cycle Cycle Cycle Cycle 1 2 3 4 5 6 7 8 9 Itération 1 ADD Itération 2 LOAD LOAD Itération 3 ADD LOAD LOAD Itération 4 STORE STORE ADD STORE 6 LOAD LOAD 2.2.2 Optimisation automatique L’objectif de cette partie est d’essayer d’optimiser automatiquement les codes grace aux options d’optimisation qu’offre le compilateur. Les options de compilations testées sont les suivantes : – -funroll-loops – -ftree-vectorize – -ftree-vect-loop-version – -frename-registers Ces options ont été choisies car elles doivent permettre de tester l’effet des différentes méthodes d’optimisation expliquées dans la section précédente. L’option ”-funroll-loops” a pour objectif de forcer le compilateur à dérouler les boucles. L’option ”-ftree-vectorize” doit vectoriser le code pour utiliser les instructions SIMD du processeur. L’option ”-ftreevectorize” est sensée vectoriser les boucles. Enfin l’option ”–frename-registers” doit demander au compilateur de renommer les registres pour enlever les dépendances dues aux registres du processeur. Les graphiques suivants représentent les performances de deux codes (saxpy et dotproduct) compilés avec différentes options de compilations et pour différentes tailles de vecteurs. Chacune des options de compilation est testée en combinaison avec l’option de compilation ”-O3 ” et lui est comparée. Voici les performances obtenues pour les codes ”saxpy” et ”dotproduct” naı̈fs : 0.3 0.29 Performances (Gflops) 0.28 0.27 0.26 0.25 30000 29000 28000 27000 26000 25000 24000 23000 22000 21000 20000 19000 18000 17000 16000 15000 14000 13000 12000 11000 9000 10000 8000 7000 6000 5000 4000 3000 2000 0.24 Tailles des vecteurs "−O3" "−O3 −ftree−vect−loop−version" "−O3 −funroll−loops" "−O3 −frename−registers" "−O3 −ftree−vectorize" Fig. 1: Performances du code saxpy compilé avec différentes options d’optimisation 7 1.6 1.4 Performances (Gflops) 1.2 1 0.8 0.6 0.4 0.2 30000 29000 28000 27000 26000 25000 24000 23000 22000 21000 20000 19000 18000 17000 16000 15000 14000 13000 12000 11000 9000 10000 8000 7000 6000 5000 4000 3000 2000 0 Tailles des vecteurs "−O3" "−O3 −ftree−vect−loop−version" "−O3 −funroll−loops" "−O3 −frename−registers" "−O3 −ftree−vectorize" Fig. 2: Performances du code dotproduct compilé avec différentes options d’optimisation Compilateur pour les SPE : spu-gcc : gcc version 4.1.1 Compilateur pour le PPE : ppu32-gcc : gcc version 4.1.1 Système d’exploitation : Yellow dog linux : 2.6.23-9.ydl6.1 Plate-forme : Playstation 3 On peut voir que la seule option qui permet d’améliorer sensiblement les performances est l’option –funroll-loops qui permet de dérouler les boucles. Afin d’avoir de meilleurs performances, les codes de saxpy et dotproduct ont été vectorisés et compilés avec les mêmes options de compilation. Voici les performances obtenues : 8 2.5 Performances (Gflops) 2 1.5 1 0.5 26000 27000 28000 29000 30000 27000 28000 29000 30000 25000 26000 24000 23000 22000 21000 20000 19000 18000 17000 16000 15000 14000 13000 12000 11000 9000 10000 8000 7000 6000 5000 4000 3000 2000 0 Tailles des vecteurs "−O3" "−O3 −ftree−vect−loop−version" "−O3 −funroll−loops" "−O3 −frename−registers" "−O3 −ftree−vectorize" (a) saxpy vectorisé 7 6 Performances (Gflops) 5 4 3 2 1 25000 24000 23000 22000 21000 20000 19000 18000 17000 16000 15000 14000 13000 12000 11000 10000 9000 8000 7000 6000 5000 4000 3000 2000 0 Tailles des vecteurs "−O3" "−O3 −ftree−vect−loop−version" "−O3 −funroll−loops" "−O3 −frename−registers" "−O3 −ftree−vectorize" (b) dotproduct vectorisé Fig. 3: Performances des codes saxpy et dotproduct vectorisés compilés avec différentes options d’optimisation Compilateur pour les SPE : spu-gcc : gcc version 4.1.1 Compilateur pour le PPE : ppu32-gcc : gcc version 4.1.1 Système d’exploitation : Yellow dog linux : 2.6.23-9.ydl6.1 Plate-forme : Playstation 3 On constate que les options de compilation passées au compilateur n’ont pas plus d’effet sur les codes vectorisés que sur les codes naı̈fs, mais que les performances des codes vectorisés sont meilleures que celles des codes non vectorisés. 9 Les faibles performances obtenues avec l’optimisation automatique du code par le compilateur nous incitent à essayer d’optimiser ces noyaux de calcul en modifiant directement les codes de ceux-ci. 2.2.3 Optimisation au niveau source Blas 1 Le premier code à optimiser est le code saxpy. Le code de départ est le code vectorisé utilisé dans la partie précédente. Code de départ : (spu add et spu mul sont des intrinsics permettant l’addition et la multiplication des composantes des vecteurs passés en paramètre). for ( i =0; i < n ; i ++) { Y [ i ] = spu_add ( spu_mul (a , X [ i ]) , Y [ i ]); } Le code assembleur annoté correspondant à cette boucle est le suivant : 000005 000006 000007 000013 000014 000019 000020 0 d 56 1 d -678901 1 789012 0 - - - - -345678 0d 4 1d - - - - -901234 0D 01 000020 1 D 0123 . L4 : ai lqx lqx fma nop stqx ai . L8 : brnz $6 , $6 , -1 $10 , $7 , $4 $3 , $7 , $5 $9 , $8 , $10 , $3 127 $9 , $7 , $5 $7 , $7 ,16 $6 ,. L4 On peut voir une dépendance entre l’instruction fma et les deux load (lqx) qui la précède, ainsi qu’une deuxième dépendance entre cette même fma et le store (stqx) qui suit. Ces dépendances introduisent des stalls qui réduisent considérablement les performances du programme. En effet, à chaque passage dans la boucle on perd 5 cycles en attendant la fin du load et 5 autres cycles en attendant le résultat de la fma. Afin d’éviter ce problème de dépendance on fait un déroulage de boucle pour pouvoir entrelacer les load, les fma et les store de plusieurs itérations sans avoir de dépendances. Voici le code C une fois déroulé d’un facteur 4 : for ( i =0; i < n ; i +=4) { Y [ i ] = spu_add ( spu_mul ( av , X [ i ]) , Y [ i ]); Y [ i +1] = spu_add ( spu_mul ( av , X [ i +1]) , Y [ i +1]); Y [ i +2] = spu_add ( spu_mul ( av , X [ i +2]) , Y [ i +2]); Y [ i +3] = spu_add ( spu_mul ( av , X [ i +3]) , Y [ i +3]); } 10 Et voici le code assembleur correspondant : . L4 : - - -90 ai 901234 lqd 0 nop 012345 lqd 12 clgt 123456 lqd 234567 lqd 345678 lqd - -678901 fma - - - - - -234567 stqd 345678 lqd - - - - -901234 fma - - - - -567890 stqd 678901 lqd - - - - -234567 fma 0123 - - - - - -89 stqd 01234 9 lqd 01 ai - - - -567890 fma - - - - - -123456 stqd 23 ai . L8 : 62 1 D 2345 brnz 09 09 10 10 11 11 12 13 16 22 23 29 35 36 42 48 49 50 55 61 62 0D 1D 0D 1D 0D 1D 1 1 0d 1d 1 0 1 1 0d 1d 1 0 0d 1d 0D $11 , $11 ,4 $19 ,0( $9 ) 127 $20 ,0( $8 ) $7 , $12 , $11 $17 ,16( $8 ) $5 ,32( $8 ) $6 ,48( $8 ) $18 , $10 , $19 , $20 $18 ,0( $8 ) $16 ,16( $9 ) $15 , $10 , $16 , $17 $15 ,16( $8 ) $14 ,32( $9 ) $13 , $10 , $14 , $5 $13 ,32( $8 ) $4 ,48( $9 ) $9 , $9 ,64 $3 , $10 , $4 , $6 $3 ,48( $8 ) $8 , $8 ,64 $7 ,. L4 On peut nettement voir le déroulage, mais on se rend compte que cela ne change rien aux performances du code puisque l’on conserve des stalls sur chaque fma et sur chaque store. Cependant on peut constater que les instructions des différentes itérations ne sont pas entrelacées, car le compilateur ne réordonne pas les instructions. Nous devons donc entrelacer à la main les instructions pour pouvoir augmenter les performances de ce code. Comme les loads et les stores ne s’exécutent pas sur le même pipeline du processeur que les fmas, on peut faire un load (ou un store) en parallèle avec une fma. Ainsi, pour entrelacer efficacement les instructions au sein de la boucle, il faut faire les load et les fma en même temps puis faire les stores. for ( i =0; i < n /4; i ++) { LOAD X1 [ i ] LOAD Y1 [ i ] FMA LOAD X2 [ i ] LOAD Y2 [ i ] FMA LOAD X3 [ i ] LOAD Y3 [ i ] FMA LOAD X4 [ i ] LOAD Y4 [ i ] FMA STORE STORE STORE STORE } 11 Le code assembleur généré est le suivant : 000020 000021 000022 000023 000024 000025 000026 000027 000028 000028 000030 000032 000034 000034 000036 000038 000039 000040 000041 0d 1d 1 1 1 1 1 1 0D 1D 0 0 0D 1D 1 1 0d 1d 0D 01 -123456 234567 345678 456789 567890 678901 789012 890123 890123 -012345 -234567 -456789 456789 -678901 -890123 9 -012345 12 000041 1 D 1234 . L4 : ai lqx lqx lqx lqx lqx lqx lqx fma lqx fma fma fma stqx stqx stqx nop stqx ai . L8 : brnz $12 , $12 , -1 $29 , $10 , $13 $28 , $10 , $17 $27 , $10 , $16 $26 , $10 , $20 $25 , $10 , $15 $24 , $10 , $19 $23 , $10 , $14 $5 , $11 , $28 , $29 $22 , $10 , $18 $21 , $11 , $26 , $27 $3 , $11 , $24 , $25 $7 , $11 , $22 , $23 $5 , $10 , $13 $21 , $10 , $16 $3 , $10 , $15 127 $7 , $10 , $14 $10 , $10 ,16 $12 ,. L4 Ici le compilateur a quand même déplacé des instructions. Il a remonté des loads car les fmas ont besoin des résultats des loads pour s’exécuter. Faire deux loads puis une fma comme dans le code C aurait entrainé des stalls sur les fma le temps que les résultats des loads soient disponibles. Ce code peut encore être optimisé car on voit qu’il subsiste un cycle de stall sur les 3 dernières fma et sur les 3 derniers stores. De plus seul une fma et un load sont lancés en parallèle. Pour éviter les stalls sur les fma, un déroulage d’un facteur 8 est possible. Ainsi il y a plus d’instructions dans la boucle pour pouvoir espacer les laods et les fma de 6 cycles : for ( i =0; i < n /8; i ++) { LOAD x 6 { LOAD LOAD FMA } x 5 { STORE FMA } x 3 STORE x 5 } Dans ce cas les fma sont bien toutes lancées en parallèle avec une autre instruction, mais les 5 derniers stores sont des stalls. Pour éviter les stalls sur les stores, il faut mettre en place un pipeline logiciel : ce qui signifie commencer par faire 4 séries de ”fma ; load” puis terminer les load et enfin faire les stores. Pour que les résultats soient corrects, il faut conserver un certain ordre dans les instructions : pour l’itération i, les loads doivent être faits avant la fma et la fma doit être faite avant les stores. Il y a donc une dépendance entre les itérations successives de la boucle. Par conséquent, on ne peut plus faire un déroulage comme précédemment. Cette fois, on est obligé de diviser les tableaux en 4 zones égales et de faire à chaque itération de boucle, une itération pour chacune des zones des tableaux ainsi divisés, comme le montre le schéma suivant : Fig. 4: Vecteur divisé en 4 zones égales, chacune accédée en parallèle 12 Le pseudo code C obtenu avec le pipeline logiciel est le suivant : for ( i =0; i < n /4; i ++) { { FMA LOAD } x 4 LOAD x 4 STORE x 4 } // fma de l ’ iteration i -1 // laod de l ’ iteration i // loads le l ’ iteration i // stores de l ’ iteration i -1 Voici le code assembleur obtenu : 0D 1D 0d 1d 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 1 1 . L5 : nop 127 hbrp #3 fma $24 , $8 , $17 , $13 lqx $17 , $7 , $20 fma $23 , $8 , $11 , $10 lqx $13 , $7 , $5 fma $22 , $8 , $9 , $6 lqx $11 , $7 , $27 fma $21 , $8 , $18 , $16 lqx $10 , $7 , $12 a $30 , $5 , $7 lqx $9 , $7 , $26 a $29 , $12 , $7 lqx $6 , $7 , $15 a $28 , $15 , $7 lqx $18 , $7 , $25 a $3 , $14 , $7 lqx $16 , $7 , $14 ai $4 , $4 , -1 stqd $24 , -16( $30 ) ai $19 , $19 ,1 stqd $23 , -16( $29 ) ai $7 , $7 ,16 stqd $22 , -16( $28 ) stqd $21 , -16( $3 ) . L12 : brnz $4 ,. L5 4 4 567890 -678901 789012 789012 890123 890123 901234 901234 01 012345 12 123456 23 234567 34 345678 45 456789 56 567890 67 678901 789012 8901 Les performances de cette boucle sont bonnes, puisque d’une part on ne voit plus de stall et que d’autre part comme toutes les fma sont lancées ne même temps qu’un laod, alors le maximum d’instructions qu’il est possible de lancer simultanément l’est. Les performances de ce code ainsi optimisé sont les suivantes : 13 8 7 Performances (Gflops) 6 5 4 3 2 1 29696 28672 27648 26624 25600 24576 23552 22528 21504 20480 19456 18432 17408 16384 15360 14336 13312 12288 11264 10240 9216 8192 7168 6144 5120 4096 3072 2048 1024 0 Tailles des vecteurs Perf saxpy Perf saxpy_spu Fig. 5: Performances du code saxpy optimisé comparées aux performances du code d’IBM Compilateur pour les SPE : spu-gcc : gcc version 4.1.1 Compilateur pour le PPE : ppu32-gcc : gcc version 4.1.1 Système d’exploitation : Yellow dog linux : 2.6.23-9.ydl6.1 Plate-forme : Playstation 3 Les performances obtenues avec ces optimisations sont proches de celles du code d’ibm, cela confirme que le code présente de bonnes performances. En appliquant exactement les mêmes optimisations au code dotproduct, mais en ajoutant une réduction sur le vecteur résultant à la fin de la boucle, on obtient les performances suivantes : 14 18 16 14 Perf (Gflops) 12 10 8 6 4 2 29696 28672 27648 26624 25600 24576 23552 22528 21504 20480 19456 18432 17408 16384 15360 14336 13312 12288 11264 10240 9216 8192 7168 6144 5120 4096 3072 2048 0 Tailles des vecteurs Perf dotproduct Perf sdot_spu Fig. 6: Performances du code dotproduct optimisé comparées aux performances du code d’IBM Compilateur pour les SPE : spu-gcc : gcc version 4.1.1 Compilateur pour le PPE : ppu32-gcc : gcc version 4.1.1 Système d’exploitation : Yellow dog linux : 2.6.23-9.ydl6.1 Plate-forme : Playstation 3 Blas 2 Le code de blas 2 étudié est le produit matrice-vecteur sgemv. Le code de départ est le code vectorisé suivant : for ( i =0; i < m ; i ++) tmp = zero ; for ( j =0; j < n ; tmp += X [ j ] } Reduction par Store } { j ++) { * Y [ j ]; somme du vecteur tmp Le code assembleur de la boucle la plus interne est le suivant : 000071 000071 000072 000073 000078 000078 1 D 0D 1D 1 0 0D . L20 : ai lqx lqx ai fma . L29 : brnz -12 123456 234567 34 - - - -890123 8901 $4 , $4 , -1 $3 , $5 , $13 $2 , $5 , $12 $5 , $5 ,16 $6 , $3 , $2 , $6 $4 ,. L20 Pour essayer d’augmenter les performances, il est possible de diviser la matrice X en 4 et de dérouler la boucle sur i d’un facteur 4 pour traiter 4 lignes de celle-ci en une seule itération sur j, comme le montre le schéma suivant : 15 4 lignes de la matrice accédées en parallèle : Fig. 7: Matrice divisée en 4 portions : Le code de la boucle ainsi déroulée est le suivant : (X1 à X4 représentent les 4 portions de la matrice X) for ( i =0; i < m /4; i ++) { tmp = zero ; for ( j =0; j < n ; j ++) { tmp1 += X1 [ j ] * Y [ j ]; tmp2 += X2 [ j ] * Y [ j ]; tmp3 += X3 [ j ] * Y [ j ]; tmp4 += X4 [ j ] * Y [ j ]; } Reduction par somme des vecteurs tmp Store x 4 } Le code assembleur tel qu’il est généré par le compilateur : 000096 000096 000097 000098 000099 000100 000101 000103 000104 000105 000106 000106 1 D 0D 1D 1 1 1 1 0 0 0 0 0D 01 012 0123 01234 012345 12 -345678 456789 567890 678901 -67 6789 789 89 9 6789 . L20 : ai lqx lqx lqx lqx lqx ai fma fma fma fma . L29 : brnz $8 , $8 , -1 $35 , $7 , $30 $37 , $7 , $22 $4 , $7 , $29 $36 , $7 , $28 $6 , $7 , $27 $7 , $7 ,16 $9 , $35 , $37 , $9 $11 , $35 , $4 , $11 $12 , $35 , $36 , $12 $16 , $35 , $6 , $16 $8 ,. L20 Les fma et les laods n’étant pas entrelacés pour exploiter le parallélisme d’instructions, on va donc faire un pipeline logiciel comme pour les codes saxpy et dotproduct. 16 Le code C suivant montre le boucle interne avec un pipeline logiciel : for ( i =0; i < m /4; i ++) { tmp = zero ; for ( j =0; j < n ; j ++) { tmp1 += x1 * y1 ; x1 = X1 [ j ]; tmp2 += x2 * y1 ; x2 = X2 [ j ]; tmp3 += x3 * y1 ; x3 = X3 [ j ]; tmp4 += x4 * y1 ; x4 = X4 [ j ]; y1 = Y1 [ j ]; } Reduction par somme des vecteurs tmp Store x 4 } Le code assembleur généré est le suivant : 000100 000100 000101 000101 000102 000102 000103 000103 000104 000104 000105 000106 1 d 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 0d 012345 --012345 123456 123456 234567 234567 345678 345678 45 456789 56 -6789 . L22 : fma lqx fma lqx fma lqx fma lqx ai lqx ai . L28 : brnz $15 , $19 , $5 , $15 $5 , $3 , $26 $2 , $9 , $19 , $2 $9 , $3 , $21 $8 , $7 , $19 , $8 $7 , $3 , $28 $13 , $19 , $6 , $13 $6 , $3 , $27 $4 , $4 , -1 $19 , $3 , $29 $3 , $3 ,16 $4 ,. L22 On peut voir qu’à chaque itération, il y a trois cycles de stalls sur la première fma. Ce stall vient de la dépendance avec le dernier load. Afin d’éviter ce stall, nous allons faire un déroulage d’un facteur 2 de la boucle interne pour avoir plus d’instructions à entrelacer : for ( j =0; j < n /4; j +=2) { { FMA LOAD } x 4 LOAD { FMA LOAD } x 4 LOAD } 17 Le code assembleur de la boucle interne devient donc : 000114 000115 000116 000116 000117 000117 000118 000118 000119 000119 000120 000121 000122 000123 000124 000124 000125 000125 000126 000126 000127 000127 000128 000128 000129 000129 000130 0d 1d 0D 1D 0D 1D 0D 1D 0D 1D 0 0 0 0 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 0d 45678 -5678 678901 678901 789012 789012 890123 890123 90 901234 01 12 23 34 45 4 567890 567890 678901 678901 789012 789012 890123 890123 90 901234 01 000131 1 d -1234 . L22 : fma lqx fma lqx fma lqx fma lqx a lqx a a a a ai lnop fma lqd fma lqd fma lqd fma lqd clgt lqd ai . L28 : brnz $54 , $22 , $14 , $16 $48 , $13 , $31 $52 , $25 , $14 , $9 $22 , $13 , $29 $50 , $24 , $14 , $17 $25 , $13 , $26 $47 , $23 , $14 , $12 $24 , $13 , $30 $51 , $26 , $13 $23 , $13 , $28 $49 , $30 , $13 $46 , $28 , $13 $53 , $29 , $13 $55 , $31 , $13 $15 , $15 ,2 $16 , $18 , $48 , $54 $14 ,16( $55 ) $9 , $21 , $48 , $52 $18 ,16( $53 ) $17 , $20 , $48 , $50 $21 ,16( $51 ) $12 , $19 , $48 , $47 $20 ,16( $49 ) $45 , $33 , $15 $19 ,16( $46 ) $13 , $13 ,32 $45 ,. L22 Cette fois toutes les fma peuvent être lancées en parallèle avec un laod, et il ne reste plus qu’un seul cycle de stall sur la première itération de la boule. Les performances sont donc satisfaisantes. D’autres versions du produit matrice-vecteur sont possibles, par exemple en utilisant un produit scalaire à la place de la boucle interne. Voici les performances obtenues pour les différentes versions du produit matrice-vecteur : 18 14 12 Performances (Gflops) 10 8 6 4 2 16 128 240 352 464 576 688 800 912 1024 1136 1248 1360 1472 1584 1696 1808 1920 2032 2144 2256 2368 2480 2592 2704 2816 2928 3040 3152 3264 3376 3488 3600 3712 3824 3936 4048 0 Nombre de colonnes de la matrice Perf sgemv sgemv_spu sgemv dotproduct sgemv sdot La courbe sgemv représente les performances du code que l’on vient d’optimiser. La coube sgemv spu représente les performances du code d’IBM. La courbe sgemv dotproduct représente les performances du produit matrice-vecteur en utilisant le prosuit scalaire dorproduct (optimisé dans la partie précédente). La courbe sgemv sdot représente les performances du produit matrice-vecteur en utilisant le produit scalaire sdot d’IBM. Fig. 8: Performances de différentes versions du code sgemv en fonction du nombre de colonnes de la matrice, pour un nombre constant de lignes (32). Compilateur pour les SPE : spu-gcc : gcc version 4.1.1 Compilateur pour le PPE : ppu32-gcc : gcc version 4.1.1 Système d’exploitation : Yellow dog linux : 2.6.23-9.ydl6.1 Plate-forme : Playstation 3 Les performances du code optimisé sont les meilleures pour cette configuration de matrice. Cependant, c’est le code d’ibm qui est le meilleur sur des matrices rectangulaires dans ”l’autre sens”, c’est-à-dire avec un grand nombre de lignes mais peu de colonnes, comme on peut le constater sur le tableau suivant : Taille de la matrice M 1024 512 256 128 64 32 N 32 64 128 256 512 1024 Performances des différents codes (en Gflops) Sgemv Sgemv avec Sgemv avec sgemv ibm optimisé dotproduct sdot spu 16,6 4,08 1,43 1,52 16,24 4,99 2,45 2,63 15,61 7,83 3,65 4,29 14,45 9,24 4,92 6,2 12,54 10,15 5,95 (7,16) 8,02 9,94 10,67 6,65 (7,76) 9,37 19 Blas 3 Le code pris comme exemple dans le cadre des blas de niveaux 3 est le produit matrice- matrice sgemm. Ce code calcul la matrice C = A x B, les dimensions des matrices étant les suivantes : – A : m x k (m lignes, k colonnes) – B : k x n (k lignes, n colonnes) – C : m x n (m lignes, n colonnes) En optimisant la version naı̈ve de ce code de la même manière que pour le produit matrice-vecteur, on obtient un code efficace pour k grand (grand nombre de lignes pour A et grand nombre de colonnes pour B). Afin d’obtenir également de bonnes performances pour d’autre configurations (notamment quand k est petit) il est nécessaire l’intervertir les boucles. Cette inversion peut être faite car il n’y a aucune dépendance entre les différentes itérations des boucles. Les performances des deux versions du produit matrice-matrice pour différentes tailles de matrice sont synthétisées dans les graphes suivants : 20 25 Performances (Gflops) 20 15 10 5 1984 1888 1792 1696 1600 1504 1408 1312 1216 1120 1024 928 832 736 640 544 448 352 256 160 64 0 K sgemm sgemm_spu (IBM) (a) sgemm pour m=4, n=32 25 Performances (Gflops) 20 15 10 5 2032 1936 1840 1744 1648 1552 1456 1360 1264 1168 1072 976 880 784 688 592 496 400 304 208 112 16 0 N sgemm sgemm_spu (IBM) (b) sgemm m=4, k=4 Fig. 9: Comparaison des performances des différentes code sgemm optimisés aux performances du code d’IBM 21 Compilateur pour les SPE : spu-gcc : gcc version 4.1.1 Compilateur pour le PPE : ppu32-gcc : gcc version 4.1.1 Système d’exploitation : Yellow dog linux : 2.6.23-9.ydl6.1 Le code assembleur de la version la plus performante est le suivant : 000246 000247 000248 000249 000249 000250 000250 000251 000252 000253 000253 000254 000254 000255 000256 000257 000258 000259 000259 000260 000260 000261 000261 000262 000262 000263 000264 000265 000266 000267 000267 000268 000269 000270 000270 000271 000273 000274 000274 000275 000275 000276 000276 000277 000277 000278 000278 000279 000279 000280 000280 000281 000282 000283 0 0 0 0D 1D 0D 1D 0 0 0D 1D 0D 1D 0 0 0 0 0D 1D 0D 1D 0D 1D 0D 1D 0 0 0 0 0D 1D 0 0 0D 1D 0 0 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 0D 1D 1 0 0D 01 012 0123 6789 789 89 9 9 012345 012345 123456 234567 345678 3 456789 456789 567890 678901 789012 890123 901234 9 012345 012345 123456 1 234567 234567 345678 456789 567890 678901 789012 789012 890123 901234 012345 012345 12 -345678 456789 4 567890 567890 678901 678901 789012 7 890123 890123 901234 901234 012345 012345 123456 23 3 000283 1 D 3456 . L48 : fma fma fma nop hbrp fma lqx fma fma fma lnop fma lqx fma fma fma fma fma hbrp fma lqx fma lnop fma lqx fma fma fma fma fma lqx fma fma fma lqx ai fma fma lnop fma lqx fma lqx fma lnop fma lqx fma lqx fma lqx lqx ai nop . L55 : brnz $76 , $25 , $10 , $16 $7 , $25 , $14 , $28 $8 , $25 , $13 , $31 127 # 1 $9 , $25 , $11 , $20 $25 , $12 , $39 $3 , $21 , $10 , $30 $2 , $21 , $14 , $27 $4 , $21 , $13 , $15 $5 , $21 , $11 , $29 $21 , $12 , $35 $6 , $24 , $10 , $76 $78 , $24 , $14 , $7 $79 , $19 , $10 , $3 $31 , $19 , $14 , $2 $30 , $19 , $13 , $4 # 2 $28 , $19 , $11 , $5 $19 , $12 , $34 $77 , $24 , $13 , $8 $76 , $24 , $11 , $9 $24 , $12 , $38 $27 , $23 , $10 , $6 $15 , $18 , $14 , $31 $29 , $18 , $13 , $30 $20 , $18 , $11 , $28 $16 , $18 , $10 , $79 $18 , $12 , $33 $7 , $23 , $14 , $78 $8 , $23 , $13 , $77 $9 , $23 , $11 , $76 $23 , $12 , $37 $26 , $26 , -1 $30 , $17 , $10 , $16 $28 , $22 , $14 , $7 $16 , $22 , $10 , $27 $10 , $12 , $45 $27 , $17 , $14 , $15 $14 , $12 , $48 $15 , $17 , $13 , $29 $31 , $22 , $13 , $8 $13 , $12 , $47 $29 , $17 , $11 , $20 $17 , $12 , $32 $20 , $22 , $11 , $9 $22 , $12 , $36 $11 , $12 , $46 $12 , $12 ,16 127 $26 ,. L48 Il est possible de repérer où l’on perd du temps : – On perd un cycle à l’instruction 249 à cause de l’instruction hbrp (hint for branch prediction) qui est lancée en même temps qu’un nop alors qu’une fma pourrait être lancée à la place du nop. – On perd un cycle à l’instruction 271 et un autre cycle à l’instruction 282 à cause d’un ai (add immediate) qui utilise le même pipeline que les fma. – On perd un cycle à l’instruction 273 à cause d’un stall Au total on perd donc 4 cycles sur 32 utilies (32 fma), soit 12,5% de perte. Cela qui explique que les performances que nous obtenons sont éloignés de 12% des performances crêtes. 22 2.3 Benchmark des communications Pour avoir de bonnes performances sur des codes utilisants plusieurs SPE, il est nécessaire de comprendre en détail le fonctionnement des communications sur ce processeur. L’objectif de cette section est donc de tester les différents types de communications disponibles sur Cell, afin élaborer un modèle de performance de communications. Le processeur Cell propose deux mécanismes pour transférer des données entre ses différents cœurs : – Chaque SPE dispose d’une ”mailbox” pour recevoir des données en provenance du PPE. Le PPE dispose quand à lui de 2 ”mailbox”, l’une pour recevoir des interruptions en provenance des SPE, et l’autre pour recevoir des données en provenance des SPE. La taille de chaque message est de 32 bits – Des transfert DMA sont possibles sur le Cell à la fois entre le PPE et les SPE et entre différentes SPE. 2.3.1 Communication par mailbox Les fonctions utiles pour réaliser un transfert par ”mailbox” sont les suivantes : Sur les SPE : spu read in mbox : Cette fonction renvoie un message de la ”mailbox” du SPE appelant cette fonction. Cette fonction est bloquante si la ”mailbox” est vide. spu write out mbox : Cette fonction permet d’écrire un message dans la mailbox du PPE. Cette fonction est bloquante si la mailbox du PPE est pleine. Sur le PPE : spe in mbox write : Cette fonction permet au PPE d’écrire un message dans la ”mailbox” du SPE passé en paramètre de la fonction. spe in mbox status : Cette fonction permet au PPE de connaı̂tre le nombre de messages dans la ”mailbox” du SPE passé en paramètre de la fonction. spe out mbox read : Permet au PPE de lire un message en provenance d’un SPE passé en paramètre de la fonction. spe out mbox status : Permet au PPE de connaı̂tre le nombre de messages écrits par le SPE passé en paramètre de la fonction. La communication par ”mailbox” ne permet que le transfert de messages de 32 bits, pour réaliser un transfert plus important, il est nécessaire de faire plusieurs transferts de 32 bits successifs. Le graphique suivant montre l’évolution du débit de transfert par mailbox en fonction de la taille des données transférées : 23 1.2 1 Debit (Octets/s) 0.8 0.6 0.4 0.2 0 1536 3072 4608 6144 7680 9216 10752 12288 13824 15360 16896 18432 19968 21504 23040 24576 26112 27648 29184 30720 32256 33792 35328 36864 38400 39936 0 Taille du transfert Debit lecture Debit ecriture Fig. 10: Débit de transfert par mailbox Compilateur pour les SPE : spu-gcc : gcc version 4.1.1 Compilateur pour le PPE : ppu32-gcc : gcc version 4.1.1 Système d’exploitation : Yellow dog linux : 2.6.23-9.ydl6.1 Plate-forme : Playstation 3 2.3.2 Communication par DMA La taille d’un transfert DMA doit être 1, 2 4 ou 8 octets ou multiple de 16 octets. la taille maximale des données transférées est de 16 ko. Les données doivent être alignées en mémoire, sur au minimum 8 octets, mais un alignement sur 128 octets permet d’obtenir un meilleur debit. Les fonctions suivantes sont utilisées pour réaliser un transfert DMA sur Cell : mfc get : Permet le transfert DMA depuis le PPE vers le SPE appelant cette fonction. mfc put : Permet le transfert DMA depuis le SPE appelant cette fonction vers le PPE. mfc getl : Permet de réaliser une liste de transferts DMA depuis le PEE vers un SPE. mfc putl : Permet de réaliser un liste de transferts DMA depuis le SPE vers le PPE. Les transferts DMA sont toujours initialisés par les SPE. Une liste de transfert DMA est représentée par un tableau dont chaque élement représente un transfert DMA. Chaque élement du tableau doit être de la forme suivante : 4 octets représentant la taille du transfert et 4 octets representant l’adresse effective, c’est à dire l’adresse en mémoire centrale des données à accéder (en lecture ou en écriture). L’adresse sur le local store des SPE est un paramètre des primitives de transfert. Les performances des transferts DMA sont synthétisées dans les graphes suivants : 24 Debit (Go/s) 18 16 14 12 10 8 6 4 2 0 25 20 15 10 5 0 Debit (Go/s) Tailles du transfert (octets) Temsp (µs) (a) Transfert du PPE vers un SPE Debit (Go/s) Taille du transfert (octets) Temps (µs) (b) Transfert d’un SPE vers le PPE Fig. 11: Débit de transfert DMA sur Cell Compilateur pour le PPE : ppu32-gcc : gcc version 4.1.1 Système d’exploitation : Yellow dog linux : 2.6.23-9.ydl6.1 Plate-forme : Playstation 3 25 12 10 8 6 4 2 0 12 10 8 6 4 2 0 Temps (µs) Temps (µs) 512 5120 9728 14336 18944 23552 28160 32768 37376 41984 46592 51200 55808 60416 65024 69632 74240 78848 83456 88064 92672 97280 101888 106496 111104 115712 120320 124928 129536 512 4608 8704 12800 16896 20992 25088 29184 33280 37376 41472 45568 49664 53760 57856 61952 66048 70144 74240 78336 82432 86528 90624 94720 98816 102912 107008 111104 115200 119296 123392 127488 Compilateur pour les SPE : spu-gcc : gcc version 4.1.1 Debit (Go/s) 2.3.3 Modèle de communication D’après les mesures effectuées dans la partie précedente, on peut voir que les communications par DMA sont beaucoup plus efficaces que les communications par mailbox. Par consequent les communications par DMA sont à préférer par rapport aux communications par mailbox. Cependant les communications par mailbox sont indispensables pour réaliser un mécanisme de synchronisation entre les SPE ou le PPE. Un problème peut également se poser sur les transferts par DMA. Si le TLB du SPE réalisant un transfert n’est pas initialisé, il se produit un ”TLB miss” qui ralentit considérablement les transferts. Il se produit donc un ”TLB miss” à chaque nouvelle page mémoire accédée (pour la première fois). Sur la PS3, la taille de page est de 16ko. On peut constater les effets des ”TLB miss” sur les performances des transferts sur le graphe suivant : 16 14 12 Debit (Go/s) 10 8 6 4 2 114688 114688 98304 98304 81920 81920 65536 65536 49152 49152 32768 32768 16384 16384 0 0 0 Offset du transfert (octets) Debit (Go/s) Fig. 12: Débit de deux transferts DMA consécutifs en fonction de l’offset du transfert (par rapport à l’adresse de base) Compilateur pour les SPE : spu-gcc : gcc version 4.1.1 Compilateur pour le PPE : ppu32-gcc : gcc version 4.1.1 Système d’exploitation : Yellow dog linux : 2.6.23-9.ydl6.1 Plate-forme : Playstation 3 C’est ce mécanisme qui explique la chute du débit vers une taille de transfert de 16ko sur le graphe 11a. En effet à partir d’une certaine taille de transfert, on accéde à une page mémoire non encore accédée, d’où le ”TLB miss”. 26 3 Modèle de performances 3.1 Énoncé Nous avons vu dans la partie 2.2.3 comment générer des codes performants pour les SPE. Mais ce seul modèle n’est pas suffisant pour avoir des applications complètes performantes sur Cell. En effet il est nécessaire de prendre en compte les communications afin d’utiliser plusieurs cœurs du processeur. Pour avoir un code multi-cœurs efficace, il est évidemment nécessaire d’avoir des kernels de calculs optimisés. Mais il est aussi primordial de recouvrir les transferts de données avec des calculs. Recouvrir les transferts par les calculs signifie réaliser les calculs en parallèle des transferts. Le principe est un peu le même que le ”streaming” mais avec une plus grosse granularité. Si on arrive à recouvrir complètement les transferts avec les calculs, les performances du code multi-cœur seront les mêmes que celles du kernel de calcul utilisé multiplié par le nombre de cœurs utilisés. 3.2 Validation du modèle Afin de valider le modèle de performance énoncé précédement nous avons implémenté deux programmes utilisant l’ensemble des SPE du Cell. Ces programmes sont un produit matriciel et une décomposition LU. L’implémentation de ces deux programmes est détaillée dans les deux sections suivantes. 3.2.1 Produit matriciel multi-SPE Pour réaliser le produit matriciel multi-SPE nous avons déjà le kernel de calcul. Qui est le code sgemm optimisé en partie 2.2.3. Il ne reste donc plus qu’ à gérer les communications. Dans un premier temps nous avons répartits les calculs comme suit : – La matrice résultat est décomposée en blocs carrés de taille 64x64 – Chaque SPE calcule un ensemble de lignes de la matrice résultat. – Pour chaque bloc, le SPE qui le calcule utilise un buffer où le bloc est stocké, le calcul de ce bloc se déroule comme suit : 1. SGEMM des deux blocs numérotés 1 dans le buffer C. Chargement en parallèle des deux blocs 2. 2. SGEMM des deux blocs 2 dans le buffer C. Chargement des blocs 3 3. Itérer ainsi sur toute la dimension K. Mais cette répartition ne permet pas d’optenir les meilleurs performances possibles. le cout en temps des deux transferts est trop important pour pouvoir être complètement masqué par le calcul. Nous avons donc réparti d’une manière différente les calculs et les transferts pour éviter d’avoir deux transferts à faire pour chaque calcul. 27 1. SGEMM de A a1 et B a1 dans C a. Chargement de B b1. 2. SGEMM de A a1 et B b1 dans C b. Chargement de A b1. 3. SGEMM de A b1 et B a1 dans C c. Chargement de A a2. 4. SGEMM de A b1 et B b1 dans C d. Chargement de B a2. 5. SGEMM de A a2 et B a2 dans C a. Chargement de B b2. 6. SGEMM de A a2 et B b2 dans C b. Chargement de A b2. 7. SGEMM de A b2 et B a2 dans C c. Chargement de A a suivant. 8. SGEMM de A b2 et B b2 dans C c. Chargement de A a suivant. 9. Itérer ainsi sur toute la dimension K. De cette façon pour chaque calcul de bloc, il n’y a qu’un seul transfert à recouvrir. Avec cette répartition, il est possible de faire varier la taille ”horizontale” et la taille ”verticale” des blocs. Nous avons donc fait une série de tests pour vérifier quelles tailles de blocs permettent d’obtenir les meilleurs performances. 28 Fig. 13: Performances de sgemm sur 4 SPE pour différentes tailles de blocs Il est impossible d’avoir une taille trop grande taille ”ysize” avec un tel blocage de la matrice. En effet, nous avons a besoin de 4 buffer sur la matrice résultats et ces buffers ont une taille de ysize x ysize, donc lorsque cette dimention augmente trop, la taille de ces 4 buffers dépasse la capacité du local store des SPE. Les tailles de blocs les pour lesquels les performances sont les meilleurs sont les suivantes : 168x24 et 64x64. Nous avons donc mesuré les performances de sgemm en utilisant des block de tailles 168x24 et 64x64 et en utilisant notre propore kernel optimisé ainsi que le kernel de blas d’IBM. L’université de Dresden[?] à publié un code de sgemm utilisant les plusieurs SPE du Cell. Ce code permet d’utiliser 99.9% des performances crêtes du Cell. Nous avons donc ajouté les performances de ce code à nos comparaisons. Voici les performances de différentes versions du produit matriciel multi-SPE : 29 450 400 Performances (Gflops) 350 300 250 200 150 100 50 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Nombre de SPE sgemm 64x64 ibm blas 168x24 sgemm 168x24 dresden ibm blas 64x64 Fig. 14: Comparaison des performances de différentes versions de produit matriciel en fonction du nombre de SPE utilisés. (Produit de deux matrices 8192 x 8192) Compilateur pour les SPE : spu-gcc : gcc version 4.1.1 Compilateur pour le PPE : ppu32-gcc : gcc version 4.1.1 Système d’exploitation : Linux qs22-01 2.6.18-128.el5 Plate-forme : Blade QS22 Pour un nombre de SPE utilisés inférieur à 8, les kernels qui offrent les meilleurs performances sont les kernels rectangulaire. Mais dès le passage à deux Cell (nombre de SPE supérieur à 8) seuls les kernels carrés offrent une scalabilité parfaite. Cette répartition des calculs et de transferts et l’utilisation de blocs de taille 64x64 nous permet d’obtenir 80% des performances crêtes de la machine. Ce rapport est le même que le rapport entre les performances du kernel utilisé et les performances crêtes d’un SPE. Ceci nous confirme qu’il est possible de recouvrir entièrement les communications avec le temps de calcul. Ce recouvrement parfait nous permet ainsi d’avoir une scalabilité parfaite. 3.2.2 Décomposition LU Le décomposition LU est un code d’algèbre linéaire permettant de décomposer une matrice sous la forme de deux matrice, l’une triangulaire supérieur, l’autre triangulaire inférieur. Cette décomposition peut être utilisée dans la résolution de systemes linéaires. 30 La décomposition LU permet, avec M donnée, de calculer les matrices L et U tels que M = L.U avec U triangulaire supérieur et L triangulaire inférieur. L’algorithme séquentiel est le suivant : Pour k de 1 a N Pour j de k +1 a N M (k , j ) = M (k , j ) / M (k , k ) Pour i de k +1 a N M (i , j ) = M (i , j ) - M (i , k ) * M (k , j ) FinPour FinPour FinPour Les deux matrices sont stockées dans la même matrice : U dans le partie supérieure de la matrice et L dans la partie inférieure. Pour introduire du parallèlisme dans cet algorithme nous avons écrit une version par bloc de cet algorithme : La matrice est divisée en blocs carré. Pour chaque bloc les mises à jours sont les suivantes : Si bloc sur la diagonal (z = x et t = y) : forall ( int i = 0 ; i < x ; i ++ ) { A [ z : t , x : y ] -= A [ z : t , i ] * A [ i , x : y ] } LU ( A [ z : t , x : y ]) Si bloc sous-diagonal : forall ( int i A[z:t } for ( int i = A[z:t A[z:t } = 0 ; i < x ; i ++ ) { , x : y ] -= A [ z : t , i ] * A [ i , x : y ] x ; i < y ; i ++ ) { , i ] = 1/ A [i , i ] * A [ z : t , i ] , i +1: y ] -= A [ z : t , i ] * A [ i , i : y ] Si bloc sur-diagonal : forall ( int i = 0 ; i < z ; i ++ ) { A [ z : t , x : y ] -= A [ z : t , i ] * A [ i , x : y ] } for ( int i = z ; i < t ; i ++ ) { A [ i : t , x : y ] -= A [ i : t , i ] * A [ i , z : y ] } Toutes les mises à jour utilisant des blocs externes se rapportent en fait à une sorte de sgemm (il suffit de changer un + en -). Pour respecter les dépendances des mises à jours, il faut proceder aux mises à jour par front diagonal comme le montre le shéma cicontre. Pour la décomposition d’une matrice 1024x1024 avec des blocs de taille 64x64 on obtient des performances d’environ 6 Gflops sur un SPE. Les performances sont médiocres car d’un part nous n’avons pas eu le temps de mettre en place un quadruple buffering sur les mises à jours utilisant des blocs externes (comme 31 nous avons fait sur sgemm), et d’autre part les mises à jour internes aux blocs n’ont pas été vectorisées. Ce sont donc deux pistes à explorer pour augmenter les performances de ce code. Un quadruple buffering des mises à jours externes devrait permettre d’augmenter significativement les performances car ce type de mise à jour représente la plus grande partie des calculs dans ce code, notement à partir de la moitié du traitement de la matrice. 32 4 Générateur de code Après avoir énoncé le modèle de performance de la partie 3 nous savons que les performances des codes sur cell dépendent directement des performances des kernels. Par consequent il est important de pouvoir générer un grand nombre de kernels à tester automatiquement afin d’obtenir de bons kernels, en particulier une version non transposée du produit matriciel. Le but de cette section est de décrire le fonctionnement du générateur de codes permettant de générer ces différentes versions et d’exposer les résultats obtenus. 4.1 Description Le générateur de code permet uniquement de générer un grand nombre de versions du même code : la sgemm. Les versions générées diffèrent par le facteur de déroulage de chacune des boucles et l’ordre des boucles. 4.1.1 Fonctionnement L’architecture du répertoire contenant le générateur est la suivante : . |-|-|-| | | | |-‘-- Makefile README kernel | - - Makefile | - - asm | - - size . h ‘-- spu_sgemm_unroll . c kernel_gen . pl sgemm_gen . pl Le script sgemm gen.pl permet de générer les kernels en eux-même, ces kernels étant générés dans le répertoire kernel sous forme de fichiers C portant des noms permettant de repérer la version du code. Par exemple le fichier sgemm unroll 8 4 1.c contient les kernels avec la boucle i déroulée d’un facteur 8, la boucle j déroulée d’un facteur 4 et la boucle k non déroulée (déroulée d’un facteur 1). Chaque fichier de la sorte contient trois kernels différents, chacun avec une boucle interne différente. Le script kernel gen.pl permet de générer les fichers permettant de compiler et de tester les kernels générés. Il génère les fichiers spu sgemm unrolli unrollj unrollk.c et bench sgemm unrolli unrollj unrollk.c.pour chaque kernel. Ce script génère également des benchmarks pour un grand nombre de tailles différentes de matrices. C’est ce script qui appelle le script sgemm gen.pl pour générer toutes les versions de kernels à tester. Dans le répertoire kernel/asm, sont générés les codes assembleur correspondant à chaque kernel. Ces codes sont générés et annotés avec spu timing au moment de la compilation. 4.1.2 Utilisation Les arguments possibles du script kernel gen.pl sont : clean : Nettoie le répertoire ”kernel”. generate : Génère les différentes versions de kernels. compile : Compile tous les kernels dans le répertoire ”kernel”. run : Teste tous les kernels compilés du répertoire ”kernel”. 33 Une utilisation possible de ce script est donc : ./kernel gen.pl clean generate compile run Le script sgemm gen.pl admet les arguments suivants : -m facteur : facteur de déroulage de la boucle ”i”. -n facteur : facteur de déroulage de la boucle ”j”. -k facteur : facteur de déroulage de la boucle ”k”. Si il est appelé sans argument il génère par defaut les kernels sans aucun déroulage de boucle. 4.2 Résultats Pour des multiplications de matrices de tailles 64x64 les meilleurs performances que nous ayons obtenues sont de 19.87 Gfops, soit les même performances que notre kernel optimisé en partie 2.2.3 utilisant une matrice transposée. le kernel nous permettant d’obtenir ces performances est un kernel avec les boucle dans l’order i, j, k (k est la boucle le plus interne) les boucles i et j sont déroulées d’un facteur 8 et la boucle k déroulée d’un facteur 4. De plus pour des matrices plus grosse les performances de ce kernel sont de 22.53 Gflops (taille M :64, N :64, K :400) : la génération automatique d’un grand nombre de versions de code nous a permit d’obtenir un kernel dont les performances sont les mêmes que le kernel écrit à la main par ”tatonnement”. 34 5 Conclusion Ce stage m’a permis d’approfondir différentes notions étudiées en cours, ainsi que de mettre en pratique un grand nombre de concepts enseignés à l’école. Comme nous l’avons déjà expliqué, il existe encore de nombreuses pistes de travail notement au niveau de la décomposition LU. De plus pour l’instant le générateur de code permet uniquement de générer des codes de sgemm. Il pourrait également être intéressant de travailler sur un compilateur source à source permettant d’optimiser automatiquement des codes pour Cell. Je tiens à remercier Denis Barthou, Julien Jaeger et Alexandre X. Duchateau Navarret pour leur encadrement durant ce stage qui a été extrèmement enrichissant pour moi. 35