MPI是什么
Message Passing Interface 是一种消息传递编程模型,是这种模型的代表和事实上的标准,用于编写并行程序。主要思想是将一个程序分解为多个进程,这些进程相互通信并协作完成任务。MPI可以在多台计算机或者多个计算节点上执行,还可以利用不同的通信机制进行进程间的通信。
由2022年图灵将获得者----Jack j.dongarra发起。
是一种新的库描述,不是一种语言,共有上百个函数调用接口,提供C和Fortran语言;
MPI是一种标准,规范的代表,而不是具体实现,当前所有的并行计算机制造商都提供对MPI的支持:
- Intel MPI
- Open MPI
- mpich
MPI应用场景
主要应用于高性能计算和分布式计算领域,如
- 科学计算,如天体物理学,量子化学,材料科学,加速复杂的计算和模拟;
- 海量数据处理,并行加速处理大规模数据集的过程,如图像处理,信号处理,机器学习和深度学习;
- 工程模拟,如用于工程领域的模拟和优化,例如计算流体力学,有限元分析,结构学力学等,用来加速数值计算和仿真。
Linux环境准备
我的是ubuntu20.04,以下是OpenMPI环境安装过程:
sudo apt-get install openmpi-bin libopenmpi-dev
编写Hello World程序
#include "mpi.h"
#include <iostream>
int main(int argc, char *argv[])
{
int err = MPI_Init(&argc,&argv);
std::cout << "MPI Hello World" << std::endl;
err = MPI_Finalize();
return 0;
}
编译程序,需要使用MPI自带的编译器,而不是GCC/G++,使用起来和GCC/G++差不多,底层调用的还是这些编译器。
mpic++ main.cpp -o a.out
运行程序,需要依赖可执行程序mpirun
mpirun -np 2 a.out
其中 -np 指定启动进程(一般为后台进程)的个数,该数字和你机器配置相关,建议小于CPU数量或物理核心数。
这里将打印出2个 "MPI Hello World"
MPI常用接口
- 消息传递接口, 如MPI_Send 和 MPI_Receive 等,用于实现进程之间的点对点通;
- 阻塞与非阻塞接口,上述MPI_Send 和 MPI_Receive是阻塞接口,MPI提供了MPI_Isend 和 MPI_Irecv 等接口,提供非阻塞的通信方式;
- 广播和全局操作接口:MPI_Bcast 和 MPI_Scatter 接口用于广播和分发数据,而 MPI_Reduce 和 MPI_Allreduce 接口则用于执行聚合操作;
- 数据类型接口,提供了一些用于定义各种数据类型的接口,如 MPI_Datatype 和 MPI_Type_contiguous,用于支持更复杂的数据通信和操作;
- 包括上述代码示例中,用于进程管理和性能调试的接口,如 MPI_Init 环境初始化 和 MPI_Finalize 环境结束等。
- MPI_Comm_rank,获取当前进程在指定通信域中的进程标识符;
int rank;
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
- MPI_Comm_size:获取指定通信域(包括 MPI_COMM_WORLD 和自定义的通信域)中进程的总数;
int size;
MPI_Comm_size(MPI_COMM_WORLD, &size);
- MPI_Comm_split:根据指定的颜色信息,将指定通信域中的进程分隔成多个子通信域,并返回分隔后的新通信域;
- MPI_Comm_dup:复制指定通信域,返回一个新的通信域;
- MPI_Comm_free:释放指定通信域的内存资源,并将通信域设为 MPI_COMM_NULL
- MPI_Comm_create:通过指定的进程标识符和通信域,创建一个新的通信域
- MPI_Comm_spawn:在指定的通信域中新建一个或多个进程,并返回每个新进程的通信域
- MPI_Intercomm_create:在两个指定的通信域之间创建一个新的“互联通信域”,用于实现不同通信域之间的通信。
通信域:
MPI 中一组可能需要进行通信的进程的逻辑结构。通信域中的每个进程都有一个唯一的标识符(通信域内部的秩/等级(rank)),并可以通过指定目标进程的标识符来对目标进程进行通信。不同通信域的进程不能直接进程通信,但是,可以通过MPI_Intercomm_create将两个不通的域合并为一个互联的通信域,同时返回在每个通信域中所代表的所有进程的通信域标识符和秩。
MPI定义了几个默认的通信域:
- MPI_COMM_WORLD 多进程所有进程,缺省的通信域名,可以通过
MPI_Comm_rank
获取进程在这个通信域中的rank值; - MPI_COMM_SELF 单进程的通信域,应用于如只与自身通信的特殊作用域;
举个例子
#include "mpi.h"
#include <unistd.h>
#include <iostream>
int main(int argc, char *argv[])
{
int err = MPI_Init(&argc,&argv);
int rank,size;
//获取当前进程在通信域中的rank值
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
//获取当前通信域的进程总数
MPI_Comm_size(MPI_COMM_WORLD, &size);
std::cout << "Hello World from process " << rank << " of " << size << std::endl;
//环境清理,必须加,否则MPI程序不结束
err = MPI_Finalize();
return 0;
}
编译执行:mpirun -np 2 a.out
Hello World from process 1 of 2
Hello World from process 0 of 2
MPI的并行模式
主从模式
拥有一个主进程,主进程作为任务的分配方,从进程执行任务,主进程负责管理数据的分发和接受;
SPMD
Single Program Multiple Data 该模式下,多个进程运行同一个程序,但每个进程处理不同的数据。这种模式是最常见和最基础的并行模式,MPI_COMM_WORLD 通信域通常被用来实现这种模式
MPMD
Multiple Program Multiple Data 该模式下,多个进程在不同的程序中运行,每个进程独立地执行不同的任务。这种模式适用于需要针对特定问题进行优化的情况,MPI_Comm_spawn 通信接口可以用来生成子进程
数据并行
数据被分割为多个子数据块,每个进程处理其中的一部分数据块,进程之间通过数据传输进行协作。数据并行是实现大规模并行化的经典模式,MPI_Send 和 MPI_Recv 等接口可用于实现数据传输
任务并行
各进程运行相同的程序,但处理不同的任务。进程协作完成整个任务,在任务的不同阶段,每个进程执行不同的功能。任务并行又可以细分为 pipeline 并行、模块并行、工作站并行等模式。
流水线并行
任务被划分为多个阶段,在每个阶段中,数据沿着流水线被处理,不同的进程专门处理不同的阶段。这种模式适用于有序处理数据的场景,可以提高并行程序的效率。
模块并行
该模式将一个程序分成若干个子任务,每个子任务由不同的进程执行。不同的子任务之间具有依赖关系,需要通过数据传输和同步操作来完成计算。
非同步并行
进程之间没有明确的同步,机制,各自执行独立的子任务,进程之间通过消息传递进行通信。适用于简单问题和小规模的并行计算。
MPI通信
点对点通信
用于一个进程和一个接收进程之间的数据交互,有分为阻塞和非阻塞通信模式。
阻塞接口
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)
非阻塞接口
int MPI_Isend(void *buf, int count, MPI_Datatype datatype, int dest, int tag,
MPI_Comm comm, MPI_Request *request)
int MPI_Irecv(void *buf, int count, MPI_Datatype datatype, int source, int tag,
MPI_Comm comm, MPI_Request *request)
判断通信是否完成:
MPI_Wait 函数的作用是等待一个非阻塞式 MPI 通信操作(如 MPI_Isend 和 MPI_Irecv)完成,直到其请求对象进入完成状态为止。如果请求对象尚未完成,则 MPI_Wait 函数会阻塞当前进程的执行,直到请求对象的状态为完成状态才返回。MPI_Wait 函数的用法如下:
int MPI_Wait(MPI_Request *request, MPI_Status *status)
MPI_Test 函数的作用与 MPI_Wait 函数类似,都是等待一个请求对象进入完成状态,但 MPI_Test 函数是非阻塞的,即当请求对象尚未完成时,MPI_Test 函数会立即返回一个标志值,而不会阻塞当前进程的执行。MPI_Test 函数的用法如下:
int MPI_Test(MPI_Request *request, int *flag, MPI_Status *status)
消息类型
- 基本数据类型(Primitive data types):包括 MPI_CHAR、MPI_SHORT、MPI_INT、MPI_LONG、MPI_UNSIGNED_CHAR、MPI_UNSIGNED_SHORT、MPI_UNSIGNED、MPI_UNSIGNED_LONG、MPI_FLOAT、MPI_DOUBLE等,可以用于传输常见的数据类型和数值类型;
- 复杂数据类型(Derived data types):包括 MPI_DATATYPE、MPI_ARRAY、MPI_STRUCT 等,可以用于传输由多个变量组成的结构化数据类型
- 特殊数据类型:包括 MPI_PACKED 等,可用于封装任意类型的数据
- 自定义数据类型:MPI 还提供了自定义数据类型的支持,用户可以根据实际需要定义自己的数据类型,并将其注册到 MPI 环境中,以供后续使用
阻塞通信代码示例
//0号进程给1号进程发送数据
#include "mpi.h"
#include <unistd.h>
#include <iostream>
int main(int argc, char *argv[])
{
int err = MPI_Init(&argc,&argv);
int rank,size;
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
if(0 == rank)
{
int sendNum = 99;
int tag = 0;
MPI_Send(&sendNum, 1 ,MPI_INT ,1 , tag , MPI_COMM_WORLD);
}
else
{
int recvNum = 0;
int tag = 0;
MPI_Status status;
std::cout << "before recv , recv num = " << recvNum << std::endl;
MPI_Recv(&recvNum, 1 ,MPI_INT , 0, tag, MPI_COMM_WORLD, &status);
std::cout << "before recv , recv num = " << recvNum << std::endl;
}
err = MPI_Finalize();
return 0;
}
非阻塞通信代码示例
int main(int argc, char *argv[])
{
int err = MPI_Init(&argc,&argv);
int rank,size;
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
int data = 100;
MPI_Request request;
MPI_Status status;
if(rank > 0)
{
MPI_Irecv(&data,1,MPI_INT,rank-1,0,MPI_COMM_WORLD,&request);
std::cout << "rank = " << rank << " recived data is : " << data << std::endl;
MPI_Wait(&request, &status);
std::cout << "rank = " << rank << " recived data is : " << data << std::endl;
}
if(rank < size - 1)
{
data = rank;
MPI_Isend(&data,1,MPI_INT, (rank + 1)%size, 0 ,MPI_COMM_WORLD, &request);
MPI_Wait(&request, &status);
}
err = MPI_Finalize();
return 0;
}