Paralelismo híbrido MPI + OpenMP
Por qué hace falta (láminas 2–6)
Sección titulada «Por qué hace falta (láminas 2–6)»- Ley de Moore: la potencia de los supercomputadores sigue creciendo, pero ya no por relojes más rápidos sino por más núcleos. Un nodo actual tiene 2 sockets de 52 a 96 núcleos (Intel Xeon Max 9470, AMD EPYC 9004) y hasta 8 sockets en Sapphire Rapids.
- Cómputo vs. memoria: los FLOPs crecieron mucho más rápido que el ancho de banda de la memoria y de la red. Mover datos es el cuello de botella.
- Energía: la potencia dinámica de un chip CMOS crece como el cubo de la frecuencia:
P_dyn(f) ≈ α · f³. Subir el reloj ya no compensa.
La consecuencia (lámina 6) es usar muchos núcleos lentos en vez de pocos rápidos (“the chickens and the ox”), aprovechar la memoria local de cada nodo (híbrido) y lograr mucho paralelismo. Para el programador eso significa conocer mejor el hardware y escribir código más complejo.
Paradigmas de programación
Sección titulada «Paradigmas de programación»| Memoria compartida (OpenMP) | Memoria distribuida (MPI) |
|---|---|
| Los hilos leen y escriben en la RAM común | Los procesos envían y reciben mensajes |
| Se sincronizan porque compiten por los datos | Solo usan sus variables locales |
| Los hilos pertenecen a un proceso | Cada proceso tiene uno o más hilos |
Según la lámina 7: MPI sigue predominando; el híbrido MPI + OpenMP (+ CUDA) se usa cada vez más en supercomputadores; y aparecen lenguajes PGAS (Partitioned Global Address Space: GASPI, Coarray Fortran, UPC, Chapel).
Paralelismo híbrido
Sección titulada «Paralelismo híbrido»MPI se usa para la comunicación entre procesos y OpenMP dentro de cada proceso.
| Ventajas | Desventajas |
|---|---|
| Mayor escalabilidad: aumenta la granularidad (menos procesos, más trabajo por proceso) | Código más complejo |
| Mejor balance de carga (los hilos se reparten el trabajo del proceso) | Hay que hacer bien las dos cosas: MPI y OpenMP |
| Mejor uso de memoria (menos copias de datos y de halos) | Efectos NUMA: un hilo que lee la memoria del otro socket es más lento |
Algoritmos que se benefician (lámina 11):
- los que tienen escalabilidad MPI limitada;
- los que necesitan balance de carga dinámico;
- los que corren en hardware con poca memoria por núcleo y mucha comunicación MPI;
- las aplicaciones de paralelismo masivo.
Modos de operación (lámina 16)
Sección titulada «Modos de operación (lámina 16)»Para un nodo de 16 núcleos hay un espectro:
| Modo | Procesos MPI | Hilos por proceso |
|---|---|---|
| MPI puro (pure MPI node) | 16 | 1 |
| Híbrido | 4 | 4 |
| SMP puro (pure SMP node) | 1 | 16 |
En el híbrido, el hilo maestro de cada proceso es el que hace las llamadas MPI.
Cómo leerlo: el mismo nodo de 16 núcleos en los tres modos de la tabla. A la izquierda cada núcleo es un proceso MPI distinto (r0…r15). En el centro hay 4 procesos (un color cada uno) y cada proceso tiene 4 hilos OpenMP (h0…h3) que comparten su memoria. A la derecha un solo proceso con 16 hilos. En los tres casos se usan los 16 núcleos; cambia quién comparte memoria y cuántos mensajes hacen falta.
Repartir un nodo: procesos MPI × hilos OpenMP
Cada nodo tiene 8 núcleos. Elige cuántos procesos MPI van en cada nodo; el resto de núcleos se llena con hilos OpenMP dentro de cada proceso.
#!/bin/bash #SBATCH --job-name=hibrido_mpi_openmp #SBATCH --output=hibrido_mpi_openmp.out #SBATCH --nodes=2 #SBATCH --ntasks=2 #SBATCH --ntasks-per-node=1 #SBATCH --cpus-per-task=8 module load mpich/4.0 export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK # = 8 mpirun ./hibrido
peekaboo (16 líneas, orden no determinista)Peekaboo desde el hilo 0 de un total de 8 y desde el proceso 1 de un total de 2 en nodo-02 Peekaboo desde el hilo 4 de un total de 8 y desde el proceso 1 de un total de 2 en nodo-02 Peekaboo desde el hilo 7 de un total de 8 y desde el proceso 1 de un total de 2 en nodo-02 Peekaboo desde el hilo 6 de un total de 8 y desde el proceso 0 de un total de 2 en nodo-01 Peekaboo desde el hilo 2 de un total de 8 y desde el proceso 0 de un total de 2 en nodo-01 Peekaboo desde el hilo 1 de un total de 8 y desde el proceso 0 de un total de 2 en nodo-01 Peekaboo desde el hilo 2 de un total de 8 y desde el proceso 1 de un total de 2 en nodo-02 Peekaboo desde el hilo 3 de un total de 8 y desde el proceso 1 de un total de 2 en nodo-02 Peekaboo desde el hilo 6 de un total de 8 y desde el proceso 1 de un total de 2 en nodo-02 Peekaboo desde el hilo 5 de un total de 8 y desde el proceso 1 de un total de 2 en nodo-02 Peekaboo desde el hilo 4 de un total de 8 y desde el proceso 0 de un total de 2 en nodo-01 Peekaboo desde el hilo 0 de un total de 8 y desde el proceso 0 de un total de 2 en nodo-01 Peekaboo desde el hilo 1 de un total de 8 y desde el proceso 1 de un total de 2 en nodo-02 Peekaboo desde el hilo 7 de un total de 8 y desde el proceso 0 de un total de 2 en nodo-01 Peekaboo desde el hilo 5 de un total de 8 y desde el proceso 0 de un total de 2 en nodo-01 Peekaboo desde el hilo 3 de un total de 8 y desde el proceso 0 de un total de 2 en nodo-01
Estructura de un programa híbrido
Sección titulada «Estructura de un programa híbrido»- Se inicializa MPI.
- Se crean regiones paralelas OpenMP dentro de cada proceso.
- Se pueden llamar funciones MPI en las partes seriales y dentro de las regiones paralelas.
- Se finaliza MPI.
#include <stdio.h>#include "mpi.h"#include <omp.h>int main(int argc, char *argv[]) { int numprocs, rank, namelen; char processor_name[MPI_MAX_PROCESSOR_NAME]; int iam = 0, np = 1; MPI_Init(&argc, &argv); MPI_Comm_size(MPI_COMM_WORLD, &numprocs); MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Get_processor_name(processor_name, &namelen); #pragma omp parallel num_threads(4) default(shared) private(iam, np) { np = omp_get_num_threads(); iam = omp_get_thread_num(); printf("Peekaboo desde el hilo %d de un total de %d y desde el proceso %d de un total de %d en %s\n", iam, np, rank, numprocs, processor_name); } MPI_Finalize();}Compilar y ejecutar: hay que pasar las dos cosas, el compilador de MPI y la bandera de OpenMP.
mpicc -fopenmp peekaboo_mpi_omp.cmpirun -np 2 ./a.outSalida real con 2 procesos × 4 hilos en una sola máquina (el nombre del equipo se cambió por mi-pc):
Peekaboo desde el hilo 3 de un total de 4 y desde el proceso 0 de un total de 2 en mi-pcPeekaboo desde el hilo 1 de un total de 4 y desde el proceso 0 de un total de 2 en mi-pcPeekaboo desde el hilo 0 de un total de 4 y desde el proceso 0 de un total de 2 en mi-pcPeekaboo desde el hilo 1 de un total de 4 y desde el proceso 1 de un total de 2 en mi-pcPeekaboo desde el hilo 3 de un total de 4 y desde el proceso 1 de un total de 2 en mi-pcPeekaboo desde el hilo 2 de un total de 4 y desde el proceso 1 de un total de 2 en mi-pcPeekaboo desde el hilo 0 de un total de 4 y desde el proceso 1 de un total de 2 en mi-pcPeekaboo desde el hilo 2 de un total de 4 y desde el proceso 0 de un total de 2 en mi-pc8 líneas (2 × 4) en orden no determinista: los dos procesos y sus hilos corren a la vez.
Máquinas híbridas (lámina 20)
Sección titulada «Máquinas híbridas (lámina 20)»Una máquina de 268 nodos con 8 núcleos cada uno puede usarse como:
- 1 proceso MPI × 8 hilos OpenMP por nodo;
- 2 procesos × 4 hilos;
- 4 procesos × 2 hilos.
Con 2 nodos, 1 proceso por nodo y 4 hilos, cada proceso imprime desde su nodo:
0 running on compute-2-25.local thread= 0 of 4...1 running on compute-3-14.local thread= 3 of 4En clústeres
Sección titulada «En clústeres»Khipu: 4 procesos MPI × 8 hilos OpenMP
Sección titulada «Khipu: 4 procesos MPI × 8 hilos OpenMP»#!/bin/bash# Un Job script para la ejecución de un código híbrido MPI-OpenMP#SBATCH --job-name=hibrido_mpi_openmp#SBATCH --output=hibrido_mpi_openmp.out#SBATCH --ntasks=4#SBATCH --cpus-per-task=8#SBATCH --partition=docencia
module load mpich/4.0
# OMP_NUM_THREADS = número de CPUs por tarea pedido a Slurmexport OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK
# No hace falta -np: mpirun lo toma de la configuración de Slurmmpirun ./hibrido
module unload mpich/4.0La clave: --ntasks = número de procesos MPI, --cpus-per-task = hilos por proceso, y OMP_NUM_THREADS se iguala a $SLURM_CPUS_PER_TASK.
Perlmutter (NERSC)
Sección titulada «Perlmutter (NERSC)»Sistema heterogéneo (plataforma Cray) con 3072 CPUs (15.4 PFLOPs pico) y 1792 GPUs (69.5 PFLOPs pico). Cada AMD EPYC 7713 tiene 64 núcleos físicos y 128 hilos.
#SBATCH --nodes=2#SBATCH --ntasks-per-node=128srun check-mpi.gnu.pm#SBATCH --nodes=2#SBATCH --ntasks-per-node=2#SBATCH --cpus-per-task=128export OMP_PROC_BIND=spreadexport OMP_PLACES=threadsexport OMP_NUM_THREADS=64srun check-hybrid.gnu.pmOMP_PROC_BIND=spread reparte los hilos lo más lejos posible entre sí (por todo el socket) y OMP_PLACES=threads fija cada hilo a un hilo de hardware. Así los hilos no migran de núcleo y cada proceso usa la memoria de su socket (evita efectos NUMA).
Ejemplo completo: matriz × vector híbrido (matvec_hibrido.cpp, semana 11)
Sección titulada «Ejemplo completo: matriz × vector híbrido (matvec_hibrido.cpp, semana 11)»Es el programa de la práctica matriz × vector del parcial, ahora con MPI entre procesos y OpenMP dentro de cada uno:
| Paso | Código | Paradigma |
|---|---|---|
| 1 | MPI_Init_thread(…, MPI_THREAD_FUNNELED, &provided) | MPI: avisa que habrá hilos, pero solo el hilo maestro llama a MPI |
| 2 | #pragma omp parallel for que pone A_loc y y_loc en 0 | first touch (abajo) |
| 3 | MPI_Scatter(A, filas·N, …) + MPI_Bcast(x, N, …) | MPI: cada proceso recibe N/p filas y el vector completo |
| 4 | #pragma omp parallel for schedule(static) sobre las filas locales | OpenMP: los hilos se reparten las filas del proceso |
| 5 | MPI_Gather(y_loc, filas, …) | MPI: rank 0 junta y |
| 6 | verifica y[i] = (i+1)·N(N−1)/2 | resultado conocido, error relativo 0 |
Lo nuevo:
MPI_THREAD_FUNNELED: los niveles de soporte de hilos sonSINGLE<FUNNELED<SERIALIZED<MULTIPLE. Aquí basta FUNNELED porque todas las llamadas MPI están fuera de las regiones paralelas, en el hilo maestro. Lo correcto es comprobar queprovided ≥el nivel pedido.- First touch (efecto NUMA): en Linux, una página de memoria se asigna en el banco de memoria del socket del hilo que la escribe primero. Si
MPI_Scatter, que corre en el hilo maestro, escribieraA_locprimero, toda la matriz quedaría en el socket del maestro, y los hilos del otro socket leerían memoria remota (más lenta). Por eso cada hilo pone a 0 sus filas con el mismoschedule(static)que usará después. Es la “desventaja: efectos NUMA” de la lámina 10, resuelta en el código. OMP_PROC_BIND=close OMP_PLACES=coresfija cada hilo a un núcleo, para que no migre y pierda esa localidad.reps = 50: la matriz se reparte una vez (N² datos) y se multiplica 50 veces. El comentario del código separa “Cómputo (por producto)” de “Comunicación (una vez)”.
Medido al ejecutarlo en una laptop de 8 núcleos y 12 hilos (N = 4096, 50 productos), una corrida por configuración:
| Procesos × hilos | Cómputo por producto | Comunicación (Scatter + Bcast + Gather) |
|---|---|---|
| 1 × 8 | 0.0109 s (3.1 GFLOP/s) | 0.026 s |
| 2 × 4 | 0.0090 s (3.7 GFLOP/s) | 0.024 s |
| 4 × 2 | 0.0127 s (2.7 GFLOP/s) | 0.112 s |
| 8 × 1 | 0.0030 s (11.2 GFLOP/s) ⚠️ | 0.696 s |
Cómo leerlo:
- La comunicación crece con los procesos: repartir 4096² doubles (128 MB) cuesta 0.03 s con 1–2 procesos y 0.7 s con 8. Con más procesos hay más copias, y en un solo nodo los mensajes MPI son copias de memoria. Esa es la ventaja del híbrido: menos procesos ⇒ menos datos movidos.
- ⚠️ El 11.2 GFLOP/s de 8 × 1 no es fiable. El tiempo lo mide solo el rank 0 y los
MPI_Barrierdel código están comentados. Cada proceso empieza a calcular apenas recibe su parte, así que el rank 0 no mide a los demás. Para comparar bien hay que descomentar las barreras y reducir conMPI_MAXel tiempo de todos (observación propia). - Matriz × vector hace 2 flops por cada double leído de A: está limitado por memoria, no por cálculo. Con pocos GFLOP/s el ancho de banda ya se satura.
¿Por qué el híbrido no siempre es más eficiente? (lámina 26)
Sección titulada «¿Por qué el híbrido no siempre es más eficiente? (lámina 26)»- OpenMP escala con el paralelismo implícito, pero solo hasta el límite de hardware de un nodo.
- Durante la comunicación MPI, todos los hilos esperan (idle) menos uno.
- Crear muchos hilos tiene un costo.
- Faltan librerías y compiladores optimizados para OpenMP.
Ejemplo de la lámina 25 (ecuación de Poisson 3D en un IBM Blue Gene de 10 240 nodos × 4 núcleos): las versiones nuevas (optimizadas), tanto MPI puro como MPI + OMP, escalan mucho mejor que las viejas, y entre ellas la híbrida nueva es ligeramente mejor. La mejora viene sobre todo de optimizar el código, no solo de agregar OpenMP.
La lámina también menciona códigos reales: SPHYNX y SPH-EXA (hidrodinámica SPH) y el problema de N cuerpos con OMP vs HPX, híbrido y MPI + CUDA.
Ejercicio
Sección titulada «Ejercicio»Tienes 3 nodos de 16 núcleos y quieres 2 procesos MPI por nodo. Escribe las líneas #SBATCH y el export necesarios. ¿Cuántas líneas imprime el peekaboo si no fijas num_threads(4)?
Ver solución
#SBATCH --nodes=3#SBATCH --ntasks-per-node=2 # 6 procesos MPI en total#SBATCH --cpus-per-task=8 # 16 núcleos / 2 procesosexport OMP_NUM_THREADS=$SLURM_CPUS_PER_TASKmpirun ./hibridoSin num_threads(4), cada proceso usa OMP_NUM_THREADS = 8 hilos: 6 × 8 = 48 líneas. Con num_threads(4) en el código, la cláusula gana sobre la variable de entorno: 6 × 4 = 24 líneas, y quedan 4 núcleos ociosos por proceso.
Errores típicos
Sección titulada «Errores típicos»- Usar
MPI_Initen un programa con hilos que llaman a MPI: hay que pedir el nivel conMPI_Init_thread(como mínimoMPI_THREAD_FUNNELEDsi solo el maestro llama a MPI). - Pedir más procesos × hilos que núcleos (sobresuscripción): los hilos se turnan el núcleo y el tiempo empeora.
- No igualar
OMP_NUM_THREADSa--cpus-per-tasken Slurm: o sobran núcleos ociosos o hay sobresuscripción. - Suponer que el híbrido siempre gana al MPI puro (la lámina 26 explica por qué no).
Práctica extra
Sección titulada «Práctica extra»Tienes 2 nodos de 16 núcleos y quieres 4 procesos MPI por nodo, con todos los núcleos ocupados. ¿Qué pides a Slurm y cuánto vale OMP_NUM_THREADS?
Ver solución
--nodes=2, --ntasks-per-node=4 (8 procesos en total), --cpus-per-task=4 y OMP_NUM_THREADS=4. Total: 8 procesos × 4 hilos = 32 núcleos = 2 × 16. Es el modo híbrido 4 × 4 de la figura, en cada nodo.
Autoevaluación
0/6 correctas · 0 respondidasFuente ·U4.5 Paralelismo-hibrido.pdf (láminas 2–26) · semana_11/matvec_hibrido.cpp