NEP 53 — NumPy 2.0 的 NumPy C-API 演进#

作者:

Sebastian Berg <sebastianb@nvidia.com>

状态:

草稿

类型:

标准

创建:

2022-04-10

摘要#

NumPy C-API 用于下游项目(通常通过 Cython)来扩展 NumPy 功能。支持这些包通常意味着 C-API 的演进速度很慢,并且在正常的 NumPy 版本中无法进行某些更改,因为 NumPy 必须保证向后兼容性:针对旧版 NumPy 版本(例如 1.17)编译的下游包通常可以与新版 NumPy 版本(例如 1.25)一起工作。

NumPy 2.0 版本允许部分打破这一承诺:我们可以接受使用 NumPy 1.17 编译的 SciPy 版本(例如 SciPy 1.10)不能与 NumPy 2.0 一起工作。但是,仍然必须易于创建单个 SciPy 二进制文件,使其与 NumPy 1.x 和 NumPy 2.0 都兼容。

鉴于这些限制,此 NEP 概述了允许对我们的 C-API 进行重大更改的路径。与为 NumPy 2.0 提出的 Python API 更改类似,NEP 的目标是允许在一定程度上进行更改,从而预计大多数下游包无需或只需进行少量代码更改。

此 NEP 的实现将包括两个步骤

  1. 作为一般改进的一部分,从 NumPy 1.25 开始,使用 NumPy 构建将默认导出旧的 API 版本,以允许与最新可用 NumPy 版本向后兼容的构建。(除非选择加入,否则新 API 不可使用。)

  2. NumPy 2.0 将

    • 需要重新编译下游包以针对 NumPy 2.0,才能与 NumPy 2.0 兼容。

    • 在 NumPy 1.x 上运行时,需要 numpy2_compat 作为依赖项。

    • 需要一些下游代码更改才能适应更改的 API。

动机和范围#

NumPy API 包含 300 多个函数和许多宏。许多这些都是过时的:有些只在 NumPy 内部使用,仅为了与 NumPy 的前身兼容而存在,或者没有或只有一个已知的下游用户(即 SciPy)。

此外,NumPy 使用的许多结构始终是公开的,这使得在主要版本之外无法更改它们。一些更改已经计划多年,并且是 NPY_NO_DEPRECATED_API 和进一步弃用的原因,如C API 弃用中所述。

虽然我们可能几乎没有理由更改数组结构的布局(例如 PyArrayObject_fields),但通过更改 PyArray_Descr 结构可以更容易地开发和改进 dtype。

此 NEP 主要作为示例提出了一些对我们 C-API 的具体更改。但是,更多更改将逐案处理,我们不打算在此 NEP 中提供完整的更改列表。

添加状态超出范围#

新的开发,例如 CPython 对子解释器和 HPy API 的支持,可能需要 NumPy C-API 以可能需要(或至少更喜欢)传递状态的方式演进。

目前,我们不打算在此处包含此类更改。我们不能期望用户进行大量的代码更新,例如将 HPy 上下文传递给许多 NumPy 函数。

虽然我们可以在 NumPy 2.0 中为此目的引入第二个 API,但我们预计这是不必要的,并且此处介绍的规定

  • 能够使用最新的 NumPy 版本进行编译,但与旧版本兼容;

  • 以及更新 numpy2_compat 包的可能性。

应该允许在次要版本中添加此类 API。

使用和影响#

向后兼容的构建#

向后兼容的构建将在文档中更详细地描述。简而言之,我们将允许用户使用类似于

#define NPY_TARGET_VERSION NPY_1_22_API_VERSION

的定义来选择他们希望编译的版本(要与之兼容的最低版本)。默认情况下,向后兼容性将使生成的二进制文件与支持相同 Python 版本的最旧 NumPy 版本兼容:NumPy 1.19.x 是第一个支持 Python 3.9 的版本,NumPy 1.25 支持 Python 3.9 或更高版本,因此 NumPy 1.25 默认与 1.19 兼容。因此,_新_ API 的用户可能需要添加定义,但希望与旧版本兼容的用户无需执行任何操作,除非他们希望具有非常长的兼容性。

过去几年的 API 新增内容非常有限,因此这种更改最多应该对全球少数用户造成影响。

这种机制与Python 受限 API非常相似,因为 NumPy 的 C-API 对 ABI 稳定性有类似的需求。

破坏 C-API 并更改 ABI#

NumPy 拥有过多的函数,其中许多是别名。以下列出了我们计划删除的内容的示例,用户必须适应才能与 NumPy 2.0 兼容

  • PyArray_MeanPyArray_Std 是类似于 arr.mean()arr.std() 的未经测试的实现。我们计划删除这些,因为它们可以用方法调用相对容易地替换。

  • MapIter API 函数(和结构)集允许在下游实现类似于高级索引的语义。此 API 只有一个历史上的已知用户(theano),并且用例可以通过其他方式更快更容易地实现。该 API 复杂,需要深入了解 NumPy 才能发挥作用,并且它的公开使得实现更加困难。除非发现新的重要用例,否则我们建议将其删除。

ABI 更改的一个示例是更改 PyArray_Descrnp.dtype 实例的结构)的布局,以允许更大的最大 itemsize 和新的标志(对未来的自定义用户 DType 有用)。对于此特定更改,直接访问结构字段的用户必须更改其代码。下游搜索表明这应该不会很常见,主要影响是

  • descr->elsize 字段(以及其他字段)的访问必须替换为类似于 PyDataType_ITEMSIZE(descr) 的宏(NumPy 可能会在需要时包含版本检查)。

  • 用户定义 dtype 的实现者将不得不更改几行代码,幸运的是,此类用户定义的 dtype 很少。(细节是我们将结构重命名为 PyArray_DescrProto 用于静态定义,并显式地从 NumPy 获取实际实例。)

最后一个例子是将NPY_MAXDIMS增加到64NPY_MAXDIMS主要用于静态分配临时空间。

func(PyArrayObject *arr) {
    npy_intp shape[NPY_MAXDIMS];
    /* Work with a shape or strides from the array */
}

如果NumPy在一个小版本中将其更改为64,那么如果代码是用NPY_MAXDIMS=32编译的,但传入的是一个40维数组,则会导致未定义的行为。但是,更大的值在以前版本的NumPy中也是正确的最大值,因此对于NumPy 2.0的更改通常是安全的。(可以想象一些代码想要知道实际的运行时值。我们实际上还没有见过这样的代码,但它需要进行调整。)

对Cython用户的影响#

Cython用户可以通过cimport numpy as cnp使用NumPy C-API。由于Cython开发的不确定性,对Cython用户的影响有两种情况。

如果可以依赖Cython 3,那么Cython用户受到的影响将比C-API用户_更小_,因为Cython 3允许我们隐藏结构布局更改(即对PyArray_Descr的更改)。如果不是这种情况,并且我们必须支持Cython 0.29.x(这是Cython 3之前的历史分支),那么Cython用户也必须使用类似PyDataType_ITEMSIZE()的函数/宏(或使用Python对象)。不幸的是,这在Cython代码中不太常见,但也不太可能是dtype结构体字段/属性的常见模式。

另一个影响是,一些未来的API新增内容(例如新类)可能需要放在一个单独的.pyd文件中,以避免Cython生成在旧版NumPy版本上会失败的代码。

最终用户和打包的影响#

以与NumPy 2.0兼容的方式打包需要重新编译依赖于NumPy C-API的下游库。这可能需要一些时间,尽管希望这个过程可以在NumPy 2.0本身发布之前开始。

此外,为了更容易地在NumPy 2.0中进行更大的更改,我们预计会创建一个numpy2_compat包。当库使用NumPy 2.0构建但又想要支持NumPy 1.x时,它将必须依赖于numpy2_compat。最终用户不需要知道此依赖关系,并且在模块丢失时可以引发提示性错误。

一些新的API可以回传#

允许用户使用最新版本的NumPy编译的一个很大的优势是,在某些情况下,我们将能够回传新的API。一些新的API函数可以用旧的函数来编写,或者直接包含。

注意

可以通过兼容的numpy2_compat包,将NumPy 1.x中存在的但私有的函数公开。

这意味着一些新的API添加可以更快地提供给下游用户。它们需要新的NumPy版本进行_编译_,但它们的轮子可以向后兼容于早期版本。

实现#

实现的第一部分(允许为早期API版本构建)非常简单,因为NumPy C-API多年来发展缓慢。一些结构体字段将默认隐藏,并且除非用户选择使用较新的API版本,否则在较新版本中引入的函数将被标记并隐藏。实现可以在PR 23528中找到。

第二部分主要在于以不会破坏向后兼容性并且API中断对下游库的可管理的方式识别和实现所需的更改。我们所做的每一次更改都必须简要说明如何适应API更改(即替代函数)。

NumPy 2兼容性和API表更改#

为了允许更改API表,NumPy 2.0将提供与NumPy 1.x不同的表(表是函数和符号的列表)。

为了兼容性,我们需要将1.x表转换为2.0表。理论上这只能在头文件中完成,但这似乎很笨拙。因此,我们建议添加一个numpy2_compat包。这个包的主要目的是在一个地方提供1.x表到2.x表的转换(填补任何必要的空白)。

引入此包解决了“过渡”问题,因为它允许用户

  • 安装与2.0和1.x兼容的SciPy版本

  • 并继续使用NumPy 1.x,因为他们使用的其他包尚未兼容。

numpy2_compat的导入(以及缺少时的错误)将作为import_array()调用的一部分由NumPy头文件插入。

替代方案#

始终可以选择不进行某些更改(例如,由于下游用户注意到他们继续需要它)。例如,如果需要,可以使用一个调用array.mean()的函数来替换函数PyArray_Mean

NEP建议通过引入兼容性包numpy2_compat来允许对我们的API表进行更大的更改。我们可以在不引入此类包的情况下进行许多更改。

默认API版本可以选择为较旧版本或当前版本。较旧版本的目标是那些希望获得比NEP 29建议更大的兼容性的库。选择当前版本默认为为不分发轮子的用户删除不必要的兼容性垫片。建议的默认值选择偏向于分发轮子并希望兼容范围类似于NEP 29的库。这是因为兼容性垫片应该轻量级,我们预计很少有库需要更长的兼容性。

向后兼容性#

如上所述,向后兼容性是通过以下方式实现的:

  1. 强制下游使用NumPy 2.0重新编译

  2. 提供numpy2_compat库。

但依赖于用户根据“用法和影响”部分中的描述来适应已更改的C-API。

讨论#

  • numpy/numpy#5888之前提到,允许在我们的头文件中导出旧版API版本将很有帮助。这从未实现,相反,我们依赖于oldest-support-numpy

  • 此提案的初稿于2023年4月3日在NumPy 2.0规划会议上提出。

参考文献和脚注#