Forum Programmation.autre Allocation de mémoire NUMA dans un code parallèle (threads)

Posté par  .
Étiquettes : aucune
5
18
avr.
2012

Bonjour à tous,

Je sais que parmi vous se cachent des habitués du calcul haute-performance qui pourront m’éclairer sur l’architecture NUMA.

Voilà, j’ai là un petit code de simulation parallélisé avec OpenMP et que je compte faire tourner sur un cluster SMP. Avant de changer le code trop en profondeur pour aller dans quelque direction que ce soit, je me pose quelques questions auxquelles je ne parviens pas à trouver de réponse dans le grand Ternet.

  • Si chaque thread alloue sa propre mémoire, le système d’exploitation lui octroiera-t-il par défaut de la mémoire proche de lui sans explicitement passer par une allocation prenant explicitement en compte l’architecture NUMA ?
  • Sinon, cela revient-il au même que de laisser un thread faire toutes les allocations mémoires puisque de toutes façons les autres threads pourront venir taper dedans ? Dans ce deuxième cas, vaut il quand-même mieux allouer des morceaux de mémoire séparés auxquels accéderont les différents threads, ou une seule allocation d’un très gros tableau produit-elle le même résultat ?

Je ne sais pas si ma question est assez claire, je dois avouer n’être pas du tout expert en architecture matérielle, et je n’ai pas de personne compétente accessible facilement. En tout cas, si vous avez des pistes, je serais ravi d’écouter vos conseils car je n’aurai pas vraiment l’occasion de jouer avec le cluster de calcul pour tâtonner et me faire ma propre expérience.

