Annex IX: Containers

En Finis Terrae II es posible la ejecución de contenedores de software a través de las aplicaciones uDocker y Singularity. Un contenedor de software consiste en un conjunto formado por la aplicación, sus dependencias, librerías y archivos de configuración que se puede ejecutar sobre el anfitrión. A diferencia de una máquina virtual, un contenedor se ejecutará sobre la máquina anfitrión compartiendo el kernel con él. Por lo tanto su tamaño será mucho menor que el de una máquina virtual y su ejecución y despliegue será más rápido.

uDocker utiliza el formato de imágenes de Docker que se pueden obtener, por ejemplo, desde *DockerHub*. Esta posibilidad permite la ejecución de aplicaciones que de manera nativa no podrían ejecutarse debido a incompatibilidades o ausencia de librerías en la máquina anfitrión. Entre otras funcionalidades, uDocker permite mapear directorios de la máquina anfitrión para que sean accesibles desde el contenedor.

Singularity también es compatible con los contenedores de Docker, pero Singularity trabaja con su propio formato y, por lo general, es necesaria una conversión previa. Tanto la creación como la conversión de contenedores debe hacerse con privilegios de superusuario, por lo que NO se puede en Finis Terrae II. El proceso habitual será la creación del contenedor en la máquina del usuario y su posterior copiado a su cuenta de Finis Terrae II. Singularity también provee de herramientas para el almacenamiento y descarga de contenedores públicos y privados como son: *SingularityHub* y *Sylabs Cloud*.

Uso de uDocker en Finis Terrae II

uDocker se encuentra instalado a disposición de todos los usuarios y se puede cargar como un módulo,

$ module load udocker/1.1.3-python-2.7.15

Una vez cargado, podemos consultar la ayuda general de la aplicación con el comando help. Los comandos son similares a los de Docker

$ udocker help

Por defecto, cuando se carga el módulo, se apunta al repositorio de imágenes gestionado por el CESGA. Este repositorio está en una ruta del sistema que no tiene permisos de escritura para los usuarios regulares, así que las imágenes que se encuentran en él sólo se pueden utilizar, pero no modificar. Más adelante veremos cómo crear un repositorio de imágenes en alguno de nuestros sistemas de archivos, lo cual nos permitirá descargar imágenes, almacenarlas y crear contenedores de uso personal.

Para listar las imágenes disponibles en el repositorio del CESGA a partir de las cuales podríamos crear un contenedor:

$ udocker images

Por ejemplo, en este momento están disponibles las siguientes:

REPOSITORY

feelpp/feelpp-toolboxes:latest .

openporousmedia/opmreleases:latest .

quay.io/fenicsproject/stable:1.6.0 .

tensorflow/tensorflow:latest-gpu .

tensorflow/tensorflow:latest .

victorsndvg/salome-v7.8.0:nvidia .

Para consultar los contenedores disponibles en el repositorio del CESGA, usamos el comando ps

$ udocker ps

El cual nos devolverá una lista con los contenedores que podemos ejecutar por defecto,

** CONTAINER ID P M NAMES IMAGE**

39d36fd9-8175-39d5-a620-d23614f8bb06 . R [‘tensorflow-0121-gpu’] tensorflow/tensorflow:latest-gpu

4a5d9cd9-8682-35db-8459-903a49510d39 . R [‘opm-2017_04’]openporousmedia/opmreleases:latest

4fa3d9f1-6198-30e4-8681-cbc00b904d25 . R [‘tf2’] tensorflow/tensorflow:0.12.0-rc0

54253cfe-5bce-3b0a-8e87-ac31db761a84 . R [‘tf-gpu’] tensorflow/tensorflow:latest-gpu

5be91b67-15eb-3962-9193-47cfdfdc6e4f . R [‘salome-7_8_0’] victorsndvg/salome-v7.8.0:nvidia

71a214cd-3de9-348f-a660-bfbe12c2a943 . R [‘tf2-gpu’] tensorflow/tensorflow:latest-gpu

