MPI入门

ModelEngine·创作计划征文活动 10w+人浏览 1.4k人参与

零、 概念

MPI:Message Passing Interface,MPI定义了一种进程间消息传递的接口。
实现MPI的库并没有统一,也就是说有很多不同的实现版本,例如OpenMPI、MPICH、MVAPICH、Intel MPI。

MPI的设计是SPMD(single program multiple data):即instruction是相同的,但是data是不同的。换句话说我们只有一份代码,这一份代码拷贝多份,然后不同进程执行同一份代码处理不同的数据。

一、 example

#include<stdio.h>
#include<mpi.h>

int main(int argc, char* argv[]){
	MPI_Init(NULL, NULL);
	int world_rank;
	MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
	int world_size;
	MPI_Comm_size(MPI_COMM_WORLD, &world_size);
	printf("Process %d of %d: Hello, world!\n", world_rank, world_size);
	MPI_Finalize();
}
  • MPI_Init(NULL, NULL);就是将通信环境建立起来。
  • MPI_Finalize():会将管理这些进程的所有资源释放。
  • MPI_COMM_WORLD:这是MPI的一个通信器,它包含了所有MPI创建的所有进程
  • MPI_Comm_rank:通信器里面进程的编号,必须连续。
  • MPI_Comm_size:通信器中进程的数量。

使用MPI主要是利用MPI的通信功能,MPI的通信方式主要是两种

  • point-to-point:点对点通信。
  • collectives:集合通信。

在这里插入图片描述
进程的开销是比较大的,因为每个进程都需要有自己的VM Table。

二、 point-to-point 点到点通信

点对点通信主要是用连个函数

  • MPI_Send:用于发送消息
  • MPI_Recv:用于接受消息
int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)

在这里插入图片描述
buf:内存地址,对于Send而言就是传递消息的起始地址,对于Recv而言就是用于存储接受到消息的存储起始地址,由于这里是两个进程,所以他们的VM是独立的,所以这里是需要两个不同的地址。
count:是传递多少个消息。
datatype:是传递消息的类型,这里消息的类型应该是相同的。
dest\source:对于Send,dest是它传递消息的目的地,对于Recv,source是它接受消息的来源。
tag:发送消息的标签。
comm:就是上面所说的通信器,Commucator不只可以是MPI_COMM_WORLD,也可以对这个通信器再次进行划分。
status:是Recv比Send多出来的

上面datatype是MPI预定义了一些传递消息的类型,这些类型覆盖了C datatype
在这里插入图片描述
MPI_Recv相比于MPI_Send多一个参数MPI_Status,这个参数是可以设置为MPI_STATUS_IGNORE,这会告诉MPI我们不需要任何的status,这是一个预定义的MPI的结构。如果想要知道相关的信息。可以通过MPI_Get_count
在这里插入图片描述

在这里插入图片描述

MPI消息传递的过程:
在这里插入图片描述

进程之间传递消息会有一个系统缓冲区:proc1先把数据通过网络发送到proc2的系统缓冲区,proc再从系统缓冲区读取数据。

在这里插入图片描述

标签的作用:主要是用于区分同一发送者发送的不同消息的。

三、 collectives 组通信

1、 一到多(Broadcast,Scatter)

1.1 Broadcast

MPI_Bcast(Address, Count, Datatype, Root, Comm)

标号为Root的进程发送相同的消息给标记为Comm的通信子中的所有进程。

  • Address:对于Root进程即是发送地址也是接受地址,对其他进程而言是接受缓冲区。
  • Count:是传递消息的数量
  • Datatype:发送消息的类型
  • Root:根进程的Rank id
  • Comm:要广播的通信域

在这里插入图片描述
例子:

rank=0的进程读取一个数据value值,然后通过BroadCast将这个value值传递给其他进程并进行打印。

#include <stdio.h>
#include <mpi.h>

