MPI 将阻塞转换为非阻塞问题

Posted

技术标签:

【中文标题】MPI 将阻塞转换为非阻塞问题【英文标题】:MPI Converting Blocking to Non-Blocking Issues 【发布时间】:2015-08-17 16:21:13 【问题描述】:

我正在编写的代码使用 MPI 将大型 3 维数组(立方体)沿所有三个轴拆分为子域,以形成较小的立方体。我之前曾研究过一个更简单的二维等价物,没有任何问题。

现在,由于 MPI 有将 MPI_SEND 和 MPI_RECV 视为对小块数据的非阻塞调用这种烦人的习惯(或令人满意的习惯,取决于您如何看待),这种从 2D 到 3D 的迁移带来了很多困难。所有在 2D 中完美运行的调用在 3D 中只要稍有刺激就会开始死锁,因为必须在进程之间传递的数据现在是 3D 数组,因此更大。

经过一周的战斗并拔出很多头发,构建了一组复杂的 MPI_SEND 和 MPI_RECV 调用,它们设法跨域中每个立方体的面、边和角传递数据(适当设置了周期性和非周期性在不同的边界)顺利。幸福不会持久。在添加了一个新的边界条件后,需要在域一侧的单元之间进行额外的通信路径,代码又陷入了另一轮恶性死锁。

受够了,我决定求助于非阻塞呼叫。现在有了这么多的背景,我希望我对下面代码的意图会很清楚。我不包括我用来跨子域的边缘和角落传输数据的代码。如果我能理清立方体面之间的交流,我就能把其他所有东西都整齐地放在适当的位置。

代码使用五个数组来简化数据传输:

    rArr = 相邻单元格的等级数组

    tsArr = 用于向每个邻居发送数据的标签数组

    trArr = 用于从每个邻居接收数据的标签数组

    lsArr = 限制(索引)数组,用于描述要发送的数据块

    lrArr = 限制(索引)数组,用于描述要接收的数据块

现在,由于每个立方体有 6 个邻居共享一个面,因此 rArr、tsArr 和 trArr 都是长度为 6 的整数数组。另一方面,limits 数组是一个二维数组,如下所述:

lsArr = [[xStart, xEnd, yStart, yEnd, zStart, zEnd, dSize], !for face 1 sending
         [xStart, xEnd, yStart, yEnd, zStart, zEnd, dSize], !for face 2 sending
         .
         .
         [xStart, xEnd, yStart, yEnd, zStart, zEnd, dSize]] !for face 6 sending

因此,在单元格(进程)的第 i 个面上发送变量 dCube 的值的调用将如下发生:

call MPI_SEND(dCube(lsArr(i, 1):lsArr(i, 2), lsArr(i, 3):lsArr(i, 4), lsArr(i, 5):lsArr(i, 6)), lsArr(i, 7), MPI_DOUBLE_PRECISION, rArr(i), tsArr(i), MPI_COMM_WORLD, ierr)

另一个具有匹配目标等级和标签的进程将收到与以下相同的块:

call MPI_RECV(dCube(lrArr(i, 1):lrArr(i, 2), lrArr(i, 3):lrArr(i, 4), lrArr(i, 5):lrArr(i, 6)), lrArr(i, 7), MPI_DOUBLE_PRECISION, rArr(i), trArr(i), MPI_COMM_WORLD, stVal, ierr)

对源进程和目标进程的 lsArr 和 lrArr 进行了测试,以显示匹配的大小(但限制不同)。还检查了标签数组以查看它们是否匹配。

现在我的早期版本的阻塞调用代码运行良好,因此我对上述数组中值的正确性有 99% 的信心。如果有理由怀疑它们的准确性,我可以添加这些细节,但是帖子会变得非常长。

下面是我的代码的阻塞版本,它运行良好。如果有点棘手,我深表歉意。如果有必要进一步阐明它以找出问题,我会这样做。

subroutine exchangeData(dCube)
use someModule

implicit none
integer :: i, j
double precision, intent(inout), dimension(xS:xE, yS:yE, zS:zE) :: dCube

