MPI
之前的OpenMP和Pthread是在一台机器上,多核 —— 共享内存多处理器体系结构
MPI 是针对多机器/节点构成的局域网 ——分布式内存体系结构
本章主要讲如何使用消息传递对分布式内存系统编程
MPI概念和基本原语#
MPI是一个消息传递接口,定义了一个可以被C++、Fortran调用的函数库。
其特性:
- 所有通信、同步都需调用函数完成。无共享变量
- 提供以下函数:
- 通信
- 点对点
- 组通信
- 同步
- barrier
- 无锁机制,因为没有共享变量
- 查询
- 通信
comm通信域
例子:Hello#
#include <mpi.h>
#include <string.h>
#include <stdio.h>
const int MAX_LEN = 100;
int main(int argc, char *argv[]){
int myid,numprocs;
int namelen;
char processor_name[MAX_LEN];
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
MPI_Get_processor_name(processor_name,&namelen);//获取处理器名
printf("Hello world from processor %d of %d on %s\n",myid,numprocs,processor_name);
MPI_Finalize();
return 0;
}
#include "mpi.h"
#include <iostream>
int main(int argc, char *argv[]) {
int rank, size;
MPI::Init(argc, argv);
rank = MPI::COMM_WORLD.Get_rank();
size = MPI::COMM_WORLD.Get_size();
std::cout << "Greetings from process " << rank << "of"<< size << "\n";
MPI::Finalize();
return 0;
}
原语:
MPI_Init()
初始化int MPI_Comm_size(MPI_Comm comm, int *size)
进程数int MPI_Comm_rank(MPI_Comm comm, int *rank)
进程id,从0开始MPI_Finalize()
结束
上面四个和send,rev很重要
编译命令:mpicc -g -Wall -o mpi_hello mpi_hello.c
运行:
mpiexec -n <number of processes> <executable>
启动number of processes个可执行文件名
通信#
基本(阻塞)发送#
int MPI_Send(void* buf , int count, MPI_Datatype datatype ,
int dest , int tag, MPI_Comm comm)
消息缓冲区(buf, count, datatype)
目的进程dest—目的进程在comm指定的通信域中的编号
tag用于区分前四个参数一样的消息
comm通信域(一组可以互相发消息的进程集合),指定通信范围。
MPI_COMM_WORLD
:默认通信域,其进程组包含所有初始进程
阻塞发送——函数返回时,数据已经转给系统进行发送,缓冲区可作他用;消息可能还未送达目的进程
基本(阻塞)接受#
int MPI_Recv(void* buf, int count, MPI_Datatype datatype,
int source, int tag, MPI_Comm comm, MPI_Status *status)
阻塞接收——等待,直至收到匹配的(source和tag都相同)的消息,缓冲可作他用
source可以是comm中的编号,或MPI_ANY_SOURCE
tag为特定标签(需匹配)或MPI_ANY_TAG
接收的数据量必须<=指定的
status包含更多信息(如接收到的消息大小
MPI是强数据类型传输,支持自定义数据类型
MPI预定义数据类型与C数据类型的对应关系:
MPI预定义数据类型 | 相应的C数据类型 |
---|---|
MPI_CHAR | signed char |
MPI_SHORT | signed short int |
MPI_INT | signed int |
MPI_LONG | signed long int |
MPI_UNSIGNED_CHAR | unsigned char |
MPI_UNSIGNED_SHORT | unsigned short int |
MPI_UNSIGNED | unsigned int |
MPI_UNSIGNED_LONG | unsigned long int |
MPI_FLOAT | float |
MPI_DOUBLE | double |
MPI_LONG_DOUBLE | long double |
MPI_BYTE | 无对应类型 |
MPI_PACKED | 无对应类型 |
简单通信进程#
#include <math.h>
#include <mpi.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
int myid, namelen;
char message[20];
MPI_Status status;//
char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Init(&argc, &argv);//
MPI_Comm_rank(MPI_COMM_WORLD, &myid);//
MPI_Get_processor_name(processor_name, &namelen);//
if (myid == 0) {
strcpy(message, " Hello process 1");
printf("process 0 on %s send: %s\n", processor_name, message);
MPI_Send(message, 20, MPI_CHAR, 1, 99, MPI_COMM_WORLD);//
} else if (myid == 1) {
MPI_Recv(message, 20, MPI_CHAR, 0, 99, MPI_COMM_WORLD, &status);//
printf("process 1 on %s received: %s\n", processor_name, message);
}
MPI_Finalize();
return 0;
}
#include <mpi.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
int rank, size, i, buf[1];
MPI_Status status;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
if (rank == 0) {
for (i = 0; i < (size - 1); i++) {
MPI_Recv(buf, 1, MPI_INT, MPI_ANY_SOURCE,//
MPI_ANY_TAG, MPI_COMM_WORLD, &status);// 从任意进程接收消息
printf("Msg=%d from %d with tag %d\n",buf[0], status.MPI_SOURCE, status.MPI_TAG);//
}
} else {
buf[0] = rank;
MPI_Send(buf, 1, MPI_INT, 0, 100 - rank, MPI_COMM_WORLD);
}
MPI_Finalize();
return 0;
}
采用四核的输出:
Msg=1 from 1 with tag 99
Msg=3 from 3 with tag 97
Msg=2 from 2 with tag 98
并行排序#
只是思路代码,不完整
# include <mpi.h>
# include <stdio.h>
int main(int argc,char **argv){
int rank,a[1000],b[500];
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
if(rank==0) {
MPI_Send(&a[500],500,MPI_INT,1,0,MPI_COMM_WORLD);//将 a[500] 的后半部分发送给进程1进行排序
sort(a,500);//排a前半部分
MPI_Recv(b,500,MPI_INT,1,0,MPI_COMM_WORLD,MPI_STATUS_IGNORE);//接受从b来的代码
//然后应该合并
}else if(rank==1) {
MPI_Recv(b,500,MPI_INT,0,0,MPI_COMM_WORLD,MPI_STATUS_IGNORE);//b接受代码
sort(b,500);//b排序
MPI_Send(b,500,MPI_INT,0,0,MPI_COMM_WORLD);//b发送代码
}
MPI_Fanilize();
return 0;
}
MPI编程模型#
消息传递两种编程模型:
- 对等式
- 主从式
Jacobi迭代
B[i][j]=0.25*(A[i][j]上下左右的和)
对等
主从
略了,感觉不考
组通信#
一个进程组(通信域)内的所有进程同时参加通信
- 所有参与进程的函数调用形式完全相同
- 哪些进程参加以及组通信的上下文都是由调用时指定的通信域限定的
- 在组通信中不需要通信消息标志tag参数
广播和归约#
广播是指一个进程将数据发送到其他所有进程,所有参与通信的进程都会接收到该数据。广播操作一般用于一个进程(根进程)向所有其他进程分发数据
归约操作是将多个进程的数据根据某种操作(如求和、求最大值、求最小值等)进行合并,并将结果返回给一个进程(通常是根进程)。
one to all broadcast | all to one reduction |
---|---|
一个进程向其他所有进程发送相同数据 | |
初始,只有源进程有一份 m 个字的数据 广播操作后,所有进程都有一份相同数据 |
初始,每个进程都有一份 m 个字的数据 归约操作后, p 份数据经过计算(加、乘、 得到一份数据(结果),传送到目的进程 |
int MPI_Bcast(void* buffer,int count,MPI_Datatype datatype,
int root, MPI_Comm comm)
- root: 每个调用广播操作的进程中root都相同
- count:待广播的数据的个数
int MPI_Reduce (void* sendbuf , void* recvbuf , int count,MPI_Datatype datatype,
MPI_Op op, int root, MPI_Comm comm)
散发和聚集#
散发(Scatter) 操作是将一个进程的数据分发给所有其他进程。具体来说,根进程(通常是 rank == 0
)拥有一个大的数据块,它将数据分割成多个小块并将每个小块发送给不同的进程。
是 one to all 个体化通信
MPI_Scatter(void *sendbuf,int count,MPI_Datatype MPI_INT,
void *recvbuf,int count,MPI_Datatype MPI_INT, int root, MPI_Comm comm;
sendbuf
: 根进程持有的发送数据缓冲区。count
: 每个进程接收的元素数量。
MPI_Scatterv(void *sendbuf,int count,int *displs,MPI_Datatype MPI_INT,
void *recvbuf,int count,MPI_Datatype MPI_INT, int root,MPI_Comm commD);
允许ROOT向各个进程发送个数不等的数据
(折半法,每次将自己的一半传给邻居)
聚集(Gather) 操作是将多个进程的数据收集到一个进程中,通常是根进程。每个进程将自己的局部数据发送到根进程,根进程将这些数据合并在一起。
MPI_Gather(void *sendbuf,int count,MPI_Datatype MPI_INT,
void *recvbuf,int count,MPI_Datatype MPI_INT, int root,MPI_Comm commD);
recvcount :从每个进程接收的数据个数,而不是一共需要接收的数据个数
MPI_Gatherv(void *sendbuf,int count,MPI_Datatype MPI_INT,
void *recvbuf,int count,int *displs,MPI_Datatype MPI_INT, int root,MPI_Comm commD);
可以从不同的进程接收不同数量的数据
recvcounts:整型数组(长度为组的大小),其值为从每个进程接收的数据个数
displs:整数数组,每个入口表示相对于recvbuf的位移
初始,每个节点保存一个消息
第一步:奇数节点→偶数节点,偶数节点将两个消息连接
第二步:不能被4整除的偶数节点→4倍数节点,消息连接
All to All 广播和归约#
All-to-All 操作是指每个进程与其他所有进程交换数据。换句话说,每个进程都能接收到来自所有其他进程的数据。
All-to-All 广播#
全局聚集 相当于组内每个进程都执行一次收集
int MPI_Allgather(void* sendbuf, int sendcount, MPI_Datatype sendtype,
void* recvbuf, int recvcount, MPI_Datatype recvtype,MPI_Comm comm)
All-to-All归约#
=归约并散发,上面右图每个点都有
int MPI_Reduce_scatter(void* sendbuf,void* recvbuf, int *recvcount, MPI_Datatype sendtype,
MPI_Op op,MPI_Comm comm)
全归约#
int MPI_Allreduce(void *sendbuf,void* recvbuf,int count,MPI_Datatype datatype,MPI_Op op,MPI_Comm comm)
扫描#
int MPI_Scan(void *sendbuf,void* recvbuf,int count,MPI_Datatype datatype,MPI_Op op,MPI_Comm comm)
All to All 个体化通信#
每个节点都向其它每个节点发送一个不同的消息( all to all 广播是发送相同消息)
应用:矩阵转置
int MPI_Alltoall()
Barrier#
路障,只有当所有语句都执行了该调用后才一起向下执行
int MPI_Barrier(MPI_Comm comm)
非阻塞通信#
int MPI_Wait()//单一非阻塞通信
int MPI_Waitany()//多个,任何一个完成就返回
int MPI_Waitall() //所有完成即返
itn MPI_Waitsome() //不为0的对象完成即返
检测非阻塞通信
int MPI_Test()
多线程混合编程#
MPI+Pthread/OpenMP
MPI_Init_thread
与基础的 MPI_Init
不同,MPI_Init_thread
允许程序明确指定和查询所需的线程支持级别。
int MPI_Init_thread(int *argc, char ***argv, int required, int *provided);
required
- 指定所需的线程支持级别,常用的选项包含以下 4 个
provided
- 用于返回 MPI 实际支持的线程级别。
四种线程安全级别#
MPI_THREAD_SINGLE//只有一个线程
MPI_THREAD_FUNNELED//多线程,只有主线程会进行MPI调用(调用MPI_Init_thread的那个线程)
MPI_THREAD_SERIALIZED//多线程,同时只有一个线程会进行MPI调用
MPI_THREAD_MULTIPLE//多线程,任何线程任何时候都会进行MPI调用,有一些避免竞争条件的限制
安全级别递增
即一个完全线程安全的会实现MPI_THREAD_MULTIPLE
调用 MPI_Init()
的程序假定只支持 MPI_THREAD_SINGLE
**`
作者:AuroraKelsey
出处:https://www.cnblogs.com/AuroraKelsey/p/18669405
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了