Otimizando o Desempenho

Antes de submeter jobs para processamento definitivo, é fundamental realizar estudos de otimização. O objetivo é encontrar o equilíbrio ideal entre recursos solicitados, tempo de espera na fila e tempo de execução.

Por Que Otimizar?

  1. Verificar se a instalação do software foi bem-sucedida

  2. Descobrir os recursos mínimos necessários para boa performance

  3. Aumentar a prioridade solicitando apenas o necessário

  4. Reduzir o tempo de espera na fila

  5. Obter resultados mais rápido com uso eficiente de recursos

Fatores a Considerar

Variáveis de otimização

Fator

Impacto

Número de CPUs

Mais CPUs podem acelerar, mas nem sempre linearmente

Memória disponível

Afeta capacidade de processamento e evita swapping

Número de nós

Distribuir entre nós pode introduzir latência de comunicação

Tempo solicitado

Jobs mais curtos têm prioridade maior

Local de I/O (Leitura/Escrita) dos dados

/tmp/ é muito mais rápido que /store/

Exemplo Prático de Otimização

Vamos acompanhar o usuário Spock otimizando seu programa “dobra_4.py”.

Cenário: Programa processa arquivos de entrada e produz resultados. Spock quer encontrar a configuração ideal.

Teste 1: 24 processos em 12 nós (1 CPU por processo)

teste1.sh
#!/bin/bash
#SBATCH -t 24:00:00
#SBATCH -n 24
#SBATCH -N 12
#SBATCH -c 1

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 srun python dobra_4.py --input=energia.in,combustivel.in

Resultados:

  • Tempo de execução: 13h55min

  • Espera na fila: 10h

  • Tempo total até resultado: 23h55min

Importante

O comando srun é uma ferramenta usada no Slurm Workload Manager (um agendador de tarefas comumente usado em computadores de alto desempenho e supercomputadores). Ele é usado para solicitar recursos computacionais (como CPUs, GPUs e memória) e executar tarefas paralelas nesses recursos.

Teste 2: 24 processos em 8 nós (1 CPU por processo)

teste2.sh
#!/bin/bash
#SBATCH -t 24:00:00
#SBATCH -n 24
#SBATCH -N 8
#SBATCH -c 1

Resultados:

  • Tempo de execução: 11h15min

  • Espera na fila: 6h

  • Tempo total até resultado: 17h15min ✓ Melhor até agora

Teste 3: 24 processos em 12 nós (2 CPUs por processo)

teste3.sh
#!/bin/bash
#SBATCH -t 24:00:00
#SBATCH -n 24
#SBATCH -N 12
#SBATCH -c 2      # 2 CPUs por processo = 48 CPUs totais

Resultados:

  • Tempo de execução: 6h30min (mais rápido!)

  • Espera na fila: 25h (muito mais espera)

  • Tempo total até resultado: 31h30min

Teste 4: 24 processos em 1 nó (2 CPUs por processo)

teste4.sh
#!/bin/bash
#SBATCH -t 24:00:00
#SBATCH -n 24
#SBATCH -N 1
#SBATCH -c 2      # 2 CPUs por processo = 48 CPUs totais

Resultados:

  • Tempo de execução: 3h (excelente!)

  • Espera na fila: 72h (muito longa)

  • Tempo total até resultado: 75h

Tabela Comparativa

Resultados dos testes

Teste

Processos

Nós

CPUs/Proc

Total CPUs

Espera

Execução

Tempo total

1

24

12

1

24

10:00

13:55

23:55

2

24

8

1

24

06:00

11:15

17:15

3

24

12

2

48

25:00

06:30

31:30

4

24

1

2

48

72:00

03:00

75:00

Conclusão de Spock: O Teste 2 oferece o melhor equilíbrio entre espera e execução (17h15min totais).

Estratégias de Otimização

  1. Comece com recursos mínimos

    # Teste inicial com poucos recursos
    #SBATCH -n 1
    #SBATCH -t 01:00:00
    #SBATCH --mem=2G
    
  2. Aumente gradualmente

    # Próximo teste
    #SBATCH -n 4
    #SBATCH -t 02:00:00
    #SBATCH --mem=8G
    
  3. Monitore o uso real

    # Após o job terminar
    sacct -j JOBID --format=JobID,MaxRSS,Elapsed,CPUTime
    
  4. Ajuste baseado nos dados

    • Se MaxRSS é muito menor que a memória solicitada, reduza a memória

    • Se Elapsed é muito menor que a tempo solicitado, reduza o tempo

    • Se CPU time é muito maior que Elapsed, há paralelização ineficiente

HTC: “Quebrando” Grandes Inputs (HTC)

O GridUnesp pode ser usado de duas formas:

  • HPC (High Performance Computing): Muitas threads para um único job

  • HTC (High Throughput Computing): Muitos jobs pequenos processando partes de um grande problema

Exemplo de HTC:

Em vez de 1 job processando um arquivo de 10 GB:

# Único job grande
./processar dados_completos_10GB.dat

você pode dividir em 100 jobs processando arquivos de 100 MB cada:

job_array_htc.sh
#!/bin/bash
#SBATCH --array=1-100
#SBATCH -n 1
#SBATCH -t 01:00:00
#SBATCH --mem=2G

export INPUT="dados_parte_${SLURM_ARRAY_TASK_ID}.dat"
export OUTPUT="resultado_parte_${SLURM_ARRAY_TASK_ID}.dat"

job-nanny ./processar $INPUT > $OUTPUT

Vantagens do HTC:

  • Cada job pequeno começa mais rápido

  • Melhor aproveitamento de recursos ociosos

  • Menor impacto de falhas (apenas poucos sub-jobs são perdidos)

  • Escalabilidade quase linear

Trade-offs entre /tmp/ e /store/

Comparação de performance

Configuração

Local de I/O

Velocidade

Quando usar

1 nó, sem flags

/tmp/

⚡⚡⚡ Muito rápida

I/O intensivo, dados <100GB

1 nó, LARGE_FILES="true"

/store/

⚡ Média

Dados >100GB, 1 nó

Múltiplos nós

/store/

⚡ Média

Jobs MPI, dados compartilhados

Recomendação:

  1. Sempre que possível, use 1 nó e /tmp/ para I/O intensivo

  2. Para dados muito grandes (>100GB), use export LARGE_FILES="true" no script de submissão

  3. Para jobs MPI, aceite a latência do /store/ em troca da escalabilidade

Checklist de Otimização

Antes de submeter seu job definitivo:

  • [__] Testei com uma amostra pequena dos dados

  • [__] Monitorei o uso real com sacct

  • [__] Ajustei memória para o valor real (com margem de segurança)

  • [__] Ajustei tempo para o valor real (com margem de segurança)

  • [__] Escolhi o número ideal de CPUs/processos

  • [__] Decidi entre 1 nó (/tmp/) ou múltiplos nós (/store/)

  • [__] Considerei dividir em Job Array (HTC)

  • [__] Verifiquei o Fair Share do meu grupo (Política de Prioridade)

Ver também