使用Numba时如何并行化这个Python for循环

时间:2017-10-25 04:37:25

标签: python parallel-processing anaconda sparse-matrix numba

我使用了Anaconda的Python分发版以及Numba,并且我编写了以下Python函数,该函数将稀疏矩阵 A (存储在CSR格式)通过密集向量 x

@jit
def csrMult( x, Adata, Aindices, Aindptr, Ashape ):

    numRowsA = Ashape[0]
    Ax       = numpy.zeros( numRowsA )

    for i in range( numRowsA ):
        Ax_i = 0.0
        for dataIdx in range( Aindptr[i], Aindptr[i+1] ):

            j     = Aindices[dataIdx]
            Ax_i +=    Adata[dataIdx] * x[j]

        Ax[i] = Ax_i

    return Ax 

此处 A 是一个较大的scipy稀疏矩阵,

>>> A.shape
( 56469, 39279 )
#                  having ~ 142,258,302 nonzero entries (so about 6.4% )
>>> type( A[0,0] )
dtype( 'float32' )

x 是一个numpy数组。以下是调用上述函数的代码片段:

x       = numpy.random.randn( A.shape[1] )
Ax      = A.dot( x )   
AxCheck = csrMult( x, A.data, A.indices, A.indptr, A.shape )

请注意 @jit -decorator告诉Numba为 csrMult() 函数进行即时编译。

在我的实验中,我的函数csrMult()scipy .dot() 方法相比两倍。对于Numba来说,这是一个令人印象深刻的结果。

然而,MATLAB仍然执行这个矩阵向量乘法,比<{1}} 快<6> 。我相信这是因为MATLAB在执行稀疏矩阵向量乘法时使用多线程。

问题:

如何在使用Numba时并行化外部csrMult() - 循环?

Numba过去常常使用 for 功能,这样可以很容易地并行化并行化 prange() -loops。不幸的是,Numba不再有for [实际上,这是错误的,请参阅下面的编辑]。 那么现在,将这个prange()循环并行化的正确方法是什么,Numba的for函数已经消失了?

当从Numba移除prange()时,Numba的开发人员会考虑哪些替代方案?

  

