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