1.6。创建 Numpy 通用函数

原文: http://numba.pydata.org/numba-doc/latest/user/vectorize.html

1.6.1。 @vectorize装饰器

Numba 的 vectorize 允许 Python 函数将标量输入参数用作 NumPy ufuncs 。创建传统的 NumPy ufunc 并不是最直接的过程,而是涉及编写一些 C 代码。 Numba 让这很容易。使用 vectorize() 装饰器,Numba 可以将纯 Python 函数编译为一个 ufunc,它在 NumPy 阵列上运行的速度与用 C 编写的传统 ufunc 一样快。

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

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

  • 渴望或装饰时间,编译:如果您将一个或多个类型签名传递给装饰器,您将构建 Numpy 通用函数(ufunc)。本小节的其余部分描述了使用装饰时编译构建 ufunc。
  • 懒惰或调用时编译:当没有给出任何签名时,装饰器会给你一个 Numba 动态通用函数( DUFunc ),它在使用以前不支持的输入类型调用时动态编译新内核。后面的小节“动态通用函数”更深入地描述了这种模式。

如上所述,如果您将签名列表传递给 vectorize() 装饰器,您的函数将被编译为 Numpy ufunc。在基本情况下,只会传递一个签名:

  1. from numba import vectorize, float64
  2. @vectorize([float64(float64, float64)])
  3. def f(x, y):
  4. return x + y

如果您传递了多个签名,请注意必须在最不具体的签名之前传递大多数特定签名(例如,在双精度浮点数之前单精度浮点数),否则基于类型的分派将无法按预期工作:

  1. @vectorize([int32(int32, int32),
  2. int64(int64, int64),
  3. float32(float32, float32),
  4. float64(float64, float64)])
  5. def f(x, y):
  6. return x + y

该函数将按预期在指定的数组类型上工作:

  1. >>> a = np.arange(6)
  2. >>> f(a, a)
  3. array([ 0, 2, 4, 6, 8, 10])
  4. >>> a = np.linspace(0, 1, 6)
  5. >>> f(a, a)
  6. array([ 0\. , 0.4, 0.8, 1.2, 1.6, 2\. ])

但它将无法在其他类型上工作:

  1. >>> a = np.linspace(0, 1+1j, 6)
  2. >>> f(a, a)
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. TypeError: ufunc 'ufunc' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

你可能会问自己,“为什么我要经历这个而不是使用 @jit 装饰器编译一个简单的迭代循环?”。答案是 NumPy ufuncs 会自动获得其他功能,如缩小,累积或广播。使用上面的例子:

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

也可以看看

ufuncs 的标准功能(NumPy 文档)。

vectorize() 装饰器支持多个 ufunc 目标:

目标 描述
中央处理器 单线程 CPU
平行 多核 CPU
CUDA CUDA GPU 注意这会创建一个类似 ufunc 的 对象。有关详细信息,请参阅 CUDA ufunc 的文档。

一般准则是为不同的数据大小和算法选择不同的目标。 “cpu”目标适用于小数据大小(约小于 1KB)和低计算强度算法。它具有最少的开销。 “并行”目标适用于中等数据大小(大约小于 1MB)。线程增加了一点延迟。 “cuda”目标适用于大数据量(大约 1MB)和高计算强度算法。向 GPU 传输内存和从 GPU 传输内存会增加大量开销。

1.6.2。 @guvectorize装饰器

虽然 vectorize() 允许你一次写一个元素的 ufunc,但 guvectorize() 装饰器更进一步,并允许你编写可以工作的 ufuncs 在输入数组的任意数量的元素上,并获取和返回不同维度的数组。典型的例子是运行中值或卷积滤波器。

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

这是一个非常简单的例子:

  1. @guvectorize([(int64[:], int64, int64[:])], '(n),()->(n)')
  2. def g(x, y, res):
  3. for i in range(x.shape[0]):
  4. res[i] = x[i] + y

底层的 Python 函数只是将一个给定的标量(y)添加到一维数组的所有元素中。宣言更有意思。那里有两件事:

  • 输入和输出 布局 的声明,符号形式:(n),()-&gt;(n)告诉 NumPy 该函数采用 n 元素一维数组,一个标量(用符号表示为空元组())并返回 n 元素一维数组;
  • @vectorize中支持的具体 签名 列表;这里我们只支持int64数组。

注意

1D 数组类型也可以接收标量参数(形状为()的参数)。在上面的例子中,第二个参数也可以声明为int64[:]。在这种情况下,该值必须由y[0]读取。

我们现在可以通过一个简单的例子来检查已编译的 ufunc 的作用:

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

好处是 NumPy 将根据其形状自动调度更复杂的输入:

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

注意

vectorize()guvectorize() 都支持传递nopython=True ,如同@jit 装饰器。使用它来确保生成的代码不会回退到对象模式

1.6.3。动态通用功能

如上所述,如果您没有将任何签名传递给 vectorize() 装饰器,您的 Python 函数将用于构建动态通用函数,或 DUFunc 。例如:

  1. from numba import vectorize
  2. @vectorize
  3. def f(x, y):
  4. return x * y

结果f()DUFunc 实例,以没有支持的输入类型开头。在调用f()时,只要传递以前不支持的输入类型,Numba 就会生成新的内核。鉴于上面的示例,以下一组解释器交互说明了动态编译的工作原理:

  1. >>> f
  2. <numba._DUFunc 'f'>
  3. >>> f.ufunc
  4. <ufunc 'f'>
  5. >>> f.ufunc.types
  6. []

上面的例子显示 DUFunc 实例不是 ufunc。而不是子类 ufunc, DUFunc 实例通过保持 ufunc 成员,然后将 ufunc 属性读取和方法调用委托给此成员(也称为类型聚合)来工作。当我们查看 ufunc 支持的初始类型时,我们可以验证没有。

我们试着打电话给f()

  1. >>> f(3,4)
  2. 12
  3. >>> f.types # shorthand for f.ufunc.types
  4. ['ll->l']

如果这是一个普通的 Numpy ufunc,我们会看到一个异常抱怨 ufunc 无法处理输入类型。当我们用整数参数调用f()时,我们不仅会收到答案,而且我们可以验证 Numba 是否创建了支持 C long整数的循环。

我们可以通过使用不同的输入调用f()来添加其他循环:

  1. >>> f(1.,2.)
  2. 2.0
  3. >>> f.types
  4. ['ll->l', 'dd->d']

我们现在可以验证 Numba 是否为处理浮点输入添加了第二个循环"dd-&gt;d"

如果我们将输入类型混合到f(),我们可以验证 Numpy ufunc 强制转换规则是否仍然有效:

  1. >>> f(1,2.)
  2. 2.0
  3. >>> f.types
  4. ['ll->l', 'dd->d']

此示例演示了使用混合类型调用f()会导致 Numpy 选择浮点循环,并将整数参数转换为浮点值。因此,Numba 没有创建一个特殊的"dl-&gt;d"内核。

DUFunc 行为导致我们得到类似于上面“ @vectorize 装饰器”小节中给出的警告的点,但是在装饰器中没有签名声明顺序,呼叫顺序很重要。如果我们首先传入浮点参数,那么任何带有整数参数的调用都将被转换为双精度浮点值。例如:

  1. >>> @vectorize
  2. ... def g(a, b): return a / b
  3. ...
  4. >>> g(2.,3.)
  5. 0.66666666666666663
  6. >>> g(2,3)
  7. 0.66666666666666663
  8. >>> g.types
  9. ['dd->d']

如果您需要对各种类型签名的精确支持,您应该在 vectorize() 装饰器中指定它们,而不是依赖于动态编译。