Uso de MPI y OpenMP



Los supercomputadores en general, y Magerit en particular, son capaces de realizar tareas de una forma muy rápida no solo por la calidad de los procesadores, sino especialmente por el reparto de trabajo que se hace de manera que a cada uno le corresponda una o varias subtareas. Mediante la comunicación entre los procesadores, es posible obtener el resultado final. En Magerit el paralelismo se puede conseguir mediante MPI y OpenMP. En este tutorial vemos con un sencillo ejemplo (programas Hola Mundo) cómo funcionan ambos mecanismos y cómo modificar su comportamiento desde el jobfile.

MPI

MPI (Message Passing Interface) es un mecanismo para la programación paralela por paso de mensajes. Por ello, no es necesario que todos los procesos MPI ejecuten en el mismo nodo. El número de procesos MPI se controla con la directiva #@ total_tasks y cada uno ejecuta siempre en un core distinto. En el jobfile, la llamada a srun hace que se lancen el número de procesos definido en #@ total_tasks, cada uno de los cuales ejecuta el programa especificado. En este ejemplo ejecutamos 18 procesos.

  • Código:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sched.h>
    #include <mpi.h>
    
    int main(int argc, char **argv){
        int rank, size;
        MPI_Init(&argc,&argv);
        MPI_Comm_rank(MPI_COMM_WORLD, &rank);
        MPI_Comm_size(MPI_COMM_WORLD, &size);
        printf("Soy el proceso MPI %d de %d. Estoy en el core %d del nodo %s.\n",
        rank, size, sched_getcpu(), getenv("SLURM_NODEID"));
        sleep(5);
        MPI_Finalize();
        return 0;
    }
    
  • Jobfile:

    #!/bin/bash
    #@ arch = (ppc64|power|intel)
    #@ initialdir = /home/<project_id>
    #@ output = SCRATCH/out-HMMPI.log
    #@ error = SCRATCH/err-HMMPI.log
    #@ total_tasks = 18
    #@ wall_clock_limit = 00:02:00
    
    srun PROJECT/holamundompi
    
  • En este caso son necesarios 2 nodos, ya que en uno solo se pueden ejecutar como máximo 16 procesos MPI (uno por core):

    Soy el proceso MPI 1 de 18. Estoy en el core 8 del nodo 0.
    Soy el proceso MPI 3 de 18. Estoy en el core 9 del nodo 0.
    Soy el proceso MPI 16 de 18. Estoy en el core 0 del nodo 1.
    Soy el proceso MPI 2 de 18. Estoy en el core 1 del nodo 0.
    Soy el proceso MPI 4 de 18. Estoy en el core 2 del nodo 0.
    Soy el proceso MPI 5 de 18. Estoy en el core 10 del nodo 0.
    Soy el proceso MPI 6 de 18. Estoy en el core 3 del nodo 0.
    Soy el proceso MPI 8 de 18. Estoy en el core 4 del nodo 0.
    Soy el proceso MPI 10 de 18. Estoy en el core 5 del nodo 0.
    Soy el proceso MPI 7 de 18. Estoy en el core 11 del nodo 0.
    Soy el proceso MPI 9 de 18. Estoy en el core 12 del nodo 0.
    Soy el proceso MPI 12 de 18. Estoy en el core 6 del nodo 0.
    Soy el proceso MPI 11 de 18. Estoy en el core 13 del nodo 0.
    Soy el proceso MPI 13 de 18. Estoy en el core 14 del nodo 0.
    Soy el proceso MPI 14 de 18. Estoy en el core 7 del nodo 0.
    Soy el proceso MPI 0 de 18. Estoy en el core 0 del nodo 0.
    Soy el proceso MPI 15 de 18. Estoy en el core 15 del nodo 0.
    Soy el proceso MPI 17 de 18. Estoy en el core 8 del nodo 1.
    

El reparto en este caso ha sido 2 procesos en el nodo 0 y 16 en el 1. Sin embargo, podría haber sido de otra manera. Es probable que el nodo 0 lo estemos compartiendo con un trabajo de otro usuario.

OpenMP

