mpi4py 简介

本文从本人简书博客同步过来

前言

计算机编程语言很多,但是适合高性能数值计算的语言却并不多,在高性能计算的项目中通常会使用到的语言有 Fortran、C、C++ 等,他们是传统的高性能计算机语言,这主要得益于它们的静态编译特性,使得有它们生成的机器代码,在底层上做了很多优化,能够充分发挥硬件的性能,但是这一特性也限制了它们的灵活性和易用性。一些动态的计算机语言在灵活性和易用性方面有着明显的优势,但是由于性能等其他方面的原因却并不适合用来做大规模的数值计算,更别说用到高性能计算领域了。这其中有一个例外,就是 Python 计算机语言。作为一种解释型的动态语言,并且不需要类型声明,Python 非常的简单易用,但是其运行性能却比 Fortran、C、C++ 等常规的高性能数值计算语言要差的多。但是近年来 Python 却在数值计算领域占据了越来越大的份额,甚至在高性能计算领域也看到越来越多 Python 的身影。这是因为 Python 已经不仅仅只是作为一个单独的计算机编程语言,而是变成了一个由庞大的库和工具组成的完整生态系统,在数值计算方面也是如此,包含着有 Numpy、Scipy、Pandas 等构成的科学数值计算栈。这些数值计算库和工具在底层一般封装和调用优秀的由 Fortran、C、C++ 等实现的高效算法库,因此在一定程度上弥补了 Python 本身性能上的不足,却丝毫不损害其灵活性和易用性。但是将 Python 应用到高性能计算上还需要有相应的支持工具,MPI(消息传递接口)就是其中非常重要的一个。本文就将简要介绍 mpi4py,一个构建在 MPI 之上的 Python库,允许在 Python 环境下使用 MPI 接口进行多进程并行甚至分布式的高性能计算。

MPI (Message Passing Interface)

MPI 的全称是 Message Passing Interface,即消息传递接口。它是一种用于编写并行程序的标准,包括协议和和语义说明,他们指明其如何在各种实现中发挥其特性,有 MPICH、OpenMPI 等一些具体的实现,提供 Fortran、C、C++ 的相应编程接口。MPI 的目标是高性能,大规模性,和可移植性。MPI 在今天仍为高性能计算的主要模型。

MPI 的工作方式很好理解,我们可以同时启动一组进程,在同一个通信域中不同的进程都有不同的编号,程序员可以利用 MPI 提供的接口来给不同编号的进程分配不同的任务和帮助进程相互交流最终完成同一个任务。就好比包工头给工人们编上了工号然后指定一个方案来给不同编号的工人分配任务并让工人相互沟通完成任务。

MPI 的具体实现并没有提供 Python 的编程接口,这就使得我们没法直接地使用 Python 调用 MPI 实现高性能的计算,不过幸运的是,我们有 mpi4py。mpi4py 是一个构建在 MPI 之上的 Python 库,主要使用 Cython 编写,它以一种面向对象的方式提供了在 Python 环境下调用 MPI 标准的编程接口,这些接口是构建在 MPI-2 C++ 编程接口的基础之上的,因此和 C++ 的 MPI 编程接口非常类似,了解和有 C、C++ MPI 编程经验的人很容易地上手和使用 mpi4py 编写基于 MPI 的高性能并行计算程序。

mpi4py 简介

mpi4py 是一个构建在 MPI 之上的 Python 库,它使得 Python 的数据结构可以方便的在多进程中传递。

mpi4py 是一个很强大的库,它实现了很多 MPI 标准中的接口,包括点对点通信,集合通信、阻塞/非阻塞通信、组间通信等,基本上能用到的 MPI 接口都有相应的实现。不仅是任何可以被 pickle 的 Python 对象,mpi4py 对具有单段缓冲区接口的 Python 对象如 numpy 数组及内置的 bytes/string/array 等也有很好的支持并且传递效率很高。同时它还提供了 SWIG 和 F2PY 的接口能够将 C/C++ 或者 Fortran 程序在封装成 Python 后仍然能够使用 mpi4py 的对象和接口来进行并行处理。

下面对在 Python 环境中使用 mpi4py 的接口进行并行编程作一个简要的介绍。

