Aula 10 - Tipos de comunicação MPI
Na aula anterior vimos a comunicação ponto-a-ponto, utilizando operações como MPI_Send e MPI_Recv, e analisamos como o custo de comunicação depende da latência e da largura de banda da rede por meio do experimento de ping-pong.
Nesta aula vamos dar mais um passo no uso do MPI. Em vez de trabalhar apenas com comunicação ponto-a-ponto, vamos ver alguns padrões de comunicação e também as operações coletivas, onde vários processos participam da troca de dados ao mesmo tempo.
Para isso, vamos implementar alguns exemplos e observar como as mensagens circulam entre os processos.
Ping-pong¶
Código base:
#include <mpi.h> // Biblioteca principal do MPI para comunicação entre processos
#include <iostream>
#include <cstring>
int main(int argc, char** argv) {
int rank; // Variável que armazenará o "rank" (identificador) do processo
MPI_Status status; // Estrutura que armazenará o status da comunicação MPI
char mensagem[100]; // Vetor de caracteres para armazenar a mensagem a ser enviada/recebida
// Inicializa o ambiente MPI (todos os processos são iniciados)
MPI_Init(&argc, &argv);
// Descobre o "rank" do processo atual dentro do comunicador global (MPI_COMM_WORLD)
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
// Se este for o processo de rank 0 (emissor inicial)
if (rank == 0) {
// Copia a string "Olá" para a variável mensagem
std::strcpy(mensagem, "Olá");
// Envia a mensagem para o processo de rank 1
// Parâmetros: buffer, tamanho, tipo, destino, tag, comunicador
MPI_Send(mensagem, std::strlen(mensagem) + 1, MPI_CHAR, 1, 0, MPI_COMM_WORLD);
// Imprime no terminal que a mensagem foi enviada
std::cout << "Processo 0 enviou: " << mensagem << std::endl;
// Aguarda a resposta do processo 1
// Parâmetros: buffer, tamanho máximo, tipo, origem, tag, comunicador, status
MPI_Recv(mensagem, 100, MPI_CHAR, 1, 0, MPI_COMM_WORLD, &status);
// Imprime a mensagem recebida
std::cout << "Processo 0 recebeu: " << mensagem << std::endl;
}
// Se este for o processo de rank 1 (receptor inicial)
else if (rank == 1) {
// Recebe a mensagem enviada pelo processo 0
MPI_Recv(mensagem, 100, MPI_CHAR, 0, 0, MPI_COMM_WORLD, &status);
// Imprime a mensagem recebida
std::cout << "Processo 1 recebeu: " << mensagem << std::endl;
// Prepara a resposta "Oi"
std::strcpy(mensagem, "Oi");
// Envia a resposta de volta ao processo 0
MPI_Send(mensagem, std::strlen(mensagem) + 1, MPI_CHAR, 0, 0, MPI_COMM_WORLD);
// Imprime que a mensagem foi enviada
std::cout << "Processo 1 enviou: " << mensagem << std::endl;
}
else {
// Todos os outros processos apenas informam que estão ociosos
std::cout << "Processo " << rank << " está ocioso neste exercício." << std::endl;
}
// Finaliza o ambiente MPI (todos os processos encerram)
MPI_Finalize();
return 0;
}
Compile o programa:¶
mpic++ -FlagdeOtimização seu_codigo.cpp -o seu_binario
Script SLURM¶
#!/bin/bash
#SBATCH --job-name=mpi_hello
#SBATCH --output=saida%j.txt
#SBATCH --partition=express
#SBATCH --mem=1GB
#SBATCH --nodes=2
#SBATCH --ntasks=5
#SBATCH --cpus-per-task=1
#SBATCH --time=00:02:00
#SBATCH --export=ALL
# Execute o seu binário com o MPI
mpirun -np $SLURM_NTASKS ./seu_binario
Submeta o job com SLURM:¶
sbatch SeuSlurm.slurm
Você deve ver algo como isso:¶
[liciascl@head-node mpi]$ cat saida.txt
Processo 2 está ocioso neste exercício.
Processo 3 está ocioso neste exercício.
Processo 0 enviou: Olá
Processo 0 recebeu: Oi
Processo 1 recebeu: Olá
Processo 1 enviou: Oi
Processo 4 está ocioso neste exercício.
Token em anel¶
A ideia é perceber o custo de coletar informações sequencialmente.
teste o token em anel: cada rank adiciona uma informação nova ao vetor e passa adiante.
Execute em 2, 3 e 4 nós .
Compare com o mesmo problema usando MPI_Gather.
Token em anel (comunicação sequencial)¶
A ideia é que cada processo adicione seu rank em um vetor e passe esse vetor para o próximo processo. O último processo devolve o vetor ao rank 0.
Isso cria uma comunicação sequencial, passando por todos os processos.
Código¶
#include <mpi.h> // Biblioteca principal do MPI (Message Passing Interface)
#include <iostream> // Biblioteca para entrada e saída (cout)
#include <vector> // Biblioteca para usar std::vector
int main(int argc, char** argv) {
// Inicializa o ambiente MPI
// Todos os processos começam a execução aqui
MPI_Init(&argc, &argv);
int rank, size;
// Descobre o identificador único (rank) do processo atual
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
// Descobre quantos processos estão participando do comunicador
MPI_Comm_size(MPI_COMM_WORLD, &size);
// Vetor que representará o "token" que circula entre os processos
// Cada posição será preenchida por um processo diferente
std::vector<int> token(size);
// Processo inicial do anel
if(rank == 0){
// O primeiro processo adiciona seu rank na primeira posição do vetor
token[0] = rank;
// Envia o vetor para o próximo processo (rank 1)
MPI_Send(token.data(), size, MPI_INT, 1, 0, MPI_COMM_WORLD);
// Aguarda o retorno do vetor vindo do último processo do anel
MPI_Recv(token.data(), size, MPI_INT, size-1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
// Após percorrer todos os processos, o vetor estará completo
std::cout << "Token final: ";
// Imprime o conteúdo final do vetor
for(int i=0;i<size;i++)
std::cout << token[i] << " ";
std::cout << std::endl;
}
else{
// Recebe o vetor do processo anterior no anel
MPI_Recv(token.data(), size, MPI_INT, rank-1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
// Adiciona seu próprio rank no vetor
token[rank] = rank;
// Calcula qual será o próximo processo do anel
// O operador % garante que o último processo envie de volta ao rank 0
int next = (rank + 1) % size;
// Envia o vetor atualizado para o próximo processo
MPI_Send(token.data(), size, MPI_INT, next, 0, MPI_COMM_WORLD);
}
// Finaliza o ambiente MPI
// Todos os processos encerram a execução aqui
MPI_Finalize();
return 0;
}
Compile o programa:¶
mpic++ -FlagdeOtimização seu_codigo.cpp -o seu_binario
Script SLURM¶
#!/bin/bash
#SBATCH --job-name=mpi_hello
#SBATCH --output=saida%j.txt
#SBATCH --partition=express
#SBATCH --mem=1GB
#SBATCH --nodes=2
#SBATCH --ntasks=5
#SBATCH --cpus-per-task=1
#SBATCH --time=00:02:00
#SBATCH --export=ALL
# Execute o seu binário com o MPI
mpirun -np $SLURM_NTASKS ./seu_binario
Submeta o job com SLURM:¶
sbatch SeuSlurm.slurm
Versão usando MPI_Gather¶
Agora cada processo simplesmente envia seu valor para o processo rank 0, e o MPI faz a coleta automaticamente.
Essa comunicação é coletiva, e a biblioteca MPI normalmente usa algoritmos otimizados em árvore.
Código¶
#include <mpi.h>
#include <iostream>
#include <vector>
int main(int argc, char** argv) {
// Inicializa o ambiente MPI
// Todos os processos começam a execução aqui
MPI_Init(&argc, &argv);
int rank, size;
// Obtém o identificador único (rank) do processo atual
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
// Obtém o número total de processos no comunicador
MPI_Comm_size(MPI_COMM_WORLD, &size);
// Cada processo cria um valor local.
// Aqui usamos o próprio rank apenas como exemplo
int valor = rank;
// Vetor que armazenará os resultados coletados
// Apenas o processo 0 precisa desse vetor
std::vector<int> resultado;
// Se este for o processo root (rank 0),
// ele aloca espaço para armazenar um valor de cada processo
if(rank == 0)
resultado.resize(size);
// Operação coletiva que coleta dados de todos os processos
// Cada processo envia 1 inteiro (valor)
// O processo root recebe todos os valores no vetor "resultado"
MPI_Gather(&valor, // endereço do dado local que será enviado
1, // quantidade de elementos enviados
MPI_INT, // tipo de dado enviado
resultado.data(),// buffer onde o root armazenará os dados
1, // quantidade de elementos recebidos de cada processo
MPI_INT, // tipo de dado recebido
0, // rank do processo root (destino final)
MPI_COMM_WORLD); // comunicador utilizado
// Apenas o processo root imprime os resultados
if(rank == 0){
std::cout << "Valores coletados: ";
// Imprime todos os valores recebidos
for(int i=0;i<size;i++)
std::cout << resultado[i] << " ";
std::cout << std::endl;
}
// Finaliza o ambiente MPI
MPI_Finalize();
return 0;
}
Compile o programa:¶
mpic++ -FlagdeOtimização seu_codigo.cpp -o seu_binario
Script SLURM¶
#!/bin/bash
#SBATCH --job-name=mpi_hello
#SBATCH --output=saida%j.txt
#SBATCH --partition=express
#SBATCH --mem=1GB
#SBATCH --nodes=2
#SBATCH --ntasks=5
#SBATCH --cpus-per-task=1
#SBATCH --time=00:02:00
#SBATCH --export=ALL
# Execute o seu binário com o MPI
mpirun -np $SLURM_NTASKS ./seu_binario
Submeta o job com SLURM:¶
sbatch SeuSlurm.slurm
Distribuição de dados com MPI_Scatter¶
Neste exemplo, um vetor grande é criado no processo 0 e distribuído entre todos os processos. Cada processo recebe uma parte do vetor e calcula a soma local.
Ideia¶
rank 0cria um vetor grande.- O vetor é dividido entre os processos com
MPI_Scatter. - Cada processo calcula uma soma parcial da sua parte.
#include <mpi.h>
#include <iostream>
#include <vector>
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int rank, size;
// Descobre o rank do processo e o número total de processos
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
const int N = 1000000; // tamanho total do vetor global
std::vector<int> dados;
// Apenas o processo 0 cria o vetor completo
if(rank == 0){
dados.resize(N);
// Preenche o vetor com valores crescentes
// Isso facilita visualizar os intervalos distribuídos
for(int i=0;i<N;i++)
dados[i] = i;
}
// Cada processo receberá uma fração do vetor
int local_size = N / size;
std::vector<int> local(local_size);
// Distribui partes do vetor para todos os processos
MPI_Scatter(dados.data(),
local_size,
MPI_INT,
local.data(),
local_size,
MPI_INT,
0,
MPI_COMM_WORLD);
// Calcula soma local
int soma_local = 0;
for(int i=0;i<local_size;i++)
soma_local += local[i];
// Mostra qual intervalo foi recebido
std::cout << "Processo " << rank
<< " recebeu intervalo ["
<< local.front() << ", "
<< local.back() << "]"
<< " | soma local = "
<< soma_local
<< std::endl;
MPI_Finalize();
}
Compile o programa:¶
mpic++ -FlagdeOtimização seu_codigo.cpp -o seu_binario
Script SLURM¶
#!/bin/bash
#SBATCH --job-name=mpi_hello
#SBATCH --output=saida%j.txt
#SBATCH --partition=express
#SBATCH --mem=1GB
#SBATCH --nodes=2
#SBATCH --ntasks=5
#SBATCH --cpus-per-task=1
#SBATCH --time=00:02:00
#SBATCH --export=ALL
# Execute o seu binário com o MPI
mpirun -np $SLURM_NTASKS ./seu_binario
Submeta o job com SLURM:¶
sbatch SeuSlurm.slurm
Combinação de resultados com MPI_Reduce¶
Agora cada processo possui um valor (ou resultado parcial) e queremos combinar esses valores em um único resultado final.
Aqui usamos MPI_Reduce para calcular a soma total das somas locais.
- Cada processo possui uma soma parcial.
MPI_Reducecombina todas as somas.- O resultado final aparece no processo 0.
#include <mpi.h>
#include <iostream>
int main(int argc, char** argv) {
// Inicializa o ambiente MPI
MPI_Init(&argc, &argv);
int rank, size;
// Descobre o identificador do processo
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
// Descobre quantos processos existem
MPI_Comm_size(MPI_COMM_WORLD, &size);
// Cada processo cria um valor local
int valor_local = rank * 222 + 100;
// Mostra o valor local antes da redução
std::cout << "Processo " << rank
<< " possui valor local = "
<< valor_local << std::endl;
int soma_total = 0;
// Operação coletiva: soma todos os valores locais
MPI_Reduce(&valor_local,
&soma_total,
1,
MPI_INT,
MPI_SUM,
0,
MPI_COMM_WORLD);
// Apenas o processo raiz recebe o resultado final
if(rank == 0){
std::cout << "\n---- Resultado da redução ----\n";
std::cout << "Todos os valores foram somados no processo 0\n";
std::cout << "Soma total = " << soma_total << std::endl;
}
MPI_Finalize();
}
Compile o programa:¶
mpic++ -FlagdeOtimização seu_codigo.cpp -o seu_binario
Script SLURM¶
#!/bin/bash
#SBATCH --job-name=mpi_hello
#SBATCH --output=saida%j.txt
#SBATCH --partition=express
#SBATCH --mem=1GB
#SBATCH --nodes=2
#SBATCH --ntasks=5
#SBATCH --cpus-per-task=1
#SBATCH --time=00:02:00
#SBATCH --export=ALL
# Execute o seu binário com o MPI
mpirun -np $SLURM_NTASKS ./seu_binario
Submeta o job com SLURM:¶
sbatch SeuSlurm.slurm