73c09df7-2c4e-3395-a8f3-09a9a755c9f6 . R [‘fenics-1_6_0’] quay.io/fenicsproject/stable:1.6.0

b697b467-22dd-3afb-a8d4-2ccbc36207a6 . R [‘feelpp-toolboxes’] feelpp/feelpp-toolboxes:latest

ce6ea3b4-7a3e-3b49-a6db-cb55f64575d0 . R [‘tf’] tensorflow/tensorflow:latest

Creación de un repositorio local de imágenes

Como mencionamos anteriormente, cuando se carga el módulo udocker, por defecto apunta a un repositorio de imágenes y contenedores gestionado por el CESGA. Dado que existe la posibilidad de importar imágenes del docker-hub y crear contenedores a partir de ellas, se hace necesario crear un repositorio en alguno de nuestros sistemas de archivos, bien sea el $HOME, $STORE, $LUSTRE, o alguno otro que tengamos disponible.

$ echo $UDOCKER_DIR

Para crear un repositorio local se utiliza el comando mkrepo, por ejemplo, si queremos situarlo en nuestro directorio $HOME en la carpeta “repo_local” haríamos lo siguiente,

$ udocker mkrepo $HOME/repo_local

Sin embargo,** puede ser interesante utilizar $STORE como lugar donde colocar nuestro repositorio privado**, debido a que cada contenedor genera una gran cantidad de ficheros y puede llegar a llenar la cuota del $HOME con rapidez.

Una vez creado el repositorio, debemos apuntar a él, ya que por defecto udocker apunta al del CESGA,

$ export UDOCKER_DIR=$HOME/repo_local

O en el caso de utilizar el filesystem $STORE,

$ export UDOCKER_DIR=$STORE/repo_local

Es importante resaltar que no podemos apuntar simultáneamente a dos repositorios, debemos seleccionar uno de ellos únicamente.

Ejecución de contenedores

La ejecución de un contenedor por defecto se hace con el comando run

$ udocker run [options] <container-id-or-name>

Por ejemplo, si quisiéramos ejecutar el contenedor cuyo nombre es **‘tf2-gpu’ **podríamos hacerlo de dos formas distintas, a través de su “ID” o a través de su nombre,

$ udocker run 71a214cd-3de9-348f-a660-bfbe12c2a943

$ udocker run tf2-gpu

La ejecución de los anteriores comandos provocará que se despliegue el contenedor en el cual estaremos logueados como usuario root, esto es útil para realizar instalaciones dentro de él.

El siguiente ejemplo permitiría ejecutar una shell de bash en el contenedor tf2 como usuario cesga_user y montar los tres principales sistemas de archivos de Finis Terrae II ($HOME, $LUSTRE Y $STORE),

$ udocker run –user=<usuario> –volume=$HOME –volume=$LUSTRE –volume=$STORE / –workdir=$HOME tf2 /bin/bash

Para cada comando, podemos consultar la ayuda específica acompañando con la opción –help, por ejemplo, para el comando “run”,

$ udocker run –help *****run: execute a container run [options] <container-id-or-name>*

*[…]*

Pull de imágenes y creación de contenedores

Una vez creado el repositorio local de imágenes, es posible escribir en él y almacenar allí las imágenes que podemos obtener del Docker-hub. Por ejemplo, podemos buscar en imágenes de fedora,

$ udocker search fedora

$ udocker pull fedora

Una vez hecho esto podemos crear contenedores a partir de esta imagen utilizando el comando create, para el cual es recomendable utilizar la opción –name para asociarle un nombre identificativo,

$ udocker create –name=primer_contenedor fedora

Pull desde un registry

Es posible utilizar un registry distinto al DockerHub, por ejemplo, para hacer un pull de un RH7 desde el de Red Hat:

$ udocker pull –registry=https://registry.access.redhat.com rhel7

A partir de este podríamos crear nuestro contenedor de Red Hat 7

$ udocker create –name=rh7 rhel7

$ udocker run rh7

Principales operaciones del comando udocker

