如何在 Fortran 中有效地计算矩阵内积?

Posted

技术标签:

【中文标题】如何在 Fortran 中有效地计算矩阵内积?【英文标题】:How to efficiently calculate matrix inner product in Fortran? 【发布时间】:2018-04-08 13:16:49 【问题描述】:

我正在尝试计算类似于 Fortran 中加权矩阵内积的东西。我用于计算内积的当前脚本如下

! --> In
real(kind=8), intent(in), dimension(ni, nj, nk, nVar) :: U1, U2
real(kind=8), intent(in), dimension(ni, nj, nk) :: intW

! --> Out
real(kind=8), intent(out) :: innerProd

! --> Local
integer :: ni, nj, nk, nVar, iVar

! --> Computing inner product
do iVar = 1, nVar
    innerProd = innerProd + sum(U1(:,:,:,iVar)*U2(:,:,:,iVar)*intW)
enddo

但是我发现我目前使用的上述脚本效率不是很高。使用 NumPy 在 Python 中可以执行相同的操作,如下所示,

import numpy as np 
import os

# --> Preventing numpy from multi-threading
os.environ['OPENBLAS_NUM_THREADS'] = '1'
os.environ['MKL_NUM_THREADS'] = '1'   

innerProd = 0

# --> Toy matrices
U1 = np.random.random((ni,nj,nk,nVar))
U2 = np.random.random((ni,nj,nk,nVar))
intW = np.random.random((ni,nj,nk))

# --> Reshaping 
U1 = np.reshape(np.ravel(U1), (ni*nj*nk, nVar))
U2 = np.reshape(np.ravel(U1), (ni*nj*nk, nVar))
intW = np.reshape(np.ravel(intW), (ni*nj*nk))

# --> Calculating inner product
for iVar in range(nVar):
    innerProd = innerProd + np.dot(U1[:, iVar], U2[:, iVar]*intW)

使用 Numpy 的第二种方法似乎比使用 Fortran 的方法快得多。对于ni = nj = nk = nVar = 130的具体情况,两种方法所用时间如下

 fortran_time = 25.8641 s
 numpy_time = 6.8924 s

我尝试使用来自 BLAS 的ddot 改进我的 Fortran 代码,如下所示,

do iVar = 1, nVar
    do k = 1, nk
        do j = 1, nj
            innerProd = innerProd + ddot(ni, U1(:,j,k,iVar), 1, U2(:,j,k,iVar)*intW(:,j,k), 1)
        enddo
    enddo
enddo

但时间并没有明显改善。对于ni = nj = nk = nVar = 130 的情况,上述方法所用的时间是~24s。 (我忘了提到我用'-O2'选项编译了Fortran代码以优化性能)。

不幸的是,Fortran 中没有用于逐元素矩阵乘法的 BLAS 函数。而且我不想在 Fortran 中使用 reshape,因为与 Python 不同,Fortran 中的 reshape 会导致将我的数组复制到一个新数组,从而导致更多的 RAM 使用。

有没有什么办法可以加快 Fortran 的性能,从而接近 Numpy 的性能?

【问题讨论】:

请注意 kind=8 是不可移植且丑陋的。 你确定 numpy 使用单核吗? @AndrasDeak 是的,我很确定 numpy 只使用一个内核。我使用 htop 检查了核心使用情况。由于多线程,我非常有信心 NumPy 更快,但令我惊讶的是我错了。 我想知道将您的权重数组复制到完整大小并摆脱循环是否会有所帮助。你能试试吗? (我留下了这个评论,然后以为我很困惑并删除了它,但现在我再次认为这实际上与您的内在产品相对应;但我仍然不确定)。 我已经尝试过你的代码,循环在 0.6 秒内完成。您如何安排问题的时间? 【参考方案1】:

您可能没有按照自己的想法进行计时。这是一个完整的fortran示例

program test                                                        
    use iso_fortran_env, r8 => real64                               
    implicit none                                                   

    integer, parameter :: ni = 130, nj = 130, nk = 130, nvar = 130  
    real(r8), allocatable :: u1(:,:,:,:), u2(:,:,:,:), w(:,:,:)     
    real(r8) :: sum, t0, t1                                         
    integer :: i,j,k,n                                              

    call cpu_time(t0)                                               
    allocate(u1(ni,nj,nk,nvar))                                     
    allocate(u2(ni,nj,nk,nvar))                                     
    allocate(w(ni,nj,nk))                                           
    call cpu_time(t1)                                               
    write(*,'("allocation time(s):",es15.5)') t1-t0                 

    call cpu_time(t0)                                               
    call random_seed()                                              
    call random_number(u1)                                          
    call random_number(u2)                                          
    call random_number(w)                                           
    call cpu_time(t1)                                               
    write(*,'("random init time (s):",es15.5)') t1-t0               

    sum = 0.0_r8                                                    
    call cpu_time(t0)                                               
    do n = 1, nvar                                                  
        do k = 1, nk                                                
            do j = 1, nj                                            
                do i = 1, ni                                        
                    sum = sum + u1(i,j,k,n)*u2(i,j,k,n)*w(i,j,k)    
                end do                                              
            end do                                                  
        end do                                                      
    end do                                                          
    call cpu_time(t1)                                               
    write(*,'("Sum:",es15.5," time(s):",es15.5)') sum, t1-t0        

