numba最新官方文档(待续)

  • 2021 年 2 月 20 日
  • AI

//numba.readthedocs.io/en/stable/user/troubleshoot.htmlnumba.readthedocs.io

1、numba的安装问题,默认用pip安装就行,但是numba的并行功能如果要使用intel高性能的tbb后端则需要对tbb进行配置,

//software.intel.com/content/www/us/en/develop/documentation/advisor-user-guide/top/appendix/adding-parallelism-to-your-program/adding-the-parallel-framework-to-your-build-environment/defining-the-tbbroot-environment-variable.htmlsoftware.intel.com

win10可见上,比较麻烦,numba的官网里提到tbb和openmp在不同的条件下表现各有优劣,嫌麻烦直接pip install 就行;

各种组件安装可见:

Installation – Numba 0.52.0-py3.7-linux-x86_64.egg documentationnumba.readthedocs.io

使用默认的pip安装基本够用了

2、numba的noPython模式:

Numba的用法很简单,目标函数加一个装饰器就可以了,@jit装饰器从根本上以两种编译模式( nopython模式和object模式)运行,基本上要选择nopython的模式,否则完全没法加速,nopython编译模式实质上是编译装饰后的函数,以便其完全运行而无需Python解释器的参与,这是使用Numbajit装饰器的推荐和最佳实践方法,因为它可以带来最佳性能。

如果在nopython模式下编译失败,则Numba会提示你用object模式0,在这种模式下,Numba将识别可以编译的循环并将其编译为在机器代码中运行的函数,并在解释器中运行其余代码。为了获得最佳性能,请避免使用此模式

3、原理性部分不解释了,之前的文章写得够清楚了:

马东什么:为什么python这么慢?numba解决了什么问题?zhuanlan.zhihu.com图标


然后统一介绍一下,numba中的各种装饰器

1、jit

from numba import jit

@jit
def f(x, y):
    # A somewhat trivial example
    return x + y


from numba import jit, int32

@jit(int32(int32, int32))
def f(x, y):
    # A somewhat trivial example
    return x + y

numba支持像cython一样定义函数输入的变量的数据类型,但是这并不会起到加速的作用,而在cython中,指定输入的变量的数据类型可以大大加速程序运行的速度。

@jit
def square(x):
    return x ** 2


@jit
def hypot(x, y):
    return math.sqrt(square(x) + square(y))

不同的numba函数可以互相调用。

@jit装饰器可以使用多种数据类型:

  • void是什么都不返回的返回类型(实际上是None从Python调用时返回的)
  • intpuintp是指针大小的整数(分别为有符号和无符号)
  • intc和uintc等效于Cint和 整数类型unsigned int
  • int8uint8int16uint16int32uint32int64uint64相应的位宽度的被固定宽度的整数(符号和无符号)
  • float32float64分别是单精度和双精度浮点数
  • complex64complex128分别是单精度和双精度复数
  • 可以通过索引任何数字类型来指定数组类型,例如float32[:] 用于一维单精度数组或int8[:,:]用于8位整数的二维数组,依次类推

然后介绍jit的一些重要配置:

@jit(nopython=True)
def f(x, y):
    return x + y


@jit(nogil=True)
def f(x, y):
    return x + y



@jit(cache=True)
def f(x, y):
    return x + y



@jit(nopython=True, parallel=True)
def f(x, y):
    return x + y

Numba有两种编译模式:nopython模式object模式。前者产生的代码的运行速度要快得多,但是有一些局限性,可能迫使Numba退回到后者。为防止Numba退回,而是引发错误,请使用nopython=True

每当Numba将Python代码优化为仅适用于本机类型和变量(而不是Python对象)的本机代码时,就不再需要保持Python的全局解释器锁(GIL)。通过nogil=True,Numba会释放GIL。

在发布了GIL的情况下运行的代码与其他执行Python或Numba代码的线程(相同的编译函数或另一个)并行运行,从而使您能够利用多核系统。如果函数是在object模式下编译的,则这是不可能的。

使用时nogil=True,您必须警惕多线程编程的常见陷阱(一致性,同步,竞争条件等)。

为了避免每次调用Python程序时都要进行编译,可以指示Numba将函数编译的结果写入基于文件的缓存中。这可以通过传递cache=True 来实现;