OpenMP (Open Multi-Processing) es un API para C/C++ y Fortran para la programación paralela de memoria compartida. Esto quiere decir que todos los hilos en los que OpenMP divide el programa comparten memoria RAM. Por tanto, es necesario que todos estén ejecutando en el mismo nodo. El número de hilos de cada proceso MPI se controla con la directiva #@ cpus_per_task. Debido a la arquitectura de los nodos de Magerit, el valor de esta directiva debe ser una potencia de 2 menor o igual a 16. En este ejemplo ejecutamos 8 hilos OpenMP.

  • Código:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sched.h>
    #include <omp.h>
    
    int main (int argc, char **argv){
        int tid,nthreads;
        #pragma omp parallel private(tid)
        {    
            tid = omp_get_thread_num(); 
            nthreads = omp_get_num_threads();
            printf("Soy el hilo  OpenMP %d de %d. Estoy en el core %d del nodo %s.\n", 
            tid, nthreads, sched_getcpu(), getenv("SLURM_NODEID"));
            sleep(5);
        } 
        return 0;
    }
    
  • Jobfile:

    #!/bin/bash
    #@ arch = (ppc64|power|intel)
    #@ initialdir = /home/<project_id>
    #@ output = SCRATCH/out-HMOMP.log
    #@ error = SCRATCH/err-HMOMP.log
    #@ total_tasks = 1
    #@ cpus_per_task = 8
    #@ wall_clock_limit = 00:02:00
    
    srun PROJECT/holamundoomp
    
  • Como vemos, todos los hilos están en el mismo nodo. En la arquitectura Intel es posible que haya varios ejecutando en el mismo core (tecnología Hyper-Threading):

    Soy el hilo  OpenMP 0 de 8. Estoy en el core 15 del nodo 0.
    Soy el hilo  OpenMP 3 de 8. Estoy en el core 8 del nodo 0.
    Soy el hilo  OpenMP 1 de 8. Estoy en el core 9 del nodo 0.
    Soy el hilo  OpenMP 2 de 8. Estoy en el core 10 del nodo 0.
    Soy el hilo  OpenMP 4 de 8. Estoy en el core 11 del nodo 0.
    Soy el hilo  OpenMP 5 de 8. Estoy en el core 8 del nodo 0.
    Soy el hilo  OpenMP 7 de 8. Estoy en el core 9 del nodo 0.
    Soy el hilo  OpenMP 6 de 8. Estoy en el core 8 del nodo 0.
    

MPI + OpenMP

Además de usarlos por separado, también podemos combinar ambos mecanismos. Por ejemplo, podemos hacer que cada proceso MPI ejecute en un nodo distinto y dentro de cada uno se desplieguen varios hilos OpenMP. En este ejemplo ejecutamos 5 procesos MPI, cada uno de los cuales despliega 4 hilos OpenMP.

  • Código:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sched.h>
    #include <mpi.h>
    #include <omp.h>
    
    int main (int argc, char** argv){
        int rank, size; 
        MPI_Init (&argc, &argv);
        MPI_Comm_rank (MPI_COMM_WORLD, &rank);
        MPI_Comm_size (MPI_COMM_WORLD, &size);
        int nthreads, tid;
        #pragma omp parallel private(tid)
        {      
            tid = omp_get_thread_num();
            nthreads = omp_get_num_threads();
            printf("Soy el hilo OpenMP %d de %d del proceso MPI %d de %d. 
            Estoy en el core %d del nodo %s.\n", 
            tid,nthreads,rank,size,sched_getcpu(),getenv("SLURM_NODEID")); 
            sleep(5);
        }  
        MPI_Finalize();
        return 0;
    }
    
  • Jobfile:

    #!/bin/bash
    #@ arch = (ppc64|power|intel)
    #@ initialdir = /home/<project_id>
    #@ output = PROJECT/out-HMOMPMPI.log
    #@ error = PROJECT/err-HMOMPMPI.log
    #@ total_tasks = 5
    #@ cpus_per_task = 4
    #@ wall_clock_limit = 00:02:00
    
    srun PROJECT/holamundompiomp
    
  • Vemos como, de los 5 procesos MPI que hay en total, 4 ejecutan en un nodo y 1 en otro. Obviamente, todos los hilos de un mismo proceso están en el mismo nodo, pues comparten memoria.

    Soy el hilo OpenMP 0 de 4 del proceso MPI 0 de 5. Estoy en el core 11 del nodo 0.
    Soy el hilo OpenMP 3 de 4 del proceso MPI 0 de 5. Estoy en el core 9 del nodo 0.
    Soy el hilo OpenMP 1 de 4 del proceso MPI 0 de 5. Estoy en el core 9 del nodo 0.
    Soy el hilo OpenMP 2 de 4 del proceso MPI 0 de 5. Estoy en el core 8 del nodo 0.
    Soy el hilo OpenMP 2 de 4 del proceso MPI 2 de 5. Estoy en el core 10 del nodo 1.
    Soy el hilo OpenMP 0 de 4 del proceso MPI 2 de 5. Estoy en el core 11 del nodo 1.
    Soy el hilo OpenMP 3 de 4 del proceso MPI 2 de 5. Estoy en el core 11 del nodo 1.
    Soy el hilo OpenMP 1 de 4 del proceso MPI 2 de 5. Estoy en el core 2 del nodo 1.
    Soy el hilo OpenMP 0 de 4 del proceso MPI 3 de 5. Estoy en el core 13 del nodo 1.
    Soy el hilo OpenMP 2 de 4 del proceso MPI 3 de 5. Estoy en el core 13 del nodo 1.
    Soy el hilo OpenMP 1 de 4 del proceso MPI 3 de 5. Estoy en el core 4 del nodo 1.
    Soy el hilo OpenMP 3 de 4 del proceso MPI 3 de 5. Estoy en el core 5 del nodo 1.
    Soy el hilo OpenMP 1 de 4 del proceso MPI 4 de 5. Estoy en el core 14 del nodo 1.
    Soy el hilo OpenMP 0 de 4 del proceso MPI 4 de 5. Estoy en el core 15 del nodo 1.
    Soy el hilo OpenMP 3 de 4 del proceso MPI 4 de 5. Estoy en el core 14 del nodo 1.
    Soy el hilo OpenMP 2 de 4 del proceso MPI 4 de 5. Estoy en el core 7 del nodo 1.
    Soy el hilo OpenMP 0 de 4 del proceso MPI 1 de 5. Estoy en el core 9 del nodo 1.
    Soy el hilo OpenMP 1 de 4 del proceso MPI 1 de 5. Estoy en el core 0 del nodo 1.
    Soy el hilo OpenMP 2 de 4 del proceso MPI 1 de 5. Estoy en el core 1 del nodo 1.
    Soy el hilo OpenMP 3 de 4 del proceso MPI 1 de 5. Estoy en el core 8 del nodo 1.
    
    

