簡體   English   中英

使用MPI獲得性能提升

[英]Performance gain using MPI

我測試了並行化(幾乎)“令人難以置信的並行”(即完全可並行化)算法的性能增益,該算法總結了前N整數:

串行算法很簡單:

N = 100000000
print sum(range(N))

我的雙核筆記本電腦(聯想X200)的執行時間:0m21.111s。

並行化(帶mpi4py)版本使用3個節點; 節點0計算整數的下半部分之和,節點1計算上半部分的總和。 兩者都將結果(通過comm.send )發送到節點2,節點2匯總兩個數字並打印結果:

from mpi4py import MPI

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

N = 100000000

if rank == 0: 
  s = sum(range(N/2))
  comm.send(s,dest=2,tag=11)
elif rank == 1:
  s = sum(range(N/2+1,N))
  comm.send(s,dest=2,tag=11)
elif rank == 2:
  s1 = comm.recv(source=0, tag=11)
  s2 = comm.recv(source=1, tag=11)
  print s1+s2

我的雙核筆記本電腦的兩個核心都被充分利用; 執行時間現在:15.746s。

我的問題:至少在理論上,執行時間幾乎應該減半。 哪個開銷吃了4秒? (當然不是s1 + s2)。 那些發送/接收命令是否耗時?

編輯:在閱讀了答案並重新思考問題之后,我認為4秒(在某些運行中甚至更多)被生成兩個長度為50000000的列表所導致的高內存流量所吞噬; 我的筆記本電腦的兩個核心共享一個公共內存(至少是主內存;我認為它們有獨立的L2緩存),而這正是瓶頸:因此,兩個內核通常都希望同時訪問內存(以獲取內存)下一個列表元素),其中一個必須等​​待...

如果我使用xrange而不是range ,則會延遲生成下一個列表元素並分配很少的內存。 我測試了它並運行與上面相同的程序,xrange僅需11秒!

你是如何做時間的,你的筆記本電腦是什么?

如果你正在從shell進行計時,你可能(正如BiggAl建議的那樣)在啟動python時遇到延遲。 這是真正的開銷,值得了解,但可能不是您的直接關注。 而且我在成像方面遇到了麻煩,這會導致4秒的開銷... [ 編輯補充說 :雖然BiggAl建議它真的可能是,在Windows下]

我認為更可能的問題是內存帶寬限制。 雖然您將通過此設置完全使用兩個內核,但您只有很多內存帶寬,這可能最終成為限制。 每個核心都試圖寫入大量數據(范圍(N / 2)),然后讀入(總和)以進行相當適度的計算量(整數),因此我懷疑計算不是瓶頸。

我在Nehalem盒子上使用timeit運行相同的設置,每個核心具有相當不錯的內存帶寬,並且確實獲得了預期的加速:

from mpi4py import MPI
import timeit

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

N = 10000000

def parSum():
    if rank == 0:
        ...etc

def serSum():
    s = sum(range(N))

if rank == 0:
    print 'Parallel time:'
    tp = timeit.Timer("parSum()","from __main__ import parSum")
    print tp.timeit(number=10)

    print 'Serial time:'
    ts = timeit.Timer("serSum()","from __main__ import serSum")
    print ts.timeit(number=10)

我得到了

$ mpirun -np 3 python ./sum.py
Parallel time:
1.91955494881
Serial time:
3.84715008736

如果你認為這是一個內存帶寬問題,你可以通過人為地計算計算來測試它; 比如使用numpy和做更復雜的范圍函數的sum(numpy.sin(range(N/2+1,N)))sum(numpy.sin(range(N/2+1,N))) 這應該傾斜從內存訪問到計算的平衡。

在下文中,我假設您使用的是Python 2.x.

根據筆記本電腦的硬件規格,進程0和1之間可能存在大量內存爭用。

range(100000000/2)創建一個列表,在我的PC上占用1.5GB的RAM,所以你在兩個進程之間看到3GB的RAM。 使用兩個內核迭代這兩個列表可能會導致內存帶寬問題(和/或交換)。 這是不完美並行化的最可能原因。

使用xrange而不是range不會生成列表,並且應該通過使計算CPU綁定來更好地並行化。

順便說一下,你的代碼中有一個錯誤:第二個(x)range應該從N/2開始,而不是N/2+1

我的問題:至少在理論上,執行時間幾乎應該減半。 哪個開銷吃了4秒?

