NEP 56 — NumPy 主命名空间中的 Array API 标准支持#
- 作者:
Ralf Gommers <ralf.gommers@gmail.com>
- 作者:
Mateusz Sokół <msokol@quansight.com>
- 作者:
Nathan Goldbaum <ngoldbaum@quansight.com>
- 状态:
最终
- 取代:
NEP 30 — NumPy 数组的鸭子类型化 - 实现, NEP 31 — NumPy API 的上下文局部和全局覆盖, NEP 37 — 类似 NumPy 模块的调度协议, NEP 47 — 采用 Array API 标准
- 类型:
标准跟踪
- 创建日期:
2023-12-19
- 决议:
摘要#
本 NEP 提议在 NumPy 2.0 版本发布时,为其主命名空间添加对 2022.12 版 Array API 标准的几乎完整支持。
在主命名空间中采用有许多优点;最重要的是,对于依赖 NumPy 并希望开始支持其他数组库的库而言。SciPy 和 scikit-learn 是两个已在这条道路上前进的著名库。在主命名空间中支持 Array API 标准的需求,源于这些库从实验性 numpy.array_api
实现(带有不同数组对象)中学到的经验教训。对于其他数组库、像 Numba 这样的 JIT 编译器以及可能更容易在不同数组库之间切换的最终用户来说,也将带来益处。
动机与范围#
注意
本 NEP 中提出的主要变更已在 2023 年 4 月的 NumPy 2.0 开发者会议上提出(有关该会议的演示文稿,请参阅此处),并在会上获得了认可。NumPy 2.0 的大部分实现工作已经合并。至于其余部分,拉取请求(PR)已准备就绪——这些主要是 Array API 支持的特定项,如果没有该背景,我们可能不会考虑将其纳入 NumPy。本 NEP 将更详细地关注这些 API 和 PR。
NEP 47 — 采用 Array API 标准 包含了在 NumPy 中添加 Array API 支持的动机。本 NEP 扩展并取代了 NEP 47。NEP 47 旨在创建一个独立的 numpy.array_api
子模块而非在主命名空间中实现的主要原因是类型转换规则差异太大。随着基于值的类型转换被移除(NEP 50 — Python 标量的提升规则),这个问题将在 NumPy 2.0 中得到解决。让 NumPy 成为 Array API 标准的超集将显著提高代码向其他库(如 CuPy、JAX、PyTorch 等)的可移植性,从而解决 2020 年 NumPy 用户调查中一项最重要的用户请求[4](GPU 支持)。有关其与主命名空间之间差异的概述,请参阅numpy.array_api API 文档(1.26.x)(请注意,“严格性”差异不适用)。
numpy.array_api
模块目前仍标记为实验性,其使用经验表明,独立的严格实现和独立的数组对象主要适用于测试目的,但不适合在下游库中常规使用。在主命名空间中提供支持可以解决这个问题。因此,本 NEP 取代了 NEP 47。numpy.array_api
模块将被移至一个独立的包,以便于更轻松地进行更新,而不受 NumPy 发布周期的限制。
Array API 标准中的一些关键设计规则(例如,输出 dtype 可从输入 dtype 预测,没有由关键字控制不同返回数量的多态 API)也将应用于不属于 Array API 标准的 NumPy 函数,因为这些设计规则现在普遍被认为是良好实践。特别是这两条设计规则,使得 Numba 和其他 JIT 编译器更容易支持 NumPy 或与 NumPy 兼容的 API。我们注意到,将现有参数设为仅位置参数和仅关键字参数对于未来添加到 NumPy 的函数来说是个好主意,但不会对现有函数进行此操作,因为每次此类更改都会破坏向后兼容性,而且对于编写可跨支持标准的库移植的代码来说,这并非必要。现在将这些设计规则应用于主命名空间中的所有函数,还有一个额外原因,那就是这样可以更容易地处理 NumPy 中已有的新函数的潜在标准化——否则,由于需要向后兼容性,这些函数可能会被阻止或被迫使用替代函数名。
新增到主命名空间的函数与 NumPy 的其余部分良好集成至关重要。因此,它们应按预期遵循广播和其他规则,并与 NumPy 的所有 dtype 配合使用,而不仅仅是标准中包含的那些。对于向后不兼容的更改也是如此(例如,线性代数函数需要以相同的方式支持批处理,并将最后两个轴视为矩阵)。因此,NumPy 应该变得更加一致,而不是更不一致。
以下是我们认为这套完整提议变更的主要预期收益和成本:
收益
这将使数组消费库(例如 SciPy 和 scikit-learn,以及更上层的较小库)能够实现对多个数组库的支持,
这将消除其他数组库在选择实现何种 API 时“必须在 NumPy API 和 Array API 标准之间做出选择”的问题,
使 CuPy、JAX、PyTorch、Dask、Numba 等库和编译器更容易匹配或支持 NumPy,通过提供更明确和最小化的目标 API 接口,以及通过解决一些由 NumPy 语义引起、JIT 编译器难以支持的差异,
一些独立于标准但仍有益处的新功能:添加
matrix_transpose
和ndarray.mT
,添加vecdot
,引入matrix_norm
/vector_norm
(它们可以被制成 gufuncs,vecdot 已经有使其成为 gufunc 的 PR),NumPy API 与其他数组库 API 之间更紧密的对应关系将降低最终用户在不同数组库之间切换时的学习曲线,
Array API 标准的行为往往比 NumPy 本身更一致(在两者存在差异的情况下,例如请参阅标准中的线性代数设计原则和数据依赖输出形状页面),
成本
若干向后兼容性破坏(大部分是轻微的,详见下文的“向后兼容性”部分),
扩大主命名空间的大小,大约增加 20 个别名(例如,使用 C99 名称的
acos
及相关函数,作为arccos
及相关函数的别名)。
总的来说,我们认为收益显著大于成本——而且收益是永久性的,而成本则主要是暂时的。特别是,对于希望与 NumPy 实现兼容的数组库和编译器而言,收益是巨大的。因此,整个 PyData(或科学 Python)生态系统的长期收益——因为下游库能够更容易地支持多个数组库——也同样巨大。所需的破坏性变更数量相当有限,且这些变更的影响似乎不大。虽然并非毫无痛苦,但我们认为其影响小于 NumPy 2.0 中其他破坏性变更的影响,是值得付出的代价。
本 NEP 的范围包括
为了支持 2022.12 版 Array API 标准而需要对 NumPy Python API 进行的更改,包括在主命名空间以及
numpy.linalg
和numpy.fft
中,对现有 NumPy 函数行为的更改,这些函数尚未(或暂时未)包含在 Array API 标准中,以与标准的关键设计原则保持一致。
本 NEP 的范围不包括
与 Array API 标准无关的 NumPy Python API 的其他更改,
对 NumPy C API 的更改。
本 NEP 将取代以下 NEP
NEP 37 — 类似 NumPy 模块的调度协议(从未实现;
__array_module__
的概念与__array_namespace__
基本相同)NEP 47 — 采用 Array API 标准(已在
numpy.array_api
中以实验性标签实现,将被移除)
用途与影响#
我们考虑到几种不同类型的用户:编写数值代码的最终用户;依赖 NumPy 并希望开始支持多个数组库的下游包;以及旨在实现类似 NumPy 或与 NumPy 兼容 API 的其他数组库和工具。
从 Array API 支持中受益最显著的用户可能是那些希望开始支持 CuPy、PyTorch、JAX、Dask 或其他类似库的下游库。SciPy 和 scikit-learn 在这方面已经取得了相当大的进展,并在其自身 API 的一小部分中成功支持了 CuPy 数组和 PyTorch 张量(该支持仍标记为实验性)。
他们使用的主要原则是,用一个工具函数来替换常规的 import numpy as np
,该工具函数从输入数组中检索数组库的命名空间。他们将其命名为 xp
,如果输入是 NumPy 数组,它实际上是 np
的别名;如果是 CuPy 数组,则是 cupy
;如果是 PyTorch 张量,则是 torch
。这个 xp
允许编写适用于所有这些库的代码——因为 Array API 标准是它们共同的基础。作为一个具体示例,这段代码取自 scipy.cluster
def vq_py(obs, code_book, check_finite=True):
"""Python version of vq algorithm"""
xp = array_namespace(obs, code_book)
obs = as_xparray(obs, xp=xp, check_finite=check_finite)
code_book = as_xparray(code_book, xp=xp, check_finite=check_finite)
if obs.ndim != code_book.ndim:
raise ValueError("Observation and code_book should have the same rank")
if obs.ndim == 1:
obs = obs[:, xp.newaxis]
code_book = code_book[:, xp.newaxis]
# Once `cdist` has array API support, this `xp.asarray` call can be removed
dist = xp.asarray(cdist(obs, code_book))
code = xp.argmin(dist, axis=1)
min_dist = xp.min(dist, axis=1)
return code, min_dist
它看起来大多像普通的 NumPy 代码,但如果输入是 PyTorch 张量,它也将运行并返回 PyTorch 张量。当然,除了这个基本示例,这个故事还有更多内容。scikit-learn[1] 和 SciPy[2] 关于其经验和影响的这些博客文章(在某些情况下性能大幅提升——LinearDiscriminantAnalysis.fit
在 GPU 上使用 PyTorch 比 NumPy 提高了约 28 倍)描绘了一幅更完整的图景。
对于直接使用 NumPy 的最终用户来说,除了 NumPy 与他们可能也想使用的其他库之间的差异更少之外,几乎没有变化。这缩短了他们的学习曲线,并使在 NumPy 和 PyTorch/JAX/CuPy 之间切换变得更容易。此外,他们应该受益于数组消费库开始支持多个数组库,使他们在科学计算或数据科学中使用 Python 包栈的体验更加无缝。
最后,对于其他数组库的作者以及像 Numba 这样的工具来说,使 NumPy 与 Array API 标准对齐的 API 改进也将为他们节省时间。由于行为更可预测,设计规则([3])以及在某些情况下像 unique_*
这样的新 API,更容易在 GPU 和 JIT 编译器上实现。
向后兼容性#
具有向后兼容性影响的更改分为以下几类:
在 NumPy 目前允许更灵活行为的一些地方,为了保持一致性/严格性而抛出错误,
某些逐元素函数和归约函数的返回数组的 dtype,
少数容差关键字的数值行为,
移至
numpy.linalg
并支持堆叠/批处理的函数,asarray
和array
中copy
关键字的语义,对
numpy.fft
功能的更改。
为了保持一致性/严格性而抛出错误包括:
使
.T
在维度大于 2 时报错,使
cross
在大小为 2 的向量上报错(仅支持大小为 3 的向量),使
solve
在输入不明确时报错(仅当x2.ndim == 1
时才将x2
视为向量),outer
在处理多维输入时将引发错误而非展平,
我们预计这类更改的影响将很小。
某些逐元素函数和归约函数的返回数组的 Dtype 包括需要保留 dtype 的函数:如果输入具有整数 dtype,ceil
、floor
和 trunc
将开始返回具有相同整数 dtype 的数组。
我们预计这类更改的影响将很小。
数值行为的更改 包括
pinv
的rtol
默认值从1e-15
更改为依赖于 dtype 的默认值None
,其解释为max(M, N) * finfo(result_dtype).eps
,matrix_rank
的tol
关键字更改为rtol
,并具有不同的解释。此外,matrix_rank
将不再支持一维数组输入,
为这些容差更改发出 FutureWarning
似乎不合理;它们对绝大多数用户来说是虚假的警告,并且会迫使用户硬编码一个容差值以避免警告。数值结果的改变原则上是不受欢迎的,因此,尽管我们预计影响会很小,但在主要版本中进行此更改会更好。
我们预计这类更改的影响中等。它是唯一不会导致明确异常或警告的更改类别,因此如果它确实产生影响(例如,下游测试开始失败或用户注意到行为变化),可能需要用户投入更多精力来追踪问题。这种情况应该不常发生——在实现此更改的 PR 合并一个月后(参见 gh-25437),截至目前报告的影响是 AstroPy 中出现了一个测试失败。
移至 numpy.linalg 并支持堆叠/批处理的函数 是 diagonal
和 trace
函数。它们是标准中 linalg
子模块的一部分,而不是主命名空间。因此,它们将在 numpy.linalg
中引入。它们将作用于最后两个轴,而非前两个轴。这样做是为了保持一致性,因为这是现在其他 NumPy 函数的工作方式,并支持“堆叠”(或在其他库中更常用的术语“批处理”)。因此,linalg
和主命名空间中同名函数的行为将有所不同。这在技术上并非破坏性更改,但由于同名函数的行为不同,可能会造成混淆。我们可能会弃用 np.trace
和 np.diagonal
来解决此问题,但最好不要立即弃用,以避免用户需要编写 if-2.0-else
条件代码。
我们预计这类更改的影响将很小。
asarray
和 array
中 copy
关键字的语义 对于 copy=False
将从“如果需要则复制”变为“永不复制”。现在有三种行为类型而非两种——copy=None
表示“如果需要则复制”。
我们预计这类更改的影响中等。如果用户由于显式使用了 copy=False
但之前无论如何都会进行复制而遇到异常,他们必须检查代码并确定代码的意图是旧语义还是新语义(两者可能性大致相同),并适当地调整代码。我们预计大多数情况是 np.array(..., copy=False)
,因为直到几年前,它的开销低于 np.asarray(...)
。不过这个问题已经解决,并且 np.asarray(...)
是惯用的 NumPy 用法。
对 numpy.fft 的更改:numpy.fft
子模块中的所有函数都需要为 32 位输入 dtype 保留精度,而不是提升到 float64
/complex128
。这是一个值得期待的更改,与 NumPy 整体设计一致——但该模块中函数调用返回的数组的较低精度或 dtype 可能会影响用户。此更改是通过基于 gufunc 的新实现和 PocketFFT 的 C++ 版本内嵌实现的(gh-25711)。
对 numpy.fft
的一个较小的向后不兼容更改是,通过禁止 s
中出现 None
值,并要求如果使用 s
,则必须同时指定 axes
(参见 gh-25495),使 n 维变换中 s
和 axes
参数的行为更容易理解。
我们预计这类更改的影响将很小。
适应更改与工具支持#
Array API 的某些部分已经实现,作为 NumPy 2.0 通用 Python API 清理工作的一部分(参见 NEP 52),例如
建立一种与 Array API 兼容的
inf
和nan
命名方式。移除隐晦的 dtype 名称,并为每种 dtype 建立(与 Array API 兼容的)规范名称。
迁移到与 NEP 52 兼容代码库的所有说明都可在 NumPy 2.0 迁移指南 中找到。
此外,为了自动迁移 Python API 更改,还实现了一个新的 ruff
规则。值得指出的是,新规则 NP201 仅用于遵循 NEP 52 的更改,不包括使用 Array API 标准中的新函数,也不包括上面讨论的某些类型的向后不兼容更改的 API。
为了自动迁移到与 Array API 兼容的代码库,正在实现一个新规则(参见 issue ruff#8615 和 PR ruff#8910)。
随着这两个规则的到位,下游用户应该能够,在自动化可能的范围内,将他们的项目更新为一个与库无关的代码库,从而受益于不同的数组库和设备。
无法自动处理的向后不兼容更改(例如,线性代数函数 rtol
默认值的更改)将以与 NumPy 2.0 中任何其他向后不兼容更改相同的方式处理——通过文档、发布说明、API 迁移和跨多个版本的弃用。
详细说明#
在本节中,我们将重点介绍特定的 API 添加和功能,如果没有该标准,并且我们不必考虑/担心其主要目标——编写可在多个数组库及其支持的功能(如 GPU 和其他硬件加速器或 JIT 编译器)之间移植的代码,我们将不会考虑将其引入 NumPy。
device
支持#
设备支持也许是最明显的例子。NumPy 是且将保持为仅限 CPU 的库,那么为什么还要费力在多个函数中引入 ndarray.device
属性或 device=
关键字呢?这一功能纯粹是为了让编写可在不同库之间移植的代码变得更容易。.device
属性将返回一个表示 CPU 的对象,并且该对象将被接受作为 device=
关键字的输入。例如
# Should work when `xp` is `np` and `x1` a numpy array
x2 = xp.asarray([0, 1, 2, 3], dtype=xp.float64, device=x1.device)
这对于 NumPy 来说将按预期工作,从输入列表创建一个一维 NumPy 数组。它也将适用于 CuPy 及类似库,在这些库中,它可能会在 GPU 或其他受支持的设备上创建一个新数组。
isdtype
#
Array API 标准引入了一个新的函数 isdtype
用于 dtype 的自省,因为 NumPy 中没有合适的替代方案。最接近的是 np.issubdtype
,但它假定了一个复杂的类层次结构,而其他数组库不具备这种结构,它也不是最符合人体工程学的 API,并且需要更大的 API 表面(np.floating
及相关函数)。isdtype
将成为自省 dtype 的新的规范方式。它对 dtype 的所有要求是 __eq__
被实现,并且在与同一库中的其他 dtype 比较时具有预期行为。
请注意,作为 NEP 52 工作的一部分,一些 dtype 别名已被移除,并且 Python 和 C 的规范名称已进行文档化。另请参阅 gh-17325,其中涵盖了 NumPy 缺乏良好 API 的相关问题。
copy
关键字语义#
asarray
和 array
中的 copy
关键字现在将支持具有新含义的 True
/False
/None
。
True
- 始终创建副本。False
- 永不创建副本。如果需要副本,则引发ValueError
。None
- 仅在必要时才创建副本(此前为False
)。
astype
中的 copy
关键字将保持其当前含义,因为在请求转换为不同 dtype 时,“永不复制”没有太大意义。
语义更改仍有一个小问题:如果用户代码中使用 np.array(obj, copy=False)
,NumPy 最终可能会调用 obj.__array__
,在这种情况下,将结果转换为 NumPy 数组是 obj.__array__
实现者的责任。因此,我们还需要为 __array__
添加一个 copy=None
关键字,并传递 copy 关键字值——同时注意在 __array__
的实现者尚未拥有新关键字时不要破坏向后兼容性(在这种情况下将发出 DeprecationWarning
,以允许逐步过渡)。
新函数名称别名#
在 NumPy 2.0 的 Python API 清理工作中(参见 NEP 52 — NumPy 2.0 的 Python API 清理),我们投入了大量精力移除别名。因此,引入新别名必须有充分的理由。在这种情况下,需要它以匹配其他库。添加的主要别名集是用于三角函数,其中 Array API 标准选择遵循 C99 和其他库的约定,使用 acos
、asin
等,而不是 arccos
、arcsin
等。NumPy 通常也遵循 C99;目前尚不完全清楚为什么多年前会做出这种命名选择。
总共有 13 个别名添加到主命名空间,2 个别名添加到 numpy.linalg
三角函数:
acos
、acosh
、asin
、asinh
、atan
、atanh
、atan2
位操作函数:
bitwise_left_shift
、bitwise_invert
、bitwise_right_shift
其他函数:
concat
、permute_dims
、pow
在
numpy.linalg
中:tensordot
、matmul
未来 NumPy 可以选择从其 __dir__
中隐藏原始名称,以引导用户使用每个函数的首选拼写。
具有重叠语义的新关键字#
类似于函数名称别名,也有一些新关键字与现有关键字有重叠。
correction
关键字用于std
和var
(与ddof
重叠)stable
关键字用于sort
和argsort
(与kind
重叠)
correction
名称是为了清晰度(“自由度增量”不易理解)。stable
与 kind
互补,kind
已经有 'stable'
选项(尽管单独的关键字可能更容易发现,因此无论如何都值得拥有),允许库保留更改/改进稳定和不稳定排序算法的权利。
新的 unique_*
函数#
unique
函数,带有影响返回元组基数的 return_index
、return_inverse
和 return_counts
参数,在 Array API 中被四个相应的函数取代:unique_all
、unique_counts
、unique_inverse
和 unique_values
。这些新函数避免了多态性,这往往是 JIT 编译器和静态类型化的问题。因此,使用这些函数有助于像 Numba 这样的工具以及像 Mypy 这样的静态类型检查器用户。
np.bool
添加#
曾经存在于 NumPy 中但后来被移除的别名之一是 np.bool
。为了符合 Array API,它以不同的含义重新引入,因为它现在指向 NumPy 的布尔类型而非 Python 内置类型。这个改变是个好主意,我们无论如何都打算进行,因为 bool
比 bool_
更好听。然而,如果它不是 Array API 标准的一部分,我们可能不会将该名称的重新引入安排在 2.0 版本。
未采用的标准部分#
标准中规定的一些事项,我们提议 不 遵循(至少目前如此)。这些是
要求
sum
和prod
在dtype=None
时,总是将低精度浮点 dtype 向上转换为float64
。理由:这可能具有破坏性(例如,
float32_arr - float32_arr.mean()
将会产生一个float64
数组,并使内存使用加倍)。尽管这种向上转换已经用于低精度整数 dtype 的输入,并且在那里似乎有助于防止溢出,但要求浮点 dtype 也如此似乎不太合理。array-api#731 被提出以重新考虑标准中的这一设计选择,并且已被下一版本标准接受。
在许多地方将函数签名设为仅位置参数和仅关键字参数。
理由:2022.12 版的标准规定为“必须”,但在即将发布的 2023.12 版中,这已被放宽为“应该”,以承认不这样做也是可以的——毕竟数组库的用户仍然可以使用推荐的风格编写代码。对于 NumPy 来说,这些更改将是有益的,我们很可能随着时间推移引入其中许多或所有更改(事实上,ufuncs 已经兼容),然而没有必要急于进行此更改——在 2.0 版本中这样做会造成不必要的干扰。
要求“原地操作必须与其对应的二元(即,双操作数,非赋值)操作具有相同的行为(包括特殊情况)”(不包括对视图的影响)。
理由:该要求非常合理,并且可能是大多数 NumPy 用户所期望的行为。然而,弃用原地操作符的不安全类型转换是一种难以预测其影响的更改。因此,这需要首先进行调查,然后如果影响足够小,则有可能根据 NumPy 的常规向后兼容性准则弃用当前行为。
此议题在 gh-25621 中跟踪。
注意
我们注意到一个保留的 NumPy 特有行为是,在大多数标准和其他数组库返回 0-D 数组的情况下(例如索引和归约),NumPy 返回数组标量而非 0-D 数组。数组标量基本上是 0-D 数组的鸭子类型,这是标准所允许的(它不强制只存在一种数组类型,也不包含 isinstance
检查或其他不适用于数组标量的语义)。过去一年中,关于从 NumPy 中移除数组标量,或者至少默认不再返回它们的可能性,进行了多次讨论。然而,这将是一项巨大的努力,并且存在技术风险和更改影响的不确定性,目前尚无人承担。鉴于数组标量实现了一个大体上与数组兼容的接口,这似乎不是 Array API 标准兼容性(或总体而言)方面优先级最高的项目。
实现#
Array API 标准支持的跟踪问题(gh-25076)记录了实现全面支持的进展并链接到相关讨论。它列出了所有验证或提供 Array API 支持的相关 PR(已合并和待处理)。
由于 NEP 52 在某种程度上与本 NEP 融合,我们也可以在其跟踪问题(gh-23999)中找到一些相关的实现和讨论。
作为首批合并的 PR 之一,它包含了一个新的 CI 任务,该任务添加了 array-api-tests 测试套件。通过这种方式,我们可以更好地控制每次添加的函数/别名批次,并确保实现符合 Array API 标准(参见 gh-25167)。
然后,我们继续每次合并一个批次,添加特定的 API 部分。下面我们列出了一些更重要的内容,包括本 NEP 前几节中讨论过的一些
替代方案#
在 NumPy 主命名空间中实现 Array API 标准支持的替代方案包括
一个或多个被取代的 NEP,或
使
ndarray.__array_namespace__()
返回一个带有兼容函数的隐藏命名空间(甚至另一个新的公共命名空间),完全不实现对 Array API 标准的支持。
所有被取代的 NEP 与 Array API 标准相比都存在一些缺点,并且到目前为止,该标准已经投入了大量工作——以及被其他关键库采纳。因此,这些替代方案没有吸引力。考虑到对这个话题的浓厚兴趣,什么都不做也没有吸引力。“隐藏命名空间”选项将是本提案的一个较小更改。我们倾向于不这样做,因为它会导致重复的实现存在,一个更复杂的实现(例如,静态类型化的潜在问题),并且仍然存在两种本质上相同的 API 版本。
从 NumPy 中移除 numpy.array_api
的一个替代方案是将其保留在当前位置,因为它仍然有用——它是测试下游代码是否真正可在不同数组库之间移植的最佳方式。这是一个非常合理的替代方案,但略微偏向于将该模块变为一个独立的包。
讨论#
参考文献和脚注#
版权#
本文档已进入公共领域。