MPI(Message-Passing-Interface)消息传递接口
1.MPI安装
python mpi安装mpi4py的python库
pip install mpi4py
下载MicrosoftWindowsMPI安装包网址:https://github.com/icepoint666/MPI中的MSMpiSetup.rar
安装后得到Bin文件夹:

将MicrosoftMPI/Bin文件夹路径添加到环境变量:

命令行输入mpiexec运行,出现帮助表示安装成功
2.试验代码
from mpi4py import MPI
comm = MPI.COMM_WORLD
comm_rank = comm.Get_rank()
comm_size = comm.Get_size()
print 'I'm the %d process of %d processes" % (comm_rank, comm_size)

2.点对点传输
点对点通信.其实就是最简单的进程A向进程B发送信息,而进程B向进程A接收信息.这是关于两个进程之间的通信.
示例代码:
from mpi4py import MPI
comm = MPI.COMM_WORLD
comm_rank = comm.Get_rank()
comm_size = comm.Get_size()
data = [comm_rank]*5
comm.send(data,dest=(comm_rank+1)%comm_size)
data_recv =comm.recv(source=(comm_rank-1)%comm_size)
print "my rank is %d, and Ireceived:" %comm_rank
print data_recv
在命令行中输入命令
mpiexec -n 5 python mpip2p.py
执行结果:
my rank is 4, and Ireceived:
[3, 3, 3, 3, 3]
my rank is 3, and Ireceived:
[2, 2, 2, 2, 2]
my rank is 2, and Ireceived:
[1, 1, 1, 1, 1]
my rank is 0, and Ireceived:
[4, 4, 4, 4, 4]
my rank is 1, and Ireceived:
[0, 0, 0, 0, 0]
指定启动5个mpi进程来执行后面的程序。相当于对脚本拷贝了5份,每个进程运行一份,互不干扰。在运行的时候代码里面唯一的不同,就是各自的rank也就是ID不一样。
Get_rank()函数:获取当前进程rank值
Get_size()函数:获取总共的进程数
send()函数:将数据送给rank为dest的值的进程
recv()函数:接收rank为source的值的数据
消息传递的同步异步性:
recv是阻塞函数,也就是说进程要收到发送方的数据,这个函数才返回.
而send是不确定的,也就是说它有时候是阻塞,有时候是非阻塞.当发送的数据不多的时候,mpi会将数据存到一个系统缓冲区,然后马上进行send方法的返回.而当数据量很大超过缓冲区的大小的时候,mpi需要等待接收方接收,然后把数据拷贝给接收方,再进行send方法的返回.
简单来说,数据量少->非阻塞(同步),数据量大->阻塞(异步).
除了send和recv方法,还有Send和Recv方法.,这样区分是由于要传递的数据的性质差异.当我们要传递int,float,list,dict等python内置类型的数据的时候,我们使用小写的方法.而当使用buffer类型的数据的时候,我们要使用大写的方法.
send的多个版本:
事实上,除了大写小写的版本,send还有不同的版本,这个不同是基于不同的发送策略的,而这些版本都有大小写之分.
bsend:缓冲模式,数据写入缓冲区,马上返回,用户必须确保缓冲区大小足够
ssend:同步模式,等接收方接收才返回
rsend:就绪模式,发送时必须确保接收方处于等待接收的状态,否则产生错误
send:标准模式(bsend+ssend),send实际上就是bsend和ssend的结合体.
3.多点传输:
from mpi4py import MPI
comm = MPI.COMM_WORLD
comm_rank = comm.rank()
comm_size = comm.size()
if comm_rank == 0:
data = [1,2,3]
for i in range(comm_size - 1):
comm.send(data,dest=i+1)
else:
data = comm.recv(source = 0)
print "Process %d receive"%comm_rank,data
运行结果:
mpiexec -n 6 python mpimp.py
Process 1 receive [1,2,3]
Process 2 receive [1,2,3]
Process 3 receive [1,2,3]
Process 4 receive [1,2,3]
Process 5 receive [1,2,3]
此做法漏洞:
在单机上跑这n个进程好像没所谓,CPU始终在工作,时间复杂度也是O(n)级别.
但假如是n台机器分别跑这n个进程,第0台机器始终在发送数据,而其他机器的大部分时间都在排队,等第0台机器往自己发送数据.这样的话,这堆机器要运行完这堆进程,需要O(n)时间.等于一台机器的工作效率,不是满意的结果。
广播(改进):
想到了,我们可以像p2p那样做,有数据的机器都帮忙向没有数据的机器发送数据,这样的话时间复杂度是可以降低到O(logn)的!
mpi有实现这样操作的接口,bcast函数
改进代码:
from mpi4py import MPI
comm = MPI.COMM_WORLD
comm_rank = comm.rank()
comm_size = comm.size()
if comm_rank == 0:
data = [1,2,3]
comm.bcast(data, root=0)
else:
data = comm.bcast(None, root=0)
print "Process %d receive"%comm_rank,data
bcast()函数:无论是广播者,还是被广播者,都是调用bcast函数,而不像点对点那样一个send另一个recv.bcast()函数一个根进程把数据发给其他进程。
散播:

