Sistema de filas no Ambiente HPC
Atenção: O comportamento do sistema de filas será monitorado durante os primeiros meses de operação do cluster. Durante esse período, os parâmetros do sistema poderão sofrer alguns ajustes visando aumentar a eficiência do cluster. Qualquer mudança será comunicada aos usuários.
O gerenciamento dos jobs dos usuários no cluster é feito através do SLURM Workload Manager.
O cluster é composto por 20 nós de execução (compute nodes) sendo:
- 8 nós com 40 núcleos de CPU (Intel Xeon) cada (n01...n08).
- 10 nós com 64 núcleos de CPU (AMD EPYC) cada (n09…n18).
- 1 nó com 20 núcleos de CPU (Intel Xeon) e 1 GPU Tesla K40 (gn01).
- 1 nó com 64 núcleos de CPU (AMD EPYC) e 1 GPU Tesla V100 (gn03).
As filas disponíveis no ambiente são divididas por tipo de processamento, tempo de execução, tipo de nó e tipo de CPU (Intel ou AMD). As filas são destinadas a jobs que rodam programas paralelizados, usando apenas CPU ou usando também GPU.
A tabela abaixo mostra alguns parâmetros importantes associados às filas.
¹ soma dos jobs em execução e jobs em espera.
O número máximo de núcleos alocados a cada usuário é 256, somando todos os seus jobs em execução no ambiente. Entretanto, esses núcleos são alocados respeitando os limites de cada fila.
Observação importante: Recomendamos que os seus testes sejam realizados nas filas amd_short e int_short. Nessas filas, os jobs em execução podem ocupá-la por no máximo 1 dia, o que reduzirá o seu tempo de espera. Portanto, não utilize o headnode para testes, pois a função principal dele é o gerenciamento do cluster.
Alocação de memória
O sistema de filas aloca automaticamente uma quantidade de memória RAM proporcional ao número de núcleos alocados para o seu job em cada nó do cluster. Na tabela abaixo mostramos essa quantidade de memória para cada tipo de nó.
1 Memória alocada para jobs que requisitam todos os núcleos do nó.
Não é possível solicitar para o seu job uma quantidade maior de memória RAM do que essa mostrada acima.
Muitos programas necessitam que lhes seja informada a quantidade de memória alocada para eles utilizarem, enquanto outros programas se adaptam à quantidade de memória disponível. Se o seu programa for do primeiro tipo, não se esqueça de informar corretamente a quantidade de memória alocada para o job.
Área de scratch
A área de scratch é uma área especial destinada para arquivos temporários que são utilizados com alta frequência durante a execução do job e que muitas vezes são arquivos grandes. A área de scratch é feita de forma que o acesso, leitura e escrita dos arquivos seja a mais rápida possível. Tipicamente ela está numa partição separada da partição /home onde ficam os arquivos dos usuários. Programas que escrevem muito em arquivos temporários geralmente utilizam alguma variável de ambiente ou alguma opção própria do programa para informar em qual diretório esses arquivos devem ser escritos. Não se esqueça de informar a área de scratch para esses programas, para que não haja queda de desempenho dos mesmos, que pode ser grande.
Após a utilização da área de scratch pelo seu job (vide abaixo o tópico “Exportando variáveis de ambiente), é fundamental retirar os arquivos que não precisam permanecer neste diretório. Se eles forem importantes para outros fins, faça um backup antes de removê-los dessa área. O script do exemplo 2 da seção “Scripts de Submissão” do guia do usuário faz esse backup e depois envia os arquivos para o diretório de submissão do job.
Scratch global
O cluster possui um storage dedicado e configurado com sistema de arquivos paralelo BeeGFS para escrita e leitura rápida e eficiente de dados temporários. Ele está acessível a todos os nós através de interface de alta velocidade Infiniband de 100 Gbps (exceto o nó gn01 que está conectado via rede Ethernet). Esse storage possui espaço de 97 TB e está montado em /scratch/global. Essa é a área de scratch preferencial do cluster, principalmente para jobs que utilizem mais de 1 nó (usando MPI) e jobs que realizam muita escrita e leitura em disco, visto que os discos locais de vários nós do cluster são bastante limitados.
Scratch local
Além do scratch global, cada nó possui também uma área de scratch local, montada em /scratch/local, que pode ser utilizada. A tabela abaixo mostra o espaço disponível para cada tipo de nó do cluster.
O scratch local pode ser vantajoso para jobs que fazem pouca escrita e leitura de arquivos temporários, ou quando o job utiliza o nó inteiro, e portanto não irá competir por escrita e leitura em disco com outros jobs.
Tipicamente a submissão de jobs é feita através de um arquivo texto, chamado de script, que fornece todas as informações necessárias para o gerenciador de filas e contém o roteiro dos comandos e programas a serem executados.
O job é submetido ao gerenciador de filas através do comando
sbatch <script> |
Ao submeter o job, o SLURM irá retornar o número do mesmo (JOB_ID) e todas as mensagens do job serão direcionadas para o arquivo de saída do SLURM, que tem o formato slurm-<JOB_ID>.out.
Tipicamente o arquivo de script tem a seguinte estrutura:
{Definição do shell} {Definição das opções do SLURM} {Exportação de variáveis de ambiente (se necessário)} {Carregamento dos módulos necessários} {Execução do programa} |
Opções do SLURM (#SBATCH)
O SLURM permite um grande número de opções que ajudam a gerenciar e direcionar os jobs no sistema. As principais opções são as seguintes:
#SBATCH --job-name=<nome> - Nome do job #SBATCH --partition=<fila> - Nome da fila #SBATCH --ntasks=T - Número de tarefas #SBATCH --nodes=N1,N2 - Número mínimo e máximo de nós #SBATCH --ntasks-per-node=P - Número de tarefas por nó #SBATCH --time=DD-HH:MM:SS - Tempo máximo do job #SBATCH --gres=<recurso> - Solicita um recurso especial |
Por default, cada tarefa equivale a 1 núcleo (core). Portanto iremos usar os termos tarefa e núcleo indistintamente.
Carregando os módulos necessários
Antes de executar um software, é necessário configurar o ambiente para o mesmo. Para isso utilizamos o comando
module load <módulo1> … <móduloN> |
que carrega todos os módulos necessários para o software que será usado no job. Isso significa basicamente que as variáveis de ambiente necessárias para que o executável e as bibliotecas que ele utiliza sejam encontrados e outros parâmetros necessários para o funcionamento do software sejam definidos.
Se for necessário definir variáveis extras para o software, você mesmo pode exportar essas variáveis dentro do script.
Exportando variáveis de ambiente
Se houver necessidade de definir variáveis de ambiente adicionais (que não são carregadas usando module load) para o programa que será executado no job, isso pode ser feito usando o comando export. Por exemplo
export OMP_NUM_THREADS=1 |
define a variável OMP_NUM_THREADS com valor 1 e a variável poderá ser usada dentro do script e acessada por qualquer programa executado no job. No caso de programas que escrevem muito em arquivos temporários, pode-se exportar também uma variável (por exemplo, SCRATCH_DIR) que informa ao programa que será executado onde é a área de scratch:
export SCRATCH_DIR=/scratch/local |
ou
export SCRATCH_DIR=/scratch/global |
Cada programa tem um nome próprio para essa variável. Exporte a variável correta para o seu programa.
Variáveis de ambiente do SLURM
O SLURM possui diversas variáveis de ambiente que podem ser usadas no script para passar informações relevantes para o programa que será executado, ou para imprimir informações relevantes do job no arquivo de saída do SLURM (slurm-<JOB_ID>.out). Algumas das mais relevantes são:
$SLURM_SUBMIT_DIR - Diretório de onde o job foi submetido $SLURM_JOB_NAME - Nome do job $SLURM_JOB_NODELIST - Lista dos nós alocados ao job $SLURM_NTASKS - Número de núcleos alocados ao job |
A lista completa de variáveis de ambiente do SLURM pode ser vista aqui.
Exemplos de scripts do SLURM
Exemplo de script para rodar um job paralelo que não utiliza MPI (portanto roda em apenas 1 nó) na fila int_medium (tempo máximo de 15 dias):
#!/bin/bash #SBATCH --job-name=nome_do_meu_job #SBATCH --partition=int_medium #SBATCH --ntasks=20 #SBATCH --nodes=1 module load <módulo> cd $SLURM_SUBMIT_DIR meu_programa < input > output |
Nesse exemplo, automaticamente será reservado 44000 MB (20 * 2200 MB) de memória RAM para o job.
Exemplo de script para rodar um job paralelo (usando MPI) usando 128 núcleos distribuídos em 2 nós na fila amd_medium (tempo máximo de 7 dias):
#!/bin/bash #SBATCH --job-name=nome_do_meu_job #SBATCH --partition=amd_medium #SBATCH --ntasks=128 #SBATCH --nodes=2 module load <módulo> cd $SLURM_SUBMIT_DIR mpirun -np $SLURM_NTASKS meu_programa < input > output |
Nesse exemplo, automaticamente será reservado 484 GB de memória RAM (242 GB em cada nó) para o job.
Exemplo de script para rodar um job na fila gpu_int_k40 reservando um nó inteiro para o job:
#!/bin/bash #SBATCH --job-name=nome_do_meu_job #SBATCH --partition=gpu_int_k40 #SBATCH --ntasks=20 #SBATCH --nodes=1 #SBATCH --gres=gpu:1 module load <módulo> cd $SLURM_SUBMIT_DIR meu_programa < input > output |
Exemplo de script para realizar um job de teste usando 10 núcleos em 1 nó e que durará no máximo 2 horas:
#!/bin/bash #SBATCH --job-name=job_de_teste #SBATCH --partition=int_short #SBATCH --ntasks=10 #SBATCH --time=2:00:00 module load <módulo> cd $SLURM_SUBMIT_DIR meu_programa < input > output |
Com o comando squeue é possível ver todos os jobs submetidos para a fila, em execução ou em espera. As formas mais usadas do comando são:
squeue - Ver todos os jobs squeue -u <usuário> - Ver apenas os jobs do usuário <usuário> |
Mais detalhes sobre outras opções podem ser vistos através do comando
squeue --help |
ou clicando aqui .
Com o comando scancel pode-se cancelar um job em espera ou terminar um job que está em execução:
scancel <JOB_ID> |
Para ser notificado acerca de eventos dos jobs (início, término, falhas e etc), basta inserir em seu script de submissão as seguintes diretivas:
#SBATCH --mail-type=ALL #SBATCH --mail-user=<endereço de email> |
Onde “<endereço de email>” deve ser substituído por um endereço de email de sua escolha.
Se há recursos disponíveis no ambiente para atender à solicitação de todos os jobs submetidos pelos usuários que estejam dentro dos limites impostos pelo sistema, então todos os jobs entram em execução, respeitando-se os limites. Entretanto, em geral esse não é o caso num ambiente com muitos usuários, e assim surge a necessidade de se utilizar um sistema de prioridades para classificar os jobs que ficam na fila esperando pela disponibilização de recursos.
No SLURM esse sistema de priorização é feito utilizando-se vários fatores. No nosso cluster o cálculo da prioridade do job é estabelecido da seguinte forma:
Prioridade = 2.000*FatorTemporal + 10.000*FairShare + 5.000*FatorDeFila |
Todos os fatores variam de 0 a 1. Portanto, a prioridade máxima de um job é 17.000 e a mínima é 0 (embora seja muito difícil a prioridade zerar). Vejamos o que significa cada fator desses:
Com o comando sprio pode-se ver a prioridade de todos os jobs em espera:
sprio - Ver a prioridade de todos os jobs sprio -u <usuário> - Ver apenas os jobs do usuário <usuário> sprio -p <fila> - Ver apenas os jobs da fila <fila> |
Usando o backfill para diminuir o tempo de espera do seu job
O padrão do sistema de filas é executar os jobs pela ordem de prioridade (veja fórmula acima). Mas se você puder estimar com bastante segurança o tempo máximo de execução do seu job, e ele for bem menor que o tempo limite da fila na qual o job irá rodar, utilizar a opção --time pode ajudar a encaixar seu job antes de outros com maior prioridade através do algoritmo de backfill utilizado pelo SLURM. Esse algoritmo permite que jobs de prioridade mais baixa sejam executados antes, desde que sua execução não atrapalhe nenhum job de maior prioridade, ou seja, desde que ele termine antes do tempo previsto para iniciar a execução de qualquer job de maior prioridade. Entretanto, esse recurso deve ser usado com segurança pois se o job levar mais tempo que o previsto pela opção --time, ele será terminado.
Se, por exemplo, seu job for levar mais que 1 dia e no máximo 4 dias para terminar, então ele deve ser submetido para a fila int_medium ou amd_medium, e nesse caso é vantajoso utilizar
#SBATCH --time=4-0 |
Um outro uso interessante da opção --time seria para encurtar o tempo de espera de jobs de teste. As filas int_short e amd_short tem tempo máximo de 24 horas e possuem prioridade mais alta por isso. Mas é possível utilizar o backfill para rodar jobs de teste - que sejam ainda mais curtos do que essas 24 horas - antes de jobs de prioridade mais alta na mesma fila. Por exemplo, utilize
#SBATCH --time=2:00:00 |
Com essa opção fica bem mais fácil de encaixar seu job mais rápido quando houver recursos disponíveis.