mpi4py多進程實例/舉例

前言:

看了那麼多關於mpi4py使用的,卻沒見到一個能夠舉例在實際情況中的使用,筆者也是初學者,於是花了一整個下午來找例子並詳細解答,希望能幫助想用mpi4py的後來者

提醒:這裏不討論如何使用mpi4py裏面的函數,只舉例mpi4py在實際中的應用
關於mpi4py的函數,可以見官網(英文)以及一些博客或者知乎

以下是例子,有時間會隨時更新更多例子…

例1,計算π\pi

例1,計算π\pi 參考自Parallel programming in Python: mpi4py
這裏採用的計算π\pi的式子是(維基-pi):以防有人不能進維基百科,截圖如下:
在這裏插入圖片描述
如果我們只是爲了知道π\pi的值,可以調用積分包計算即可,但是我們的目的是要多進程並行計算這個積分,所以不調用包。對於單進程而言,我們可以使用求和代替積分:
π=0141+x2dx0141+x2Δx\pi=\int_0^1\frac{4}{1+x^2}dx\simeq\sum_0^1\frac{4}{1+x^2}\Delta x
在解釋代碼之前,筆者需要提到的是,使用numpy的程序通過數組計算也是非常快的

import numpy as np 
import time

def pi_comput(step):
    partial_pi = 0
    dx = 1/step
    for i in np.arange(0,1,dx):
        partial_pi += 4/(1+i*i)*dx
    return partial_pi


t0 = time.time()
print('pi is :>>> ',pi_comput(10000000))
t1 = time.time()
print('time cost: %s sec'%(t1-t0))

在這裏插入圖片描述
可以看到當我們取10000000步長時,已經可以比較準確的計算出pi了,但是時間卻花了19秒,我們的目標是減少時間使用
上面的公因子dx可以提前(下文也會提到),將函數改爲:

def pi_comput(step):
    partial_pi = 0
    dx = 1/step
    for i in np.arange(0, 1, dx):
        partial_pi += 4/(1+i*i)
    return partial_pi*dx

這樣時間減到了16秒
(注:像這種簡單的問題,我們實際上藉助numpy的數組計算是非常快的,只花了0.1376秒,比多進程還快,這裏使用6個進程花了0.938秒;具體可以參考筆者另一博文3.2節
以下代碼保留原作者的英文註釋,筆者加入中文註釋

from mpi4py import MPI
import time
import math

t0 = time.time()

comm = MPI.COMM_WORLD
rank = comm.Get_rank()
nprocs = comm.Get_size()
#上面都是mpi4py多進程的標準輸入,每次開頭如此輸入即可
# number of integration steps
nsteps = 10000000
# step size
dx = 1.0 / nsteps

if rank == 0:
    # determine the size of each sub-task
    # 這裏divmod可以同時得到商和餘數,如divmod(10,3)得到3和1
    ave, res = divmod(nsteps, nprocs)
    #counts得到的是每個進程計算的數量個數,如第一個進程算前1000個,第二個進程算1000以後
    counts = [ave + 1 if p < res else ave for p in range(nprocs)]

    # determine the starting and ending indices of each sub-task
    starts = [sum(counts[:p]) for p in range(nprocs)]
    ends = [sum(counts[:p+1]) for p in range(nprocs)]

    # save the starting and ending indices in data
    data = [(starts[p], ends[p]) for p in range(nprocs)]
else:
    data = None

data = comm.scatter(data, root=0)

# compute partial contribution to pi on each process
partial_pi = 0.0
for i in range(data[0], data[1]):
    x = (i + 0.5) * dx
    partial_pi += 4.0 / (1.0 + x * x)
partial_pi *= dx

partial_pi = comm.gather(partial_pi, root=0)

if rank == 0:
    print('pi is :>>> ',sum(partial_pi))   筆者加的
    print('pi computed in {:.3f} sec'.format(time.time() - t0))
    print('error is {}'.format(abs(sum(partial_pi) - math.pi)))

分步解釋
1)前期處理

if rank == 0:
    ave, res = divmod(nsteps, nprocs)  # ave=16,res=4
    # 
    counts = [ave + 1 if p < res else ave for p in range(nprocs)]

    # determine the starting and ending indices of each sub-task
    starts = [sum(counts[:p]) for p in range(nprocs)]
    ends = [sum(counts[:p+1]) for p in range(nprocs)]

    # save the starting and ending indices in data
    data = [(starts[p], ends[p]) for p in range(nprocs)]
