前言
在高性能計算的項目中我們通常都會使用效率更高的編譯型的語言例如C、C++、Fortran等,但是由于Python的靈活性和易用性使得它在發展和驗證算法方面備受人們的青睐于是在高性能計算領域也經常能看到Python的身影了。本文簡單介紹在Python環境下使用MPI接口在叢集上進行多程序并行計算的方法。
MPI(Message Passing Interface)
這裡我先對MPI進行一下簡單的介紹,MPI的全稱是Message Passing Interface,即消息傳遞接口。
它并不是一門語言,而是一個庫,我們可以用Fortran、C、C++結合MPI提供的接口來将串行的程式進行并行化處理,也可以認為Fortran+MPI或者C+MPI是一種再原來串
行語言的基礎上擴充出來的并行語言。
它是一種标準而不是特定的實作,具體的可以有很多不同的實作,例如MPICH、OpenMPI等。
它是一種消息傳遞程式設計模型,顧名思義,它就是專門服務于程序間通信的。
MPI的工作方式很好了解,我們可以同時啟動一組程序,在同一個通信域中不同的程序都有不同的編号,程式員可以利用MPI提供的接口來給不同編号的程序配置設定不同的任務和幫助程序互相交流最終完成同一個任務。就好比包工頭給勞工們編上了工号然後指定一個方案來給不同編号的勞工配置設定任務并讓勞工互相溝通完成任務。
Python中的并行
由于CPython中的GIL的存在我們可以暫時不奢望能在CPython中使用多線程利用多核資源進行并行計算了,是以我們在Python中可以利用多程序的方式充分利用多核資源。
與多線程的共享式記憶體不同,由于各個程序都是互相獨立的,是以程序間通信再多程序中扮演這非常重要的角色,Python中我們可以使用multiprocessing子產品中的<code>pipe</code>、<code>queue</code>、<code>Array</code>、<code>Value</code>等等工具來實作程序間通訊和資料共享,但是在編寫起來仍然具有很大的不靈活性。而這一方面正是MPI所擅長的領域,是以如果能夠在Python中調用MPI的接口那真是太完美了不是麼。
MPI與mpi4py
mpi4py是一個建構在MPI之上的Python庫,主要使用Cython編寫。mpi4py使得Python的資料結構可以友善的在多程序中傳遞。
mpi4py是一個很強大的庫,它實作了很多MPI标準中的接口,包括點對點通信,組内集合通信、非阻塞通信、重複非阻塞通信、組間通信等,基本上我能想到用到的MPI接口mpi4py中都有相應的實作。不僅是Python對象,mpi4py對numpy也有很好的支援并且傳遞效率很高。同時它還提供了SWIG和F2PY的接口能夠讓我們将自己的Fortran或者C/C++程式在封裝成Python後仍然能夠使用mpi4py的對象和接口來進行并行處理。可見mpi4py的作者的功力的确是非常了得。
mpi4py
這裡我開始對在Python環境中使用mpi4py的接口進行并行程式設計進行介紹。
MPI環境管理
mpi4py提供了相應的接口<code>Init()</code>和<code>Finalize()</code>來初始化和結束mpi環境。但是mpi4py通過在<code>__init__.py</code>中寫入了初始化的操作,是以在我們<code>from mpi4py import MPI</code>的時候就已經自動初始化mpi環境。
<code>MPI_Finalize()</code>被注冊到了Python的C接口<code>Py_AtExit()</code>,這樣在Python程序結束時候就會自動調用<code>MPI_Finalize()</code>, 是以不再需要我們顯式的去掉用<code>Finalize()</code>。
通信域(Communicator)
mpi4py直接提供了相應的通信域的Python類,其中<code>Comm</code>是通信域的基類,<code>Intracomm</code>和<code>Intercomm</code>是其派生類,這根MPI的C++實作中是相同的。
同時它也提供了兩個預定義的通信域對象:
1. 包含所有程序的<code>COMM_WORLD</code>
2. 隻包含調用程序本身的<code>COMM_SELF</code>
通信域對象則提供了與通信域相關的接口,例如擷取目前程序号、擷取通信域内的程序數、擷取程序組、對程序組進行集合運算、分割合并等等。
點對點通信
mpi4py提供了點對點通信的接口使得多個程序間能夠互相傳遞Python的内置對象(基于pickle序列化),同時也提供了直接的數組傳遞(numpy數組,接近C語言的效率)。
如果我們需要傳遞通用的Python對象,則需要使用通信域對象的方法中小寫的接口,例如<code>send()</code>,<code>recv()</code>,<code>isend()</code>等。
如果需要直接傳遞資料對象,則需要調用大寫的接口,例如<code>Send()</code>,<code>Recv()</code>,<code>Isend()</code>等,這與C++接口中的拼寫是一樣的。
MPI中的點到點通信有很多中,其中包括标準通信,緩存通信,同步通信和就緒通信,同時上面這些通信又有非阻塞的異步版本等等。這些在mpi4py中都有相應的Python版本的接口來讓我們更靈活的處理程序間通信。這裡我隻用标準通信的阻塞和非阻塞版本來做個舉例:
阻塞标準通信
這裡我嘗試使用mpi4py的接口在兩個程序中傳遞Python list對象。
執行效果:
非阻塞标準通信
所有的阻塞通信mpi都提供了一個非阻塞的版本,類似與我們編寫異步程式不阻塞在耗時的IO上是一樣的,MPI的非阻塞通信也不會阻塞消息的傳遞過程中,這樣能夠充分利用處理器資源提升整個程式的效率。
來張圖看看阻塞通信與非阻塞通信的對比:
非阻塞通信的消息發送和接受:
同樣的,我們也可以寫一個上面例子的非阻塞版本。
例如我想傳遞長度為10的int數組,MPI的C++接口是:
在mpi4py的接口中也及其類似, Comm.Send()中需要接收一個Python list作為參數,其中包含所傳資料的位址,長度和類型。
來個阻塞标準通信的例子:
組通信
MPI組通信和點到點通信的一個重要差別就是,在某個程序組内所有的程序同時參加通信,mpi4py提供了友善的接口讓我們完成Python中的組内集合通信,友善程式設計同時提高程式的可讀性和可移植性。
下面就幾個常用的集合通信來小試牛刀吧。
廣播
廣播操作是典型的一對多通信,将跟程序的資料複制到同組内其他所有程序中。
在Python中我想将一個清單廣播到其他程序中:
執行結果:
發散
與廣播不同,發散可以向不同的程序發送不同的資料,而不是完全複制。
例如我想将0-9發送到不同的程序中:
發散結果:
收集
收集過程是發散過程的逆過程,每個程序将發送緩沖區的消息發送給根程序,根程序根據發送程序的程序号将各自的消息存放到自己的消息緩沖區中。
收集結果:
其他的組内通信還有歸約操作等等由于篇幅限制就不多講了,有興趣的可以去看看MPI的官方文檔和相應的教材。
mpi4py并行程式設計實踐
這裡我就上篇中的二重循環繪制map的例子來使用mpi4py進行并行加速處理。
我打算同時啟動10個程序來将每個0軸需要計算和繪制的資料發送到不同的程序進行并行計算。
是以我需要将<code>pO2s</code>數組發散到10個程序中:
之後我需要在每個程序中根據接受到的<code>pO2s</code>的資料再進行一次<code>pCOs</code>循環來進行計算。
最終将每個程序計算的結果(TOF)進行收集操作:
由于代碼都是涉及的專業相關的東西我就不全列出來了,将mpi4py改過的并行版本放到10個程序中執行可見:
效率提升了10倍左右。
總結
本文簡單介紹了mpi4py的接口在python中進行多程序程式設計的方法,MPI的接口非常龐大,相應的mpi4py也非常龐大,mpi4py還有實作了相應的SWIG和F2PY的封裝檔案和類型映射,能夠幫助我們将Python同真正的C/C++以及Fortran程式在消息傳遞上實作統一。有興趣的同學可以進一步研究一下,歡迎交流。
原文釋出時間為:2017-02-23
本文作者:PytLab