mpi4py 点到点通信

上一篇中我们已经通过几个简单的例子展示了使用 mpi4py 进行 Python 的并行编程方法,大家可以看到使用 mpi4py 进行 MPI 并行编程是比较简单和方便的,但是要用好 mpi4py,写出实用的功能强大的 MPI 并行计算程序,我们还需要对 mpi4py 所提供对各种方法及其用法有更深入的了解。下面我们从最基本的点到点通信开始,详细地介绍 mpi4py 的各个有用的通信方法,并给出其使用方法的程序代码。

点到点通信要求必须有 send 和 recv 配对。比如说,如果多个发送进程向同一个目标进程连续发送两个消息,这两个消息都能与接收端的同一个 recv 匹配,则接收的顺序必须与发送的顺序一致。如果接收端进程同时也发起了两个连续的 recv,都与同一个消息匹配,且第一个 recv 仍在等待的状态下,第二个 recv 不能先于第一个操作收取第二个与其匹配的消息。消息传递中的 tag 具有决定性作用,如果进程使用的是单线程,且没有在接收消息操作中使用通配符(即 MPI.ANY_SOURCE)作为 tag,则发送的顺序与接收顺序严格匹配。

点到点通信共有12对,分别对应阻塞通信方式1组(4个)和非阻塞通信方式2组(分别为非重复的非阻塞和可重复的非阻塞),其中有一些还分为以小写字母开头的方法和以大写字母开头的方法,前者适用与可以被 pickle 系列化的任何 Python 对象,而后者对数组类型的数据通信更为高效。详细的分类见下表:

点到点通信分类

可以看出这些发送函数都遵循一定的命名规范:??[S/s]end,其中 B/b 表示缓存模式(Buffer),R/r 表示就绪模式(Ready),S/s 表示同步方式(Synchonous),I/i 表示立即发送,即非阻塞方式发送(Imediately)。不带任何前缀修饰的 Send/send 称为标准模式。I/i 可分别与 B/b,R/r,S/s 组合,得出如上表所示的各种通信模式。

消息通信的数据传递流程如下:

  1. 发送端调用发送函数;
  2. MPI 环境从发送缓冲区提取需要发送的数据,据此组装发送消息;
  3. 将组装的消息发送给目标;
  4. 接收端收取可匹配的消息,并将其解析到接收缓冲区。

以上概要地介绍了 mpi4py 中的点到点通信方法及其消息传递的流程,在后面我们将依次介绍四种阻塞通信模式并给出相应的程序实例。下一篇中我们从标准的阻塞通信模式开始

上一篇中概要地介绍了 mpi4py 中的点到点通信方法及其消息传递的流程,下面我们介绍 mpi4py 中标准的阻塞通信模式。

阻塞通信是指消息发送方的 send 调用需要接收方的 recv 调用的配合才可完成。即在发送的消息信封和数据被安全地“保存”起来之前,send 函数的调用不会返回。标准模式的阻塞 send 调用要求有接收进程的 recv 调用配合。

下面是 mpi4py 中用于标准阻塞点到点通信的方法接口(MPI.Comm 类的方法):

send(self, obj, int dest, int tag=0)
recv(self, buf=None, int source=ANY_SOURCE, int tag=ANY_TAG, Status status=None)

Send(self, buf, int dest, int tag=0)
Recv(self, buf, int source=ANY_SOURCE, int tag=ANY_TAG, Status status=None)

首先交代一下这些方法调用中用到的参数,mpi4py 中其它的点到点通信方法都有类似的参数格式。

