Profileurs mémoire MALT et NUMAPROF

Posté par  (site web personnel) . Édité par Pierre Jarillon, Davy Defaud, palm123 et claudex. Modéré par Pierre Jarillon. Licence CC By‑SA.
Étiquettes :
48
2
sept.
2018
C et C++

Outils de profilage

En calcul à haute performance — HPC (High Performance Computing) —, les problèmes liés à la mémoire deviennent de plus en plus critiques, qu’il s’agisse du contrôle de la consommation mémoire des applications, de la limitation des interactions avec le système d’exploitation (trop nombreuses allocations, trop petites allocations…) et de choix de placement (NUMA) ; et relativement peu d’outils libres permettent de profiler les applications sur ce terrain. Deux outils récemment mis en ligne apportent une part de réponse à ces questions.

La suite de l’article présentera MALT et NUMAPROF plus en détails…

MALT

MALT (MALloc Tracker) a été développé lors d’un post‐doc comme un outil de profilage d’allocation mémoire. L’outil reprend la sémantique efficace du couple valgrind‐kcachegrind, mais appliquée au suivi des allocations mémoire d’une application C, C++ ou Fortran.

Fournissant une interface plus complète que kcachegrind, MALT fournit une interface graphique Web exportée par un petit serveur en Node.js. Cette approche a un double intérêt :

  1. rapidité de développement pour un rendu agréable, en utilisant les bibliothèques JavaScript Angular, Bootstrap, D3JS ;
  2. sur une grappe de serveurs distante, l’interface est rendue localement en se connectant au serveur distant par un ssh-port-forward, ce qui évite les ralentissements gênants et habituels liés à un X forward d’une interface Qt ou GTK ;
  3. possibilité de travailler à plusieurs sur le même profil.

L’outil fournit entre autres :

  • un résumé global sur la consommation de l’application ;
  • des annotation du source code pour les différentes métriques ;
  • des compteurs pour les tailles minimum et maximum d’allocation, le nombre d’allocations et la durée de vie ;
  • des graphiques temporels ;
  • la distribution de la taille des objets alloués ;
  • la distribution des allocations sur les différents fils d’exécution.

Exemple d’interface :
Capture d’écran de l’interface Web de MALT

NUMAPROF

En calcul à haute performance et pour un certain nombre de serveurs, il est désormais courant de rencontrer des architectures dites NUMA (Non‐Uniform Memory Access). Autrement dit, avoir plusieurs processeurs sur la même carte mère, chacun attaché à ses propres bancs mémoire. La mémoire distante étant accessible de manière transparente, mais avec un surcoût. Cette topologie apparait même désormais à l’intérieur des processeurs eux‐mêmes, si l’on considère la gamme Xeon Phi d’Intel et certains processeurs AMD pour les serveurs.

Rappelons que sur les systèmes modernes, la mémoire vue par un programme est une mémoire dite virtuelle que le système d’exploitation est en charge, en collaboration avec le processeur, de faire correspondre à la mémoire physique. Cette correspondance entre les deux espaces est faite à l’aide du mécanisme de pagination consistant à découper ces deux espaces en pages (en général, 4 Kio ou 2 Mio sur les architectures x86 et x86-64).

Lorsqu’un segment est alloué par une application, il est initialement purement virtuel, le système d’exploitation autorisant cet espace mémoire, mais n’y projetant pas immédiatement de page physique. Ce n’est que lors du premier accès (dit « first touch ») que le système d’exploitation sera notifié et attachera une page à l’endroit touché.

Ce moment est critique sur architecture NUMA, car c’est à ce moment que le système d’exploitation va décider (en fonction de la position courante du fil d’exécution effectuant l’accès) sur quel nœud NUMA placer la page et donc les données. Le problème pour le développeur étant que cette association se fait de manière implicite par la première lecture‐écriture, et non par un appel explicite de fonction. Ceci conduit, dans de nombreuses applications, à des problèmes ignorés et de mauvaises correspondances difficiles à vérifier.

C’est dans ce cadre qu’a été développé NUMAPROF, en se basant sur pintool pour suivre tous les accès mémoire de l’application et reporter les correspondances NUMA. L’outil reprend une interface très similaire à MALT et fournit entre autres :

  • une annotation du source code ;
  • des métriques donnant le nombre d’accès distants, locaux, MCDRAM (pour les Intel Knight Landing), accès non liés (« bindés ») ;
  • une distribution sur les fils d’exécution ;
  • une matrice d’accès permettant de rapidement évaluer le comportement global de l’application.

Attention, l’outil ne prend pour l’instant en charge que les architectures x86-64.

Exemple d’interface :
Interface Web de NUMAPROF