为已知具有并行语义的函数中的那些操作启用自动并行化(和相关的优化)。有关支持的操作的列表,请参见使用@jit自动并行化。此功能通过传递启用,parallel=True并且必须与nopython=True一起使用:


更加灵活的generated_git

虽然jit()装饰是很多情况下是有用的,但是有时你想实现取决于输入类型有不同的返回的功能。那么可以使用generated_jit(),它装饰允许用户控制在编译时的特化的选择,同时完全保留了JIT功能的运行时执行速度。

假设您想编写一个函数,该函数根据某些约定返回给定值是否为“缺失”值。为了便于说明,我们采用以下定义:

  • 对于浮点参数,缺少的值是 NaN
  • 对于Numpy datetime64和timedelta64参数,缺少的值是 NaT
  • 其他类型没有缺失值的概念。

使用generated_jit()装饰器可以轻松实现该编译时逻辑 :

from numba import generated_jit, types

@generated_jit(nopython=True)
def is_missing(x):
    """
    Return True if the value is missing, False otherwise.
    """
    if isinstance(x, types.Float):
        return lambda x: np.isnan(x)
    elif isinstance(x, (types.NPDatetime, types.NPTimedelta)):
        # The corresponding Not-a-Time value
        missing = x('NaT')
        return lambda x: x == missing
    else:
        return lambda x: False

这里有几件事要注意:

  • 装饰函数将使用 参数的Numba类型而不是其值进行调用。
  • 装饰的函数实际上并不计算结果,它返回一个可调用的函数,用于实现给定类型的函数的实际定义。
  • 可以在编译时预先计算一些数据(missing 上面的变量),以使它们在已编译的实现中重用。
  • 函数定义使用与修饰后的函数中的参数相同的名称,这是确保通过名称传递参数按预期工作所必需的。

generated_jit的参数和jit完全一样;


在介绍numba的ufunc函数之前,先介绍numpy的ufunc函数,实际上我们平常使用的向量计算大都是一种内置的numpy的ufunc函数,ufunc是numpy速度如此迅速的一个重要原因,即向量化计算:

Gemfield:为什么向量化计算(vectorization)会这么快?zhuanlan.zhihu.com图标

简单来说,向量化计算就是将多次循环的计算转化为一次矩阵的运算,例如矩阵的加法,简单实现循环两个矩阵每一行的数据进行相加,但是从矩阵的角度,进行一次计算即可,具体的例子看看这几篇文章的demo就可以了:

numpy的ufunc_pyStar_公众号的博客-CSDN博客blog.csdn.net图标NumPy-ufunc 函数_烟雨-CSDN博客blog.csdn.net图标NumPy ufunc通用函数c.biancheng.net

当然,这里我们要提的是自定义的ufunc,即通过编写python代码,并且通过一些方式将代码逻辑转化为可以实现向量化计算的函数。

例如这里的例子:

Numpy:自定义ufunc函数_bqw的博客-CSDN博客_numpy 自定义函数blog.csdn.net图标

# triangle_wave()是一个将x值转换成三角波上对应的y值
def triangle_wave(x,c,c0,hc):
    x = x - int(x)
    if x >= c: r =0.0
    elif x< c0: r = x/c0*hc
    else: r = (c-x)/(c-c0)*hc
    return r

在numpy中我们可以使用:

triangle_ufunc1 = np.frompyfunc(triangle_wave,4,1)
y2 = triangle_ufunc1(x,0.6,0.4,1.0)

4和1分别表示函数的输入参数的个数和函数的输出结果的个数。

除此之外还可以使用vectorize:

triangle_ufunc2 = np.vectorize(triangle_wave,otypes=[np.float])
y3 = triangle_ufunc2(x,0.6,0.4,1.0)
print(y3.dtype)

二者的区别在于后者可以显式地定义输入和输出的数据的类型

numpy.vectorize – NumPy v1.21.dev0 Manualwww.osgeo.cn

简单来说,numpy的向量化函数和pandas中的map是非常相似的

numba也存在一样的功能:

在标量上运行的函数使用@vectorize

在高维数组和标量上运行的函数使用@guvectorize

@vectorize

