【问题标题】:Comparing Python accelerators (Cython,Numba,f2py) to Numpy einsum将 Python 加速器(Cython、Numba、f2py)与 Numpy einsum 进行比较
【发布时间】:2016-05-07 13:09:55
【问题描述】:

我将 Python 加速器(Numba、Cython、f2py)与针对特定问题的简单 For 循环和 Numpy 的 einsum 进行比较(见下文)。到目前为止,Numpy 在这个问题上是最快的(速度快了 6 倍),但是如果我应该尝试其他优化,或者我做错了什么,我想要一些反馈。这个简单的代码基于一个更大的代码,它有许多这样的 einsum 调用,但没有显式的 for 循环。我正在检查这些加速器是否可以做得更好。

在 Mac OS X Yosemite 上使用 Python 2.7.9 完成计时,并从 Homebrew 安装 gcc-5.3.0 (--with-fortran --without-multilib)。也做了 %timeit 电话;这些单次调用时间相当准确。

In [1]: %run -i test_numba.py
test_numpy: 0.0805640220642
Matches Numpy output: True

test_dumb: 1.43043899536
Matches Numpy output: True

test_numba: 0.464295864105
Matches Numpy output: True

test_cython: 0.627640008926
Matches Numpy output: True

test_f2py: 5.01890516281
Matches Numpy output: True

test_f2py_order: 2.31424307823
Matches Numpy output: True

test_f2py_reorder: 0.507861852646
Matches Numpy output: True

主要代码:

import numpy as np
import numba
import time
import test_f2py as tf2py
import pyximport
pyximport.install(setup_args={'include_dirs':np.get_include()})
import test_cython as tcyth

def test_dumb(f,b):
    fnew = np.empty((f.shape[1],f.shape[2]))
    for i in range(f.shape[0]):
        for l in range(f.shape[3]):
            fnew += f[i,:,:,l] * b[i,l]
    return fnew


def test_dumber(f,b):
    fnew = np.empty((f.shape[1],f.shape[2]))
    for i in range(f.shape[0]):
        for j in range(f.shape[1]):
            for k in range(f.shape[2]):
                for l in range(f.shape[3]):
                    fnew[j,k] += f[i,j,k,l] * b[i,l]
    return fnew

@numba.jit(nopython=True)
def test_numba(f,b):
    fnew = np.zeros((f.shape[1],f.shape[2])) #NOTE: can't be empty, gives errors
    for i in range(f.shape[0]):
        for j in range(f.shape[1]):
            for k in range(f.shape[2]):
                for l in range(f.shape[3]):
                    fnew[j,k] += f[i,j,k,l] * b[i,l]
    return fnew

def test_numpy(f,b):
    return np.einsum('i...k,ik->...',f,b)

def test_f2py(f,b):
    return tf2py.test_f2py(f,b)

def test_f2py_order(f,b):
    return tf2py.test_f2py(f,b)

def test_f2py_reorder(f,b):
    return tf2py.test_f2py_reorder(f,b)

def test_cython(f,b):
    return tcyth.test_cython(f,b)

if __name__ == '__main__':

    #goal is to create: fnew = sum f*b over dim 0 and 3.
    f = np.random.rand(32,33,2000,64)
    b = np.random.rand(32,64)

    f1 = np.asfortranarray(f)
    b1 = np.asfortranarray(b)

    f2 = np.asfortranarray(np.transpose(f,[1,2,0,3]))

    funcs = [test_dumb,test_numba, test_cython, \
            test_f2py,test_f2py_order,test_f2py_reorder]

    tstart = time.time()    
    fnew_numpy= test_numpy(f,b)
    tstop = time.time()
    print test_numpy.__name__+': '+str(tstop-tstart)
    print 'Matches Numpy output: '+str(np.allclose(fnew_numpy,fnew_numpy))
    print ''

    for func in funcs:
        tstart = time.time()
        if func.__name__ == 'test_f2py_order':
            fnew = func(f1,b1)
        elif func.__name__ == 'test_f2py_reorder':
            fnew = func(f2,b1)
        else:
            fnew = func(f,b)
        tstop = time.time()
        print func.__name__+': '+str(tstop-tstart)
        print 'Matches Numpy output: '+str(np.allclose(fnew,fnew_numpy))
        print ''

f2py 文件(使用 f2py -c -m test_f2py test_f2py.F90 编译):

!file: test_f2py
subroutine test_f2py(f,b,fnew,n1,n2,n3,n4)

integer  :: n1,n2,n3,n4
real(8), dimension(n1,n2,n3,n4) :: f
real(8), dimension(n1,n4) :: b
real(8), dimension(n2,n3) :: fnew
!f2py intent(in) f
!f2py intent(in) b
!f2py intent(out) fnew
!f2py intent(in) n1
!f2py intent(in) n2
!f2py intent(in) n3
!f2py intent(in) n4

integer :: i1,i2,i3,i4

do i1=1,n1
    do i2=1,n2
        do i3=1,n3
            do i4=1,n4
                fnew(i2,i3) = fnew(i2,i3) + f(i1,i2,i3,i4)*b(i1,i4)
            enddo
        enddo
    enddo
enddo

end subroutine test_f2py

subroutine test_f2py_reorder(f,b,fnew,n1,n2,n3,n4)

integer  :: n1,n2,n3,n4
real(8), dimension(n1,n2,n3,n4) :: f
real(8), dimension(n3,n4) :: b
real(8), dimension(n1,n2) :: fnew
!f2py intent(in) f
!f2py intent(in) b
!f2py intent(out) fnew
!f2py intent(in) n1
!f2py intent(in) n2
!f2py intent(in) n3
!f2py intent(in) n4