Sources

Les deux outils sont libres et disponibles sur GitHub. Vous trouverez les sources, captures d’écran, documentations et liens vers des outils similaires sur : https://memtt.github.io/.

Aller plus loin

  • # Excellent

    Posté par  (site web personnel) . Évalué à 10.

    Merci pour ce petit billet sur les outils du monde HPC, c'est assez rare d'en voir donc d’autant plus appréciable.

    Le profilage mémoire est malheureusement un domaine assez peu connu et très peu enseigné qui est pourtant tout aussi significatif que le profilage du temps d'execution….Les allocateurs standard ayant généralement la fâcheuse tendance à ne pas beaucoup aimer le multi-threading et encore moins les arch NUMA.

  • # ça à l'air sympa.

    Posté par  . Évalué à 1.

    Je testerai dès que j'aurais un peu de temps :) Merci pour ces outils !

    • [^] # Re: ça à l'air sympa.

      Posté par  . Évalué à 0. Dernière modification le 06 septembre 2018 à 20:48.

      Je n'ai pas pu aller très loin dans mes tests malheureusement, le LD_PRELOAD echoue chez moi :

          > malt ../test_omega_solver.exe                                     
          MALT : Start memory instrumentation of test_omega_solver.exe - 120072 by library override.
          malt: line 146: 120072 Segmentation fault      LD_PRELOAD="$MALT_LIB" "$@"
      
      • [^] # Re: ça à l'air sympa.

        Posté par  (site web personnel) . Évalué à 1.

        Votre code est-il accessible quelque part que je cherche le bug ?

        The force is in doing, not trying.

        • [^] # Re: ça à l'air sympa.

          Posté par  . Évalué à 0.

          Non désolé, il est confidentiel…
          Peut-être que cela vient aussi de la configuration de la machine, ce ne serait pas la première fois.
          Cela dit le "make test" passe bien lui.

          Merci quand même.

  • # Accès

    Posté par  (site web personnel) . Évalué à 2.

    Maîtriser les allocations (nombres et tailles) est une chose, mais les gains importants en performance résident surtout sur les accès.
    Plus ils sont "distants" les uns des autres moins les caches matériels sont efficaces.
    De mémoire, il y a un facteur 200 entre la latence au niveau du cache L1 et la mémoire vive.

    J'espère qu'un jour on aura un outil pour aider à optimiser les accès.

    • [^] # Re: Accès

      Posté par  . Évalué à 3.

      Pardon d'avance pour ma remarque certainement incompétente, mais n'est-ce pas justement le boulot du compilateur d'optimiser ce genre de trucs? Je crois comprendre les raisons pour lesquelles les évolutions des langages (typiquement, C++) tendent à donner de moins en moins de marge aux compilateurs pour en redonner au programmeur, mais il reste important, à mon avis, de garder à l'esprit que la micro-optimisation n'est pas un hobby amusant pour la plupart des gens. La micro-optimisation mobilise de telles compétences et un tel effort de compréhension des subtilités des langages, des compilateurs, et de l'architecture du matériel, que ça serait confortable pour tout le monde si un système automatisé (compilateur ou autre) pouvait prendre ça en compte, quitte à générer un bytecode différent en fonction de la taille des différents caches par exemple…

      • [^] # Re: Accès

        Posté par  . Évalué à 4.

        Je pense que cela dépasse ce que le compilateur peut faire, tu peux avoir à réorganiser toutes tes structures de données ou à repenser ton algorithme.

        Par contre aider à analyser tes accès ce n'est pas ce que numaprof veut faire ? Je n'ai lu qu'en travers mais c'est ce que j'avais compris.

      • [^] # Re: Accès

        Posté par  (site web personnel) . Évalué à 4.

        Le compilateur n'est pas encore assez intelligent pour optimiser les accès, il peut réorganiser "un peu" les structures de données.

        Mais pour des cas "simples" comme celui : multiplication de matrice

        for (i = 0; i < N; i++) {
            for (j = 0; j < N; j++) {
                for (k = 0; k < N; k++)
                    res[i][j] += mat1[i][k]*mat2[k][j];
            }
        }

        il faut un bon outil pour voir/comprendre le problème et écrire la version optimisée.

        • [^] # Re: Accès

          Posté par  (site web personnel) . Évalué à 2.

          Il n'y a qu'à voir le temps que met la la librairie ATLAS pour tester et identifier les mécanismes de traitements matriciels les plus adaptés suivant la machine sur lequel elle est compilée…

          Python 3 - Apprendre à programmer dans l'écosystème Python → https://www.dunod.com/EAN/9782100809141

      • [^] # Re: Accès

        Posté par  (site web personnel) . Évalué à 3.

        Le compilateur ne peux rien dès qu'il s'agit d'allocation dynamique et du placement dans le temps long de l'application, seul le programmeur a la vue globale. La question du NUMA elle est très difficile à ce niveau dans l'état des connaissance. La principale source d'erreur pour le programmeur au delà du besoin de vision globale est que le noyau essai de faire de son mieux de manière transparente, donc si on ne comprends pas bien ce qu'il fait on se plante sans s'en rendre compte (d'où NUMAPROF).

        The force is in doing, not trying.

    • [^] # Re: Accès

      Posté par  (site web personnel) . Évalué à 4. Dernière modification le 03 septembre 2018 à 21:14.

      Maîtriser les allocations (nombres et tailles) est une chose, mais les gains importants en performance résident surtout sur les accès.

      Les allocations sont déjà un bon debut. Ca permet entre autres d'évaluer la fragmentation qui a aussi un impact sur les accès.

    • [^] # Re: Accès

      Posté par  (site web personnel) . Évalué à 3.

      C'est vrai pour des micros applications, mais lorsque les codes deviennent gros (millions de ligne de code) les problèmes de gestion s'accumule et il n'est pas rare de voir pointer les fonctions malloc/free dans la top liste des X fonctions les plus appelée et les plus coûteuse.

      The force is in doing, not trying.

  • # J’ai testé MALT

    Posté par  . Évalué à 3.

    Je viens d’essayer MALT sur un code de calcul de mécanique des fluides que je développe en Fortran. Je testerai NUMAPROF plus tard car pour ces tests, j’ai lancé les calculs sur ma machine de bureau.

    L’outil (MALT) semble bien pratique mais j’ai du mal à interpréter le résultat. Quand je regarde les volumes alloués, toutes les fonctions qui se démarquent sont celles d’initialisation, avant les calculs lourds. Ce doit juste être cette métrique qui n’est pas pertinente dans le cas de mon code. En terme de nombre d’allocations, là j’ai une fonction inattendue qui ressort (mais étonnamment, la ligne incriminée pointe son appel, pas l’intérieur de celle-ci). Bien qu’assez improbable vu la nature des paramètres de cette fonction, je me suis dis que ça pouvait être dû à un tableau qui serait passé en copie plutôt que par référence et ai donc tenté avec différents niveaux d’optimisation pour voir si le problème évoluait en fonction des efforts que fait GCC pour rendre mon code performant. Eh bien, je n’ai pas réussi à obtenir quelque-chose de MALT pour un niveau d’optimisation O3 ou avec « link time optimization ».

    Du coup, les conditions dans lequel je teste mon code sont assez différentes de celles dans lesquelles il tourne usuellement. C’est une limitation due au Fortran, à MALT ou à mon incompréhension de l’outil ?

    J’avoue qu’au départ, je suis mécanicien (des fluides), pas développeur, et je ne serais pas surpris d’être passé à côté d’un truc important.

    Merci pour ces outils en tout cas. Il n’y pas grand chose de tel pour étudier les codes HPC et j’ajoute avec plaisir MALT (et plus tard NUMAPROF) à ma trousse à outils.

    • [^] # Re: J’ai testé MALT

      Posté par  (site web personnel) . Évalué à 5.

      Content de savoir que vous avez testé l'outil, votre code est-il open source ? Si vous avez des problèmes et que ce n'est pas trop compilé à faire tourner je peux jeter un oeil.

      Si les fonctions d'allocation sont toutes à l'initialisation c'est parfait, c'est ce qu'il faut idéalement, donc éviter d'en avoir (sauf impossibilité ou en tout cas de manière raisonné) tout au long au milieu des calculs. Reste alors à vérifier par exemple qu'il ne s'agit pas de tas de petites allocations de ~1-8 octets (métrique min_size) nuisant aux caches ou vérifier que l'application ne fragmente pas si une partie des allocations sont libérées (requested_memory décroissante alors que la mémoire virtuelle ne suis pas cette tendance dans les graphiques temporels).

      Oui je suppose qu'il y a quelques limitations pour la détection des lignes avec -O3 si le compilo est trop agressif et notamment si vous faite du LTO qui je suppose perd la plupart des outils de debug (j'utilise l'outil addr2line pour faire la correspondance). Mais cela ne doit pas non plus trop souvent changer les résultats. Un run en O0/O1 devrait donner des résultats plus précis sans fausser tant que ça les résultats (biais uniquement sur les durées de vie des blocs, mais pas pour les autres métriques : tailles…..). De toute façon MALT a un overhead non nul donc les résultats temporels sont de toute façon entachés d'une erreur, je pense qu'il est bon de recommencer de tourner en O0/O1 sans LTO pour profiler avec MALT.

      Concernant votre problème de source d'allocation sur une ligne qui je le suppose fait du calcul. Ne serait-ce pas une ligne utilisant des allocatable array ? J'ai constaté (avec surprise) en développant l'outil que Gfortran peut générer par lui même des allocations dans ce cas. Le compilo Intel (ifort) lui ne le fait qu'au-delà d'un seuil configurable (par défaut infini). Cela est décrit dans les docs des deux compilo, si cela vous intéresse je peux essayer de retrouver les liens.

      The force is in doing, not trying.

      • [^] # Re: J’ai testé MALT

        Posté par  . Évalué à 3.

        Merci pour cette réponse détaillée !

        Je confirme que les allocations étonnantes apparaissent lors du passage d’une « allocatable array » comme paramètre d’une fonction qui attend une « assumed-shape array (intent(in)) ». Normalement, il n’y a pas de raison de faire une copie, mais il semble bien que ce soit GFortran qui en ajoute une (version 6.3.0, je devrais peut-être aussi essayer avec une plus récente). C’est pourquoi, je me disais que peut-être que cette allocation inutile disparaîtrait à un niveau d’optimisation supérieur. Je testerai avec IFort pour comparer. Le code en question n’est pas open-source (bien qu’il devrait « bientôt » le devenir).

        En tout cas, après une utilisation un peu plus longue de MALT, j’ai fait deux découvertes agréables :
        - mon code ne semble finalement pas si mal codé :-) (au moins du point de vue des allocations, les cache miss que m’indiquent callgrind sont plus difficiles à améliorer) ;
        - MALT est vraiment pratique et je m’attends à ce qu’il m’aide beaucoup sur d’autres codes de calcul (notamment un en C++ où beaucoup d’objets sont créés et détruits mais je ne sais pas a priori lesquels ont une incidence importante sur le temps de calcul).

        Merci pour l’outil. Merci aussi pour la proposition d’aide. Il se trouve que MALT est assez facile à utiliser pour que je n’en aie finalement pas besoin. Prochaine étape, tester NUMAPROF !

        • [^] # Re: J’ai testé MALT

          Posté par  . Évalué à 4. Dernière modification le 05 septembre 2018 à 10:07.

          Juste pour compléter, je ne sais pas ce qui posait problème hier, mais j’utilisais un script de compilation avec beaucoup trop d’options et j’ai du me mélanger les pinceaux. Mes essais d’aujourd’hui m’ont montré que MALT profile bien mon code avec tous les niveaux de compilation.

          Du coup, j’ai pu constater que l’option "-fstack-arrays" faisait disparaître le "malloc" étonnant. Par contre, vu ce que fait cette option, reste que la copie inutile du tableau doit encore avoir lieu, elle est juste cachée, mais ça, ce n’est pas du ressort de MALT.

      • [^] # Re: J’ai testé MALT

        Posté par  (site web personnel) . Évalué à 3.

        "Reste alors à vérifier par exemple qu'il ne s'agit pas de tas de petites allocations de ~1-8 octets (métrique min_size)"

        Il y a des gens qui font des allocations de 8 octets ?! Une si petite allocation implique l'utilisation massive de pointeurs, qui prennent de la place, qui implique beaucoup d'indirection dans la mémoire, et une utilisation des mémoires caches toutes pourris (une ligne de cache L1 est de 32 octets, cela veut dire qu'à chaque lecture, 32 octets sont transférés). Le CPU et gcc veulent des gros morceaux de mémoire (multiple de 2 ou 4 Mo, pour utiliser les 'huge' pages) avec des accès séquentiels ou avec un pattern qui peut se prédire, de 32 ou 64 octets. De préférence, il faut grouper lectures, puis écritures, pour éviter de forcer l'unité mémoire à vérifier, que l'on ne relit pas, ce que l'on est en train d'écrire, ce qui créé des cycles d'attentes.

        "La première sécurité est la liberté"

        • [^] # Re: J’ai testé MALT

          Posté par  (site web personnel) . Évalué à 2.

          Il y a des gens qui font des allocations de 8 octets ?!
          

          Et oui…. hélas…, c'est plus fréquents que ce que l'on pense et même de 1 octet !!! Chose que j'ai appris en passant MALT sur certains code et lorsque je développais un allocateur mémoire en thèse.
          D'où l'utilité de MALT pour les pointer et les régler s'il y en a trop (en général n'y en a qu'une 10e donc on s'en fiche, mais tout de même).

          Souvent cela arrrive aux travers de templates mal employés par exemaple avec un bool dans un smart pointer pour citer un des travers du C++.

          The force is in doing, not trying.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.