并行与分布式计算:MPI入门(六)
Section 6 MPI 入门
吐槽:罗老师的教学逻辑实在是太混乱了,看他的PPT要花比学教材多两倍的时间,学到教材里一半的知识(总量多,可那种组织形式进到脑子有效值就一半)。
6.1 Why MPI?
(因为要写作业)
6.1.1 MPI与OpenMP
MPI的目标是高性能,大规模性,和可移植性。
先前我们介绍的OpenMP主要是基于共享内存的并行系统,适合多核CPU上的并行程序开发。而MPI是多主机联网协作的工具(当然也可以相对低效地用于单主机上的多核计算)。
尽管MPI的编写相对麻烦烦,由于使用了进程间通讯的方式协调导致并行效率相对低一些,但因其可以协调主机间的并行计算,所以在并行规模上的可伸缩性极强——从PC到超级计算机均可。
总而言之,OpenMP是为单主机上的多线程计算而设计的(仅能在单台主机工作),而MPI是为多主机协作设计的。
6.1.2 为什么要了解MPI
MPI是最重要的MP library标准。尽管现在可能有一些更加方便更加modern的library,学习MPI仍然有助于使你更好的了解collective communication(组通信)的概念
6.2 MPI基础讲解
先来个HelloWorld吧
#include <mpi.h>
int main(int argc, char *argv[])
{
int npes, myrank;
MPI_Init(&argc, &argv);//初始化MPI
MPI_Comm_size(MPI_COMM_WORLD, &npes);//npes获得通信域中的进程总数
MPI_Comm_rank(MPI_COMM_WORLD, &myrank);//myrank获得本进程的序号
printf("From process %d out of %d, Hello World!\n",myrank, npes);
MPI_Finalize();//结束MPI
return 0;
}
与OpenMP不同,MPI并不拆解某个代码块的任务,而是程序员将任务拆分完成后,指挥不同处理器执行的工具。所有的处理器都将执行相同的代码,区分具体任务形式的往往是进程的序号
- 例如,筛法求素数的时候,将长为1000数组均分为10段,第k号进程处理第k段,即下标从 ( k − 1 ) ∗ 100 + 1 (k-1)*100+1 (k−1)∗100+1到 k ∗ 100 k*100 k∗100的部分
6.2.1 头文件
#include<mpi.h>
6.2.2 基本库函数
int MPI_Init(int*argc,char***argv);//初始化MPI环境,必须出现在其他MPI函数之前
int MPI_Finalize();//结束MPI环境(执行各种clean-up tasks)
int MPI_Comm_size(MPI_COMM comm, int *size);//size对应位置获得通信域中的进程总数
int MPI_Comm_rank(MPI_COMM comm, int *rank);//rank对应位置获得当前进程的序号
(所谓的通讯域就是能相互传递消息的处理器形成的一个集合)
6.2.3 编译
mpicc -o name example.c
mpic++ -o name example.cpp
将example中的代码编译并将可执行程序存入name文件
6.2.4 执行
指定进程数目
mpirun -np number name
生成number个进程来跑name
指定主机处理器
需要时考虑使用 --host 和 --hostfile
Multiple Program Multiple Data(MPMD)
mpirun-np 1 master : -np 7 slave
上例中,cmd命令为两种完全不同的执行程序展开共八个进程,这些执行程序将处于同一通信域中(即可以通过MPI彼此通信)
6.2.5 Send & Receive
Non-buffered blocking
对于信息传递来说,最简单(也是最安全)的收发模式就是sender和receiver在确定彼此状态后才能执行。
sender将发送send请求,receiver将发送准许请求的消息,只有两者接上了,才会发送data。也就是说,不管是receiver还是sender,先执行的都要等待后执行的。
所谓的non-buffered blocking就是指
- sender开始执行后,必须等待receiver准许进行sending,获得准许后,sender完成sending才能返回,从sender开始到返回这段时间原进程在sender处停滞
- receiver开始执行后,只有等待sender准备好发送请求,receiver完成receiving才能返回,从receiver开始到返回这段时间原进程在receiver处停滞
缺点:将产生大量的idling开销(除非两者精确的同时执行)
buffered blocking
还有一种收发模式:sender直接将data复制到接受进程的缓冲区,receiver直接从本进程缓冲区复制走data
缓存(buffering)实际上是把空闲开销(idling overhead)替换成了缓冲存取开销(buffer copying overhead)。
- 当接收端没有相关通信硬件时,sender将打断接收端的进程,把data复制到接收进程的缓冲区
- 接收端肯定还是需要等待发送端把东西发送过来才能继续
Non-blocking
non-blocking模式不管到底安全不安全,请求发送后就继续执行后续语句,准许发送后也去执行后续语句。当请求和准许match了,正式执行send和receive
- 对于无缓冲的non-blocking模式,潜在的危险主要有两方面:发送端后续的程序可能会在发送前改变待发送的数据,从而导致正式发送的数据实际上是有错误的;接收端后续的程序可能会在接收前就访问待接收数据。
使用non-blocking模式需要程序员很好的理解和掌握程序的内容
Message Passing Libraries一般会同时提供blocking和non-blocking的原语
MPI的send模式
int MPI_Send(
void *message,//被传送数据的起始地址
int count,//被传送的数据项个数
MPI_Datatype datatype,//被传送的数据类型
int dest,//接收进程号
int tag,//信息的标记(可以说明用途)
MPI_Comm comm//通信域
);
遵从阻塞模式,只有发送缓冲区可以使用时(例如数据已被安全送到接收缓冲区或中间缓冲区)才返回。一般来说,系统会把这些消息复制到系统缓冲区。
其他一些可以考虑的发送方式(了解有相关方法即可,具体使用时再考虑细节)
- MPI_Send:
- MPI_Bsend:立刻返回
- MPI_Ssend:匹配到了接收器才返回
- MPI_Rsend:只有确定对应接收器一定早已待命才会使用这个语句;在这种特殊情况下,这个sender会给系统提供一些信息,从而可以减少一些开销
- MPI_Isend:遵从非阻塞模式,但不能立刻再次使用发送缓冲区
MPI的receive模式
int MPI_Recv(
void *message,//要被存放的起始位置
int count,//能接收的最大数目
MPI_Datatype datatype,//接收的数据类型
int source,//发送进程号
int tag,//信息的标记(可以说明用途)
MPI_Comm comm,//通信域
MPI_Status *status//一条类型为MPI_Status的记录,Recv完成后,可以访问status获得函数相关的状态信息
);
特别说明,从status中可以获得下面几项内容
- status->MPI_source 发送进程号
- status->MPI_tag 接收到的消息的tag
- status->MPI_ERROR 错误情况
为什么要有status呢?
- MPI_Recv中的source一般设置为常数MPI_ANY_SOURCE,从而接收任意来源的信息
- MPI_Recv中的tag一般设置为常数MPI_ANY_TAG,从而接收任意tag的信息
在这种不对发送来源和发送标签做限制的情况下,需要查探状态记录(status)才能确定某特定信息的发送进程或者tag值
此外,还有一个辅助函数
int MPI_Get_count(MPI_Status* status,MPI_Datatype datatype,int* count);
该辅助函数可以给出具体接收到的数据条目数
(接收函数仅给由接收端给出了最大容量,所以确定具体接收到的数据数目也是一个需要解决的事情)