mpi: locking vs no locking

Tengo problemas para entender el concepto de locking de comunicación y comunicación sin locking en MPI. ¿Cuáles son las diferencias entre los dos? ¿Cuáles son las ventajas y desventajas?

¡Gracias!

La comunicación de locking se realiza utilizando MPI_Send() y MPI_Recv() . Estas funciones no regresan (es decir, bloquean) hasta que la comunicación finaliza. Simplificando algo, esto significa que el búfer pasado a MPI_Send() se puede reutilizar, ya sea porque MPI lo guardó en algún lugar o porque el destino lo recibió. De forma similar, MPI_Recv() regresa cuando el buffer de recepción ha sido llenado con datos válidos.

Por el contrario, la comunicación sin locking se realiza utilizando MPI_Isend() y MPI_Irecv() . Estas funciones regresan inmediatamente (es decir, no bloquean) incluso si la comunicación aún no ha finalizado. Debe llamar a MPI_Wait() o MPI_Test() para ver si la comunicación ha finalizado.

La comunicación de locking se usa cuando es suficiente, ya que es algo más fácil de usar. La comunicación sin locking se usa cuando es necesario, por ejemplo, puede llamar a MPI_Isend() , hacer algunos cálculos y luego hacer MPI_Wait() . Esto permite que los cálculos y la comunicación se superpongan, lo que generalmente conduce a un mejor rendimiento.

Tenga en cuenta que la comunicación colectiva (por ejemplo, all-reducir) solo está disponible en su versión de locking hasta MPIv2. IIRC, MPIv3 introduce comunicación colectiva no bloqueante.

Aquí puede ver una descripción general rápida de los modos de envío de MPI.

Esta publicación, aunque es un poco antigua, pero contengo la respuesta aceptada. la statement “Estas funciones no vuelven hasta que la comunicación haya finalizado” es un poco erróneo porque el locking de las comunicaciones no garantiza ningún apretón de manos por las operaciones de envío y recepción.

Lo primero que debe saber es que el envío tiene cuatro modos de comunicación: Estándar, Tampón, Sincrónico y Listo, y cada uno de estos puede ser de locking y no locking.

A diferencia del envío, recibir tiene solo un modo y puede ser de locking o no locking .

Antes de seguir adelante, también se debe aclarar que menciono explícitamente cuál es el búfer MPI_Send \ Recv y cuál es el búfer del sistema (que es un búfer local en cada procesador propiedad de la biblioteca MPI utilizada para mover datos entre los rangos de una comunicación grupo)

BLOQUEO DE LA COMUNICACIÓN : El locking no significa que el mensaje fue entregado al receptor / destino. Simplemente significa que el búfer (enviar o recibir) está disponible para su reutilización. Para reutilizar el búfer, es suficiente copiar la información a otra área de memoria, es decir, la biblioteca puede copiar los datos del búfer a la ubicación de memoria propia en la biblioteca y luego, digamos por ejemplo, puede devolver MPI_Send.

El estándar MPI deja muy claro desacoplar el almacenamiento en memoria intermedia del mensaje de las operaciones de envío y recepción. Un envío de locking se puede completar tan pronto como se haya almacenado el mensaje en el buzón, aunque no se haya publicado ninguna recepción coincidente. Pero en algunos casos, el almacenamiento en memoria intermedia de mensajes puede ser costoso y, por lo tanto, la copia directa desde el búfer de envío al búfer de recepción puede ser eficiente. Por lo tanto, el estándar MPI proporciona cuatro modos de envío diferentes para darle al usuario cierta libertad para seleccionar el modo de envío apropiado para su aplicación. Echemos un vistazo a lo que sucede en cada modo de comunicación:

1. Modo estándar

En el modo estándar , depende de la Biblioteca MPI, ya sea que almacene el mensaje saliente o no. En el caso en que la biblioteca decida almacenar en búfer el mensaje saliente, el envío puede completarse incluso antes de que se haya invocado la recepción coincidente. En el caso en que la biblioteca decida no almacenar en búfer (por razones de rendimiento, o por falta de espacio en el búfer), el envío no volverá hasta que se haya publicado una recepción coincidente y los datos en el búfer de envío se hayan movido al búfer de recepción.

Por lo tanto, MPI_Send en modo estándar no es local en el sentido de que el envío en modo estándar puede iniciarse ya sea que se haya publicado una recepción coincidente y su finalización exitosa puede depender de la ocurrencia de una recepción coincidente (debido a que es implementación dependiente si el mensaje será almacenado o no).

La syntax para el envío estándar está a continuación:

 int MPI_Send(const void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm) 

