Tutoriel : Le roofline sans peine !

Est-il possible d’améliorer les performances de telle ou telle boucle ? Si la performance de votre code est bornée par les accès mémoire, c’est peine perdue. Mais comment le savoir ? Ce tutoriel a pour but de vous donner quelques pistes.

Article mis en ligne le 20 septembre 2017
dernière modification le 21 septembre 2017
logo imprimer

Avant de commencer....

  • Ce tutoriel est de niveau avancé : pour pouvoir le suivre, vous devez avoir les prérequis suivants :
    • Être familier avec les commandes unix
    • Connaître la programmation : de préférence fortran, éventuellement C
    • Savoir écrire un fichier make, compiler un programme, l’exécuter en utilisant le système de queue de Calmip
  • Afin d’exécuter ce tutoriel, il vous faut installer sur votre compte sur eos les codes de tests ci-contre.
  • Nous utiliserons quelques outils simple d’Unix, et notamment l’outil placement développé à Calmip
  • Mais nous utiliserons aussi les outils d’Intel : compilateur fortran et environnement de développement
  • Avant toute chose vous devez donc initialiser l’environnement de développement d’intel, de préférence le plus récent. A l’heure où ce tutoriel est mis en ligne il faut faire :

Étude du code

Allez dans le répertoire 01-vectorisation_boucle et éditez le fichier vect.f90. Les fonctions importantes sont :

  • init qui initialise 5 vecteurs [1] de flottants appelés a,b,c,d,e. La dimension est de 625 millions de cellules, soit 625 * 8 * 5 = 25 Go alloués dynamiquement.
  • test0 qui va calculer a = b * c + d * e

Éditez maintenant le fichier Makefile :

  • Vers la ligne 16, une variable vous permet d’activer ou désactiver la vectorisation voir ici
  • Compilez et exécutez avec et sans la vectorisation. Le fichier make appelle deux fois le compilateur : une fois pour générer l’exécutable, et une autre fois pour générer le code assembleur et le copier dans un fichier lisible.

Vous pouvez constater que :

  1. La vectorisation est bien activée (instructions vXXX) :
  2. Et pourtant ça ne va pas plus vite ! (voir les fichiers out.txt*)

Damned ! Le processeur est-il en panne ? Même pas ! Mais avant de comprendre ce qui se passe, étudions la notion de bande passante mémoire.

Observer la mémoire avec des outils simples

Allez dans le répertoire 02-bande_passante. Editez le Makefile et vers la ligne 12 supprimez openmp (pour que le programme n’aille pas trop vite on ne verrait rien), et compilez :

Lancez le programme sur un nœud, connectez-vous sur le nœud et appelez la commande top :

Observez les colonnes VIRT et RES : VIRT est dès le début du job à 24.4g, alors que RES augmente progressivement.

  • VIRT montre la réservation en termes de mémoire virtuelle, celle-ci est fixée dès l’appel à allocate (ligne 15 du programme fortran).
  • RES montre la mémoire résidente, c’est-à-dire réellement allouée. La mémoire n’est allouée qu’en cas de besoin, au fur et à mesure que les vecteurs se remplissent (fonction init).
    PNG

Recompilez avec openmp (fichier Makefile), et éditez batch.slurm pour mettre la variable MODE sur scatter. Cette variable est utilisée avec la commande placement.Exécutez la ligne bash suivante :

Vous devez observer un affichage qui ressemble à cela :
PNG
On voit aisément que :

  • Le programme utilise 10 "threads" (lettres A en rouge) réparties sur chaque socket, donc 5 cœurs sont utilisés sur chaque socket
  • La mémoire est répartie également sur les deux sockets (lettre A en violet), et chaque thread accède à la mémoire qui se trouve connectée à son processeur (socket).

Éditez maintenant batch.slurm pour mettre la variable MODE sur compact. Exécutez la même commande que précédemment. Cette fois vous devez observer un affichage qui ressemble à cela :
PNG
On voit aisément que :

  • Le programme utilise toujours 10 "threads" (lettres A en rouge) mais cette fois elles tournent sur les 10 cœurs du premier socket.
  • La mémoire est allouée sur le même socket (lettre A en violet), donc là encore chaque thread accède à la mémoire sur le bon socket. Par contre, par rapport au cas précédent, la bande passante mémoire sera deux fois plus faible. En effet, on peut voir dans les fichiers de sortie qu’on va bien moins vite en placement compact qu’en scatter.

Enfin, éditez à nouveau batch.slurm pour repasser en SCATTER, et éditez également le fichier vect.f90 : aux alentours de la ligne 54, commentez l’appel openmp, afin de ne pas utiliser openmp dans la fonction init. recompilez, exécutez comme précédemment, et cette fois voici ce que vous obtenez :
PNG

On voit cette fois que les threads se distribuent sur les deux sockets, mais la mémoire, ayant été initialisée de manière séquentielle par la thread 0, n’est allouée que sur le premier socket. Il s’ensuit que la moitié des threads devra accéder à la mémoire de manière plus lente, donc la bande passante ne sera pas optimale.

La conclusion est que pour avoir de bonnes performances il est important d’initialiser les données en utilisant openmp . Sur le même sujet, voir cet article