else:
    data = None   

這裏以步長爲100,進程數爲6來說明。
則上面的

  • nsteps = 100
  • nprocs = 6
  • dx = 0.01

則在第一個if語句中計算得到:

  • ave = 16
  • res = 4

p in range(6)即表示p=array([0,1,2,3,4,5]), p是一個數組,當p<res時,counts=ave+1否則counts=ave;則可以得到counts爲
在這裏插入圖片描述
表示前4個進程計算17個步長數,後兩個進程計算16個步長數
同理可以得到starts,ends的值
在這裏插入圖片描述
分別表示進程進行的開始和結束位置。現在將開始和結束位置表示在一個元組裏面,即data的值
在這裏插入圖片描述
上面可以作爲多進程運算的模板,以後計算進程的開始和結束位置時可用。
到此我們已經結束了當rank=0時的表述,我們輸入進程個數(即mpirun -np 6 python xxx.py裏面的6)時,首先進行的是rank=0。

2)分工計算
這裏只需要一句代碼即可,即使用scatter來分發數據,分到6個進程中

data = comm.scatter(data, root=0) #root=0表示從rank=0的進程來分發

3)計算π\pi
下面的partial_pi指的是被積函數

partial_pi = 0.0  初始化partial_pi=0
for i in range(data[0], data[1]):
    x = (i + 0.5) * dx
    partial_pi += 4.0 / (1.0 + x * x)
partial_pi *= dx

在scatter分發完後,每個進程有一個計算區間,如第一個進程的區間是(0,17),則data[0]=0,data[1]=17;
所以對於第一個進程,上面爲:

for i in range(0,17) 

關於

x = (i+0.5)*dx

這一句實際上應該是x=i*dx,作者加了0.5是因爲python裏面的range不包括最後一個數,所以作者自己加了0.5這個數,需要記住的是,這個數很小,所以關係不大,也可以直接用x=i*dx這樣比較直觀。
最後一句作者寫的是:

partial_pi *= dx

這是因爲公因子可以提前,在筆者另一博文3.2節也提到過。
於是上面的6個進程分別計算對應的區間,得到6個結果
4)合併結果

partial_pi = comm.gather(partial_pi, root=0)

每個進程分工計算完成之後,得到了6個結果,現在要將結果合併,使用gather函數,這個函數合併之後的結果是一個列表,比如這6個進程的結果分別是a,b,c,d,e,f,g,那麼gather之後是

[a,b,c,d,e,f,g]

因此對partial_pi合併,並放到root=0即第一個進程中,所以在後面if語句輸出時,只要找到rank=0即可輸出;
最後需要提醒的是,由於我們得到的是列表,而最後pi是一個值,我們需要用sum函數將這個列表求和。

if rank == 0:
    print('pi is :>>> ',sum(partial_pi))
    print('pi computed in {:.3f} sec'.format(time.time() - t0))
    print('error is {}'.format(abs(sum(partial_pi) - math.pi)))

5)結果
使用6個進程計算的結果是0.94秒(注:不同計算機的運行時間會有一定偏差)
在這裏插入圖片描述
如前往所說,這個計算結果趕不上使用數值計算快。

那麼是否有更快的方法?答案是肯定的,因爲我們使用多進程提高了運算速度,使用numpy又提高了運算速度,那最好的辦法就是將兩者同時使用。

6)進一步提升
我們借鑑筆者另一博文3.2節(建議看一看),將for循環使用數組來代替,則下面的代碼:

for i in range(data[0], data[1]):
    x = (i + 0.5) * dx
    partial_pi += 4.0 / (1.0 + x * x)
partial_pi *= dx

改爲:

x = np.arange(data[0],data[1])*dx
partial_pi = np.sum(4/1+x*x)*dx

注意在開始要import numpy;另外這裏我不使用作者的+0.5處理;運行時間降到了0.052!!!
在這裏插入圖片描述
這是我們最後的程序:

from mpi4py import MPI
import time
import math
import numpy as np

t0 = time.time()

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

# number of integration steps
nsteps = 10000000
# step size
dx = 1.0 / nsteps

