Cluster Franky¶
Para ter acesso ao Cluster Franky você precisa configurar suas credenciais de acesso e realizar acesso remoto via SSH.
As chaves foram enviadas para o seu email Insper, Faça o download da pasta completa, que contém os arquivos id_rsa (chave privada) e id_rsa.pub (chave pública), salve essas chaves em algum lugar que você não vai esquecer, depois, siga as instruções abaixo para configurar corretamente seu acesso ao Cluster Franky.
Conecte-se ao cluster utilizando o comando SSH:
O login é o seu "usuario Insper", o endereço de IP foi fornecido durante a aula.
Se você está com o terminal aberto na pasta em que está a sua chave SSH, basta usar o comando:
ssh -i id_rsa login@ip_do_cluster
Se você abriu o terminal em qualquer lugar, então, o comando é este aqui:
ssh -i caminho_para_a_chave_ssh/id_rsa login@ip_do_cluster
Ambientação no Cluster Franky¶
Antes de começar a fazer pedidos de recursos pro SLURM, vamos conhecer os diferentes hardwares que temos disponível no Franky. Vamos utilizar alguns comandos de sistema operacional para ler os recursos de CPU, memória e GPU disponíveis
Comandos linux que serão utilizados:¶
lscpu: mostra detalhes da CPU (núcleos, threads, memória cache...)cat /proc/meminfo: mostra detalhes sobre a memória RAMnvidia-smi: mostra detalhes de GPU, se disponível
Explorando com o SRUN¶
Vale lembrar que podemos pedir via SRUN um terminal dentro do nó de computação, para, de forma livre, executar qualquer comando:
srun --partition=sunny_cpu --mem=1G --pty bash
Para sair, basta digitar no terminal:
exit
Ou, podemos usar o SRUN com um comando definido que será executado no nó de computação de forma direta pelo terminal:
Para visualizar o hardware disponível na fila escolhida:
srun --partition=sunny_cpu --mem=1G --pty bash -c \
"echo '=== HOSTNAME ==='; hostname; echo; \
echo '=== MEMORIA (GB) ==='; \
cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable|Swap' | \
awk '{printf \"%s %.2f GB\\n\", \$1, \$2 / 1048576}'; \
echo; \
echo '=== CPU INFO ==='; \
lscpu | grep -E 'Model name|Socket|Core|Thread|CPU\\(s\\)|cache'
echo '=== GPU INFO ==='; \
if command -v nvidia-smi &> /dev/null; then nvidia-smi; else echo 'nvidia-smi não disponível'; fi"
Você sempre pode usar o HW de CPU da fila GPU
srun --partition=sunny_gpu --mem=1G --pty bash -c \
"echo '=== HOSTNAME ==='; hostname; echo; \
echo '=== MEMORIA (GB) ==='; \
cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable|Swap' | \
awk '{printf \"%s %.2f GB\\n\", \$1, \$2 / 1048576}'; \
echo; \
echo '=== CPU INFO ==='; \
lscpu | grep -E 'Model name|Socket|Core|Thread|CPU\\(s\\)|cache'
echo '=== GPU INFO ==='; \
if command -v nvidia-smi &> /dev/null; then nvidia-smi; else echo 'nvidia-smi não disponível'; fi"
O comando sinfo mostra quais são as filas e quais são os status dos nós
sinfo
scontrol show partition
squeue mostra a fila do Cluster, você pode verificar informações sobre os jobs lançaos para o SLURM como este comando:
squeue
Se algo deu errado e você ficou com job preso na fila, pode cancelar todos os seus jobs com o comando:
scancel -u $USER
Ou um job especifico com o comando:
scancel id_do_job
SBATCH — Submissão de Jobs no SLURM¶
sbatch é o comando usado para enviar um job para a fila do cluster.
Diferente do srun, ele não é interativo.
Você cria um script e o SLURM executa quando houver recursos disponíveis.
Para criar um arquivo lançador de job:
nano teste.slurm
Cole o conteúdo abaixo:
#!/bin/bash
#SBATCH --job-name=sbatch_belezinha # Nome do job (aparece no squeue)
#SBATCH --partition=sunny_gpu # Fila (partition) onde o job será executado
#SBATCH --cpus-per-task=1 # Número de threads
#SBATCH --mem=1G # Memória RAM solicitada
#SBATCH --time=00:05:00 # Tempo máximo de execução (HH:MM:SS)
#SBATCH --output=meu_orgulho_%j.log # Arquivo de saída (%j = ID do job)
echo "=== HOSTNAME ==="
hostname
echo
echo "=== MEMORIA (GB) ==="
cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable|Swap' | \
awk '{printf "%s %.2f GB\n", $1, $2 / 1048576}'
echo
echo "=== CPU INFO ==="
lscpu | grep -E 'Model name|Socket|Core|Thread|CPU\(s\)|cache'
echo
echo "=== GPU INFO ==="
if command -v nvidia-smi &> /dev/null; then
nvidia-smi
else
echo "nvidia-smi não disponível"
fi
#Sleep desnecessário, só para você conseguir enxergar o seu job na fila do slurm
sleep 20
Salve usando Crtl + s,
e saia usando Crlt + x.
Para submeter o job:
sbatch teste.sh
Você verá algo como:
Submitted batch job 12345
O número é o ID do job.
Para verificar se o lançador deu certo e o seu job está rodando:
squeue
squeue -u $USER
Quando o job terminar, será criado um arquivo como:
saida_12345.log
Visualize com:
cat saida_12345.log
Testando a APS1 no Cluster Franky¶
Crie um novo arquivo dentro da pasta scratch e cole o código base da APS1:
Você pode utilizar o nano para criar um arquivo novo
nano base.cpp
Código base da APS1 para facilitar a sua vida
base.cpp
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <limits>
#include <string>
#include <vector>
using namespace std;
const int CAPACIDADE_NAVIO = 5;
struct Porto {
string nome;
double x;
double y;
};
/*
POLEMICA:
Este trecho contém problemas que deixam o código ineficiente.
*/
double distancia(Porto origem, Porto destino) {
return sqrt(pow(origem.x - destino.x, 2) +
pow(origem.y - destino.y, 2));
}
/*
POLEMICA:
- Recebe o vetor de portos por valor.
- Preenche a matriz por colunas, prejudicando a localidade de memória.
*/
vector<double> criarMatrizDistancias(vector<Porto> portos) {
int n = static_cast<int>(portos.size());
// Matriz n x n armazenada em um vetor simples.
vector<double> matriz(n * n);
for (int j = 0; j < n; ++j) {
for (int i = 0; i < n; ++i) {
matriz[i * n + j] = distancia(portos[i], portos[j]);
}
}
return matriz;
}
/*
Calcula o custo de uma ordem de entrega.
O navio parte do porto de origem carregando ate CAPACIDADE_NAVIO.
Cada destino representa a entrega de um conteiner.
Quando a carga termina, o navio retorna ao porto de origem para buscar
um novo lote. Depois da ultima entrega, ele retorna ao porto de origem.
POLEMICA:
- O porto, o vetor de destinos e a rota são recebidos por valor.
- A matriz de distancias e reconstruida para cada rota avaliada.
*/
double calcularCusto(Porto origem,
vector<Porto> destinos,
vector<int> rota) {
vector<Porto> todosPortos;
todosPortos.push_back(origem);
for (int i = 0; i < static_cast<int>(destinos.size()); ++i) {
todosPortos.push_back(destinos[i]);
}
vector<double> matriz = criarMatrizDistancias(todosPortos);
int quantidadePortos = static_cast<int>(todosPortos.size());
double custo = 0.0;
int carga = CAPACIDADE_NAVIO;
int atual = 0;
for (int i = 0; i < static_cast<int>(rota.size()); ++i) {
if (carga == 0) {
// Distancia entre o porto atual e o porto de origem (indice 0).
custo += matriz[atual * quantidadePortos];
atual = 0;
carga = CAPACIDADE_NAVIO;
}
// O destino 0 esta armazenado na posicao 1 da matriz, pois a
// posicao 0 e reservada para o porto de origem.
int destino = rota[i] + 1;
custo += matriz[atual * quantidadePortos + destino];
atual = destino;
--carga;
}
// Retorno ao porto de origem depois da ultima entrega.
custo += matriz[atual * quantidadePortos];
return custo;
}
/*
Busca exaustiva por todas as permutacoes dos portos de destino.
POLEMICA:
Origem, destinos e rota são recebidos por valor. Dessa forma, varios
dados são copiados a cada chamada recursiva.
melhorCusto e melhorRota são referencias apenas para que a versao base
produza corretamente a melhor solucao encontrada.
*/
void permutar(Porto origem,
vector<Porto> destinos,
vector<int> rota,
int inicio,
double& melhorCusto,
vector<int>& melhorRota) {
if (inicio == static_cast<int>(rota.size())) {
double custo = calcularCusto(origem, destinos, rota);
if (custo < melhorCusto) {
melhorCusto = custo;
melhorRota = rota;
}
return;
}
for (int i = inicio; i < static_cast<int>(rota.size()); ++i) {
swap(rota[inicio], rota[i]);
permutar(origem,
destinos,
rota,
inicio + 1,
melhorCusto,
melhorRota);
swap(rota[inicio], rota[i]);
}
}
/*
POLEMICA:
Origem, destinos e rota também são recebidos por valor nesta funcao.
*/
void imprimirRota(Porto origem,
vector<Porto> destinos,
vector<int> rota) {
cout << origem.nome;
int carga = CAPACIDADE_NAVIO;
for (int i = 0; i < static_cast<int>(rota.size()); ++i) {
if (carga == 0) {
cout << " -> " << origem.nome << " [novo lote]";
carga = CAPACIDADE_NAVIO;
}
cout << " -> " << destinos[rota[i]].nome;
--carga;
}
cout << " -> " << origem.nome << '\n';
}
int main(int argc, char* argv[]) {
if (argc < 2) {
cout << "Uso: " << argv[0] << " <numero_de_destinos>\n";
cout << "Exemplo: " << argv[0] << " 8\n";
return 1;
}
int n = atoi(argv[1]);
vector<Porto> portosDisponiveis = {
{"Paranagua", 100, 180},
{"Rio_Grande", 120, 300},
{"Itajai", 90, 220},
{"Rio_de_Janeiro", 210, 80},
{"Salvador", 350, 310},
{"Suape", 430, 420},
{"Pecem", 510, 500},
{"Manaus", 260, 650},
{"Las_Palmas", 800, 460},
{"Rotterdam", 1100, 220},
{"Hamburgo", 1160, 190},
{"Antuerpia", 1120, 230}
};
if (n <= 0 || n > static_cast<int>(portosDisponiveis.size())) {
cout << "Numero de destinos invalido. Use um valor entre 1 e "
<< portosDisponiveis.size() << ".\n";
return 1;
}
Porto origem{"Santos", 150, 100};
vector<Porto> destinos;
for (int i = 0; i < n; ++i) {
destinos.push_back(portosDisponiveis[i]);
}
vector<int> rota;
for (int i = 0; i < n; ++i) {
rota.push_back(i);
}
vector<int> melhorRota;
double melhorCusto = numeric_limits<double>::max();
auto inicioTempo = chrono::steady_clock::now();
permutar(origem,
destinos,
rota,
0,
melhorCusto,
melhorRota);
auto fimTempo = chrono::steady_clock::now();
chrono::duration<double> tempo = fimTempo - inicioTempo;
cout << "Melhor custo: " << melhorCusto << " milhas nauticas\n";
cout << "Melhor rota: ";
imprimirRota(origem, destinos, melhorRota);
cout << "Tempo: " << tempo.count() << " segundos\n";
return 0;
}
Para gerar o binário do código sequencial:¶
g++ -SuaFlagDeOtimizaçao base.cpp -o base
Para gerar o binário do código com suporte a paralelismo:¶
g++ -fopenmp -SuaFlagDeOtimizaçao paralelo.cpp -o paralelo
Para submeter o seu binário com o srun:¶
Sem suporte a paralelismo o comando fica assim:
Usando a fila de CPU
srun --partition=sunny_cpu --mem=1G ./meu_programa_sequencial
Usando a fila de GPU
srun --partition=sunny_cpu --mem=1G --gres=gpu:1 ./meu_programa_sequencial
Com suporte a paralelismo:
srun --partition=sunny_cpu --mem=1G --cpus-per-task=4 ./meu_programa_paralelo
Usando a fila de GPU
srun --partition=sunny_gpu --mem=1G --cpus-per-task=4 ./meu_programa_paralelo
Para submeter o seu binário com o sbatch:¶
Se quiser submeter um job com sbatch na fila CPU sequencial:
runCPU.slurm
#!/bin/bash
#SBATCH --job-name=exemplo_sequencial_cpu
#SBATCH --output=saida_%j.txt
#SBATCH --time=00:10:00
#SBATCH --partition=sunny_cpu
#SBATCH --mem=1G # 1 GiB por nó
./base
sbatch runCPU.slurm
Se quiser submeter um job com sbatch na fila GPU:
runGPU.slurm
#!/bin/bash
#SBATCH --job-name=exemplo
#SBATCH --output=saida_%j.txt
#SBATCH --time=00:10:00
#SBATCH --gres=gpu:1
#SBATCH --partition=sunny_gpu
#SBATCH --mem=1G # 1 GiB por nó
./base
sbatch runGPU.slurm
Se quiser submeter um job com sbatch na fila CPU com suporte a paralelismo:
runCPU.slurm
#!/bin/bash
#SBATCH --job-name=exemplo_paralelo_cpu # nome do job
#SBATCH --output=saida_%j.txt # arquivo de saida com identificação única
#SBATCH --time=00:10:00 # pedimos 10 minutos
#SBATCH --cpus-per-task=16 # quantidade de threads
#SBATCH --partition=sunny_cpu # nome da fila
#SBATCH --mem=1G # 1 GiB por nó
export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK # informando a quantidade de threads disponível para o binário
./meu_binario # binário que deve ser compilado com a flag '-fopenmp'
Se quiser automatizar os testes variando o número de threads você pode usar algo como:
Código demo se quiser testar
demo.cpp
#include <iostream>
#include <vector>
#include <omp.h>
#include <cstdlib>
bool eh_primo(long long n) {
if (n < 2)
return false;
for (long long i = 2; i * i <= n; i++) {
if (n % i == 0)
return false;
}
return true;
}
int main(int argc, char* argv[]) {
long long LIMITE = 10000000;
omp_set_dynamic(0);
int num_threads = omp_get_max_threads();
std::vector<long long> quantidade_primos(num_threads, 0);
std::vector<long long> quantidade_numeros(num_threads, 0);
std::vector<long long> primeiro_numero(num_threads, -1);
std::vector<long long> ultimo_numero(num_threads, -1);
double inicio = omp_get_wtime();
#pragma omp parallel
{
int id = omp_get_thread_num();
long long primos = 0;
long long numeros = 0;
#pragma omp for schedule(guided)
for (long long numero = 0; numero < LIMITE; numero++) {
if (primeiro_numero[id] == -1) {
primeiro_numero[id] = numero;
}
ultimo_numero[id] = numero;
numeros++;
if (eh_primo(numero)) {
primos++;
}
}
quantidade_primos[id] = primos;
quantidade_numeros[id] = numeros;
}
double fim = omp_get_wtime();
long long total_primos = 0;
std::cout << "\nDistribuicao das tarefas\n";
std::cout << "============================================================\n";
for (int i = 0; i < num_threads; i++) {
total_primos += quantidade_primos[i];
std::cout
<< "Thread " << i
<< " | numeros = " << quantidade_numeros[i]
<< " | intervalo = ["
<< primeiro_numero[i]
<< ", "
<< ultimo_numero[i]
<< "]"
<< " | primos = "
<< quantidade_primos[i]
<< "\n";
}
std::cout << "\n============================================================\n";
std::cout << "Threads: " << num_threads << "\n";
std::cout << "Total de primos: " << total_primos << "\n";
std::cout << "Tempo total: " << fim - inicio << " s\n";
return 0;
}
g++ -fopenmp demo.cpp -o demo
#!/bin/bash
#SBATCH --job-name=teste_escalabilidade
#SBATCH --output=demo.txt
#SBATCH --time=00:20:00
#SBATCH --cpus-per-task=16 #valor máximo de threads permitidas
#SBATCH --partition=sunny_cpu
#SBATCH --mem=1G
echo "========================================"
echo "Job ID: $SLURM_JOB_ID"
echo "Nó: $SLURMD_NODENAME"
echo "CPUs disponíveis: $SLURM_CPUS_PER_TASK"
echo "========================================"
# Testa de 1 até o número de CPUs solicitado ao Slurm
for N in $(seq 1 $SLURM_CPUS_PER_TASK)
do
export OMP_NUM_THREADS=$N
echo
echo "========================================"
echo "Teste com $N thread(s)"
echo "OMP_NUM_THREADS=$OMP_NUM_THREADS"
echo "========================================"
time ./demo
done
sbatch runCPU.slurm
Se quiser submeter um job com sbatch na fila GPU, mas com suporte a paralelismo em CPU:
runGPU.slurm
#!/bin/bash
#SBATCH --job-name=exemplo # nome do job
#SBATCH --output=saida_%j.txt # arquivo de saida com identificação única
#SBATCH --time=00:10:00 # pedimos 10 minutos
#SBATCH --cpus-per-task=16 # Quantidade de threads
#SBATCH --partition=sunny_gpu # Nome da fila
#SBATCH --mem=1G # 1 GiB por nó
export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK
./meu_binario
sbatch runGPU.slurm