end program

还有输出:

$ gfortran -O2 -o inner_product inner_product.f90            
$ time ./inner_product 
allocation time(s):    3.00000E-05
random init time (s):    5.73293E+00
Sum:    3.57050E+07 time(s):    5.69066E-01

real    0m6.465s
user    0m4.634s
sys 0m1.798s

在这个 fortran 代码中计算内积的时间不到 10%。你如何/什么时间是非常重要的。你确定你在 fortran 和 python 版本中的时间是一样的吗?你确定你只计时 inner_product 计算?

【讨论】:

【参考方案2】:

这样可以避免制作任何副本。 (注意 blas ddot 方法仍然需要为元素产品制作副本)

   subroutine dot3(n,a,b,c,result)
   implicit none
   real(kind=..) a(*),b(*),c(*),result
   integer i,n
   result=0
   do i=1,n
    result=result+a(i)*b(i)*c(i)
   enddo
   end

dot3 是外部的,意味着 not 在模块/包含结构中。 kind 显然应该匹配主声明。

在主代码中:

  innerprod=0
  do iVar = 1, nVar 
  call dot3(ni*nj*nk, U1(1,1,1,iVar),U2(1,1,1,iVar),intW,result)
  innerProd=innerProd+result
  enddo

【讨论】:

是的,我刚才尝试了这种方法。但是运行时间仍然没有改善。它仍然是~24-25 s。我很惊讶我在 Fortran 中尝试过的所有方法都具有几乎相同的运行时间。 @AdhityaRavi 我不会对此感到惊讶。在金属附近,您只需要遍历内存中的每个项目,乘以三个双精度数,然后总结结果;没有太大的改进空间。我们可以做的是尽量减少开销,例如创建临时数组。只有在高级语言中,根据您的方法可能会出现巨大差异,例如本机 python 循环与 numpy 魔术。但是,如果 4 的差异因素不想消失,无论你做什么,我仍然会怀疑多核恶作剧...... @AndrasDeak 我猜你是对的。我也感觉到 numpy 以某种方式是多线程的,因为我找不到其他解释来说明它快 4 到 5 倍。我尝试使用os.environ['OPENBLAS_NUM_THREADS'] = '1'os.environ['MKL_NUM_THREADS'] = '1' 来抑制这种情况。但是,我不确定如何确保 numpy 使用单核。 @AdhityaRavi 众所周知,影响这一点的 envvar 取决于 numpy 使用的 blas 库,所以它可能是其他东西,例如 OMP_NUM_THREADS 我认为。不过,您之前提到的 CPU 负载应该是确定的。无论如何,您始终可以通过从 numpy 版本中删除线程限制开关来检查它是否运行得快 n 倍(以及 CPU 负载是否相应增加)。 请注意,过程是外部的、内部的、模块的还是其他任何对序列关联处理参数的方式没有影响。【参考方案3】:

比较 Numpy 和 Fortran 代码时,我有同样的观察结果。

差异原来是 BLAS 的版本,我发现使用 netlib 中的 DGEMM 类似于循环,并且比 OpenBLAS 慢大约三倍(请参阅this 答案中的配置文件)。

对我来说最令人惊讶的是,OpenBLAS 提供的代码比仅编译 Fortran 三重嵌套循环要快得多。看来这就是 GotoBLAS 的重点,它是用汇编代码为处理器架构手写的。

即使定时正确、正确排序循环、避免复制并使用每个优化标志(在 gfortran 中),性能仍然比 OpenBLAS 慢大约三倍。我没有尝试过 ifort 或 pgi,但我想知道这是否解释了@kvantour 的赞成评论“我的循环在 0.6 秒内完成”(注意,在某些实现中,内在的 matmul 被 BLAS 取代)。

【讨论】:

我不再从事我发布此问题的项目。但是,这听起来像是一个非常合理的解释。感谢您的回答,我会将此线程转发给仍在使用慢速 fortran 代码的同事,以检查我的情况是否也发生了这种情况,以便我可以关闭;)。

以上是关于如何在 Fortran 中有效地计算矩阵内积?的主要内容,如果未能解决你的问题,请参考以下文章

fortran 错误地调用子例程

如何更有效地存储距离矩阵?

在Matlab中有效地计算成对平方欧几里德距离

如何在 Fortran 中初始化二维数组

如何在 Fortran 中初始化二维数组

如何在 Fortran 中初始化二维数组