以小写字母开头的 send 方法可以发送任意可被 pickle 系列化的 Python 对象 obj,在发送之前这个对象被 pickle 系列化为字符串或二进制数据,int 类型的 dest 指明发送的目的地(要接收该消息的进程的进程号),可选的 int 类型的 tag 指明发送消息的 tag。recv 方法可以有一个可选的 buf 参数以指明一个预先分配好的内存缓冲区作为接收缓冲区。在大多数情况下都用不着这个参数,只有在某些需要考虑优化消息接收的情况下,你可以预先分配一个足够大的内存缓冲区,并用这个缓冲区来反复接收多个消息。另外需要注意的是这个缓冲区中存放的是被 pickle 系列化了的字符串和二进制数据,你需要使用 pickle.loads 来恢复所接收的 Python 对象。可选的 int 类型的 source 指明接收的消息源(发送该消息的进程的进程号),可选的 tag 指明消息的 tag,另外一个可选的 status 参数可以传人一个 MPI.Status 对象。接收进程可指定通用接收信封即 MPI.ANY_SOURCE,MPI.ANY_TAG,接收来自任何源进程的任意 tag 消息。可见,send 和 recv 操作是非对称的,即发送方必须给出特定的目的地址,而接收方则可以从任意源接收谢谢。从任意源和任意 tag 接收的消息可能无法判断其来源,这时可以从 status 对象的相关属性中找到对应的信息。recv 方法返回所接收的 Python 对象。

以大写字母开头的 Send/Recv 方法具有几乎一样的参数,不同的是其第一个参数 buf 应该是一个长度为2或3的 list 或 tuple,类似于 [data, MPI.DOUBLE],或者 [data, count, MPI.DOUBLE],以指明发送/接收数据缓冲区,数据计数以及数据类型。当 count 省略时会利用 data 的字节长度和数据类型计算出对应的 count。对 numpy 数组,其计数和数据类型可以自动推断出来,因此可以直接以 data 作为第一个参数传给 buf

下面分别给出 send/recv 和 Send/Recv 的使用例程。

# send_recv.py

from mpi4py import MPI


comm = MPI.COMM_WORLD
rank = comm.Get_rank()

send_obj = {'a': [1, 2.4, 'abc', -2.3+3.4J],
            'b': {2, 3, 4}}

if rank == 0:
    comm.send(send_obj, dest=1, tag=11)
    recv_obj = comm.recv(source=1, tag=22)
elif rank == 1:
    recv_obj = comm.recv(source=0, tag=11)
    comm.send(send_obj, dest=0, tag=22)

print 'process %d receives %s' % (rank, recv_obj)

运行结果如下:

$ mpiexec -n 2 python send_recv.py
process 0 receives {'a': [1, 2.4, 'abc', (-2.3+3.4j)], 'b': set([2, 3, 4])}
process 1 receives {'a': [1, 2.4, 'abc', (-2.3+3.4j)], 'b': set([2, 3, 4])}
# Send_Recv.py

import numpy as np
from mpi4py import MPI


comm = MPI.COMM_WORLD
rank = comm.Get_rank()

count = 10
send_buf = np.arange(count, dtype='i')
recv_buf = np.empty(count, dtype='i')

if rank == 0:
    comm.Send([send_buf, count, MPI.INT], dest=1, tag=11)
    # comm.Send([send_buf, MPI.INT], dest=1, tag=11)
    # comm.Send(send_buf, dest=1, tag=11)

    comm.Recv([recv_buf, count, MPI.INT], source=1, tag=22)
    # comm.Recv([recv_buf, MPI.INT], source=1, tag=22)
    # comm.Recv(recv_buf, source=1, tag=22)
elif rank == 1:
    comm.Recv([recv_buf, count, MPI.INT], source=0, tag=11)
    # comm.Recv([recv_buf, MPI.INT], source=0, tag=11)
    # comm.Recv(recv_buf, source=0, tag=11)

    comm.Send([send_buf, count, MPI.INT], dest=0, tag=22)
    # comm.Send([send_buf, MPI.INT], dest=0, tag=22)
    # comm.Send(send_buf, dest=0, tag=22)

print 'process %d receives %s' % (rank, recv_buf)

运行结果如下:

