为什么启动更多进程时基于 Fortran 的 MPI 例程需要更多计算时间
Why does Fortran based MPI routine take more compute time when more processes are launched
我基于 Fortran 的 MPI 代码不涉及进程间通信。只有每个进程的计算才会完成并计时。我的平台是 Intel Sandy Bridge。代码是使用 mpiifort 包装器编译的。我有两个无法解释的观察结果:
1) 计算时间随着涉及的机器越来越多而增加,其中每台机器有 16 个内核(2 个处理器,每个处理器 8 个内核)。例如,16 个 MPI 等级最多耗时 5.74 秒,32 个耗时 13.64 秒,48 个耗时 18.26 秒,而 64 个耗时 25.92 秒。由于不涉及任何通信,因此无论名称 MPI 排名如何启动,我都希望得到大致相同的时间。代码包含在下面。
2) 在下面代码的第 2 步中,调用了一个子例程。如果我用实际代码替换子例程调用,程序运行得更快。例如,16 个 MPI rank 最多耗时 5.63E-02 秒,32 个耗时 0.1762,48 个耗时 0.33 秒,而 64 个耗时 0.3612 秒。
使用 ifort 编译的程序的串行版本也表现出类似的行为:子程序调用需要 0.77 秒,没有子程序调用需要 5.19E-02 秒。
我在这里附上代码。一是不调用子程序,二是调用子程序,三是子程序本身
mult.f(无子程序调用):
program mul
include 'mpif.h'
integer DIM, dim1, dim2, dim3, E
parameter (DIM=8, E=512, dim1=DIM, dim2=DIM, dim3=DIM)
integer ierr, rank, size, t2, t4, t6, t8
real*8 A(dim1, dim1), B(dim2, dim2), C(dim3, dim3)
real*8 u(dim1, dim2, dim3, E)
real*8 dudr (dim1, dim2, dim3, E), duds (dim1, dim2, dim3, E)
real*8 dudt (dim1, dim2, dim3, E), dudr2 (dim1, dim2, dim3)
integer val, i, j, k, l, r, s, t, start, end
integer iseed /3/
real tbeg, tend
CHARACTER(len=32) :: arg
call getarg(1, arg)
call MPI_INIT (ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr)
call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
! Step 1: Initialize
do i = 1, dim1
do j = 1, dim1
A (i, j) = ran(iseed);
enddo
enddo
do i = 1, dim2
do j = 1, dim2
B (i, j) = A(j, i);
enddo
enddo
do i = 1, dim3
do j = 1, dim3
C (i, j) = A(j, i);
enddo
enddo
do m = 1, E
do i=1, dim1
do j=1, dim2
do k = 1, dim3
u (i, j, k, m) = (ran(iseed)*400.0) - 200.0
enddo
enddo
enddo
enddo
! Step 2: Compute derivatives
tbeg = mpi_wtime()
do m = 0, 5000
do ie=1, E
DO t2=1,8
DO t4=1,8
DO t6=1,8
DO t8=1,8
dudr(t8,t4,t6,ie) = 0
dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)+u(t2,t4,t6,ie)
$ *C(t8,t2)
ENDDO
DO t8=1,8
dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)
$ +u(t2,t4,t6,ie)*C(t8,t2)
ENDDO
ENDDO
ENDDO
ENDDO
enddo
enddo
tend = mpi_wtime()
print *, DIM, E, tend-tbeg, rank
call MPI_FINALIZE (ierr)
end
mult.f(带子程序)
program mul
include 'mpif.h'
integer DIM, dim1, dim2, dim3, E
parameter (DIM=8, E=512, dim1=DIM, dim2=DIM, dim3=DIM)
integer ierr, rank, size
real*8 A(dim1, dim1), B(dim2, dim2), C(dim3, dim3)
real*8 u(dim1, dim2, dim3, E)
real*8 dudr (dim1, dim2, dim3, E), duds (dim1, dim2, dim3, E)
real*8 dudt (dim1, dim2, dim3, E), dudr2 (dim1, dim2, dim3)
integer val, i, j, k, l, r, s, t, start, end
integer iseed /3/
real tbeg, tend
CHARACTER(len=32) :: arg
call getarg(1, arg)
call MPI_INIT (ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr)
call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
! Step 1: Initialize
do i = 1, dim1
do j = 1, dim1
A (i, j) = ran(iseed);
enddo
enddo
do i = 1, dim2
do j = 1, dim2
B (i, j) = A(j, i);
enddo
enddo
do i = 1, dim3
do j = 1, dim3
C (i, j) = A(j, i);
enddo
enddo
do m = 1, E
do i=1, dim1
do j=1, dim2
do k = 1, dim3
u (i, j, k, m) = (ran(iseed)*400.0) - 200.0
enddo
enddo
enddo
enddo
! Step 2: Compute derivatives
tbeg = mpi_wtime()
do m = 0, 5000
do ie=1, E
call mysubroutine(u, C, dudr, ie)
enddo
enddo
tend = mpi_wtime()
print *, DIM, E, tend-tbeg, rank
call MPI_FINALIZE (ierr)
end
mysubroutine(子程序调用):
SUBROUTINE mysubroutine(u, a, dudr, ie)
integer DIM, dim1, dim2, dim3, E
parameter (DIM=8, E=512, dim1=DIM, dim2=DIM, dim3=DIM)
real*8 u(DIM, DIM, DIM, E)
real*8 a(DIM, DIM)
real*8 dudr (DIM, DIM, DIM, E)
integer t8
integer t6
integer t4
integer t2
integer i
integer j
integer k
integer l
!dir$ ASSUME_ALIGNED a: 64
!dir$ ASSUME_ALIGNED u: 64
!dir$ ASSUME_ALIGNED dudr: 64
DO t2=1,8
DO t4=1,8
DO t6=1,8
DO t8=1,8
dudr(t8,t4,t6,ie)
$ =0
dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)+u(t2,t4,t6,ie)
$ *a(t8,t2)
ENDDO
DO t8=1,8
dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)
$ +u(t2,t4,t6,ie)*a(t8,t2)
ENDDO
ENDDO
ENDDO
ENDDO
END
使用以下命令编译代码:
mpiifort -O3 -mcmodel=large -xavx mult.f rose.f -o baseline
这里有两个不同的问题:
- 为什么调用子程序的代码比较慢;和
- 为什么随着 MPI 进程数量的增加,代码会变慢。
我将尝试按顺序回答(我必须承认我的猜测是疯狂的)。
子程序较慢
如果您查看您的代码,您会发现为了增加计算时间,您将实际计算包含在一个包含 5001 次迭代的 m
循环中。但是如果你看看你的实际计算,你会发现,虽然你确实使用你的 dudr
数组来更新它,但你总是从 dudr(t8,t4,t6,ie)=0
开始。所以它以前的值是无关紧要的,因为你删除了它。因此,只有 m
循环的最后一次迭代有任何效果...
但是为了让编译器看到它,它需要看到例程的主体。因此,通过调用子例程,您迫使编译器实际执行某项操作 5001 次,而通过内联它,您让它有机会意识到它毫无意义,而且只有一次迭代就可以了!
好吧,这主要是我的猜测,但我已经看到很多次了,我相信我离真相不远了。事实上,由于您随后对 dudr
不执行任何操作,我什至感到惊讶的是编译器会在您的大循环之外生成任何东西...
更多的 MPI 进程需要更长的时间
好吧,对于这一点,我会做一个更大胆的猜测:我相信不知何故,您不是在多个节点中提交,而是在一个节点上错误地提交了您的代码。在那里,许多 MPI 进程争相访问资源(尤其是内存带宽),并且随着进程数量的增加,花费的时间越来越长......我可能是错的,但目前,这是我最好的解释.
一句至理名言
当您尝试对性能进行基准测试时,请确保始终向编译器隐藏您对计算的实际结果不感兴趣。这可以通过打印一些值,或将其传递给外部例程(可能什么也不做,但编译器不知道)来完成。否则,您可能会惊讶于它的 聪明 以及它可以删除多少 死代码 .
我基于 Fortran 的 MPI 代码不涉及进程间通信。只有每个进程的计算才会完成并计时。我的平台是 Intel Sandy Bridge。代码是使用 mpiifort 包装器编译的。我有两个无法解释的观察结果:
1) 计算时间随着涉及的机器越来越多而增加,其中每台机器有 16 个内核(2 个处理器,每个处理器 8 个内核)。例如,16 个 MPI 等级最多耗时 5.74 秒,32 个耗时 13.64 秒,48 个耗时 18.26 秒,而 64 个耗时 25.92 秒。由于不涉及任何通信,因此无论名称 MPI 排名如何启动,我都希望得到大致相同的时间。代码包含在下面。
2) 在下面代码的第 2 步中,调用了一个子例程。如果我用实际代码替换子例程调用,程序运行得更快。例如,16 个 MPI rank 最多耗时 5.63E-02 秒,32 个耗时 0.1762,48 个耗时 0.33 秒,而 64 个耗时 0.3612 秒。
使用 ifort 编译的程序的串行版本也表现出类似的行为:子程序调用需要 0.77 秒,没有子程序调用需要 5.19E-02 秒。
我在这里附上代码。一是不调用子程序,二是调用子程序,三是子程序本身
mult.f(无子程序调用):
program mul
include 'mpif.h'
integer DIM, dim1, dim2, dim3, E
parameter (DIM=8, E=512, dim1=DIM, dim2=DIM, dim3=DIM)
integer ierr, rank, size, t2, t4, t6, t8
real*8 A(dim1, dim1), B(dim2, dim2), C(dim3, dim3)
real*8 u(dim1, dim2, dim3, E)
real*8 dudr (dim1, dim2, dim3, E), duds (dim1, dim2, dim3, E)
real*8 dudt (dim1, dim2, dim3, E), dudr2 (dim1, dim2, dim3)
integer val, i, j, k, l, r, s, t, start, end
integer iseed /3/
real tbeg, tend
CHARACTER(len=32) :: arg
call getarg(1, arg)
call MPI_INIT (ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr)
call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
! Step 1: Initialize
do i = 1, dim1
do j = 1, dim1
A (i, j) = ran(iseed);
enddo
enddo
do i = 1, dim2
do j = 1, dim2
B (i, j) = A(j, i);
enddo
enddo
do i = 1, dim3
do j = 1, dim3
C (i, j) = A(j, i);
enddo
enddo
do m = 1, E
do i=1, dim1
do j=1, dim2
do k = 1, dim3
u (i, j, k, m) = (ran(iseed)*400.0) - 200.0
enddo
enddo
enddo
enddo
! Step 2: Compute derivatives
tbeg = mpi_wtime()
do m = 0, 5000
do ie=1, E
DO t2=1,8
DO t4=1,8
DO t6=1,8
DO t8=1,8
dudr(t8,t4,t6,ie) = 0
dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)+u(t2,t4,t6,ie)
$ *C(t8,t2)
ENDDO
DO t8=1,8
dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)
$ +u(t2,t4,t6,ie)*C(t8,t2)
ENDDO
ENDDO
ENDDO
ENDDO
enddo
enddo
tend = mpi_wtime()
print *, DIM, E, tend-tbeg, rank
call MPI_FINALIZE (ierr)
end
mult.f(带子程序)
program mul
include 'mpif.h'
integer DIM, dim1, dim2, dim3, E
parameter (DIM=8, E=512, dim1=DIM, dim2=DIM, dim3=DIM)
integer ierr, rank, size
real*8 A(dim1, dim1), B(dim2, dim2), C(dim3, dim3)
real*8 u(dim1, dim2, dim3, E)
real*8 dudr (dim1, dim2, dim3, E), duds (dim1, dim2, dim3, E)
real*8 dudt (dim1, dim2, dim3, E), dudr2 (dim1, dim2, dim3)
integer val, i, j, k, l, r, s, t, start, end
integer iseed /3/
real tbeg, tend
CHARACTER(len=32) :: arg
call getarg(1, arg)
call MPI_INIT (ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr)
call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
! Step 1: Initialize
do i = 1, dim1
do j = 1, dim1
A (i, j) = ran(iseed);
enddo
enddo
do i = 1, dim2
do j = 1, dim2
B (i, j) = A(j, i);
enddo
enddo
do i = 1, dim3
do j = 1, dim3
C (i, j) = A(j, i);
enddo
enddo
do m = 1, E
do i=1, dim1
do j=1, dim2
do k = 1, dim3
u (i, j, k, m) = (ran(iseed)*400.0) - 200.0
enddo
enddo
enddo
enddo
! Step 2: Compute derivatives
tbeg = mpi_wtime()
do m = 0, 5000
do ie=1, E
call mysubroutine(u, C, dudr, ie)
enddo
enddo
tend = mpi_wtime()
print *, DIM, E, tend-tbeg, rank
call MPI_FINALIZE (ierr)
end
mysubroutine(子程序调用):
SUBROUTINE mysubroutine(u, a, dudr, ie)
integer DIM, dim1, dim2, dim3, E
parameter (DIM=8, E=512, dim1=DIM, dim2=DIM, dim3=DIM)
real*8 u(DIM, DIM, DIM, E)
real*8 a(DIM, DIM)
real*8 dudr (DIM, DIM, DIM, E)
integer t8
integer t6
integer t4
integer t2
integer i
integer j
integer k
integer l
!dir$ ASSUME_ALIGNED a: 64
!dir$ ASSUME_ALIGNED u: 64
!dir$ ASSUME_ALIGNED dudr: 64
DO t2=1,8
DO t4=1,8
DO t6=1,8
DO t8=1,8
dudr(t8,t4,t6,ie)
$ =0
dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)+u(t2,t4,t6,ie)
$ *a(t8,t2)
ENDDO
DO t8=1,8
dudr(t8,t4,t6,ie)=dudr(t8,t4,t6,ie)
$ +u(t2,t4,t6,ie)*a(t8,t2)
ENDDO
ENDDO
ENDDO
ENDDO
END
使用以下命令编译代码:
mpiifort -O3 -mcmodel=large -xavx mult.f rose.f -o baseline
这里有两个不同的问题:
- 为什么调用子程序的代码比较慢;和
- 为什么随着 MPI 进程数量的增加,代码会变慢。
我将尝试按顺序回答(我必须承认我的猜测是疯狂的)。
子程序较慢
如果您查看您的代码,您会发现为了增加计算时间,您将实际计算包含在一个包含 5001 次迭代的 m
循环中。但是如果你看看你的实际计算,你会发现,虽然你确实使用你的 dudr
数组来更新它,但你总是从 dudr(t8,t4,t6,ie)=0
开始。所以它以前的值是无关紧要的,因为你删除了它。因此,只有 m
循环的最后一次迭代有任何效果...
但是为了让编译器看到它,它需要看到例程的主体。因此,通过调用子例程,您迫使编译器实际执行某项操作 5001 次,而通过内联它,您让它有机会意识到它毫无意义,而且只有一次迭代就可以了!
好吧,这主要是我的猜测,但我已经看到很多次了,我相信我离真相不远了。事实上,由于您随后对 dudr
不执行任何操作,我什至感到惊讶的是编译器会在您的大循环之外生成任何东西...
更多的 MPI 进程需要更长的时间
好吧,对于这一点,我会做一个更大胆的猜测:我相信不知何故,您不是在多个节点中提交,而是在一个节点上错误地提交了您的代码。在那里,许多 MPI 进程争相访问资源(尤其是内存带宽),并且随着进程数量的增加,花费的时间越来越长......我可能是错的,但目前,这是我最好的解释.
一句至理名言
当您尝试对性能进行基准测试时,请确保始终向编译器隐藏您对计算的实际结果不感兴趣。这可以通过打印一些值,或将其传递给外部例程(可能什么也不做,但编译器不知道)来完成。否则,您可能会惊讶于它的 聪明 以及它可以删除多少 死代码 .