if rank == 0:
    # determine the size of each sub-task
    ave, res = divmod(nsteps, nprocs)
    counts = [ave + 1 if p < res else ave for p in range(nprocs)]

    # determine the starting and ending indices of each sub-task
    starts = [sum(counts[:p]) for p in range(nprocs)]
    ends = [sum(counts[:p+1]) for p in range(nprocs)]

    # save the starting and ending indices in data
    data = [(starts[p], ends[p]) for p in range(nprocs)]

else:
    data = None

data = comm.scatter(data, root=0)
# compute partial contribution to pi on each process
partial_pi = 0.0
x = np.arange(data[0],data[1])*dx
partial_pi = np.sum(4/(1+x*x))*dx
partial_pi = comm.gather(partial_pi, root=0)

if rank == 0:
    print('pi is :>>> ',sum(partial_pi))
    print('pi computed in {:.3f} sec'.format(time.time() - t0))
    print('error is {}'.format(abs(sum(partial_pi) - math.pi)))

7)總結
我相信詳細看完了博文的朋友收穫應該是比較大的。

  • 這裏我們使用for來求pi的值,花了19秒,然後在此基礎上將公因子提出來改進代碼,之後運行花了16秒
  • 我們採用了多進程的方法之後,將時間縮短到了0.94秒(巨大進步)
  • 我們採用numpy數組的方法(另一博文3.2節)花了0.165秒,在此基礎上將公因子提出來改進代碼,之後運行花了0.138秒(numpy的進步也是巨大的)
  • 最後我們結合多進程和numpy同時使用,改進代碼之後花了0.052秒(提高了365倍!!)

8) appendix
上面我們給每個進程分區間的時候基本是等比例分的,而現實中會出現很多不等比例分的情況,比如當x比較小的時候運算很快,但是x比較大的時候運算很慢,這個時候我們希望將x比較大的部分多分一些給多個進程。
從上面的例子我們可以看到,實際上就是data中的數組自己定義即可,比如將上面的程序中的if部分改爲:
即將:

if rank == 0:
    # determine the size of each sub-task
    # 這裏divmod可以同時得到商和餘數,如divmod(10,3)得到3和1
    ave, res = divmod(nsteps, nprocs)
    #counts得到的是每個進程計算的數量個數,如第一個進程算前1000個,第二個進程算1000以後
    counts = [ave + 1 if p < res else ave for p in range(nprocs)]

    # determine the starting and ending indices of each sub-task
    starts = [sum(counts[:p]) for p in range(nprocs)]
    ends = [sum(counts[:p+1]) for p in range(nprocs)]

    # save the starting and ending indices in data
    data = [(starts[p], ends[p]) for p in range(nprocs)]
else:
    data = None

改爲

if rank == 0:
    data = [(0,round(1/10*nsteps)),(round(1/10*nsteps),round(3/10*nsteps)),(round(3/10*nsteps),nsteps)]
else:
    data = None

或者直接爲

if rank == 0:
    data = [(0,10),(10,30),(30,100)]
else:
    data = None

在這裏插入圖片描述
此時我們運行的時候-np後面只能接3,表示3個進程,因爲我們分成了3個,如果要將進程變成一個變量,則需要將nprocs引進來。

比如對於\ell從lmin到lmax,要求對進程每次任務對半分。比如有5個進程,\ell爲100,第一個進程處理到一半即從lmin到50,第二個進程處理到剩下的一半,即從50到75,第三個進程又處理到剩下的一半,即從75到88(四捨五入),…如此分法,一直到結束lmax,相當於是(12)n(\frac{1}{2})^n
下面這個函數可以實現這個功能(臨時寫的,肯定有更好的寫法)

import numpy as np 
import copy

lmin = 2
lmax = 100
nprocs = 5

def binary_split(nprocs):
    frac = 0
    end = []
    for i in range(1, nprocs):
        frac += 1/np.power(2, i)
        end.append(frac)
    end = list(np.round(np.array(end)*lmax))
    end.append(lmax)
    end = [int(x) for x in end]
    start = copy.copy(end)
    start.insert(0,lmin)
    start.pop(-1)
    data = [(start[i],end[i]) for i in range(nprocs)]
    return start,end,data
print(binary_split(nprocs))

在這裏插入圖片描述

發佈了229 篇原創文章 · 獲贊 273 · 訪問量 80萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章