Python中MPI消息传递接口

MPI(Message-Passing-Interface)消息传递接口

1.MPI安装

python mpi安装mpi4py的python库

pip install mpi4py

下载MicrosoftWindowsMPI安装包网址:https://github.com/icepoint666/MPI中的MSMpiSetup.rar

安装后得到Bin文件夹:

Python中MPI消息传递接口_第1张图片

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

Python中MPI消息传递接口_第2张图片

命令行输入mpiexec运行,出现帮助表示安装成功

Python中MPI消息传递接口_第3张图片
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)

Python中MPI消息传递接口_第4张图片

2.点对点传输

点对点通信.其实就是最简单的进程A向进程B发送信息,而进程B向进程A接收信息.这是关于两个进程之间的通信.

示例代码:

#mpip2p.py
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.多点传输:

#mpimp.py
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函数

改进代码:

#mpimp.py
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()函数一个根进程把数据发给其他进程。

散播:

Python中MPI消息传递接口_第5张图片

散播的函数和广播的参数是一样的,只是返回值不一样.

注意!散播的发送方也会接收到数据(和概念图有出入),

散播里列表里元素的分发不是按进程0就分得第0个元素,进程1就第1个元素这样的.而是一种类似随机的打乱的分发策略.

散播发送的数据,data(列表)里元素的个数必须等于进程的个数.否则会出错。

示例代码:

#mpisca.py
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

收集:

散播的逆操作:

#mpigather.py
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()规约函数:

它相当于在收集的过程中不断地进行两元运算,最终在接收方那里只有一个值,而不是一个列表.

也就是说规约函数

示例代码:通过 113+1517+...=π4 1 − 1 3 + 1 5 − 1 7 + . . . = π 4 计算圆周率

#mpireduce.py
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)

这也就是为什么接收放在前面的原因了

你可能感兴趣的:(python)