// 标准的 main 函数写法
int main(int argc, char **argv) {
    int rank, value;

    // 1. 初始化 MPI 环境
    MPI_Init(&argc, &argv);

    // 2. 获取当前进程的 ID (rank)
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);

    // 3. 进入循环
    do {
        // --- 步骤 A: 只有 Rank 0 负责读取数据 ---
        if (rank == 0) {
            printf("Please enter a number (negative to quit): "); // 提示信息
            scanf("%d", &value);
        }

        // --- 步骤 B: 广播数据 ---
        // 关键点:
        // - 对于 Rank 0:这是发送操作,把刚才 scanf 读到的 value 发给别人。
        // - 对于 Rank 1, 2...:这是接收操作,从 Rank 0 接收数据写入自己的 value 变量。
        // - 这是一个同步点,所有人都会在这里等,直到 value 被分发完毕。
        MPI_Bcast(&value, 1, MPI_INT, 0, MPI_COMM_WORLD);

        // --- 步骤 C: 大家一起打印 ---
        // 此时,所有进程里的 value 变量都变成了 Rank 0 输入的那个值
        printf("Process %d got %d\n", rank, value);

    } while (value >= 0); // 如果输入负数,所有进程都会退出循环

    // 4. 清理 MPI 环境
    MPI_Finalize();
    return 0;
}

1.2 Scatter

MPI_Scatter(SendAddress, SendCount, SendDatatype, RecvAddress, RecvCount, RecvDatatype, Root, Comm)

相较于BroadCast,Scatter是给每个进程发送一份不同的数据,同时也包括他自己,这n个消息在Root进程的发送缓冲区中按标号顺序有序地存放,每个接受缓冲由三元组(RecvAddress, RecvCount, RecvDatatype)标识,非root进程忽略发送缓冲。

在这里插入图片描述
将10、20、30、40分别发送给rank为0、1、2、3的进程

#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>

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); // 假设 size = 4

    int *send_buffer = NULL;
    int recv_data; // 每个进程只接收 1 个整数

    // 1. 只有 Root 需要准备大数组
    if (rank == 0) {
        send_buffer = (int*)malloc(size * sizeof(int));
        // 初始化数据: [10, 20, 30, 40]
        for (int i = 0; i < size; i++) {
            send_buffer[i] = (i + 1) * 10;
        }
        printf("[Root] 准备分发数据: 10, 20, 30, 40\n");
    }

    // 2. 执行 Scatter
    // 关键点:sendcount 填 1,表示给“每个人”发 1 个 int
    // recvcount 也填 1,表示“每个人”收 1 个 int
    MPI_Scatter(send_buffer, 1, MPI_INT, 
                &recv_data, 1, MPI_INT, 
                0, MPI_COMM_WORLD);

    // 3. 打印结果
    printf("Process %d received value: %d\n", rank, recv_data);

    // 4. 清理内存
    if (rank == 0) {
        free(send_buffer);
    }

    MPI_Finalize();
    return 0;
}

1.3 Scatterrv

scatter是平均主义,root进程向其他进程发送相等的数据,而scatterrv则是root进程向其他进程发送个数不等的数据。

int MPI_Scatterv(
    const void *sendbuf,        // 发送缓冲区 (仅 Root)
    const int *sendcounts,      // [数组] 发给每个进程的数据个数 (仅 Root)
    const int *displs,          // [数组] 发给每个进程的数据在 sendbuf 中的偏移量 (仅 Root)
    MPI_Datatype sendtype,      // 发送数据类型 (仅 Root)
    
    void *recvbuf,              // 接收缓冲区 (所有人)
    int recvcount,              // 自己要接收多少个数据 (所有人)
    MPI_Datatype recvtype,      // 接收数据类型 (所有人)
    
    int root, MPI_Comm comm     // 控制参数
);

2、 多到一

2.1 Reduce

所有进程向同一进程发消息与是broadcast逆操作

