Otimizando o Desempenho

Antes de enviar simulações para processamento de maneira definitiva, é fortemente recomendado aos usuários realizarem estudos de optimização. O objetivo é:

  1. verificar se a instalação do pacote/software/programa/aplicação/biblioteca foi feita de maneira adequada;

  2. descobrir quais o recursos míninos necessários para realizar simulações de alta performance (velocidade, desempenho, eficiência);

  3. alocar o mínimo de recursos possível para aumentar a prioridade do job na fila;

  4. obter os melhores resultados com o menor tempo possível.

Naturalmente, e dependendo da aplicação científica utilizada, o cenário utópico seria poder alocar todos os recursos do cluster. Contudo, este é um cenário impossível dado que há centenas de projetos e usários cadastros no GridUnesp solicitando os recursos, os quais são compartilhados (veja a seção Estatísticas). Assim sendo, faz-se necessária a realização de testes para verificar quais recursos demandar, e em quais quantidades.

Comparando exemplos

  • Uma simulação que aloca 4 CPUs com um limite máximo de processamento de 2 dias tem maior probabilidade de obter os recursos demandados do que uma simulação que aloca a mesma quantidade de CPUs para um prazo de 30 dias.

  • Um job que solicita 12 processos tem menor prioridade na fila se comparado a outro que solicita apenas 4.

  • Um processamento que necessita de 56 CPUs em um único nó provavelmente levará um tempo significativamente maior para obter os recursos demandados do que um job que solicita 56 CPUs espalhados em vários nós.

  • Uma aplicação científica que consegue usar várias threads pode ter um desempenho (velocidade de processamento) maior caso utilize 4 CPUs em vez de apenas 1.

  • Uma aplicação que possa usar vários processos simultaneamente pode ter um desempenho maior caso utilize 4 processos em vez de apenas 2.

  • Um programa pode ser mais eficiente se executado com 12 processos espalhados por apenas 3 worder nodes do que espalhado por 6 nodes.

Cuidado

De acordo com a maneira como o GridUnesp está configurado atualmente, algumas aplicações têm seu desempenho diminuído drasticamente caso utilize processos espalhados por vários nós. De acordo com o explicado na Seção Informações sobre o Storage, ao solicitar vários worker nodes, o job utiliza a partição /store/, a qual é um file system compartilhado entre os nós. O que significa que poderá haver um problema de latência (demora na comunicação): os nós precisam realizar uma comunicação entre si, além do tempo de transferência de dados na rede e do tempo de leitura e escrita naquela partição.

Por outro lado, utilzando processos que envolvam apenas 1 worker node, os dadas são armazenados na partição /tmp/ do nó, então a escrita e leitura é feita diretamente nesse disco, aumentando assim o desempenho do processamento.

Contudo, cada caso é um caso, cada software é um software, e precisa testar para verificar em quais casos vale a pena concentrar o processamento em apenas um worker node ou não. Em alguns casos - principalmente em períodos em que o cluster está sendo bastante demandado e o tempo de espera para disponibilização de recursos é significativo - talvez valha a pena espalhar o processamento em vários nós, ainda que a aplicação tenha sua enficiência reduzida.

Vamos supor que que um usuário chamado Spock (cujo login no GridUnesp seja spock) queira fazer estudos de optimização para verificar qual seria o cenário ideal de recursos para seus jobs. Digamos que o programa baseado em Python que ele criou consegue processar os arquivos de input em vários processos espalhados por vários nós. E como se trata de uma etapa de otimização, vamos supor que os inputs usados correspondam a 1/10 (um décimo) do tamanho original.

  1. Inicialmente, Spock construiu o script de submissão abaixo espalhando 24 processos por 12 nós e solicitando uma fila short (24 horas). Para utilizar vários nós simultaneamente, é preciso habilitar o módulo referente a memória distruída (module load intel/compilers, por exemplo). Spock aprendeu sobre memória distribuída consultando a Seção Memória Distribuída. Ao fim, ele toma nota que o tempo total de execução foi de 13 horas e 55 minutos. Porém, demorou 10 horas para o Slurm liberar os recursos.

#!/bin/bash
#SBATCH -t 24:00:00 -n 24 -N 12

export INPUT="energia.in combustivel.in dobra_4.py"
export OUTPUT="light-speed.data"

module load intel/compilers
module load anaconda3
source activate startrek

job-nanny python dobra_4.py --input=energia.in,combustivel.in
  1. Na sequência, testa o mesmo script considerando 24 processos em 8 nós. Desta vez, o tempo total foi reduzido para 11 horas e 15 minutos. O tempo de espera na fila foi de 6 horas.

#!/bin/bash
#SBATCH -t 24:00:00 -n 24 -N 8
  1. Em um terceiro teste, espalha 24 processos em 12 nós e ainda solicita 2 CPUs por processo. Agora, em vez de estar solicitando 24 CPUs no total, esse número subiu para 48 (24 processos x 2 CPUs). Ao final, anota que o tempo total foi de 6 horas e meia. Mas apesar da melhora de desempenho, teve que esperar por 25 horas na fila.

#!/bin/bash
#SBATCH -t 24:00:00 -n 24 -N 12 -c 2
  1. Por último, decidiu que solicitaria 24 processos em 1 único nó, sendo 2 CPUs por processo (totalizando 48 CPUs). Após esperar 3 dias pela disponibilização dos recursos, a simulação processou os inputs em apenas 3 horas.

#!/bin/bash
#SBATCH -t 24:00:00 -n 24 -N 1 -c 2

Spock então resumiu os resultados de desempenho em uma tabela.

Teste

Processos

Nós

CPUs por

processo

Total de

CPUS

Espera por

recursos (horas)

Tempo de

execução (horas)

1

24

12

1

24

10:00:00

13:55:00

2

24

8

1

24

06:00:00

11:15:00

3

24

12

2

48

25:00:00

06:30:00

4

24

1

2

48

72:00:00

03:00:00

Naturalmente que Spock poderia realizar novos testes, considerando outras combinações. Todavia, decidiu se ater inicialmente apenas a esses resultados. Concluiu então que o cenário (4), apesar do desempenho excelente, não valia a espera na fila. Já quanto aos cenários (1) e (3), se somasse o tempo de espera pela liberação de recursos com o tempo de execução, levaria 23h55min e 31h30min respectivamente para obter os resultados. Por fim, o teste (2) traria resultados em 17h15min, sendo o melhor desempenho dos 4 casos. Logo, decidiu que usaria -n 24 -N 8 ao usar o programa dobra_4.py para processar aquele tipo de arquivos de input.

“Quebrando” grandes inputs

É importante frisar que o GridUnesp, muito embora seja um cluster de High Performance Computing (HPC) - utilizando muitas threads para processar uma única aplicação -, também pode ser usado como um High Throughput Computing (HTC) - executando vários jobs simultâneos com partes menores de um grande arquivo de input.

Em outras palavras, para algumas aplicações científicas (não são todas), é possível “quebrar” um grande arquivo de input de 10 GB em 100 arquivos menores de 100 MB, por exemplo. Assim, seria possível submeter 100 jobs distintos (cada qual analisando um input de 100 MB), em vez de “rodar” 1 único job que analisa um input de 10 GB. É natural então concluir que o processamento de cada 1 dos 100 jobs distintos seria muito mais rápido e consumiria bem menos recursos.

Neste exemplo, um único job processando 10 GB necessitaria alocar muita memória (muitas threads). Já os 100 jobs distintos (analisando 100 MB cada) iria poder demandar uma quantidade muito menor de memória.