MPI
MPI(Message Passing Interface,消息传递接口)
数据类型
- MPI_INT:整数类型,对应C语言中的int类型。
- MPI_FLOAT:单精度浮点数类型,对应C语言中的float类型。
- MPI_DOUBLE:双精度浮点数类型,对应C语言中的double类型。
- MPI_CHAR:字符类型,对应C语言中的char类型。
- MPI_LOGIC:逻辑类型,对应C语言中的_Bool类型(在stdbool.h头文件中定义)
MPI_Op
MPI_Op是一个枚举类型,它定义了各种操作符,这些操作符可以用于执行元素级别的操作。以下是一些常用的操作符:
- MPI_OP_NULL:用于初始化操作对象。
- MPI_MAX:用于找出数组中的最大值。
- MPI_MIN:用于找出数组中的最小值。
- MPI_SUM:用于计算数组中所有元素的和。
- MPI_PROD:用于计算数组中所有元素的乘积。
- MPI_LAND:用于执行元素级别的“与”操作。
- MPI_BAND:用于执行元素级别的“或非”操作。
第一个MPI程序
首先,我们应该先包含进一个头文件<mpi.h>
MPI程序和普通的C程序的区别在于有一个开始的函数和结束的函数来标识MPI部分,再在这个部分进行你想要进行的操作
函数说明
MPI_Init( )
MPI_Init( ):进入MPI环境并完成所有的初始化工作,标志并行代码的开始
1 | int MPI_Init(int *argc, char **argv) |
int MPI_Init(int *argc, char **argv) ,用于初始化MPI环境
参数说明:
argc:指向整数的指针,表示命令行参数的数量。argv:指向字符指针的指针,表示命令行参数的值。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
MPI_Finalize( )
MPI_Finalize( ):从MPI环境中退出,标志并行代码的结束
1 | int MPI_Finalize(void) |
int MPI_Finalize(void) ,用于结束MPI环境。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
Hello World
1 |
|
获取进程数量
在MPI编程中,我们常常需要获取指定通信域的进程个数,以确定程序的规模。
一组可以相互发送消息的进程集合叫做通信子,通常由MPI_Init()在用户启动程序时,定义由用户启动的所有进程所组成的通信子,缺省值为 MPI_COMM_WORLD 。这个参数是MPI通信操作函数中必不可少的参数,用于限定参加通信的进程的范围。
函数说明
1 | int MPI_Comm_size(MPI_Comm comm, int *rank) |
int MPI_Comm_size(MPI_Comm comm, int *rank) ,获取指定通信域的进程个数。
参数说明:
comm:通信器对象,表示一组进程之间的通信关系。rank:指向整数的指针,用于存储进程数量。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
示例代码
1 |
|
在上述代码中,MPI_Comm_size(MPI_COMM_WORLD, &world_size) 将获取默认通信器(MPI_COMM_WORLD,通常包含所有参与执行的应用程序进程)中的进程数量,并将其存储在 world_size 变量中。
实验说明
使用函数MPI_Comm_size获取通信域中的进程个数并打印出来。
1 |
|
获取进程id
同样,我们也常常需要输出当前进程的id,以此来判断具体哪个进程完成了对应的任务。
函数说明
1 | int MPI_Comm_rank(MPI_Comm comm, int *rank) |
int MPI_Comm_rank(MPI_Comm comm, int *rank) ,用于获取当前进程在指定通信器中的编号。
参数说明:
comm:通信器对象,表示一组进程之间的通信关系。rank:指向整数的指针,用于存储当前进程的编号。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
1 |
|
实验说明
在每个进程中,使用函数MPI_Comm_rank来获取当前进程的id并打印出来。
输出结果:由于并行程序执行顺序的不确定性,你的结果的顺序可能和这个结果不一致。
0
1
3
2
1 |
|
消息传递
在并行编程中,消息传递占了很大的比重。良好的消息传递是正常完成进程/节点之间操作的基本条件
在这里先介绍的最基本发送/接收函数
最基本的发送 / 接收函数都是以缓冲区作为端点,通过参数配置来完成指定操作
函数说明
发送数据
发送缓冲区中的信息到目标进程
1 | int MPI_Send(void* msg_buf_p, int msg_size, MPI_Datatype msg_type, int dest, int tag, MPI_Comm communicator) |
MPI_Send(void* msg_buf_p, int msg_size, MPI_Datatype msg_type, int dest, int tag, MPI_Comm communicator) ,用于将数据从源进程发送到目标进程。
参数说明:
msg_buf_p:指向要发送数据的缓冲区的指针。msg_size:要发送的数据的大小(以字节为单位)。msg_type:数据的类型。dest:目标进程的标识符。tag:一个整数标签,用于区分不同的消息。communicator:通信器对象,表示一组进程之间的通信关系。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
接收数据
1 | int MPI_Recv(void* msg_buf_p, int buf_size, MPI_Datatype msg_type, int source, int tag, MPI_Comm communicator, MPI_Status *status_p) |
用于在并行计算环境中接收来自特定源进程的数据,并将其存储在指定的接收缓冲区中。
void* msg_buf_p: 这是指向接收缓冲区的指针。该缓冲区将存储从源进程接收到的数据。int buf_size: 这是接收缓冲区的大小(以字节为单位)。它应该与发送缓冲区的大小相匹配。MPI_Datatype msg_type: 这是指定接收缓冲区中数据类型的MPI数据类型对象。int source: 这是发送数据的源进程的标识符。MPI库将只从这个进程接收数据。int tag: 这是用于区分不同消息的整数标签。如果两个进程同时发送数据,它们可以使用不同的标签来区分它们的消息。MPI_Comm communicator: 这是通信器对象,它定义了进程组和通信模式。在这个例子中,通信器对象表示一个并行计算环境。MPI_Status *status_p: 这是指向MPI状态对象的指针。MPI状态对象包含了关于接收操作的信息,如是否成功接收到数据、接收时间等。
实验说明
把id为0的进程当作根进程,然后在除此之外的进程中使用函数MPI_Send发送一句”hello world!”到根进程中,然后在根进程中把这些信息打印出来。
输出结果:一系列的”hello world!”
字符串的名字就是字符串的首地址。在C语言中,字符串是由字符数组表示的,每个字符都有一个唯一的地址。所以,字符串的名字实际上就是指向字符串首字符的指针。
1 |
|
规约(reduce)
在现实生活中,我们常常需要对于数据做同一种操作,并将结果返回到指定的进程中,这个过程称为集合通信。例如,将数据分散到各个进程中,先在各个进程内进行求和,再在全局完成求和-平均这个操作,这个过程是一个规约的过程。
一般来说,集合通信包括通信、同步和计算三个功能。不过,目前我们暂时不需要关注整个过程,而是先使用一个规约函数去体验一下集合通信。
函数说明
规约函数,所有进程将待处理数据通过输入的操作子operator计算为最终结果并将它存入目标进程中。
1
2 int MPI_Reduce(void * input_data_p, void * output_data_p, int count, MPI_Datatype datatype, MPI_Op operator, int dest_process, MPI_Comm comm)
进程的待处理数据存放的地址; 存放最终结果的目标进程的地址;缓冲区中的数据个数;数据类型;操作子(加减);目标进程的编号;
int MPI_Reduce(void * input_data_p, void * output_data_p, int count, MPI_Datatype datatype, MPI_Op operator, int dest_process, MPI_Comm comm),用于将数据从源进程收集到目标进程。参数说明:
input_data_p:指向输入数据的指针。output_data_p:指向输出数据的指针。count:要处理的元素数量。datatype:数据类型。operator:MPI操作符,用于指定如何组合输入数据以生成输出数据。dest_process:目标进程的标识符。comm:通信器对象,表示一组进程之间的通信关系。返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
MPI_Reduce()函数是Message Passing Interface (MPI) 库中用于并行计算的一个重要函数,主要用于在一组进程间对数据进行规约操作(如求和、最大值、最小值等)。以下是该函数的详细解析:
1
2
3
4 int MPI_Reduce(void *input_data_p, void *output_data_p,
int count, MPI_Datatype datatype,
MPI_Op op, int dest_process,
MPI_Comm comm);
参数解释:
void *input_data_p: 指向参与规约操作的输入缓冲区的指针。每个进程提供一块相同大小的数据区域,这些数据将按照指定的操作符被规约。
void *output_data_p: 指向规约结果存储位置的指针。只有指定的目标进程(dest_process)会接收到最终的规约结果。
int count: 表示要处理的数据元素个数。例如,如果datatype是MPI_INT且count为10,则表示有10个整数参与规约。
MPI_Datatype datatype: 指定参与规约的数据类型,可以是MPI预定义的类型,如MPI_INT、MPI_FLOAT、MPI_DOUBLE等,也可以是用户自定义的复合数据类型。
MPI_Op op: 指定要执行的规约操作,例如MPI_SUM(求和)、MPI_MAX(求最大值)、MPI_MIN(求最小值)等。也可以通过MPI_Op_create自定义规约操作。
int dest_process: 规约结果存放的目标进程的 rank(标识符)。所有进程都将它们的局部结果发送到这个目标进程,然后由目标进程执行最后的规约操作得到全局结果。
MPI_Comm comm: 指定通信器,通常是一个进程组(communicator),如MPI_COMM_WORLD,它包含了参与规约的所有进程。函数返回一个整数值,若成功则返回MPI_SUCCESS,否则返回错误代码。
示例用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int my_rank, size;
MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
double local_sum = 0.0;
// 假设我们已经填充了本地数据
for (int i = 0; i < 100; ++i) {
local_sum += some_local_array[i];
}
double global_sum;
MPI_Reduce(&local_sum, &global_sum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
if (my_rank == 0) {
printf("The global sum is: %f\n", global_sum);
}
MPI_Finalize();
return 0;
}在这个例子中,所有进程都会计算其本地数据之和,然后通过
MPI_Reduce函数将各自的局部和规约成一个全局总和,结果存储在rank为0的进程中。
在上面的示例中,
count设置为1表示每个进程中参与规约操作的数据元素个数为1。这意味着我们正在对单个double类型的数值进行规约。然而,在实际应用中,
count可以根据需要设置成任意大于0的整数。例如,如果每个进程有一段包含多个相同类型数据(如一个double数组)的缓冲区,并希望对这些数据进行规约,那么count应设为该数组的元素个数。假设每个进程都有一个长度为100的double数组,想要求和:
1
2
3
4 double local_data[100];
// 初始化local_data...
double global_sum;
MPI_Reduce(local_data, &global_sum, 100, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);在这个例子中,
count被设置为100,表示每个进程将它所拥有的100个double元素进行规约。最终结果是一个全局的总和,存储在rank为0的进程中的global_sum变量中。
实验说明
使用函数MPI_Reduce来完成加法规约到根进程的操作,并在根进程打印出总和和平均值。
输出结果:由于这里是测试用例,所以每个进程的数值都是取3.0。所以,输出的平均值应该是3。
1 |
|
广播(Bcast)
在一个集合通信中,如果属于一个进程的数据被发送到通信子中的所有进程,这样的集合通信叫做广播。
函数说明
广播函数,从一个id值为source的进程将一条消息广播发送到通信子内的所有进程,包括它本身在内。
1 | int MPI_Bcast(void* buffer, int count, MPI_Datatype datatype, int source, MPI_Comm comm) |
int MPI_Bcast(void* buffer, int count, MPI_Datatype datatype, int source, MPI_Comm comm) ,用于在并行计算中广播数据。
参数说明:
buffer:指向要广播的缓冲区的指针。count:要广播的元素数量。datatype:缓冲区中元素的数据类型。source:广播数据的源进程的标识符。如果为0,则表示使用默认的源进程。comm:通信器对象,表示一组进程之间的通信关系。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
MPI_Bcast,用于在并行计算环境中的所有进程间进行广播操作。该函数将源进程(由参数source指定)的数据广播到 communicator (comm) 中的其他所有进程中。函数原型:
1 int MPI_Bcast(void* buffer, int count, MPI_Datatype datatype, int source, MPI_Comm comm);参数解析:
void* buffer: 指向要发送或接收数据的缓冲区的指针。在非源进程中,这个缓冲区会被来自source进程的数据覆盖;在源进程中,它包含要广播的数据。
int count: 要发送或接收的数据元素个数。例如,如果数据类型为MPI_INT,则count表示整数的数量。
MPI_Datatype datatype: 数据类型的标识符,可以是预定义的基本类型如MPI_INT、MPI_DOUBLE等,也可以是用户自定义的复合类型。
int source: 广播源进程的rank(编号)。只有这个进程提供数据,其余所有进程都会接收到同样的数据。
MPI_Comm comm: 通信器,它定义了参与此次广播的所有进程集合。通常使用MPI_COMM_WORLD,即包含了所有参与计算的进程。函数返回值:
MPI_Bcast函数返回一个int型数值,如果成功完成,则返回MPI_SUCCESS,否则返回错误代码。功能描述:
MPI_Bcast函数使得所有进程中的buffer区域内容相同,其内容来源于source进程的相应内存区域。这样,在并行程序中,一个进程可以通过广播的方式快速地将其数据同步给其他所有进程。
实验说明
使用函数MPI_Bcast在根进程中发送一个数组到其他进程,并在其他进程中打印出来。
输出结果:
In process 1, arr[0]=1 arr[1]=2 arr[2]=3 arr[3]=4 arr[4]=5
In process 3, arr[0]=1 arr[1]=2 arr[2]=3 arr[3]=4 arr[4]=5
…
In process n, arr[0]=1 arr[1]=2 arr[2]=3 arr[3]=4 arr[4]=5
…
1 |
|
收集(gather)
有时候我们希望在一个进程中从所有进程获取信息,例如将所有进程中的一个数组都收集到根进程中作进一步的处理,这样的集合通信我们叫做收集。
函数说明
收集函数,根进程(目标进程)从所有进程(包括它自己)收集发送缓冲区的数据,再根据发送这些数据的进程id将它们依次存放到自已的缓冲区中
1 | int MPI_Gather(void* sendbuf, int sendcount, MPI_Datatype sendtype, void* recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm) |
int MPI_Gather(void* sendbuf, int sendcount, MPI_Datatype sendtype, void* recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm) ,用于将一个进程的局部数据收集到根进程中。
参数说明:
sendbuf:指向发送缓冲区的指针,该缓冲区包含要发送的数据。sendcount:要发送的元素数量。sendtype:发送缓冲区中元素的数据类型。recvbuf:指向接收缓冲区的指针,该缓冲区将存储接收到的数据。recvcount:每个进程应接收的元素数量。recvtype:接收缓冲区中元素的数据类型。root:根进程的标识符,即数据分发的起点。comm:通信器对象,表示一组进程之间的通信关系。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
实验说明:
使用函数MPI_Gather在根进程中从所有进程接收一个数组,并在根进程中打印出来。
输出结果:
Now is process 1’s data: arr[0]=1 arr[1]=2 arr[2]=3 arr[3]=4 arr[4]=5
Now is process 4’s data: arr[0]=1 arr[1]=2 arr[2]=3 arr[3]=4 arr[4]=5
Now is process 2’s data: arr[0]=1 arr[1]=2 arr[2]=3 arr[3]=4 arr[4]=5
…
Now is process n’s data: arr[0]=1 arr[1]=2 arr[2]=3 arr[3]=4 arr[4]=5
1 |
|
散发(scatter)
在前面我们学习了收集(gather)操作,那么与之相对应也有一个相反的集合通信操作,即根进程向所有进程发送缓冲区的数据,称为散发。
需要特别说明的是,散发操作和广播操作的区别在于发送到各个进程的信息可以是不同的。
函数说明
MPI_SCATTER是MPI_GATHER的逆操作,另外一种解释是根进程通过MPI_Send发送一条消息,这条消息被分成n等份,第i份发送给组中的第i个处理器, 然后每个处理器如上所述接收相应的消息。
1
2 int MPI_Scatter(void* sendbuf, int sendcount, MPI_Datatype sendtype, void* recvbuf, int recvcount, MPI_Datatype recvtype,int root, MPI_Comm comm)
发送缓冲区的起始地址;发送的数据个数;数据类型;接收缓冲区的起始地址;待接收的元素个数;数据类型;发送进程id;通信子
int MPI_Scatter(void* sendbuf, int sendcount, MPI_Datatype sendtype, void* recvbuf, int recvcount, MPI_Datatype recvtype,int root, MPI_Comm comm),用于在并行计算中将数据从根进程分散到其他所有进程。参数说明:
sendbuf:指向发送缓冲区的指针,该缓冲区包含要发送的数据。sendcount:要发送的元素数量。sendtype:发送缓冲区中元素的数据类型。recvbuf:指向接收缓冲区的指针,该缓冲区将存储接收到的数据。recvcount:每个进程应接收的元素数量。recvtype:接收缓冲区中元素的数据类型。root:根进程的标识符,即数据分发的起点。comm:通信器对象,表示一组进程之间的通信关系。返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int rank, size;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
int sendbuf[10];
int recvbuf[10];
if (rank == 0) {
// 根进程初始化发送缓冲区
for (int i = 0; i < 10; i++) {
sendbuf[i] = i + 1;
}
}
// 使用MPI_Scatter进行数据分发
MPI_Scatter(sendbuf, 10, MPI_INT, recvbuf, 10, MPI_INT, 0, MPI_COMM_WORLD);
printf("Process %d received data: ", rank);
for (int i = 0; i < 10; i++) {
printf("%d ", recvbuf[i]);
}
printf("\n");
MPI_Finalize();
return 0;
}上述示例演示了如何使用
MPI_Scatter函数将数据从根进程分散到其他所有进程。根进程初始化发送缓冲区,然后调用MPI_Scatter函数进行数据分发。其他进程接收到数据后,将其打印出来。
实验说明:
使用函数MPI_Scatter在根进程中向所有进程发送对应数组,并在对应进程中打印出来。
输出结果:
Now is process 1: arr[0]=5 arr[1]=6 arr[2]=7 arr[3]=8 arr[4]=9
Now is process 4: arr[0]=20 arr[1]=21 arr[2]=22 arr[3]=23 arr[4]=24
Now is process 2: arr[0]=10 arr[1]=11 arr[2]=12 arr[3]=13 arr[4]=14
…
Now is process n: arr[0]=5n arr[1]=5n+1 arr[2]=5n+2 arr[3]=5n+3 arr[4]=5*n+4
…
1 |
|
Reduce和Gatherr的区别
MPI_Reduce 和 MPI_Gather 是 MPI(Message Passing Interface)中用于并行计算中的两种不同的集合通信操作。它们的主要区别在于数据的处理方式和目的:
MPI_Reduce:
- 功能:
MPI_Reduce函数是用来执行一个全局的规约操作,例如求和、最大值、最小值等,将所有进程中的独立数据集合到一个进程中去,并且在这个过程中应用了一个指定的操作符对这些数据进行归约。 - 用法:每个进程提供一块本地数据,然后通过调用
MPI_Reduce函数,系统会根据提供的操作符(如MPI_SUM、MPI_MAX等)在所有进程中对该类型的数据进行归约运算,最终结果只存在于调用者指定的一个进程中(通常称为根进程,由参数决定)。 - 示例:如果每个进程有一个整数变量,使用
MPI_Reduce可以将所有进程的整数加起来得到总的和。
- 功能:
MPI_Gather:
- 功能:
MPI_Gather函数则是用来从所有参与通信的进程中收集相同数量的数据,并将这些数据按照进程的秩顺序排列后,合并到根进程的一个连续内存区域中。 - 用法:每个进程都有一个本地数据缓冲区,调用
MPI_Gather后,所有进程的数据都会被收集到根进程的一个大缓冲区中,形成一个数组,其中数组的第i个元素来自于秩为i的进程的本地数据。 - 示例:如果有10个进程,每个进程有一个长度为10的浮点数数组,使用
MPI_Gather可以在根进程处创建一个新的大小为10x10的二维数组,其中每一列来自一个进程的原始数组。
- 功能:
总结来说,MPI_Reduce是做数据的聚合和操作,目标是得到单个进程上的唯一结果;而MPI_Gather则更像是数据的汇总,不涉及数据之间的操作,目标是将各个进程的数据拼接到一起形成一个完整的集合。
Bcast和Scatter的区别
MPI_Bcast(Broadcast)和MPI_Scatter(Scatter)是Message Passing Interface (MPI) 中用于并行计算的两种集合通信操作,它们的主要区别在于数据分发的方式和目的:
MPI_Bcast:
- 功能:
MPI_Bcast函数执行的是广播操作,即从一个指定的进程(称为“根进程”)将相同的数据发送给 communicator 内的所有其他进程。 - 用法:在调用
MPI_Bcast时,所有进程都会提供一个缓冲区,但只有根进程的缓冲区中的数据会被广播。执行后,所有参与通信的进程中缓冲区的内容都会被更新为根进程缓冲区中广播出去的数据。 - 示例场景:例如,如果根进程有一个全局参数需要所有的其他进程知道,那么就可以使用
MPI_Bcast来快速地让所有进程获取到相同的参数值。
- 功能:
MPI_Scatter:
- 功能:
MPI_Scatter函数则负责将根进程的一个大数组分割成多个子块,并将这些子块分别发送给 communicator 内的不同进程。 - 用法:每个进程都提供一个接收缓冲区,但内容大小通常小于或等于根进程的发送缓冲区。执行后,各个进程接收到的数据来自原始大数组的不同部分,而不是完全相同的数据。
- 示例场景:比如,在分布式矩阵乘法中,根进程拥有一个大的矩阵A,它可能需要将矩阵A的各行分散给不同的进程进行局部计算,这时可以使用
MPI_Scatter将矩阵A拆分成多份并分发给各个进程。
- 功能:
总结来说,MPI_Bcast用于向所有进程发送相同的数据副本,而MPI_Scatter则是将数据分解后分配给各个进程,每个进程得到的数据不同,通常是原始数据的一个子集。
计算运行时间
可以使用MPI_Wtime函数在并行代码中计算运行时间,用MPI_Wtick来查看精度。
函数说明
MPI_WTIME:返回一个用浮点数表示的秒数, 它表示从过去某一时刻到调用时刻所经历的时间
MPI_WTICK:返回MPI_WTIME的精度,单位是秒,可以认为是一个时钟滴答所占用的时间
1 | double MPI_Wtime(void) |
double MPI_Wtime(void) 是一个用于获取当前进程的墙钟时间的函数。它返回一个双精度浮点数,表示从某个固定时间点(通常是程序启动时)到现在所经过的时间,单位为秒。这个函数通常用于性能分析和调试。
double MPI_Wtick(void) 是一个用于获取当前进程的墙钟时间间隔的函数。它返回一个双精度浮点数,表示从上一次调用 MPI_Wtime() 函数到现在所经过的时间间隔,单位为秒。这个函数通常用于性能分析和调试。
实验说明
使用函数MPI_Wtime计算并行代码的运行时间,并且在两次计算时间的函数之间用函数MPI_WTICK打印出精度
输出结果:
The precision is: 1e-06
Hello World!I’m rank … of …, running … seconds
1 |
|
同步
例如,希望保证所有进程中并行代码在某个地方同时开始运行,或者在某个函数调用结束之前不能返回。
这时候我们就需要使用到MPI_Barrier函数。
函数说明:
阻止调用直到communicator中所有进程已经完成调用,就是说,任意一次进程的调用只能在所有communicator中的成员已经开始调用之后进行。
1 | int MPI_Barrier(MPI_Comm comm) |
MPI_Barrier(MPI_Comm comm) 是一个用于同步进程的函数。它接受一个参数:
MPI_Comm comm:一个通信器对象,表示要同步的进程组。
该函数返回一个整数,表示操作的结果。如果操作成功,返回值为 MPI_SUCCESS;否则,返回一个非零的错误代码。
实验说明:
在计算运行时间的信息之前调用MPI_Barrier函数完成同步。
输出结果:
The precision is: 1e-06
Hello World!I’m rank … of …, running … seconds.
在此示例程序中,可能是否调用函数不影响最终输出,但这并不意味着效果相同。
1 |
|
组的管理
创建(1)
组是一个进程的有序集合,在实现中可以看作是进程标识符的一个有序集。组内的每个进程与一个整数rank相联系,序列号从0开始并且是连续的。我们可以在通信组中使用组,来描述通信空间中的参与者并对这些参与者进行分级(这样在通信空间中为它们赋予了唯一的名字)
由此可见,组是我们对进程集合更高一级的抽象,我们可以在组的基础上对各个进程进行更进一步的操作,例如通过虚拟拓扑来辅助并行操作的实现。
在这里我们先介绍两个特殊的 预定义组,MPI_GROUP_EMPTY和MPI_GROUP_NULL。
需要特别说明的是,前者是一个空组的有效句柄,可以在组操作中作为一个参数使用;而后者是一个无效句柄,在组释放时会被返回。
函数说明
MPI_Comm_group用来建立一个通信组对应的新进程组
MPI_Group_rank查询调用进程在进程组里的rank
1
2
3 int MPI_Comm_group(MPI_Comm comm, MPI_Group *group)
int MPI_Group_rank(MPI_Group group, int *rank)
MPI_Comm_group(MPI_Comm comm, MPI_Group *group)是一个用于获取进程组的函数。它接受两个参数:
MPI_Comm comm:一个通信器对象,表示要查询的进程组所属的通信器。MPI_Group *group:一个指向MPI_Group类型的指针,用于存储查询到的进程组。该函数返回一个整数,表示操作的结果。如果操作成功,返回值为
MPI_SUCCESS;否则,返回一个非零的错误代码。
MPI_Group_rank(MPI_Group group, int *rank)是一个用于获取进程组中某个进程的排名的函数。它接受两个参数:
MPI_Group group:一个进程组对象,表示要查询的进程组。int *rank:一个指向整数类型的指针,用于存储查询到的进程排名。该函数返回一个整数,表示操作的结果。如果操作成功,返回值为
MPI_SUCCESS;否则,返回一个非零的错误代码。
实验说明
建立一个与初始通信子MPI_COMM_WORLD相联系的组,打印出当前进程在进程组的rank。
输出结果:
rank: 1
rank: 0
…
rank: n
顺序不唯一
1 |
|
创建(2)
上一节我们知道,可以用MPI_Comm_group函数来获得与通信组MPI_COMM_WORLD相关联的组句柄。
那么我们可以用这个组句柄做什么呢?
首先,我们可以通过这个最原始的组句柄来创建更多的、满足我们需要的组。
在这里需要特别说明的是,MPI没提供凭空构造一个组的的机制,而只能从其它以前定义的组中构造。最基本的组是与初始通信子MPI_COMM_WORLD相联系的组(可通过函数MPI_COMM_GROUP获得〕,其它的组在该组基础上定义。
函数说明
基于已经存在的进程组创建一个新的组,并指明被包含(included)其中的成员进程。
1
2 int MPI_Group_incl(MPI_Group old_group, int count, int *members, MPI_Group *new_group)
旧进程组 要包含在新进程组的进程数量 要放入新进程组的进程的编号数组 指针指向新进程组这个函数用于创建一个新的进程组,该组包含旧进程组中指定的成员。
参数解析:
old_group:一个MPI_Group类型的变量,表示要复制的旧进程组。count:一个整数,表示要包含在新的进程组中的进程数量。members:一个整数数组,包含了要包含在新进程组中的进程的标识符。new_group:一个MPI_Group类型的指针,指向新创建的进程组。函数执行后,
new_group将指向一个新创建的进程组,该进程组包含了old_group中指定的成员。
实验说明
基于与初始通信子MPI_COMM_WORLD相联系的组创建一个新的组,这个新的组的成员是通信者MPI_COMM_WORLD的奇数编号的进程。
输出结果格式应如下:
In process n: odd rank is x
…
需要特别说明的是,如果在偶数编号的进程中,也就是不属于这个组的进程中输出这个值,MPI_Group_rank会返回MPI_UNDEFINED作为group_rank的值,表示它不是 worker_group的成员,在MPICH里是-32766。
1 |
|
创建(3)
同样,我们在基于旧进程组创建一个新的组的时候,可能希望排除一些成员进程。
当然,我们可以通过选择出剩下的成员进程的方法来达成我们的目的,但是MPI提供了更好的办法去实现它。
函数说明
基于已经存在的进程组创建一个新的组,并指明不被包含(excluded)其中的成员进程。
1
2 int MPI_Group_excl(MPI_Group old_group, int count, int *nonmembers, MPI_Group *new_group)
旧进程组;要包含在新的进程组中的进程数量;不需要放入新进程组的进程的编号;新进程组的指针;这个函数用于创建一个新的进程组,该组包含旧进程组中指定的成员,但不包含非成员列表中的进程。
参数解析:
old_group:一个MPI_Group类型的变量,表示要复制的旧进程组。count:一个整数,表示要包含在新的进程组中的进程数量。nonmembers:一个整数数组,包含了不包含在新进程组中的进程的标识符。new_group:一个MPI_Group类型的指针,指向新创建的进程组。函数执行后,
new_group将指向一个新创建的进程组,该进程组包含了old_group中指定的成员,但不包含nonmembers列表中的进程。
实验说明
基于与初始通信子MPI_COMM_WORLD相联系的组创建一个新的组,这个新的组的成员是通信者MPI_COMM_WORLD的偶数编号的进程。
输出结果格式应如下:
In process n: even rank is x
…
需要特别说明的是,如果在奇数编号的进程中,也就是不属于这个组的进程中输出这个值,MPI_Group_rank会返回MPI_UNDEFINED作为group_rank的值,表示它不是 worker_group的成员,在MPICH里这个值是-32766。
1 |
|
相对编号
在创建组之后,可能会有这个疑惑:如果知道了在组MPI_COMM_WORLD中某些进程的编号,如何根据这些编号来操作在不同组的同一进程来完成不同的任务呢?
MPI提供了这样的函数以应付这种常见的情景。
函数说明
检测两个不同组中相同进程的相对编号。如果属于进程组1的某个进程可以在ranks1中找到,而这个进程不属于进程组2,则在ranks2中对应ranks1的位置返回值为MPI_UNDEFINED。
1 | int MPI_Group_translate_ranks(MPI_Group group1, int count, int *ranks1, MPI_Group group2, int *ranks2) |
MPI_Group_translate_ranks函数用于将一个进程组中的进程排名映射到另一个进程组中的进程排名。它接受以下参数:
group1:第一个进程组,类型为MPI_Group。count:要转换的进程数量,类型为整数。ranks1:指向包含要转换的进程排名的数组的指针,类型为整数指针。group2:第二个进程组,类型为MPI_Group。ranks2:指向存储转换后的进程排名的数组的指针,类型为整数指针。
该函数返回一个int类型的值,表示操作的结果。如果操作成功,返回值为MPI_SUCCESS;否则,返回一个非零的错误代码。
实验说明
建立两个进程组,打印出进程组2中对应进程组1的进程的编号。
输出结果格式:
The rank in group2 is: -32766
The rank in group2 is: 0
…
1 |
|
释放
既然有了组的构造,那么与之对应也存在组的析构。
函数说明
调用函数会标记一个被释放的组对象,组句柄被调用置为MPI_GROUP_NULL。任何正在使用此组的操作将正常完成。
1 int MPI_Group_free(MPI_Group *group)
int MPI_Group_free(MPI_Group *group)是一个MPI(Message Passing Interface,消息传递接口)函数,用于释放一个已经创建的进程组。参数:
group:指向要释放的进程组的指针。返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
实验说明
建立一个进程组,打印出它的size,然后释放它。
输出结果格式应如下:
Now the size is n
Now the group is freed.
1 |
|
比较
有时候我们想要对两个进程组做最基本的判断,例如成员是否相同,次序是否一致等等。
MPI同样提供了这样的函数来完成这个功能。
函数说明
如果在两个组中成员和次序完全相等,返回MPI_IDENT。例如在group1和group2是同一句柄时就会发生这种情况。如果组成员相同而次序不同则返回MPI_SIMILAR,否则返回MPI_UNEQUAL
1 | int MPI_Group_compare(MPI_Group group1, MPI_Group group2, int *result) |
int MPI_Group_compare(MPI_Group group1, MPI_Group group2, int *result) 是一个MPI(Message Passing Interface,消息传递接口)函数,用于比较两个进程组是否相等。
参数:
group1:指向第一个要比较的进程组的指针。group2:指向第二个要比较的进程组的指针。result:指向一个整数的指针,用于存储比较结果。如果group1和group2相等,则*result的值为0;如果group1包含group2的所有进程,则*result的值为正数;如果group2包含group1的所有进程,则*result的值为负数;如果group1和group2没有共同的进程,则*result的值为正数且大于等于group1的大小。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
实验说明
创建一个新的组,通过调整输出两个不同的结果。
输出结果格式应如下:
Now the groups are identical.
Now the groups are unequal.
…
1 |
|
通信子的管理
创建
在实际开发中,我们往往需要很多不同的通信子来满足需求,这时候就需要创建新的通信子。
函数说明
用由group所定义的通信组及一个新的上下文创建了一个新的通信子newcomm。对于不在group中的进程,函数返回MPI_COMM_NULL。
1 | int MPI_Comm_create(MPI_Comm comm, MPI_Group group, MPI_Comm *newcomm) |
int MPI_Comm_create(MPI_Comm comm, MPI_Group group, MPI_Comm *newcomm) 是一个MPI(Message Passing Interface,消息传递接口)函数,用于创建一个新的通信器。
参数:
comm:指向现有通信器的指针group:指向要包含在新通信器中的进程组的指针newcomm:指向新创建的通信器的指针
返回值:
- 函数执行成功时返回0
- 如果发生错误,则返回非零错误代码
实验说明
复制一个新的通信子,并以此为基础创建一个新的通信子。由于示例是用奇数编号的进程来创建通信子的,所以只在奇数进程中输出结果。
输出结果格式应如下:
The new comm’s size is 2.
The new comm’s size is 2.
…
注意,如果没有添加创建函数的代码,天河可能会由于超时返回长时间没有响应的提示信息。
1 |
|
复制
在之前的学习中,我们经常使用系统帮助我们创建的初始组内通信子MPI_COMM_WORLD作为通信子的输入。
其实,还有两个系统默认创建的通信子,一个是COMM_SELF,另一个是COMM_NULL。
COMM_SELF仅仅包含了当前进程,而COMM_NULL则什么进程都没有包含。
在通信子的创建中,需要特别注意的是MPI中有一个”鸡生蛋, 蛋生鸡”的特点,即所有MPI通信子的创建都是由基础通信子,即MPI_COMM_WORLD(是在MPI的外部被定义的),创建的。而这些被创建的通信子又可以作为新的通信子创建的基础。
这个模型是经过讨论后确定的,目的是为了提高用MPI写程序的安全性。
函数说明
复制已存在的通信子comm。
1 | int MPI_Comm_dup(MPI_Comm comm,MPI_Comm *newcomm) |
int MPI_Comm_dup(MPI_Comm comm, MPIint MPI_Comm_dup(MPI_Comm comm, MPI_Comm *newcomm)` 是一个MPI(Message Passing Interface,消息传递接口)函数,用于复制一个现有的通信器。
参数:
comm:指向要复制的现有通信器的指针。newcomm:指向新创建的通信器的指针。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
实验说明
复制一个新的通信子,需要特别说明的是,结果显示MPI_IDENT表示上下文(context)和组(group)都相同,MPI_CONGRUENT表示上下文不同(different)但组完全相同(identical),MPI_SIMILAR表示上下文不同,组的成员相同但次序不同(similar),否则就是MPI_UNEQUAL。
输出结果格式应如下:
The comms are congruent.
…
1 |
|
释放
同样,通信子也存在析构的操作。
函数说明:
1 | int MPI_Comm_free(MPI_Comm *comm) |
用由group所定义的通信组及一个新的上下文创建了一个新的通信子newcomm。对于不在group中的进程,函数返回MPI_COMM_NULL。
int MPI_Comm_free(MPI_Comm *comm) int MPI_Comm_free(MPI_Comm *comm) 是一个MPI(Message Passing Interface,消息传递接口)函数,用于释放一个已经创建的通信器。
参数:
comm:指向要释放的通信器的指针。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
实验说明
这是一个标志通信对象撤消的集合操作。值得注意的是,这个函数操作只是将句柄置为MPI_COMM_NULL,任何使用此通信子的挂起操作都会正常完成;仅当没有对此对象的活动引用时,它才会被实际撤消。
输出结果格式应如下:
The comm is freed.
…
1 |
|
划分
有时候我们希望根据拓扑来创建不同的域,例如创建一个二维数组,显然一个个创建是很不方便的,这时候我们需要用到一个新的函数来进行划分。
函数说明
1 | int MPI_Comm_split(MPI_Comm comm, int color, int key, MPI_Comm *newcomm) |
函数将与comm相关的域划分为若干不相连的子域,根据color和key参数决定每个进程所处的位置。
int MPI_Comm_split(MPI_Comm comm, int color, int key, MPI_Comm *newcomm) ,用于将一个通信器分割成两个子通信器。
参数:
comm:指向要分割的通信器的指针。color:一个整数,表示当前进程所属的子通信器。如果当前进程属于第一个子通信器,则color为0;如果当前进程属于第二个子通信器,则color为1。key:一个整数,用于确定如何将进程分配到子通信器中。通常,这个值应该是一个全局常量,以确保所有进程都使用相同的键值进行分割。newcomm:指向新创建的子通信器的指针。
返回值:
- 函数执行成功时返回0。
- 如果发生错误,则返回非零错误代码。
实验说明
创建一个二维数组,根据行与列进行求和,在每个进程中输出坐标和求出的和。
输出结果格式应如下:
I’m process n, my coordinates are (x, y), row sum is p, column sum is q.
…
1 |
|
获取处理器名
有时候在实际处理中我们可能需要将进程迁移至不同的处理器,而MPI提供了获取处理器名的函数以简单地允许这种行为。
注意在MPI中不需要定义这种迁移。
函数说明
1 | int MPI_Get_processor_name ( char *name, int *resultlen) |
返回调用时调用所在的处理器名。
MPI_Get_processor_name是一个用于获取当前进程名称的 MPI(Message Passing Interface,消息传递接口)函数。它接受两个参数:一个字符指针name,用于存储进程名称;一个整数指针resultlen,用于存储实际存储在name中的进程名称长度。函数原型如下:
1 int MPI_Get_processor_name ( char *name, int *resultlen );示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc, char *argv[]) {
char processor_name[MPI_MAX_PROCESSOR_NAME];
int name_length;
MPI_Init(&argc, &argv);
MPI_Get_processor_name(processor_name, &name_length);
printf("Processor name: %s
", processor_name);
printf("Name length: %d
", name_length);
MPI_Finalize();
return 0;
}
实验说明
在每个进程中,使用函数MPI_Get_processor_name来获取当前进程的处理器名并打印出来。
Hello, world. I am PROCESS_NAME.
1 |
|
地址偏移量
在通信操作中,我们常常需要对地址进行传递或操作,例如传送/接收缓冲区。
而一个位置在内存中的地址可以通过MPI_ADDRESS函数获得。
函数说明
1 | int MPI_Address(void* location, MPI_Aint *address) |
MPI_Address是一个用于获取内存地址的函数,它接受两个参数:一个指向内存位置的指针location和一个指向MPI_Aint类型的指针address。MPI_Aint是一个整数类型,用于表示任意大小的地址。函数原型如下:
1 int MPI_Address(void* location, MPI_Aint *address);使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc, char** argv) {
int rank;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
int local_variable = 42;
MPI_Aint local_address;
MPI_Address(&local_variable, &local_address);
printf("Rank %d: Local variable address is %p", rank, (void*)local_address);
MPI_Finalize();
return 0;
}在这个示例中,我们首先初始化 MPI 环境,然后获取当前进程的等级(
rank)。接着,我们定义了一个局部变量local_variable,并使用MPI_Address函数获取其内存地址。最后,我们将结果打印出来。
###实验说明
给出三个临时变量a, b, c, 分别求出a与b、a与c之间的地址偏移量。
输出结果:
由于这里采用的变量类型为int,所以如果变量地址是连续的话应该是:
The distance between a and b is 4
The distance between a and c is 8
1 |
|
数据的打包(pack)
有时候我们希望将不连续的数据或是不相同的数据类型的数据一起发送到其他进程,而不是效率很低地逐个发送。
一个解决这个问题的方案是将数据封装成包,再将数据包放到一个连续的缓冲区,发送到接收缓冲区后再提取出来尽心解包。
值得注意的是,打包/解包函数有时候还会用来代替系统缓存策略。此外,对于在MPI顶层进一步开发附加的通信库会起到辅助的作用。
函数说明:
1 | int MPI_Pack(void* inbuf, int incount, MPI_datatype datatype, void *outbuf, int outcount, int *position, MPI_Comm comm) |
这是一个MPI(Message Passing Interface,消息传递接口)函数,用于将数据打包到输出缓冲区中。下面是对代码的解析:
1 | int MPI_Pack(void* inbuf, int incount, MPI_datatype datatype, void *outbuf, int outcount, int *position, MPI_Comm comm) |
inbuf: 输入缓冲区的指针,指向要打包的数据。incount: 输入缓冲区中要打包的元素数量。datatype: 数据类型的对象,指定了输入缓冲区中元素的类型。outbuf: 输出缓冲区的指针,指向打包后的数据存储位置。outcount: 输出缓冲区中可用的空间大小。position: 一个整数指针,用于返回当前在输出缓冲区中的位置。comm: 通信器对象,用于指定进程之间的通信。
该函数的作用是将输入缓冲区中的数据按照指定的数据类型进行打包,并将打包后的数据存储到输出缓冲区中。打包后的数据可以在不同的进程之间传输或存储。
请注意,这只是一个函数声明,具体的实现细节和用法可能因使用的MPI库而有所不同。
实验说明
在源进程中打包发送一个数据包到进程1,进程1解包并打印出数据。
输出结果
The number is 1 and 2
1 |
|
数据的解包(unpack)
解包是对应于打包的MPI操作。
需要特别注意的是:MPI_RECV和MPI_UNPACK的区别: 在MPI_RECV中, count参数指明的是可以接收的最大项数. 实际接收的项数是由接收的消息的长度来决定的. 在MPI_UNPACK中, count参数指明实际打包的项数; 相应消息的”size”是position的增加值. 这种改动的原因是”输入消息的大小” 直到用户决定如何解包之前是不能预先确定的;从解包的项数来确定”消息大小”也是很困难的。
函数说明
1 | int MPI_Unpack(void* inbuf, int insize, int *position, void *outbuf, int outcount, MPI_Datatype datatype, MPI_Comm comm) |
MPI_Unpack函数用于将打包的数据从输入缓冲区解包到输出缓冲区。
参数说明:
inbuf:指向输入缓冲区的指针,其中包含要解包的数据。insize:输入缓冲区的大小(以字节为单位)。position:一个整数指针,用于返回当前在输入缓冲区中的位置。outbuf:指向输出缓冲区的指针,用于存储解包后的数据。outcount:输出缓冲区的大小(以元素为单位)。datatype:数据类型的对象,指定了输入缓冲区和输出缓冲区中元素的类型。comm:通信器对象,用于指定进程之间的通信。
该函数的作用是将打包的数据从输入缓冲区解包到输出缓冲区,以便在不同的进程之间传输或存储。解包后的数据可以按照指定的数据类型进行解析和使用。
实验说明
在源进程中打包发送一个数据包到编号为1的进程,编号为1的进程解包并打印出数据。
输出结果
The number is 1 and 2
1 |
|
组的管理-集合类操作
对于两个集合,我们经常对其进行各种各样的集合操作,例如交/并。
MPI同样提供了对组的集合类操作。
函数说明
1 | int MPI_Group_union(MPI_Group group1, MPI_Group group2, MPI_Group *newgroup) |
MPI_Group_union函数用于计算两个进程组的并集。它接受三个参数:
group1:第一个进程组。group2:第二个进程组。newgroup:指向新进程组的指针,该进程组将包含group1和group2的并集。该函数返回一个整数,表示操作的结果。如果成功,返回值为
MPI_SUCCESS;否则,返回一个非零的错误代码。以下是一个示例代码片段,演示如何使用
MPI_Group_union函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main(int argc, char *argv[]) {
MPI_Init(&argc, &argv);
// 创建两个进程组
MPI_Group group1, group2;
MPI_Comm_group(MPI_COMM_WORLD, &group1);
MPI_Comm_group(MPI_COMM_WORLD, &group2);
// 计算进程组的并集
MPI_Group newgroup;
int result = MPI_Group_union(group1, group2, &newgroup);
if (result != MPI_SUCCESS) {
printf("Error: MPI_Group_union failed.\n");
return -1;
}
// 打印结果
printf("Process groups union successful.\n");
// 释放资源
MPI_Group_free(&group1);
MPI_Group_free(&group2);
MPI_Group_free(&newgroup);
MPI_Finalize();
return 0;
}请注意,上述代码仅为示例,实际使用时需要根据具体情况进行适当的修改和错误处理。
实验说明
将组按照编号的奇偶分为两个新的组,再用并操作将它们合起来,输出各个进程在新的组的编号。
输出结果
In process 0: union rank is 2
In process 1: union rank is 0
In process 2: union rank is 3
In process 3: union rank is 1
1 |
|