A continuación mostramos una lista de los principales comandos de udocker

Syntax:

** udocker <command> [command_options] <command_args>**

Donde <command> puede ser:

  • **search **Busca en el Docker Hub imágenes de contenedores

  • **pull **Hace un pull de una imagen desde el repositorio de docker, que por defecto es el dockerhub. Las siguientes opciones están disponibles

    • **–index=url **Especifica un index diferente a index.docker.io

    • **–registry=url **Especifica un registro distinto a

      resgistry-1.docker.io

    • **–httpproxy=proxy **

  • create **Crea un contenedor desde una imagen del repositorio local. Se recomiendata utilizar la opción **–name para asignarle un nombre fácil de recordar.

  • **run **Ejecuta un contenedor. En el siguiente apartado veremos un **punto importante sobre los diferentes modos de ejecución. **Ya que esto afectará al rendimiento de posibles aplicaciones.

  • **images **Lista las imágenes disponibles en el repositorio local.

  • ps

Lista los contenedores creados.

  • rm

****Elimina un contenedor previamente creado a partir de una imagen.

  • rmi

Elimina una imagen de un contenedor previamente descargada o importada. Con la opción **-f **se fuerza la eliminación aunque ocurran errores durante el borrado

  • inspect

****Muestra la información sobre los metadatos de un contenedor, acepta también como input el ID de la imagen de la que proviene.

  • import

Importa un tarball desde un archivo. Puede ser utilizado para importar un contenedor que ha sido exportado utilizando **docker export **creando una nueva imagen en el repositorio local.

    • Con la opción –tocontainer se crea el contenedor sin crear

      una imagen intermedia

    • Con la opción –clone se importa un contenedor creado con

      udocker (udocker export –clone) sin crear una imagen intermedia.

  • load

**Carga en el repositorio local un tarball que contiene una imagen de Docker, es equivalente a hacer un pull desde el Docker Hub, pero en este caso se carga la imagen desde un archivo. Es la opción que utilizaríamos para cargar una imagen guardada con **docker save.

  • protect/unprotect

****Protege una imagen o un contenedor de borrados accidentales

  • mkrepo

****Crea un repositorio local en el directorio especificado DIRECTORY, por defecto, cuando se carga el módulo utiliza una ruta protegida contra escritura que debe ser modificada para crear un repositorio privado de imágenes y contenedores.

  • login

****Para hacer login en un Docker registry. Solo soporta autentificación con username y password.

  • setup

**Esta opción es de **suma importancia para ejecución de aplicaciones multihilo. Debido a limitaciones en la funcionalidades de la versión del sistema operativo disponible actualmente en el Finis Terrae II, determinadas aplicaciones que utilizan varios hilos de computación pueden ver su rendimiento afectado, en cuyo caso ha de modificarse el modo de ejecución del contenedor mediante la opción setup. La forma de modificación es la siguiente,

**$ udocker setup –execmode=<modo> CONTAINER-ID | CONTAINER-NAME **

Donde el modo recomendado para la ejecución de aplicaciones multihilo en caso de rendimiento degradado sería F2, a continuación ponemos una lista de modos recomendados,

|image22|

  • clone

Esta opción duplica un contenedor creando una réplica completamente exacta, pero recibiendo un CONTAINER-ID diferente. Es recomendable renombrarla utilizando la opción –name=NAME

Y como mencionamos anteriormente, cada comando tiene su ayuda específica añadiendo la opción “–help”.

Envío a cola utilizando contenedores de uDocker

En esta sección veremos cómo realizar una ejecución mediante el uso de contenedores utilizando el sistema de colas para enviar el contenedor a un nodo de cálculo. Utilizando el siguiente script de ejemplo podemos hacernos una idea de cómo sería el script que arranca el contenedor cuando el trabajo entra en ejecución.

En el siguiente ejemplo vemos un trabajo que solicita un nodo y 24 tareas, un tiempo de ejecución de 5 minutos y ser enviado a la partición thinnodes, en el que desplegará el contenedor “rh7” y ejecutará el comando “hostname”

