MPI 环境搭建
下载并安装MPICH2
-
下载并打开“mpich2-1.4.1p1-win-ia64.msi”。
-
安装64位的MPICH2
如果安装的时候,给出如下的错误提示,没有启动.NET 服务
这是需要在“WINDOWS”功能中勾选“.NET FRAMEWORK”的选项
然后就可以点击“下一步”安装
选择默认的安装路径,勾选“Everyone”
当出现防火墙提示时,专有网络和公用网络都要勾选
-
添加环境变量
在系统变量中的path下添加
C:\Program Files\MPICH2\bin
- 打开路径“C:\Program Files\MPICH2\bin”,打开“wmpiregister.exe”文件
- 输入Microsoft的用户名和密码(同锁屏时用户名和密码)
如果弹出防火墙警报,两个都要允许
-
出现提示“Password encypted into the Registry”,点击OK即可。
-
以管理员的身份运行cmd
-
输入命令
smpd -install -phrase behappy
(注意:这里的behappy需要与安装时的Passphrase一样), 如果出现提示“MPICH2 Process Manager, Argonne National Lab installed.”则说明安装成功。输入命令
smpd -status
查询进程状态输入命令
mpiexec -validate
检测是否注册成功,成功则会提示“SUCCESS”。
-
进入文件夹
C:\Program Files\MPICH2\examples
,运行…\bin\mpiexec.exe -n 2 cpi.exe
程序运行正常则说明MPICH2安装配置成功
- 打开路径
C:\Program Files\MPICH2\bin
下的“wmpiexec.exe”文件。
点右上角“三个点”,选择路径“C:\Program Files\MPICH2\examples”下的“cpi.exe”文件
勾选“run in an separate window”。设置进程数,这里设置了4。点击Execute。
运行结果如下:
在VS中配置MPI环境
- 打开VS,新建Visual C++里面的"Windows 控制台程序”
- 打开项目的属性
-
先将上面的“配置”改为“debug”,平台选择X64,然后点击左侧“C/C++”,我们要修改的是“附加包含目录,点击“<编辑…>”
-
点击“新建”图标,然后点击“三个点”。
-
选择路径“C:\Program Files (x86)\MPICH2\include”,点击“选择文件夹”。然后“确定”。
-
点击左侧“链接器”,这里我们要修改的是“附加库目录”。同上,点击“<编辑…>”,点击“新建”图标,然后点击“三个点”。
- 选择路径“C:\Program Files\MPICH2\lib”,点击“选择文件夹”。然后“确定”。
- 展开“链接器”,点击“输入”,在右侧第一项“附加依赖项”,前面加上“mpi.lib;”,注意不要忘记分号。
-
点击“应用”、“确定”完成配置。
-
写一个输出的程序,生成exe。
-
右键单击“解决方案”,点击“在文件资源管理器中打开文件夹(X)”
- 打开Debug文件夹,点击上方地址栏空白处,复制好路径。
-
打开cmd,输入
mpiexec -n 4 C:\Users\zyw\source\repos\testmpi\Debug\testmpi
,因为上一步复制过路径,所以该命令行中的路径处可以用粘贴方式输入。“-n 4”是参数,是指4个进程,后面就是路径+文件名 -
运行结果如下,一共输出四个Hello World
参考博客:
(16条消息) 【mpich2】图文教程:mpich2的安装、配置、测试、vs配置、命令行测试(没有使用)_苍狼的博客-CSDN博客_mpich2配置与测试
(16条消息) 如何在Window7系统中安装MPICH2_怡暘-CSDN博客
(16条消息) windows下安装mpich2_执剑者罗辑的博客-CSDN博客_mpich2
CentOS 安装OpenMPI3.1.0
- 下载OpenMPI 源码
wget https://download.open-mpi.org/release/open-mpi/v3.1/openmpi-3.1.0.tar.gz
- 解压缩OpenMPI源码
tar -zxvf openmpi-3.1.0.tar.gz
-
安装OpenMPI
openmpi-3.1.0/
./configure --prefix=/usr/local/openmpi
make && make install
-
配置环境变量
whereis openmpi
vim ~/.bash_profile
-
将以下两句添加到.bash_profile文件末尾位置,按Esc后:wq保存修改
export PATH=$PATH:/usr/local/openmpi/bin export LD_LIBRARY_PATH=/usr/local/openmpi/lib
-
使用source命令激活修改
source ~/.bash_profile
-
验证安装
cd examples/
make
./hello_c
华为上安装不了,没有root 权限
MPI 原理以及编程
MPI 简介
MPI的概念
MPI 的概念
Massage Passing Interface:是消息传递函数库的标准规范,由MPI论坛开发。
- 一种新的库描述,不是一种语言。共有上百个函数调用接口,提供与C和Fortran语言的绑定
- MPI是一种标准或规范的代表,而不是特指某一个对它的具体实现
- MPI是一种消息传递编程模型,并成为这种编程模型的代表和事实上的标准
MPI的特点
MPI有以下的特点:
- 消息传递式并行程序设计
指用户必须通过显式地发送和接收消息来实现处理机间的数据交换。
在这种并行编程中,每个并行进程均有自己独立的地址空间,相互之间访问不能直接进行,必须通过显式的消息传递来实现。
这种编程方式是大规模并行处理机(MPP)和机群(Cluster)采用的主要编程方式。 - 并行计算粒度大,特别适合于大规模可扩展并行算法
用户决定问题分解策略、进程间的数据交换策略,在挖掘潜在并行性方面更主动,并行计算粒度大,特别适合于大规模可扩展并行算法 - 消息传递是当前并行计算领域的一个非常重要的并行程序设计方式
MPI的 通信机制
MPI 点对点的通信类型
相关概念解释
- 通讯器(communicator)。通讯器定义了一组能够互相发消息的进程。在这组进程中,每个进程会被分配一个序号,称作秩(rank),进程间显性地通过指定秩来进行通信。
- 通信的基础建立在不同进程间发送和接收操作。一个进程可以通过指定另一个进程的秩以及一个独一无二的消息标签(tag)来发送消息给另一个进程。接受者可以发送一个接收特定标签标记的消息的请求(或者也可以完全不管标签,接收任何消息),然后依次处理接收到的数据。类似这样的涉及一个发送者以及一个接受者的通信被称作点对点(point-to-point)通信。
- 当然在很多情况下,某个进程可能需要跟所有其他进程通信。比如主进程想发一个广播给所有的从进程。在这种情况下,手动去写一个个进程点对点的信息传递就显得很笨拙。而且事实上这样会导致网络利用率低下。MPI 有专门的接口来帮我们处理这类所有进程间的集体性(collective)通信。
点对点的通信模型
MPI是一种基于消息传递的编程模型,不同进程间通过消息交换数据。
所谓点对点的通信就是一个进程跟另一个进程的通信,而下面的聚合通信就是一个进程和多个进程的通信
标准模式:
该模式下MPI有可能先缓冲该消息,也可能直接发送,可理解为直接送信或通过邮局送信。是最常用的发送方式。
由MPI决定是否缓冲消息
- 没有足够的系统缓冲区时或出于性能的考虑,MPI可能进行直接拷贝:仅当相应的接收完成后,发送语句才能返回。
- MPI缓冲消息:发送语句在相应的接收语句完成前返回。
缓冲模式
特征: 通过用户定义的缓冲区传送消息
通过用户指定的缓冲区传送消息,这里的缓冲区指的是应用缓冲区。需要用户程序事先申请一块足够大的缓冲区,用户定义的缓冲区只能用于缓存模式,一个进程一次只能绑定一块用户缓冲区,通过MPI_Buffer_attch
实现。
发送是本地的: 完成不依赖于与其匹配的接收操作。发送的结束仅表明消息进入用户指定的缓冲区中。
MPI_Buffer_detach
来回收申请的缓冲区,阻塞模式下该操作直到缓存区的数据传输完才返回。
缓冲模式在相匹配的接收未开始的情况下,总是将送出的消息放在缓冲区内,这样发送者可以很快地继续计算,然后由系统处理放在缓冲区中的消息。
占用额外内存,一次内存拷贝。
其函数调用形式为:MPI_BSEND(…)
。B代表缓冲.
同步模式
特征: 要先进行握手,同步发送与接收
本质特征:收方接收该消息的缓冲区已准备好,不需要附加的系统缓冲区
任意发出:发送请求可以不依赖于收方的匹配的接收请求而任意发出
成功结束:仅当收方已发出接收该消息的请求后才成功返回,否则将阻塞。
非本地的
可用于实现进程同步
函数调用:MPI_SSEND(...)
, S 代表同步
就绪模式
特征: 接收必须先于发送,即有请求才有发送
发送请求仅当有匹配的接收后才能发出,否则出错。在就绪模式下,系统默认与其相匹配的接收已经调用。接收必须先于发送
它依赖于接收方的匹配的接收请求,不可以任意发出。
其函数调用形式为:MPI_RSEND(…)
。R代表就绪。
正常情况下可用标准模式替换,除可能影响性能外,不影响结果。
- 点对点通信的阻塞性分析
上述的四种模式都有阻塞通信和非阻塞通信两个版本
阻塞通信
就是按照上面的流程进程自己要等待指定的操作实际完成,或者所涉及的数据被MPI系统安全备份后,函数才返回后才能进行下一步的操作。
非阻塞通信
通信函数总是立即返回,实际操作由MPI后台进行,需要调用其它函数来查询通信是否完成,通信与计算可重叠。
因为阻塞通信时保证了数据的安全性,所以通信还是返回后,其他的函数或者语句能够对这些数据资源直接访问。
但是非阻塞通信函数立即返回,不能保证数据已经被传送出去或者被备份或者已经读入使用,所以即使你没有阻塞,后面的语句可以继续执行,如果你要操纵上面所说的数据,将会导致发送或接收产生错误。所以对于非阻塞操作,要先调用等待MPI_Wait()
或测试MPI_Test()
函数来结束或判断该请求,然后再向缓冲区中写入新内容或读取新内容。
非阻塞函数调用
发送语句的前缀由MPI_改为MPI_I
, I指的是immediate,即可改为非阻塞,否则是阻塞。示例如下:
标准模式: MPI_Send(…)->MPI_Isend(…)
Buffer模式:MPI_Bsend(…)->MPI_Ibsend(…)
- 通信函数的返回和完成,通信的完成
通信函数的返回:
在阻塞状态下通信函数的返回是在通信即将完成或者完成之后(根据不同的模式有不同设置,如缓冲模式发送函数的返回是在缓冲区的数据传输完之后返回)。在非阻塞状态下,通信函数立即返回。
通信函数的完成:
通信的完成=发送函数的完成+接收函数的完成
发送的完成: 代表发送缓冲区中的数据已送出,发送缓冲区可以重用。它并不代表数据已被接收方接收。数据有可能被缓冲;
这里有一个特例,就是同步模式,它的发送完成==接收方已初始化接收,数据将被接收方接收。
接收的完成:代表数据已经写入接收缓冲区。接收者可访问接收缓冲区,status对象已被释放。它并不代表相应的发送操作已结束(即操作函数可返回)。
通信函数的返回和通信函数完成是不一样的。
通过MPI_Wait()
和MPI_Test()
来判断通信是否已经完成;
点对点通信的常用函数
-
MPI程序的初始化
任何一个MPI程序在调用MPI函数之前,首先调用的是初始化函数
MPI_INIT
建立MPI的执行环境
int MPI_Init(int *argc, char ***agrv)
-
获取通信域包含的进程数
MPI_Comm_Size(MPI_Comm,int *size)
-
获取当前进程标识
MPI_Comm_rank(MPI_Comm comm,int *rank)
-
消息的发送
源程序将缓存的数据发送到目的进程
阻塞发送:
MPI_Send(void* buf,int count,MPI_Datatype datatype, int dest,int tag,MPI_Comm comm)
非阻塞发送:
MPI_Isend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request)
非阻塞缓冲模式:MPI_Ibsend
非阻塞同步模式:MPI_Issend
非阻塞就绪模式:MPI_Irsend
- buf : 发送缓存的起始地址
- count: 发送的数据个数
- datatype: 数据类型
- des标识号
- Tag: 消息标志
- comm: 通信域
- request: 非阻塞通信完成对象(句柄)
-
消息的接收
目的进程从源进程接收数据,存入缓存
阻塞式接收MPI_Recv(void * buf,int count,MPI_Datatype datatype,int source,int tag,MPI_Comm comm,MPI_Status * status)
非阻塞式接收MPI_Irecv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request* request)
- buf: 接收缓存的起始地址
- count:最多可接受的数据个数
- datatype:数据类型
- source: 发送数据的进程标识号
- Tag: 消息标志
- comm: 通信域
- Status:返回状态
-
消息的探测
MPI_Probe()
和MPI_Iprobe()
函数探测接收消息的内容。用户根据探测到的消息内容决定如何接收这些消息,如根据消息大小分配缓冲区等。前者为阻塞方式,即只有探测到匹配的消息才返回;后者为非阻塞,即无论探测到与否均立即返回.
阻塞式消息探测:MPI_Probe(int source, int tag, MPI_Comm comm, MPI_Status* status)
非阻塞式消息探测:MPI_Iprobe(int source, int tag, MPI_Comm comm, int*flag, MPI_Status* status)
- source 数据源的rank,可以是MPI_ANY_SOURCE
- tag 数据标签,可以是MPI_ANY_TAG
- comm 通信空间/通信域
- flag 布尔值,表示探测到与否(只用于非阻塞方式)
- status status对象,包含探测到消息的内容
-
通信的检查
必须等待指定的通信请求完成后才能返回,成功返回时,status 中包含关于所完成的通信的消息,相应的通信请求被释放,即request 被置成MPI_REQUEST_NULL
MPI_Wait(MPI_Request* request, MPI_Status * status)
不论通信是否完成都立刻返回,flag为1表示通信完成
MPI_Test(MPI_Request *request,int *flag, MPI_Status *status)
其他通信检测函数
int MPI_Waitany(int count,MPI_Request *array_of_requests,int *index, MPI_Status *status)
int MPI_Waitall(int count,MPI_Request *array_of_requests,MPI_Status *array_of_statuses)
int MPI_Waitsome(int incount,MPI_Request *array_of_requests,int *outcount,
int *array_of_indices,MPI_Status *array_of_statuses)
int MPI_Testany(int count,MPI_Request *array_of_requests,int *index, int *flag,MPI_Status *status)
int MPI_Testall(int count,MPI_Request *array_of_requests,int *flag,MPI_Status *array_of_statuses)
int MPI_Testsome(int incount,MPI_Request *array_of_requests,int *outcount,
int *array_of_indices,MPI_Status *array_of_statuses)
-
请求的释放和撤销
MPI_Request_free(MPI_Request request)
释放指定的通信请求(及所占用的内存资源)
若该通信请求相关联的通信操作尚未完成,则等待通信的完成,因此通信请求的释放并不影响该通信的完成
该函数成功返回后request 被置为MPI_REQUEST_NULL
一旦执行了释放操作,该通信请求就无法再通过其它任何的调用访问
MPI_Cancel(MPI_Request request)
非阻塞型,用于取消一个尚未完成的通信请求
在MPI系统中设置一个取消该通信请求的标志后立即返回,具体的取消操作由MPI系统在后台完成。
MPI_CANCEL允许取消已调用的通信请求,但并不意味着相应的通信一定会被取消:若相应的通信请求已经开始,则它会正常完成,不受取消操作的影响;若相应的通信请求还没开始,则可以释放通信占用的资源。
调用MPI_CANCEL 后,仍需用MPI_WAIT,MPI_TEST或MPI_REQUEST_FREE 来释放该通信请求。
MPI_Test_cancelled(MPI_Status status,int *flag)
检测通信请求是否被取消 -
MPI 程序结束
MPI_Finalize()
:结束MPI程序的执行
MPI 预定义的数据类型
MPI聚合通信
MPI 集体通信(collective communication)指的是一个涉及 communicator 里面所有进程的一个方法。这节课我们会解释集体通信以及一个标准的方法
MPI 根据参与集合通信双方的进程数目,可大致分为三类
- 一对多:
Bcast
,Scatter
,Scatterv
- 多对一:
Gather
,Gatherv
,Reduce
- 多对多:
Allgather
,Allgatherv
,Allreduce
,Reduce_scatter
,Alltoall
,Alltoallv
,Alltoallw
,Exscan
此外,集合通信还包括一个同步操作Barrier, 同步栅格,即所有的进程到达后才可以继续执行。
-
broadcasting (广播)。
集体通信需要记住的一点是它在进程间引入了同步点的概念。这意味着所有的进程在执行代码的时候必须首先都到达一个同步点才能继续执行后面的代码。 -
MPI_Barrier
MPI 使用MPI_Barrier(MPI_Comm communicator)
来进行同步。
如上图,这个方法会构建一个屏障,任何进程都没法跨越屏障,直到所有的进程都到达屏障。这边有一个示意图。假设水平的轴代表的是程序的执行,小圆圈代表不同的进程。进程0在时间点 (T 1) 首先调用 MPI_Barrier。然后进程0就一直等在屏障之前,之后进程1和进程3在 (T 2) 时间点到达屏障。当进程2最终在时间点 (T 3) 到达屏障的时候,其他的进程就可以在 (T 4) 时间点再次开始运行。
MPI_Barrier 在很多时候很有用。其中一个用途是用来同步一个程序,使得分布式代码中的某一部分可以被精确的计时。 -
MPI_Bcast
广播 (broadcast) 是标准的集体通信技术之一。一个广播发生的时候,一个进程会把同样一份数据传递给一个 communicator 里的所有其他进程。广播的主要用途之一是把用户输入传递给一个分布式程序,或者把一些配置参数传递给所有的进程。
如上图,进程0是我们的根进程,它持有一开始的数据。其他所有的进程都会从它这里接受到一份数据的副本。
MPI_Bcast
函数
MPI_Bcast(
void* data,
int count,
MPI_Datatype datatype,
int root,
MPI_Comm communicator)
尽管根节点和接收节点做不同的事情,它们都是调用同样的这个 MPI_Bcast 函数来实现广播。当根节点(在我们的例子是节点0)调用 MPI_Bcast 函数的时候,data 变量里的值会被发送到其他的节点上。当其他的节点调用 MPI_Bcast 的时候,data 变量会被赋值成从根节点接受到的数据。
#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
void my_bcast(void* data, int count, MPI_Datatype datatype, int root,
MPI_Comm communicator) {
int world_rank;
MPI_Comm_rank(communicator, &world_rank);
int world_size;
MPI_Comm_size(communicator, &world_size);
if (world_rank == root) {
// If we are the root process, send our data to everyone
int i;
for (i = 0; i < world_size; i++) {
if (i != world_rank) {
MPI_Send(data, count, datatype, i, 0, communicator);
}
}
}
else {
// If we are a receiver process, receive the data from the root
MPI_Recv(data, count, datatype, root, 0, communicator, MPI_STATUS_IGNORE);
}
}
int main(int argc, char** argv) {
MPI_Init(NULL, NULL);
int world_rank;
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
int data;
if (world_rank == 0) {
data = 100;
printf("Process 0 broadcasting data %d\n", data);
my_bcast(&data, 1, MPI_INT, 0, MPI_COMM_WORLD);
}
else {
my_bcast(&data, 1, MPI_INT, 0, MPI_COMM_WORLD);
printf("Process %d received data %d from root process\n", world_rank, data);
}
MPI_Finalize();
}
MPI_Bcast
int MPI_Bcast (
void buffer, /* 发送/接收buf/
int count, /*元素个数*/
MPI_Datatype datatype,
int root, /*指定根进程*/
MPI_Comm comm)
根进程既是发送缓冲区也是接收缓冲区
用法实例
int p, myrank;
float buf;
MPI_Comm comm;
MPI_Init(&argc, &argv);
/*得进程编号*/
MPI_Comm_rank(comm, &my_rank);
/* 得进程总数 */
MPI_Comm_size(comm, &p);
if(myrank==0)
buf = 1.0;
MPI_Bcast(&buf,1,MPI_FLOAT,0, comm);
MPI_Gather
Root进程从n个进程的每一个接收各个进程(包括它自已)的消息. 这n个消息的连接按序号rank进行, 存放在Root进程的接收缓冲中.
int MPI_Gather (
void *sendbuf,
int sendcnt,
MPI_Datatype sendtype,
void *recvbuf,
int recvcount,
MPI_Datatype recvtype,
int root,
MPI_Comm comm )
【例】
int p, myrank;
float data[10];/*分布变量*/
float* buf;
MPI_Comm comm;
MPI_Init(&argc, &argv);
/*得进程编号*/
MPI_Comm_rank(comm,&my_rank);
/* 得进程总数 */
MPI_Comm_size(comm, &p);
if(myrank==0)
buf=(float*)malloc(p*10*sizeof(float);/*开辟接收缓冲区*/
MPI_Gather(data,10,MPI_FLOAT,
buf,10,MPI_FlOAT,0,comm);
MPI_Scatter
把指定根进程中的数据分散发送给组中的所有进程,包括自己
MPI_Scatter(*sendbuf,sendcnt,sendtype,*recvbuf,recvcnt,recvtype,root,comm)
MPI_Reduce
在组内的所有进程中执行一个规约操作,并把结果存放在一个进程中。
MPI_Reduce(*sendbuf,*recvbuf,count,datatype,op,root,comm)
MPI 集合通信函数
MPI_MAX 最大
MPI_MIN 最小
MPI_SUM 求和
MPI_PROC 乘积
MPI_LAND 逻辑与
MPI_LOR 逻辑或
MPI_BAND 位运算“与”
MPI_BOR 位运算“或”
MPI_Allgather
: 将各个进程的向量数据聚集为一个大向量,并分发到每个进程中,相当于各进程同步该大向量的各部分分量。相比于MPI_Gather(),只是少了个root参数。
MPI_Wtime
:返回一个用浮点数表示的秒数,仅某一时刻到调用时刻所经历的时间(s)
double start_time,end_time,total_time;
...
start_time=MPI_Wtime();
end_time=MPI_Wtime();
total_time=end_time-start_time;
printf("it took %f seconds\n",total_time);
编程实例
1. 测试用例
#include "stdio.h"
#include "mpi.h"
int main(){
int size,rank,namelength;
char name[MPI_MAX_PROCESSOR_NAME];
MPI_Init(NULL,NULL);
MPI_Comm_size(MPI_COMM_WORLD,&size);
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
MPI_Get_processor_name(name,&namelength);
printf("size=%d,rank=%d,name=%s,len=%d \n",size,rank,name,namelength);
fflush(stdout);
MPI_Finalize();
return 0;
}
【运行方法1】
-
打开C:\Program Files\MPICH2\bin文件下的wmpiexec.exe文件
-
打开VS项目下生成的exe文件,在
Number of process
中设置线程数,勾选run in an seperate window
,最后点击Execute
【运行方法2】
1.打开cmd, 输入mpiexec -n 4 C:\Users\zyw\source\repos\testmpi3\Debug\testmpi3
2. Hello World
【例1】
#include <stdio.h>
#include <mpi.h>
int main() {
MPI_Status status;
char string[] = "xxxxx";
char buf[] = "HELLO";
//void* pbuf = buf;
int myid;
MPI_Init(NULL, NULL);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
if (myid == 2)
MPI_Send(buf, 5, MPI_CHAR, 7, 1234, MPI_COMM_WORLD);
if (myid == 7) {
MPI_Recv(string, 5, MPI_CHAR, 2, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
printf("Got %s from P%d, tag %d\n", string, status.MPI_SOURCE, status.MPI_TAG);
fflush(stdout);
}
MPI_Finalize();
}
#include <mpi.h>
#include <stdio.h>
int main(int argc, char** argv) {
// 初始化 MPI 环境
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);
// 得到当前进程的名字
char processor_name[MPI_MAX_PROCESSOR_NAME];
int name_len;
MPI_Get_processor_name(processor_name, &name_len);
// 打印一条带有当前进程名字,秩以及
// 整个 communicator 的大小的 hello world 消息。
printf("Hello world from processor %s, rank %d out of %d processors\n",
processor_name, world_rank, world_size);
// 释放 MPI 的一些资源
MPI_Finalize();
}
解释:
搭建一个 MPI 程序的第一步是引入 #include <mpi.h> 这个头文件。然后 MPI 环境必须先使用MPI_Init
初始化
在 MPI_Init
的过程中,所有 MPI 的全局变量或者内部变量都会被创建。举例来说,一个通讯器 communicator 会根据所有可用的进程被创建出来(进程是我们通过 mpi 运行时的参数指定的),然后每个进程会被分配独一无二的秩 rank。当前来说,MPI_Init 接受的两个参数是没有用处的,不过参数的位置保留着,可能以后的实现会需要用到。
MPI_Comm_size
会返回 communicator 的大小,也就是 communicator 中可用的进程数量。在我们的例子中,MPI_COMM_WORLD(这个 communicator 是 MPI 帮我们生成的)这个变量包含了当前 MPI 任务中所有的进程,因此在我们的代码里的这个调用会返回所有的可用的进程数目。
MPI_Comm_rank
这个函数会返回 communicator 中当前进程的 rank。 communicator 中每个进程会以此得到一个从0开始递增的数字作为 rank 值。rank 值主要是用来指定发送或者接受信息时对应的进程
MPI_Get_processor_name
会得到当前进程实际跑的时候所在的处理器名字。
MPI_Finalize
是用来清理 MPI 环境的。这个调用之后就没有 MPI 函数可以被调用了
3. Nonblocking send/receive
发送语句的前缀由MPI_改为MPI_I
, I指的是immediate,即可改为非阻塞,否则是阻塞。
MPI_Isend (*buf, count, datatype,destination, tag, comm, MPI_Request *request)
: 非阻塞发送
MPI_Irecv (*buf, count, datatype,source, tag, comm, MPI_Request*request)
:非阻塞接收
MPI_Wait(MPI_Request *request,MPI_Status *status)
: 通信检查,必须等待指定的通信请求完成后才能返回,成功返回时,status 中包含关于所完成的通信的消息,相应的通信请求被释放,即request 被置成MPI_REQUEST_NULL。
#include "mpi.h"
#include <stdio.h>
int main(int argc,char *argv[]){
int numtasks,rank,dest,source,tag=1234;
char inmsg[]="xxxxx",outmsg[]="HELLO";
MPI_Status stats[2];
MPI_Request reqs[2];
MPI_Init(NULL,NULL);
MPI_Comm_size(MPI_COMM_WORLD,&numtasks);
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
if(rank==0){
dest=1;
MPI_Isend(&outmsg,5,MPI_CHAR,dest,tag,MPI_COMM_WORLD,&reqs[0]);
printf("Task %d:Send %s while inmsg=%s \n",rank,outmsg,inmsg);
fflush(stdout);
MPI_Wait(&reqs[0],&stats[0]);
printf("Task %d:Send %s while inmsg=%s reqs[0]=%d \n",rank,outmsg,inmsg,reqs[0]);
fflush(stdout);
}
else if(rank==1){
source=0;
MPI_Irecv(&inmsg,5,MPI_CHAR,source,tag,MPI_COMM_WORLD,&reqs[1]);
printf("Task %d:Received %s \n",rank,inmsg);
fflush(stdout);
MPI_Wait(&reqs[1],&stats[1]);
printf("Task %d:Received %s reqs[1]=%d \n",rank,inmsg,reqs[1]);
fflush(stdout);
}
MPI_Finalize();
return 0;
}
在本例中,一共有两个进程,0号进程进行非阻塞发送,1号进程非阻塞式接收。首先发送方发送通信请求并输出“Task0:Send…” ,然后发送函数立刻返回,此时接收方未接受到请求 输出“Task1:Received”。 过一段时间后,接收方接收到请求,此时发送方的wait函数才可以返回,然后发送方输出“Task 0:Send HELLO”,request [0]被置成738197504最后接收方接收到信息后wait函数返回,输出“Task1: Received HELLO”,request 被置成738197504
4. broadcast and collective communication
广播 (broadcast) 是标准的集体通信技术之一。一个广播发生的时候,一个进程会把同样一份数据传递给一个 communicator 里的所有其他进程。广播的主要用途之一是把用户输入传递给一个分布式程序,或者把一些配置参数传递给所有的进程。
MPI_Bcast
:一对多广播同样的消息
int MPI_Bcast (
void buffer, /* 发送/接收buf/
int count, /*元素个数*/
MPI_Datatype datatype,
int root, /*指定根进程*/
MPI_Comm comm)
根进程既是发送缓冲区也是接收缓冲区。在本例中将0号进程作为root, 即把0号进程中的buffer值,分别为0,1,2 送到0号,1号,2号进程。所以在广播结束后,进程0,1,2 的buffer[0]=0,buffer[1]=1,buffer[2]=2。
4.1 bcast 计算pi
【例1】
#include <stdio.h>
#include <mpi.h>
#define N 100000
int main() {
int myid, numprocs, i, n;
double mypi, pi, h, sum, x;
n = N;
MPI_Init(NULL, NULL);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
h = 1.0 / N;
sum = 0.0;
for (i = myid + 1; i <= N; i += numprocs) {
x = h * ((double)i - 0.5);
sum += (4.0 / (1.0 + x * x));
}
mypi = h * sum;
MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
if (myid == 0) {
printf("pi is approximately %.16f\n", pi);
fflush(stdout);
}
MPI_Finalize();
}
【例2】
#include<stdio.h>
#include<math.h>
#include "mpi.h"
double func(double xi)
{
return (4.0 / (1.0 + xi*xi));
}
int main(int argc,char* argv[])
{
int n=1000000000,myid,numprocs,i;
double pi,h,xi,res,startTime,endTime;
pi=0.0;
h = 1.0/(double)n;
res = 0.0;
MPI_Init(&argc,&argv);
MPI_Status status;
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
MPI_Bcast(&n,1,MPI_INT,0,MPI_COMM_WORLD);
startTime=MPI_Wtime();
if(myid!=0)
{
for(int i=myid;i<=n;i+=(numprocs-1))
{
xi = h * ((double)i - 0.5);
res += func(xi);
}
res = h * res;
MPI_Send(&res, 1, MPI_DOUBLE, 0, 99, MPI_COMM_WORLD);
}
else
{for(i=1;i<numprocs;i++)
{
MPI_Recv(&res,1,MPI_DOUBLE,i,99,MPI_COMM_WORLD,&status);
pi=pi+res;}
endTime = MPI_Wtime();
printf("\nPI is %f\nTime is : %f\n",pi,endTime - startTime);}
MPI_Finalize();
return 0;
}
【例3】
#include "mpi.h"
#include <stdio.h>
#include <math.h>
double f( double );
double f( double a )
{
return (4.0 / (1.0 + a*a));
}
int main( int argc, char *argv[])
{ int done = 0, n, myid, numprocs, i;
double PI25DT = 3.141592653589793238462643;
double mypi, pi, h, sum, x;
double startwtime = 0.0, endwtime;
int namelen;
char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
MPI_Get_processor_name(processor_name,&namelen);
fprintf(stderr,"Process %d on %s\n", myid, processor_name);
n = 1000000000;
while (!done)
{
if (myid == 0)
{
startwtime = MPI_Wtime();
}
MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
if (n == 0)
done = 1;
else {
h = 1.0 / (double) n;
sum = 0.0;
for (i = myid + 1; i <= n; i += numprocs)
{
x = h * ((double)i - 0.5);
sum += f(x);
}
mypi = h * sum;
MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
done=1; }
}
if (myid == 0) {
printf("pi is approximately %.16f, Error is %.16f\n", pi, fabs(pi - PI25DT));
endwtime = MPI_Wtime();
printf("wall clock time = %f\n", endwtime-startwtime);
}
MPI_Finalize();
return 0;
}
4.2 bcast 广播信息
#include "mpi.h"
#include <stdio.h>
#define N 3
int main(int argc,char *argv[]) {
int i, myrank, nprocs;
int buffer[N];
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
MPI_Comm_size(MPI_COMM_WORLD, &nprocs);
printf("myrank=%d \n before bcasting my buffer's data is: \n ", myrank);
for (int i = 0; i < N; i++) {
buffer[i] = myrank + i;
printf("buffer[%d]=%d\n", i, buffer[i]);
}
printf("\n");
MPI_Bcast(buffer, N, MPI_INT, 0, MPI_COMM_WORLD);
printf("after bcasting my buffer's data is:\n");
for (int i = 0; i < N; i++)
printf("buffer[%d]=%d\n", i, buffer[i]);
printf("\n");
MPI_Finalize();
return 0;
}
5. gather
MPI_gather
: 多对一收集各个进程的消息
int MPI_Gather (
void *sendbuf,
int sendcnt,
MPI_Datatype sendtype,
void *recvbuf,
int recvcount,
MPI_Datatype recvtype,
int root,
MPI_Comm comm )
Root进程从n个进程的每一个接收各个进程(包括它自已)的消息. 这n个消息的连接按序号rank进行, 存放在Root进程的接收缓冲中。
在本例中,开始通信前,每个进程的send区域的值分别为0~4,10~14,20~24。 在通信结束后跟进程的值为0~4、10~14、20~24, 即所有的进程把自己的消息发送给根进程的接收区。
5.1 gather 通信
#include "mpi.h"
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[]) {
int size, rank;
int send[5];
int* recv;
int i = 0;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
for (; i < 5; i++) {
send[i] = i + rank * 10;
}
printf("------------------------------\n");
for (i = 0; i < 5; i++)
printf("%d ", send[i]);
printf("\n");
if (rank == 0)
recv = (int*)malloc(size * 5 * sizeof(int));
MPI_Gather(send, 5, MPI_INT, recv, 5, MPI_INT, 0, MPI_COMM_WORLD);
if (rank == 0) {
printf("I'm root process, and the data that i received is:\n");
for (i = 0; i < size * 5; i++)
printf("%d", recv[i]);
}
printf("\n");
}
5.2 gather 计算pi
#include<stdio.h>
#include"mpi.h"
#include <stdlib.h>
#include<stddef.h>
double func(double xi)
{
return (4.0 / (1.0 + xi*xi));
}
int main(int argc,char *argv[])
{
int n=1000000000,myid,numprocs,root,i,sendnum;
double pi,h,xi,res,startTime,endTime,*recvbuf;
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
MPI_Bcast(&n,1,MPI_INT,0,MPI_COMM_WORLD);
root = 0;
pi=0.0;
h = 1.0/(double)n;
res = 0.0;
sendnum = numprocs;
startTime=MPI_Wtime();
for(int i=myid;i<n;i+=numprocs)
{
xi = h * ((double)i -0.5);
res += func(xi);
}
res = h * res;
if(myid==root)
{
recvbuf = (double *)malloc( sendnum * sizeof(double));}
MPI_Gather(&res,1,MPI_DOUBLE,recvbuf,1,MPI_DOUBLE,root,MPI_COMM_WORLD);
MPI_Barrier(MPI_COMM_WORLD);
if (myid==root)
{for(i=0;i<numprocs;i++){
pi=pi+recvbuf[i];}
free(recvbuf);
printf("pi is approximately %f\n", pi);
endTime = MPI_Wtime();
printf("wall clock time = %f\n", endTime-startTime);
}
MPI_Finalize();
return 0;
}
该代码是先通过广播机制,将n发送给各个进程。然后各个进程分别分担一些迭代过程。再通过MPI_Gather函数将每个进程的计算结果手机到根进程中,最后设置一个barrier,当所有进程的结果都发送到根进程后,将根进程中收集的所有迭代结果循环累加得到最终的pi值。
6. scatter
MPI_Scatter
:把指定根进程中的数据分散发送给组中的所有进程,包括自己
MPI_Scatter(*sendbuf,sendcnt,sendtype,*recvbuf,recvcnt,recvtype,root,comm)
在本例中,通信前各个进程的接收缓冲区的值均为0,root进程的接收缓冲区中的值分别为0~5,scatter 之后,每个进程的接收区值分别为01,23,45,说明数据分散成功
#include "mpi.h"
#include <stdio.h>
#include <stdlib.h>
#define N 2
int main(int argc,char *argv[]) {
int size, rank;
int* send;
int* recv;
int i = 0;
int j = 0;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
recv = (int*)malloc(N * sizeof(int));
for (; j < N;j++)
recv[j] = 0;
if (rank == 0) {
send = (int*)malloc(size * N * sizeof(int));
for (; i < size * N; i++) {
send[i] = i;
}
}
printf("-----------\nrank=%d\n", rank);
for (j = 0; j < N; j++) {
printf("传输前recv: recv_buffer[%d]=%d\n", j, recv[j]);
}
printf("-----------\n");
MPI_Scatter(send, N, MPI_INT, recv, N, MPI_INT, 0, MPI_COMM_WORLD);
printf("------------\nrank=%d\n", rank);
for (j = 0; j < N; j++)
{
printf("传输后recv:recv_buffer[%d]=%d\n", j, recv[j]);
}
printf("----------------\n");
MPI_Finalize();
return 0;
}
7. reduce
MPI_Reduce
:在组内的所有进程中执行一个规约操作,并把结果存放在一个进程中。
MPI_Reduce(*sendbuf,*recvbuf,count,datatype,op,root,comm)
7.1 reduce 通信
#include "mpi.h"
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[]) {
int myid, nprocs;
int i;
int send[3];
int recv[3];
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &nprocs);
printf("------------\n");
printf("myrank=%d\n", myid);
for (i = 0; i < 3; i++) {
send[i] = myid + i;
printf("send[i]=%d", send[i]);
}
printf("\n");
MPI_Reduce(send, recv, 3, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);
if (myid == 0) {
for (i = 0; i < 3; i++)
printf("recv buf -sum:%d\n", recv[i]);
}
MPI_Finalize();
return 0;
}
初始时,进程0,1,2的发送缓冲区中的值分别为0~2,1~3,2~4 在进行规约操作时,各个进程缓冲区中对应位置的值相加,其结果分别为3,6,9.
7.2 reduce 计算pi
#include<stdio.h>
#include<math.h>
#include "mpi.h"
double func(double xi)
{
return (4.0 / (1.0 + xi*xi));
}
int main(int argc,char* argv[])
{
int n=1000000000,myid,numprocs;
double pi,h,xi,res,startTime,endTime;
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
MPI_Bcast(&n,1,MPI_INT,0,MPI_COMM_WORLD);
if(myid==0)
{
startTime=MPI_Wtime();
}
h = 1.0/(double)n;
res = 0.0;
for(int i=myid;i<n;i+=numprocs)
{
xi = h * ((double)i - 0.5);
res += func(xi);
}
res = h * res;
MPI_Reduce(&res,&pi,1,MPI_DOUBLE,MPI_SUM,0,MPI_COMM_WORLD);
if(myid==0)
{
endTime = MPI_Wtime();
printf("\nPI is %f\nTime is : %f\n",pi,endTime - startTime);
}
MPI_Finalize();
return 0;
}
8. openMp和mpi 混合编程
#include "stdio.h"
#include "mpi.h"
#include "omp.h"
#include "math.h"
#define NUM_THREADS 8
long int n=10000000;
int main(int argc,char*argv[])
{
int my_rank,numprocs;
long int i,my_n,my_first_i,my_last_i;
double my_pi=0.0,pi,h,x,startTime,endTime;
MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
h=1.0/n;
my_n=n/numprocs;
my_first_i=my_rank*my_n;
my_last_i=my_first_i+my_n;
omp_set_num_threads(NUM_THREADS);
startTime=MPI_Wtime();
for(i=my_first_i;i<my_last_i;i++)
{
x=(i+0.5)*h;
my_pi=my_pi+4.0/(1.0+x*x);
}
MPI_Reduce(&my_pi,&pi,1,MPI_DOUBLE,MPI_SUM,0,MPI_COMM_WORLD);
if(my_rank==0)
{
printf("Approximation of pi:%15.13f\n",pi*h);
endTime = MPI_Wtime();
printf("Time is : %f\n",endTime - startTime);
}
MPI_Finalize();
return 0;
}
MPI主要实现的是多主机联网协作进行并行计算,OpenMP更适合单台计算机(多核)共享内存结构上的并行计算。MPI针对的是进程,openMP针对的是线程。通俗的说就是,在只使用MPI进行计算时,每个进程会分配一些迭代,即每个进程内部仍然使用了for循环。当使用了openmp后,每个进程for循环的的迭代有不同的线程来分担,从而实现进程间核线程间的并行计算,速度变的更快了。
MPI 多节点通信
9.1算法设计
算法设计
求解思路主要围绕着循环计算各部分梯形面积展开,其算法设计也主要针对循环过程的二级并行划分开展。具体如下:
(1) 节点间任务划分 采用均匀划分或交叉划分方法,将计算区间S划分为np个小区间 s i , S = ⋃ s i , i = 1 , 2 , ⋯ , n p s_{i}, S=\bigcup s_{i}, i=1,2, \cdots, n p si,S=⋃si,i=1,2,⋯,np,若采用交叉划分,每个区间所含的循环次数尽量为节点内核心数的整数倍。
(2) 节点内任务划分 在每个计算节点内调用OpenMP for并行制导语句,将所分配的循环分配给不同的线程,期间需要调整每个线程分配的循环次数,尽可能做到负载均衡,求解流程如下所示。
9.2 程序代码
编写代码pi_multinode.c
, 实现mpi和openmp混合编程计算pi的值。并可以显示进程数和进程所运行的节点。
#include "mpi.h"
#include <stdio.h>
#include <math.h>
#include "omp.h"
double f(double);
double f(double a)
{
return (4.0 / (1.0 + a*a));
}
int main(int argc,char *argv[])
{
int n, myid, numprocs, i;
double PI25DT = 3.141592653589793238462643;
double mypi, pi, h, sum, x;
double startwtime = 0.0, endwtime;
int namelen;
char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
MPI_Get_processor_name(processor_name,&namelen);
fprintf(stdout,"Process %d of %d is on %s\n",
myid, numprocs, processor_name);
fflush(stdout);
n = 10000; /* default # of rectangles */
if (myid == 0)
startwtime = MPI_Wtime();
MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
h = 1.0 / (double) n;
sum = 0.0;
/* A slightly better approach starts from large i and works back */
#pragma omp parallel for reduction(+:sum)private(x,i)
for (i = myid + 1; i <= n; i += numprocs)
{
x = h * ((double)i - 0.5);
sum += f(x);
}
mypi = h * sum;
MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
if (myid == 0) {
endwtime = MPI_Wtime();
printf("pi is approximately %.16f, Error is %.16f\n",
pi, fabs(pi - PI25DT));
printf("wall clock time = %f\n", endwtime-startwtime);
fflush(stdout);
}
MPI_Finalize();
return 0;
}
9.3 单节点测试
-
通过xftp将代码上传
-
在xshell 中连接两个节点
172.31.46.191
172.31.46.192
-
然后kungpeng192的对话窗口键入命令
mpicc pi_multinode.c -o pi_multinode -fopenmp
, 生成名为pi_multinode的可执行文件。 -
然后kungpeng192的对话窗口键入命令
mpiexec -n 4 ./multinode
出现如下图所示的结果,一共有四个进程,这四个进程都是运行在kunpeng192上的,说明单节点运行成功。
9.4 多节点配置
9.4.1 host配置
kungpeng192的对话窗口键入命令vim /etc/hosts
然后添加两行
172.31.46.191 kunpeng191
172.31.46.192 kunpeng192
添加完成后按shift
+esc
+:
, 并输入wq
保存修改
9.4.2 配置ssh免密登录
- kengpeng192生成公钥
kungpeng192的对话窗口键入命令:
cd ~/.ssh/ # 若没有该目录,请先执行一次ssh localhost
ssh-keygen -t rsa # 会有提示,接着连按3次回车
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys #将自己的公钥追加到authorized_keys里
-
kengpeng191生成公钥
kungpeng191的对话窗口键入命令:
cd ~/.ssh/ # 若没有该目录,请先执行一次ssh localhost
ssh-keygen -t rsa # 会有提示,接着连按3次回车
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys #将自己的公钥追加到authorized_keys里
- kungpeng191产生的公钥发送给kunpeng192
kungpeng191的对话窗口键入命令:
cd ~/.ssh/ # 若没有该目录,请先执行一次ssh localhost
ssh-keygen -t rsa # 会有提示,接着连按3次回车
scp ./id_rsa.pub student74@kunpeng192:~/.ssh/kunpeng191_id_rsa.pub # 191复制自己的公钥给192
- kunpeng192将kunpeng191发送过来的公钥追加到authorized_keys
kungpeng192的对话窗口键入命令:
cat ~/.ssh/kunpeng191_id_rsa.pub >> ~/.ssh/authorized_keys
- kungpeng192修改文件权限并将authorized_keys文件发送给kunpeng191
chmod 600 ~/.ssh/authorized_keys #可能有时不修改也不影响无密登录但还是建议修改
scp ./authorized_keys student74@kunpeng191:~/.ssh/authorized_keys
- 删除kunpeng192下的kunpeng191_id_rsa.pub
kungpeng192的对话窗口键入命令:
rm ~/.ssh/kunpeng191_id_rsa.pub #(好像这个删不删都可以。。)
- 验证免密登录
ssh kunpeng192 # kungpeng191的对话窗口键入
ssh kunpeng191 # kungpeng192的对话窗口键入
出现如下图,说明免密登录成功。
9.5 多节点运行
- 然后将可执行文件cpi发送给kunpeng191
在kunpeng192中键入命令:
scp -r /home/student74/pi_multinode student74@kunpeng191:/home/student74/
-
编写hosts.txt文件,并用xftp上传hosts.txt文件至kunpeng192
-
将hosts.txt文件发送给kunpeng191
在kunpeng192中键入命令:
scp -r /home/student74/hosts.txt student74@kunpeng191:/home/student74/
出现如上图说明传输成功
- 然后在kunpeng191或者kunpeng 192下执行如下语句
mpiexec -f hosts.txt -n 4 ./pi_multinode
mpiexec -n 1 ./pi_multinode
结果中既有kunpeng191又有192 说明多节点运行成功。而且在多节点上运行时,速度大概是单节点上速度的3倍。
然后,分别针对单节点和多节点在不同的进程数下(分别取1,10,50,100) 重复5次计算pi值,并得到其每种进程数下耗时的平均值,如下表所示:
然后根据如上的实验结果,做出如下折线图:
通过观察上述的图表,可以看出:
- 在进程数相同的情况下,单节点运算更耗时
- 无论是单节点还是多节点,随着进程数的增加,其消耗时间也随之增加
- 随着进程数的增加,单节点耗时增长的速度更快
9.6 性能分析
分别设置核心数为24,48,92分别测试其耗时(对于MPI + OpenMP程序,核心数 = 节点数 * 单节点核心数;MPI程序,核心数 = 进程数。)
通过对比分析发现随着核心数的增加,其耗时都会增加。但是MPI+OpenMP混合编程时,效率要比只进行MPI时要高。
通过查阅资料发现MPI + OpenMP和MPI并行程序执行过程中时间的分布按照:并行计算时间、通信时间两部分进行统计,随着计算规模的增加,MPI程序并行计算部分花费时间由11.95 s减少为0.64 s,降低近19倍,占总计算时间的百分比由91.0%减小为8.6%;进程间通信消耗的时间由1.09 s增加至6.77 s,提高6.2倍,占总计算时间的9.0%提高至91.4%。进程间通信开销远高于并行计算所花费的时间,极大的制约了并行效率的提高。
在同样的计算规模下,MPI + OpenMP程序并行部分计算花费时间由11.92 s减少为1.27秒,占总计算时间的百分比由99.7%减少为73.4%;进程通信部分消耗的时间由0.04 s增加为0.46 s,占总计算时间的百分比由0.3%提高至26.6%。采用MPI + OpenMP混合并行程序方法,有效降低了进程间通信的时间开销,大大提高了程序的并行执行效率和可扩展性。
因此可以得到如下的结论:
随着进程数的增加,MPI + OpenMP两种并行编程方式下,应用问题并行部分的计算时间均逐渐减小,但MPI + OpenMP并行程序的通信消耗时间只占MPI并行程序的6.8%。由此可见,MPI + OpenMP混合并行程序在很好地继承了两种并行编程环境的优点的同时,可以有效克服两种并行编程环境的缺点,可以有效提高并行程序的效率。
总结
-
MPI 集体通信(collective communication)指的是一个涉及 communicator 里面所有进程的一个方法。这节课我们会解释集体通信以及一个标准的方法
MPI 根据参与集合通信双方的进程数目,可大致分为三类
- 一对多:
Bcast
,Scatter
,Scatterv
- 多对一:
Gather
,Gatherv
,Reduce
- 多对多:
Allgather
,Allgatherv
,Allreduce
,Reduce_scatter
,Alltoall
,Alltoallv
,Alltoallw
,Exscan
此外,集合通信还包括一个同步操作Barrier, 同步栅格,即所有的进程到达后才可以继续执行。
- 一对多:
-
执行命令总结
//MPI
mpicc/mpic++ -o mpi[可执行文件名] mpi.c/mpi.cpp # 编译命令
mpirun/mpiexec -np [线程数] ./mpi[可执行文件] # 执行命令
//OpenMP
gcc/g++ -o omp omp.c/omp.cpp -fopenmp # 编译命令
./omp # 执行命令
//MPI + OpenMP
mpicc/mpic++ -o test omp.c/omp.cpp -fopenmp # 编译命令
./test [参数1] [参数2] … # 执行命令
参考
参考博客:
【1】(16条消息) 【mpich2】图文教程:mpich2的安装、配置、测试、vs配置、命令行测试(没有使用)_苍狼的博客-CSDN博客_mpich2配置与测试
【2】(16条消息) 如何在Window7系统中安装MPICH2_怡暘-CSDN博客
【3】(16条消息) windows下安装mpich2_执剑者罗辑的博客-CSDN博客_mpich2
【4】CentOS 7.6安装OpenMPI3.1.0 https://drugai.blog.csdn.net/article/details/106787587