Python 对象及数组的传递

mpi4py 可以在不同的进程间传递任何可以被 pickle 系列化的内置和用户自定义 Python 对象,这些对象一般在发送阶段被 pickle 系列化为 ASCII 或二进制格式,然后在接收阶段恢复成对应的 Python 对象。

这种数据传递方式虽然简单通用,却并不高效,特别是在传递大量的数据时。对类似于数组这样的数据,准确来说是具有单段缓冲区接口(single-segment buffer interface)的 Python 对象,如 numpy 数组及内置的 bytes/string/array 等,可以用一种更为高效的方式直接进行传递,而不需要经过 pickle 系列化和恢复。

按照 mpi4py 的惯例,传递可以被 pickle 系列化的通用 Python 对象,可以使用通信子(Comm 类,后面会介绍)对象的以小写字母开头的方法,如 send(),recv(),bcast(),scatter(),gather() 等。但是如果要以更高效的方式传递具有单段缓冲区接口的 Python 对象,如 numpy 数组,则只能使用通信子对象的以大写字母开头的方法,如 Send(),Recv(),Bcast(),Scatter(),Gather() 等。

MPI 环境管理

mpi4py 提供了相应的接口 MPI.Init(),MPI.Init_thread() 和 MPI.Finalize() 来初始化和结束 MPI 环境。但是 mpi4py 通过在 init.py 中写入了初始化的操作,因此在我们 from mpi4py import MPI 的时候就已经自动初始化了 MPI 环境。

MPI_Finalize() 被注册到了 Python 的 C 接口 Py_AtExit(),这样在 Python 进程结束时候就会自动调用 MPI_Finalize(), 因此不再需要我们显式的去调用。

通信子(Communicator)

mpi4py 提供了相应的通信子的 Python 类,其中 MPI.Comm 是通信子的基类,在它下面继承了 MPI.Intracomm 和 MPI.Intercomm 两个子类,这跟 MPI 的 C++ 实现中是相同的。下图是通信子类的继承关系。

同时它也提供了两个预定义的通信子对象:

  • 包含所有进程的 MPI.COMM_WORLD;

  • 只包含调用进程本身的 MPI.COMM_SELF。

可以由它们创建其它新的通信子。

可以通过通信子所定义的一些方法获取当前进程号、获取通信域内的进程数、获取进程组、对进程组进行集合运算、分割合并等等。

关于通信子与进程组的操作在后面会进行详细的介绍。

点到点通信

点到点通信是消息传递系统最基本的功能,mpi4py 的点到点通信使得数据可以在一对进程之间互相传递,一端发送,一端接收。

mpi4py 中提供了一系列发送和接收函数用以在进程间传递带有 tag 的类型数据。数据的类型信息允许传递的数据在不同的计算机架构下进行相应的转换,同时也使得不经过 pickle 而高效地传递不连续的数据和用户自定义的数据变得可能。tag 使得接收端可以有选择性地进行消息的接收。

mpi4py 中的点到点通信包括阻塞、非阻塞和持续通信。下面分别对其做简要的介绍。

阻塞通信

mpi4py 中最基本的发送和接收函数是阻塞式的,这些函数会阻塞程序的执行直到参与通信的数据缓冲区可以安全地被其它程序重用。MPI.Comm.Send(), MPI.Comm.Recv(),MPI.Comm.Sendrecv() 及其对应的 MPI.Comm.send(),MPI.Comm.recv(),MPI.Comm.sendrecv() 提供对阻塞通信的支持。

非阻塞通信

所有的阻塞通信函数都提供了一个对应的非阻塞的版本,这些非阻塞通信函数允许计算和通信的同时执行,这在一些情况下可以极大地提高计算性能。mpi4py 中的 MPI.Comm.Isend() 和 MPI.Comm.Irecv() 会分别初始化一个发送和接收操作,然后立即返回一个 MPI.Request 实例,在程序某个合适的地方可以调用 MPI.Request.Test(),MPI.Request.Wait() 和MPI.Request.Cancel() 来测试、等待或者取消本次通信。

下面看看阻塞通信与非阻塞通信的对比:

非阻塞通信的消息发送和接受:

持续通信