Numba的向量化功能可将采用标量输入参数的Python函数用作NumPy 的ufuncs。创建传统的NumPy ufunc不是最简单的过程,它涉及编写一些C代码。Numba可以轻松做到这一点。使用vectorize()装饰器,Numba可以将纯Python函数编译为ufunc,该ufunc以与用C编写的传统ufunc一样快的方式在NumPy数组上运行。

使用vectorize(),您可以将函数编写为对输入标量(而不是数组)进行操作。Numba将生成周围的循环(或内核),从而允许对实际输入进行有效的迭代。

vectorize()装饰有两种操作模式:

  • 急切地编译:如果将一个或多个类型签名传递给装饰器,则将构建Numpy通用函数(ufunc)。
  • 延迟或调用时编译:如果未提供任何签名,则装饰器将为您提供Numba动态通用函数(DUFunc),当使用以前不受支持的输入类型进行调用时,该函数可以动态编译新内核。

例如

from numba import vectorize, float64

@vectorize([float64(float64, float64)])
def f(x, y):
    return x + y


@vectorize([int32(int32, int32),
            int64(int64, int64),
            float32(float32, float32),
            float64(float64, float64)])
def f(x, y):
    return x + y

可以针对不同地数据类型实现不同地返回,括号里表示参数的数据类型,括号外表示返回值的数据类型

除此,我们自定义的ufun具有一些高级的内置方法:

>>> a = np.arange(12).reshape(3, 4)
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> f.reduce(a, axis=0)
array([12, 15, 18, 21])
>>> f.reduce(a, axis=1)
array([ 6, 22, 38])
>>> f.accumulate(a)
array([[ 0,  1,  2,  3],
       [ 4,  6,  8, 10],
       [12, 15, 18, 21]])
>>> f.accumulate(a, axis=1)
array([[ 0,  1,  3,  6],
       [ 4,  9, 15, 22],
       [ 8, 17, 27, 38]])

ufunc和jit一样支持不同的硬件调用:

一般准则是针对不同的数据大小和算法选择不同的目标。“ cpu”目标适用于较小的数据大小(大约小于1KB)和低计算强度算法。它的通信开销最少。“并行”目标适用于中等数据大小(约小于1MB),因为线程调用增加了一定的时间开销。“ cuda”目标适用于大数据量(大约大于1MB)和高计算强度算法,因为往返于GPU的内存转移会增加大量通信开销。


@guvectoriz

虽然vectorize()允许您编写一次在一个元素上工作的ufunc,但是guvectorize()装饰器使概念更进一步,并允许您编写将在任意数量的输入数组的元素上工作的ufunc,并采用和返回不同维数的数组。典型示例是运行中值或卷积滤波器。

vectorize()函数相反,guvectorize()函数不需要使用return返回其结果值:它们将其作为数组参数,必须由函数填充。这是因为该数组实际上是由NumPy的调度机制分配的,该机制调用Numba生成的代码。

@guvectorize([(int64[:], int64, int64[:])], '(n),()->(n)')

def g(x, y, res):
    for i in range(x.shape[0]):
        res[i] = x[i] + y

底层的Python函数只是将给定的标量(y)添加到一维数组的所有元素中。更有趣的是声明。有两件事:

  • 输入和输出形式的声明,以符号形式: (n),()->(n)告诉NumPy,该函数采用n元一维数组,标量(用空元组符号表示())并返回n元一维数组;
  • 支持的具体类型按@vectorize; 在此,如上面的示例所示,我们演示了int64数组。

现在,我们可以通过一个简单的示例检查已编译的ufunc的功能:

 >>> a = np.arange(5)
>>> a
array([0, 1, 2, 3, 4])
>>> g(a, 2)
array([2, 3, 4, 5, 6])

令人高兴的是,NumPy将根据其形状自动分派更复杂的输入:

>>> a = np.arange(6).reshape(2, 3)
>>> a
array([[0, 1, 2],
       [3, 4, 5]])
>>> g(a, 10)
array([[10, 11, 12],
       [13, 14, 15]])
>>> g(a, np.array([10, 20]))
array([[10, 11, 12],
       [23, 24, 25]])
 

牛逼这功能。

vectorize()和guvectorize()支持通过nopython=True作为@jit装饰。使用它来确保生成的代码不会退回到object模式。





性能技巧:

jit(nopython=True)=njit性能>>jit(nopython=False)