2. Modo Buffered

Al igual que en el modo estándar, el envío en modo almacenado se puede iniciar independientemente del hecho de que se haya publicado una recepción coincidente y el envío se complete antes de que se publique una recepción coincidente. Sin embargo, la diferencia principal surge del hecho de que si el envío se mira y no se publica una recepción coincidente, el mensaje saliente debe almacenarse en búfer. Tenga en cuenta que si se envía la recepción coincidente, el envío en búfer puede reunirse felizmente con el procesador que inició la recepción, pero en caso de que no haya recepción, el envío en modo de búfer debe almacenar el mensaje saliente para permitir que se complete el envío. En su totalidad, un envío en búfer es local . La asignación de búfer en este caso está definida por el usuario y en el caso de que no haya suficiente espacio en el búfer, se produce un error.

Sintaxis para el envío de buffer:

 int MPI_Bsend(const void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm) 

3. Modo sincrónico

En el modo de envío síncrono, el envío se puede iniciar ya sea que se haya publicado o no una recepción coincidente. Sin embargo, el envío se completará satisfactoriamente solo si se ha publicado una recepción coincidente y el receptor ha comenzado a recibir el mensaje enviado por el envío síncrono. La finalización del envío síncrono no solo indica que el búfer en el envío puede reutilizarse, sino también el hecho de que el proceso de recepción ha comenzado a recibir los datos. Si tanto el envío como la recepción se bloquean, la comunicación no se completa en ninguno de los extremos antes de que el procesador se comunique con el procesador.

Sintaxis para envío síncrono:

 int MPI_Ssend(const void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm) 

4. Modo listo

A diferencia de los tres modos anteriores, un envío en el modo listo solo se puede iniciar si la recepción coincidente ya se ha publicado. La finalización del envío no indica nada acerca de la recepción coincidente y simplemente dice que el búfer de envío se puede reutilizar. Un envío que utiliza el modo listo tiene la misma semántica que el modo estándar o un modo síncrono con la información adicional sobre una recepción coincidente. Un progtwig correcto con un modo de comunicación preparado puede reemplazarse con envío síncrono o un envío estándar sin efecto en el resultado, aparte de la diferencia de rendimiento.

Sintaxis para el envío listo:

 int MPI_Rsend(const void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm) 

Habiendo revisado los 4 envíos de locking, pueden parecer diferentes en principio, pero dependiendo de la implementación, la semántica de un modo puede ser similar a la de otro.

Por ejemplo, MPI_Send en general es un modo de locking, pero dependiendo de la implementación, si el tamaño del mensaje no es demasiado grande, MPI_Send copiará el mensaje saliente del búfer de envío al búfer del sistema (‘que es el caso en el sistema moderno) y regresará inmediatamente. Veamos un ejemplo a continuación:

 //assume there are 4 processors numbered from 0 to 3 if(rank==0){ tag=2; MPI_Send(&send_buff1, 1, MPI_DOUBLE, 1, tag, MPI_COMM_WORLD); MPI_Send(&send_buff2, 1, MPI_DOUBLE, 2, tag, MPI_COMM_WORLD); MPI_Recv(&recv_buff1, MPI_FLOAT, 3, 5, MPI_COMM_WORLD); MPI_Recv(&recv_buff2, MPI_INT, 1, 10, MPI_COMM_WORLD); } else if(rank==1){ tag = 10; //receive statement missing, nothing received from proc 0 MPI_Send(&send_buff3, 1, MPI_INT, 0, tag, MPI_COMM_WORLD); MPI_Send(&send_buff3, 1, MPI_INT, 3, tag, MPI_COMM_WORLD); } else if(rank==2){ MPI_Recv(&recv_buff, 1, MPI_DOUBLE, 0, 2, MPI_COMM_WORLD); //do something with receive buffer } else{ //if rank == 3 MPI_Send(send_buff, 1, MPI_FLOAT, 0, 5, MPI_COMM_WORLD); MPI_Recv(recv_buff, 1, MPI_INT, 1, 10, MPI_COMM_WORLD); } 

Veamos qué está sucediendo en cada rango en el ejemplo anterior

El rango 0 está intentando enviar al rango 1 y al rango 2, y recibir del rango 1 andd 3.

El rango 1 está intentando enviar al rango 0 y al rango 3 y no recibir nada de ningún otro rango

El rango 2 está tratando de recibir del rango 0 y luego realizar alguna operación con los datos recibidos en el recv_buff.

El rango 3 está intentando enviar al rango 0 y recibir del rango 1

