Creation d'un conteneur Singularity avec la dernière version de R et utilisation de Rmpi

Après plusieurs semaines de galère avec Rmpi sur notre cluster, voici comment j'ai procédé. Si ça ne vous intérésse pas, vous pouvez passer directement à la partie utilisation.

Creation du conteneur

L'idée est de créer un conteneur Singularity qui contient la dernière version de R.

La recette est disponible ici. Je me suis inspiré des autres recettes que j'ai déjà fait. Pour faire simple, on part d'une version d'Ubuntu sur laquelle on rajoute les dépôts pour le CRAN.

Un peu de paramétrage pour enlever les warnings sur les langues, une installation d'OpenMPI qui correspond à celle sur le cluster, j'y rajoute quelques modifications pour adapter au cluster, et nous y voilà.

Vous pouvez lancer un shell sur le conteneur pour voir son contenu ainsi :

singularity shell /share/apps/sing-images/3.8.7/R-4.2.1-openmpi-4.1.1-jre11.sif
cat /etc/os-release
> NAME="Ubuntu"
> VERSION="22.04.1 LTS (Jammy Jellyfish)"
...

mpirun --version
> mpirun (Open MPI) 4.1.1

Utilisation de MPI

Par défaut, est installée sur tous les noeuds du cluster la version 4.1.1 de OpenMPI.

Dans un conteneur singularity, tant que la version de MPI sur l'hôte correspond à celle dans le conteneur, vous n'aurez pas de soucis particulier. Pour des raisons historiques, j'ai également installé la version de OpenMPI 4.0.1 et 1.6.2, dans /share/apps/bin/openmpi/.

module add openmpi-4.0.1
which mpirun
> /share/apps/bin/openmpi/4.0.1/bin/mpirun
# ok

Pour avoir la liste des modules pour mpi :

module keyword mpi

On peut compiler un petit programme MPI en C pour vérifier que tout fonctionne correctement.

#include <stdio.h>
#include <mpi.h>

#define MAX_LEN 25

int main(int argc, char **argv) {
      int rank, size;
      FILE *fp;       //je declare un fichier qui est en fait un buffer sur une socket...
      char hostnm[MAX_LEN + 1];

      MPI_Init(&argc, &argv);
      MPI_Comm_size(MPI_COMM_WORLD, &size);
      MPI_Comm_rank(MPI_COMM_WORLD, &rank);
      fp = popen("hostname", "r");
      //while(fgets(hostnm, sizeof(hostnm-1),fp)!=NULL && hostnm!="")
      while(fscanf(fp, "%[^\n]", hostnm))
          {
          printf("Hello, world. I am %d of %d. ( %s )\n", rank, size, hostnm);
          }

        pclose(fp);
        MPI_Finalize();
        return 0;
}

Ce programme affiche simplement un "hello" depuis chaque "rank" et chaque hôte MPI.

which mpicc
mpicc helloMPI.c -o helloMPI.ompi4

Le script de soumission hellompi.sbatch ressemble à ça :

#!/bin/bash

# Name of the job in SGE
#SBATCH --job-name=hellompi_withhostfile
## Name of the queue to use
#SBATCH --partition=small
# Maximum hardware time allowed to this job
#SBATCH --time=20:00:00
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=4

#module add mpi/openmpi-x86_64
module add openmpi/4.1.1

# check available environment variables
##printenv

# cleaning previous runs
rm hostfile machinefile 2>/dev/null

scontrol show hostnames > hostfile
awk -v NTASKS_PER_NODE=$SLURM_NTASKS_PER_NODE '{print $0" slots="NTASKS_PER_NODE}' hostfile >> machinefile
mpirun -hostfile machinefile -np $SLURM_NTASKS helloMPI.ompi4 2>/dev/null

Normalement, en faisant votre sbatch hellompi.sbatch, vous devriez voir, dans votre fichier de sortie (slurm-JOBID.out), quelque chose du type :

Hello, world. I am 3 of 8. ( valkyrie-108 )
Hello, world. I am 0 of 8. ( valkyrie-108 )
Hello, world. I am 2 of 8. ( valkyrie-108 )
Hello, world. I am 1 of 8. ( valkyrie-108 )
Hello, world. I am 6 of 8. ( valkyrie-109 )
Hello, world. I am 7 of 8. ( valkyrie-109 )
Hello, world. I am 4 of 8. ( valkyrie-109 )
Hello, world. I am 5 of 8. ( valkyrie-109 )

