Ir al contenido

MPI básico

Message passing: procesos con memoria local que se comunican enviando/recibiendo mensajes. MPI es el API estándar (MPI Forum, 1.0 en 1994) para C, C++ y Fortran; implementaciones MPICH y OpenMPI. Paralelismo explícito; funciona en CPU, nodo, NUMA y cluster, y admite híbrido (MPI + OpenMP).

ejemplo01.cpp
#include <mpi.h>
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
int ierr, rank, size, dato = 10;
ierr = MPI_Init(&argc, &argv); // una sola vez, al inicio
MPI_Comm_size(MPI_COMM_WORLD, &size); // número total de procesos
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // mi id: 0..size-1
if (ierr == MPI_SUCCESS)
cout << "Hola mundo desde el proceso " << rank << " de " << size << endl;
if (rank == 2) dato = 20; // cada proceso tiene SU copia de dato
cout << "Proceso " << rank << " tiene dato = " << dato << endl;
double time = MPI_Wtime(); // reloj en segundos (double)
cout << "Tiempo del proceso " << rank << " = " << time << endl;
MPI_Finalize();
}
Ventana de terminal
mpic++ -o ejemplo01 ejemplo01.cpp # C: mpicc · Fortran: mpif90
mpirun -np 4 ./ejemplo01
MPI_Send(&buffer, count, datatype, dest, tag, comm)
MPI_Recv(&buffer, count, datatype, source, tag, comm, &status)
  • El mensaje es un array de count elementos de datatype (MPI_INT, MPI_FLOAT, MPI_DOUBLE, MPI_CHAR, MPI_BYTE, MPI_PACKED…). El tipo enviado debe coincidir con el recibido (soporte para arquitecturas heterogéneas).
  • El recibo se confirma si source, dest y tag corresponden (count y datatype no garantizan el emparejamiento). Comodines: MPI_ANY_SOURCE, MPI_ANY_TAG.
  • status.MPI_SOURCE, status.MPI_TAG, MPI_Get_count(&status, type, &count).
ejemplo02.cpp (fragmento)
if (rank == 0) {
numero = 10;
MPI_Send(&numero, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);
for (int i = 0; i < 10; i++) arr_numero[i] = i;
MPI_Send(arr_numero, 10, MPI_INT, 1, 0, MPI_COMM_WORLD); // un array: count = 10
} else if (rank == 1) {
MPI_Recv(&numero, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &estado);
MPI_Recv(arr_numero, 10, MPI_INT, 0, 0, MPI_COMM_WORLD, &estado);
}
RutinaSe completa cuando…
MPI_Ssend (síncrono)el receptor empezó a recibir (acuse de recibo)
MPI_Bsend (buffered)siempre de inmediato: copia a un buffer del usuario (MPI_Buffer_attach/detach)
MPI_Send (standard)síncrono o buffered según la implementación (mensaje chico ⇒ buffer; grande ⇒ síncrono)
MPI_Rsend (ready)de inmediato; si el receptor no estaba listo ⇒ error. Solo si el performance es crítico
MPI_Recvel mensaje llegó al buffer
ejemplo03.cpp — broadcast con Send/Recv
if (rank == 0) {
numero = 4;
for (int i = 1; i < size; i++)
MPI_Send(&numero, 1, MPI_INT, i, 0, MPI_COMM_WORLD); // p-1 envíos secuenciales
} else {
MPI_Recv(&numero, 1, MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &estado);
}

Costo O(p(α+β))O(p(\alpha+\beta)): lineal en pp. MPI_Bcast lo hace en árbol.

Medir tiempos: separar cómputo y comunicación

Sección titulada «Medir tiempos: separar cómputo y comunicación»

Patrón de la práctica 3: una barrera antes de cada fase, para que todos arranquen juntos y el reloj mida la fase y no la espera por procesos atrasados.

