Numba 运行时的注意事项

The Numba Runtime (NRT)nopython 模式 的 Python 子集提供了语言运行时。NRT 是一个独立的 C 库,具有 Python 绑定。这使得 NPM 运行时功能可以在没有 GIL 的情况下使用。目前,NRT 中唯一实现的语言功能是内存管理。

内存管理

NRT 为 NPM 代码实现了内存管理。它使用 原子引用计数 来实现线程安全的确定性内存管理。NRT 为每个分配维护一个单独的 MemInfo 结构来存储相关信息。

与 CPython 合作

为了使 NRT 与 CPython 合作,NRT 的 Python 绑定提供了适配器,用于转换导出内存区域的 Python 对象。当这样的对象作为 NPM 函数的参数使用时,会创建一个新的 MemInfo 并获取对 Python 对象的引用。当一个 NPM 值返回给 Python 解释器时,会检查相关的 MemInfo``(如果有的话)。如果 ``MemInfo 引用了一个 Python 对象,则释放底层 Python 对象并返回。否则,MemInfo 会被包装在一个 Python 对象中并返回。根据类型,可能需要额外的处理。

当前实现支持 Numpy 数组和任何导出缓冲区的类型。

编译器端协作

NRT 引用计数要求编译器根据使用情况发出 incref/decref 操作。当引用计数降至零时,编译器必须在 NRT 中调用析构函数例程。

优化

编译器可以简单地发出 incref/decref 操作。它依赖于一个优化过程来移除冗余的引用计数操作。

在版本0.52.0中实现了一个新的优化过程,以移除属于以下四种控制流结构之一的引用计数操作——每个基本块、菱形、扇出、扇出+raise。有关它们的描述,请参阅 NUMBA_LLVM_REFPRUNE_FLAGS 的文档。

旧的优化过程在块级别运行以避免控制流分析。它依赖于 LLVM 函数优化过程来简化控制流、栈到寄存器以及简化指令。它通过匹配并移除每个块内的 incref 和 decref 对来工作。可以通过将 NUMBA_LLVM_REFPRUNE_PASS 设置为 0 来启用旧的优化过程。

重要假设

旧版本(0.52.0之前)和新版本(0.52.0之后)的优化过程都假设唯一能消费引用的函数是 NRT_decref。重要的是没有其他函数会消费引用。由于这些过程操作在LLVM IR上,这里的“函数”指的是LLVM调用指令中的任何被调用者。

总结来说,所有暴露给引用计数优化传递的函数 必须不 消耗计数引用,除非通过 NRT_decref 进行。

旧优化过程的特性

由于 pre-0.52.0 版本的 引用计数优化过程 需要 LLVM 函数优化过程,该过程在 LLVM IR 文本上进行。优化后的 IR 随后被重新物化为一个新的 LLVM 内存位码对象。

调试泄漏

要在 NRT MemInfo 中调试引用泄漏,每个 MemInfo python 对象都有一个 .refcount 属性用于检查。要从 NRT 分配的 ndarray 获取 MemInfo,请使用 .base 属性。

要在NRT中调试内存泄漏,numba.core.runtime.rtsys 定义了 .get_allocation_stats()。它返回一个包含自程序启动以来分配和释放次数的命名元组。检查分配和释放计数器是否匹配是判断NRT是否泄漏的最简单方法。

调试 C 语言中的内存泄漏

numba/core/runtime/nrt.h 的开头有这些行:

/* Debugging facilities - enabled at compile-time */
/* #undef NDEBUG */
#if 0
#   define NRT_Debug(X) X
#else
#   define NRT_Debug(X) if (0) { X; }
#endif

