.. _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!