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;
}
相关推荐
灵感__idea8 小时前
JavaScript高级程序设计(第5版):无处不在的集合
前端·javascript·程序员
dmy10 小时前
n8n内网快速部署
运维·人工智能·程序员
憨憨睡不醒啊12 小时前
如何让LLM智能体开发助力求职之路——构建属于你的智能体开发知识体系📚📚📚
面试·程序员·llm
程序员岳焱13 小时前
Java 程序员成长记(二):菜鸟入职之 MyBatis XML「陷阱」
java·后端·程序员
liangdabiao15 小时前
让AI写出真正可用的图文并茂的帖子(微信公众号,小红书,博客)
程序员
安妮的心动录15 小时前
人是习惯的结果
面试·程序员·求职
小兵张健16 小时前
笔记本清灰记录
程序员
陈随易19 小时前
Univer v0.8.0 发布,开源免费版 Google Sheets
前端·后端·程序员
陈随易2 天前
Element Plus 2.10.0 重磅发布!新增Splitter组件
前端·后端·程序员
陈随易2 天前
2025年100个产品计划之第11个(哆啦工具箱) - 像哆啦A梦口袋一样丰富的工具箱
前端·后端·程序员