do j = 1, 3
    if (mod(edArr(j), 2) == 0) then    !!edArr = [xRank, yRank, zRank]
        i = 2*j - 1
        call MPI_SEND(dCube(lsArr(1, i):lsArr(2, i), lsArr(3, i):lsArr(4, i), lsArr(5, i):lsArr(6, i)), &
                      lsArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), tsArr(i), MPI_COMM_WORLD, ierr)

        i = 2*j
        call MPI_RECV(dCube(lrArr(1, i):lrArr(2, i), lrArr(3, i):lrArr(4, i), lrArr(5, i):lrArr(6, i)), &
                      lrArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), trArr(i), MPI_COMM_WORLD, stVal, ierr)
    else
        i = 2*j
        call MPI_RECV(dCube(lrArr(1, i):lrArr(2, i), lrArr(3, i):lrArr(4, i), lrArr(5, i):lrArr(6, i)), &
                      lrArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), trArr(i), MPI_COMM_WORLD, stVal, ierr)

        i = 2*j - 1
        call MPI_SEND(dCube(lsArr(1, i):lsArr(2, i), lsArr(3, i):lsArr(4, i), lsArr(5, i):lsArr(6, i)), &
                      lsArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), tsArr(i), MPI_COMM_WORLD, ierr)
    end if

    if (mod(edArr(j), 2) == 0) then
        i = 2*j
        call MPI_SEND(dCube(lsArr(1, i):lsArr(2, i), lsArr(3, i):lsArr(4, i), lsArr(5, i):lsArr(6, i)), &
                      lsArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), tsArr(i), MPI_COMM_WORLD, ierr)

        i = 2*j - 1
        call MPI_RECV(dCube(lrArr(1, i):lrArr(2, i), lrArr(3, i):lrArr(4, i), lrArr(5, i):lrArr(6, i)), &
                      lrArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), trArr(i), MPI_COMM_WORLD, stVal, ierr)
    else
        i = 2*j - 1
        call MPI_RECV(dCube(lrArr(1, i):lrArr(2, i), lrArr(3, i):lrArr(4, i), lrArr(5, i):lrArr(6, i)), &
                      lrArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), trArr(i), MPI_COMM_WORLD, stVal, ierr)

        i = 2*j
        call MPI_SEND(dCube(lsArr(1, i):lsArr(2, i), lsArr(3, i):lsArr(4, i), lsArr(5, i):lsArr(6, i)), &
                      lsArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), tsArr(i), MPI_COMM_WORLD, ierr)
    end if
end do
end subroutine exchangeData

基本上它沿着每个方向,x、y 和 z,首先从奇数面发送数据,然后从偶数面发送数据。我不知道是否有更简单的方法可以做到这一点。这是在无数几乎让我发疯的死锁代码之后得出的。跨边缘和角落发送数据的代码甚至更长。

现在是我现在遇到的实际问题。我用以下代码替换了上面的代码(可能有点天真?)

subroutine exchangeData(dCube)
use someModule

implicit none
integer :: i, j
integer, dimension(6) :: fRqLst
integer :: stLst(MPI_STATUS_SIZE, 6)
double precision, intent(inout), dimension(xS:xE, yS:yE, zS:zE) :: dCube

fRqLst = MPI_REQUEST_NULL
do i = 1, 6
    call MPI_IRECV(dCube(lrArr(1, i):lrArr(2, i), lrArr(3, i):lrArr(4, i), lrArr(5, i):lrArr(6, i)), &
                        lrArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), trArr(i), MPI_COMM_WORLD, fRqLst(i), ierr)
end do

do i = 1, 6
    call MPI_SEND(dCube(lsArr(1, i):lsArr(2, i), lsArr(3, i):lsArr(4, i), lsArr(5, i):lsArr(6, i)), &
                       lsArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), tsArr(i), MPI_COMM_WORLD, ierr)
end do
call MPI_WAITALL(6, fRqLst, stLst, ierr)
call MPI_BARRIER(MPI_COMM_WORLD, ierr)
end subroutine exchangeData