#!/bin/bash

#SBATCH -N 1

#SBATCH -n 24 #(24 tareas en total)

#SBATCH -t 00:05:00 #( 5 min ejecucion )

#SBATCH -p thinnodes

module load udocker/1.1.3-python-2.7.15

# Si queremos usar nuestro repo local apuntaremos a la dirección correcta

export UDOCKER_DIR=$STORE/repo_local

udocker run –hostenv –hostauth –user=<usuario> -v /mnt -v /tmp -v=$HOME –workdir=$HOME rh7 hostname

Si tenemos la necesidad de ejecutar una serie de comandos lo más adecuado es crear un script con ellos en un directorio que montemos cuando se ejecute el contenedor y ejecutarlo de la siguiente manera:

udocker run –hostenv –hostauth –user=<usuario> -v /mnt -v /tmp -v=$HOME –workdir=$HOME rh7 /bin/bash comandos.sh

Donde comandos.sh será un script con aquello que queremos ejecutar a través del contenedor, por ejemplo,

# !/bin/bash

echo $PWD >> /home/cesga/$USER/prueba_$USER

ls -l >> /home/cesga/$USER/prueba_$USER

cat /etc/os-release >> /home/cesga/$USER/prueba_$USER

hostname >> /home/cesga/$USER/prueba_$USER

Y simplemente enviaríamos el script que llama a uDocker con sbatch.

Envío de trabajos MPI

Es posible realizar ejecuciones con Open MPI y udocker utilizando Slurm en Finis Terrae II. Cada uno de los procesos MPI será un contenedor. Este mecanismo es ejecutado por el mpiexec del anfitrión y estos contenedores serán capaces de comunicarse a través de la interfaz Infiniband del anfitrión.

Los requisitos para poder realizar estas ejecuciones son; el código en el contenedor ha de ser compilado con la misma versión del MPI disponible en el anfitrión. Esto es un requisito indispensable para que la ejecución funcione, por lo tanto, ha de descargarse dentro del contenedor la versión de Open MPI que se vaya a utilizar en Finis Terrae II y ser compilada dentro.

A mayores, los paquetes openib y libibvers tienen que ser instaladas para poder compilar Open MPI sobre infiniband. Una descripción completa de este procedimiento puede ser consultado en el apartado 4 “Running MPI Jobs” de la *documentación oficial*.

Uso de Singularity en Finis Terrae II

Singularity está accesible para los usuarios desde el sistema de módulos del Finis Terrae II:

$ module load singularity/3.1.1

Para comenzar a trabajar con Singularity, el primer paso es consultar la ayuda que proporciona la aplicación:

$ singularity –help

También puedes encontrar la respuestas a muchas preguntas frecuentes en la sección *FAQ* de su *página oficial*. Además, encontrarás información más detallada en sección *Guía de usuarios* de esa página.

A diferencia de Docker o uDocker, la aplicación Singularity no gestiona un repositorio de imágenes. Cada imagen es un único fichero que se almacena en el sistema de ficheros del anfitrión. Las imágenes de Singularity, al contener sistemas operativos completos, pueden llegar a ser grandes, por lo que recomendamos almacenarlas en $STORE, en donde tendrás una cuota de espacio mayor que en $HOME.

Obtener y ejecutar contenedores de Singularity

Para obtener imágenes desde el registro oficial de Singularity se utiliza el comando “pull”. La referencia a las imágenes sigue la siguiente sintaxis “URI://Collection/Imagen:tag”, en el que “shub://” es la URI que referencia al registro oficial de Singularity. Además mediante la opción “–name” se puede especificar la ruta y el nombre de la archivo destino en el sistema de ficheros local. Singularity se basa en los principios de portabilidad y reproducibilidad, por lo que, por defecto, las imágenes de Singularity son inmutables, archivos que contienen un sistema de ficheros *SquashFS* comprimido y de solo lectura.

$ singularity pull –name openmpi-ring-1.10.simg shub://MSO4SC/Singularity:ring_1.10.7

