【问题标题】:How do I use MPI to send the correct number of derived-type objects?如何使用 MPI 发送正确数量的派生类型对象?
【发布时间】:2016-02-05 15:22:33
【问题描述】:

对 MPI 有一些经验,但对派生类型等一些更高级的方面没有经验,这是我的问题所涉及的。

我正在处理的代码有几个尺寸为(-1:nx+2,-1:ny+2,-1:nz+2) 的数组。为了清楚起见,每个进程都有自己的值nxnynz。数组之间有重叠。例如,一个 proc 上的 x(:,:,-1:2) 将表示与“下方”proc 上的 x(:,:,nz-1:nz+2) 相同的信息。

已定义派生的cell_zface 类型:

idir = 3
sizes = (/nx_glb, ny_glb, nz_glb/)   !These nums are the same for all procs.
subsizes = (/nx, ny, 2/)
mpitype = MPI_DATATYPE_NULL
CALL MPI_TYPE_CREATE_SUBARRAY(3, sizes, subsizes, starts, &
    MPI_ORDER_FORTRAN, mpireal, mpitype, errcode)
CALL MPI_TYPE_COMMIT(mpitype, errcode)
cell_zface = mpitype

现在,这个派生类型在多个MPI_SENDRECV 调用中成功使用。例如

CALL MPI_SENDRECV( &
        x(-1,-1,   1), 1, cell_zface, proc_z_min, tag, &
        x(-1,-1,nz+1), 1, cell_zface, proc_z_max, tag, &
        comm, status, errcode)

据我了解,此调用是在 procs 之间发送和接收数组的两个“水平”切片(即 x-y 切片)。

我想做一些不同的事情,即发送四个“水平”切片。所以我试试

call mpi_send(x(-1,-1,nz-1), 2, cell_zface,    &
                  proc_z_max, rank, comm, mpierr)

附带接收。

最后,我的问题:代码运行,但错误。 AFAICT,即使我使用“2”而不是“1”作为计数参数,它也只发送两个水平切片。我可以通过两次调用 mpi_send 来解决这个问题:

call mpi_send(x(-1,-1,nz-1), 1, cell_zface,    &
                  proc_z_max, rank, comm, mpierr)
call mpi_send(x(-1,-1,nz+1), 1, cell_zface,    &
                  proc_z_max, rank, comm, mpierr)

附带接收,但这肯定不漂亮。

那么,为什么mpi_send 只发送两个水平切片,即使我将计数参数设置为“2”?有没有一种干净的方式来做我想做的事情?

【问题讨论】:

  • 你必须明白,即使你有自己的“类型”,你真正拥有的是一个巨大的连续内存块。因此,虽然您可以发送两个 cell_zface 类型,但类型本身在内存中的大小仅与您的类型使用的第一个和最后一个(一维)内存位置之间的距离一样大。 Aka,您的类型的大小不是真正的nx*ny*nz
  • 要完成你想做的事情,你必须给你的派生数据类型一个不同的范围。请注意,数据类型的大小将相同,但“范围”(或此数据类型的连续版本将包含的内存量)将不同。我相信您可以通过MPI_Type_create_resized 完成此操作。

标签: fortran mpi fortran90 fortran95 derived-types


【解决方案1】:

可以这么说,每个 MPI 数据类型都有两种大小。一个是真实大小,即存储数据类型引用的所有重要数据所需的内存量。可以将其视为该数据类型的元素在实际消息中占用的空间量。

另外一个大小就是所谓的extent。 MPI 中的每个数据类型都是以下类型的指令的集合:“从提供的缓冲区位置偏移 dispi 并读/写基本类型的元素 键入i”。所有 (typei, dispi) 对的集合称为数据类型的类型映射。最小偏移量称为下界,最大偏移量+基本类型在该偏移量处的大小+所需的任何填充称为上限。数据类型的范围是上限和下限之间的差异,并给出最短的连续内存区域的大小,包括该数据类型访问的所有位置。

由于 MPI 要求在任何通信操作期间不得多次读取或写入内存位置,因此类型映射中的对必须引用不相交的位置。因此,数据类型的真实范围总是大于或等于其大小。

MPI 在访问该数据类型的连续元素时使用该数据类型的范围。以下声明:

MPI_SEND(buf, n, dtype, ...)

结果:

  • MPI 按照编码为dtype 的类型映射的规则从位置buf 获取一个dtype 类型的元素;
  • MPI 获取从位置buf + extent(dtype) 开始的下一个元素;
  • ...
  • MPI 采用从位置buf + (n-1)*extent(dtype) 开始的n-th 元素。