someModule 是一个占位符模块,包含所有变量。实际上它们分布在一系列模块中,但我现在将对其进行掩饰。主要思想是使用非阻塞 MPI_IRECV 调用来启动每个进程以接收数据,然后使用一系列阻塞 MPI_SEND 调用发送数据。但是,我怀疑如果事情这么简单,并行编程会是小菜一碟。

此代码给出一个 SIGABRT 并以双释放错误退出。此外,它似乎是一只黑森虫,有时会消失。

错误信息:

*** Error in `./a.out': double free or corruption (!prev): 0x00000000010315c0 ***
*** Error in `./a.out': double free or corruption (!prev): 0x00000000023075c0 ***
*** Error in `./a.out': double free or corruption (!prev): 0x0000000001d465c0 ***

Program received signal SIGABRT: Process abort signal.

Program received signal SIGABRT: Process abort signal.

Backtrace for this error:

Program received signal SIGABRT: Process abort signal.

Backtrace for this error:

Program received signal SIGABRT: Process abort signal.

Backtrace for this error:

Backtrace for this error:
#0  0x7F5807D3C7D7
#1  0x7F5807D3CDDE
#2  0x7F580768ED3F
#3  0x7F580768ECC9
#4  0x7F58076920D7
#5  0x7F58076CB393
#6  0x7F58076D766D
#0  0x7F4D387D27D7
#1  0x7F4D387D2DDE
#2  0x7F4D38124D3F
#3  0x7F4D38124CC9
#4  0x7F4D381280D7
#5  0x7F4D38161393
#0  #6  0x7F4D3816D66D
0x7F265643B7D7
#1  0x7F265643BDDE
#2  0x7F2655D8DD3F
#3  0x7F2655D8DCC9
#4  0x7F2655D910D7
#5  0x7F2655DCA393
#6  0x7F2655DD666D
#7  0x42F659 in exchangedata_ at solver.f90:1542 (discriminator 1)
#7  0x42F659 in exchangedata_ at solver.f90:1542 (discriminator 1)
#8  0x42EFFB in processgrid_ at solver.f90:431
#9  0x436CF0 in MAIN__ at solver.f90:97
#8  0x42EFFB in processgrid_ at solver.f90:431
#9  0x436CF0 in MAIN__ at solver.f90:97
#0  0x7FC9DA96B7D7
#1  0x7FC9DA96BDDE
#2  0x7FC9DA2BDD3F
#3  0x7FC9DA2BDCC9
#4  0x7FC9DA2C10D7
#5  0x7FC9DA2FA393
#6  0x7FC9DA30666D
#7  0x42F659 in exchangedata_ at solver.f90:1542 (discriminator 1)
#8  0x42EFFB in processgrid_ at solver.f90:431
#9  0x436CF0 in MAIN__ at solver.f90:97
#7  0x42F659 in exchangedata_ at solver.f90:1542 (discriminator 1)
#8  0x42EFFB in processgrid_ at solver.f90:431
#9  0x436CF0 in MAIN__ at solver.f90:97

我尝试使用“(鉴别器 1)”部分在此站点上搜索类似的错误,但找不到任何错误。我还搜索了 MPI 产生双释放内存损坏错误的情况,但再次无济于事。

我还必须指出,错误消息中的第 1542 行对应于我的代码中的阻塞 MPI_SEND 调用。

当我将 gfortran 4.8.2 与 ompi 1.6.5 一起使用时,出现了上述错误。但是,我也尝试使用 Intel fortran 编译器运行上述代码并收到一条奇怪的错误消息:

[21] trying to free memory block that is currently involved to uncompleted data transfer operation

我在网上搜索了上述错误,几乎一无所获。 :( 所以这也是一个死胡同。完整的错误信息有点太长了,但下面是它的一部分:

*** glibc detected *** ./a.out: munmap_chunk(): invalid pointer: 0x0000000001c400a0 ***
*** glibc detected *** ./a.out: malloc(): memory corruption: 0x0000000001c40410 ***
*** glibc detected *** ./a.out: malloc(): memory corruption: 0x0000000000a67790 ***
*** glibc detected *** ./a.out: malloc(): memory corruption: 0x0000000000a67790 ***
*** glibc detected *** ./a.out: free(): invalid next size (normal): 0x0000000000d28c80 ***
*** glibc detected *** ./a.out: malloc(): memory corruption: 0x00000000015354b0 ***
*** glibc detected *** ./a.out: malloc(): memory corruption: 0x00000000015354b0 ***
*** glibc detected *** ./a.out: free(): invalid next size (normal): 0x0000000000f51520 ***
[20] trying to free memory block that is currently involved to uncompleted data transfer operation
 free mem  - addr=0x26bd800 len=3966637480
 RTC entry - addr=0x26a9e70 len=148800 cnt=1
Assertion failed in file ../../i_rtc_cache.c at line 1397: 0
internal ABORT - process 20
[21] trying to free memory block that is currently involved to uncompleted data transfer operation
 free mem  - addr=0x951e90 len=2282431520
 RTC entry - addr=0x93e160 len=148752 cnt=1
Assertion failed in file ../../i_rtc_cache.c at line 1397: 0
internal ABORT - process 21

如果我的错误是一些粗心或知识不足,以上细节可能就足够了。如果是更深层次的问题,我很乐意进一步阐述。

提前致谢!

【问题讨论】:

您必须假设 Send/Recv 会阻塞。他们不必这样做。使用 Ssend 可以帮助您调试问题,因为它总是会阻塞直到匹配发生。非阻塞几乎总是进行边界交换的正确方式。 您可以先尝试使用连续缓冲区。我不太确定 Fortran 数组切片是如何工作的。至少应该支持 Fortran 2008 绑定,但它们可能有问题。 非阻塞 MPI 和非连续数组在 Fortran 中非常危险。 MPI3 仍然没有得到很好的支持,尤其是对于 gfortran。我推荐 MPI 派生类型并只传递缓冲区的第一个元素。 见***.com/questions/19455051/… 谢谢! cmets 发光。看起来我可能对 Fortran 的数组切片过于信任了。由于我还不熟悉派生 MPI 数据类型的使用,我将把非连续数组复制到一个单独的连续数组中,暂时发送它,看看它是如何工作的。 【参考方案1】:

虽然这个问题吸引了有用的 cmets,但我相信发布一个关于该建议如何帮助我解决问题的答案可能对那些将来可能会因同样问题偶然发现这篇文章的人有用。

正如所指出的,在 Fortran 中使用非连续数组进行非阻塞 MPI 调用 - 坏主意

我使用了将非连续数组复制到连续数组并使用它的想法。但是,通过阻塞调用,非连续数组似乎表现良好。由于我使用的是阻塞 MPI_SEND 和非阻塞 MPI_IRECV,因此代码只复制一份 - 仅用于接收,并继续像以前一样非连续地发送数据。这似乎目前有效,但如果以后可能会导致任何问题,请在 cmets 中警告我。

它确实添加了很多重复的代码行(破坏美观:P)。这主要是因为所有 6 个面的发送/接收限制并不相同。因此,用于临时存储要接收的数据的数组必须为六个面中的每一个单独分配(和复制)。

subroutine exchangeData(dCube)
use someModule

implicit none
integer :: i
integer, dimension(6) :: fRqLst
integer :: stLst(MPI_STATUS_SIZE, 6)
double precision, intent(inout), dimension(xS:xE, yS:yE, zS:zE) :: dCube
double precision, allocatable, dimension(:,:,:) :: fx0, fx1, fy0, fy1, fz0, fz1

allocate(fx0(lrArr(1, 1):lrArr(2, 1), lrArr(3, 1):lrArr(4, 1), lrArr(5, 1):lrArr(6, 1)))
allocate(fx1(lrArr(1, 2):lrArr(2, 2), lrArr(3, 2):lrArr(4, 2), lrArr(5, 2):lrArr(6, 2)))
allocate(fy0(lrArr(1, 3):lrArr(2, 3), lrArr(3, 3):lrArr(4, 3), lrArr(5, 3):lrArr(6, 3)))
allocate(fy1(lrArr(1, 4):lrArr(2, 4), lrArr(3, 4):lrArr(4, 4), lrArr(5, 4):lrArr(6, 4)))
allocate(fz0(lrArr(1, 5):lrArr(2, 5), lrArr(3, 5):lrArr(4, 5), lrArr(5, 5):lrArr(6, 5)))
allocate(fz1(lrArr(1, 6):lrArr(2, 6), lrArr(3, 6):lrArr(4, 6), lrArr(5, 6):lrArr(6, 6)))

fRqLst = MPI_REQUEST_NULL
call MPI_IRECV(fx0, lrArr(7, 1), MPI_DOUBLE_PRECISION, rArr(1), trArr(1), MPI_COMM_WORLD, fRqLst(1), ierr)
call MPI_IRECV(fx1, lrArr(7, 2), MPI_DOUBLE_PRECISION, rArr(2), trArr(2), MPI_COMM_WORLD, fRqLst(2), ierr)
call MPI_IRECV(fy0, lrArr(7, 3), MPI_DOUBLE_PRECISION, rArr(3), trArr(3), MPI_COMM_WORLD, fRqLst(3), ierr)
call MPI_IRECV(fy1, lrArr(7, 4), MPI_DOUBLE_PRECISION, rArr(4), trArr(4), MPI_COMM_WORLD, fRqLst(4), ierr)
call MPI_IRECV(fz0, lrArr(7, 5), MPI_DOUBLE_PRECISION, rArr(5), trArr(5), MPI_COMM_WORLD, fRqLst(5), ierr)
call MPI_IRECV(fz1, lrArr(7, 6), MPI_DOUBLE_PRECISION, rArr(6), trArr(6), MPI_COMM_WORLD, fRqLst(6), ierr)

do i = 1, 6
    call MPI_SEND(dCube(lsArr(1, i):lsArr(2, i), lsArr(3, i):lsArr(4, i), lsArr(5, i):lsArr(6, i)), &
                       lsArr(7, i), MPI_DOUBLE_PRECISION, rArr(i), tsArr(i), MPI_COMM_WORLD, ierr)
end do

call MPI_WAITALL(6, fRqLst, stLst, ierr)
dCube(lrArr(1, 1):lrArr(2, 1), lrArr(3, 1):lrArr(4, 1), lrArr(5, 1):lrArr(6, 1)) = fx0
dCube(lrArr(1, 2):lrArr(2, 2), lrArr(3, 2):lrArr(4, 2), lrArr(5, 2):lrArr(6, 2)) = fx1
dCube(lrArr(1, 3):lrArr(2, 3), lrArr(3, 3):lrArr(4, 3), lrArr(5, 3):lrArr(6, 3)) = fy0
dCube(lrArr(1, 4):lrArr(2, 4), lrArr(3, 4):lrArr(4, 4), lrArr(5, 4):lrArr(6, 4)) = fy1
dCube(lrArr(1, 5):lrArr(2, 5), lrArr(3, 5):lrArr(4, 5), lrArr(5, 5):lrArr(6, 5)) = fz0
dCube(lrArr(1, 6):lrArr(2, 6), lrArr(3, 6):lrArr(4, 6), lrArr(5, 6):lrArr(6, 6)) = fz1
deallocate(fx0, fx1, fy0, fy1, fz0, fz1)
end subroutine exchangeData

这部分抵消了我试图通过将等级和标签存储在数组中来获得的优势。我这样做主要是为了将发送和接收呼叫置于一个循环中。有了这个修复,只有发送调用可以进入循环。

由于在子例程的每次调用中分配和取消分配会浪费时间,因此可以将数组放入模块中并在代码开头进行分配。每次调用的限制都不会改变。

当对角和边缘也应用相同的方法时,它确实会使代码有点膨胀,但它似乎可以工作。 :)

感谢cmets。

【讨论】:

以上是关于MPI 将阻塞转换为非阻塞问题的主要内容,如果未能解决你的问题,请参考以下文章

可以将 SQLAlchemy 配置为非阻塞吗?

如何将套接字重置为阻塞模式(在我将其设置为非阻塞模式之后)?

MPI 非阻塞发送/接收

连续 MPI 非阻塞调用

非阻塞 MPI 和会合协议

使用epoll时需要将socket设为非阻塞吗?