在編寫並行計算的程序時,一定要注意“進程餓死”的問題,剛接觸MPI並行計算的同學可能不太注意這個問題,也很容易寫出這樣的程序。那麼何爲餓死呢,簡單講述一下:
並行計算大佬劉文志“風辰”在《並行算法設計與性能優化》一書中給出了“餓死”幾個定義(書的P140處):
所謂“餓死”是指某個控制流一直得不到計算,本質上是一種負載不均衡的問題。大多數情況下可能和優先級有關係,另外就是軟件開發人員的失誤。餓死的直觀表現就是控制流一直在等待,就好像進入了死循環。比如系統已優先級調度控制流,軟件開發人員錯誤滴爲某個控制流分配了一個非常低的優先級,那麼可能只要系統有任務並行,這個控制流就不可能得到處理器。另外一種餓死是從任務的角度來說,也即某個任務一直存在系統中,沒有控制流來計算它。
我遇到的“餓死”是任務級別的,和風辰說的第二種情況類似,但是又不完全一樣,是在MPI並行計算中遇到的問題。我用MPI做地震勘探中的多炮並行數值模擬,即任務級別的並行。當發4個MPI進程算7炮地震數據時,就會發生負載不均衡的問題。前三個進程都可以計算2炮,第四個進程只能計算1炮。
爲了使進程同步,我們一般用MPI_Barrier(comm)語句強制所有進程在某個地方等待,這裏Barrier的設置很關鍵,搞不好就會出現餓死的問題,我們看兩段簡單的代碼:
有餓死問題:
/* loop shot by shot */
for (is=s_beg;is<s_end;is++)
{
/****************** caculate forward wavefield *******************/
wavefield_forward(nx,ny,nz);
/****************** caculate residual wavefield ******************/
Com_Resi(nx,ny,nz);
/****************** caculate backward wavefield ******************/
wavefield_backward(nx,ny,nz);
MPI_Barrier(comm);
/********************** caculate gradient *********************/
Com_Grad(nx,ny,nz);
MPI_Barrier(comm);
/* collect the gradient on this processor */
for (ixyz=0;ixyz<nxyz;ixyz++)
{
h_gradV[ixyz] += h_gradVT[ixyz];
}
MPI_Barrier(comm);
}/* end shot loops */
MPI_Barrier(comm);
當is=0時,每個進程都有任務可以計算,大家基本同步進行,此時MPI_Barrier不會出問題。
當is=1時,進程1~3還有第二炮可以算,進程4卻沒得任務可以算了,當遇到MPI_Barrier的時候,進程1~3就會等待進程4前來匯合,然後同步往下走,但進程4已經結束炮的計算轉入下一階段的任務處理。不幸的是,進程1~3不知道進程4已經走到下一站,在遇到MPI_Barrier還在癡癡地等待。事實上即便等上一萬年,進程1~3都等不到進程4在這一環節上來匯合了,這就是發生了餓死的問題。
無餓死問題:
/* loop shot by shot */
for (is=s_beg;is<s_end;is++)
{
/****************** caculate forward wavefield *******************/
wavefield_forward(nx,ny,nz);
/****************** caculate residual wavefield ******************/
Com_Resi(nx,ny,nz);
/****************** caculate backward wavefield ******************/
wavefield_backward(nx,ny,nz);
// MPI_Barrier(comm);
/********************** caculate gradient *********************/
Com_Grad(nx,ny,nz);
// MPI_Barrier(comm);
/* collect the gradient on this processor */
for (ixyz=0;ixyz<nxyz;ixyz++)
{
h_gradV[ixyz] += h_gradVT[ixyz];
}
// MPI_Barrier(comm);
}/* end shot loops */
MPI_Barrier(comm);
這裏,我們把循環中的MPI_Barrier都註銷掉。即讓這四個進程獨立地計算任務,相互之間不用等待。這樣進程1~3在算第二炮時就不會再癡癡地等待進程4。在循環結束後設置MPI_Barrier,意在告訴這四個進程在循環計算的過程中可以獨立地行計算,但在循環計算結束後,必須停下來等待,等到4個進程都跑到這裏時再轉入下一階段的計算。