570 likes | 726 Views
Teo. 5: Paradigma "Message Passing". Algoritmos paralelos Glen Rodríguez. Introducción.
E N D
Teo. 5: Paradigma "Message Passing" Algoritmos paralelos Glen Rodríguez
Introducción • El paradigma de paso de mensajes es uno de los más viejos en computación paralela. Su adopción es amplia, debido a su antigüedad y al hecho que no exige mayores demandas del hardware, sólo que haya algún tipo de comunicación entre CPUs.
Principios del "Message-Passing" • Atributos clave: • Espacio de memoria particionado (no usa memoria compartida) • Sólo soporta paralelización explícita (programada directamente por el programador) • Vista lógica: “p” nodos o CPUs corriendo “p” procesos, cada uno con su espacio propio de memoria.
Principios del "Message-Passing" • Cada variable básica debe pertenecer a una de esas memorias: partición explícita de la data. Hay que aprovechar localidad de la data • Todas las interacciones (read-only o read/write) necesitan la cooperación de 2 procesos, uno que envía y otro que recibe data. El proceso que envía a veces no tiene mayor relación de cómputo con la data que envía: Patrones de uso de data no naturales. Especialmente en programas dinámicos y/o no estructurados. Pero el programador siempre sabe el costo de comunicación del programa.
Estructura de programas con paso de mensajes • Paradigma asíncrono o débilmente síncrono. En el asíncrono, todas las tareas concurrentes se ejecutan asíncronamente. Fácil de programar, pero riesgo de "race conditions" y comportamiento no predecible. • Los débilmente síncronos son un poco más tediosos de programar, pero con mucho menos desventajas. las tareas se sincronizan para interactuar. Salvo eso, son asíncronas. Seguir y predecir el programa es más fácil.
Estructura de programas con paso de mensajes • Más flexible • Menos escalable (modificar a mano el programa) • Se adecua al modelo SPMD.
Operaciones Send y Receive • Esquema general: • send(void *sendbuf, int nelems, int dest) • receive(void *recvbuf, int nelems, int source) • Donde sendbuf apunta a una o más variables (array) que serán enviadas, y recvbuf a las que se reciben, nelems es el número de variables, dest y source identifican al proceso destino y origen.
Operaciones Send y Receive • Se envía de verdad la(s) variable(s) en sendbuf? Mire estos procesos: • P0 P1 • a = 100; receive(&a, 1, 0) • send(&a, 1, 1); printf("%d\n", a); • a=0; • Se debe enviar a=100, no a=0. • Si bloqueo en send: OK. Pero si no bloqueo, tengo que copiar a a una var. temporal ! Si hay soporte de DMA, send puede trabajar mientras ejecuto a=0.
Operaciones con bloqueo • Send/receive con bloqueo y sin buffers
Operaciones con bloqueo • En (a) y (c) se puede observar tiempos ociosos (por que send y receive no coinciden en el tiempo) • OJO: Deadlock (bloqueo mutuo) • P0 P1 • send(&a, 1, 1); send(&a, 1, 0); • receive(&b, 1, 1); receive(&b, 1, 0);
Operaciones con bloqueo • Send/receive con bloqueo y con buffers (a) Con soporte de HW para los buffers (b) Sin soporte de HW para buffers
Operaciones con bloqueo • Menos tiempo ocioso pero más administración (de buffers). • Espacio de memoria para buffers no es infinito. • Impacto: • P0 P1 • for (i = 0; i < 1000; i++) { for (i = 0; i < 1000; i++) { • produce_data(&a); receive(&a, 1, 0); • send(&a, 1, 1); consume_data(&a); • } } • Analizar: Si son simultaneos vs. P1 se atrasa. Se llenará el buffer? Que pasa si se llena?
Operaciones con bloqueo • Bloqueos mutuos: si hay buffer, aún podrían haber deadlocks, por que los receive aún son bloqueantes. • P0 P1 • receive(&a, 1, 1); receive(&a, 1, 0); • send(&b, 1, 1); send(&b, 1, 0); • Por qué receive es bloqueante? Qué pasaría si ejecute este receive y luego pido x=a+1? consistencia semántica
Operaciones no bloqueantes • No aseguran consistencia semántica, pero son potencialmente más rápidos. • Se acompañan de operaciones check-status, para saber si se puede violar la consistencia de un send o receive ya ejecutado. • Después se puede verificar si la comunicación de datos se llevó a cabo y/o esperar que acaben.
Operaciones no bloqueantes Se postean mensajes de “data lista para enviar” o “lista para recibir”
MPI (Message Passing Interface) • Es una interfaz estándar (1993) para la programación usando el paradigma de paso de mensajes. • Sirve tanto para grandes computadores de memoria compartida, como para clusters o redes de ordenadores heterogéneos (incluido el grid computing). • Definida para C, C++ y FORTRAN. • Comprende una gran cantidad de funciones (más de 120), macros, etc. • Pero con 6 funciones básicas se puede empezar a programar usando MPI.
Implementaciones de MPI • Existen bastantes implementaciones del estándar MPI. Algunas son debidas a proveedores de hardware que las proporcionan (optimizadas) paras sus máquinas (IBM, HP, SGI…), otras son desarrolladas en el ámbito académico. • Algunas implementaciones son: • MPICH (MPICH 2 implementa MPI2) • Disponible para múltiples devices incluido globus2. • LAM/MPI • OpenMPI unión de varios proyectos (FT-MPI, LA-MPI, LAM/MPI, y PACX-MPI) .
Uso de MPI • Se puede usar MPI para: • Programas paralelos “portables”. • Librerías paralelas. • Programas que no se ajusten a un modelo de paralelismo en los datos. • Cuando no usar MPI: • Si se puede usar HPF o un programa paralelo Fortran 90. • Si se pueden usar librerías de más alto nivel (que pueden haber sido escritas usando MPI). • Si se usa 1 sola computadora con SMP (varios CPUs o cores) podrías usarse OpenMP
MPI tipos de datos • Se definen los siguientes tipos de datos MPI: • MPI_CHAR char • MPI_SHORT short int • MPI_INT int • MPI_LONG long int • MPI_UNSIGNED_CHAR • MPI_UNSIGNED_SHORT • MPI_UNSIGNED • MPI_UNSIGNED_LONG • MPI_FLOAT float • MPI_DOUBLE double • MPI_LONG_DOUBLE long double • MPI_BYTE • MPI_PACKED • Corresponde a los de C, pero se añaden el tipo byte, y el empaquetado, que permite enviar simultáneamente datos de distintos tipos.
MPI Funciones básicas • Funciones básicas: • MPI_Init => Inicialización de MPI. • MPI_Finalize => Termina MPI. • MPI_Comm_size => Para averiguar el número de procesos. • MPI_Comm_rank => Identifica el proceso. • MPI_Send => Envía un mensaje. • MPI_Recv => Recibe un mensaje. • Referencia del estándar en • http://www-unix.mcs.anl.gov/mpi/
MPICH • MPICH es una implementación gratuita de MPI 1.2 • Implementa parte del nuevo estándar MPI-2 • MPICH 2 implementa MPI-2 • Es una implementación muy “portable”: • Supercomputadores de memoria distribuida • Sistemas de memoria compartida • Clusters • Grid • Esta se puede compilar para distintos “devices” de comunicación. Consideraremos dos: • Ch3:nemesis • Usa meoria compartida, Myrinet o sockets según convenga. • Ch3:sock • Usa sockets
Escribiendo programas MPI • De las seis funciones básicas que mencionamos antes MPI_Init y MPI_Finalize son imprescindibles para que haya un programa MPI. • Veamos un ejemplo trivial (hello.c) #include "mpi.h" #include <stdio.h> int main( int argc, char **argv ) { MPI_Init( &argc, &argv ); printf( "Hola Mundo\n" ); MPI_Finalize(); return 0; }
Programas MPI (compilación) • El programa anterior solo inicializa y termina el entorno MPI. Entre tanto cada proceso imprime un mensaje por pantalla. • Para un programa pequeño como este podemos hacer una llamada directamente al comando de compilación: • mpicc (para programas C) • mpif77 (Fortran 77). • mpif90 (Fortran 90) • mpicxx (C++) • Para aplicaciones más complicadas conviene usar un Makefile. • En nuestro caso anterior: mpicc –o hello hello.c
Ejecución de un programa MPI con MPICH • El comando mpiexec se usa para ejecutar los programas MPI en MPICH. • mpiexec admite diversas opciones. Destacaremos las siguientes: • -n N: N indica el número de procesos que se quiere en la ejecución del programa. • -machinefile mfile: fichero de máquinas para correr en lugar del estándar (se usa en combinación con –n). • Si queremos correr nuestro programa en dos máquinas de la lista estándar machinefile: mpiexec -n 2 hello
Ejemplo de machinefile grid073.pepe.edu.pe:2 grid074.pepe.edu.pe:2 grid075.pepe.edu.pe:4 grid076.pepe.edu.pe:2 grid077.pepe.edu.pe:1 grid078.pepe.edu.pe:2 198.162.0.10:2 198.162.0.11:1 198.162.0.12:1 198.162.0.13:4 198.162.0.14:2
Delimitación del alcance de la comunicación • Grupos separados de subprocesos trabajando en diferentes subprogramas. • Invocación paralela de librerías paralelas: • Los mensajes propios de la aplicación deben mantenerse separados de los de la librería. • MPI proporciona grupos de procesos • Inicialmente el grupo “all”. • Se proporcionan rutinas para la administración de los grupos. • Todas las comunicaciones, no solo las colectivas, tienen lugar en grupos. • Un grupo (group) y un contexto (context) se combinan en un comunicador (communicator). • Fuente y destino en una operación send o receive se refieren al rank en el grupo asociado con un comunicador dado. Se puede usar MPI_ANY_SOURCE en una operación receive.
MPI tipos de comunicación • La comunicación MPI entre procesos puede ser de dos tipos: • Punto a punto: el proceso “origen” conoce el identificador del proceso “destino” y envía un mensaje dirigido solo a él. Se usan las funciones MPI_Send y MPI_Recv. • Colectiva: Operaciones en las que participan todos los procesos de un operador. Ejemplo: • “Broadcast”: El proceso origen manda el mensaje a todos los demas (que pertenezcan al mismo comunicador). Esto es típico de un esquema “master-slave”. Se usa la función MPI_Bcast. • Las funciones MPI de recepción de datos son por lo general “bloqueantes”, es decir, un proceso que debe recibir un mensaje espera hasta que de hecho lo ha recibido completo.
Obteniendo información de MPI • Usaremos ahora dos más de las seis funciones básicas: MPI_Comm_size y MPI_Comm_rank. • Así averiguaremos desde el programa el número de procesos que participan y la identificación de cada uno. int main( argc, argv ) { int rank, size; MPI_Init( &argc, &argv ); MPI_Comm_rank( MPI_COMM_WORLD, &rank ); MPI_Comm_size( MPI_COMM_WORLD, &size ); printf( “Hola Mundo! Soy el proceso %d de %d\n", rank, size ); MPI_Finalize(); return 0; }
Funciones de comunicación MPI • A continuación veremos un poco más en detalle las funciones básicas de envío y recepción de mensajes punto a punto y broadcast. • MPI_Send • MPI_Recv • MPI_Bcast • MPI_Reduce
La operación básica de envío (bloqueante) es: La operación básica de recepción correspondiente: Sintaxis de MPI_Send y MPI_Recv MPI_Send( start, count, datatype, dest, tag, comm ) • start: puntero a los datos a enviar • count: número de elementos a enviar • datatype: tipo de dato • dest: Identificación del proceso destino • tag: etiqueta de la comunicación • comm: Identificación del comunicador MPI_Recv(start, count, datatype, source, tag, comm, status) • start: puntero para la recepción de los datos • count: número de elementos • datatype: tipo de dato • source: Identificación del proceso origen • tag: etiqueta de la comunicación • comm: Identificación del comunicador • status: puntero para acceso a información sobre mensaje
Ejemplo MPI_Send/MPI_Recv char msg[100]; if(my_rank==0) { sprintf(msg,"\n\n\t Esto es un mensaje del proceso %d al proceso %d",source,dest); MPI_Send(msg,100,MPI_CHAR,dest,TAG,MPI_COMM_WORLD); printf("\n Mensaje enviado a %d",dest); } else if(my_rank==1) { MPI_Recv(msg,100,MPI_CHAR,source,TAG,MPI_COMM_WORLD,&status); printf("\n Mensaje recibido en %d",dest); printf(msg); }
Adquiriendo información sobre un mensaje • MPI_TAG y MPI_SOURCE son usados principalmente cuando hay MPI_ANY_TAG y/o MPI_ANY_SOURCE en la llamada MPI_Recv. • MPI_Get_count se usa para determinar la cantidad de datos de un tipo dado que se han recibido. MPI_Status status; MPI_Recv( ..., &status ); ... status.MPI_TAG; ... status.MPI_SOURCE; MPI_Get_count( &status, datatype, &count );
Status • Para saber que paso con un receive typedef struct MPI_Status { int MPI_SOURCE; int MPI_TAG; int MPI_ERROR; };
Ojo con los tags • Que pasa si no se usan buffers?: • int a[10], b[10], myrank; • MPI_Status status; • ... • MPI_Comm_rank(MPI_COMM_WORLD, &myrank); • if (myrank == 0) { • MPI_Send(a, 10, MPI_INT, 1, 1, MPI_COMM_WORLD); • MPI_Send(b, 10, MPI_INT, 1, 2, MPI_COMM_WORLD); • } • else if (myrank == 1) { • MPI_Recv(b, 10, MPI_INT, 0, 2, MPI_COMM_WORLD); • MPI_Recv(a, 10, MPI_INT, 0, 1, MPI_COMM_WORLD); • }
Mejor así (impares envían primero) • int a[10], b[10], npes, myrank; • MPI_Status status; • ... • MPI_Comm_size(MPI_COMM_WORLD, &npes); • MPI_Comm_rank(MPI_COMM_WORLD, &myrank); • if (myrank%2 == 1) { • MPI_Send(a, 10, MPI_INT, (myrank+1)%npes, 1, MPI_COMM_WORLD); • MPI_Recv(b, 10, MPI_INT, (myrank-1+npes)%npes, 1, MPI_COMM_WORLD); • } • else { • MPI_Recv(b, 10, MPI_INT, (myrank-1+npes)%npes, 1, MPI_COMM_WORLD); • MPI_Send(a, 10, MPI_INT, (myrank+1)%npes, 1, MPI_COMM_WORLD); • }
Enviando y recibiendo a la vez • MPI_Sendrecv envía y recibe a la vez. • MPI_Sendrecv no tiene los problemas de bloqueo mutuo circular como MPI_Send y el MPI_Recv: • int MPI_Sendrecv(void *sendbuf, int sendcount, MPI_Datatype senddatatype, int dest, int sendtag, void *recvbuf, int recvcount, MPI_Datatype recvdatatype, int source, int recvtag, MPI_Comm comm, MPI_Status *status)
Ejemplo • int a[10], b[10], npes, myrank; • MPI_Status status; • … • MPI_Comm_size(MPI_COMM_WORLD, &npes); • MPI_Comm_rank(MPI_COMM_WORLD, &myrank); • MPI_SendRecv(a, 10, MPI_INT, (myrank+1)%npes, 1, b, 10, MPI_INT, (myrank-1+npes)%npes, 1, MPI_COMM_WORLD, &status); • ...
Overlapping de Comunicación y Computación: No bloqueantes • MPI_Send y MPI_Recv son bloqueantes con buffer. • Versiones No Bloqueantes: • MPI_Isend(start, count, datatype, dest, tag, comm, request) • MPI_Irecv(start, count, datatype, dest, tag, comm, request) • MPI_Wait(request, status) • MPI_Test(MPI_Request *request, int *flag, MPI_Status *status)
Ejemplo • int a[10], b[10], myrank; • MPI_Status status; • MPI_Request requests[2]; • ... • MPI_Comm_rank(MPI_COMM_WORLD, &myrank); • if (myrank == 0) { • MPI_Send(a, 10, MPI_INT, 1, 1, MPI_COMM_WORLD); • MPI_Send(b, 10, MPI_INT, 1, 2, MPI_COMM_WORLD); • } • else if (myrank == 1) { • MPI_Irecv(b, 10, MPI_INT, 0, 2, &requests[0], MPI_COMM_WORLD); • MPI_Irecv(a, 10, MPI_INT, 0, 1, &requests[1], MPI_COMM_WORLD); • } • ...
Operaciones colectivas • Comunicación colectiva: envío de un mensaje de uno a muchos. Se hace con MPI_Bcast (Broadcast) • Típicamente un master envía los mismos datos a sus esclavos. • Por ejemplo, en la paralelización del entrenamiento de una red neuronal enviamos a todos los esclavos los nuevos pesos al final de cada época de entrenamiento. • Operaciones colectivas: se realiza una operación matemática distribuida y se devuelve el resultado al root de la operación • También es típico en un esquema master-slave. • En la neura se utiliza por ejemplo en la suma de los errores de todos los esclavos. • Operaciones definidas: • Aritméticas: suma, multiplicación… • Lógicas: AND, OR…
Broadcast: Reduce: Sintaxis de Broadcast y Reduce MPI_Bcast(start, count, datatype, root, comm) • start: puntero a los datos a enviar • count: número de elementos a enviar • datatype: tipo de dato • root: identificación del proceso origen • comm: Identificación del comunicador MPI_Reduce(start, result, count, datatype, operation, root, comm) • start: puntero a los datos a enviar • result: puntero para almacenar el resultado • count: número de elementos a enviar • datatype: tipo de dato • operation: identificación de la operación colectiva • root: identificación del proceso origen • comm: Identificación del comunicador
Operaciones de Reduce • MPI_MAX maximum integer, float, real, complex • MPI_MIN minimum integer, float, real, complex • MPI_SUM sum integer, float, real, complex • MPI_PROD product integer, float, real, complex • MPI_LAND logical AND integer logical • MPI_BAND bit-wise AND integer, MPI_BYTE • MPI_LOR logical OR integer logical • MPI_BOR bit-wise OR integer, MPI_BYTE • MPI_LXOR logical XOR integer logical • MPI_BXOR bit-wise XOR integer, MPI_BYTE • MPI_MAXLOC max value and location • MPI_MINLOC min value and location
Ejemplo BroadCast Char msg[100]; if(my_rank==source) { sprintf(msg,"\n Esto es un mensaje del proceso %d a todos los demás",sourc e); MPI_Bcast(msg,100,MPI_CHAR,source,MPI_COMM_WORLD); printf("\n Mensaje enviado a todos desde %d",source); } else { MPI_Bcast(msg,100,MPI_CHAR,source,MPI_COMM_WORLD); printf("\n Mensaje recibido en %d desde %d",my_rank,source); printf(msg); }
Ejemplo Reduce int value; int result; value = my_rank; MPI_Reduce(&value,&result,1,MPI_INT,MPI_SUM,source,MPI_COMM_WORLD); if(my_rank==source) { printf("\n Resultado de la suma colectiva %d", result); }
Topologías y Embedding • Topología por default en MPI: lineal (0,1,2,…) • Se pueden crear topología virtuales o cartesianas (1D, 2D, 3D, etc)
Creando topología • int MPI_Cart_create(MPI_Comm comm_old, int ndims, int *dims, int *periods, int reorder, MPI_Comm *comm_cart) • Ejemplo (a) con reorden MPI_Comm comm_2d; dims[0] = dims[1] = 4; reorden=1; /* periods 0 si plano, 1 si vuelta al mundo */ periods[0] = periods[1] = 1; MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, reorden, &comm_2d);
Nombrando procesos en una topología • Hay dos IDs: Coordenadas, rango • Dado las coordenadas, obtener rango: • int MPI_Cart_rank(MPI_Comm comm_cart, int *coords, int *rank) • Dado rango, obtener coordenadas: • int MPI_Cart_coords(MPI_Comm comm_cart, int rank, int maxdims, int *coords)