MPI编程
简介
定义
MPI = Message Passing Interface,提供Message Passing方式的API来写程序。也就是说,不需要实际去做一个库(Library)函数,可以直接调用MPI的API来写并行程序。
好处
- portable,便携;
- Scalable,可扩展的;
- Flexible,灵活。
Programming Model
SPMD
Single Program Multiple Data。单一的过程中,多个数据或单个程序、多数据计算。
任务是分裂,并同时运行在多个处理器上,以不同的输入更快地获得结果。
要注意的是:
-
MPI编程需要一开始就决定处理器的数量,不能在运行过程中添加新的线程(MPI增加了添加新线程的API,但不建议使用),也就是一开始就要决定运行MPI程序时的平行度。
-
每个处理器执行的相同的代码,launch到处理器时,会到不同的brach,同时会分配给每个branch不同的ID,然后通过不同的ID读不同的数据的partition(划分),处理不同的数据。
Communication Methods
这里参考了下这篇博客。
几个定义
一、从communicated processes的角度
分为Synchronous communication(同步通信)和Asynchronous Communication(异步通信)。
1.Synchronous communication
Simultaneously(同步)发送/接受数据。
2.Asynchronous Communication
Non-simultaneously(非同步)发送/接受数据。
二、从function calls的角度
分为Blocking(阻塞)和Non-blocking(非阻塞)。
1.Blocking
由于一个function未完成,程序会blocking在一个地方,直到function的完成
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
2.Non-blocking
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
在Linux下,一个socket的文件描述符(file descriptor)默认就是阻塞模式的。在这种模式下,即便这个socket压根没有收到任何数据,我们的
read
调用也会一直阻塞在那里,无法返回,直到有数据到达为止。 如果我们把这个socket的文件描述符用
fcntl
设置为非阻塞的。在这种模式下,如果这个socket没有收到任何数据,我们的read
调用会立刻返回一个错误。这个时候,我们的程序就知道目前没法从这个socket里读到数据了,索性去干点别的事情,过段时间再调用read
。当一个应用进程对一个非阻塞的文件描述符循环调用read
时,我们称之为轮询(polling
)。
Blocking calls与synchronous、non-blocking calls 与asynchronous之间不是一一对应的关系,但一般来说,我们会使用blocking calls来实现synchronous,使用non-blocking calls来实现asynchronous。
Message Passing
1.Synchronous/Blocking Message Passing
2.Asynchronous/Non-Blocking Message Passing
与同步数据传输相比,需要加入一个Message Buffer。
Message Buffer一般是在sender和/或receiver端之间的一段内存空间。因此两者的通信变为:
sender端传递出数据给message buffer,而不是直接给receiver数据。当receiver执行recv()
命令时,从message buffer获取数据。
但是需要考虑到的是,如果receiver在sender向message buffer之前执行recv()
,此时获取的可能是之前存在message buffer的数据或者是空数据,因此需要在message buffer中添加一个flag/status,receiver执行recv()
判断message buffer的状态。
MPI API
Getting Start
- 首先
#include mpi.h
; - MPI calls:
注意:
- MPI的函数一般返回的是一个
Error Code
;因此获取数据的时候不是通过返回值获取,而是通过指针获取的。 - 上面的serial code虽然在
MPI_INIT()
外,但仍然会重复执行。MPI_INIT()
所包含的部分只是说会通过message passing interface来进行沟通。
Communicators 和 Groups
一个group有一个communicator,每一个communicator有一个ID。
默认有一个MPI_COMM_WORLD communicator。
Point-to-Point Communication Routines
-
buffer
:buffer
是一个地址,指到的是要接受或者发送的那个地址。(需要注意的是:首先要allocate
一个buffer
地址,如果不先allocate
一个buffer
地址会报错,并且allocate
的地址空间要大于等于后面type
类型对应的长度 乘以count
的大小得出来的size的值) -
type
和count
决定buffer
空间的大小。type
决定存放的是什么类型的数据,count
决定数据元素的数量,type
对应的长度乘以count
就是size
的大小。 -
type
的值为:MPI_CHAR
,MPI_SHORT
,MPI_INT
,MPI_LONG
等。它们都是define
的值,例如MPI_INT
可能实际上对应的是数字4
,但我们最好不要直接定义该参数为数字4
,因为不同电脑int
的长度不同,使用MPI_INT
更加保险。 -
source/dest
:int
类型的值,实际上是sender/receiver的ID。 -
tag
:int
类型的值,是一个可选项,目的是分传递的数据的类型。发送与接收的tag相同时,才会receiver才会真正的接收message buffer
中的数据。如果要接收任意数据,使用MPI_ANY_TAG
。 -
request
:non-blocking
时使用。在non-blocking
中,一个动作不论receiver是否接收到,sender发送到message buffer
中之后,就会去做其他的事情。同样,receiver不管是否真的发送完消息,接收到message buffer
中的数据(即使是一部分数据)后,都会继续去做计算。但有时,receiver只有在真的接收到sender发送的数据时,才能做下一步的计算。因此,如果需要检查sender是否发送完消息,就需要用到这里的request
了。也就是说,request
的作用就是检查sender端是否发送完了数据。 -
status
:实际上是一个结构体,记录了receive
的讯息,如API call是否成功,从message buffer
中获取了多少数据。
blocking的例子
MPI_Comm_rank(MPI_COMM_WORLD, &myRank); /* find process rank */
if (myRank == 0) {
int x=10;
MPI_Send(&x, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);
}else if (myRank == 1) {
int x;
MPI_Recv(&x, 1, MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD,status);
}
这里要注意的是,两个x
并不是share-memory的,第一个x
位于sender这个process对应的地址空间上,第二个x
位于receiver这个process对应的地址空间上。
non-blocking的例子
MPI_Comm_rank(MPI_COMM_WORLD, &myrank);/* find process rank */
if (myrank == 0) {
int x=10;
MPI_Isend(&x, 1, MPI_INT, 1, 0, MPI_COMM_WORLD, req1);
compute();
} else if (myrank == 1) {
int x;
MPI_Irecv(&x,1,MPI_INT,0,MPI_ANY_TAG,MPI_COMM_WORLD,req1);
}
MPI_Wait(req1, status);
MPI_Wait()
是一个blocking call,程序会卡在这里,直到req1
完成,也就是receiver真正接收到了数据。
MPI_Test()
返回一个flag,这个flag代表request是否完成。
Collective Communication Routines
Collective Calls
所有collective calls都是blocking calls,一个group中的所有process都需要执行这个collective calls,否则会卡住。
MPI_Barrier(comm)
:所有属于comm
这个group的任务,所有在这个group的process都需要执行MPI_Barrier(comm)
这条命令,否则会程序会卡住。
MPI_Bcast(&buffer,count,datatype,root,comm)
:将id为root
的这个process的buffer中的值broadcast(广播)出去,这条消息将被其他所有process接收到。注意其他所有process的buffer一定要先allocate
好,否则将出现问题。
MPI_Scatter(&sendbuf,sendcnt,sendtype,&recvbuf,recvcnt,recvtype,root,comm)
:
从sender端的值,要分配给receiver端。把buffer中在arrange之内的做均分,分配到每一个receiver端。sendcnt
表示了sender端counter的大小,recvcnt
表示了receiver端counter的大小。使用这个函数,将会把sender端buffer中的内容平均分配,并给到receiver端。但是,如果sender端只能分3块,而receiver有5个,那么会有两个receiver process不会收到消息,程序仍会继续运行。MPI_Gather(&sendbuf,sendcnt,sendtype,&recvbuf,recvcnt,recvtype,root,comm)
:
从多个send端发送消息,消息汇总到一个receive端中。(与MPI_Scatter()
作用相反)
sendcnt
:在send buffer中元素的数量;number of elements in send buffer (integer)
recvcnt
:一次接收元素的数量。number of elements for any single receive (integer, significant only at root)
MPI_Reduce(&sendbuf,&recvbuf,count,datatype,op,dest,comm)
:
通过MPI_Reduce()
这个函数,将几个process的结果整合到一个process中,并且将数据本身也进行整合。op
这个参数,可以选择如何整合:比如,MPI_SUM
其他buffer中的结果相加,结果给dest
进程的存储在recvbuf
中。
MPI_Allgather(&sendbuf,sendcnt,sendtype,&recvbuf,recvcnt,recvtype,root,comm)
:
MPI_Allreduce(&sendbuf, &recvbuf, count, datatype, op, comm)
:
Group and Communicator Routines
先有Group
,通过Group
创建Communicator
。
MPI_Comm_group(Comm,&Group)
,:通过已知的Communicator,获取对应的group的token。MPI_Group_incl(Group, size, ranks[], &NewGroup)
:如果要创建新的group,只能从原来的group中选定哪几个作为新group的成员。MPI_Comm_create(Comm, NewGroup, &NewComm)
:创建新的communicator。
这里需要注意的是:MPI_Group_incl()
和``MPI_Comm_create()参数中,第一个参数
Group和
Comm,是父集的
Group和
Comm`。
上面三个都是Collective Calls!!!
MPI I/O
MPI_File_open()
collective call!!
使用MPI_File_open()
而不是C语言库中的fopen()
是因为:如果使用fopen()
,多个进程请求同一个文件的时候,会造成错误(系统为了防止同时rewrite的问题,禁止同一个文件同时被打开多次)。
Read & Write
Collective I/O
对小I/O比较好
Independent I/O
对大I/O较好
MPI-IO API
小小的吐槽:最近在看台湾人的网课,一直程式程式的,搞得我有点被带偏Orz。打出来的是程序,实际上心里想的是程式哈哈哈,口音真是一个奇怪的东西。*
小小的吐槽:最近在看台湾人的网课,一直程式程式的,搞得我有点被带偏Orz。打出来的是程序,实际上心里想的是程式哈哈哈,口音真是一个奇怪的东西。