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

发布于 2021-01-29 17:14:15

我一直在尝试优化一段涉及大型多维数组计算的python代码。我在numba上得到了违反直觉的结果。我正在2015年中期的MBP,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了:

%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

似乎“并行”甚至不占用单个内核的大部分资源,因为它的用法top表明python的“并行”命中率约为40%,cpu的命中率为100%,numexpr的命中率为300%

关注者
0
被浏览
145
1 个回答
  • 面试哥
    面试哥 2021-01-29
    为面试而生,有面试问题,就找面试哥。

    @guvectorize实现有两个问题。首先是您正在@guvectorize内核中进行所有循环,因此Numba并行目标实际上没有什么可以并行化。@vectorize和@guvectorize都在ufunc
    /
    gufunc中的广播维度上并行化。由于gufunc的签名是2D,而您的输入是2D,因此只有一次对内部函数的调用,这解释了您看到的唯一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目标中受益匪浅。



知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看