int MPI_Reduce(
    const void *sendbuf,      // 发送缓冲区 (所有人都要填)
    void *recvbuf,            // 接收缓冲区 (仅 Root 有效,存最终结果)
    int count,                // 数据个数 (每个进程提交多少个数据)
    MPI_Datatype datatype,    // 数据类型
    MPI_Op op,                // 归约操作符 (告诉 MPI 怎么算)
    int root,                 // 根进程 Rank
    MPI_Comm comm             // 通信域
);

op (操作符):这是 Reduce 的灵魂。MPI 预定义了很多常用操作:

  • MPI_SUM: 求和 (Sum) —— 最常用
  • MPI_MAX: 求最大值 (Maximum)
  • MPI_MIN: 求最小值 (Minimum)
  • MPI_PROD: 求乘积 (Product)
  • MPI_LAND: 逻辑与 (Logical AND)
  • MPI_LOR: 逻辑或 (Logical OR)

在这里插入图片描述

通过积分法求 π \pi π的值

#include <mpi.h>
#include <stdio.h>
#include <math.h>

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);

    long n = 1000000; // 总共切分成 100万 个小矩形
    double h = 1.0 / (double)n; // 每个矩形的宽
    double sum = 0.0;

    // 1. 每个进程计算属于自己那部分矩形的面积
    // 步长为 size,即 Rank 0 算第 0, 4, 8... 个矩形
    for (long i = rank + 1; i <= n; i += size) {
        double x = h * ((double)i - 0.5);
        sum += 4.0 / (1.0 + x * x);
    }
    double my_pi = h * sum; // 这是当前进程算出来的“局部 pi”

    printf("Rank %d calculated partial pi: %f\n", rank, my_pi);

    // 2. 使用 Reduce 汇总结果
    double pi = 0.0;
    // sendbuf: &my_pi (每个人的局部结果)
    // recvbuf: &pi    (Root 用来存总结果)
    // op:      MPI_SUM (求和)
    MPI_Reduce(&my_pi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);

    // 3. 只有 Root 打印最终结果
    if (rank == 0) {
        printf("Final Pi is approx: %.16f\n", pi);
    }

    MPI_Finalize();
    return 0;
}

2.2 Allreduce

和Reduce类似但是没有root参数,所有进程都要获得reduce后的结果
在这里插入图片描述

2.3 Reduce_scatter

和allReduce的区别是Reduce_scatter每个进程只拿到一部分规约的结果,而allreduce是拿到全部的结果。

2.4 Scan

MPI_Scan (前缀扫描 / 前缀和) 是 MPI 中一种非常特殊的组通信操作,它执行的是前缀归约 (Prefix Reduction) 计算。

对于通信域中的每一个进程 i(Rank 0 到 Rank n-1),MPI_Scan 会计算从 Rank 0 到 Rank i 所有进程输入数据的归约结果。

数学公式:假设每个进程 i 的输入数据是 A i A_i Ai,操作符是 + + + (SUM)。
Rank 0 的结果: A 0 A_0 A0
Rank 1 的结果: A 0 + A 1 A_0 + A_1 A0+A1
Rank 2 的结果: A 0 + A 1 + A 2 A_0 + A_1 + A_2 A0+A1+A2

Rank i 的结果: ∑ k = 0 i A k \sum_{k=0}^{i} A_k k=0iAk

int MPI_Scan(
    const void *sendbuf,    // 发送缓冲区 (自己的数据)
    void *recvbuf,          // 接收缓冲区 (存累计结果)
    int count,              // 数据个数
    MPI_Datatype datatype,  // 数据类型
    MPI_Op op,              // 操作符 (MPI_SUM, MPI_MAX 等)
    MPI_Comm comm           // 通信域
);

计算前缀和

