MPI: 阻塞 vs 非阻塞

我很难理解 MPI 中阻塞通信和非阻塞通信的概念。这两者有什么不同?优点和缺点是什么?

90476 次浏览

使用 MPI_Send()MPI_Recv()完成阻塞通信。这些函数在通信完成之前不会返回(即阻塞)。简而言之,这意味着传递给 MPI_Send()的缓冲区可以被重用,或者是因为 MPI 将其保存在某处,或者是因为它已经被目的地接收。类似地,当接收缓冲区被有效数据填充时,MPI_Recv()返回。

相比之下,无阻塞通信是使用 MPI_Isend()MPI_Irecv()完成的。即使通信尚未完成,这些函数也会立即返回(即,它们不会阻塞)。您必须调用 MPI_Wait()MPI_Test()以查看通信是否已完成。

只要足够,就可以使用阻塞通信,因为它比较容易使用。必要时使用非阻塞通信,例如,您可以调用 MPI_Isend(),执行一些计算,然后执行 MPI_Wait()。这允许计算和通信重叠,通常可以提高性能。

请注意,集体通信(例如 all-reduce)只能在 MPIv2以下的阻塞版本中使用。MPIv3引入了非阻塞集体通信。

MPI 的发送模式的一个快速概述可以看到 给你

例如,在使用阻塞通信时,必须注意发送和接收呼叫 看看这个代码

 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);
}

这种情况下会发生什么?

  1. 进程0将 x 发送给进程1并阻塞直到进程1接收到 x。
  2. 进程1将 y 发送给进程0并阻塞直到进程0接收到 y,但是
  3. 进程0被阻塞,因此进程1被阻塞为无穷大,直到这两个进程被终止。

很简单。

非阻塞意味着计算和传输数据可以同时发生在单个进程。

虽然阻塞意味着,嘿,伙计,你必须确保你已经完成了传输数据,然后回来完成下一个命令,这意味着,如果有一个传输之后的计算,计算必须在传输成功之后。

这个帖子虽然有点老,但是我争取到了公认的答案。“这些函数直到通信结束才返回”的说法有点误导,因为阻塞通信并不能保证在发送和接收操作时有任何握手。

首先需要知道的是,发送有四种模式的通信方式是: 标准,缓冲,同步准备好了,它们都可以是 阻塞非阻塞

与发送不同,接收只有一种模式可以是 挡住了也可以是 无阻塞

在进一步讨论之前,还必须清楚,我明确提到了哪个是 发送恢复缓冲区,哪个是 系统缓冲区系统缓冲区(它是 MPI Library 所拥有的每个处理器中的本地缓冲区,用于在通信组的级别之间移动数据)

封锁通讯 : 阻塞并不意味着消息被传递到接收方/目的地。它仅仅意味着(发送或接收)缓冲区可以重用。要重用缓冲区,将信息复制到另一个内存区域就足够了,例如,库可以将缓冲区数据复制到库中自己的内存位置,然后,例如,MPI _ send 可以返回。

MPI 标准非常清楚地将消息缓冲与发送和接收操作解耦。即使没有发送匹配的接收,阻塞发送也可以在消息被缓冲后立即完成。但是在某些情况下,消息缓冲可能是昂贵的,因此从发送缓冲区直接复制到接收缓冲区可能是有效的。因此,MPI 标准提供了四种不同的发送模式,使用户在为其应用程序选择适当的发送模式时有一定的自由度。让我们看看在每种交流方式中都发生了什么:

1. 标准模式

标准模式下,由 MPI Library 决定是否对发出的消息进行缓冲。在库决定缓冲传出消息的情况下,发送甚至可以在调用匹配的接收之前完成。在库决定不缓冲的情况下(出于性能原因,或者由于缓冲区空间不可用) ,发送将不会返回,直到匹配的接收已经发送,并且发送缓冲区中的数据已经移动到接收缓冲区中。

因此,从 标准模式下的 MPI _ send 是非本地的的意义上说,无论匹配的接收是否已经发布,标准模式下的发送都可以启动,并且它的成功完成可能取决于匹配接收的出现(因为实现取决于消息是否会被缓冲)。

标准发送的语法如下:

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

2. 缓冲模式

与标准模式一样,可以在缓冲模式下启动发送,而不管匹配的接收是否已经发送,也不管发送是否在匹配的接收发送之前完成。然而,主要的区别出现在这样一个事实上,即如果发送被盯着,并且没有匹配的接收被张贴,那么传出的消息 必须的将被缓冲。注意,如果匹配的接收被发布,缓冲发送可以愉快地与启动接收的处理器会合,但是在没有接收的情况下,缓冲模式下的发送必须缓冲发送消息,以允许发送完成。缓冲发送的全部内容是 本地。在这种情况下,缓冲区分配是用户定义的,如果缓冲区空间不足,将发生错误。

缓冲区发送的语法:

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

3. 同步模式

