Memória Compartilhada

A memória compartilhada é um modelo de paralelismo em que múltiplas threads (linhas de execução) compartilham o mesmo espaço de memória dentro de um único nó (servidor).

Conceitos Básicos

  • Threads: Múltiplas linhas de execução dentro de um mesmo processo

  • Memória compartilhada: Todas as threads enxergam a mesma memória

  • OpenMP: Padrão mais comum para programação com memória compartilhada

  • SMP (Symmetric Multi-Processing): Arquitetura com múltiplos processadores compartilhando memória

Características:

  • Todos os núcleos utilizados estão no mesmo nó

  • Acesso à mesma área de memória

  • Comunicação entre threads é muito rápida

  • Limitado aos recursos de um único nó

OpenMP

OpenMP é uma API para programação paralela em C/C++ e Fortran, baseada em diretivas de compilação.

Exemplo em C

omp_hello.c
/******************************************************************************
 * OpenMP Example - Hello World
 * O master thread cria uma região paralela.
 * Todas as threads obtêm seu número único e imprimem.
 ******************************************************************************/
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[])
{
  int nthreads, tid;

  /* Fork: cria um time de threads */
  #pragma omp parallel private(nthreads, tid)
  {
    /* Obtém o número da thread */
    tid = omp_get_thread_num();
    printf("Hello World from thread = %d\n", tid);

    /* Apenas a thread master faz isso */
    if (tid == 0)
    {
      nthreads = omp_get_num_threads();
      printf("Number of threads = %d\n", nthreads);
    }

  }  /* Join: todas as threads se encontram aqui */

  return 0;
}

Compilando

No GridUnesp, recomendamos o compilador Intel, mas GCC também está disponível:

# Com Intel
module load intel/compilers
icc -openmp omp_hello.c -o omp_hello

# Com GCC
module load gcc/10.2.0
gcc -fopenmp omp_hello.c -o omp_hello

Script de Submissão

No SLURM, o número de threads é controlado pela diretiva -c (--cpus-per-task).

job_openmp.sh
#!/bin/bash
#SBATCH -J openmp_test
#SBATCH -N 1                # 1 nó (obrigatório para memória compartilhada)
#SBATCH -c 8                # 8 threads
#SBATCH -t 01:00:00
#SBATCH --mem=16G

export INPUT="entrada.dat"
export OUTPUT="saida_openmp.dat"

# Define número de threads para OpenMP
export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK

# Carrega o módulo do compilador
module load intel/compilers

job-nanny srun ./omp_hello

Variáveis Importantes

  • OMP_NUM_THREADS: Número de threads a serem usadas

  • OMP_STACKSIZE: Tamanho da pilha por thread

  • OMP_SCHEDULE: Tipo de escalonamento de loops

export OMP_NUM_THREADS=28
export OMP_STACKSIZE=256M
export OMP_SCHEDULE="dynamic"

Limitações

  • Limitado a 1 nó (não pode usar múltiplos servidores)

  • Máximo de threads por nó: 56 (28 cores físicos + hyper-threading)

  • Máximo de threads disponível por nó: 52 (26 cores físicos + hyper-threading)

  • Memória total limitada à RAM do nó: 128 GB

Divisão de Recursos do Cluster (Especialização de Núcleos)

Tipo de Nó

Capacidade Total

Reservado ao SO

Disponível para Jobs

Nós Regulares (node[001-056])

28 Cores / 56 CPUs

2 Cores / 4 CPUs

26 Cores / 52 CPUs

Nó de GPU (gpunode001)

48 Cores / 96 CPUs

4 Cores / 8 CPUs

44 Cores / 88 CPUs

Quando Usar Memória Compartilhada

Ideal para:

  • Problemas que cabem na memória de um único nó

  • Loops pesados que podem ser paralelizados

  • Aplicações com muita comunicação entre threads

  • Quando a latência de rede seria um problema

Não recomendado para:

  • Problemas que não cabem em 128 GB de RAM

  • Aplicações que precisam de mais de 52 threads

  • Quando o problema é naturalmente distribuído

Exemplo Prático: Produto Matricial

matmul_omp.c
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>

#define N 1000

int main() {
    double A[N][N], B[N][N], C[N][N];
    double start, end;

    // Inicialização
    #pragma omp parallel for
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            A[i][j] = i + j;
            B[i][j] = i - j;
            C[i][j] = 0.0;
        }
    }

    start = omp_get_wtime();

    // Multiplicação paralela
    #pragma omp parallel for
    for (int i = 0; i < N; i++) {
        for (int k = 0; k < N; k++) {
            for (int j = 0; j < N; j++) {
                C[i][j] += A[i][k] * B[k][j];
            }
        }
    }

    end = omp_get_wtime();
    printf("Tempo com %d threads: %f segundos\n",
           omp_get_max_threads(), end - start);

    return 0;
}

Script para teste de escalabilidade:

test_omp_scaling.sh
#!/bin/bash
# Testar com 1, 2, 4, 8, 16, 28, 52 threads
for threads in 1 2 4 8 16 28 52; do
    cat > job_${threads}.sh << EOF
#!/bin/bash
#SBATCH -J omp_${threads}
#SBATCH -N 1
#SBATCH -c ${threads}
#SBATCH -t 00:30:00
#SBATCH --mem=16G
#SBATCH --output=omp_${threads}_%j.out

export OMP_NUM_THREADS=${threads}
module load intel/compilers
./test_omp
EOF

    sbatch job_${threads}.sh
done

Dicas de Otimização

  1. Balanceamento de carga: Use schedule dinâmico para loops irregulares

    #pragma omp parallel for schedule(dynamic, 100)
    
  2. Evite false sharing: Coloque variáveis compartilhadas em diferentes linhas de cache

  3. Use variáveis privadas: Declare variáveis temporárias como privadas

    #pragma omp parallel for private(temp)
    
  4. Nested parallelism: Cuidado ao aninhar regiões paralelas

    export OMP_NESTED=FALSE
    

Ver também