Memória Distribuída

A memória distribuída é um modelo de paralelismo em que múltiplos processos executam em diferentes nós (servidores), comunicando-se via troca de mensagens.

Conceitos Básicos

  • Processos: Unidades de execução independentes, cada uma com sua própria memória

  • MPI (Message Passing Interface): Padrão para comunicação entre processos

  • Troca de mensagens: Processos se comunicam explicitamente enviando/recebendo dados

  • Escalabilidade: Pode utilizar centenas ou milhares de nós

Características:

  • Processos podem estar em diferentes nós

  • Cada processo tem seu próprio espaço de memória

  • Comunicação é explícita (mais complexa)

  • Escalabilidade quase ilimitada

MPI

MPI é o padrão mais utilizado para programação com memória distribuída. No GridUnesp, temos duas implementações:

  • Intel MPI (recomendado, otimizado para hardware Intel)

  • OpenMPI (open-source, amplamente utilizado)

Exemplo em C

mpi_hello.c
#include <mpi.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
  int numtasks, rank, len, rc;
  char hostname[MPI_MAX_PROCESSOR_NAME];

  // Inicializa o MPI
  MPI_Init(&argc, &argv);

  // Número total de processos
  MPI_Comm_size(MPI_COMM_WORLD, &numtasks);

  // Rank (ID) deste processo
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);

  // Nome do nó onde está executando
  MPI_Get_processor_name(hostname, &len);

  printf("Processo %d de %d executando em %s\n",
         rank, numtasks, hostname);

  // Finaliza o MPI
  MPI_Finalize();

  return 0;
}

Compilando

# Com Intel MPI
module load intel/compilers
module load intel/mpi
mpiicc mpi_hello.c -o mpi_hello

# Com OpenMPI
module load openmpi/4.1.5
mpicc mpi_hello.c -o mpi_hello

Script de Submissão

No SLURM, o número de processos é controlado pela diretiva -n (--ntasks).

job_mpi.sh
#!/bin/bash
#SBATCH -J mpi_test
#SBATCH -N 4                # 4 nós
#SBATCH -n 112              # 112 processos totais
#SBATCH --ntasks-per-node=28 # 28 processos por nó
#SBATCH -t 02:00:00
#SBATCH --mem-per-cpu=4G

export INPUT="dados_entrada/ mpi_hello"
export OUTPUT="resultados_mpi/"

# Carrega o módulo MPI
module load openmpi/4.1.5

# Executa com srun (recomendado para SLURM)
job-nanny mpirun -n 112 ./mpi_hello

Distribuição de Processos

Opção 1: Especificar número total de processos

#SBATCH -n 112   # 112 processos totais
# O SLURM decide como distribuir entre os nós

Opção 2: Especificar processos por nó

#SBATCH -N 4
#SBATCH --ntasks-per-node=28   # 28 processos em cada nó
# Total: 4 × 28 = 112 processos

Opção 3: Especificar mínimo e máximo de nós

#SBATCH -n 112
#SBATCH -N 2-4   # Mínimo 2, máximo 4 nós
# SLURM decide a distribuição ótima

Comunicação entre Processos

Exemplo: Soma Distribuída

mpi_sum.c
#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>

#define ARRAY_SIZE 1000000

int main(int argc, char *argv[]) {
    int rank, size;
    int *data = NULL;
    int local_sum = 0, global_sum = 0;

    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);

    // Processo 0 inicializa os dados
    if (rank == 0) {
        data = (int*)malloc(ARRAY_SIZE * sizeof(int));
        for (int i = 0; i < ARRAY_SIZE; i++)
            data[i] = i + 1;
    }

    // Calcula quantos elementos cada processo recebe
    int chunk_size = ARRAY_SIZE / size;
    int *local_data = (int*)malloc(chunk_size * sizeof(int));

    // Distribui os dados (MPI_Scatter)
    MPI_Scatter(data, chunk_size, MPI_INT,
                local_data, chunk_size, MPI_INT,
                0, MPI_COMM_WORLD);

    // Cada processo calcula sua soma local
    for (int i = 0; i < chunk_size; i++)
        local_sum += local_data[i];

    // Reúne as somas parciais (MPI_Reduce)
    MPI_Reduce(&local_sum, &global_sum, 1, MPI_INT,
               MPI_SUM, 0, MPI_COMM_WORLD);

    if (rank == 0) {
        printf("Soma total: %d\n", global_sum);
        free(data);
    }

    free(local_data);
    MPI_Finalize();
    return 0;
}

