Numba Python - 如何有效地利用并行性?



我一直在尝试利用 Numba 来加速大数组计算。我一直在测量 GFLOPS 中的计算速度,它一直远远低于我对 CPU 的期望。

我的处理器是 i9-9900k,根据 float32 基准测试,它应该能够超过 200 GFLOPS。在我的测试中,我从未超过大约 50 GFLOPS。这在所有 8 个内核上运行。

在单个内核上,我实现了大约 17 GFLOPS,这(我相信(是理论性能的 50%。我不确定这是否可以改进,但它不能很好地扩展到多核这一事实是一个问题。

我正在尝试学习这一点,因为我计划编写一些图像处理代码,这些代码迫切需要尽可能提高速度。我也觉得在我涉足 GPU 计算之前,我应该先了解这一点。

下面是一些示例代码,其中包含我编写快速函数的一些尝试。我正在测试的操作是将数组乘以 float32,然后将整个数组相加,即 MAC 操作。

如何获得更好的效果?

import os
# os.environ["NUMBA_ENABLE_AVX"] = "1"
import numpy as np
import timeit
from timeit import default_timer as timer
import numba
# numba.config.NUMBA_ENABLE_AVX = 1
# numba.config.LOOP_VECTORIZE = 1
# numba.config.DUMP_ASSEMBLY = 1
from numba import float32, float64
from numba import jit, njit, prange
from numba import vectorize
from numba import cuda
lengthY = 16 # 2D array Y axis
lengthX = 2**16 # X axis
totalops = lengthY * lengthX * 2 # MAC operation has 2 operations
iters = 100
doParallel = True

@njit(fastmath=True, parallel=doParallel)
def MAC_numpy(testarray):
output = (float)(0.0)
multconst = (float)(.99)
output = np.sum(np.multiply(testarray, multconst))
return output

@njit(fastmath=True, parallel=doParallel)
def MAC_01(testarray):
lengthX = testarray.shape[1]
lengthY = testarray.shape[0]
output = (float)(0.0)
multconst = (float)(.99)
for y in prange(lengthY):
for x in prange(lengthX):
output += multconst*testarray[y,x]
return output

@njit(fastmath=True, parallel=doParallel)
def MAC_04(testarray):
lengthX = testarray.shape[1]
lengthY = testarray.shape[0]
output = (float)(0.0)
multconst = (float)(.99)
for y in prange(lengthY):
for x in prange(int(lengthX/4)):
xn = x*4
output += multconst*testarray[y,xn] + multconst*testarray[y,xn+1] + multconst*testarray[y,xn+2] + multconst*testarray[y,xn+3]
return output

# ======================================= TESTS =======================================
testarray = np.random.rand(lengthY, lengthX)
# ==== MAC_numpy ====
time = 1000
for n in range(iters):
start = timer()
output = MAC_numpy(testarray)
end = timer()
if((end-start) < time): #get shortest time
time = end-start
print("nMAC_numpy")
print("output = %f" % (output))
print(type(output))
print("fastest time = %16.10f us" % (time*10**6))
print("Compute Rate = %f GFLOPS" % ((totalops/time)/10**9))
# ==== MAC_01 ====
time = 1000
lengthX = testarray.shape[1]
lengthY = testarray.shape[0]
for n in range(iters):
start = timer()
output = MAC_01(testarray)
end = timer()
if((end-start) < time): #get shortest time
time = end-start
print("nMAC_01")
print("output = %f" % (output))
print(type(output))
print("fastest time = %16.10f us" % (time*10**6))
print("Compute Rate = %f GFLOPS" % ((totalops/time)/10**9))
# ==== MAC_04 ====
time = 1000
for n in range(iters):
start = timer()
output = MAC_04(testarray)
end = timer()
if((end-start) < time): #get shortest time
time = end-start
print("nMAC_04")
print("output = %f" % (output))
print(type(output))
print("fastest time = %16.10f us" % (time*10**6))
print("Compute Rate = %f GFLOPS" % ((totalops/time)/10**9))

如何获得更好的结果?

1st学习如何避免做无用的工作 - 你可以直接消除一半的FLOP-s而不是所有RAM-I/O-s避免的一半,每个成本为每次写回+100~350 [ns]

由于 MUL 和 ADD( a.C + b.C ) == ( a + b ).C的分配性质,最好先np.sum( A ),然后再用(浮点数(常数MUL总和。

#utput = np.sum(np.multiply(testarray, multconst)) # AWFULLY INEFFICIENT
output = np.sum(            testarray)*multconst #######################

2nd了解如何按照处理顺序最好地对齐数据(缓存行重用可以让您更快地重用预取的数据~100x。不按照这些已经预先获取的数据副作用对齐矢量化代码,只会让您的代码支付许多倍的 RAM 访问延迟,而不是智能地重用已经支付的数据块。根据这一原则设计对齐的工作单元意味着更多的 SLOC,但回报是值得的——谁现在免费获得更快的 CPU + RAM~100x或者免费获得大约~100x加速,只是因为不编写设计糟糕或天真的循环迭代器?

3rd学习如何有效地利用numpynumba代码块内部的矢量化(块导向(操作,并避免按numba花时间自动分析调用签名(每次调用时,您为此自动分析支付额外的时间,而您已经设计了代码并且确切地知道哪些数据类型将要去那里,那么为什么每次调用numba块时都要为自动分析支付额外的时间???(

第4了解扩展的阿姆达尔定律在哪里,将所有相关的附加成本和处理原子性放入游戏中,支持您获得加速的愿望,而不是支付超过您获得的回报(至少证明附加成本的合理性...... - 为没有得到任何奖励支付额外费用是可能的,但对代码的性能没有有益的影响(相反(

第 5了解手动创建的内联何时以及如何保存您的代码,一旦步骤 1-4 被很好地学习并以适当的工艺进行常规练习( 使用流行的 COTS 框架很好,但这些框架可能会在几天的工作后提供结果,而手工制作的单一用途智能设计的汇编代码能够在大约 12 分钟内获得相同的结果(! 没有几天没有任何 GPU/CPU 技巧等 - 是的,更快 - 只需不执行比大型矩阵数据数值处理所需的步骤多一步(


我有没有提到float32在小尺度上的处理速度比float64慢,而在更大的数据~ n [GB]尺度上,RAM I/O 时间增长得更慢,以获得更有效的float32预取?这在这里永远不会发生,因为float64数组在这里处理。当然,除非显式指示构造函数向下转换默认数据类型,如下所示:
np.random.rand( lengthY, lengthX ).astype( dtype = np.float32 )

>>> np.random.rand( 10, 2 ).dtype
dtype('float64')

避免大量内存分配是另一种性能技巧,numpy调用签名都支持。对大型阵列使用此选项将为您节省大量浪费在大型临时阵列的 mem-分配上的额外时间。重用已经预先分配的内存区域和明智控制的gc监管是专业的另一个标志,专注于低延迟和性能设计

最新更新