我对使用 MPI 还很陌生。我的问题如下:我有一个矩阵,其中 2000 行和 3 列存储为二维数组(不是连续数据)。在不改变数组结构的情况下,根据进程数np,每个进程应该得到矩阵的一部分。
例子:
A: 2000 个 3 列的二维数组,np = 2,然后 P0 得到 A 的前半部分,即前 1000 行乘 3 列的二维数组,P1 得到后半部分,即后 1000 行3 列。
现在 np 可以是任何数字(只要它除以行数)。有什么简单的方法可以解决这个问题吗?
我将不得不使用 FORTRAN 90 来完成这项任务。
谢谢
最佳答案
由于列优先存储,直接使用分散/聚集操作在 Fortran 中按行分布 2D 数组很棘手(但并非不可能)。以下是两种可能的解决方案。
纯 Fortran 90 解决方案:使用 Fortran 90,您可以指定数组部分,例如 A(1:4,2:3)
,这将从矩阵 A
中取出一个小的 4x2 块。您可以将数组切片传递给 MPI 例程。注意当前的 MPI 实现(符合现在旧的 MPI-2.2 标准),编译器将创建段数据的临时连续副本并将其传递给 MPI 例程(因为临时存储的生命周期没有明确定义,一个不应将数组部分传递给非阻塞 MPI 操作,如 MPI_ISEND
)。 MPI-3.0 引入了新的非常现代的 Fortran 2008 接口(interface),允许 MPI 例程直接获取数组部分(没有中间数组),并支持将部分传递给非阻塞调用。
对于数组部分,您只需在根进程中实现一个简单的 DO
循环:
INTEGER :: i, rows_per_proc
rows_per_proc = 2000/nproc
IF (rank == root) THEN
DO i = 0, nproc-1
IF (i /= root) THEN
start_row = 1 + i*rows_per_proc
end_row = (i+1)*rows_per_proc
CALL MPI_SEND(mat(start_row:end_row,:), 3*rows_per_proc, MPI_REAL, &
i, 0, MPI_COMM_WORLD, ierr)
END IF
END DO
ELSE
CALL MPI_RECV(submat(1,1), 3*rows_per_proc, MPI_REAL, ...)
END IF
纯 MPI 解决方案(也适用于 FORTRAN 77):首先,您必须使用
MPI_TYPE_VECTOR
声明向量数据类型。块的数量是 3
,块长度是每个进程应该得到的行数(例如 1000
),步幅应该等于矩阵的总高度(例如 2000
)。如果此数据类型称为 blktype
,则以下内容将发送矩阵的上半部分:REAL, DIMENSION(2000,3) :: mat
CALL MPI_SEND(mat(1,1), 1, blktype, p0, ...)
CALL MPI_SEND(mat(1001,1), 1, blktype, p1, ...)
使用
MPI_SEND
调用 blktype
将从指定的起始地址获取 1000
元素,然后跳过下一个 2000 - 1000 = 1000
元素,获取另一个 1000
等等,总共 3
次。这将形成大矩阵的 1000 行子矩阵。您现在可以运行一个循环来向通信器中的每个进程发送不同的子块,从而有效地执行分散操作。为了接收这个子块,接收过程可以简单地指定:
REAL, DIMENSION(1000,3) :: submat
CALL MPI_RECV(submat(1,1), 3*1000, MPI_REAL, root, ...)
如果您不熟悉 MPI,这就是您需要了解的有关 Fortran 中按行散布矩阵的全部信息。如果您很了解 MPI 的类型系统是如何工作的,那么请提前阅读更优雅的解决方案。
(有关如何使用 Jonathan Dursi 的
MPI_SCATTERV
执行此操作的出色描述,请参阅 here。他的解决方案处理按列拆分 C 矩阵,这与此处的问题基本相同,因为 C 以行主要方式存储矩阵。Fortran版本如下。)您也可以使用
MPI_SCATTERV
但它非常复杂。它建立在上面介绍的纯 MPI 解决方案之上。首先,您必须将 blktype
数据类型的大小调整为一个新类型,该类型的范围等于 MPI_REAL
的范围,以便可以指定数组元素中的偏移量。这是必需的,因为 MPI_SCATTERV
中的偏移量是指定数据类型范围的倍数,而 blktype
的范围是矩阵本身的大小。但是由于跨步存储,两个子块的起始位置仅相隔 4000
字节( 1000
乘以 MPI_REAL
的典型范围)。要修改类型的范围,可以使用 MPI_TYPE_CREATE_RESIZED
:INTEGER(KIND=MPI_ADDRESS_KIND) :: lb, extent
! Get the extent of MPI_REAL
CALL MPI_TYPE_GET_EXTENT(MPI_REAL, lb, extent, ierr)
! Bestow the same extent upon the brother of blktype
CALL MPI_TYPE_CREATE_RESIZED(blktype, lb, extent, blk1b, ierr)
这将创建一个新的数据类型
blk1b
,它具有 blktype
的所有特征,例如可用于发送整个子块,但在数组操作中使用时,MPI 只会以单个 MPI_REAL
的大小而不是整个矩阵的大小来推进数据指针。使用这种新类型,您现在可以将 MPI_SCATTERV
的每个块的开头定位在 mat
的任何元素上,包括任何矩阵行的开头。具有两个子块的示例:INTEGER, DIMENSION(2) :: sendcounts, displs
! First sub-block
sendcounts(1) = 1
displs(1) = 0
! Second sub-block
sendcounts(2) = 1
displs(2) = 1000
CALL MPI_SCATTERV(mat(1,1), sendcounts, displs, blk1b, &
submat(1,1), 3*1000, MPI_REAL, &
root, MPI_COMM_WORLD, ierr)
这里第一个子块的位移是
0
,与矩阵的开头重合。第二个子块的位移是 1000
,即它将从第一列的第 1000 行开始。在接收方的数据计数参数是 3*1000
元素,它匹配子块类型的大小。
关于mpi - 分解矩阵,以便每个进程使用 MPI 获得矩阵的份额,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13630529/