Skip to content

Conteneurs avec Singularity

Introduction

La conteneurisation des logiciels est devenue très populaire, particulièrement via l'utilisation de Docker. Néanmoins, Docker ne peut pas fonctionner dans un environnement HPC pour des raisons de sécurité. Il existe plusieurs alternatives à Docker qui s'accordent parfaitement avec l'environnement HPC, et Singularity est celle qui couvre le plus grand nombre d'usages (dont par exemple l'importation de conteneurs Docker).

Singularity est un logiciel open-source qui permet d'exécuter des conteneurs, autrement dit qui permet la virtualisation au niveau système d'exploitation. L'un de ses principaux usages est d'apporter la conteneurisation et la reproductibilité à l'informatique scientifique et au monde HPC (offrant donc la possibilité à chaque utilisateur de gérer ses environnements reproductibles).

Pourquoi Singularity ?

Il reste néanmoins des différences importantes entre Singularity et Docker :

  • Docker et Singularity ont leur propre format de conteneur ;
  • les conteneurs Docker peuvent être importés par Singularity ;
  • les conteneurs Docker nécessitent des droits administrateur pour fonctionner, ce qui n'est pas possible sur un cluster de calcul partagé ;
  • Singularity permet de travailler avec des conteneurs avec les droits utilisateur classique.

Avec Singularity, l'utilisateur est autonome :

  • il gère ses images, de simples fichiers .sif dans son répertoire home ;
  • il peut récupérer des images à partir du docker hub, du singularity hub, du Sylabs container library ou d'autres sources ;
  • il lance les conteneurs au sein d'un job sur le cluster.

Sur le cluster

Singularity est déployé à la fois sur les noeuds de calcul et sur les frontaux. Cela permet aux utilisateurs de préparer le chargement des images à partir des frontaux, et d'utiliser le temps alloué sur les noeuds pour le calcul.

[user@hpclogin01 ~]$ singularity --version
singularity version 3.7.3-1.el7

Récupérer une image

Vous pouvez envoyer dans votre répertoire personnel vos images Singularity avec un scp par exemple. Vous pouvez également utiliser la commande pull de Singularity pour télécharger des images pré-construites à partir de différentes sources.

Par exemple, à partir du Singularity hub :

[user@hpclogin01 ~]$ singularity pull shub://vsoch/hello-world
INFO:    Downloading shub image
59.8MiB / 59.8MiB [================================] 100 % 20.9 MiB/s 0s

[user@hpclogin01 ~]$ ls
hello-world_latest.sif
L'image a été téléchargée dans le fichier hello-world_latest.sif ; on peut également préciser le nom de destination :
[user@hpclogin01 ~]$ singularity pull --name hello.sif shub://vsoch/hello-world
INFO:    Downloading shub image
59.8MiB / 59.8MiB [================================] 100 % 20.9 MiB/s 0s

[user@hpclogin01 ~]$ ls
hello.sif

Ou à partir du Docker hub :

[user@hpclogin01 ~]$ singularity pull docker://godlovedc/lolcow
INFO:    Converting OCI blobs to SIF format
INFO:    Starting build...
Getting image source signatures
  [...]
Writing manifest to image destination
Storing signatures
2021/05/20 11:33:07  info unpack layer: sha256:8e860504ff1ee5dc7953672d128ce1e4aa4d8e3716eb39fe710b849c64b20945
INFO:    Creating SIF file...

[user@hpclogin01 ~]$ ls
lolcow_latest.sif

Pour plus d'informations, consultez la documentation.

Note 1 : cache Singularity

Singularity gère un cache d'images localement dans votre répertoire home (~/.singularity/cache), pour permettre de ne pas re-télécharger inutilement des images qui n'auraient pas changé. Ce cache peut vite prendre beaucoup d'espace. Vous pouvez le vider avec singularity cache clean.

Note 2 : espace temporaire

Lorsque Singularity assemble une image à partir des morceaux qu'il récupère sur les sources externes (Docker hub, Singularity hub, Sylabs Container Library, ...), il peut avoir besoin d'une grande quantité d'espace disque temporairement, qu'il va par défaut aller chercher dans /tmp. Cependant, sur les frontaux et sur les noeuds, cet espace /tmp est plutôt petit (4Go maximum). Il est conseillé de changer le répertoire utilisé par défaut pour les fichiers temporaires en faisant par exemple :

$ TMPDIR=/storage/scratch/$LOGNAME/tmp
$ mkdir -p $TMPDIR
$ export TMPDIR
$ singularity pull ...

Travailler avec les images

Supposons que l'on dispose déjà d'une image nommée hello.sif récupérée comme précédemment.

Shell

Avec la commande shell, on peut démarrer un nouveau shell à l'intérieur de l'image du conteneur et interagir avec, comme si c'était une petite machine virtuelle :

[user@hpclogin01 ~]$ singularity shell hello.sif
Singularity> pwd
/home/user

Singularity> ls
hello.sif

Singularity> cat /etc/os-release
NAME="Ubuntu"
VERSION="14.04.6 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.6 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

On remarque bien que la commande cat /etc/os-release est exécutée au sein du conteneur, étant donné que les serveurs du cluster utilisent CentOS et non Ubuntu 14.04.6. Cela nous apprend également que le conteneur avec lequel on travaille pour l'exemple a été construit à partir d'Ubuntu 14.04.6.

Commandes dans un conteneur

La commande exec permet d'exécuter directement des programmes au sein du conteneur. Par exemple :

[user@hpclogin01 ~]$ singularity exec hello.sif cat /etc/os-release
NAME="Ubuntu"
VERSION="14.04.6 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.6 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

Exécution d'un conteneur

Une image Singularity contient un script par défaut qui peut être lancé avec la commande run :

[user@hpclogin01 ~]$ singularity run hello.sif
RaawwWWWWWRRRR!! Avocado!

Au sein de l'image, ce script par défaut est /singularity :

[user@hpclogin01 ~]$ singularity shell hello.sif
Singularity> cat /singularity
#!/bin/sh
exec /bin/bash /rawr.sh

Singularity> cat /rawr.sh
#!/bin/bash
echo "RaawwWWWWWRRRR!! Avocado!"

Accès aux fichiers depuis un conteneur

Lorsqu'on se trouve à l'intérieur d'une exécution d'un conteneur, tous les fichiers et chemins ne sont par défaut pas accessibles. Exemple :

[user@hpclogin01 ~]$ echo "Hello" > /storage/scratch/hellofile.txt
[user@hpclogin01 ~]$ cat /storage/scratch/hellofile.txt
Hello

[user@hpclogin01 ~]$ singularity shell hello.sif
Singularity> ls /storage/scratch/
ls: cannot access /storage/scratch/: No such file or directory

Il suffit d'indiquer à Singularity que l'on souhaite rendre disponibles certains chemins au sein du conteneur lors de son exécution avec l'option --bind :

[user@hpclogin01 ~]$ singularity shell --bind /storage:/storage hello.sif
Singularity> cat /storage/scratch/hellofile.txt
Hello

On peut également définir une variable d'environnement SINGULARITY_BINDPATH qui a le même effet :

[user@hpclogin01 ~]$ export SINGULARITY_BINDPATH="/storage"
[user@hpclogin01 ~]$ singularity shell hello.sif
Singularity> cat /storage/scratch/hellofile.txt
Hello

Votre répertoire home est déjà accessible par défaut au sein du conteneur, avec le même chemin.

Jobs sur le cluster avec Singularity

Bien peu de contraintes sont liées à l'utilisation de Singularity sur le cluster au sein d'un job.

Dans un job interactif

Dans un job interactif, on peut par exemple faire comme suit :

[user@hpclogin01 ~]$ srun --partition=debug --ntasks=1 --cpus-per-task=4 --mem=8G --pty bash

[user@hpcphi01 ~]$ singularity run hello.sif
RaawwWWWWWRRRR!! Avocado!

[user@hpcphi01 ~]$ singularity exec hello.sif bash
Singularity> echo $SLURM_JOB_ID
37166694

Singularity> echo $SLURM_CPUS_ON_NODE
4

Dans un job classique

Voici un exemple de job lançant un conteneur avec Singularity :

#!/bin/bash
#SBATCH --job-name=singularity-example
#SBATCH --partition=normal
#SBATCH --ntasks=1
#SBATCH --nodes=1
#SBATCH --cpus-per-task=8
#SBATCH --mem=32g
#SBATCH --mail-type=ALL
#SBATCH --mail-user=user.email@uca.fr
#SBATCH --time=06:00:00

# Prepare working directory on node
WORKDIR=/storage/scratch/$LOGNAME/$SLURM_JOB_ID
TMPDIR=$WORKDIR/tmp
INPUTDIR=$WORKDIR/input
mkdir -p $WORKDIR
mkdir -p $TMPDIR
mkdir -p $INPUTDIR

# Tell programs to use $TMPDIR instead of /tmp/ when creating temporary files
# (/tmp/ is small, /storage/scratch/ offers more space)
export TMPDIR

# Get input data 
INPUTFILE=$INPUTDIR/genome.fasta
wget "http://biodatabanks.example/files/genome.fasta" -O $INPUTFILE
...

# Launch a tool in the Singularity container
singularity exec --bind /storage:/storage my_workflow_image.sif bash /workflow.sh --input=$INPUTFILE --workdir=$WORKDIR --threads=$SLURM_CPUS_ON_NODE

# Get output results
cp $WORKDIR/output_results.txt ~/

# Clean-up
rm -rf $WORKDIR

# end of script

Dans un job avec GPU

Rappel : accès GPU

L'accès aux files GPU sur le cluster se fait sur demande auprès du Mésocentre.

On commence par préparer l'image qui nous intéresse :

[user@hpclogin01 ~]$ singularity build tf-py3.simg docker://tensorflow/tensorflow:1.14.0-gpu-py3
INFO:    Starting build...
Getting image source signatures
[...]
INFO:    Creating SIF file...
INFO:    Build complete: tf-py3.simg

On demande ensuite une allocation GPU au cluster et on lance notre test avec Singularity :

[user@hpclogin01 ~]$ srun --partition=gpu --ntasks=1 --cpus-per-task=2 --mem=4G --gres=gpu:1 --pty bash

[user@hpcgpu01 ~]$ singularity exec --nv tf-py3.simg python -c 'import tensorflow as tf; print("GPU available = ", tf.test.is_gpu_available())'
2021-05-20 14:20:29.742655: I tensorflow/core/platform/cpu_feature_guard.cc:142] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
2021-05-20 14:20:29.773686: I tensorflow/stream_executor/platform/default/dso_loader.cc:42] Successfully opened dynamic library libcuda.so.1
2021-05-20 14:20:31.006915: I tensorflow/compiler/xla/service/service.cc:168] XLA service 0x3bf4780 executing computations on platform CUDA. Devices:
2021-05-20 14:20:31.006949: I tensorflow/compiler/xla/service/service.cc:175]   StreamExecutor device (0): Tesla V100-SXM2-32GB, Compute Capability 7.0
[...]
2021-05-20 14:20:31.625934: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1763] Adding visible gpu devices: 0
2021-05-20 14:20:31.628651: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1181] Device interconnect StreamExecutor with strength 1 edge matrix:
2021-05-20 14:20:31.628687: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1187]      0
2021-05-20 14:20:31.628700: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1200] 0:   N
2021-05-20 14:20:31.633252: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1326] Created TensorFlow device (/device:GPU:0 with 30591 MB memory) -> physical GPU (device: 0, name: Tesla V100-SXM2-32GB, pci bus id: 0000:0b:00.0, compute capability: 7.0)
GPU available =  True

