我一直在尝试利用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))
Q:我如何获得更好结果?
1 [[st:了解如何避免做无用的工作-您可以直接消除FLOP的一半 不说所有的一半避免了RAM-I / O,每个I / O的成本为+100~350 [ns]
/回写
( 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 #######################
重用预取的数据。不对齐这些预先提取的数据副作用带来的矢量化代码只会让您的代码付出RAM访问延迟的许多倍,而不是聪明地重用已经支付的数据块。根据此原理设计对齐的工作单元意味着还有更多SLOC,但值得的是-谁能立即免费获得2 nd:了解如何按照处理顺序最佳地对齐数据(高速缓存行重用使您
~100x
更快
~100x
更快的CPU + RAM或免费获得~100x
加速,仅仅是因为不编写糟糕或天真的循环迭代器?3 rd:了解如何有效利用numpy
或numba
代码块内部的矢量化(块定向)操作,并避免按下numba来花费时间自动分析呼叫签名(每次调用时您都要为此自动分析花费额外的时间,而您已经设计了代码并确切知道将要去到那里的数据类型,所以为什么每次都要花费额外的时间进行自动分析,块被调用???)
4 th
:了解扩展的Amdahl's Law在何处具有所有相关的附加成本和处理原子性,可以满足您加速的愿望,而不再付出更多比您将获得的回报(至少要证明附加成本合理)...-可能会因没有获得任何报酬而付出额外的费用,但对代码的性能没有任何有益的影响(相反)]5 th
:了解第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-alloc上的时间。重用已经预先分配的内存区域和明智地控制gc
策略是专业人员的又一个标志,专注于低延迟和性能设计