散播的函数和广播的参数是一样的,只是返回值不一样.
注意!散播的发送方也会接收到数据(和概念图有出入),
散播里列表里元素的分发不是按进程0就分得第0个元素,进程1就第1个元素这样的.而是一种类似随机的打乱的分发策略.
散播发送的数据,data(列表)里元素的个数必须等于进程的个数.否则会出错。
示例代码:
from mpi4py import MPI
comm = MPI.COMM_WORLD
comm_rank = comm.Get_rank()
comm_size = comm.Get_size()
if comm_rank == 0:
data = [1,2,3,4,5,6]
else:
data = None
data = comm.scatter(data, root=0)
print "Process %d receive"%comm_rank,data
运行结果:
mpiexec -n 6 python mpisca.py
Process 1 receive 2
Process 4 receive 5
Process 2 receive 3
Process 0 receive 1
Process 3 receive 4
Process 5 receive 6
收集:
散播的逆操作:
from mpi4py import MPI
comm = MPI.COMM_WORLD
comm_rank = comm.Get_rank()
comm_size = comm.Get_size()
if comm_rank == 0:
data = comm.gather(comm_rank, root=0)
print data
else:
comm.gather(comm_rank,root=0)
mpiexec -n 8 python mpigather.py
[0, 1, 2, 3, 4, 5, 6, 7]
reduce()规约函数:
它相当于在收集的过程中不断地进行两元运算,最终在接收方那里只有一个值,而不是一个列表.
也就是说规约函数
示例代码:通过 1−13+15−17+...=π4 1 − 1 3 + 1 5 − 1 7 + . . . = π 4 计算圆周率
from mpi4py import MPI
comm = MPI.COMM_WORLD
comm_rank = comm.Get_rank()
comm_size = comm.Get_size()
k = (1.0 if comm_rank%2 == 0 else -1.0)/(2*comm_rank +1)
data = comm.reduce(k, root=0,op=MPI.SUM)
if comm_rank == 0:
pi = data*4
print "PI = %.6f"%pi
运行结果:
C:\Python27\Scripts\ML\MPI>mpiexec -n 12 python mpireduce.py
PI = 3.058403
注意事项:
1.并行计算的reduce,scatter,gather在执行信息交互函数是并行,信息交互完之后,每个进程统一从函数中出来,执行接下来的代码
2.上述函数root秩代表根节点:scatter传播,gather接收,reduce最终汇总结果的进程,
3每台机器reduce复杂度,只有O(logn),reduce函数MPI_SUMj操作:
假设九个进程
1, 2, 3, 4, 5, 6, 7, 8, 9
1+2, 3+4, 5+6, 7+8, 9
1+2+3+4, 5+6+7+8, 9
1+2+3+4+5+6+7+8, 9
1+2+3+4+5+6+7+8+9
4.单机的话不要开几百个进程,不是开玩笑的
5.注意的是,散播和reduce中发送接收到的返回值,不是接收方最终得到的返回值,而是一个none.
alltogether:收集后再广播一次,allreduce:reduce+bcast
barrier是一种全局同步,就是说全部进程进行同步.
当一个进程调用barrier的时候,它会被阻塞.
当所有进程都调用了barrier之后,barrier会同时解除所有进程的阻塞.
但运行起来发现并不是这回事.所有进程没有像期待那样先全部输出begin,再全部输出end,barrier这个函数仿佛形同虚设.
其实这里问题不是在barrier,而是在print.
我们OS的IO是有缓冲的,一个数据要出现在屏幕上,简单来说是经过内存->标准IO文件->控制台屏幕.
而进程间不共享IO文件(后面会学到如何在MPI的进程里共享文件),共享控制台屏幕.
因此屏幕上语句的顺序依赖OS什么时候将IO文件里的内容推到屏幕上.
我们强制让内存->标准IO文件和标准IO文件->控制台屏幕这两步一起进行,也就是加上flush语句.
form mpi4py import MPI
import sys
comm = MPI.COMM_WORLD
comm_rank = comm.Get_rank()
comm_size = comm.Get_size()
print comm_rank,'begin'
sys.stdout.flush()
comm.barrier()
print comm_rank,'end'
sendrecv()函数
发送send+接收recv
data = sendrecv(data,dest=1)
关于进程
from mpi4py import MPI
comm = MPI.COMM_WORLD
comm_rank = comm.Get_rank()
comm_size = comm.Get_size()
data_send = [comm_rank]*5
comm.send(data_send,dest=(comm_rank+1)%comm_size)
data_recv = comm.recv(source=(comm_rank-1)%comm_size)
print (my rank is %d, and Ireceived: %comm_rank)
print data_recv
这里面有个需要注意的问题,如果我们要发送的数据比较小的话,mpi会缓存我们的数据,然后继续执行后面的指令,而不会等待对方进程执行recv指令接收这个数据。
但是,如果要发送数据量很大,[rank]*500程序就会很卡,因为所有进程都会卡在发送这条指令,等待下一个指令发起接收指令,但是进程是执行完发送的指令才能接收的指令,这就和死锁差不多。
一般修改如下:
from mpi4py import MPI
comm = MPI.COMM_WORLD
comm_rank = comm.Get_rank()
comm_size = comm.Get_size()
data_send = [comm_rank]*500
if comm_rank == 0:
comm.send(data_send, dest=(comm_rank+1)%comm_size)
if comm_rank > 0:
data_recv = comm.recv(source=(comm_rank-1)%comm_size)
comm_send(data_send,dest=(comm_rank-1)%comm_size)
if comm_rank == 0:
data_recv = comm.recv(source=(comm_rank-1)%comm_size)
这也就是为什么接收放在前面的原因了