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