integer :: i1,i2,i3,i4

do i3=1,n3
    do i4=1,n4
        do i1=1,n1
            do i2=1,n2
                fnew(i1,i2) = fnew(i1,i2) + f(i1,i2,i3,i4)*b(i3,i4)
            enddo
        enddo
    enddo
enddo

end subroutine test_f2py_reorder

还有 Cython .pyx 文件(在主程序中使用 pyximport 编译):

#/usr/bin python
import numpy as np
cimport numpy as np

def test_cython(np.ndarray[np.float64_t,ndim=4] f, np.ndarray[np.float64_t,ndim=2] b):
    # cdef np.ndarray[np.float64_t,ndim=4] f
    # cdef np.ndarray[np.float64_t,ndim=2] b
    cdef np.ndarray[np.float64_t,ndim=2] fnew = np.empty((f.shape[1],f.shape[2]),dtype=np.float64)
    cdef int i,j,k,l
    cdef int Ni = f.shape[0]
    cdef int Nj = f.shape[1]
    cdef int Nk = f.shape[2]
    cdef int Nl = f.shape[3]

    for i in range(Ni):
        for j in range(Nj):
            for k in range(Nk):
                for l in range(Nl):
                    fnew[j,k] += f[i,j,k,l] * b[i,l]
    return fnew

【问题讨论】:

  • 既然你已经有了工作代码,你的问题可能更适合CodeReview.SE
  • 在我的笔记本电脑 (OSX 10.9.5) 上运行 Numba 0.23.1 test_numpy() 使用 %timeit 每次循环需要 75.5 毫秒,test_numba() 每次循环需要 123 毫秒,因此差异不会看起来和你的测试一样极端。在对您调用一次的 numba 代码进行基准测试时要特别小心,以便在基准测试之外实际 jit 代码,否则您将在您的数字中包含该成本,而随后的每次调用都会快得多。

标签: python numpy cython numba f2py


【解决方案1】:

通常这些加速器用于加速带有 Python 循环或大量中间结果的代码,而 einsum 已经得到了很好的优化 (see source)。你不应该期望他们轻松击败einsum,但你可能会在性能上接近它。

对于 Numba,从基准测试中排除编译时间很重要。这可以简单地通过运行 jited 函数两次(使用相同类型的输入)来完成。例如。使用 IPython 我得到:

f = np.random.rand(32,33,500,64)
b = np.random.rand(32,64)

%time _ = test_numba(f,b)  # First invocation
# Wall time: 466 ms
%time _ = test_numba(f,b)
# Wall time: 73 ms
%timeit test_numba(f, b)
# 10 loops, best of 3: 72.7 ms per loop
%timeit test_numpy(f, b)
# 10 loops, best of 3: 62.8 ms per loop

可以对您的 Cython 代码进行多项改进:

  1. 禁用对数组边界和环绕的检查,请参阅compiler directives
  2. 指定数组是连续的。
  3. 使用typed memoryviews

类似:

cimport cython
import numpy as np

@cython.boundscheck(False)
@cython.wraparound(False)
def test_cython(double[:,:,:,::1] f, double[:,::1] b):
    cdef int i, j, k, l, Ni, Nj, Nk, Nl
    Ni = f.shape[0]
    Nj = f.shape[1]
    Nk = f.shape[2]
    Nl = f.shape[3]

    fnew = np.empty((Nj, Nk))
    cdef double[:,::1] fnew_v = fnew

    for i in range(Ni):
        for j in range(Nj):
            for k in range(Nk):
                for l in range(Nl):
                    fnew_v[j,k] += f[i,j,k,l] * b[i,l]
    return fnew

在最新的 Ubuntu 15.10 (x86) 上,这给了我与 einsum 相同的速度。但是,在具有 Anaconda 发行版的同一台 PC 上的 Windows (x86) 上,此 Cython 代码的速度大约是 einsum 的一半。我认为这可能与 gcc 版本(5.2.1 与 4.7.0)和插入 SSE 指令的能力有关(einsum 使用 SSE2 内在函数编码)。也许提供不同的编译器选项会有所帮助,但我不确定。

我几乎不知道任何 Fortran,所以我无法对此发表评论。

既然你的目标是击败einsum,我认为下一步显然是着眼于提高并行度。使用cython.parallel 生成一些线程应该相当容易。如果这还没有使您的系统内存带宽饱和,那么您可以尝试明确包含最新的 CPU 指令,如 AVX2 和 Fused Multiply-Add。

您可以尝试的另一件事是重新排序和重塑f 并使用np.dot 进行操作。如果你的 Numpy 带有一个好的 BLAS 库,这应该可以实现几乎所有你能想到的优化,但代价是失去了通用性,并且可能是一个非常昂贵的 f 数组副本。

【讨论】:

    【解决方案2】:

    解析完字符串参数后,einsum 使用 nditer 的编译版本在所有轴上执行积和计算。源码在 numpy github 上很容易找到。

    不久前,我编写了一个 einsum 类似的工作,作为编写补丁的一部分。作为其中的一部分,我编写了一个 cython 脚本来执行乘积之和。您可以在以下位置查看此代码:

    https://github.com/hpaulj/numpy-einsum

    我没有尝试让我的代码以einsum 的速度运行。我只是想了解它是如何工作的。

    【讨论】:

      猜你喜欢
      • 2019-11-17
      • 2014-07-11
      • 1970-01-01
      • 1970-01-01
      • 2016-07-31
      • 2017-08-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多