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 :

module load singularity-3.1
singularity shell /share/apps/sing-images/3.1/basic-r-Singularity3.5.sif
cat /etc/os-release
> NAME="Ubuntu"
> VERSION="16.04.6 LTS (Xenial Xerus)"
...

Note: pour l'installation de Singularity 3.1 sur le cluster (sous CentOS 6 actuellement) depuis les sources, vous pouvez lire cet article.

Utilisation de MPI avec Singularity

Tant que la version de MPI sur l'hôte correspond à celle dans le conteneur, vous n'aurez pas de soucis particulier. Pour cette raison, j'ai installé la dernière version de OpenMPI (actuellement la 4.0.1), dans /share/apps/bin/openmpi/4.0.1. La compilation ne pose pas de problème particulier.

J'ai rajouté un modulefiles pour faciliter son utilisation. Ce dernier est incompatible avec le module chargé par défaut rocks-openmpi. Il faut donc enlever ce dernier avant de charger le nouveau module openmpi.

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

On va, au préalable rajouter quelques options pour OpenMPI :

mkdir ~/.openmpi
echo 'OMPI_MCA_btl="^openib,sm"' >> ~/.openmpi/mca-params.conf
echo 'OMPI_MCA_plm_rsh_args="-p 12345"' >> ~/.openmpi/mca-params.conf
# normalement, ça suffit, mais il se peut que vous ayez besoin également de la ligne suivante
echo 'OMPI_MCA_btl_tcp_if_include="em1"' >> ~/.openmpi/mca-params.conf

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.

mpicc helloMPI.c -o helloMPI.ompi4

Le script de soumission ressemble à ça :

#!/bin/bash
#$ -S /bin/bash
#$ -pe mpi 10
#$ -N hellompi_cemeb
#$ -cwd
#$ -q cemeb.q
#$ -l h_rt=00:10:00

module rm rocks-openmpi
module load singularity-3.1
module load openmpi-4.0.1

mpirun -np $NSLOTS singularity exec /share/apps/sing-images/3.1/basic-r-3.5.sif ./helloMPI.ompi4

Normalement, en faisant votre qsub, vous devriez voir, dans votre fichier de sortie, quelque chose du type :

Hello, world. I am 9 of 10. ( compute-0-16.local )
Hello, world. I am 0 of 10. ( compute-0-22.local )
Hello, world. I am 1 of 10. ( compute-0-22.local )
Hello, world. I am 4 of 10. ( compute-0-22.local )
Hello, world. I am 5 of 10. ( compute-0-22.local )
Hello, world. I am 2 of 10. ( compute-0-22.local )
Hello, world. I am 7 of 10. ( compute-0-22.local )
Hello, world. I am 6 of 10. ( compute-0-22.local )
Hello, world. I am 3 of 10. ( compute-0-22.local )
Hello, world. I am 8 of 10. ( compute-0-16.local )

Nous arrivons donc à faire du MPI dans singularity, 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.

Voici donc comment faire :

module load R-3.5.3
wget https://cran.r-project.org/src/contrib/Rmpi_0.6-9.tar.gz
OPENMPI=/share/apps/bin/openmpi/4.0.1
R CMD INSTALL --configure-args="--with-Rmpi-include=$OPENMPI/include   --with-Rmpi-libpath=$OPENMPI/lib --with-Rmpi-type='OPENMPI' " Rmpi_0.6-9.tar.gz

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.

Là, ça se complique. De nombreux posts sur Internet parle de ce problème (ex.1, ex.2, ex.3 ou sur StackOverflow).

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 *,
  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"

En fait, j'ai trouvé la solution sur un post du NIH (chapitre R MPI jobs).

Jusqu'à présent, je soumettais les jobs avec la ligne

mpirun -np 1 singularity run /share/apps/sing-images/3.1/basic-r-3.5.sif --vanilla --slave --args $PE_HOSTFILE < test_rmpi.R

C'est en effet ce qui est recommandé par la documentation de Rmpi. En effet, la répartition des jobs est gérée directement par Rmpi et non MPI directement. Cependant, la commande précédente ne fonctionne pas réellement sur du multi-hôte dans notre cas. J'ai simplement changé en :

mpiexec singularity exec /share/apps/sing-images/3.1/basic-r-3.5.sif Rscript test_rmpi.R

Le script de soumission devient donc :

#!/bin/bash
# Shell to use
#$ -S /bin/bash
# Name of the job in SGE
#$ -N testremy
# Name of the queue to use -q
# Maximum hardware time allowed for this job
#$ -l h_rt=02:00:00
# run in the current directory
#$ -cwd
#$ -q cemeb.q
# Utiliser l'environnement parallele mpi avec 10 slots
#$ -pe mpi 10

module rm rocks-openmpi
module load singularity-3.1
module load openmpi-4.0.1


mpiexec singularity exec /share/apps/sing-images/3.1/basic-r-3.5.sif Rscript test_rmpi.R

Avec test_rmpi.R qui contient :

library(Rmpi)


cl <- makeMPIcluster(mpi.universe.size() - 1)

#setDefaultClusterOptions("homogeneous"=F)
#setDefaultClusterOptions("rscript"="/usr/local/bin/Rscript")

r <- clusterEvalQ(cl, R.version.string)
print(unlist(r))
stopCluster(cl)
mpi.quit()
remy@cluster-mbb:~/tests_Rmpi$ cat test_rmpi2.R
library(Rmpi)
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())

Le fichier d'erreur risque de se remplir, mais normalement, toutes les tâches se sont quand même bien déroulées...