MPI_INTEGERMPI_REAL 等原始数据类型的范围与基本类型(INTEGERREAL 等)的大小相匹配 + 架构要求的任何填充,这使得它可以通过简单地指定元素的数量来发送基本类型的数组。

现在,回到你的情况。您正在创建一个数据类型,该数据类型涵盖来自 nx_glb x ny_glb x nz_glb 数组的 nx x ny x 2 子数组。该数据类型的大小确实是nx * ny * 2 乘以mpireal 的大小,但范围实际上是nx_glb * ny_glb * nz_glb 乘以mpireal 的范围。换句话说:

MPI_SEND(buf, 2, cell_zface, ...)

不会从buf 的大数组中提取两个连续的nx x ny x 2 平板。相反,它将从位置 (startx, starty, start 开始的两个大小为 nx_glb x ny_glb x nz_glb 的连续数组中的每一个中提取一个平板z) 在每个数组中。如果您的程序在运行时没有出现段错误,请认为自己很幸运。

现在是棘手的部分。 MPI 允许通过人为地设置下限和上限的值来为每个数据类型提供一个虚假的范围(这就是为什么我将前面定义的范围称为“真”)。这样做不会影响数据类型或其类型映射的大小(即 MPI 仍会使用相同的偏移量并操作相同基本类型的元素),但会影响 MPI 在访问给定数据类型的连续元素时在内存中的跨度。早些时候,设置范围是通过将数据类型“夹在”特殊伪类型MPI_LBMPI_UB 的元素之间的限制来完成的。从 MPI-2 开始,MPI_TYPE_CREATE_RESIZED 被用来实现相同的目标。

integer(kind=MPI_ADDRESS_KIND) :: lb, extent
integer :: newtype

! First obtain the extent of the old type used to construct cell_zface
call MPI_TYPE_GET_EXTENT(mpireal, lb, extent, errcode)
! Adjust the extent of cell_zface
extent = (nx_glb * ny_glb * subsizes(3)) * extent
call MPI_TYPE_CREATE_RESIZED(cell_zface, lb, extent, newtype, errcode)
call MPI_TYPE_COMMIT(newtype, errcode)
! Get rid of the previous type
call MPI_TYPE_FREE(cell_zface, errcode)
cell_zface = newtype

您现在可以使用cell_zface 发送多个连续的平板。

另一种可能更简单的方法是在调用 MPI_TYPE_CREATE_SUBARRAY 时将数组的第 3 维的大小设置为等于子数组的第 3 维的大小:

idir = 3
subsizes = (/nx, ny, 2/)
sizes = (/nx_glb, ny_glb, subsizes(3)/)   !These nums are the same for all procs.
mpitype = MPI_DATATYPE_NULL
CALL MPI_TYPE_CREATE_SUBARRAY(3, sizes, subsizes, starts, &
    MPI_ORDER_FORTRAN, mpireal, mpitype, errcode)
CALL MPI_TYPE_COMMIT(mpitype, errcode)
cell_zface = mpitype

在这两种情况下,我都假设starts(3) 等于0

【讨论】:

  • 没想到需要做这么多工作才能得到答案!非常感谢您提供完整且非常有帮助的答案。你就是男人!
  • 所以,如果我理解正确的话,这种数据类型的步幅几乎是整个 3D 数组的大小。这是一个我知道您无法明确回答的问题,但如果您有任何见解,我将不胜感激:您可能已经发现我没有编写此代码。那么,步幅等于整个数组的大小是否有意义?我想不出任何有用的应用程序。当然,步幅等于一维或二维的乘积。但是整个阵列呢?谢谢。
  • 子数组数据类型构造函数的主要目的是用于 MPI-IO。不同的 MPI 等级提供相同的数组维度并在其中指定不同的部分,这使它们能够(并行)访问存储为单个二进制文件的数组数据。这种数据类型的范围故意等于整个数组的大小 - 它允许人们轻松地从连续存储的此类数组的集合中读取。子数组构造函数也可用于表示域分解中晕圈区域的数据类型,但必须小心并牢记其来源。
  • 这种跨步对您的代码是否有意义,取决于数据类型的使用方式。对于问题中给出的示例,答案肯定是否定的;拥有一个其范围等于大数组的两个 XY 平面占用的内存量的数据类型更有意义。
  • 请参阅 this question 以了解 MPI_TYPE_CREATE_SUBARRAY 的预期用途的一个很好的示例(尽管在 C 中)。
猜你喜欢
  • 2015-09-10
  • 1970-01-01
  • 2019-01-20
  • 2012-10-24
  • 2012-02-03
  • 2014-08-08
  • 2013-09-20
  • 2012-02-11
相关资源
最近更新 更多