从Numba jitted代码调用Cython函数

10

我知道一个使用Numba技术的函数调用另一个使用该技术的函数将识别这一点,并自动使用快速的C调用约定,而不是通过Python对象层进行调用,从而避免高Python函数调用开销:

import numba

@numba.jit
def foo(x):
    return x**2

@numba.jit
def bar(x):
    return 4 * foo(x)   # this will be a fast function call

我的问题是,如果我从Numba调用Cython函数是否同样适用。假设我有一个Cython模块foo.pyx:
cpdef double foo(double x):
    return x**2

除了一个标准的 Python 模块 bar.py 之外:

import numba
import foo

@numba.jit
def bar(x):
    return 4 * foo.foo(x)   # will this be a fast function call?

Numba会自动将foo.foo识别为C可调用函数吗?还是我需要通过设置CFFI包装器等方式手动告诉它?

编辑:经过进一步的思考,从Python解释器的角度来看,Cython函数只是标准的“内置”函数。因此,问题可以更普遍地提出:Numba是否优化对内置函数和方法的调用以绕过Python调用开销?

2个回答

12
可以在nopython-numba中使用Cython的cpdef/cdef函数(但不能使用def函数):
  1. 步骤:在Cython代码中,cdef/cpdef函数必须标记为api
  2. 步骤:numba.extending.get_cython_function_address可用于获取cpdef函数的地址。
  3. 步骤:可以使用ctypes从cpdef函数的地址创建CFunction,该函数可在numba-nopython代码中使用。
阅读以下内容以获取更详细的解释。
即使内置函数(PyCFunction,与Cython的def函数相同)是用C编写的,它们也没有一个可供nopython-numba代码使用的签名。
例如,来自math模块的acos函数没有签名。
`double acos(double)`

正如人们所预料的那样,但它的签名

static PyObject * math_acos(PyObject *self, PyObject *args)

基本上,为了调用这个函数,numba需要从手头的C-float构建一个Python-float,但是这被nopython=True禁止了。
然而,Cython的cpdef函数有些不同:它是一个小包装器,包装了一个真正的cdef函数,其参数是原始的C类型,如doubleint等。如果已知其地址,numba可以使用这个cdef函数。
Cython提供了一种在可移植的方式中查找cdef函数地址的方法:这些地址可以在cythonized模块的__pyx_capi__属性中找到。
然而,并不是所有的cdefcpdef函数都以这种方式公开,只有那些显式标记为C-api declarations或通过pxd文件共享的函数才会被公开。

一旦foomodule的函数foo被标记为api

cpdef api double foo(double x):
    return x*x

cpdef函数foo的地址可以在foomodule.__pyx_capi__字典中找到:

import foomodule
foomodule.__pyx_capi
# {'foo': <capsule object "double (double)" at 0x7fe0a46f0360>}

从Python的PyCapsule中提取地址非常困难。一种可能的方法是使用ctypes.pythonapi,另一种(可能更容易的方法)是利用Cython访问Python的C-API:

%%cython
from cpython.pycapsule cimport  PyCapsule_GetPointer, PyCapsule_GetName
def address_from_capsule(object capsule):
    name = PyCapsule_GetName(capsule)
    return <unsigned long long int> PyCapsule_GetPointer(capsule, name)

可以用作:
addr = address_from_capsule(foomodule.__pyx_capi__['foo'])

然而,numba提供了一个类似的功能,可以直接使用 - get_cython_function_address :
from numba.extending import get_cython_function_address
addr = get_cython_function_address("foomodule", "foo")

一旦我们获得了C函数的地址,我们就可以构建一个ctypes函数:

import ctypes
foo_functype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double)
foo_for_numba = foo_functype(addr)

这个函数可以像下面这样从nopython-numba中使用:
from numba import njit
@njit
def use_foo(x):
    return foo_for_numba(x)

现在:

use_foo(5)
# 25.0

产生预期结果。


ctype函数的参数可以在这里找到,numba可以直接识别这些参数。

_FROM_CTYPES = {
    ctypes.c_bool: types.boolean,

    ctypes.c_int8:  types.int8,
    ctypes.c_int16: types.int16,
    ctypes.c_int32: types.int32,
    ctypes.c_int64: types.int64,

    ctypes.c_uint8: types.uint8,
    ctypes.c_uint16: types.uint16,
    ctypes.c_uint32: types.uint32,
    ctypes.c_uint64: types.uint64,

    ctypes.c_float: types.float32,
    ctypes.c_double: types.float64,

    ctypes.c_void_p: types.voidptr,
    ctypes.py_object: types.ffi_forced_object,
}

你是否知道如何定义 foo 函数的参数或返回值为 numpy 的 ndarrays? - ibarrond
@ibarrond 我认为没有简单的方法可以做到这一点(除了利用不透明的Python对象)。您可以查看numba支持的参数类型:https://github.com/numba/numba/blob/61ec1fd0f69aeadece218dccf4c39ebc5c7dfbc4/numba/core/typing/ctypes_utils.py#L14-L32,而numpy的ndarray(由np.ctypeslib.ndpointer提供)不是其中之一。您可能需要手动模拟它在函数接口中的行为。 - ead
是的,这就是我得出的结论。最终,我放弃了Numba,采用了完全的Cython方法。 - ibarrond

5
有一组有限的内建函数(来自Python标准库和NumPy),Numba知道如何将它们翻译成本地代码:
任何其他内容都无法在nopython模式下被Numba编译成本地代码,因此只能使用速度较慢的objectmode
没有直接的方法可以将Cython函数传递给Numba并在nopython模式下被识别。Numba确实具有用于cffi的钩子:
http://numba.pydata.org/numba-doc/latest/reference/pysupported.html#cffi
可以利用这些钩子调用外部C代码,如果您能够在C级别创建低级包装器,则可能能够将其连接到Cython。但我不确定是否可能。我曾经写过一篇关于从Numba调用RMath函数的文章:
https://web.archive.org/web/20160611082327/https://www.continuum.io/blog/developer-blog/calling-c-libraries-numba-using-cffi
如果您选择这条路线,这可能有助于帮助您入门。

谢谢,好信息!1)您知道这是否是Numba实现的限制还是本质上困难的事情吗?2)您让它听起来好像在为Cython函数生成CFFI包装器时会有重大问题。我不能简单地在C端获取函数指针,然后使用正确的签名将其包装为CFFI / ctypes函数对象吗?您是否预见到任何麻烦? - cfh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接