numba guvectorize target='parallel' 比 target='cpu' 慢

numba guvectorize target='parallel' slower than target='cpu'

我一直在尝试优化一段涉及大型多维数组计算的 python 代码。我在使用 numba 时得到了违反直觉的结果。我 运行 正在使用 MBP,2015 年年中,2.5 GHz i7 四核,OS 10.10.5,python 2.7.11。考虑以下因素:

 import numpy as np
 from numba import jit, vectorize, guvectorize
 import numexpr as ne
 import timeit

 def add_two_2ds_naive(A,B,res):
     for i in range(A.shape[0]):
         for j in range(B.shape[1]):
             res[i,j] = A[i,j]+B[i,j]

 @jit
 def add_two_2ds_jit(A,B,res):
     for i in range(A.shape[0]):
         for j in range(B.shape[1]):
             res[i,j] = A[i,j]+B[i,j]

 @guvectorize(['float64[:,:],float64[:,:],float64[:,:]'],
    '(n,m),(n,m)->(n,m)',target='cpu')
 def add_two_2ds_cpu(A,B,res):
     for i in range(A.shape[0]):
         for j in range(B.shape[1]):
             res[i,j] = A[i,j]+B[i,j]

 @guvectorize(['(float64[:,:],float64[:,:],float64[:,:])'],
    '(n,m),(n,m)->(n,m)',target='parallel')
 def add_two_2ds_parallel(A,B,res):
     for i in range(A.shape[0]):
         for j in range(B.shape[1]):
             res[i,j] = A[i,j]+B[i,j]

 def add_two_2ds_numexpr(A,B,res):
     res = ne.evaluate('A+B')

 if __name__=="__main__":
     np.random.seed(69)
     A = np.random.rand(10000,100)
     B = np.random.rand(10000,100)
     res = np.zeros((10000,100))

我现在可以 运行 对各种功能计时:

%timeit add_two_2ds_jit(A,B,res)
1000 loops, best of 3: 1.16 ms per loop

%timeit add_two_2ds_cpu(A,B,res)
1000 loops, best of 3: 1.19 ms per loop

%timeit add_two_2ds_parallel(A,B,res)
100 loops, best of 3: 6.9 ms per loop

%timeit add_two_2ds_numexpr(A,B,res)
1000 loops, best of 3: 1.62 ms per loop

似乎 'parallel' 甚至没有使用大部分单核,因为它在 top 中的使用表明 python 达到了 ~40% cpu 'parallel',~100% 'cpu',numexpr 命中~300%。

您的@guvectorize 实现有两个问题。首先是您在@guvectorize 内核中执行所有循环,因此实际上 Numba 并行目标没有要并行化的内容。 @vectorize 和@guvectorize 都在ufunc/gufunc 中的广播维度上并行化。由于你的 gufunc 的签名是二维的,你的输入是二维的,所以只有一次调用内部函数,这解释了你看到的唯一 100% CPU 用法。

编写上述函数的最佳方法是使用常规 ufunc:

@vectorize('(float64, float64)', target='parallel')
def add_ufunc(a, b):
    return a + b

然后在我的系统上,我看到这些速度:

%timeit add_two_2ds_jit(A,B,res)
1000 loops, best of 3: 1.87 ms per loop

%timeit add_two_2ds_cpu(A,B,res)
1000 loops, best of 3: 1.81 ms per loop

%timeit add_two_2ds_parallel(A,B,res)
The slowest run took 11.82 times longer than the fastest. This could mean that an intermediate result is being cached 
100 loops, best of 3: 2.43 ms per loop

%timeit add_two_2ds_numexpr(A,B,res)
100 loops, best of 3: 2.79 ms per loop

%timeit add_ufunc(A, B, res)
The slowest run took 9.24 times longer than the fastest. This could mean that an intermediate result is being cached 
1000 loops, best of 3: 2.03 ms per loop

(这是一个与您的系统非常相似的 OS X 系统,但具有 OS X 10.11。)

虽然 Numba 的并行 ufunc 现在打败了 numexpr(我看到 add_ufunc 使用了大约 280% CPU),但它并没有打败简单的单线程 CPU 情况。我怀疑瓶颈是由于内存(或缓存)带宽造成的,但我还没有进行测量来检查。

一般来说,如果您对每个内存元素(例如,余弦)执行更多数学运算,您将从并行 ufunc 目标中获益更多。