1. 安装配置MPI库
1.1 了解基本信息
消息传递接口(Message Passing Interface, MPI)是一个跨语言的通信协议,目的是实现一个具有高性能和可移植性的大规模并行程序。
通过在每个节点(机器) 启用一个或多个进程来实现并行计算,每个进程可以含多个线程,有时可以结合使用MPI和OpenMP。
官方文档 www.open-mpi.org/doc/ 推荐4.1版本
参考书籍:《并行计算导论》张林波;《并行算法实践》陈国良
常见MPI库: intel MPI, MS MPI, Open MPI, MPICH, 建议前两种
- 安装intel oneAPI全家桶,省心;
- 单独安装intel MPI, 运行可能遇到通信问题
- 使用MS MPI,需要安装两个文件,分别是mpi库和执行程序mpiexec
1.2 在wsl上配置安装mpi库
由于配置vscode环境的繁琐,个人准备直接在wsl上配置,并且使用OpenMPI,学习来说完全够用,而且是开源的,在linux上广泛使用
安装 MPI 在 WSL 上
- 更新 WSL
在安装任何软件之前,确保你的 WSL 系统已经是最新的:
sql
sudo apt update && sudo apt upgrade -y
- 安装 OpenMPI
你可以通过包管理器 apt
来安装 OpenMPI:
python
sudo apt install openmpi-bin openmpi-common libopenmpi-dev
- 验证安装
安装完成后,你可以通过运行以下命令来验证 OpenMPI 是否已正确安装:
css
mpirun --version
如果 OpenMPI 安装成功,这条命令将输出 OpenMPI 的版本信息。
- 运行 MPI 程序
编写一个简单的 MPI 程序,保存为 hello_mpi.c
,例如:
出现头文件爆红,右击quick fix添加include路径即可
objectivec
#include <stdio.h>
int main(int argc, char** argv) {
MPI_Init(NULL, NULL);
int world_size;
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
int world_rank;
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
printf("Hello world from rank %d out of %d processors\n", world_rank, world_size);
MPI_Finalize();
return 0;
}
- 编译 MPI 程序
使用 mpicc
编译 MPI 程序:
mpicc hello_mpi.c -o hello_mpi
- 运行 MPI 程序
使用 mpirun
运行程序:
bash
mpirun -np 4 ./hello_mpi
这条命令将启动 4 个进程运行 MPI 程序,输出应该类似于:
csharp
Hello world from rank 0 out of 4 processors
Hello world from rank 1 out of 4 processors
Hello world from rank 2 out of 4 processors
Hello world from rank 3 out of 4 processors
2. MPI基础知识
2.1 阻塞/非阻塞函数,四种通信模式
MPI启用多个进程实现并行计算,每个进程都独立内存空间,进程间以消息进行信息交换(MPI 3.0 引入共享内存机制,同一节点上多个进程可以共享信息,但使用比较繁琐)。编写MPI中心在于处理通信
通信函数被调用后,需要一定时间完成通信操作,此期间:
- 调用进程可以把相关函数分为阻塞函数 ( 需要等待指定操作实际完成,或者数据被安全备份后才返回 )和非阻塞函数(不关心操作是否完成,调用后立即返回) 阻塞函数例子:手洗衣服,洗碗才能干其他事 非阻塞函数例子:机洗衣服启动,去干其他事
- 根据存储被发送数据变量的可用状态,分为缓存通信 (把数据拷贝到缓冲区,然后对缓冲区数据执行通信操作,大小由系统给定,或通过MPI_Buffer_attach和MPI_Buffer_detach申请和释放缓冲区)和非缓存通信( 直接对变量进行通信操作 ) 缓存:倒水时先倒到一个实现准备好的桶,要水的时候直接去桶里取 非缓存:直接把水倒在目的的洗衣盆里
四种通信模式:
- 标准通信(standard mode): MPI自行决定是否缓存数据;
- 缓存通信(buffered mode): 将发送消息拷贝至缓冲区立即返回,消息的发送由MPI系统在后台进行。
- 同步通信(synchronous mode): 同步发送时,接收方开始接收到消息后才正确返回
- 就绪通信(ready mode): 就绪发送时,必须确保接收方已经就绪(正等待接受该消息),否则就会调用产生一个错误
总结: 前两者标准和缓存是不需要和接收方进行沟通的;
通信函数举例
通信功能 阻塞 非阻塞
Standard Send MPI_Send MPI_Isend
Synchronous Send MPI_Ssend MPI_Issend
Buffered Send MPI_Bsend MPI_Ibsend
Ready Send MPI_Rsend MPI_Irsend
Receive MPI_Recv MPI_Irecv
Completion Check MPI_Wait MPI_Test
这边非阻塞的I指的是Immediate。MPI_Isend 全名是 Immediate Send指的是立即发送,不会阻塞程序的执行
MPI_Wait
功能: MPI_Wait
是一个阻塞的完成检查函数。调用 MPI_Wait
后,程序会暂停并等待,直到指定的通信操作完成。
使用场景: 当你需要确保一个非阻塞通信已经完成,并且不能继续执行后续代码之前,就可以使用 MPI_Wait
。例如,发送或接收完成之前,程序不应处理发送的数据或接收的数据。
MPI_Test
功能 : MPI_Test
是一个非阻塞的完成检查函数。它立即返回,并且不等待通信完成。如果通信已经完成,它会返回 true
,否则返回 false
。这样可以让程序继续执行其他操作,而不是一直等待通信完成。
使用场景 : MPI_Test
适合用于需要时不时检查通信是否完成但不愿意阻塞程序的场景。你可以在某个循环中定期使用 MPI_Test
检查通信是否完成,然后在通信完成后处理后续工作。
- 点对点通信
-
MPI_Send/ MPI_Recv:
- 点对点通信的基本函数。
MPI_Send
用于发送消息,MPI_Recv
用于接收消息。发送方和接收方通过进程编号(如P0到P1)以及标识符进行匹配。 - 这种方式是阻塞式通信,直到发送/接收完成,双方才继续执行后续操作。
- 点对点通信的基本函数。
-
MPI_Sendrecv:
- 该函数用于同时发送和接收消息。一个进程可以在发送的同时接收消息,避免使用单独的
MPI_Send
和MPI_Recv
。
- 该函数用于同时发送和接收消息。一个进程可以在发送的同时接收消息,避免使用单独的
- 广播与散播
-
MPI_Bcast
- 广播通信,某个进程(通常是根进程)将消息发送给所有进程。比如图中P0向其他进程发送消息,所有进程(P1和P2)都会接收到消息。
-
MPI_Scatter
MPI_Scatterv
- 散播通信,根进程将一组数据分成若干部分,并将每部分发送给不同的进程。
MPI_Scatter
适合均匀分配数据,而MPI_Scatterv
可以处理不均匀的数据分配。
- 散播通信,根进程将一组数据分成若干部分,并将每部分发送给不同的进程。
- 汇聚与规约
-
MPI_Reduce
- 规约操作,多个进程中的数据通过某种方式(如加和、取最小值、最大值等)进行规约,最后将结果发送到根进程。图中表现为所有进程向P0发送结果。
-
MPI_Gather
MPI_Gatherv
- 汇聚通信,所有进程将数据发送给根进程,根进程会将所有数据汇聚在一起。
MPI_Gatherv
适用于不均匀大小的数据。
- 汇聚通信,所有进程将数据发送给根进程,根进程会将所有数据汇聚在一起。
- 全局通信
-
MPI_Alltoall
MPI_Alltoallv
MPI_Alltoallw:
- 全对全通信,每个进程都向所有其他进程发送数据,并接收来自所有其他进程的数据。
MPI_Alltoall
是用于均匀数据的通信,而MPI_Alltoallv
和MPI_Alltoallw
允许处理不均匀的数据。
- 全对全通信,每个进程都向所有其他进程发送数据,并接收来自所有其他进程的数据。
v 代表 variable(可变的) ,它意味着数据的大小或数量可以根据不同的进程而变化。后缀带有 v
的函数,如 MPI_Scatterv
、MPI_Gatherv
、MPI_Alltoallv
,表示该函数支持 "不规则大小的数据" ,即不同进程之间可以发送或接收的数据量不需要相同。
- 对等模式(Peer Mode)
- 在对等模式中,所有进程是平等的,每个进程都会执行相同的工作,通常会将任务平均分配给所有进程。
- 适用场景:当每个进程所需的计算量大致相等时,使用对等模式可以提高效率,因为所有进程都同时执行相似的工作,不存在负载不平衡问题。
- 主从模式(Master-Slave Mode)
- 主从模式也被称为管理模式。在这种模式下,总任务会先被分成多个任务片段。每个片段包含一个或多个子任务,主进程(一般为主节点)负责将任务片段分配给从进程。每次分配一个任务片段,直到所有任务都分配完成。
- 适用场景:当各个任务的计算量差异较大时,这种模式更适用,因为主进程可以动态调整任务分配,以确保负载平衡。
- 该模式至少需要两个进程,一个作为主进程管理任务分配,其余作为从进程接收并执行任务。
小结:
- 对等模式 是用于 任务均匀分配 的场景,进程间负载均衡。
- 主从模式 更适用于 任务不均匀分配 的场景,由主进程动态调度任务,从进程负责计算。
2.2 openmp和mpi的一些区别
- 并行区域
- MPI: 整个程序都可以作为并行区域,即通过 MPI 库,程序可以在多个节点(集群)之间并行执行。
- OpenMP: 主要适用于单机多核并行,定义某些代码块为并行区域。
- 并行单元
- MPI: 使用进程来并行执行,进程之间独立,使用外部命令行参数指定,并且不共享内存。
- OpenMP: 使用线程来并行执行,线程间共享内存。线程数量可以通过环境变量或代码指定。
- 单元排布
- MPI: 默认进程布局是 1 维的,但可以通过用户配置成多维进程通信拓扑。
- OpenMP: 单维线程布局,较为简单。
- 信息交换
- MPI: 通过消息传递的方式来进行进程间通信,进程间不共享内存。
- OpenMP: 使用共享内存进行通信,线程之间通过共享变量进行数据交换。
- 硬件环境
- MPI: 适用于单机或集群,传统上仅支持 CPU,但也可通过适配支持 GPU。
- OpenMP: 主要用于单机多核,但标准上也逐步支持 GPU 并行计算。
- 软件环境
- MPI: 需要独立安装 MPI 库,常用的库有 MPICH 和 OpenMPI。
- OpenMP: 大多数编译器(如 GCC 和 Intel)自带 OpenMP 支持,无需额外安装。
- 并行效率
- MPI: 如果仅考虑并行部分,效率接近 1,但因为通信和同步开销,实际效率会有所下降。
- OpenMP: 实际并行效率在 0.8 到 0.9 之间,复杂度较低的计算时空送代算法约为 0.7。
MPI 更适用于跨多个节点的分布式并行计算,而 OpenMP 更适合单机上的多线程并行计算,尤其在开发环境简单、需要高效内存共享的场景下,OpenMP 是更便捷的选择。
3. MPI常用函数及并行模式示例
3.1 基本结构
3.11 MPI基本函数
MPI代码应该始终由MPI_init / MPI_Init_thread语句开始执行,并在MPI_Finalize语句之后,其余执行在两者之间,否则会得到一个不可预期的状态
objectivec
MPI_Init / MPI_Init_thread //程序的第一个执行语句,初始化MPI库
MPI_Finalize //程序正确返回之前最后一个执行语句,释放MPI库使用的资源
MPI_Comm_size //获取通信域中的进程数
MPI_Comm_rank //获取通信域中的编号
MPI_Wtime //计时函数,调用两次以计算耗时
MPI_Barrier //进程同步
MPI_COMM_WORLD
是 MPI(消息传递接口)中一个非常重要的概念。它是一个预定义的通信器,表示一个包含所有参与 MPI 程序的进程的集合。下面是对 MPI_COMM_WORLD
的详细解释:
功能
-
全局通信器:
MPI_COMM_WORLD
包含了在 MPI 程序中启动的所有进程。每个进程都有一个唯一的秩/编号(rank),从 0 开始到size - 1
,其中size
是总进程数。
-
进程间通信:
- 使用
MPI_COMM_WORLD
,你可以实现所有进程之间的通信。例如,使用MPI_Send
和MPI_Recv
函数时,可以指定MPI_COMM_WORLD
作为通信的目标,允许任意两个进程进行数据交换。
- 使用
-
同步操作:
MPI_Barrier(MPI_COMM_WORLD)
等同步操作可以确保所有进程在特定的代码位置上都达到同步点。
-
获取信息:
- 使用
MPI_Comm_size(MPI_COMM_WORLD, &size)
可以获取通信器中进程的数量。 - 使用
MPI_Comm_rank(MPI_COMM_WORLD, &rank)
可以获取当前进程在通信器中的秩。
- 使用
代码示例
以下是一个简单示例,展示如何使用 MPI_COMM_WORLD
:
objectivec
#include <stdio.h>
#include <mpi.h>
int main(int argc, char **argv) {
MPI_Init(&argc, &argv); // 初始化 MPI
int size, rank;
MPI_Comm_size(MPI_COMM_WORLD, &size); // 获取进程总数
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 获取当前进程的秩
printf("Hello from rank %d out of %d processes\n", rank, size);
MPI_Finalize(); // 结束 MPI
return 0;
}
总结
MPI_COMM_WORLD
是 MPI 中的一个核心组件,它简化了进程间的通信,使得编写并行程序更为高效。使用MPI_COMM_WORLD
,你可以轻松地管理所有进程之间的消息传递、同步和数据聚合。
objectivec
//mpi_01.c
#include "stdio.h"
#include "mpi.h"
int main(int argc, char **argv)
{
int size, rank;
double t0, t1;
MPI_Init(&argc,&argv); //MPI_Init 函数初始化 MPI 环境,必须在调用其他 MPI 函数之前调用。
//MPI_COMM_WORLD 是一个包含所有MPI程序进程的集合
MPI_Comm_size(MPI_COMM_WORLD,&size); //MPI_Comm_size 获取当前通信器(MPI_COMM_WORLD)的大小(总进程数),并存储在 size 中。
MPI_Comm_rank(MPI_COMM_WORLD,&rank);//MPI_Comm_rank 获取当前进程在通信器中的编号,并存储在 rank 中。
t0 = MPI_Wtime();//MPI_Wtime 返回当前的壁钟时间,赋值给 t0。这是测量时间的起始点。
printf("rank/size = %d%d\n",rank,size);
t1 = MPI_Wtime();//重新获取当前时间,赋值给 t1。这时 t1 是当前进程的执行时间结束点。
MPI_Barrier(MPI_COMM_WORLD);//MPI_Barrier 是一个同步操作,确保所有进程都在此点之前完成所有先前的操作
t1 -= t0;
printf("rank = %d, time = %f\n", rank, t1);
MPI_Reduce(&t1,&t0,1,MPI_DOUBLE,MPI_MAX,0,MPI_COMM_WORLD);
if( rank == 0)printf("maxTime: %f\n",t0);
MPI_Finalize();
return 0;
}
bash
# 编译及其运行
mpicc -o mpi_01 mpi_01.c
mpirun -np 4 ./mpi_01
通信域是全部或部分进程的集合,默认通信域MPI_COMM_WORLD是所有进程的一维集合,MPI通信必须在特定的通信域执行
3.12 MPI在C语言的基本数据类型
在 C 语言中,MPI(消息传递接口)提供了一些主要的数据类型,供程序员在进行进程间通信时使用。以下是 MPI 在 C 语言中主要数据类型的简要介绍:
- 基本数据类型
这些数据类型通常直接映射到 C 语言中的基本数据类型,主要包括:
-
MPI_INT:
- 对应于 C 的
int
类型,用于传输整数数据。
- 对应于 C 的
-
MPI_FLOAT:
- 对应于 C 的
float
类型,用于传输单精度浮点数。
- 对应于 C 的
-
MPI_DOUBLE:
- 对应于 C 的
double
类型,用于传输双精度浮点数。
- 对应于 C 的
-
MPI_CHAR:
- 对应于 C 的
char
类型,用于传输字符数据。
- 对应于 C 的
-
MPI_LONG:
- 对应于 C 的
long
类型,用于传输长整型数据。
- 对应于 C 的
-
MPI_SHORT:
- 对应于 C 的
short
类型,用于传输短整型数据。
- 对应于 C 的
-
MPI_UNSIGNED:
- 对应于 C 的无符号整数类型,用于传输无符号整数。
-
MPI_BYTE:
- 表示字节数据,适用于传输原始字节流。
- 复合数据类型
MPI 允许用户定义复合数据类型,用于组合多种基本类型,适用于复杂的数据结构。主要包括:
-
MPI_Type_contiguous:
- 用于创建一个由多个相同数据类型的元素组成的连续数据类型。
-
MPI_Type_vector:
- 用于创建向量数据类型,适合定期的多维数组。
-
MPI_Type_create_struct:
- 用于创建结构体数据类型,可以将不同的基本类型组合在一起。例如,创建一个包含整数和浮点数的结构体类型。
-
MPI_Type_hvector 和 MPI_Type_hindexed:
- 用于创建具有不规则数据分布的类型。
- 特殊数据类型
这些类型通常在特定场合下使用:
-
MPI_PACKED:
- 用于表示打包数据类型,允许将不同类型的数据组合成一个消息。
objectivec#include <stdio.h> // 引入标准输入输出库 #include <mpi.h> // 引入 MPI 库 int main(int argc, char **argv) { // 初始化 MPI 环境 MPI_Init(&argc, &argv); int rank; // 变量用于存储当前进程的秩 // 获取当前进程的秩 MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 使用 MPI_INT 发送整数 int value = rank * 10; // 每个进程生成一个整数值,值为进程秩乘以 10 if (rank == 0) { // 如果当前进程是秩为 0 的进程(进程 0) // 发送整型数据到进程 1 MPI_Send(&value, 1, MPI_INT, 1, 0, MPI_COMM_WORLD); } else if (rank == 1) { // 如果当前进程是秩为 1 的进程(进程 1) // 接收来自进程 0 的整型数据 MPI_Recv(&value, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); // 输出接收到的整型值 printf("Received value: %d\n", value); } // 使用 MPI_DOUBLE 发送双精度浮点数 double dvalue = rank * 1.5; // 每个进程生成一个双精度浮点数,值为进程秩乘以 1.5 if (rank == 0) { // 如果当前进程是秩为 0 的进程 // 发送双精度浮点数到进程 1 MPI_Send(&dvalue, 1, MPI_DOUBLE, 1, 1, MPI_COMM_WORLD); } else if (rank == 1) { // 如果当前进程是秩为 1 的进程 // 接收来自进程 0 的双精度浮点数 MPI_Recv(&dvalue, 1, MPI_DOUBLE, 0, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE); // 输出接收到的双精度浮点值 printf("Received double value: %f\n", dvalue); } // 结束 MPI 环境 MPI_Finalize(); return 0; // 返回 0 表示程序成功结束 }
3.2 对等模式示例
- 点对点通信
MPI_Send 和 MPI_Recv
这些是 MPI 中最基本的通信方式,用于在两个进程之间传输数据。
示例代码:
objectivec#include <stdio.h> // 引入标准输入输出库 #include <mpi.h> // 引入 MPI 库 int main(int argc, char **argv) { MPI_Init(&argc, &argv); // 初始化 MPI 环境 int rank; // 变量用于存储当前进程的秩 MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 获取当前进程的秩(rank) int value; // 用于存储要发送或接收的值 if (rank == 0) { value = 42; // 进程 0 发送值 42 // 使用 MPI_Send 将值发送到进程 1 MPI_Send(&value, 1, MPI_INT, 1, 0, MPI_COMM_WORLD); // 参数: 发送值的地址, 发送数量, 数据类型, 接收进程的秩, 消息标签, 通信域 printf("Process 0 sent value %d to Process 1\n", value); // 打印发送的信息 } else if (rank == 1) { // 使用 MPI_Recv 从进程 0 接收值 MPI_Recv(&value, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); // 参数: 接收值的地址, 接收数量, 数据类型, 发送进程的秩, 消息标签, 通信域, 状态对象 printf("Process 1 received value %d from Process 0\n", value); // 打印接收到的信息 } MPI_Finalize(); // 结束 MPI 环境 return 0; // 返回 0 表示程序成功结束 }
解释:
- 进程 0 发送一个整数值(42)到进程 1。
- 使用
MPI_Send
和MPI_Recv
进行阻塞式通信,即在发送或接收完成前,进程将暂停执行。
MPI_Sendrecv
该函数允许同时发送和接收消息。
示例代码:
objectivecc复制代码#include <stdio.h> #include <mpi.h> int main(int argc, char **argv) { MPI_Init(&argc, &argv); // 初始化 MPI 环境 int rank; // 变量用于存储当前进程的秩 int send_value, recv_value; // 用于存储发送和接收的值 MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 获取当前进程的秩 send_value = rank; // 发送自己进程的秩值 // 使用 MPI_Sendrecv 同时发送和接收 MPI_Sendrecv(&send_value, 1, MPI_INT, (rank + 1) % 2, 0, // 发送到下一个进程 &recv_value, 1, MPI_INT, (rank - 1 + 2) % 2, 0, // 从上一个进程接收 MPI_COMM_WORLD, MPI_STATUS_IGNORE); // 通信域和状态对象 printf("Process %d sent value %d and received value %d\n", rank, send_value, recv_value); // 打印发送和接收的值 MPI_Finalize(); // 结束 MPI 环境 return 0; // 返回 0 表示程序成功结束 }
解释:
- 每个进程发送其秩值并接收来自其他进程的秩值。
- 使用
MPI_Sendrecv
可以同时进行发送和接收,简化了代码。
2. 广播与散播
MPI_Bcast
用于将数据从一个进程广播到所有其他进程。
示例代码:
objectivec#include <stdio.h> #include <mpi.h> int main(int argc, char **argv) { MPI_Init(&argc, &argv); // 初始化 MPI 环境 int rank; // 变量用于存储当前进程的秩 int value; // 用于存储要广播的值 MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 获取当前进程的秩 if (rank == 0) { value = 100; // 进程 0 初始化要广播的值 } // 使用 MPI_Bcast 广播数据 MPI_Bcast(&value, 1, MPI_INT, 0, MPI_COMM_WORLD); // 参数: 广播值的地址, 广播数量, 数据类型, 根进程的秩, 通信域 printf("Process %d received value %d\n", rank, value); // 打印接收到的值 MPI_Finalize(); // 结束 MPI 环境 return 0; // 返回 0 表示程序成功结束 }
解释:
- 进程 0 初始化一个值(100),然后通过
MPI_Bcast
将其广播到所有进程。 - 所有进程都会接收到相同的值。
MPI_Scatter 和 MPI_Scatterv
MPI_Scatter
将数据从根进程分散到各个进程,而MPI_Scatterv
则适用于不均匀分配。示例代码(使用
MPI_Scatter
):objectivec#include <stdio.h> #include <mpi.h> int main(int argc, char **argv) { MPI_Init(&argc, &argv); // 初始化 MPI 环境 int rank, size; // 变量用于存储当前进程的秩和总进程数 MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 获取当前进程的秩 MPI_Comm_size(MPI_COMM_WORLD, &size); // 获取总进程数 int data[4]; // 进程 0 的数据 int recv_data; // 接收数据的变量 if (rank == 0) { // 初始化数据 for (int i = 0; i < 4; i++) { data[i] = i * 10; // 进程 0 生成一个数组 } } // 使用 MPI_Scatter 散播数据 MPI_Scatter(data, 1, MPI_INT, &recv_data, 1, MPI_INT, 0, MPI_COMM_WORLD); // 参数: 数据源, 每个进程接收数量, 数据类型, 接收变量, 根进程的秩, 通信域 printf("Process %d received value %d\n", rank, recv_data); // 打印接收到的值 MPI_Finalize(); // 结束 MPI 环境 return 0; // 返回 0 表示程序成功结束 }
解释:
- 进程 0 初始化一个数组,使用
MPI_Scatter
将数组中的每个元素分发到各个进程。 - 每个进程接收到一个元素并打印。
3. 汇聚与规约
MPI_Reduce
用于将多个进程中的数据进行规约操作,比如求和或取最大值。
示例代码:
objectivec#include <stdio.h> #include <mpi.h> int main(int argc, char **argv) { MPI_Init(&argc, &argv); // 初始化 MPI 环境 int rank, value, result; // 变量用于存储进程秩、每个进程的值和规约结果 MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 获取当前进程的秩 value = rank + 1; // 每个进程的值为其秩加 1 // 使用 MPI_Reduce 进行规约操作 MPI_Reduce(&value, &result, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD); // 参数: 输入值的地址, 结果地址, 数据数量, 数据类型, 规约操作, 根进程的秩, 通信域 if (rank == 0) { printf("Total sum is %d\n", result); // 只有进程 0 输出总和 } MPI_Finalize(); // 结束 MPI 环境 return 0; // 返回 0 表示程序成功结束 }
解释:
- 每个进程计算一个值,并通过
MPI_Reduce
将所有进程的值相加,最终结果存储在进程 0 中。 - 进程 0 输出总和。
MPI_Gather 和 MPI_Gatherv
MPI_Gather
用于将所有进程的数据发送到根进程,而MPI_Gatherv
处理不均匀大小的数据。示例代码(使用
MPI_Gather
):objectivec#include <stdio.h> #include <mpi.h> int main(int argc, char **argv) { MPI_Init(&argc, &argv); // 初始化 MPI 环境 int rank, value; // 变量用于存储进程秩和发送的值 MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 获取当前进程的秩 value = rank + 1; // 每个进程的值 // 创建数组以存储汇聚到根进程的数据 int gather_data[4]; // 假设总进程数为 4 // 使用 MPI_Gather 将所有进程的数据汇聚到进程 0 MPI_Gather(&value, 1, MPI_INT, gather_data, 1, MPI_INT, 0, MPI_COMM_WORLD); // 参数: 发送值的地址, 每个进程发送数量, 数据类型, 汇聚到根进程的数组, 汇聚数量, 数据类型, 根进程的秩, 通信域 if (rank == 0) { printf("Gathered data: "); for (int i = 0; i < 4; i++) { printf("%d ", gather_data[i]); // 打印汇聚到进程 0 的所有值 } printf("\n"); } MPI_Finalize(); // 结束 MPI 环境 return 0; // 返回 0 表示程序成功结束 }
解释:
- 每个进程将自己的值汇聚到进程 0,使用
MPI_Gather
收集所有进程的数据。
4. 全局通信
MPI_Alltoall
每个进程都向所有其他进程发送数据,并接收来自所有其他进程的数据。
示例代码:
arduino#include <mpi.h> int main(int argc, char **argv) { MPI_Init(&argc, &argv); // 初始化 MPI 环境 int rank, size; // 变量用于存储当前进程的秩和总进程数 MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 获取当前进程的秩 MPI_Comm_size(MPI_COMM_WORLD, &size); // 获取总进程数 int send_data[size]; // 发送数据的数组 int recv_data[size]; // 接收数据的数组 // 初始化发送数据 for (int i = 0; i < size; i++) { send_data[i] = rank * 10 + i; // 每个进程的发送数据 } // 使用 MPI_Alltoall 进行全对全通信 MPI_Alltoall(send_data, 1, MPI_INT, recv_data, 1, MPI_INT, MPI_COMM_WORLD); // 参数: 发送数组, 每个进程发送数量, 数据类型, 接收数组, 每个进程接收数量, 数据类型, 通信域 // 输出接收到的数据 printf("Process %d received: ", rank); for (int i = 0; i < size; i++) { printf("%d ", recv_data[i]); // 打印接收到的数据 } printf("\n"); MPI_Finalize(); // 结束 MPI 环境 return 0; // 返回 0 表示程序成功结束 }
解释:
- 每个进程初始化一个发送数组并通过
MPI_Alltoall
发送到所有其他进程,接收的数据也存储在相应的接收数组中。 - 每个进程输出自己接收到的数据。
总结
以上示例展示了 MPI 的几种主要通信模式,包括点对点通信、广播与散播、汇聚与规约以及全局通信。
4. 传输结构体数据
创建mpi数据类型,提交mpi数据类型,释放相关资源
4.1 创建MPI数据类型
结构体定义: MyStruct
定义了我们想要通过MPI发送的自定义数据结构。
MPI 初始化: MPI_Init
、MPI_Comm_rank
和 MPI_Comm_size
是标准的MPI初始化步骤。
创建自定义数据类型:
blocklengths
数组定义了每个元素的数量。types
数组定义了每个元素的MPI数据类型。offsets
数组使用offsetof
宏计算每个元素在结构体中的内存偏移量。
提交数据类型: MPI_Type_commit
优化数据类型,使其可以在MPI通信中使用。
使用自定义数据类型: 示例展示了如何使用自定义数据类型在进程间发送和接收 MyStruct
实例。
释放资源: MPI_Type_free
释放与自定义数据类型相关的资源,防止内存泄漏。
objectivec
#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>
// Define a custom struct that we want to send via MPI
typedef struct {
int id;
double value;
char name[20];
} MyStruct;
int main(int argc, char** argv) {
// Initialize the MPI environment
MPI_Init(&argc, &argv);
int rank, size;
// Get the rank of the process
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
// Get the number of processes
MPI_Comm_size(MPI_COMM_WORLD, &size);
// Step 1: Create MPI datatype
MPI_Datatype mystruct_type;
// Define the number of elements in each block
int blocklengths[3] = {1, 1, 20};
// Define the MPI datatypes of each element
MPI_Datatype types[3] = {MPI_INT, MPI_DOUBLE, MPI_CHAR};
// Calculate the memory offsets for each element in the struct
MPI_Aint offsets[3];
offsets[0] = offsetof(MyStruct, id);
offsets[1] = offsetof(MyStruct, value);
offsets[2] = offsetof(MyStruct, name);
// Create the struct datatype
MPI_Type_create_struct(3, blocklengths, offsets, types, &mystruct_type);
// Step 2: Commit MPI datatype
// This step is necessary to optimize the datatype and make it ready for use
MPI_Type_commit(&mystruct_type);
// Use the custom datatype for communication
if (rank == 0) {
// Process 0 sends data
MyStruct send_data = {42, 3.14, "Hello MPI"};
// Send the struct using our custom datatype
MPI_Send(&send_data, 1, mystruct_type, 1, 0, MPI_COMM_WORLD);
printf("Rank 0 sent: id=%d, value=%f, name=%s\n", send_data.id, send_data.value, send_data.name);
} else if (rank == 1) {
// Process 1 receives data
MyStruct recv_data;
// Receive the struct using our custom datatype
MPI_Recv(&recv_data, 1, mystruct_type, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
printf("Rank 1 received: id=%d, value=%f, name=%s\n", recv_data.id, recv_data.value, recv_data.name);
}
// Step 3: Free MPI datatype
// It's important to free the datatype when we're done to prevent memory leaks
MPI_Type_free(&mystruct_type);
// Finalize the MPI environment
MPI_Finalize();
return 0;
}
4.2 数据打包和解包
MPI中的数据打包和解包是非常有用的技术,特别是在需要发送不同类型或不连续的数据时。
- 数据打包 (MPI_Pack) 数据打包允许您将不同类型的数据组合到一个缓冲区中,以便一次性发送。
- 数据解包 (MPI_Unpack) 与打包相对应,解包允许您从接收的缓冲区中提取各个数据项。
- MPI_Probe 用于检查即将到来的消息的大小和其他属性,而不实际接收消息。
- MPI_Get_Count 用于确定在特定数据类型下接收到的元素数量。
这个例子展示了如何使用MPI_Pack、MPI_Unpack、MPI_Probe和MPI_Get_Count函数。
-
数据打包 (rank 0):
- 使用
MPI_Pack_size
计算需要的缓冲区大小。 - 使用
MPI_Pack
将不同类型的数据(整数、双精度浮点数和字符串)打包到一个缓冲区中。
- 使用
-
发送打包数据:
- 使用
MPI_Send
发送打包后的数据,类型为MPI_PACKED
。
- 使用
-
接收端探测 (rank 1):
- 使用
MPI_Probe
检查即将到来的消息。 - 使用
MPI_Get_count
确定接收消息的大小。
- 使用
-
接收打包数据:
- 根据探测到的大小分配接收缓冲区。
- 使用
MPI_Recv
接收打包的数据。
-
数据解包:
- 使用
MPI_Unpack
从接收到的缓冲区中提取各个数据项。
- 使用
这种方法的优点包括:
- 可以在一个消息中发送不同类型的数据。
- 允许发送非连续的内存区域。
- 接收方可以在接收之前知道消息的确切大小。
需要注意的是,虽然打包和解包提供了灵活性,但可能会引入一些性能开销。在某些情况下,使用派生数据类型可能更有效率。
objectivec
#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char** argv) {
// Initialize the MPI environment
MPI_Init(&argc, &argv);
int rank, size;
// Get the rank (process ID) of the current process
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
// Get the total number of processes
MPI_Comm_size(MPI_COMM_WORLD, &size);
// Ensure we have at least 2 processes for this example
if (size < 2) {
printf("This program requires at least 2 processes.\n");
MPI_Abort(MPI_COMM_WORLD, 1);
}
if (rank == 0) {
// Sender process (rank 0)
int int_data = 42;
double double_data = 3.14159;
char str_data[] = "Hello, MPI!";
// Calculate the total size needed for the packed buffer
int pack_size, double_pack_size;
// Get the space needed to pack an integer
MPI_Pack_size(1, MPI_INT, MPI_COMM_WORLD, &pack_size);
// Get the space needed to pack a double
MPI_Pack_size(1, MPI_DOUBLE, MPI_COMM_WORLD, &double_pack_size);
pack_size += double_pack_size;
// Add space for the string (including null terminator)
pack_size += strlen(str_data) + 1;
// Allocate the pack buffer based on the calculated size
char* pack_buffer = (char*)malloc(pack_size);
int position = 0; // Current position in the pack buffer
// Pack the integer data
MPI_Pack(&int_data, 1, MPI_INT, pack_buffer, pack_size, &position, MPI_COMM_WORLD);
// Pack the double data
MPI_Pack(&double_data, 1, MPI_DOUBLE, pack_buffer, pack_size, &position, MPI_COMM_WORLD);
// Pack the string data (including null terminator)
MPI_Pack(str_data, strlen(str_data) + 1, MPI_CHAR, pack_buffer, pack_size, &position, MPI_COMM_WORLD);
// Send the packed data to process 1
MPI_Send(pack_buffer, position, MPI_PACKED, 1, 0, MPI_COMM_WORLD);
// Free the allocated buffer
free(pack_buffer);
printf("Rank 0 sent packed data.\n");
} else if (rank == 1) {
// Receiver process (rank 1)
MPI_Status status;
// Probe for incoming message to get its size
MPI_Probe(0, 0, MPI_COMM_WORLD, &status);
// Get the size of the incoming packed message
int count;
MPI_Get_count(&status, MPI_PACKED, &count);
// Allocate buffer for receiving based on the probed size
char* recv_buffer = (char*)malloc(count);
// Receive the packed data
MPI_Recv(recv_buffer, count, MPI_PACKED, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
// Prepare to unpack the data
int position = 0;
int recv_int;
double recv_double;
char recv_str[20];
// Unpack the integer
MPI_Unpack(recv_buffer, count, &position, &recv_int, 1, MPI_INT, MPI_COMM_WORLD);
// Unpack the double
MPI_Unpack(recv_buffer, count, &position, &recv_double, 1, MPI_DOUBLE, MPI_COMM_WORLD);
// Unpack the string
MPI_Unpack(recv_buffer, count, &position, recv_str, 20, MPI_CHAR, MPI_COMM_WORLD);
// Print the received data
printf("Rank 1 received: int=%d, double=%f, string=%s\n", recv_int, recv_double, recv_str);
// Free the allocated buffer
free(recv_buffer);
}
// Finalize the MPI environment
MPI_Finalize();
return 0;
}