Turing : fragmentation mémoire

Symptômes

Des allocations mémoire peuvent échouer alors qu'il y a suffisamment de mémoire disponible.

Cause

Le système d'exploitation tournant sur les noeuds de calcul de Turing (appellé CNK pour Compute Node Kernel) est très simple pour venir perturber le moins possible les processus s'exécutant sur ,a BlUe Fene/Q et pour être le plus rapide possible.

Le gestionnaire mémoire du CNK est malheureusement très basique et n'est pas capable de gérer le phénomène de la fragmentation mémoire.

La mémoire peut se fragmenter lorsqu'une application fait des allocations et désallocations dynamiques répétées. Par exemple, si vous allouez un certain nombre de petits tableaux en remplissant la mémoire, puis que vous en désallouez certains, des trous vont apparaître. Si vous tentez ensuite de faire une nouvelle allocation, le système d'exploitation va essayer de trouver un emplacement libre suffisamment grand pour y mettre votre nouveau tableau. Si il n'en trouve pas (car votre tableau est plus grand que le plus grand trou), il ne pourra pas l'allouer même si l'espace total disponible est largement suffisant. Un système d'exploitation plus avancé est capable d'allouer des blocs non-contigus en mémoire ou de la défragmenter en déplaçant des blocs. L'approche suivie pour la gestion de la mémoire sur Blue Gene/Q permet d'avoir des allocations/désallocations très rapides mais au détriment de sa souplesse.

Conseils

Voici quelques petits conseils pour essayer d'écrire une application qui ne sera pas impactée par de la fragmentation mémoire :

  • Allouer les tableaux de travail une seule fois si possible au début de l'application.
  • Eviter les allocations/désallocations multiples et répétées.
  • Privilégier des allocations/désallocations de tableaux de taille fixe au cours de l'exécution de votre application.

Si vous ne parvenez pas à vous débarasser de ce phénomène, essayez de gérer vous même une partie de l'espace mémoire. Cela peut être fait en allouant un gros espace et en en distribuant des morceaux en cours d'exécution selon vos besoins. Attention, cette approche peut s'avérer très complexe.

Exemple

Voici un petit programme en C qui illustre le problème de la fragmentation mémoire :

#include <stdlib.h>
 
#define NBLOCKS 40
#define MB 1024*1024
 
int main(int argc,char **argv)
{
  int     i;
  size_t  size;
  char   *smallblocks[NBLOCKS], *bigblock;
 
  // Allocate 'small' blocks
  // With 1 rank-per-node, it will use all memory (16GB)
  size = 400*MB;
  for (i=0;i<NBLOCKS;i++)
  {
    smallblocks[i] = (char *) calloc(size, sizeof(char));
    if(smallblocks[i]==NULL)
    {
      printf(''Allocation of smallblocks[%i] failed! '',i);
      break;
    }
  }
 
  // Deallocate 1 smallblock every 2
  // After deallocation, we should have NBLOCKS/2
  // free blocks of 400MB
  for (i=0;i<NBLOCKS;i+=2) free(smallblocks[i]);
 
  // Allocate big block
  // Total freespace should be at least 20*400MB=8000MB
  size = 600*MB;
  bigblock = (char *) calloc(size, sizeof(char));
  if(bigblock==NULL)
  {
    printf(''Allocation of bigblock failed!n'');
    return EXIT_FAILURE;
  }
 
  return EXIT_SUCCESS;
}

Si nous l'exécutons avec --ranks-per-node 1 sur un seul processus, nous obtenons lors de l'exécution les sorties suivantes :

runjob --ranks-per-node 1 --np 1 : ./a.out
  
Allocation of bigblock failed!
2012-12-19 01:31:25.961 (WARN ) [0xfff68ea8b10] :6919:ibm.runjob.client.Job: normal termination with status 1 from rank 0

L'allocation d'un bloc de mémoire de 600 Mo a échoué alors qu'il y avait au moins 8000 Mo de libre.