#include <mpi.h>
#include <stdio.h>

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);

    // 1. 每个进程准备自己的数据 (这里假设是 rank + 1)
    // Rank 0: 1, Rank 1: 2, Rank 2: 3 ...
    int my_val = rank + 1;
    int prefix_sum = 0;

    // 2. 执行 Scan
    // 计算前缀和:Rank i 将得到 1+2+...+ (i+1) 的结果
    MPI_Scan(&my_val, &prefix_sum, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);

    printf("Rank %d: Input = %d, Prefix Sum = %d\n", rank, my_val, prefix_sum);

    MPI_Finalize();
    return 0;
}

在这里插入图片描述

2.5 gather

MPI_GatherMPI_Scatter逆操作,相当于通信域内所有进程(包括 Root 自己)手中的数据,收集到根进程 (Root) 的一个大缓冲区中。

int MPI_Gather(
    const void *sendbuf, int sendcount, MPI_Datatype sendtype, // 发送方参数 (所有人都要填)
    void *recvbuf, int recvcount, MPI_Datatype recvtype,       // 接收方参数 (仅 Root 有效)
    int root, MPI_Comm comm                                    // 控制参数
);
  • sendbuf: 每个人(包括 Root)都要提供自己那份要上交的数据。
  • recvbuf: 只有 Root 需要申请这块大内存(大小 = 进程数 × 每个人的数据量)。其他进程填 NULL 即可。
  • recvcount (接收个数) —— ⚠️ 高危!:这里填的含义是:“我准备从每一个进程接收多少个数据?”

这份代码是每个进程向root进程传递自己的数据rank * 10,root进程收集这些数据然后打印

#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>

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);

    // 1. 每个人准备自己的数据
    int my_data = rank * 10;
    printf("[Rank %d] My data is %d\n", rank, my_data);

    // 2. 只有 Root 准备接收的大盘子
    int *global_data = NULL;
    if (rank == 0) {
        // 假设有 4 个进程,这里就要申请 4 个 int 的空间
        global_data = (int*)malloc(size * sizeof(int));
    }

    // 3. 执行 Gather
    // 每个人上交 1 个 int,Root 从每个人那里收 1 个 int
    MPI_Gather(&my_data, 1, MPI_INT,          // 发送参数
               global_data, 1, MPI_INT,       // 接收参数 (注意 recvcount 是 1)
               0, MPI_COMM_WORLD);

    // 4. Root 打印结果
    if (rank == 0) {
        printf("[Root] 收集完毕: ");
        for (int i = 0; i < size; i++) {
            printf("%d ", global_data[i]);
        }
        printf("\n");
        free(global_data); // 别忘了释放内存
    }

    MPI_Finalize();
    return 0;
}

2.6 gatherv

MPI_Gatherv 是 MPI_Gather 的升级版(v 代表 Vector,向量/变长)。

它是 MPI_Scatterv 的逆操作。

int MPI_Gatherv(
    const void *sendbuf, int sendcount, MPI_Datatype sendtype, // 发送端 (所有人)
    
    void *recvbuf,                                             // 接收端大缓冲区 (仅 Root)
    const int *recvcounts,                                     // [数组] 每个人分别交多少个? (仅 Root)
    const int *displs,                                         // [数组] 每个人交的数据放哪? (仅 Root)
    MPI_Datatype recvtype,                                     // 接收端类型 (仅 Root)
    
    int root, MPI_Comm comm                                    // 控制参数
);

3、 多到多

MPI_Alltoall它是“每一个进程都向每一个进程执行一次 Scatter

类似矩阵转置。

在这里插入图片描述

4、 同步

MPI_Barrier不传输任何数据,仅仅是让运行的快的进程停下来等待运行的慢的进程。

int MPI_Barrier(MPI_Comm comm);

在这里插入图片描述

#include <mpi.h>
#include <stdio.h>
#include <unistd.h> // 用于 sleep