practica_3_ofi/ejercicio02_row.cpp
// --- Comunicación: Bcast de v y Scatter de A por filas ---
MPI_Barrier(MPI_COMM_WORLD);
t_aux = MPI_Wtime();
MPI_Bcast(v, N, MPI_LONG_LONG, 0, MPI_COMM_WORLD);
MPI_Scatter(A, elem_proc, MPI_INT, local_A, elem_proc, MPI_INT, 0, MPI_COMM_WORLD);
t_comm += MPI_Wtime() - t_aux;
// --- Cómputo local: cada proceso calcula sus fil_proc filas ---
MPI_Barrier(MPI_COMM_WORLD);
t_aux = MPI_Wtime();
for (i = 0; i < fil_proc; i++) {
local_x[i] = 0;
for (j = 0; j < N; j++) local_x[i] += local_A[i * N + j] * v[j];
}
t_comp = MPI_Wtime() - t_aux;
// --- Comunicación: Gather de local_x hacia x en P0 ---
MPI_Barrier(MPI_COMM_WORLD);
t_aux = MPI_Wtime();
MPI_Gather(local_x, fil_proc, MPI_LONG_LONG, x, fil_proc, MPI_LONG_LONG, 0, MPI_COMM_WORLD);
t_comm += MPI_Wtime() - t_aux;
// --- el tiempo que cuenta es el del proceso más lento ---
MPI_Reduce(&t_comp, &total_comp, 1, MPI_DOUBLE, MPI_MAX, 0, MPI_COMM_WORLD);
MPI_Reduce(&t_comm, &total_comm, 1, MPI_DOUBLE, MPI_MAX, 0, MPI_COMM_WORLD);

En un clúster no se ejecuta mpirun en el nodo de login: se compila ahí y se encola un job que reserva procesos.

  1. Compilar en el nodo de login, con los módulos del compilador y de MPI cargados: module load gnu12/12.4.0 mpich/3.4.3-ucx y luego mpic++ -O2 -DN=600 -o row_N600 ejercicio02_row.cpp. -DN=... fija el tamaño en tiempo de compilación.
  2. Escribir el script del job:
    #!/bin/bash
    #SBATCH --job-name=pd03_ej2_3
    #SBATCH --ntasks=10 # p máximo que se usará
    #SBATCH --time=05:00:00 # si se queda corto, Slurm mata el job a la mitad
    #SBATCH --partition=standard
    #SBATCH --output=slurm_%j.out
    module purge
    module load gnu12/12.4.0 mpich/3.4.3-ucx
    for p in 1 2 4 6 8 10; do mpirun -np $p ./row_N600; done
  3. Encolar con sbatch job.slurm y revisar slurm_<id>.out / .err. Dentro de la reserva, mpirun -np p con p≤p\le ntasks usa un subconjunto de los procesos.

¿Qué imprime ejemplo02.cpp con mpirun -np 3? ¿Por qué el fprintf después de MPI_Abort puede no aparecer?

Ver solución

Con size > 2, el rank 0 llama a MPI_Abort(MPI_COMM_WORLD, -1), que termina todos los procesos del comunicador inmediatamente; el fprintf está después del abort, así que normalmente no se imprime (debería ir antes). Los ranks 1 y 2 no hacen nada (el else solo se ejecuta si size ≤ 2) y son abortados.

Autoevaluación

0/4 correctas · 0 respondidas
1. MPI_Comm_rank devuelve…
2. Para emparejar un Send con un Recv deben coincidir…
3. ¿Qué modo se completa solo cuando el receptor empezó a recibir?
4. Compilar C++ con MPI:

Fuente ·U3.1 Paralelismo Distribuido-MPI.pdf · ejemplo01.cpp · ejemplo02.cpp · ejemplo03.cpp · ejemplo04.cpp · mpi_getting_started.cpp · practica_3_ofi/ejercicio02_row.cpp · practica_3_ofi/ejercicio2_3_eficiencia/khipu_eficiencia.slurm · compile_eficiencia.sh