MPI 并行编程 Parallel Programming using MPI

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, 建议前两种

  1. 安装intel oneAPI全家桶,省心;
  2. 单独安装intel MPI, 运行可能遇到通信问题
  3. 使用MS MPI,需要安装两个文件,分别是mpi库和执行程序mpiexec

1.2 在wsl上配置安装mpi库

由于配置vscode环境的繁琐,个人准备直接在wsl上配置,并且使用OpenMPI,学习来说完全够用,而且是开源的,在linux上广泛使用

安装 MPI 在 WSL 上

  1. 更新 WSL

在安装任何软件之前,确保你的 WSL 系统已经是最新的:

sql 复制代码
sudo apt update && sudo apt upgrade -y
  1. 安装 OpenMPI

你可以通过包管理器 apt 来安装 OpenMPI:

python 复制代码
sudo apt install openmpi-bin openmpi-common libopenmpi-dev
  1. 验证安装

安装完成后,你可以通过运行以下命令来验证 OpenMPI 是否已正确安装:

css 复制代码
mpirun --version

如果 OpenMPI 安装成功,这条命令将输出 OpenMPI 的版本信息。

  1. 运行 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;
}
  1. 编译 MPI 程序

使用 mpicc 编译 MPI 程序:

mpicc hello_mpi.c -o hello_mpi
  1. 运行 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中心在于处理通信

通信函数被调用后,需要一定时间完成通信操作,此期间:

  1. 调用进程可以把相关函数分为阻塞函数 ( 需要等待指定操作实际完成,或者数据被安全备份后才返回 )和非阻塞函数(不关心操作是否完成,调用后立即返回) 阻塞函数例子:手洗衣服,洗碗才能干其他事 非阻塞函数例子:机洗衣服启动,去干其他事
  2. 根据存储被发送数据变量的可用状态,分为缓存通信 (把数据拷贝到缓冲区,然后对缓冲区数据执行通信操作,大小由系统给定,或通过MPI_Buffer_attach和MPI_Buffer_detach申请和释放缓冲区)和非缓存通信( 直接对变量进行通信操作 ) 缓存:倒水时先倒到一个实现准备好的桶,要水的时候直接去桶里取 非缓存:直接把水倒在目的的洗衣盆里

