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.

 
Estrutura de filas e parâmetros básicos
 

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.

 

Fila

Tempo máximo de execução do job (dias)

Número máximo de núcleos alocados por usuário

Número máximo de jobs em execução por usuário

Número máximo de jobs por usuário1

Nós utilizados pela fila

int_short

1

80

8

10

n[01-08]

int_medium

15

40

4

6

int_large

30

20

2

4

amd_short

1

256

8

10

n[09-18]

amd_medium

7

128

4

6

amd_large

15

64

2

4

gpu_int_k40

15

20

1

2

gn01

gpu_amd_v100

15

64

1

2

gn03

all_gpus

15

104

1

2

gn01, gn03

¹ 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ó.

Nós

Filas associadas

Memória RAM alocada por núcleo

Memória RAM total disponível no nó1

n[01-08]

int_short, int_medium, int_large

2200 MB

85 GB

n[09-18]

amd_short, amd_medium, amd_large

3875 MB

242 GB

gn01

gpu_int_k40,

all_gpus

6000 MB

117 GB

gn03

gpu_amd_v100,

all_gpus

3870 MB

241 GB

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.

Nós

Espaço em disco local

Configuração

n[01-08]

4,4 TB

2 discos em RAID0

n[09-18] e gn03

1,9 TB

1 único disco

gn01

560 GB

2 discos em RAID0

 

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.

Como submeter um job

 

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.

 
  • A opção --job-name define o nome do job, o que é bastante útil para identificá-lo na lista de jobs.
  • A opção --partition define a fila onde o job vai rodar. Se essa opção não for definida, será usada a fila padrão, que é a fila int_short. Por isso, é boa prática sempre especificar a fila.
  • A opção --ntasks especifica o número total de núcleos que devem ser alocados para o job. Se essa opção for usada sozinha, o SLURM irá distribuir por conta própria esses núcleos entre os nós disponíveis, tipicamente usando o maior número de nós possível. Para programas que não usam MPI, deve-se usar junto com --nodes=1.
  • A opção --nodes especifica o número mínimo e máximo de nós que serão utilizados pelo job. O SLURM irá decidir o número apropriado de nós dentro dessa faixa. Se apenas um número for usado ele especificará o número exato de nós a serem utilizados pelo job. Se for usado --ntasks=T e --nodes=N1,N2, o SLURM irá decidir a melhor forma de distribuir os T núcleos em N1 até N2 nós.
  • A opção --ntasks-per-node especifica quantos núcleos devem ser alocados em cada nó. Deve-se usar --ntasks-per-node=P juntamente com --nodes=N, de modo que o número total de núcleos alocados ao job será o produto N * P. Se for utilizada também a opção --ntasks , então --ntasks-per-node especifica o número máximo de núcleos alocados por nó.
  • A opção --time especifica o tempo máximo do job. Esse tempo não pode ultrapassar o tempo máximo da fila, caso contrário o job ficará em estado de espera indefinidamente. Como as filas já possuem um tempo máximo definido, essa opção deve ser usada apenas quando o usuário puder estimar com bastante segurança o tempo máximo necessário para o job e esse tempo for significativamente menor que o tempo máximo da fila onde irá rodar. Nessas condições, essa opção pode ser interessante para diminuir o tempo de espera do job, por causa do agendamento de backfill do SLURM (veja abaixo).

  • A opção --gres solicita um recurso especial definido no ambiente. No nosso cluster, o único recurso especial definido é a placa de GPU existente nos nós gn01 e gn03. Quando for utilizar a GPU desses nós, utilize a opção --gres=gpu:1. A GPU não é compartilhável, portanto ao ser alocada a um job, nenhum outro job pode utilizá-la.

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

 

Como monitorar ou cancelar jobs

 

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.

Como funciona o agendamento de jobs

 

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:

  • O Fator Temporal é dado pelo tempo que o job está esperando na fila desde que se tornou elegível para entrar em execução. Ele atinge o valor máximo com 96 horas de espera e permanece nesse valor até que o job entre em execução.
  • O Fair Share representa uma espécie de compensação que reduz a prioridade dos usuários que mais usaram os recursos do cluster durante um determinado intervalo de tempo mais recente. Ele inicia com valor 1 e decresce de acordo com o uso dos recursos feito por cada usuário, tendo limite mínimo em 0. A contabilização do uso é multiplicada por um fator que decresce com o tempo, tendo meia-vida de 30 dias. Se imaginarmos os recursos do cluster divididos entre os grupos (projetos) e, dentro de cada grupo, dividido entre os usuários, o Fair Share será maior que 0,5 se o usuário consumiu menos do que a sua quota de uso e será menor que 0,5 se o usuário consumiu mais do que a sua quota de uso. É importante notar que o Fair Share de um usuário também depende do consumo de recursos dos outros usuários do mesmo grupo, uma vez que o algoritmo de Fair Share calibra os valores levando em conta a quota de uso do grupo. Mais detalhes sobre o algoritmo de fair share, podem ser vistos aqui .
  • O Fator de Fila é igual a 1 para as filas int_short e amd_short e 0 para as demais filas. Isso é feito para priorizar os jobs curtos que vão ocupar os recursos do cluster por pouco tempo.

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.