$ mpiexec -n 2 python Send_Recv.py
process 0 receives [0 1 2 3 4 5 6 7 8 9]
process 1 receives [0 1 2 3 4 5 6 7 8 9]

下面再给出一个使用一个预分配的内存缓冲区进行数据接收的例程。

# send_recv_buf.py

import pickle
import numpy as np
from mpi4py import MPI


comm = MPI.COMM_WORLD
rank = comm.Get_rank()

send_obj = np.arange(10, dtype='i')
recv_buf = bytearray(2000) # pre-allocate a buffer for message receiving

if rank == 0:
    comm.send(send_obj, dest=1, tag=11)
elif rank == 1:
    recv_obj = comm.recv(recv_buf, source=0, tag=11)
    # print recv_buf
    print pickle.loads(recv_buf)

    print 'process %d receives %s' % (rank, recv_obj)

运行结果如下:

$ mpiexec -n 2 python send_recv_buf.py
[0 1 2 3 4 5 6 7 8 9]
process 1 receives [0 1 2 3 4 5 6 7 8 9]

上面我们介绍了 mpi4py 中标准阻塞通信模式,在下一篇中我们将介绍缓冲阻塞通信模式。

上一篇中我们介绍了 mpi4py 中标准阻塞通信模式,下面我们将介绍缓冲阻塞通信模式。

缓冲通信模式主要用于解开阻塞通信的发送和接收之间的耦合。有了缓冲机制,即使在接受端没有启动相应的接收的情况下,在完成其消息数据到缓冲区的转移后发送端的阻塞发送函数也可返回。其实标准通信模式中也存在缓冲机制,它使用的是 MPI 环境所提供的数据缓冲区,是有一定大小的。使用缓冲通信模式,我们可以自己分配和组装一块内存区域用作缓冲区,缓冲区的大小可以根据需要进行控制。但需要注意的是,当消息大小超过缓冲区容量时,程序会出错。

下面是 mpi4py 中用于缓冲阻塞点到点通信的方法接口(MPI.Comm 类的方法):

bsend(self, obj, int dest, int tag=0)
recv(self, buf=None, int source=ANY_SOURCE, int tag=ANY_TAG, Status status=None)

Bsend(self, buf, int dest, int tag=0)
Recv(self, buf, int source=ANY_SOURCE, int tag=ANY_TAG, Status status=None)

这些方法调用中的参数是与标准通信模式的方法调用参数一样的。

另外我们会用到的装配和卸载用于通信的缓冲区的函数如下:

MPI.Attach_buffer(buf)
MPI.Detach_buffer()

下面分别给出 bsend/recv 和 Bsend/Recv 的使用例程。

# bsend_recv.py

from mpi4py import MPI


comm = MPI.COMM_WORLD
rank = comm.Get_rank()

# MPI.BSEND_OVERHEAD gives the extra overhead in buffered mode
BUFSISE = 2000 + MPI.BSEND_OVERHEAD
buf = bytearray(BUFSISE)

# Attach a user-provided buffer for sending in buffered mode
MPI.Attach_buffer(buf)

send_obj = {'a': [1, 2.4, 'abc', -2.3+3.4J],
            'b': {2, 3, 4}}

if rank == 0:
    comm.bsend(send_obj, dest=1, tag=11)
    recv_obj = comm.recv(source=1, tag=22)
elif rank == 1:
    recv_obj = comm.recv(source=0, tag=11)
    comm.bsend(send_obj, dest=0, tag=22)

print 'process %d receives %s' % (rank, recv_obj)

# Remove an existing attached buffer
MPI.Detach_buffer()

运行结果如下:

$ mpiexec -n 2 python bsend_recv.py
process 0 receives {'a': [1, 2.4, 'abc', (-2.3+3.4j)], 'b': set([2, 3, 4])}
process 1 receives {'a': [1, 2.4, 'abc', (-2.3+3.4j)], 'b': set([2, 3, 4])}
# Bsend_recv.py