Para ejecutar aplicaciones dentro de un contenedor Singularity se utilizan los comandos “run”, “exec” o “test”. Tanto “run” como “test” ejecutan aplicaciones predefinidas en el momento de creación de la imagen. El comando “exec” permite ejecutar un comando personalizado dentro del contenedor. Las variables de entorno del anfitrión están por defecto accesibles desde el contenedor.

$ singularity run openmpi-ring-1.10.simg

$ singularity test openmpi-ring-1.10.simg

$ singularity exec openmpi-ring-1.10.simg /usr/bin/ring

Al ejecutar comandos desde un contenedor es importante discernir qué pasa dentro o fuera del contenedor. Fíjate que a la hora de escribir datos utilizas directorios del anfitrión (no del contenedor) en los que tienes permisos. Por defecto, desde un contenedor Singularity tienes acceso a los directorios $HOME, /tmp y /var/tmp **del anfitrión. Para tener acceso a todos tus directorios en el Finis Terrae II, lo más sencillo es indicarle al contenedor que utilice **/mnt del anfitrión mediante la opción “-B”, así tendrás acceso a los volúmenes a los que accedes normalmente desde tu cuenta de usuario del Finis Terrae II ($LUSTRE, $STORE, etc.). La opción “-B” permite montar cualquier cualquier directorio del anfitrión en el contenedor, pero ten en cuenta que *la ruta de destino debe existir dentro del contenedor*.

$ singularity exec -B /mnt openmpi-ring-1.10.simg /usr/bin/ring

$ singularity exec -B /mnt openmpi-ring-1.10.simg ls $STORE

Además, puedes utilizar el comando “shell” de Singularity para interaccionar con el contenedor de forma interactiva. Cuando ejecutas este comando te devuelve un nuevo prompt y cualquier comando que escribas se ejecutará dentro del contenedor. Por defecto utiliza una shell Bourne (sh o /bin/sh). Para salir del contenedor y volver al nodo de login simplemente escribe “exit”. Al ser interactivo, el comando “shell” no está indicado para ser utilizado en trabajos enviados a cola.

**$ singularity shell -B /mnt openmpi-ring-1.10.simg **

Envío a cola utilizando contenedores Singularity

Para enviar trabajos a cola utilizando Singularity debes recordar la carga del módulo correspondiente. El envío de trabajos secuenciales o con hilos no tienen ninguna particularidad. Puedes crear un script SBATCH como habitualmente para describir la ejecución de tus trabajos con Singularity. Puedes ver un ejemplo a continuación:

#!/bin/bash

#SBATCH -p thin-shared

#SBATCH -n 1

#SBATCH -N 1

#SBATCH -t 00:01:00

module load singularity/3.1.1

singularity exec openmpi-ring-1.10.simg ring

También se pueden ejecutar aplicaciones MPI con contenedores Singularity. Existe una restricción que impone que la librería de MPI dentro del contenedor debe coincidir con la del anfitrión (en fabricante y versión) para asegurar el funcionamiento. En el caso de OpenMPI puedes consultar la versión del contenedor con un comando como el siguiente:

$ singularity exec ring_ompi_1.10.simg ompi_info | grep “Open MPI”

Una vez te asegures de que coincide con alguna versión del anfitrión y de cargarla mediante el sistema de módulos, podrás enviar tus trabajos MPI como habitualmente. En el caso de trabajos que reserven una gran cantidad de nodos puede ser interesante almacenar tu imagen en $LUSTRE para obtener mejores ratios de transferencia. Ten en cuenta que OpenMPI necesita tener acceso al directorio /scratch para gestionar algunos archivos temporales y que el contenedor puede no contener ese directorio. En ese caso debemos indicar otro, como por ejemplo /tmp que se utiliza en el siguiente ejemplo:

#!/bin/bash

#SBATCH -p thinnodes

#SBATCH -n 48

#SBATCH -N 2

#SBATCH -t 00:01:00

module load gcc/6.4.0 openmpi/1.10.7 singularity/3.1.1

