NBEP 2: 扩展点

作者:

安托万·皮特鲁

日期:

2015年7月

状态:

草稿

在 Numba 中实现新类型或函数需要挂钩到编译链中的各种机制(可能在编译链之外)。本文首先旨在考察当前的实现方式,其次提出使扩展更容易的建议。

如果某些提案被实施,我们应该首先在内部使用和实践它们,然后再向公众公开API。

备注

本文档不涉及CUDA或其他任何非CPU后端。

高级 API

目前没有高级API,使得某些用例比它们应有的更加复杂。

提议的更改

专用模块

我们提议增加一个 numba.extending 模块,该模块将公开用于扩展 Numba 的主要 API。

实现一个函数

我们建议增加一个 @overload 装饰器,允许为 nopython 模式 实现给定函数的重载。重载函数具有与实现函数相同的正式签名,并接收实际的参数类型。它应该返回一个为给定类型实现重载函数的 Python 函数。

以下示例使用这种方法实现了 numpy.where()

import numpy as np

from numba.core import types
from numba.extending import overload

@overload(np.where)
def where(cond, x, y):
    """
    Implement np.where().
    """
    # Choose implementation based on argument types.
    if isinstance(cond, types.Array):
        # Array where() => return an array of the same shape
        if all(ty.layout == 'C' for ty in (cond, x, y)):
            def where_impl(cond, x, y):
                """
                Fast implementation for C-contiguous arrays
                """
                shape = cond.shape
                if x.shape != shape or y.shape != shape:
                    raise ValueError("all inputs should have the same shape")
                res = np.empty_like(x)
                cf = cond.flat
                xf = x.flat
                yf = y.flat
                rf = res.flat
                for i in range(cond.size):
                    rf[i] = xf[i] if cf[i] else yf[i]
                return res
        else:
            def where_impl(cond, x, y):
                """
                Generic implementation for other arrays
                """
                shape = cond.shape
                if x.shape != shape or y.shape != shape:
                    raise ValueError("all inputs should have the same shape")
                res = np.empty_like(x)
                for idx, c in np.ndenumerate(cond):
                    res[idx] = x[idx] if c else y[idx]
                return res

    else:
        def where_impl(cond, x, y):
            """
            Scalar where() => return a 0-dim array
            """
            scal = x if cond else y
            return np.full_like(scal, scal)

    return where_impl

也可以实现Numba已知的函数,以支持额外的类型。以下示例使用这种方法为元组实现了内置函数 len():

@overload(len)
def tuple_len(x):
   if isinstance(x, types.BaseTuple):
      # The tuple length is known at compile-time, so simply reify it
      # as a constant.
      n = len(x)
      def len_impl(x):
         return n
      return len_impl

实现一个属性

我们建议增加一个 @overload_attribute 装饰器,允许在 nopython 模式 中实现属性获取器。

以下示例在 Numpy 数组上实现了 .nbytes 属性:

@overload_attribute(types.Array, 'nbytes')
def array_nbytes(arr):
   def get(arr):
       return arr.size * arr.itemsize
   return get

备注

overload_attribute() 签名允许通过让装饰函数返回一个 getter, setter, deleter 元组来扩展定义设置器和删除器,而不是返回单个 getter

实现一个方法

我们提议添加一个 @overload_method 装饰器,允许在 nopython 模式 中实现实例方法。

以下示例在 Numpy 数组上实现了 .take() 方法:

@overload_method(types.Array, 'take')
def array_take(arr, indices):
   if isinstance(indices, types.Array):
       def take_impl(arr, indices):
           n = indices.shape[0]
           res = np.empty(n, arr.dtype)
           for i in range(n):
               res[i] = arr[indices[i]]
           return res
       return take_impl

暴露结构成员

我们建议添加一个 make_attribute_wrapper() 函数,将内部字段作为可见的只读属性暴露出来,适用于那些由 StructModel 数据模型支持的类型。

