MPI点对点通信

点对点通信是MPI提供的最基础的接口之一,MPI中的点对点通信操作分为阻塞式与非阻塞式两种,即MPI_Send/MPI_Recv与MPI_Isend/MPI_Irecv,后者中的字母“i”代表非阻塞式通信。消息传递接口MPI的标准在2021年9月已经进入了4.0版本,这篇博客也是基于其官方文档对点对点通信接口进行介绍,主要介绍阻塞式通信的语义、通信模式的内容,原文档中第三章其余部分在有机会详细了解后可能更新在别的博客中。

MPI官网:MPI Forum

MPI4.0文档官方地址:MPI: A Message-Passing Interface Standard Version 4.0

阻塞式(Blocking)点对点通信

C语言中MPI阻塞发送与接收函数的定义与参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 阻塞式发送
* - buf:发送缓冲区
* - count:发送数据个数
* - datatype:发送数据类型
* - dest:目的进程号
* - tag:消息记号
* - comm:收发进程所处的通信域
*/
int MPI_Send(const void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm);
/* 阻塞式接收
* 其余参数基本一致,status不需要时可以设为MPI_STATUS_IGNORE
*/
int MPI_Recv(const void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status);

MPI点对点通信中发送的消息是由countdatatype类型的数据组成的,在C语言中支持的基础数据类型有下图列出的数种,其中有C语言的基础数据类型、stddef.h头文件中定义的一些数据类型、以及MPI_BYTEMPI_PACKED两种,其中MPI_BYTE固定为八位二进制数,字节在不同机器中可能长度是不同的,而MPI_BYTE在不同机器中是统一的。

image-20221206184826418

除了基础的数据类型,还有三种预定义的数据类型MPI_AINTMPI_OFFSETMPI_COUNT,以及C++中的四种:MPI_CXX_BOOLMPI_CXX_FLOAT_COMPLEXMPI_CXX_DOUBLE_COMPLEXMPI_CXX_LONG_DOUBLE_COMPLEX

MPI_Recv中,参数tagsource分别可以设为MPI_ANY_TAGMPI_ANY_SOURCE/MPI_PROC_NULL用于表达特殊的语义。在MPI中一个进程可以调用send/recv给自己发消息。参数status中包含了发送方进程tag和source的信息,也可以用它在非阻塞通信中用于返回请求的错误信息。消息中数据的长度也包含在status中,可以调用get_count函数获取该信息:

1
int MPI_Get_count(const MPI_Status *status, MPI_Datatype datatype, int *count)

阻塞式通信还有一个发送-接收操作,定义如下,调用它相当于并行的执行两个线程,一个接收一个发送。其中收发缓存不能有重叠。

1
int MPI_Sendrecv_replace(void *buf, int count, MPI_Datatype datatype, int dest, int sendtag, int source, int recvtag, MPI_Comm comm, MPI_Status *status)

数据类型匹配与转换

点对点通信中主要需要在三个阶段遵循数据类型匹配的原则,MPI_Send和对应的MPI_Recvdatatype一项需要是相同的。

  1. 数据从发送方缓存打包进一个消息时

  2. 消息从发送方传输给接收方的过程中

  3. 接收方将消息解包到接收方缓存时

数据类型可以分为type conversion和representation conversion两种,MPI的数据类型匹配规则使得MPI无需关心通信过程中的type conversion,而作为一种支持异构平台的标准,它是能够支持representation conversion,详细的转换规则就不在这篇博客中介绍了。

通信模式(Communication Modes)

阻塞式点对点通信中的“阻塞”,是指对MPI_Send的调用会阻塞程序的运行,直到发送的数据与消息头都已被安全地存下来并且发送者已经能够随意更改发送缓冲区中的数据。对于阻塞式通信MPI提供了多种通信模式的选择,例如可以通过消息缓存机制(Message Buffering)来解耦收发操作,即原本MPI_Send需要等待接收方在MPI_Recv中将消息存到本地后才能返回,而若采用消息缓存机制就能在发送方将消息存到本地的一个缓冲区后就可以返回,无需接收方有对应的接收操作。

MPI文档中这一部分主要在介绍几种通信模式的准确语义,这些语义的实现或许跟MPI通信协议有一定关系。MPI采用的通信协议主要是Eager与Rendzvous协议两种:

  • Eager协议主要用于小消息的通信,发送方假设接收方有足够的空间缓存自己发送的消息,因此在将消息发送到网络后直接退出,部分环境下主要用于128KB以下消息的发送(这个协议切换阈值貌似是一个可以配置的值)

  • Rendzvous(交会,好像是个法语单词)协议则是用于大消息,需要发送方先发个小消息告知接收方自己要发送的消息大小,接收方分配好缓冲区后返回ACK,发送方再进行消息发送。

MPI文档中给出的通信模式有四种。

Standard Mode

Standard模式下MPI_Send由MPI自行选择是否利用上面提到的Message Buffering机制进行缓存,在这种情况下调用会像先前说的那样在数据被本地缓存后退出,否则就需要接收方使用一个对应的MPI_Recv将消息接收到缓冲区后再退出。标准模式下MPI_Send是non-local的,即发送操作的成功可能还要取决于接收方一个匹配接收的发生。

Buffered Mode

该模式下就是指定利用上面提到的Message Buffering机制进行通信了,但若没有足够的缓冲空间程序就会报错,缓冲空间的由用户控制,可能需要用户进行分配,这一部分在原文档的3.6节有详细介绍。该模式的通信时local的,即成功与否仅取决于本地。

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

Synchronous Mode

正如它的名字一样Synchronous模式含有一个“同步”的语义,该模式下发送的成功完成需要等到一个与之匹配的接收被发布、并且后者已经开始接收发送的消息时发送才会成功完成,也就是说Synchronous Mode下MPI_Send的返回不仅能够表明发送缓冲区已经可以被重新使用,还表明接收方程序执行到了一个MPI_Recv处。自然,该模式下的通信时non-local的。

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

Ready Mode

Ready模式下的通信较为严格,必须要在接收方已经发布了一个匹配的接收的情况下发送方才能启动发送,否则发送操作会报错,通俗来讲应该是说在发送方执行到MPI_Send前接收方需要已经执行过MPI_Recv。Ready模式与Standard模式的语义是相同的,如果正确使用Ready模式可以减少一些性能开销,因为发送方可以确定接收方已经准备好接受消息了。

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

点对点通信语义

  • 顺序(order):如果连续发送出的两个消息都可以被一个MPI_Recv接收,那么在第一条消息被接收之前第二条消息不会被接收;如果两个发送在逻辑上是并发的,则它们可以以任意顺序被接收。

  • 执行(progress):两个进程上的一对相匹配的发送接收操作中至少有一个会被完成。

  • 公平性(fairness):MPI在处理通信时不保证公平性,一个发送可能被目的进程任意一个匹配的接收收到。

  • 资源限制:任何待定的通信操作消耗有限的系统资源,缺乏资源时会报错。