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

时间:2018-04-08 13:16:50

标签: fortran matrix-multiplication

我正在尝试计算类似于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,因为与Fortran中的python重塑不同会导致将我的数组复制到一个新的数组,导致更多的RAM使用。

有没有办法加快Fortran的性能,以便接近Numpy的性能?

3 个答案:

答案 0 :(得分:2)

您可能无法计算您认为的时间安排。这是一个完整的fortran例子

$ 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

输出:

const loadScript = (source, beforeEl, async = true, defer = true) => {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script');
    const prior = beforeEl || document.getElementsByTagName('script')[0];

    script.async = async;
    script.defer = defer;

    function onloadHander(_, isAbort) {
      if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
        script.onload = null;
        script.onreadystatechange = null;
        script = undefined;

        if (isAbort) { reject(); } else { resolve(); }
      }
    }

    script.onload = onloadHander;
    script.onreadystatechange = onloadHander;

    script.src = source;
    prior.parentNode.insertBefore(script, prior);
  });
}

在这个fortran代码中,计算内部产品的运行时间不到10%。你如何/你的时间是非常重要的。你确定你在fortran和python版本中计时相同吗?你确定你只是计算inner_product计算的时间吗?

答案 1 :(得分:1)

这可以避免制作任何副本。 (注意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是外部的,意味着模块/包含构造中的不是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

答案 2 :(得分:1)

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

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

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

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