例如,假设 PdIndexType 是 pandas 索引的 Numba 类型,以下是如何将底层 Numpy 数组作为 ._data 属性暴露出来:

@register_model(PdIndexType)
class PdIndexModel(models.StructModel):
    def __init__(self, dmm, fe_type):
        members = [
            ('values', fe_type.as_array),
            ]
        models.StructModel.__init__(self, dmm, fe_type, members)

make_attribute_wrapper(PdIndexType, 'values', '_data')

打字

Numba 类型

Numba 的标准类型在 numba.types 中声明。要声明一个新类型,可以通过子类化基类 Type 或其现有的抽象子类之一,并实现所需的功能。

提议的更改

无需更改。

值的类型推断

如果新类型的值可以作为函数参数或常量出现,则需要进行类型推断。核心机制在 numba.typing.typeof 中。

在某些情况下,某些 Python 类或类专门映射到新类型,可以扩展通用函数以在这些类上进行调度,例如:

from numba.typing.typeof import typeof_impl

@typeof_impl(MyClass)
def _typeof_myclass(val, c):
   if "some condition":
      return MyType(...)

typeof_impl 特化必须返回一个 Numba 类型实例,如果值类型推导失败则返回 None。

(当控制被类型推断的类时,一种替代 typeof_impl 的方法是在类上定义一个 _numba_type_ 属性)

在更罕见的情况下,新类型可以表示无法枚举的各种Python类,必须在 typeof_impl 泛型函数的回退实现中插入手动检查。

提议的更改

允许人们定义一个通用钩子,而无需对回退实现进行猴子补丁。

函数参数类型推断的快速路径

可选地,可能希望允许新类型参与快速类型解析(用C代码编写),以在用新类型调用JIT编译函数时最小化函数调用开销。然后必须在 _typeof.c 文件中插入所需的检查和实现,大概在 compute_fingerprint() 函数内部。

提议的更改

None. 在C Python扩展中嵌入的C代码中添加通用钩子是一个过于微妙的更改。

操作中的类型推断

各种操作(函数调用、运算符等)产生的结果值使用一组称为“模板”的帮助器进行类型化。可以通过子类化现有的基类并实现所需的推理机制来定义新模板。使用装饰器显式地将模板注册到类型推理机制中。

通过 ConcreteTemplate 基类,可以为给定操作定义一组支持的签名来实现推理。以下示例为取模运算符进行类型标注:

@builtin
class BinOpMod(ConcreteTemplate):
    key = "%"
    cases = [signature(op, op, op)
             for op in sorted(types.signed_domain)]
    cases += [signature(op, op, op)
              for op in sorted(types.unsigned_domain)]
    cases += [signature(op, op, op) for op in sorted(types.real_domain)]

(请注意,签名中使用了类型 实例,这严重限制了可以表达的泛型数量)

通过 AbstractTemplate 基类,可以以编程方式定义推理,从而赋予其完全的灵活性。以下是一个简单的示例,展示了如何表达元组索引(即 __getitem__ 操作符):

@builtin
class GetItemUniTuple(AbstractTemplate):
    key = "getitem"

    def generic(self, args, kws):
        tup, idx = args
        if isinstance(tup, types.UniTuple) and isinstance(idx, types.Integer):
            return signature(tup.dtype, tup, idx)

The AttributeTemplate 基类允许对给定类型的属性和方法进行类型化。以下是一个示例,对复数的 .real.imag 属性进行类型化:

@builtin_attr
class ComplexAttribute(AttributeTemplate):
    key = types.Complex

    def resolve_real(self, ty):
        return ty.underlying_float

    def resolve_imag(self, ty):
        return ty.underlying_float

备注

AttributeTemplate 仅用于获取属性。设置属性的值在 numba.typeinfer 中是硬编码的。

通过让用户定义一个与正在输入的函数具有相同定义的可调用对象,CallableTemplate 基类提供了一种更简单的方式来解析灵活的函数签名。例如,如果 Numba 支持列表,这里是如何假设性地输入 Python 的 sorted 函数:

@builtin
class Sorted(CallableTemplate):
    key = sorted

    def generic(self):
        def typer(iterable, key=None, reverse=None):
            if reverse is not None and not isinstance(reverse, types.Boolean):
                return
            if key is not None and not isinstance(key, types.Callable):
                return
            if not isinstance(iterable, types.Iterable):
                return
            return types.List(iterable.iterator_type.yield_type)

        return typer

(注意你可以只返回函数的返回类型,而不是完整的签名)

提议的更改

各种装饰器的命名相当模糊且令人困惑。我们建议将 @builtin 重命名为 @infer,将 @builtin_attr 重命名为 @infer_getattr,并将 builtin_global 重命名为 infer_global

全局值的两步声明有点冗长,我们建议通过允许使用 infer_global 作为装饰器来简化它:

@infer_global(len)
class Len(AbstractTemplate):
    key = len

    def generic(self, args, kws):
        assert not kws
        (val,) = args
        if isinstance(val, (types.Buffer, types.BaseTuple)):
            return signature(types.intp, val)

基于类的API可能会显得笨拙,我们可以为某些模板类型添加一个功能性API:

@type_callable(sorted)
def type_sorted(context):
    def typer(iterable, key=None, reverse=None):
        # [same function as above]

    return typer

代码生成

Numba 类型的值的具体表示

任何具体的 Numba 类型都必须能够以 LLVM 形式表示(用于变量存储、参数传递等)。通过实现一个数据模型类并使用装饰器注册它来定义该表示。标准类型的数据模型类在 numba.datamodel.models 中定义。

提议的更改

无需更改。

类型转换

Numba 类型之间的隐式转换目前是在 BaseContext.cast() 方法中作为一系列选择和类型检查实现的。要添加新的隐式转换,可以在该方法中追加一个特定类型的检查。

布尔值评估是隐式转换的一个特例(目标类型为 types.Boolean)。

备注

显式转换被视为常规操作,例如构造函数调用。

提议的更改

添加一个用于隐式转换的通用函数,基于源类型和目标类型的多重分派。以下是一个展示如何编写浮点数到整数转换的示例:

@lower_cast(types.Float, types.Integer)
def float_to_integer(context, builder, fromty, toty, val):
    lty = context.get_value_type(toty)
    if toty.signed:
        return builder.fptosi(val, lty)
    else:
        return builder.fptoui(val, lty)

操作的实现

其他操作是通过一组通用函数和装饰器来实现和注册的。例如,以下是如何实现对 Numpy 数组上 .ndim 属性的查找:

@builtin_attr
@impl_attribute(types.Kind(types.Array), "ndim", types.intp)
def array_ndim(context, builder, typ, value):
    return context.get_constant(types.intp, typ.ndim)

以下是如何在元组值上调用 len() 的实现方式:

@builtin
@implement(types.len_type, types.Kind(types.BaseTuple))
def tuple_len(context, builder, sig, args):
    tupty, = sig.args
    retty = sig.return_type
    return context.get_constant(retty, len(tupty.types))

提议的更改

审查并优化API。去掉显式编写 types.Kind(...) 的要求。移除单独的 @implement 装饰器,并将 @builtin 重命名为 @lower_builtin@builtin_attr 重命名为 @lower_getattr 等。

添加装饰器以实现 setattr() 操作,命名为 @lower_setattr@lower_setattr_generic

从/到 Python 对象的转换

某些类型需要从或转换为 Python 对象,如果它们可以作为函数参数传递或从函数返回。相应的装箱和拆箱操作使用泛型函数实现。标准 Numba 类型的实现位于 numba.targets.boxing 中。例如,以下是布尔值的装箱实现:

@box(types.Boolean)
def box_bool(c, typ, val):
    longval = c.builder.zext(val, c.pyapi.long)
    return c.pyapi.bool_from_long(longval)

提议的更改

将实现签名从 (c, typ, val) 改为 (typ, val, c),以匹配为 typeof_impl 通用函数选择的签名。