import numpy as np
from mpi4py import MPI


comm = MPI.COMM_WORLD
rank = comm.Get_rank()

# MPI.BSEND_OVERHEAD gives the extra overhead in buffered mode
BUFSISE = 2000 + MPI.BSEND_OVERHEAD
buf = bytearray(BUFSISE)

# Attach a user-provided buffer for sending in buffered mode
MPI.Attach_buffer(buf)

count = 10
send_buf = np.arange(count, dtype='i')
recv_buf = np.empty(count, dtype='i')

if rank == 0:
    comm.Bsend(send_buf, dest=1, tag=11)
    comm.Recv(recv_buf, source=1, tag=22)
elif rank == 1:
    comm.Recv(recv_buf, source=0, tag=11)
    comm.Bsend(send_buf, dest=0, tag=22)

print 'process %d receives %s' % (rank, recv_buf)

# Remove an existing attached buffer
MPI.Detach_buffer()

运行结果如下:

$ mpiexec -n 2 python Bsend_recv.py
process 0 receives [0 1 2 3 4 5 6 7 8 9]
process 1 receives [0 1 2 3 4 5 6 7 8 9]

在以上两个例程中,因为发送的数据量很小,即使不装配一个用于通信的缓冲区,程序一样可以工作(读者可以试一试),这时将使用 MPI 环境提供的缓冲区。但是当通信的数据量很大超过 MPI 环境提供的缓冲区容量时,就必须提供一个足够大的缓冲区以使程序能够正常工作。

可以用下面这个例程测试一下 MPI 环境提供的缓冲区大小。

# attach_detach_buf.py

import numpy as np
from mpi4py import MPI


comm = MPI.COMM_WORLD
rank = comm.Get_rank()

max_msg_size = 2**10
BUFSISE = 32 * max_msg_size
mpi_buf = bytearray(BUFSISE)

# Attach a big user-provided buffer for sending in buffered mode
MPI.Attach_buffer(mpi_buf)

recv_buf = np.empty((max_msg_size,), np.float64)

if rank == 0:
    print '-' * 80
    print 'With an attached big buffer:'
    print

msg_size = 1
tag = 0
while msg_size <= max_msg_size:
    msg = np.random.random((msg_size,))
    if rank == 0:
        print 'Trying with size: ', msg_size

    comm.Bsend(msg, (rank+1)%2, tag)
    comm.Recv(recv_buf, (rank+1)%2, tag)

    if rank == 0:
        print 'Completed with size: ', msg_size

    msg_size *= 2
    tag += 1

# Remove an existing attached buffer
MPI.Detach_buffer()

if rank == 0:
    print
    print '-' * 80
    print 'Without an attached big buffer:'
    print

msg_size = 1
tag = 0
while msg_size <= max_msg_size:
    msg = np.random.random((msg_size,))
    if rank == 0:
        print 'Trying with size: ', msg_size

    comm.Bsend(msg, (rank+1)%2, tag)
    comm.Recv(recv_buf, (rank+1)%2, tag)

    if rank == 0:
        print 'Completed with size: ', msg_size

    msg_size *= 2
    tag += 1

运行结果如下:

$ mpiexec -n 2 python attach_detach_buf.py
--------------------------------------------------------------------------------
With an attached big buffer:

Trying with size:  1
Completed with size:  1
Trying with size:  2
Completed with size:  2
Trying with size:  4
Completed with size:  4
Trying with size:  8
Completed with size:  8
Trying with size:  16
Completed with size:  16
Trying with size:  32
Completed with size:  32
Trying with size:  64
Completed with size:  64
Trying with size:  128
Completed with size:  128
Trying with size:  256
Completed with size:  256
Trying with size:  512
Completed with size:  512
Trying with size:  1024
Completed with size:  1024

--------------------------------------------------------------------------------
Without an attached big buffer:

Trying with size:  1
Completed with size:  1
Trying with size:  2
Completed with size:  2
Trying with size:  4
Completed with size:  4
Trying with size:  8
Traceback (most recent call last):
Completed with size:  8
Trying with size:  16
Completed with size:  16
Trying with size:  32
Completed with size:  32
Trying with size:  64
Completed with size:  64
Trying with size:  128
Completed with size:  128
Trying with size:  256
Completed with size:  256
Trying with size:  512
Traceback (most recent call last):
File "attach_detach_buf.py", line 56, in <module>
File "attach_detach_buf.py", line 56, in <module>
        comm.Bsend(msg, (rank+1)%2, tag)
File "Comm.pyx", line 286, in mpi4py.MPI.Comm.Bsend (src/mpi4py.MPI.c:64922)
comm.Bsend(msg, (rank+1)%2, tag)
mpi4py.MPI.Exception: MPI_ERR_BUFFER: invalid buffer pointer
File "Comm.pyx", line 286, in mpi4py.MPI.Comm.Bsend (src/mpi4py.MPI.c:64922)
mpi4py.MPI.Exception: MPI_ERR_BUFFER: invalid buffer pointer
-------------------------------------------------------
Primary job  terminated normally, but 1 process returned
a non-zero exit code.. Per user-direction, the job has been aborted.
-------------------------------------------------------
--------------------------------------------------------------------------
mpiexec detected that one or more processes exited with non-zero status, thus causing
the job to be terminated. The first process to do so was:

Process name: [[45613,1],0]
Exit code:    1
--------------------------------------------------------------------------

可以看出,当我们提供一个大的缓冲区时就能够成功地收发大的消息,但是当我们卸载掉这个缓冲区后,再发送大的消息时就出错了。

上面我们介绍 mpi4py 中缓冲阻塞通信模式,在下一篇中我们将介绍就绪阻塞通信模式。

上一篇中我们介绍 mpi4py 中缓冲阻塞通信模式,下面我们将介绍就绪阻塞通信模式。

在就绪通信模式下,仅当对方的接收操作启动并准备就绪时,才可发送数据,否则可能导致错误或无法预知的结果。从语义上讲,就绪发送方式与同步和标准发送完全一致,这个动作仅仅是向 MPI 环境传递了一个额外的信息,告诉它对方的接收动作已经“就绪”,不必顾虑,而可直接了当执行相应的发送操作。基于这个信息可避免一系列的缓冲操作以及收/发双方的握手操作,使得 MPI 环境可对通信做更细致的优化以提高通信效率。对发送方而言,这也意味着发送缓冲区在发送函数返回之后即可被安全地用于其它操作。

下面是 mpi4py 中用于就绪阻塞点到点通信的方法接口(MPI.Comm 类的方法),注意:在就绪通信模式中只有只有以大写字母开头的 Rsend,没有以小写字母开头的 rsend。

Rsend(self, buf, int dest, int tag=0)
Recv(self, buf, int source=ANY_SOURCE, int tag=ANY_TAG, Status status=None)

这些方法调用中的参数是与标准通信模式的方法调用参数一样的。

下面分别给出 Rsend/Recv 的使用例程。

# Rsend_Recv.py

import numpy as np
from mpi4py import MPI


comm = MPI.COMM_WORLD
rank = comm.Get_rank()

count = 10
send_buf = np.arange(count, dtype='i')
recv_buf = np.empty(count, dtype='i')

if rank == 0:
    comm.Rsend(send_buf, dest=1, tag=11)
    print 'process %d sends %s' % (rank, send_buf)
elif rank == 1:
    comm.Recv(recv_buf, source=0, tag=11)
    print 'process %d receives %s' % (rank, recv_buf)

运行结果如下:

$ mpiexec -n 2 python Rsend_Recv.py
process 0 sends [0 1 2 3 4 5 6 7 8 9]
process 1 receives [0 1 2 3 4 5 6 7 8 9]