Merci !

  • # NUMA

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

    Bonjour,
    En fait cela peut dépendre des options de compilations et des variables d'envrionnement par rapport à l'allocation NUMA. Il existe par exemple, avec le compilateur Intel la variable d'envrionnement KMP_AFFINITY qui permet de demander de la mémoire par rapport aux bancs numa et donc de ne pas utiliser les mémoires éloignées (numactl -H pour avoir une idée des distances). Après il faut tester pur voir la réaction des options parce que j'ai certains codes qui ont un gain important en liant aux bancs et d'autres où ça s'effondre…

    Je ne connais pas l'équivalent avec le compilateur GNU (faut dire j'ai pas cherché). Bon courage !

  • # First touch

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

    Par défaut, il me semble que quand tu fais un malloc, la zone mémoire n'est pas vraiment allouée tout de suite. Lors du premier accès au buffer (t[i] = 0; par exemple), le système alloue la mémoire sur le banc mémoire qui a fait l'appel (c'est le "First Touch").

    Une manière de contrôler où sont allouées les données est d'utiliser hwloc. La doc concernant la partie mémoire est ici : http://www.open-mpi.org/projects/hwloc/doc/v1.4.1/a00050.php

    • [^] # Re: First touch

      Posté par  . Évalué à 1.

      En fait, on peut choisir (jusqu'à un certain point). Mot clés utiles: NUMA memory binding policy.

      La page de man est utile. Il y a aussi des outils comme numactl qui peuvent servir, selon le contexte.

      • [^] # Re: First touch

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

        Ceci dit, de manière général, il vaut mieux se méfier de ce genre d'optimisation.

        Si je ne m'abuse, quand on utilise openMP, c'est souvent parce qu'on ne souhaite pas se plonger dans la complexité d'une parallélisation tout à fait explicite (avec des pthreads ou MPI par exemple). La démarche est donc, globalement, de faire confiance à l'outil automatique openMP + compilo + OS. Se préoccuper de l'allocation physique de la mémoire, est une démarche un peu à l'opposé. Puisqu'il s'agit de ne même pas faire confiances aux mécanismes usuelles de l'OS.

        En tout état de cause, il est possible que votre question soit totalement pertinente dans votre cas particulier. Mais on ne saurait trop vous recommander des tests de performance extensifs ; histoire d'éviter de vous échiner sur un faut problème, pour ensuite vous éreinter à corriger des complexités inutilement ajoutées.

        Pour étayer mon point de vue, un exemple : dans le noyau, les gens avaient pris l'habitude d'ajouter des instructions spécifiquement destinées à mettre en cache processeur certaines données qui seraient utilisée prochainement. Et l'habitude à vécu de nombreuses années. Jusqu'à ce qu'un jour quelqu'un prenne la peine de se rendre compte que ces instructions, au mieux n'accéléraient rien et qu'elles provoquaient souvent des ralentissements (pour les références conférer l'une des dépêches noyaux de Patrick_G dont le numéro ne me revient pas).

        « IRAFURORBREVISESTANIMUMREGEQUINISIPARETIMPERAT » — Odes — Horace

        • [^] # Re: First touch

          Posté par  . Évalué à 3.

          Merci du conseil, c’est un peu le genre de remarque que j’attendais. Cependant, si je me pose la question, ce n’est pas vraiment pour gérer moi-même la mémoire avec des fonctions prenant en compte NUMA, c’est juste pour choisir la méthode de parallélisation la plus efficace, sans vraiment sortir des clous d’OpenMP.

          À la lecture de l’ensemble des réponses, il semble que le meilleur compromis soit de ne pas me préoccuper de NUMA directement mais d’allouer un tableau de donnée par thread (façon MPI), en laissant au système d’exploitation le soin de gérer la proximité entre le stockage de ce tableau en mémoire et le thread associé. Un seul gros tableau ne pouvant être proche de tous les threads à la fois, il se retrouvera forcément loin d’un certain nombre d’entre eux. Au besoin, s’assurer que mon code accède bien la première fois à chaque espace mémoire depuis le bon thread (pour tenir compte du « first touch ») n’est pas bien compliqué.

          on ne saurait trop vous recommander des tests de performance extensifs ; histoire d'éviter de vous échiner sur un faux problème

          Effectuer de bons tests risque de me prendre autant de temps que de découper mes tableaux, j’en ferai mais ça ne me coûte pas grand chose d’envisager la méthode la plus efficace en amont de ces tests.

          Merci à tous pour vos réponses utiles !

          • [^] # Re: First touch

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

            En fait, je ne suis pas certain qu'il soit vraiment utile « d'allouer » des tableaux par filaments. En général, j'aurais tendance à me contenter d'une seule allocation et de partager ensuite la mémoire. C'est ce qui se fait de plus simple avec openMP. Cette approche présente l'avantage de la simplicité et d'un nombre réduit d'appels systèmes.

            Comme signaler plus haut, la mémoire n'est physiquement allouée qu'au moment de son premier remplissage. Du coup on peut s'attendre à ce que les données se retrouvent spontanément proches du cœur qui les traitera. À condition toutefois de bien contrôler le découpage en filament de openMP (il me semble qu'on imagine aisément qu'openMP fait N filaments pour traiter un segment, alors qu'en réalité il en fait au maximum N simultanément et qu'en permanence des filaments naissent vivent et meurent dans l'indifférence générale ;-).

            « IRAFURORBREVISESTANIMUMREGEQUINISIPARETIMPERAT » — Odes — Horace

            • [^] # Re: First touch

              Posté par  . Évalué à 2.

              (il me semble qu'on imagine aisément qu'openMP fait N filaments pour traiter un segment, alors qu'en réalité il en fait au maximum N simultanément et qu'en permanence des filaments naissent vivent et meurent dans l'indifférence générale ;-)

              Non. D’ailleurs dans le code que j’utilise, les filaments [0] sont créés une fois et synchronisés (le plus rarement possible) à coup de « barrier ». C’est à peu près ce que fait automatiquement le compilateur avec l’ordonnanceur « static », mais :

              • le code est plus simple : moins encombré de directives OpenMP;
              • plus facile à comprendre : synchronisation explicite là où elle est nécessaire, plutôt que de se poser toujours la question de l’usage ou non de la règle « nowait »;
              • et plus souple : on peut décider de faire un partage déséquilibré du calcul entre filaments si l’on sait à l’avance que certains traitement seront plus longs que d’autres, sans pour autant compliquer le code avec un ordonnancement dynamique.

              [0] Je ne connaissais pas cette traduction, elle me plaît bien, j’espère qu’elle est assez utilisée pour pouvoir l’adopter définitivement en restant compréhensible.

              Au final, le résultat obtenu ressemble assez à une parallélisation MPI (avec le double avantage de la mémoire partagée et que le code peut toujours se compiler en séquentiel pour déboguer), c’est pourquoi la découpe des tableaux n’aurait pas été si difficile. Suivant vos conseils, je vais quand même me l’épargner si elle n’apporte rien.

              • [^] # Re: First touch

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

                Pour filament, je suis certain de cette francisation et elle correspond vraiment bien dans tous les contextes ; à ceci près que d'aucuns sont si coutumiers des anglicismes qu'ils ont souvent du mal à le comprendre.

                Pour revenir à votre question, et pour être plus sûr de ne pas vous induire en erreur, je vous conseil juste de garder l'implémentation naïve aux côtés d'une version optimisée éventuelle afin de pouvoir confirmer qu'il y a bien amélioration.

                Bon travail.

                « IRAFURORBREVISESTANIMUMREGEQUINISIPARETIMPERAT » — Odes — Horace

                • [^] # Re: First touch

                  Posté par  . Évalué à 1. Dernière modification le 19 avril 2012 à 20:19.

                  je vous conseille juste de garder l'implémentation naïve

                  Ça, git le fait pour moi :-)

                  Merci de vos retours.

  • # allocs

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

    les allocations NUMA c'est pour financer les barbecues des cocos ?

Suivre le flux des commentaires

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