四种通信模式:

  1. 标准通信(standard mode): MPI自行决定是否缓存数据;
  2. 缓存通信(buffered mode): 将发送消息拷贝至缓冲区立即返回,消息的发送由MPI系统在后台进行。
  3. 同步通信(synchronous mode): 同步发送时,接收方开始接收到消息后才正确返回
  4. 就绪通信(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 检查通信是否完成,然后在通信完成后处理后续工作。


  1. 点对点通信
  • MPI_Send/ MPI_Recv:

    • 点对点通信的基本函数。MPI_Send 用于发送消息,MPI_Recv 用于接收消息。发送方和接收方通过进程编号(如P0到P1)以及标识符进行匹配。
    • 这种方式是阻塞式通信,直到发送/接收完成,双方才继续执行后续操作。
  • MPI_Sendrecv:

    • 该函数用于同时发送和接收消息。一个进程可以在发送的同时接收消息,避免使用单独的 MPI_SendMPI_Recv
  1. 广播与散播
  • MPI_Bcast

    • 广播通信,某个进程(通常是根进程)将消息发送给所有进程。比如图中P0向其他进程发送消息,所有进程(P1和P2)都会接收到消息。
  • MPI_Scatter

    MPI_Scatterv

    • 散播通信,根进程将一组数据分成若干部分,并将每部分发送给不同的进程。MPI_Scatter 适合均匀分配数据,而 MPI_Scatterv 可以处理不均匀的数据分配。
  1. 汇聚与规约
  • MPI_Reduce

    • 规约操作,多个进程中的数据通过某种方式(如加和、取最小值、最大值等)进行规约,最后将结果发送到根进程。图中表现为所有进程向P0发送结果。
  • MPI_Gather

    MPI_Gatherv

    • 汇聚通信,所有进程将数据发送给根进程,根进程会将所有数据汇聚在一起。MPI_Gatherv 适用于不均匀大小的数据。
  1. 全局通信
  • MPI_Alltoall

    MPI_Alltoallv

    MPI_Alltoallw:

    • 全对全通信,每个进程都向所有其他进程发送数据,并接收来自所有其他进程的数据。MPI_Alltoall 是用于均匀数据的通信,而 MPI_AlltoallvMPI_Alltoallw 允许处理不均匀的数据。

v 代表 variable(可变的) ,它意味着数据的大小或数量可以根据不同的进程而变化。后缀带有 v 的函数,如 MPI_ScattervMPI_GathervMPI_Alltoallv,表示该函数支持 "不规则大小的数据" ,即不同进程之间可以发送或接收的数据量不需要相同。


  1. 对等模式(Peer Mode)
  • 在对等模式中,所有进程是平等的,每个进程都会执行相同的工作,通常会将任务平均分配给所有进程。
  • 适用场景:当每个进程所需的计算量大致相等时,使用对等模式可以提高效率,因为所有进程都同时执行相似的工作,不存在负载不平衡问题。
  1. 主从模式(Master-Slave Mode)
  • 主从模式也被称为管理模式。在这种模式下,总任务会先被分成多个任务片段。每个片段包含一个或多个子任务,主进程(一般为主节点)负责将任务片段分配给从进程。每次分配一个任务片段,直到所有任务都分配完成。
  • 适用场景:当各个任务的计算量差异较大时,这种模式更适用,因为主进程可以动态调整任务分配,以确保负载平衡。
  • 该模式至少需要两个进程,一个作为主进程管理任务分配,其余作为从进程接收并执行任务。

小结:

  • 对等模式 是用于 任务均匀分配 的场景,进程间负载均衡。
  • 主从模式 更适用于 任务不均匀分配 的场景,由主进程动态调度任务,从进程负责计算。

2.2 openmp和mpi的一些区别

  1. 并行区域
  • MPI: 整个程序都可以作为并行区域,即通过 MPI 库,程序可以在多个节点(集群)之间并行执行。
  • OpenMP: 主要适用于单机多核并行,定义某些代码块为并行区域。
  1. 并行单元
  • MPI: 使用进程来并行执行,进程之间独立,使用外部命令行参数指定,并且不共享内存。
  • OpenMP: 使用线程来并行执行,线程间共享内存。线程数量可以通过环境变量或代码指定。
  1. 单元排布
  • MPI: 默认进程布局是 1 维的,但可以通过用户配置成多维进程通信拓扑。
  • OpenMP: 单维线程布局,较为简单。
  1. 信息交换
  • MPI: 通过消息传递的方式来进行进程间通信,进程间不共享内存。
  • OpenMP: 使用共享内存进行通信,线程之间通过共享变量进行数据交换。
  1. 硬件环境
  • MPI: 适用于单机或集群,传统上仅支持 CPU,但也可通过适配支持 GPU。
  • OpenMP: 主要用于单机多核,但标准上也逐步支持 GPU 并行计算。
  1. 软件环境
  • MPI: 需要独立安装 MPI 库,常用的库有 MPICH 和 OpenMPI。
  • OpenMP: 大多数编译器(如 GCC 和 Intel)自带 OpenMP 支持,无需额外安装。
  1. 并行效率
  • 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 的详细解释:

功能

  1. 全局通信器

    • MPI_COMM_WORLD 包含了在 MPI 程序中启动的所有进程。每个进程都有一个唯一的秩/编号(rank),从 0 开始到 size - 1,其中 size 是总进程数。
  2. 进程间通信

    • 使用 MPI_COMM_WORLD,你可以实现所有进程之间的通信。例如,使用 MPI_SendMPI_Recv 函数时,可以指定 MPI_COMM_WORLD 作为通信的目标,允许任意两个进程进行数据交换。
  3. 同步操作

    • MPI_Barrier(MPI_COMM_WORLD) 等同步操作可以确保所有进程在特定的代码位置上都达到同步点。
  4. 获取信息

    • 使用 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 语言中主要数据类型的简要介绍:

  1. 基本数据类型

这些数据类型通常直接映射到 C 语言中的基本数据类型,主要包括:

  • MPI_INT

    • 对应于 C 的 int 类型,用于传输整数数据。
  • MPI_FLOAT

    • 对应于 C 的 float 类型,用于传输单精度浮点数。
  • MPI_DOUBLE

    • 对应于 C 的 double 类型,用于传输双精度浮点数。
  • MPI_CHAR

    • 对应于 C 的 char 类型,用于传输字符数据。
  • MPI_LONG

    • 对应于 C 的 long 类型,用于传输长整型数据。
  • MPI_SHORT

    • 对应于 C 的 short 类型,用于传输短整型数据。
  • MPI_UNSIGNED

    • 对应于 C 的无符号整数类型,用于传输无符号整数。
  • MPI_BYTE

    • 表示字节数据,适用于传输原始字节流。
  1. 复合数据类型

MPI 允许用户定义复合数据类型,用于组合多种基本类型,适用于复杂的数据结构。主要包括:

  • MPI_Type_contiguous

    • 用于创建一个由多个相同数据类型的元素组成的连续数据类型。
  • MPI_Type_vector

    • 用于创建向量数据类型,适合定期的多维数组。
  • MPI_Type_create_struct

    • 用于创建结构体数据类型,可以将不同的基本类型组合在一起。例如,创建一个包含整数和浮点数的结构体类型。
  • MPI_Type_hvectorMPI_Type_hindexed

    • 用于创建具有不规则数据分布的类型。
  1. 特殊数据类型

这些类型通常在特定场合下使用:

  • 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 对等模式示例

    1. 点对点通信

    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_SendMPI_Recv 进行阻塞式通信,即在发送或接收完成前,进程将暂停执行。
    MPI_Sendrecv

    该函数允许同时发送和接收消息。

    示例代码:

    objectivec 复制代码
    c复制代码#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_InitMPI_Comm_rankMPI_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中的数据打包和解包是非常有用的技术,特别是在需要发送不同类型或不连续的数据时。

  1. 数据打包 (MPI_Pack) 数据打包允许您将不同类型的数据组合到一个缓冲区中,以便一次性发送。
  2. 数据解包 (MPI_Unpack) 与打包相对应,解包允许您从接收的缓冲区中提取各个数据项。
  3. MPI_Probe 用于检查即将到来的消息的大小和其他属性,而不实际接收消息。
  4. MPI_Get_Count 用于确定在特定数据类型下接收到的元素数量。

这个例子展示了如何使用MPI_Pack、MPI_Unpack、MPI_Probe和MPI_Get_Count函数。

  1. 数据打包 (rank 0):

    • 使用MPI_Pack_size计算需要的缓冲区大小。
    • 使用MPI_Pack将不同类型的数据(整数、双精度浮点数和字符串)打包到一个缓冲区中。
  2. 发送打包数据:

    • 使用MPI_Send发送打包后的数据,类型为MPI_PACKED
  3. 接收端探测 (rank 1):

    • 使用MPI_Probe检查即将到来的消息。
    • 使用MPI_Get_count确定接收消息的大小。
  4. 接收打包数据:

    • 根据探测到的大小分配接收缓冲区。
    • 使用MPI_Recv接收打包的数据。
  5. 数据解包:

    • 使用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;
}
相关推荐
小李小李不讲道理10 小时前
行动+思考 | 2024年度总结
前端·程序员·年终总结
聪小陈1 天前
圣诞节:记一次掘友让我感动的时刻
前端·程序员
百万蹄蹄向前冲1 天前
2024不一样的VUE3期末考查
前端·javascript·程序员
陈哥聊测试3 天前
软件格局在变,谁能扛起国产替代的大旗?
安全·程序员·产品
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭3 天前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
少年姜太公3 天前
从零开始详解js中的this(下)
前端·javascript·程序员
凌虚3 天前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
小华同学ai3 天前
ShowDoc:Star12.3k,福利项目,个人小团队的在线文档“简单、易用、轻量化”还专门针对API文档、技术文档做了优化
前端·程序员·github
小青鱼6 天前
AI编程-Cursor从入门到精通系列之常用概念及解释(二)
人工智能·程序员
捡田螺的小男孩6 天前
参数校验的十个建议!收藏好,别再给测试机会提bug~
java·后端·程序员