High-level extension API¶
This extension API is exposed through the numba.extending
module.
Implementing functions¶
The @overload
decorator allows you to implement arbitrary functions
for use in nopython mode functions. The function decorated with
@overload
is called at compile-time with the types of the function’s
runtime arguments. It should return a callable representing the
implementation of the function for the given types. The returned
implementation is compiled by Numba as if it were a normal function
decorated with @jit
. Additional options to @jit
can be passed as
dictionary using the jit_options
argument.
For example, let’s pretend Numba doesn’t support the len()
function
on tuples yet. Here is how to implement it using @overload
:
from numba import types
from numba.extending import overload
@overload(len)
def tuple_len(seq):
if isinstance(seq, types.BaseTuple):
n = len(seq)
def len_impl(seq):
return n
return len_impl
You might wonder, what happens if len()
is called with something
else than a tuple? If a function decorated with @overload
doesn’t
return anything (i.e. returns None), other definitions are tried until
one succeeds. Therefore, multiple libraries may overload len()
for different types without conflicting with each other.
Implementing methods¶
The @overload_method
decorator similarly allows implementing a
method on a type well-known to Numba. The following example implements
the take()
method on Numpy arrays:
@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
Implementing attributes¶
The @overload_attribute
decorator allows implementing a data
attribute (or property) on a type. Only reading the attribute is
possible; writable attributes are only supported through the
low-level API.
The following example implements the nbytes
attribute
on Numpy arrays:
@overload_attribute(types.Array, 'nbytes')
def array_nbytes(arr):
def get(arr):
return arr.size * arr.itemsize
return get
Importing Cython Functions¶
The function get_cython_function_address
obtains the address of a
C function in a Cython extension module. The address can be used to
access the C function via a ctypes.CFUNCTYPE()
callback, thus
allowing use of the C function inside a Numba jitted function. For
example, suppose that you have the file foo.pyx
:
from libc.math cimport exp
cdef api double myexp(double x):
return exp(x)
You can access myexp
from Numba in the following way:
import ctypes
from numba.extending import get_cython_function_address
addr = get_cython_function_address("foo", "myexp")
functype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double)
myexp = functype(addr)
The function myexp
can now be used inside jitted functions, for
example:
@njit
def double_myexp(x):
return 2*myexp(x)
One caveat is that if your function uses Cython’s fused types, then
the function’s name will be mangled. To find out the mangled name of
your function you can check the extension module’s __pyx_capi__
attribute.
Implementing intrinsics¶
The @intrinsic
decorator is used for marking a function func as typing and
implementing the function in nopython
mode using the
llvmlite IRBuilder API.
This is an escape hatch for expert users to build custom LLVM IR that will be
inlined into the caller, there is no safety net!
The first argument to func is the typing context. The rest of the arguments
corresponds to the type of arguments of the decorated function. These arguments
are also used as the formal argument of the decorated function. If func has
the signature foo(typing_context, arg0, arg1)
, the decorated function will
have the signature foo(arg0, arg1)
.
The return values of func should be a 2-tuple of expected type signature, and
a code-generation function that will passed to
lower_builtin()
. For an unsupported operation,
return None
.
Here is an example that cast any integer to a byte pointer:
from numba import types
from numba.extending import intrinsic
@intrinsic
def cast_int_to_byte_ptr(typingctx, src):
# check for accepted types
if isinstance(src, types.Integer):
# create the expected type signature
result_type = types.CPointer(types.uint8)
sig = result_type(types.uintp)
# defines the custom code generation
def codegen(context, builder, signature, args):
# llvm IRBuilder code here
[src] = args
rtype = signature.return_type
llrtype = context.get_value_type(rtype)
return builder.inttoptr(src, llrtype)
return sig, codegen
it may be used as follows:
from numba import njit
@njit('void(int64)')
def foo(x):
y = cast_int_to_byte_ptr(x)
foo.inspect_types()
and the output of .inspect_types()
demonstrates the cast (note the
uint8*
):
def foo(x):
# x = arg(0, name=x) :: int64
# $0.1 = global(cast_int_to_byte_ptr: <intrinsic cast_int_to_byte_ptr>) :: Function(<intrinsic cast_int_to_byte_ptr>)
# $0.3 = call $0.1(x, func=$0.1, args=[Var(x, check_intrin.py (24))], kws=(), vararg=None) :: (uint64,) -> uint8*
# del x
# del $0.1
# y = $0.3 :: uint8*
# del y
# del $0.3
# $const0.4 = const(NoneType, None) :: none
# $0.5 = cast(value=$const0.4) :: none
# del $const0.4
# return $0.5
y = cast_int_to_byte_ptr(x)
Implementing mutable structures¶
Warning
This is an experimental feature, the API may change without warning.
The numba.experimental.structref
module provides utilities for defining
mutable pass-by-reference structures, a StructRef
. The following example
demonstrates how to define a basic mutable structure:
Defining a StructRef¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | import numpy as np
from numba import njit
from numba.core import types
from numba.experimental import structref
from numba.tests.support import skip_unless_scipy
# Define a StructRef.
# `structref.register` associates the type with the default data model.
# This will also install getters and setters to the fields of
# the StructRef.
@structref.register
class MyStructType(types.StructRef):
def preprocess_fields(self, fields):
# This method is called by the type constructor for additional
# preprocessing on the fields.
# Here, we don't want the struct to take Literal types.
return tuple((name, types.unliteral(typ)) for name, typ in fields)
# Define a Python type that can be use as a proxy to the StructRef
# allocated inside Numba. Users can construct the StructRef via
# the constructor for this type in python code and jit-code.
class MyStruct(structref.StructRefProxy):
def __new__(cls, name, vector):
# Overriding the __new__ method is optional, doing so
# allows Python code to use keyword arguments,
# or add other customized behavior.
# The default __new__ takes `*args`.
# IMPORTANT: Users should not override __init__.
return structref.StructRefProxy.__new__(cls, name, vector)
# By default, the proxy type does not reflect the attributes or
# methods to the Python side. It is up to users to define
# these. (This may be automated in the future.)
@property
def name(self):
# To access a field, we can define a function that simply
# return the field in jit-code.
# The definition of MyStruct_get_name is shown later.
return MyStruct_get_name(self)
@property
def vector(self):
# The definition of MyStruct_get_vector is shown later.
return MyStruct_get_vector(self)
@njit
def MyStruct_get_name(self):
# In jit-code, the StructRef's attribute is exposed via
# structref.register
return self.name
@njit
def MyStruct_get_vector(self):
return self.vector
# This associates the proxy with MyStructType for the given set of
# fields. Notice how we are not contraining the type of each field.
# Field types remain generic.
structref.define_proxy(MyStruct, MyStructType, ["name", "vector"])
|
The following demonstrates using the above mutable struct definition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | # Let's test our new StructRef.
# Define one in Python
alice = MyStruct("Alice", vector=np.random.random(3))
# Define one in jit-code
@njit
def make_bob():
bob = MyStruct("unnamed", vector=np.zeros(3))
# Mutate the attributes
bob.name = "Bob"
bob.vector = np.random.random(3)
return bob
bob = make_bob()
# Out: Alice: [0.5488135 0.71518937 0.60276338]
print(f"{alice.name}: {alice.vector}")
# Out: Bob: [0.88325739 0.73527629 0.87746707]
print(f"{bob.name}: {bob.vector}")
# Define a jit function to operate on the structs.
@njit
def distance(a, b):
return np.linalg.norm(a.vector - b.vector)
# Out: 0.4332647200356598
print(distance(alice, bob))
|
Defining a method on StructRef¶
Methods and attributes can be attached using @overload_*
as shown in the
previous sections.
The following demonstrates the use of @overload_method
to insert a
method for instances of MyStructType
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | from numba.core.extending import overload_method
from numba.core.errors import TypingError
# Use @overload_method to add a method for
# MyStructType.distance(other)
# where *other* is an instance of MyStructType.
@overload_method(MyStructType, "distance")
def ol_distance(self, other):
# Guard that *other* is an instance of MyStructType
if not isinstance(other, MyStructType):
raise TypingError(
f"*other* must be a {MyStructType}; got {other}"
)
def impl(self, other):
return np.linalg.norm(self.vector - other.vector)
return impl
# Test
@njit
def test():
alice = MyStruct("Alice", vector=np.random.random(3))
bob = MyStruct("Bob", vector=np.random.random(3))
# Use the method
return alice.distance(bob)
|
numba.experimental.structref
API Reference¶
Utilities for defining a mutable struct.
A mutable struct is passed by reference; hence, structref (a reference to a struct).
-
class
numba.experimental.structref.
StructRefProxy
¶ A PyObject proxy to the Numba allocated structref data structure.
Notes
- Subclasses should not define
__init__
. - Subclasses can override
__new__
.
- Subclasses should not define
-
numba.experimental.structref.
define_attributes
(struct_typeclass)¶ Define attributes on struct_typeclass.
Defines both setters and getters in jit-code.
This is called directly in register().
-
numba.experimental.structref.
define_boxing
(struct_type, obj_class)¶ Define the boxing & unboxing logic for struct_type to obj_class.
Defines both boxing and unboxing.
- boxing turns an instance of struct_type into a PyObject of obj_class
- unboxing turns an instance of obj_class into an instance of struct_type in jit-code.
Use this directly instead of define_proxy() when the user does not want any constructor to be defined.
-
numba.experimental.structref.
define_constructor
(py_class, struct_typeclass, fields)¶ Define the jit-code constructor for struct_typeclass using the Python type py_class and the required fields.
Use this instead of define_proxy() if the user does not want boxing logic defined.
-
numba.experimental.structref.
define_proxy
(py_class, struct_typeclass, fields)¶ Defines a PyObject proxy for a structref.
This makes py_class a valid constructor for creating a instance of struct_typeclass that contains the members as defined by fields.
Parameters: - py_class : type
The Python class for constructing an instance of struct_typeclass.
- struct_typeclass : numba.core.types.Type
The structref type class to bind to.
- fields : Sequence[str]
A sequence of field names.
Returns: - None
-
numba.experimental.structref.
register
(struct_type)¶ Register a numba.core.types.StructRef for use in jit-code.
This defines the data-model for lowering an instance of struct_type. This defines attributes accessor and mutator for an instance of struct_type.
Parameters: - struct_type : type
A subclass of numba.core.types.StructRef.
Returns: - struct_type : type
Returns the input argument so this can act like a decorator.
Examples