在某些情况下可能需要持续不断地发送和接收一些数据,这时可以用持续通信的方法来提高通信效率。持续通信是一种特殊的非阻塞通信,它能够有效地减小进程和通信控制之间的相应开销。在 mpi4py 中可以用 MPI.Comm.Send_init() 和 MPI.Comm.Recv_init() 来创建一个持续发送和接收的操作请求,它们会返回一个 MPI.Prequest 类的实例,然后可以用 MPI.Prequest.Start() 方法发起实际的通信操作。

集合通信

集合通信允许在一个通信组内的多个进程之间同时传递数据,集合通信的语法和语意与点到点通信是一致的,不过集合通信只有阻塞版本。

经常用到的集合通信操作有以下这些:

  • 障同步(Barrier synchronization)操作;

  • 全局通信,包括广播(Broadcast),收集(Gather)和发散(Scatter);

  • 全局规约,包括求和,最大,最小等。

MPI 组通信和点到点通信的一个重要区别就是,在某个进程组内所有的进程同时参加通信,mpi4py 提供了方便的接口让我们完成 Python 中的组内集合通信,方便编程同时提高程序的可读性和可移植性。

下面简要地介绍几个集合通信的操作原理:

广播(Broadcast)

广播操作是典型的一对多通信,将根进程的数据复制到同组内其他所有进程中。

发散(Scatter)

与广播不同,发散可以向不同的进程发送不同的数据,而不是完全复制。

收集(Gather)

收集过程是发散过程的逆过程,每个进程将发送缓冲区的消息发送给根进程,根进程根据发送进程的进程号将各自的消息存放到自己的消息缓冲区中。

动态进程管理

在 MPI-1 规范中,一个 MPI 应用程序是静态的。一旦一个 MPI 程序运行起来后,就既不能增加也不能删除进程。MPI-2 规范没有了这些限制,它增加了一个进程管理模型,可以实现动态的进程管理,使得我们可以在 MPI 程序运行过程中创建新的进程或者与其它运行中的 MPI 程序建立通信连接。

在 mpi4py 中,可以调用 MPI.Intracomm.Spawn() 方法在一个组内通信子内创建一个新的独立的通信组,它会返回一个组间通信子(一个 MPI.Intercomm 类实例)。这个新创建的通信子(称作子通信子)可以通过调用 MPI.Comm.Get_parent() 来获得创建它的通信子,并与其进行组间的点到点或集合通信。

单边通信

单边通信,也称作远程内存访问(Remote Memory Access, RMA),这是对传统的双边,基于发送/接收(send/receive)的 MPI 通信模型的一个补充,单边通信是一种基于单边的推送/获取(put/get)MPI 通信模型。

在 mpi4py 中,可以通过 MPI.Win 类的实例实现单边通信。在一个通信子内调用MPI.Win.Create() 就可以创建一个 window 对象,这个 window 对象中包含着一个进程可被用于远程读写的内存缓冲区。调用 MPI.Win.Free() 可以释放掉一个不再需要的 window 对象。MPI.Win.Put(), MPI.Win.Get() 和 MPI.Win.Accumulate() 可以分别实现单边的远程读、写和规约操作。

并行 I/O

MPI 支持并行 I/O 操作。在 mpi4py 中所有的 MPI 并行操作都是通过 MPI.File 类实例完成的。所有 MPI-2 中定义的30个用作并行 I/O 操作的方法在 mpi4py 中都有对应的实现。

总结

本文简单介绍了可以使我们在 Python 环境下进行多进程的并行编程的软件库 mpi4py,MPI 的接口非常庞大,相应的 mpi4py 也非常庞大,给我们提供了非常强大而灵活的并行编程能力。mpi4py 还有实现了相应的 SWIG 和 F2PY 的封装文件和类型映射,能够帮助我们将 Python 同真正的 C/C++ 以及 Fortran 程序在消息传递上实现统一。

在下一篇中我们将介绍如何安装和使用 mpi4py。

参考

MPI for Python documentation

MPI Tutorial

《高性能计算并行编程技术-MPI并行程序设计》

《MPI并行程序设计实例教程》

你可能感兴趣的:(Python,并行计算,Python,并行计算)