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 的实现将包括两个步骤:
作为通用改进的一部分,从 NumPy 1.25 开始,默认情况下构建 NumPy 将导出较旧的 API 版本,以允许与最新可用 NumPy 版本进行向后兼容的构建。(除非选择加入,否则新 API 不可用。)
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_Mean和PyArray_Std是未经测试的实现,类似于arr.mean()和arr.std()。我们计划移除它们,因为可以通过方法调用相对容易地替换它们。MapIterAPI 函数(和结构体)集允许下游实现高级索引语义。曾经有一个*历史性*的已知用户(theano),并且该用例可以通过其他方式更快、更容易地实现。该 API 很复杂,需要深入 NumPy 才能有用,并且其暴露会使实现更加困难。除非发现新的重要用例,否则我们建议将其移除。
ABI 更改的一个示例是更改 PyArray_Descr(np.dtype 实例的结构体)的布局,以允许更大的最大项大小和新的标志(对未来的用户自定义 dtype 有用)。对于此特定更改,直接访问结构体字段的用户将不得不更改其代码。下游搜索表明这不应非常普遍,主要影响是:
对
descr->elsize(和其他)字段的访问需要替换为宏,例如PyDataType_ITEMSIZE(descr)(NumPy 可能在需要时包含版本检查)。用户定义 dtype 的实现者将不得不更改几行代码,幸运的是,这种用户定义 dtype 非常少。(细节是我们将结构体重命名为
PyArray_DescrProto用于静态定义,并显式地从 NumPy 获取实际实例。)
最后一个例子是增加 NPY_MAXDIMS 到 64。NPY_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 的兼容范围的库。这是因为兼容性填充程序应该轻量级,并且我们预计很少有库需要更长的兼容性。
向后兼容性#
如上所述,向后兼容性通过以下方式实现:
强制下游重新编译 NumPy 2.0
提供
numpy2_compat库。
但依赖用户根据“用法和影响”部分所述来适应更改的 C-API。
讨论#
numpy/numpy#5888 之前提到,允许在我们的头文件中导出较旧的 API 版本会很有帮助。这一点从未实现,而是我们依赖 oldest-support-numpy。
此提案的初稿已在 NumPy 2.0 规划会议 2023-04-03 上展示。
参考文献和脚注#
版权#
本文档已置于公共领域。[1]