编辑1:
  我更新到Numba的最新版本,即.35,prange()又回来了!它没有包含在版本.33中,我使用的版本。
  这是个好消息,但不幸的是,当我尝试使用prange()并行化我的for循环时,我收到一条错误消息。这是来自Numba文档的并行for循环example(参见第1.9.2节&#34;显式并行循环&#34;),以下是我的新代码:

prange()

当我使用上面给出的代码片段调用此函数时,收到以下错误:

  

AttributeError:nopython失败(转换为parfors)&#39; SetItem&#39;   对象没有属性&#39; get_targets&#39;

鉴于上述尝试使用from numba import njit, prange @njit( parallel=True ) def csrMult_numba( x, Adata, Aindices, Aindptr, Ashape): numRowsA = Ashape[0] Ax = np.zeros( numRowsA ) for i in prange( numRowsA ): Ax_i = 0.0 for dataIdx in range( Aindptr[i],Aindptr[i+1] ): j = Aindices[dataIdx] Ax_i += Adata[dataIdx] * x[j] Ax[i] = Ax_i return Ax 崩溃,我的问题是:

正确的方法(使用prange或替代方法)并行化这个Python prange - 循环?

如下所述,在C ++中并行化类似for循环并获得 8x 加速,在 20 -omp-threads上运行是微不足道的。必须有一种方法可以使用Numba,因为for循环是令人尴尬的平行(并且因为稀疏矩阵 - 向量乘法是科学计算中的基本操作)。

  

编辑2:
  这是我的for的C ++版本。并行化C ++版本中的csrMult()循环使得代码在我的测试中快了大约8倍。这告诉我,在使用Numba时,Python版本应该可以实现类似的加速。

for()

2 个答案:

答案 0 :(得分:5)

Numba已更新, prange()现在正在使用! (我回答了我自己的问题。)

2017年12月12日的blog post讨论了Numba并行计算功能的改进。以下是博客的相关摘录:

  

很久以前(超过20个版本!),Numba曾经支持过   成语为prange()的循环写并行。经过一个专业   在2014年重构代码库,必须删除此功能,   但它一直是最常被请求的Numba功能之一   自那时以来。英特尔开发人员并行化阵列之后   表达式,他们意识到将prange带回来是公平的   易

使用Numba版本0.36.1,我可以使用以下简单代码并行化我令人尴尬的并行for循环:

@numba.jit(nopython=True, parallel=True)
def csrMult_parallel(x,Adata,Aindices,Aindptr,Ashape): 

    numRowsA = Ashape[0]    
    Ax = np.zeros(numRowsA)

    for i in numba.prange(numRowsA):
        Ax_i = 0.0        
        for dataIdx in range(Aindptr[i],Aindptr[i+1]):

            j = Aindices[dataIdx]
            Ax_i += Adata[dataIdx]*x[j]

        Ax[i] = Ax_i            

    return Ax

在我的实验中,并行化for - 循环使得函数的执行速度比我在问题开始时发布的版本快8倍,后者已经使用了Numba,但是没有并行化。此外,在我的实验中,并行化版本比使用scipy的稀疏矩阵向量乘法函数的命令Ax = A.dot(x)快约5倍。 Numba粉碎了scipy ,我终于有了一个python稀疏矩阵向量乘法程序,和MATLAB一样快

答案 1 :(得分:4)

感谢你的量子更新,丹尼尔。
以下几行可能难以接受,但请相信我,还有更多的事情需要考虑。我曾研究过HPC /并行计算问题,这些问题包括〜 N [TB]; N > 10 的矩阵及其稀疏的伴随,因此一些经验可能对您的进一步观点有用。

警告:不要指望任何晚餐免费送达

希望并行化一段代码听起来像是一种越来越常见的当代重新表达的法术力。 问题不是代码,而是此类移动的成本。

经济是头号问题。最初由Gene Amdahl制定的Amdahl Law没有考虑[PAR] - 流程设置+ [PAR]的成本 - 流程 - 终结和&amp;终结,确实必须在每个现实世界的实施中支付。

The overhead-strict Amdahl's Law depicts the scale of these un-avoidable adverse effects and helps understand a few new aspects that have to be evaluated before one opts to introduce parallelisation(以可接受的成本付费,因为非常非常非常容易支付超过一个人可能获得的收益 - 从降低处理性能的天真失望是更容易的部分故事)。

如果愿意更好地理解这个主题并且预先计算实际的&#34,请随意阅读更多关于开销 - 严格Amdahl法律重新制定的帖子; 最小&#34; -subProblem - &#34; size &#34; ,其中总和 - {{1从现实世界的工具中获得至少合理的开销,用于将subProblem的并行拆分引入 [PAR] (不是任何&#34;只是&#34) ; - N_trully_[PAR]_processes,但是真实 - [CONCURRENT] - 这些方式并不相同。)

Python可能会获得一剂类固醇以提高性能:

Python是一个很棒的原型生态系统,而 [PARALLEL] numba 和其他编译扩展有助于提升性能方式GIL步进式python(co - ) - 处理通常比原生的更远。

在这里,您尝试强制 numpy 来安排作业几乎 - 免费,只需通过其自动numba.jit() - 时间词汇-analyser(你抛出你的代码),它们都应该理解&#34;你的全球目标(要做什么),并提出一些矢量化技巧(如何最好组装一堆CPU指令以最大限度地提高代码执行效率)。

这听起来很容易,但事实并非如此。

Travis Oliphant的团队已经在jit()工具上取得了很大的进步,但让我们切合实际,不要期望任何形式的自动魔法获得在numba - lexer +代码分析中实现,在尝试转换代码并组装更高效的机器指令流以实现高级任务的目标时。

.jit()?这里?严重?

由于@guvectorize调整大小,您可能会立即忘记[PSPACE]以某种方式有效地&#34;东西&#34;带有数据的GPU引擎,其内存占用量远远落后于GPU-GDDR的大小(完全没有说到这一点 - &#34;浅#34; GPU内核大小用于数学 - &#34;微小& #34;处理加倍,可能在numba,但稍后在[PAR]求和。

(重新 - ) - 使用数据加载GPU需要大量时间。如果支付了这笔费用,那么GPU内存延迟对于小型&#34; -GPU内核的经济性也不是很友好 - 你的GPU-SMX代码执行将必须支付〜{{ 1}}只是为了获取一个数字(很可能不会自动重新对齐以便在后续步骤中使用最佳合并的SM缓存重用,你可能会注意到,你从来没有,让我重复一遍,永远不要完全重复使用单个矩阵单元,因此缓存本身不会在每个矩阵单元格[SEQ]下提供任何内容),而智能纯350-700 [ns] - 矢量化代码可以处理矩阵 - 即使是最大的350~700 [ns] - 足迹,每个单元格中的矢量积小于numpy

这是与之比较的标准。

(分析会更好地显示这些事实,但这个原则事先是众所周知的,没有测试如何将一些1 [ns]数据移动到GPU-fabric上只是为了实现这一点。拥有。)

最糟糕的坏消息:

考虑到矩阵[PSPACE] 的内存规模,预期效果更差的是,矩阵表示的存储稀疏组织最有可能破坏性最大,如果并非所有,TB可实现的性能提升 - 在密集矩阵表示上的矢量化技巧,因为高效内存获取高速缓存行重用的可能性几乎为零,稀疏性也将破坏任何简单的方法来实现矢量化操作的紧凑映射很难保持很容易地转换为高级CPU硬件矢量处理资源。

可解决问题的清单:

  • 总是更好地预先分配向量A并将其作为另一个参数传递到代码的numba - 编译部分,以避免重复支付额外的Ax = np.zeros_like( A[:,0] ) - 费用创建(再次)新的内存分配(如果向量被怀疑在外部协调的迭代优化过程中使用,则越多)
  • 总是更好地指定(缩小普遍性,为了产生代码性能)至少numba.jit() - 调用接口指令
  • 始终查看所有[PTIME,PSPACE] - 可用选项及其各自的默认值(可能会将版本更改为版本)以了解您的具体情况(禁用GIL并更好地将目标与{{1}对齐} +硬件功能将始终有助于代码的数字密集部分)
numba.jit( "f8[:]( f4[:], f4[:,:], ... )" )