在同步发送模式下,无论是否发送了匹配的接收,都可以启动发送。但是,只有在发送了匹配的接收并且接收方已经开始接收通过同步发送发送的消息时,发送才能成功完成。同步发送的完成不仅表明发送中的缓冲区可以重用,而且表明接收进程已经开始接收数据。如果发送和接收都被阻塞,那么在通信处理器交会之前,通信在两端都没有完成。

同步发送的语法:

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

4. 准备模式

与前三种模式不同,只有在匹配的接收已经发送的情况下,才能启动就绪模式下的发送。发送的完成并不指示任何关于匹配接收的内容,只是告诉可以重用发送缓冲区。使用就绪模式的发送具有与标准模式或带有关于匹配接收的附加信息的同步模式相同的语义。具有就绪通信模式的正确程序可以替换为同步发送或标准发送,除了性能差异之外对结果没有影响。

立即发送的语法:

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

在经历了所有4个阻塞发送之后,它们在主体上可能看起来不同,但是根据实现的不同,一种模式的语义可能与另一种模式相似。

例如,MPI _ send 通常是一种阻塞模式,但是根据实现的不同,如果消息大小不是太大,MPI _ send 将把发出的消息从发送缓冲区复制到系统缓冲区(“在现代系统中大多是这种情况)并立即返回。”。让我们看看下面的例子:

//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);
}

让我们看看在上面的例子中,每个等级都发生了什么

等级0 是试图发送到等级1和等级2,并从等级1和等级3接收。

等级1 是试图发送到等级0和等级3,而不从任何其他等级收到任何东西

Rank 2 尝试从 Rank 0接收数据,然后对 recv _ buff 中接收的数据进行一些操作。

等级3 试图发送到等级0并从等级1接收

初学者感到困惑的地方是,rank 0发送到 rank 1,但 rank 1没有启动任何接收操作,因此通信 应该块或失速,而 rank 0中的第二个 send 语句根本不应该执行(这就是 MPI 文档强调的,它是实现定义的,无论发出的消息是否会被缓冲)。在大多数现代系统中,这种小型消息(这里的大小为1)将很容易被缓冲,并且 MPI _ send 将返回并执行其下一个 MPI _ send 语句。因此,在上面的例子中,即使未启动秩1中的接收,秩0中的第1个 MPI _ send 将返回并执行其下一个语句。

在一个假设的情况下,rank 3在 rank 0之前开始执行,它将把第一个 send 语句中的输出消息从 send buffer 复制到一个系统 buffer (在现代系统中;) ,然后开始执行它的 access 语句。一旦秩0完成它的两个 send 语句并开始执行接收语句,在系统中以秩3缓冲的数据将被复制到秩0的接收缓冲区中。

如果在处理器中启动了接收操作,并且没有发送匹配的消息,那么进程将会阻塞,直到接收缓冲区被它所期望的数据填满。在这种情况下,除非 MPI _ Recv 返回,否则计算或其他 MPI 通信将被阻塞/停止。

理解了 缓冲现象缓冲现象之后,应该返回并更多地考虑 发送,它具有阻塞通信的真正语义。即使 MPI _ Ssend 将发送消息从发送缓冲区复制到系统缓冲区(同样是实现定义的) ,必须注意,除非发送处理器收到来自接收进程的一些确认(低级格式) ,否则 MPI _ Ssend 不会返回。

幸运的是,MPI 决定让用户在接收和 在阻塞通信中只有一个接收端: MPI _ Recv方面更容易,并且可以与上面描述的四种发送模式中的任何一种一起使用。对于 MPI _ Recv,接收到的 阻塞手段仅在其缓冲区中包含数据后才返回。这意味着接收只能在匹配的发送启动之后完成,但并不意味着它是否能在匹配的发送完成之前完成。

在这种阻塞调用期间发生的情况是,计算被暂停,直到被阻塞的缓冲区被释放。这通常会导致计算资源的浪费,因为 Send/Recv 通常将数据从一个内存位置复制到另一个内存位置,而 CPU 中的寄存器保持空闲。

非阻塞通信 : 对于非阻塞通信,应用程序为发送和/或接收创建通信请求,然后获得句柄并终止。这就是保证进程执行所需的全部内容。即 MPI 库被通知必须执行该操作。

对于发送方,这允许通过通信进行重叠计算。

对于接收方,这允许重叠部分通信开销,即将消息直接复制到应用程序中接收方的地址空间中。

公认的答案和另一个很长的答案都提到计算和通信的重叠是一个优势。这是1。不是主要的动机。很难得到。非阻塞通信的主要优势(以及最初的动机)在于,您可以表达复杂的通信模式,而不会出现死锁,也不会出现不必要的进程序列化。

死锁: 每个人都做一个接收,然后每个人都做一个发送,例如沿着一个环。这将挂起。

序列化: 沿着线性排序,除了最后一个之外的每个人都向右发送一个消息,然后除了第一个之外的每个人都从左边接收一个消息。这将使所有进程按顺序执行,而不是并行执行。