mpirun -np 2 -mca orte_tmpdir_base /tmp singularity exec ring_ompi_1.10.simg ring

Para ejecutar tus aplicaciones GPU dentro de un contenedor, se puede utilizar la opción “–nv” a los comandos de ejecución de Singularity. Esta es una opción experimental que permite utilizar los drivers de NVIDIA dentro del contenedor. Para que esto funcione correctamente, el software del contenedor debe haber debe haber sido compilada con soporte GPU de NVIDIA y *debe contener el entorno de NVIDIA* (“**nvidia-smi*”)*. A continuación se muestra un script de ejemplo.

#!/bin/bash

#SBATCH -p gpu-shared-k2

#SBATCH –gres=gpu

#SBATCH -n 1

#SBATCH -N 1

#SBATCH -t 00:01:00

module load singularity/3.1.1

singularity exec –nv IMAGEN COMANDO

Enlazar Singularity con MPI nativo

Puede conseguirse un rendimiento muy próximo al nativo si se enlaza el MPI nativo del FinisTerrae-II con el contenedor. Esto resulta especialmente sencillo en máquinas basadas en CentOS, por su gran similitud con RedHat (usado en el FinisTerrae-II). En este caso bastaría con cargar los módulos oportunos, tal que:

module load singularity/3.1.1

module load gcc openmpi/2.1.1

A continuación, deben montarse los directorios oportunos en el contenedor. Esto puede realizarse de varias maneras, que serán más o menos cómodas según tengamos configurado el contenedor. Los directorios podrían montarse directamente en su ruta equivalente en el contexto del contenedor. Sin embargo, en ocasiones esto no será posible debido a que ya hay contenido en esos directorios que no se desea comprometer. Una solución intermedia es montar los directorios nativos y enlazar sus contenidos en los directorios originales tal que:

singularity exec -B /mnt -B /opt/cesga -B /usr/lib64:/usr/lib64_cesga -B /usr/bin:/usr/bin_cesga -B /etc/libibverbs.d CONTENEDOR COMANDO

Después, dentro del contenedor, bastaría con realizar enlaces simbólicos tal que:

ln -s /usr/lib64_cesga/* /usr/lib64/ 2>/dev/null

ln -s /usr/bin_cesga/* /usr/bin/ 2>/dev/null

Una de las ventajas del FinisTerrae-II es su sistema de carga dinámica de módulos basado en Lmod que, al hacerlo accesible para nuestro contenedor, nos permite configurar el contexto de MPI con un sencillo comando lanzado dentro del contenedor:

module load gcc openmpi/2.1.1

A partir de este momento, tanto las llamadas a mpicc como las llamadas a mpirun quedarían enlazadas con el MPI nativo del FinisTerrae-II, de manera que es posible compilar y ejecutar incluso aunque en el contenedor no se encuentre instalado MPI.

Puede consultarse un ejemplo de como se ha realizado esto con un sencillo hola mundo de prueba para MPI:

  1. Script de encolamiento (lanzado desde el FT2)

*https://github.com/albertoesmp/ft2-containers/blob/master/examples/singularity/demo_mpi.sh*

  1. Script de comandos (lanzado desde el contenedor)

*https://github.com/albertoesmp/ft2-containers/blob/master/examples/singularity/demo_mpi_commands.sh*

  1. Código fuente MPI (compilado y ejecutado desde el contenedor con el MPI del FT2)

*https://github.com/albertoesmp/ft2-containers/blob/master/examples/singularity/hello_mpi.c*

Es importante remarcar que en caso de contenedores basados en otras distribuciones como Debian, el proceso será más complicado, ya que habrá que enlazar las librerías y binarios en una estructura de directorios y variables de entorno que pueden variar sustancialmente.

Ejemplos de uso de contenedores

Pueden encontrarse distintos ejemplos de uso para uDocker y Singularity en el siguiente repositorio:

/opt/cesga/job-scripts-examples

*https://github.com/albertoesmp/ft2-containers/tree/master/examples*