NEP 53 — NumPy 2.0 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 一起使用。然而,创建单个与 NumPy 1.x 和 NumPy 2.0 都兼容的 SciPy 二进制文件仍然必须容易。

考虑到这些限制,此 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 函数(和结构体)集允许下游实现高级索引语义。曾经有一个*历史性*的已知用户(theano),并且该用例可以通过其他方式更快、更容易地实现。该 API 很复杂,需要深入 NumPy 才能有用,并且其暴露会使实现更加困难。除非发现新的重要用例,否则我们建议将其移除。

ABI 更改的一个示例是更改 PyArray_Descrnp.dtype 实例的结构体)的布局,以允许更大的最大项大小和新的标志(对未来的用户自定义 dtype 有用)。对于此特定更改,直接访问结构体字段的用户将不得不更改其代码。下游搜索表明这不应非常普遍,主要影响是:

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

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

最后一个例子是增加 NPY_MAXDIMS64NPY_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 版本,但它们的 wheel 可以向后兼容早期版本。

实现#

实现的第一部分(允许为早期 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,因为他们使用的其他包尚未兼容。

作为 import_array() 调用的一部分,NumPy 头文件将插入 numpy2_compat 的导入(并且在缺失时会报错)。

替代方案#

总是有可能决定不进行某些更改(例如,由于下游用户注意到他们仍需要它)。例如,如果需要,函数 PyArray_Mean 可以替换为调用 array.mean() 的函数。

此 NEP 提议通过引入兼容包 numpy2_compat 来允许对我们的 API 表进行更大更改。没有这个包,我们可以进行许多更改。

默认 API 版本可以被选择为较旧的版本或当前版本。较旧的版本将面向希望获得比 NEP 29 所建议的更长兼容性的库。选择当前版本将默认移除不需要的兼容性填充程序,供不分发 wheel 的用户使用。建议的默认选择有利于分发 wheel 并希望获得类似 NEP 29 的兼容范围的库。这是因为兼容性填充程序应该轻量级,并且我们预计很少有库需要更长的兼容性。

向后兼容性#

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

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

  2. 提供 numpy2_compat 库。

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

讨论#

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

  • 此提案的初稿已在 NumPy 2.0 规划会议 2023-04-03 上展示。

参考文献和脚注#