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。然而,仍然必须易于创建一个与 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 结构体,可以简化 dtypes 的开发和改进。
本 NEP 主要作为示例,提出了一些对我们 C-API 的具体更改。然而,更多的更改将逐案处理,我们不旨在在此 NEP 中提供完整的更改列表。
添加状态超出范围#
CPython 对子解释器的支持和 HPy API 等新发展可能要求 NumPy C-API 以需要(或至少倾向于)传递状态的方式进行演进。
截至目前,我们不打算在此处包含为此目的的更改。我们不能期望用户进行大量的代码更新来向许多 NumPy 函数传递例如 HPy
上下文。
虽然我们可以在 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()
。我们计划移除这些函数,因为它们可以相对容易地用方法调用替换。MapIter
系列 API 函数(和结构体)允许在下游实现类似高级索引的语义。曾有一个历史上已知的用户(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 3,并且我们必须支持 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 版本,但它们的 wheels 可以向后兼容早期版本。
实现#
实现的第一部分(允许构建针对早期 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 头文件插入。
替代方案#
总是可以选择不做某些更改(例如,由于下游用户指出他们持续需要它)。例如,如有必要,函数 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。
本提案的初稿已于 2023 年 4 月 3 日的 NumPy 2.0 规划会议上提出。
参考文献和脚注#
版权#
本文档已进入公共领域。 [1]