Híbrido: MPI + OpenMP

É possível combinar os dois modelos:

  • MPI para comunicação entre nós

  • OpenMP para paralelismo dentro de cada nó

job_hibrido.sh
#!/bin/bash
#SBATCH -J hibrido
#SBATCH -N 4                # 4 nós
#SBATCH --ntasks-per-node=4  # 4 processos MPI por nó
#SBATCH -c 7                 # 7 threads por processo MPI
#SBATCH -t 24:00:00
#SBATCH --mem=64G

export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK
export INPUT="dados.dat programa_hibrido"
export OUTPUT="resultados/"

module load openmpi/4.1.5
job-nanny srun ./programa_hibrido

Total de threads: 4 nós × 4 processos × 7 threads = 112 threads

Quando Usar Memória Distribuída

Ideal para:

  • Problemas que não cabem em um único nó

  • Aplicações que escalam bem com mais nós

  • Simulações que requerem muitos recursos

  • Quando a memória total necessária > 128 GB

Não recomendado para:

  • Problemas pequenos que cabem em um nó

  • Aplicações com muita comunicação (overhead)

  • Quando a latência da rede é crítica

Escalabilidade

Lei de Amdahl

A aceleração máxima é limitada pela porção serial do código:

Speedup = 1 / (S + (1-S)/P)

Em que:

  • S = fração serial

  • P = número de processos

Exemplo: Se 10% do código é serial, o speedup máximo teórico é 10x, mesmo com infinitos processos.

Testando Escalabilidade

test_mpi_scaling.sh
#!/bin/bash
# Teste de escalabilidade forte (problema fixo)
for nodes in 1 2 4 8; do
    cat > job_${nodes}n.sh << EOF
#!/bin/bash
#SBATCH -J mpi_${nodes}n
#SBATCH -N ${nodes}
#SBATCH --ntasks-per-node=28
#SBATCH -t 01:00:00
#SBATCH --mem-per-cpu=2G

module load openmpi/4.1.5
srun ./programa_mpi
EOF
    sbatch job_${nodes}n.sh
done

Problemas Comuns

  1. Deadlock: Processos esperando uns pelos outros indefinidamente

    // ERRADO: pode causar deadlock
    MPI_Send(...);  // Envio bloqueante
    MPI_Recv(...);  // Recebimento
    
    // CORRETO: usar send/receive combinados
    MPI_Sendrecv(...);
    
  2. Race condition: Acesso concorrente sem sincronização

    // Use operações atômicas ou coletivas
    MPI_Reduce(&local, &global, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);
    
  3. Granularidade muito fina: Overhead de comunicação domina

    • Agrupe mensagens pequenas em mensagens maiores

    • Use comunicação coletiva quando possível

Dicas de Otimização

  1. Comunicação coletiva vs ponto-a-ponto

    • Use MPI_Reduce, MPI_Bcast, MPI_Scatter quando apropriado

    • São otimizadas para o hardware

  2. Sobreposição comunicação/computação

    MPI_Isend(...);  // Não bloqueante
    // Computação enquanto a mensagem é enviada
    MPI_Wait(...);   // Espera conclusão
    
  3. Use comunicadores para organizar grupos

    MPI_Comm_split(MPI_COMM_WORLD, color, key, &new_comm);
    
  4. Evite mensagens muito pequenas

    • Overhead de latência domina

    • Agrupe dados em buffers maiores

Ver também