一些想法:

  • 你在使用python 2嗎? 如果是這樣,請使用xrange因為它創建了一個生成器/迭代器對象。 它可以節省幾毫秒,因為range將創建一個它不斷添加的完全成熟的字典,而xrange不會。 如果使用python 3, range默認會創建一個迭代器。 可能這在實踐中不會為你節省很多時間/內存,但是python開發人員顯然認為將一切都作為生成器實現是值得的,因為這是python 3中的重大事項之一。
  • 從理論上講,算法位應該快2倍。 在實踐中,它比這更復雜。 在算法開始時設置線程或進程需要花費一些成本,這會增加運行時間; 最后,在結束時同步結果是有代價的(等待連接)。 所以2倍的速度增長永遠不會實現。 對於任何算法的小值,眾所周知,串行算法優於線程對應物; 只有當你達到一個數量級時,線程創建的成本與你要注意到的天文速度增加的工作相比可以忽略不計。
  • 平衡工作可能是一個問題。 在32位系統上,可以放入寄存器的最大數字大小(因此在給定數字大小的情況下為O(1)進行添加)是4294967296(2 ^ 32)。 你的總和,在大的值,是4999999950000000.Bignum加法是你需要的肢數(數組中的元素)的O(n),所以你開始使用bignums而不是任何你可以達到減速處理單個內​​存地址。

     y = 0 for x in xrange(1, 100000000): if (x+y) > 2**32: print "X is " + str(x) print "y is " + str(y) break else: y += x 

    這表明你在N中的n加入開始變得更加昂貴。 我會嘗試計算總和達到該值以及從那里到N的值之和,然后調整工作隊列,以便在適當的時間進行分割。

    當然,在64位系統上你不應該注意到這個問題,因為2 ^ 64比你的總和大, 除非 python內部不使用uint64_t 我原以為是的。

請閱讀這個阿姆達爾定律

您的操作系統包含大量不可並行化的瓶頸。 您的語言庫也可能存在一些瓶頸。

有趣的是,您的英特爾硬件的內存寫入順序也可能有一些不可並行化的瓶頸。

負載均衡是一種理論,也會有明顯的通信延遲,但我不希望其中任何一種,即使是組合,也會產生很大的性能損失。 我猜你最大的開銷就是啟動另外兩個python解釋器實例。 希望如果您嘗試使用更大的數字,您應該會發現開銷實際上並不與N成比例,但實際上是一個大的常量加上一個依賴於N的術語。因此,您可能希望停止算法與數字並行低於性能提高的一些量。

我並不熟悉mpi,但是你可能最好在應用程序啟動時創建一個工作池,讓他們等待任務,而不是動態創建它們。 這需要更復雜的設計,但每次應用程序運行只會產生一次解釋器初始化懲罰。

我寫了一些代碼來測試mpi基礎設施的哪些部分占用時間。 此版本的代碼可以使用從1到批次和批次的不同數量的核心。 工作在核心之間平均分配,並發送回主機0到總數。 主機0也可以工作。

import time

t = time.time()
import pypar
print 'pypar init time', time.time()-t, 'seconds'

rank = pypar.rank()
hosts = pypar.size()

N = 100000000

nStart = (N/hosts) * rank
if rank==hosts-1:
    nStop = N
else:
    nStop = ( ((N/hosts) * (rank+1)) )
print rank, 'working on', nStart, 'to', nStop

t = time.time()
s = sum(xrange(nStart,nStop))
if rank == 0:
    for p in range(1,hosts):
        s += pypar.receive(p)
        pypar.send(s,p) 
else:
    pypar.send(s,0) 
    s = pypar.receive(0)
if rank==0:
    print rank, 'total', s, 'in', time.time()-t, 'seconds'
pypar.Finalize()

結果:

pypar init time 1.68600010872 seconds
1 working on 12500000 to 25000000
pypar init time 1.80400013924 seconds
2 working on 25000000 to 37500000
pypar init time 1.98699998856 seconds
3 working on 37500000 to 50000000
pypar init time 2.16499996185 seconds
4 working on 50000000 to 62500000
Pypar (version 2.1.4.7) initialised MPI OK with 8 processors
pypar init time 1.5720000267 seconds
0 working on 0 to 12500000
0 total 4999999950000000 in 1.40100002289 seconds
pypar init time 2.34000015259 seconds
6 working on 75000000 to 87500000
pypar init time 2.64600014687 seconds
7 working on 87500000 to 100000000
pypar init time 2.23900008202 seconds
5 working on 62500000 to 75000000

啟動pypar和mpi庫大約需要2.5秒。 然后實際工作需要1.4秒,以計算並與主機0通信。作為單核運行大約需要11秒。 所以使用8核可以很好地擴展。

啟動mpiexec和python幾乎沒有時間。 正如這個可悲的測試所示:

c:\Data\python speed testing>time  0<enter.txt
The current time is: 10:13:07.03
Enter the new time:

c:\Data\python speed testing>mpiexec -n 1 python printTime.py
time.struct_time(tm_year=2011, tm_mon=8, tm_mday=4, tm_hour=10, tm_min=13, tm_sec=7, tm_wday=3, tm_yday=216, tm_isdst=0)

從設置數據和庫的時間開始計算運行總和的實際時間會產生良好的性能改進。

主機的秒數圖表

可能是負載均衡不好:節點0的工作量比節點1少,因為總和較低的N / 2整數比總和較高的N / 2整數要快。 結果,節點2很早就從節點0獲得消息,並且必須等待相對長的節點1。

編輯 :Sven Marnach是對的; 它不是負載平衡,因為sum(range(N))sum(range(N,2*N))花費相同的時間量。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM