NEP 53 — 为 NumPy 2.0 发展 NumPy C-API #

作者

塞巴斯蒂安·伯格< 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 一起使用。但是,创建与 NumPy 1.x 和 NumPy 2.0 兼容的单个 SciPy 二进制文件仍然必须很容易。

鉴于这些限制,本 NEP 概述了允许对 C-API 进行重大更改的前进道路。与针对 NumPy 2.0 提出的 Python API 更改类似,NEP 旨在允许进行一定程度的更改,预计大多数下游包不需要或只需要进行少量代码更改。

该新经济政策的实施包括两个步骤:

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

  2. NumPy 2.0 将:

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

    • 在 NumPy 1.x 上运行时需要 anumpy2_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的用户可能需要添加定义,但是想要兼容旧版本的用户不需要做任何事情,除非他们希望具有特别长的兼容性。

过go几年添加的 API 非常有限,因此对于全球的一小部分用户来说,这样的更改最多是必要的。

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

打破 C-API 并更改 ABI #

NumPy 的函数太多,其中很多都是别名。以下列出了我们计划删除的内容示例,用户必须进行调整才能与 NumPy 2.0 兼容:

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

  • API 函数(和结构)集MapIter允许实现高级索引,例如下游语义。这个历史上有一个 已知的用户(theano),并且用例会以不同的方式更快、更容易地实现。 API 很复杂,需要深入 NumPy 才能发挥作用,而且它的暴露使得实现更加困难。除非发现新的重要用例,否则我们建议将其删除。

PyArray_Descr ABI 更改的一个示例是更改(实例结构)的布局np.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 */
}

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

对 Cython 用户的影响#

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

如果可以依赖 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 函数可以根据旧的 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(以及缺少时的错误)将由 NumPy 读取器作为调用的一部分插入import_array()

备择方案

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

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

默认 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 规划会议上提交。

参考文献和脚注#