Nous arrivons donc à faire du MPI sur le cluster sans trop de problème.

Installer Rmpi

Par défaut, j'ai choisi de ne pas installer Rmpi dans le conteneur car le chemin vers OpenMPI sera de type bind mount. Il faudra donc installer le package Rmpi dans votre $HOME :

OMPI_DIR=/share/apps/sing-images/3.8.7/
wget https://cran.r-project.org/src/contrib/Rmpi_0.7-1.tar.gz
#module add R/4.2.1-alt openmpi/4.1.1-shared
module add openmpi/4.1.1-shared
singularity exec --env-file /share/apps/bin/openmpi/4.1.1/override.env /share/apps/sing-images/3.8.7/20230530_R-4.2.1-4.1.1-jre11.sif R --slave -e "install.packages('Rmpi', repos='https://cloud.r-project.org', configure.args=c(\"--with-mpi=${OMPI_DIR}\", '--with-Rmpi-type=OPENMPI'))"

Utilisation de Rmpi

Il faut au préalable installer Rmpi dans votre $HOME qui correspond à votre version de R dans le conteneur avec le bon chemin vers MPI.

Quelques complications rencontrées Là, ça se complique. De nombreux posts sur Internet parle de ce problème ([ex.1](http://www.owsiak.org/r3-4-openmpi-3-0-0-rmpi-inside-macos-little-bit-of-mess/), [ex.2](https://stat.ethz.ch/pipermail/r-sig-hpc/2010-October/000776.html), [ex.3](https://stat.ethz.ch/pipermail/r-sig-hpc/2009-January/000070.html) ou sur [StackOverflow](https://www.google.com/search?q=rmpi+site%3Astackoverflow.com&oq=rmpi+site%3Astackoverflow.com)). Les problèmes viennent de : 1. Rscript appelé en full path depuis Rmpi, 2. Chemin vers R codé en dur au moment de l'installation dans Rscript [\*](#note1), 3. L'utilisation multi-versions de R et de OpenMPI car nous sommes sur un cluster, combinée avec `environment modules`, rendant impossible tout test interactif dans R, 4. L'utilisation de Singularity dans notre cas, qui va rajouter une couche de complexité (dont une `OverlayFS` et un `mount namespace`). [\*] Vous pouvez vérifier avec `hexdump -C /share/apps/bin/R/R-3.1.3/bin/Rscript |grep "bin/R"`

Contenu de test_rmpi.R :

id <- mpi.comm.rank(comm=0)
np <- mpi.comm.size (comm=0)
hostname <- mpi.get.processor.name()

msg <- sprintf ("Hello world from task %03d of %03d, on host %s \n", id , np , hostname)
cat(msg)

invisible(mpi.barrier(comm=0))
invisible(mpi.finalize())

La ligne d'exécution :

mpiexec singularity exec --env-file /share/apps/bin/openmpi/4.1.1/override.env /share/apps/sing-images/3.8.7/20230530_R-4.2.1-jre11.sif Rscript test_rmpi.R

Le script de soumission Rmpi_test.sbatch :

#!/bin/bash

# Name of the job in SGE
#SBATCH --job-name=Rmpi_test
## Name of the queue to use
#SBATCH --partition=small
# Maximum hardware time allowed to this job
#SBATCH --time=20:00:00
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=4
#SBATCH --cpus-per-task=1
#SBATCH --distribution=cyclic:cyclic
#SBATCH --mem-per-cpu=1gb

echo "Running example Rmpi script. Using $SLURM_JOB_NUM_NODES nodes with $SLURM_NTASKS tasks, each with $SLURM_CPUS_PER_TASK cores."

module add openmpi/4.1.1-shared

mpirun --hostfile machinefile -np $SLURM_NTASKS singularity exec --env-file /share/apps/bin/openmpi/4.1.1/override.env /share/apps/sing-images/3.8.7/20230530_R-4.2.1-jre11.sif R --slave -f test_rmpi.R

Le fichier d'erreur risque de se remplir, mais normalement, toutes les tâches se déroulent bien...

sbatch Rmpi_test.sbatch