Anexo: directiva tasks_per_node

Mediante el uso de la directiva #@ tasks_per_node es posible indicar cuántas tareas (procesos MPI) queremos que se ejecuten como máximo en cada nodo. Esto sirve para conseguir más memoria RAM por proceso. No obstante, para asegurarnos de que esto sea así, debemos reservar un nodo en exclusiva, porque si no es posible que los cores del nodo que no está ocupando nuestro trabajo los esté ocupando otro. Para conseguir esto, el producto tasks_per_node · cpus_per_task debe dar como resultado 16, que es el número de cores de cada nodo.

En este primer ejemplo, hemos añadido al jobfile del programa que únicamente usa MPI las directivas #@ tasks_per_node = 8 y #@ cpus_per_task = 2. Como vemos, ya no se ejecuta en 2 nodos sino en 3 diferentes:

Soy el proceso MPI 1 de 18. Estoy en el core 9 del nodo 0.
Soy el proceso MPI 13 de 18. Estoy en el core 1 del nodo 2.
Soy el proceso MPI 2 de 18. Estoy en el core 2 del nodo 0.
Soy el proceso MPI 14 de 18. Estoy en el core 2 del nodo 2.
Soy el proceso MPI 3 de 18. Estoy en el core 3 del nodo 0.
Soy el proceso MPI 9 de 18. Estoy en el core 3 del nodo 1.
Soy el proceso MPI 15 de 18. Estoy en el core 11 del nodo 2.
Soy el proceso MPI 4 de 18. Estoy en el core 12 del nodo 0.
Soy el proceso MPI 16 de 18. Estoy en el core 12 del nodo 2.
Soy el proceso MPI 5 de 18. Estoy en el core 13 del nodo 0.
Soy el proceso MPI 0 de 18. Estoy en el core 0 del nodo 0.
Soy el proceso MPI 17 de 18. Estoy en el core 13 del nodo 2.
Soy el proceso MPI 12 de 18. Estoy en el core 0 del nodo 2.
Soy el proceso MPI 7 de 18. Estoy en el core 9 del nodo 1.
Soy el proceso MPI 8 de 18. Estoy en el core 10 del nodo 1.
Soy el proceso MPI 10 de 18. Estoy en el core 12 del nodo 1.
Soy el proceso MPI 11 de 18. Estoy en el core 13 del nodo 1.
Soy el proceso MPI 6 de 18. Estoy en el core 0 del nodo 1.

Además, podemos estar seguros de que en esos tres nodos solo está ejecutando nuestro trabajo, pudiendo disponer así de toda la RAM.

Si al jobfile que hemos usado para mostrar el uso de OpenMP y le añadimos la directiva #@ tasks_per_node = 2 estaremos seguros de que los 8 cores que no usa nuestro trabajo quedan libres, aunque la salida estándar será la misma.

Por último, si en el jobfile correspondiente al programa que combina OpenMP con MPI añadimos #@ tasks_per_node = 2 ya no ejecutará en 2 nodos, sino en 3. En este caso no podemos garantizar que en estos nodos solo ejecutamos nosotros, es posible que tengamos que compartir memoria.

Nota
Para obtener más información puede visitar las webs oficiales de Open MPI y de MVAPICH, las dos implementaciones de MPI presentes en Magerit. Puede visitar también la web de OpenMP.