Des outils simples permettent de recueillir pas mal d’informations sur votre code. Mais dans l’environnement de développement, nous avons aussi des outils de développement plus sophistiqués qui peuvent aussi être mis à contribution...

Utilisation de vtune pour vérifier l’équilibrage de charge

Allez dans le répertoire 02-bande_passante. Compilez avec openmp, assurez-vous qu’openmp est bien utilisé dans la procédure d’initialisation (cf. ci-dessus), et éditez batch.slurm afin de décommenter les lignes concernant vtune (vers la ligne14).

Appelez la commande sbatch puis observez le résultat de la mesure avec :

cliquez sur le bouton Bottom-up, et cochez la case CPU Sample (en bas à droite de l’écran). Vous observez en bas de l’écran un graphique ressemblant à celui-ci :
PNG

Ce graphique montre ce qui se passe avec votre programme et ses différentes threads. Vous pouvez voir dans quelle fonction on se trouve en promenant la souris sur le graphique. Et en faisant cela, vous verrez que votre programme passe l’essentiel de son temps dans la routine d’initialisation, et moins de une seconde dans la routine test0, la seule qui nous intéresse vraiment ! Cette situation est un peu frustrante, surtout que dans un cas réel il n’est pas toujours très simple de trouver la fonction qui nous intéresse vraiment. Nous pouvons y remédier en instrumentalisant notre code :

Ouvrez le programme fortran et décommentez les lignes marquées call itt_XXX : ces lignes appellent des fonctions liées à vtune, et vont permettre d’activer ou désactiver la prise de mesure par l’outil vtune.
Puis recompilez et relancez le script batch comme précédemment, relancez amplxe-gui, vous avez ce graphique :
PNG

On ne voit pas grand-chose de plus que précédemment, mais cette fois on identifie sans ambiguïté la zone d’intérêt, et il est simple de zoomer dessus. Les petits triangles verts correspondent aux "points de mesure" de l’outil :
PNG

La couleur brun foncé correspond aux moments où du travail utile est effectué, la couleur orange correspond aux moments où les threads ne font rien d’autre qu’attendre. On peut ainsi visualiser de manière très simple l’équilibrage de charge entre les threads, en l’occurrence il est plutôt bon. (quelques millisecondes d’attente).

Maintenant, supprimons comme précédemment l’utilisation d’openmp dans la fonction init, et voyons ce qui se passe. Il suffit de commenter une ligne (vers la ligne 54 de vect.f90), recompiler, exécuter le sbatch (cela dure bien plus longtemps car l’initialisation n’est plus parallélisée) puis amplxe-gui, et nous avons le graphique suivant, plutôt éloquent en termes d’équilibrage de charge. Les threads les plus rapides (celles qui ont les données du bon côté de la mémoire) attendent les retardataires plus de 150 ms, autant dire une éternité :
PNG

Il est clair à ce stade que notre routine test0 est entièrement contrôlée par la bande passante mémoire. Nous pouvons utiliser les processeurs les plus puissants du monde et vectoriser comme des fous, cela ne changera pas. Le paragraphe suivant montre un moyen de vérifier, boucle par boucle, si nous sommes bornés par la mémoire, ou au contraire s’il y a des marges d’amélioration.

Le roofline sans peine

Nous allons utiliser un autre outil de développement d’intel : Advisor, pour générer de manière très simple le graphique décrit ici de manière théorique.
Allez dans le répertoire 03-roofline_model et ouvrez le fichier vect.f90 : vous verrez qu’en plus de la fonction test0, vous trouverez d’autres fonctions test1, test2 etc. ; chacune de ces fonctions a une intensité arithmétique différente, comme expliqué ici (ce sont les mêmes boucles).

Lancez tout d’abord la commande :

Cliquez sur le lien New project, donnez un nom au projet, et indiquez dans le formulaire qui s’ouvre le nom de l’exécutable (vect). Une fenêtre s’ouvre alors, recherchez la mention à gauche Run Roofline et cliquez sur l’icône juste à droite, elle est marquée lors du survol de la souris Get Command Line .
Un popup s’ouvre alors avec une horrible commande : cliquez sur COPY, fermez advisor puis éditez le fichier batch.slurm et collez l’horrible commande à la place de la mention COMMANDE PROPOSEE PAR ADVIZE
Exécutez sbatch et ouvrez à nouveau Advisor. Cliquez sur votre projet puis sur Open my results. Cela ouvre une fenêtre, cliquez alors sur Survey & Roofline : vous pourrez alterner entre le diagramme ROOFLINE et l’écran SURVEY.
Dans l’écran ROOFLINE cliquez sur Use Single-Threaded Roofs, vous aurez le beau toit suivant :
PNG
Ce graphique montre que les différentes routines (passez la souris sur les points verts pour voir à quelle routine chacun correspond) sont toutes, à des degrés divers, bornées par la mémoire.

Notes :

[1Nous appelons vecteur un tableau à 1 dimension



Site réalisé sous SPIP
avec le squelette ESCAL-V3
Version : 3.86.44
Hébergeur : INP Toulouse