.. _containers:
Contêineres
===========
Uma técnica recente de abstração dos detalhes de uma solução de software é a conteinirização. Esta consiste em rodar softwares dentro de um contêiner, o qual é uma máquina virtual simplificada. Os dados a serem usados são mapeados para dentro do contêiner, e os resultados são gravados fora dele. Desta forma, o processamento é feito nessa máquina virtual simplificada, como se o contêiner fosse mais uma camada "embrulhando" o software a ser usado.
Esta abstração permite que toda a configuração do sistema operacional relacionada ao software, como dependências e bibliotecas, seja feita no contêiner (configuração que pode então ser compartilhada usando algo como **git**), enquanto que o sistema operacional do grid não precisa ser modificado para rodar o software conteinerizado.
No GridUnesp, suportamos o uso do sistema de conteinerização chamado `Apptainer `_. Para usa-lo, basta fazer o download de uma imagem (ou construí-la) via submissão de job -- pois a construção de um contêiner pode levar um tempo considerável e/ou demandar muito processamento. Depois precisa incluir esse contêiner como input no script de submissão de jobs.
Como exemplo, digamos que a usuária *Leia* quisesse realizar uma simulação com a aplicação científica *Deaph Star*, a qual tenha sido desenvolvida e testada apenas com bibliotecas da distruibuição **Ubuntu 20.04-LTS** do Linux. A usuária estaria acostumada a processar essa aplicação em seu desktop pessoal e gostaria de submeter simulações usando recursos do GridUnesp. Contudo, por saber que o cluster funciona com distribuição AlmaLinux (RHEL), a solução seria processar o job usando um contêiner.
Neste caso, *Leia* teria que criar um contêiner usando comandos do **Apptainer**. Para isso, teria que realizar o download de uma imagem do Ubuntu e adicionar os pacotes/bibliotecas requisitados na documentação da *Death Star* similarmente aos que instalaria em seu desktop pessoal. Por fim, submeteria o job ao cluster executando a aplicação científica dentro do contêiner criado.
.. attention::
Os exemplos e explicações desta página não visam abarcar todos os casos de utilização de contêineres, mas apenas fazer uma breve introdução de como usá-los no GridUnesp. A Equipe do GridUnesp estará disponível para ajudar no que for possível. Todavia, é importante ter em mente que a criação dos contêineres é de responsabilidade de cada usuária/o, sendo uma ferramenta com o intuito de propiciar ainda mais flexibilidade e autonomia às simulações e análise de dados.
Detalhes sobre criação e uso de contêineres podem ser obtidos na página oficial do `Apptainer `_.
Como fazer o download de uma imagem
-----------------------------------
Para testes, usamos o contêiner `Chlomito `_ disponibilizado na internet. Criamos um job para fazer o download e construir o contêiner (um arquivo com sufixo ".sif").
.. code-block:: bash
#!/bin/bash
#SBATCH -t 24:00:00
#SBATCH --job-name=apptainer
export INPUT="arquivo.txt"
export OUTPUT="chlomito_v1.sif"
job-nanny apptainer pull chlomito_v1.sif docker://songweidocker/chlomito:v1
Usando ``sbatch``, executamos esse script, o qual resulta no arquivo *chlomito_v1.sif*.
Como usar uma imagem
--------------------
O script abaixo dá um exemplo de como usar a imagem criada pelo Apptainer.
.. code-block:: bash
#!/bin/bash
#SBATCH -t 24:00:00
#SBATCH --job-name=apptainer
export INPUT="chlomito_v1.sif"
export OUTPUT="*"
job-nanny apptainer exec chlomito_v1.sif chlomito --help
O primeiro ponto é que usamos o ``job-nanny``, que como está documentado em :ref:`script_job_nanny`, copia os arquivos do **INPUT** para o diretório onde o job será executado. Neste caso, copia o contêiner *chlomito_v1.sif*. Em seguida, o ``job-nanny`` executa o apptainer, que, por sua vez, usa o comando ``exec chlomito_v1.sif chlomito --help``. Esse *exec* informa ao Apptainer para "exec"utar o comando *chlomito --help* dentro do contêiner *chlomito_v1.sif*. Finalmente, o comando que usamos foi ``chlomito --help`` que apenas mostra a ajuda online desse software. A depender do software usado, muitas opções de linha de comando podem ser necessárias, e elas devem ser adicionadas nesse espaço do script.
Como construir um contêiner para GPU
------------------------------------
Para usar contêineres em todo seu potencial, é muito importante poder reconfigurar e construir novos contêineres. Isto é feito, no Apptainer, editando um arquivo de configuração como este abaixo. O script, denominado de *gpu.def*, mostra exemplo de como criar um contêiner para processar aplicações com GPU.
.. code-block:: bash
Bootstrap: docker
From: ubuntu:20.04
%post
export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get install -y \
openmpi-bin \
openmpi-common \
libopenmpi-dev \
gcc \
g++ \
tzdata \
python3 \
python3-pip \
python3-dev \
build-essential \
libgl1-mesa-glx \
libglib2.0.0 \
git \
&& ln -fs /usr/share/zoneinfo/UTC /etc/localtime \
&& dpkg-reconfigure -f noninteractive tzdata
pip install --upgrade pip
pip install tensorflow
%environment
export PYTHONPATH=/usr/local/lib/python3.10/dist-packages:$PYTHONPATH
Este arquivo especifica que a imagem base a ser utilizada é a imagem **Ubuntu 20.04** do repositório `Docker Hub `_. A seção ``%post`` instala alguns pacotes no contêiner usando o gerenciador de pacotes e instala o módulo *tensorflow* do **Python 3** por meio do comando *pip*.
O seguinte script constrói o contêiner *ubuntu20gpu.sif* utilizando como input o arquivo de configuração "gpu.def":
.. code-block:: bash
#!/bin/bash
#SBATCH -t 24:00:00
#SBATCH --job-name=apptainer
export INPUT="gpu.def"
export OUTPUT="ubuntu20gpu.sif"
job-nanny apptainer build ubuntu20gpu.sif gpu.def
Uma vez concluído com sucesso o processo de construção, o arquivo *ubuntu20gpu.sif* corresponderá ao contêiner **Ubuntu 20.04** com suporte a GPUs. Para usar, basta dar os comandos ``apptainer run`` ou ``apptainer exec``:
* ``apptainer run``: Executa o comando padrão definido no contêiner (runscript)
* ``apptainer exec``: Executa um comando específico dentro do contêiner
Abaixo temos um exemplo de script para processamento com suporte a GPU usando *apptainer run*.
.. attention::
Importante observar que a opção "--nv" é necessária para suporte a GPU.
.. code-block:: bash
#!/bin/bash
#SBATCH -t 24:00:00
#SBATCH --gres=gpu:2
#SBATCH --mem=16G
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=1
module load cuda/12.9
export INPUT="ubuntu20gpu.sif script_python.py"
export OUTPUT="*"
# Processa o conteúdo do script 'script_python.py' dentro do conteiner
job-nanney apptainer run --nv ubuntu20gpu.sif script_python.py
Abaixo temos um exemplo de script para processamento com suporte a GPU usando *apptainer exec*.
.. code-block:: bash
#!/bin/bash
#SBATCH -t 24:00:00
#SBATCH --gres=gpu:2 # Solicitar 2 GPUs
#SBATCH --mem=16G
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=1
module load cuda/12.9
export INPUT="ubuntu20gpu.sif script_python.py"
export OUTPUT="*"
# Executa o comando 'python3 script_python.py' dentro do conteiner
job-nanney apptainer exec --nv ubuntu20gpu.sif python3 script_python.py
Exemplo de job MPI usando Apptainer
-----------------------------------
No GridUnesp, sugerimos utilizar a solução híbrida informada na seção `Apptainer and MPI applications `_ da página do Apptainer.
Para exemplificar, iremos executar um job com MPI e Apptainer no Slurm. Primeiramente, escrevemos um programa do tipo "Hello World" para MPI (mpitest.c):
.. code-block:: C
#include
#include
#include
#include
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int world_rank, world_size;
char hostname[256];
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
gethostname(hostname, sizeof(hostname));
printf("Hello, World, from process %d of %d on host %s\n",
world_rank, world_size, hostname);
MPI_Finalize();
return 0;
}
Para compilar esse programa, iremos criar um contêiner com **Ubuntu 22.04** e **OpenMPI 4.1.5**. Necessitaremos então de um arquivo de configuração (ubuntu-openmpi.def) para construir o contêiner. Eis um exemplo retirado da página do `Apptainer `_:
.. code-block:: bash
Bootstrap: docker
From: ubuntu:22.04
%environment
# Point to OMPI binaries, libraries, man pages
export OMPI_DIR=/opt/ompi
export PATH="$OMPI_DIR/bin:$PATH"
export LD_LIBRARY_PATH="$OMPI_DIR/lib:$LD_LIBRARY_PATH"
export MANPATH="$OMPI_DIR/share/man:$MANPATH"
%post
echo "Installing required packages..."
apt-get update && apt-get install -y wget git bash gcc gfortran g++ make file bzip2
echo "Installing Open MPI"
export OMPI_DIR=/opt/ompi
export OMPI_VERSION=4.1.5
export OMPI_URL="https://download.open-mpi.org/release/open-mpi/v4.1/openmpi-$OMPI_VERSION.tar.bz2"
mkdir -p /tmp/ompi
mkdir -p /opt
# Download
cd /tmp/ompi && wget -O openmpi-$OMPI_VERSION.tar.bz2 $OMPI_URL && tar -xjf openmpi-$OMPI_VERSION.tar.bz2
# Compile and install
cd /tmp/ompi/openmpi-$OMPI_VERSION && ./configure --prefix=$OMPI_DIR && make -j$(nproc) install
# Set env variables so we can compile our application
export PATH=$OMPI_DIR/bin:$PATH
export LD_LIBRARY_PATH=$OMPI_DIR/lib:$LD_LIBRARY_PATH
Com o exemplo de script abaixo (build_container.sh), a partir do arquivo de configuração acima (ubuntu-openmpi.def), podemos construir o contêiner (ubuntu-openmpi.sif):
.. code-block:: bash
#!/bin/bash
#SBATCH --time=24:00:00
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=4
#SBATCH --mem=8G
export INPUT="ubuntu-openmpi.def"
export OUTPUT="ubuntu-openmpi.sif"
# Construir a imagem do conteiner
echo "Construindo conteiner ubuntu-openmpi.sif..."
job-nanny apptainer build ubuntu-openmpi.sif ubuntu-openmpi.def
O programa em *.C* precisa ser compilado *dentro* do contêiner. Por isso, invocamos a linha de comando do contêiner e lá rodamos o comando para compilar o código. Mostramos abaixo um exemplo de script (compile_mpi.slurm) para compilar o programa *mpitest.c*. Na linha ``job-nanny apptainer exec $CONTAINER mpicc -o $OUTPUT_BINARY $SOURCE_FILE``, o **Apptainer** executa o comando ``mpicc`` para compilar o programa dentro do contêiner.
.. code-block:: bash
#!/bin/bash
#SBATCH --job-name=compile_mpi
#SBATCH --output=compile_mpi_%j.out
#SBATCH --error=compile_mpi_%j.err
#SBATCH --time=00:10:00
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=2
#SBATCH --mem=2G
export INPUT="ubuntu-openmpi.sif mpitest.c"
export OUTPUT="mpitest"
# Definir caminhos
CONTAINER="ubuntu-openmpi.sif"
SOURCE_FILE="mpitest.c"
OUTPUT_BINARY="mpitest"
echo "Compilando $SOURCE_FILE usando o conteiner $CONTAINER"
# Compilar o programa MPI usando o conteiner
job-nanny apptainer exec $CONTAINER mpicc -o $OUTPUT_BINARY $SOURCE_FILE
# Verificar se a compilação foi bem-sucedida
if [ -f "$OUTPUT_BINARY" ]; then
echo "Compilação bem-sucedida!"
echo "Arquivo gerado: $OUTPUT_BINARY"
# Verificar o tipo do arquivo
echo "Informações do executável:"
file $OUTPUT_BINARY
# Testar execução simples (apenas para verificar)
echo "Testando execução básica..."
apptainer exec $CONTAINER ./$OUTPUT_BINARY
else
echo "Erro na compilação!"
exit 1
fi
Após a compilação do programa *mpitest.c*, é hora de executar o executável *mpitest*. Enfim, eis um exemplo de script (run_mpi.slurm) para executar o programa.
.. code-block:: bash
#!/bin/bash
#SBATCH --job-name=run_mpi
#SBATCH --output=run_mpi_%j.out
#SBATCH --error=run_mpi_%j.err
#SBATCH --time=00:15:00
#SBATCH --nodes=2 # Número de nós
#SBATCH --ntasks-per-node=4 # Tarefas por nó
# Carregar o modulo do OpenMPI do sistema
module load openmpi/4.1.8
export INPUT="ubuntu-openmpi.sif mpitest"
export OUTPUT="*"
echo "Executando programa MPI com $SLURM_NTASKS processos"
echo "Nós utilizados: $SLURM_JOB_NODELIST"
echo "Tarefas por nó: $SLURM_NTASKS_PER_NODE"
# Executar o programa MPI de dentro do conteiner em conjunto
# com o MPI do sistema.
job-nanny mpirun -np 8 apptainer exec --sharens ubuntu-openmpi.sif ./mpitest
echo "Execução MPI concluída!"
Este job irá executar 8 processos, sendo 4 deles em cada nó. O *OpenMPI* utilizado é aquele já instalado no contêiner (ubuntu-openmpi.sif) e o executável (mpitest) é processado dentro deste contêiner. O MPI instaldo no contêiner é processado em conjunto com aquele do sistema, carregado via módulo (``module load openmpi/4.1.8``). Note que a versão do *OpenMPI* instalada no contêiner, para o exemplo acima, é a **4.1.5**, ao passo que a versão do módulo é a **4.1.8**. As versões de ambos os *OpenMPI* devem ser compatíveis, mas é recomendado que sejam as mesmas.
.. attention::
Segundo a seção `Using –sharens mode `_ da página do Apptainer, no intuito de garantir que não haja conflito relacionado a *namespace* na paralelização, recomenda-se incluir o parâmetro ``--sharens`` logo após o comando ``exec``. Assim: ``apptainer exec --sharens``.
Eis a saída do arquivo de output para o exemplo sugerido:
::
Executando programa MPI com 8 processos
Nós utilizados: node[045-046]
Tarefas por nó: 4
Hello, World from process 0 of 8 on host node045
Hello, World from process 3 of 8 on host node045
Hello, World from process 1 of 8 on host node045
Hello, World from process 2 of 8 on host node045
Hello, World from process 4 of 8 on host node046
Hello, World from process 5 of 8 on host node046
Hello, World from process 6 of 8 on host node046
Hello, World from process 7 of 8 on host node046
Execução MPI concluída!