因为没有以小写字母开头的 rsend,所以不能直接地发送通用的 Python 对象,但是我们可以手动地将其 pickle 系列化之后再用 Rsend 发送,如下:

# Rsend_Recv_obj.py

import pickle
from mpi4py import MPI


comm = MPI.COMM_WORLD
rank = comm.Get_rank()

send_obj = {'a': [1, 2.4, 'abc', -2.3+3.4J],
            'b': {2, 3, 4}}

recv_buf = bytearray(2000) # pre-allocate a buffer for message receiving

if rank == 0:
    comm.Rsend(pickle.dumps(send_obj), dest=1, tag=11)
    print 'process %d sends %s' % (rank, send_obj)
elif rank == 1:
    comm.Recv(recv_buf, source=0, tag=11)
    print 'process %d receives %s' % (rank, pickle.loads(recv_buf))

    # or simply use comm.recv
    # recv_obj = comm.recv(source=0, tag=11)
    # print 'process %d receives %s' % (rank, recv_obj)

运行结果如下:

$ mpiexec -n 2 python Rsend_Recv_obj.py
process 0 sends {'a': [1, 2.4, 'abc', (-2.3+3.4j)], 'b': set([2, 3, 4])}
process 1 receives {'a': [1, 2.4, 'abc', (-2.3+3.4j)], 'b': set([2, 3, 4])}

上面两个例程并不能确保通信的安全,如果进程0恰好在进程1执行到接收动作之前即启动了发送动作,则可能会报错,因此在实际应用中,如果采用就绪的通信模式,应该确保对方的接收操作启动并准备就绪后再发送数据。

上面我们介绍 mpi4py 中就绪阻塞通信模式,在下一篇中我们将介绍同步阻塞通信模式。

上一篇中我们介绍了 mpi4py 中同步阻塞通信模式,下面我们将进入到对非阻塞通信模式的介绍。

非阻塞通信将通信和计算进行重叠,在一些情况下可以大大改善性能。特别是在那些具有独立通信控制硬件的系统上,将更能发挥其优势。

非阻塞通信需要通过发送操作的 start 函数启动发送,但并不要求操作立即执行和结束,启动发送操作的 start 调用在 MPI 环境从发送数据区取走数据之前即可返回。然后再在适当时机通过发送操作的 complete 函数来结束通信。在 start 和 complete 之间可并发进行数据传输和计算。

类似地,非阻塞通信的接收操作会发起一个接收的 start 操作,并在其它时机再通过接收的 complete 操作确认接收动作实际完成。再接收的 start 和 complete 函数之间,接收数据和执行计算可并发进行。

与阻塞通信相对应,非阻塞通信也可使用4个模式,即标准、缓冲、同步和就绪,区别在于非阻塞通信的方法在前面加了一个 I/i 前缀,但是语义与阻塞通信的各个模式一一对应。

非阻塞发送可与阻塞接收相匹配,反之,阻塞发送也可与非阻塞接收相匹配。

在非阻塞通信中一般会通过非阻塞通信对象来管理通信动作完成与否的信息。非阻塞通信的发送和接收方法会分别初始化一个发送和接收操作,然后立即返回一个MPI.Request 实例,在程序某个合适的地方可以调用 MPI.Request.Test(),MPI.Request.Wait() 和MPI.Request.Cancel() 来测试、等待或者取消本次通信。如果需要进行多重的测试或等待,可以使用 MPI.Request.Testall(),MPI.Request.Testany(),MPI.Request.Testsome(),MPI.Request.Waitall(),MPI.Request.Waitany(),MPI.Request.Waitsome()方法。

这里我们对非阻塞通信做了一个非常概要的介绍,在下一篇中我们将依次介绍非重复的非阻塞通信的四种通信模式,并给出相应的例程。让我们首先从非重复的标准通信开始



 

发布了64 篇原创文章 · 获赞 264 · 访问量 106万+

猜你喜欢

转载自blog.csdn.net/bbbeoy/article/details/103910156