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 1.10)编译的 SciPy 版本无法与 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 1.x 上运行时需要 numpy2_compat 作为依赖项。

    • 需要对某些下游代码进行更改以适应已更改的 API。

动机和范围#

NumPy API 包含 300 多个函数和众多宏。其中许多已经过时:有些仅在 NumPy 内部使用,仅出于与 NumPy 前身兼容的目的而存在,或者没有或只有一个已知的下游用户(即 SciPy)。

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

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

此 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 实例的结构)的布局,以允许更大的最大项大小和新标志(对未来的自定义用户 DType 有用)。对于此特定更改,直接访问结构字段的用户将必须更改其代码。下游搜索表明这应该不常见,主要影响是

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

  • 用户定义数据类型的实现者将不得不更改几行代码,幸运的是,此类用户定义的数据类型很少。(细节是我们将结构重命名为 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,因为他们正在使用的其他包尚未兼容。

NumPy头文件将在import_array()调用的过程中插入numpy2_compat的导入(以及缺少时的错误)。

替代方案#

始终有可能决定不进行某些更改(例如,由于下游用户注意到他们继续需要它)。例如,如果需要,函数PyArray_Mean可以替换为调用array.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计划会议上发布。

参考文献和脚注#