取消定义 NDEBUG(取消注释 #undef NDEBUG 行)将启用 NRT 中的断言检查。

启用 NRT_Debug(将 #if 0 替换为 #if 1)会在 NRT 内部开启调试打印。

递归支持

在编译一对相互递归的函数时,由于编译器一次只处理一个函数,其中一个函数将包含未解析的符号引用。在机器代码由LLVM生成之前,为这些未解析的符号分配内存并初始化为*未解析符号中止*函数(nrt_unresolved_abort)的地址。这些符号会被跟踪并在编译新函数时解析。如果一个错误阻止了这些符号的解析,将调用中止函数,引发一个``RuntimeError``异常。

The unresolved symbol abort 函数在 NRT 中定义为零参数签名。调用者可以安全地使用任意数量的参数调用它。因此,它可以安全地替代预期的被调用者。

从C代码中使用NRT

外部编译的C代码应使用 NRT_api_functions 结构作为函数表来访问NRT API。该结构在 numba/core/runtime/nrt_external.h 中定义。用户可以使用实用函数 numba.extending.include_path() 来确定Numba提供的C头文件的包含目录。

numba/core/runtime/nrt_external.h
#ifndef NUMBA_NRT_EXTERNAL_H_
#define NUMBA_NRT_EXTERNAL_H_

#include <stdlib.h>

typedef struct MemInfo NRT_MemInfo;

typedef void NRT_managed_dtor(void *data);

typedef void *(*NRT_external_malloc_func)(size_t size, void *opaque_data);
typedef void *(*NRT_external_realloc_func)(void *ptr, size_t new_size, void *opaque_data);
typedef void (*NRT_external_free_func)(void *ptr, void *opaque_data);

struct ExternalMemAllocator {
    NRT_external_malloc_func malloc;
    NRT_external_realloc_func realloc;
    NRT_external_free_func free;
    void *opaque_data;
};

typedef struct ExternalMemAllocator NRT_ExternalAllocator;

typedef struct {
    /* Methods to create MemInfos.

    MemInfos are like smart pointers for objects that are managed by the Numba.
    */

    /* Allocate memory

    *nbytes* is the number of bytes to be allocated

    Returning a new reference.
    */
    NRT_MemInfo* (*allocate)(size_t nbytes);
    /* Allocates memory using an external allocator but still using Numba's MemInfo.
     *
     * NOTE: An externally provided allocator must behave the same way as C99
     *       stdlib.h's "malloc" function with respect to return value
     *       (including the behaviour that occurs when requesting an allocation
     *        of size 0 bytes).
     */
    NRT_MemInfo* (*allocate_external)(size_t nbytes, NRT_ExternalAllocator *allocator);

    /* Convert externally allocated memory into a MemInfo.

    *data* is the memory pointer
    *dtor* is the deallocator of the memory
    */
    NRT_MemInfo* (*manage_memory)(void *data, NRT_managed_dtor dtor);

    /* Acquire a reference */
    void (*acquire)(NRT_MemInfo* mi);

    /* Release a reference */
    void (*release)(NRT_MemInfo* mi);

    /* Get MemInfo data pointer */
    void* (*get_data)(NRT_MemInfo* mi);

} NRT_api_functions;



#endif /* NUMBA_NRT_EXTERNAL_H_ */

在Numba编译的代码中,可以使用 numba.core.unsafe.nrt.NRT_get_api() 内在函数来获取指向 NRT_api_functions 的指针。

以下是使用 nrt_external.h 的示例:

#include <stdio.h>
#include "numba/core/runtime/nrt_external.h"

void my_dtor(void *ptr) {
    free(ptr);
}

NRT_MemInfo* my_allocate(NRT_api_functions *nrt) {
    /* heap allocate some memory */
    void * data = malloc(10);
    /* wrap the allocated memory; yield a new reference */
    NRT_MemInfo *mi = nrt->manage_memory(data, my_dtor);
    /* acquire reference */
    nrt->acquire(mi);
    /* release reference */
    nrt->release(mi);
    return mi;
}

在调用NRT之前,确保NRT已初始化是很重要的,从Python中调用 numba.core.runtime.nrt.rtsys.initialize(context) 将产生预期效果。同样,代码片段如下:

from numba.core.registry import cpu_target # Get the CPU target singleton
cpu_target.target_context # Access the target_context property to initialize

对于 Numba 的 CPU 目标(默认),将实现相同的效果。未能初始化 NRT 将导致访问冲突,因为 NRT_MemSys 结构中将缺少各种内部原子操作的函数指针。

未来计划

NRT 的计划是制作一个独立的共享库,可以链接到 Numba 编译的代码,包括在 Python 解释器内和没有 Python 解释器的情况下使用。为了实现这一点,我们将进行一些重构:

  • numba NPM 代码引用在 “helperlib.c” 中的静态编译代码。这些函数应移至 NRT。