int main(int argc, char** argv) {
    MPI_Init(&argc, &argv);

    int rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);

    // 1. 模拟不同步的工作负载
    if (rank == 0) {
        printf("Rank 0: 我跑得飞快!\n");
    } else {
        sleep(2); // 模拟 Rank 1 偷懒睡了 2 秒
        printf("Rank %d: 我睡醒了,才赶过来...\n", rank);
    }

    // 2. 设立栅栏
    // Rank 0 会在这里卡住约 2 秒,等待其他进程
    MPI_Barrier(MPI_COMM_WORLD);

    // 3. 这里的代码只有等所有人到齐后才会执行
    printf("Rank %d: 大家都到齐了,一起出发!\n", rank);

    MPI_Finalize();
    return 0;
}

输出结果:

Rank 0: 我跑得飞快!
(这里会停顿 2 秒...)
Rank 1: 我睡醒了,才赶过来...
Rank 0: 大家都到齐了,一起出发!
Rank 1: 大家都到齐了,一起出发!

四、 阻塞通信模式

1. 标准通信模式

标准通信模式这里特指的是MPI_Send,从底层机制上看包括缓存和不缓存两种机制,是否缓存是有MPI决定的,而不是由程序员决定,,这通常是取决于通信传递内容的大小,如果内容较大则不采用缓存,如果内存较小则不采用缓存模式。
在这里插入图片描述

cycle waiting循环等待死锁

MPI_Comm_dup (MPI_COMM_WORLD, &comm);  
if(myid==0) 
{  
	MPI_Recv(bufA0,1,MPI_Float,1,101,comm,status);
	MPI_Send(bufB0,1,MPI_Float,1,100,comm);
} else if(myid==1)
{  
	MPI_Recv(bufA1,1,MPI_Float,0,100,comm,status);
	MPI_Send(bufB1,1,MPI_Float,0,101,comm);
}

在这里插入图片描述

避免死锁就是避免循环等待
在这里插入图片描述

if ( myid==0) 
{  
	MPI_Recv(bufA0,1,MPI_Float,1,101, comm, status); 						
	MPI_Send(bufB0,1,MPI_Float,1,100, comm);
} else if(myid==1) {  
	MPI_Send(bufB1,1,MPI_Float,0,101, comm);
	MPI_Recv(bufA1,1,MPI_Float,0,100, comm, status);
}  ......

2. 缓存通信模式

这里的缓存通信模式是指的显示的缓存。

MPI_Bsend,用户手动对通信缓存区进行申请、使用和释放,和MPI_Send的区别就是使用的是系统分配的缓存区,还是用户申请的缓存区。

在这里插入图片描述
MPI_BUFFER_ATTACH:将自己申请的内存交给MPI使之成为缓存区。

MPI_BUFFER_DETACH:回收MPI中的缓存区,这一调用是阻塞调用,他一直阻塞到缓存被发送完才返回,返回后用户可以选择释放缓存区,或者继续使用缓存区。

3. 同步通信模式

MPI_Ssend
当发送方调用 MPI_Ssend 时,它不仅要把数据发出去,还要一直等待,直到接收方调用了匹配的 MPI_Recv 并且开始接收数据了,发送方才会从函数返回。

同步点:发送和接收在时间上通过这个操作“同步”了。发送方结束 Ssend 时,它不仅知道数据发出去了,还确切地知道接收方已经准备接收了。

在这里插入图片描述

4. 就绪通信模式

这里要求在调用MPI_Rsend时,接收程序已经调用MPI_Recv了。

只有当接收进程的接收操作已经启动时,才可以在发送 进程启动发送操作,否则,当发送操作启动而相应的接 收还没有启动时,发送操作将出错。

在这里插入图片描述

就绪通信模式很容易导致死锁,下面是一种安全的就绪通信模式,这里是一方采用非阻塞的Recv,就比如进程1调用MPI_IRECV后接着执行下面的MPI_SEND,而不是等待对方进程的MPI_RSEND,这里就不会导致死锁,如果双方都调用MPI_RECV就会导致死锁。

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值