Donde los principiantes se confunden es que el rango 0 está enviando al rango 1 pero el rango 1 no ha comenzado ninguna operación de recepción, por lo tanto, la comunicación debe bloquear o atascar y el segundo en el rango 0 no debe ejecutarse (y esto es lo que MPI la documentación enfatiza que se define su implementación independientemente de si el mensaje saliente será almacenado o no). En la mayoría de los sistemas modernos, los mensajes de tamaños pequeños (aquí el tamaño es 1) se almacenarán fácilmente y MPI_Send devolverá y ejecutará su próxima instrucción MPI_Send. Por lo tanto, en el ejemplo anterior, incluso si no se inicia la recepción en el rango 1, volverá 1st MPI_Send en el rango 0 y ejecutará su siguiente statement.

En una situación hipotética donde el rango 3 comienza la ejecución antes del rango 0, copiará el mensaje saliente en la primera instrucción de envío del búfer de envío a un búfer del sistema (en un sistema moderno;) y luego comenzará a ejecutar su instrucción de recepción. Tan pronto como el rango 0 finaliza sus dos instrucciones de envío y comienza a ejecutar su instrucción de recepción, los datos almacenados en el sistema por rango 3 se copian en el buffer de recepción en el rango 0.

En caso de que haya una operación de recepción iniciada en un procesador y no se envíe un envío coincidente, el proceso se bloqueará hasta que el búfer de recepción se llene con los datos que está esperando. En esta situación, un cómputo u otra comunicación MPI será bloqueada / detenida a menos que MPI_Recv haya regresado.

Habiendo entendido los fenómenos de almacenamiento en búfer , uno debería volver y pensar más acerca de MPI_Ssend, que tiene la verdadera semántica de una comunicación de locking. Incluso si MPI_Ssend copia el mensaje saliente desde el búfer de envío a un búfer del sistema (que nuevamente se define como implementación), se debe tener en cuenta que MPI_Ssend no regresará a menos que el procesador emisor haya recibido algún reconocimiento (en formato de bajo nivel) del proceso receptor.

Afortunadamente, MPI decidió mantener las cosas más fáciles para los usuarios en términos de recepción y solo hay una recepción en la comunicación de locking: MPI_Recv , y se puede usar con cualquiera de los cuatro modos de envío descritos anteriormente. Para MPI_Recv, bloquear significa que recibe devoluciones solo después de que contiene los datos en su búfer. Esto implica que la recepción puede completarse solo después de que se haya iniciado un envío coincidente, pero no implica si puede completarse o no antes de que se complete el envío coincidente.

Lo que sucede durante tales llamadas de locking es que los cálculos se detienen hasta que se libera el búfer bloqueado. Esto generalmente conduce al desperdicio de recursos computacionales ya que Send / Recv usualmente copia datos desde una ubicación de memoria a otra ubicación de memoria, mientras que los registros en la CPU permanecen inactivos.

COMUNICACIÓN SIN BLOQUEO : para la comunicación sin locking, la aplicación crea una solicitud de comunicación para enviar y / o recibir y recupera un identificador y luego finaliza. Eso es todo lo que se necesita para garantizar que el proceso se ejecute. Es decir, se notifica a la biblioteca de MPI que la operación debe ejecutarse.

Para el lado del emisor, esto permite la superposición del cálculo con la comunicación.

Para el lado del receptor, esto permite superponer una parte de la sobrecarga de comunicación, es decir, copiar el mensaje directamente en el espacio de direcciones del lado receptor en la aplicación.

Al usar la comunicación de locking debe tener cuidado con las llamadas de envío y recepción, por ejemplo, mire este código

  if(rank==0) { MPI_Send(x to process 1) MPI_Recv(y from process 1) } if(rank==1) { MPI_Send(y to process 0); MPI_Recv(x from process 0); } 

¿Qué pasa en este caso?

  1. El proceso 0 envía x al proceso 1 y bloques hasta que el proceso 1 recibe x.
  2. El proceso 1 envía y a procesar 0 y bloques hasta que el proceso 0 recibe y, pero
  3. el proceso 0 está bloqueado de modo que el proceso 1 bloques para el infinito hasta que los dos procesos se maten.

Es fácil.

El no locking significa que el cálculo y la transferencia de datos pueden ocurrir al mismo tiempo para un solo proceso.

Mientras Bloqueo significa, amigo, tienes que asegurarte de que ya has terminado de transferir los datos y luego volver para terminar el próximo comando, lo que significa que si hay una transferencia seguida de un cálculo, el cálculo debe ser posterior al éxito de la transferencia.