Les bibliothèques CUDA chargées dans cet exemple sont intégrées dans le conteneur, il n'y a donc pas besoin de faire de module load cuda/11.2.152 par exemple.

En revanche, le lecteur attentif aura bien noté le paramètre --nv passé à Singularity qui permet l'accès aux GPU. Le conteneur n'a bien entendu accès qu'aux GPU alloués dans le cadre du job, ce que l'on peut vérifier avec la commande nvidia-smi dans le conteneur :

[user@hpcgpu01 ~]$ singularity exec --nv tf-py3.simg nvidia-smi
Thu May 20 14:31:18 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.32.03    Driver Version: 460.32.03    CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  Off  | 00000000:0B:00.0 Off |                    0 |
| N/A   33C    P0    55W / 300W |      0MiB / 32510MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

Enfin, pour un job non interactif avec GPU, on peut utiliser par exemple :

#!/bin/bash
#SBATCH --job-name=singularity-gpu-example
#SBATCH --partition=gpu
#SBATCH --ntasks=1
#SBATCH --nodes=1
#SBATCH --cpus-per-task=2
#SBATCH --mem=8g
#SBATCH --mail-type=ALL
#SBATCH --mail-user=user.email@uca.fr
#SBATCH --time=09:00:00
#SBATCH --gres=gpu:1

# Prepare working directory on node
WORKDIR=/storage/scratch/$LOGNAME/$SLURM_JOB_ID
TMPDIR=$WORKDIR/tmp
INPUTDIR=$WORKDIR/input
mkdir -p $WORKDIR
mkdir -p $TMPDIR
mkdir -p $INPUTDIR

# Tell programs to use $TMPDIR instead of /tmp/ when creating temporary files
# (/tmp/ is small, /storage/scratch/ offers more space)
export TMPDIR

# Get input data 
INPUTFILE=$INPUTDIR/genome.fasta
wget "http://biodatabanks.example/files/genome.fasta" -O $INPUTFILE
...

# Launch a tool using GPU in the Singularity container
singularity exec --nv --bind /storage:/storage tensorflow_workflow_image.sif bash /workflow.sh --input=$INPUTFILE --workdir=$WORKDIR --threads=$SLURM_CPUS_ON_NODE

# Get output results
cp $WORKDIR/output_results.txt ~/

# Clean-up
rm -rf $WORKDIR

# end of script