NEP 54 — SIMD 基础设施演进:在迁移到 C++ 时采用 Google Highway#

作者:

Sayed Adel, Jan Wassenberg, Matti Picus, Ralf Gommers, Chris Sidebottom

状态:

已接受

类型:

标准跟踪

创建时间:

2023-07-06

决议:

TODO

摘要#

我们正在将 SIMD 内部函数框架 Universal Intrinsics 从 C 迁移到 C++。我们也将构建系统迁移到了 Meson。Google Highway 内部函数项目提议我们使用 Highway 来替代我们的 Universal Intrinsics,如 NEP 38 中所述。这是一个复杂且多方面的决定——本 NEP 旨在描述所涉及的权衡以及需要完成的工作。

动机与范围#

我们希望将基于 C 的 Universal Intrinsics(参见 NEP 38)重构为 C++。这项工作已进行了一段时间,Google 的 Highway 被建议作为替代方案,它已经用 C++ 编写,并支持可伸缩的 SVE 和其他可重用组件(如 VQSort)。

从 C 迁移到 C++ 的动机是 (a) 代码可读性和开发便利性,(b) 需要添加对无大小 SIMD 指令的支持(例如,ARM 的 SVE,RISC-V 的 RVV)。

作为可读性改进的一个例子,这是我们当前 C 通用内部函数框架中的一行典型 C 代码

// The @name@ is the numpy-specific templating in .c.src files
npyv_@sfx@  a5 = npyv_load_@sfx@(src1 + npyv_nlanes_@sfx@ * 4);

这将更改为(如 PR gh-21057 中实现的那样)

auto a5 = Load(src1 + nlanes * 4);

如果上面的 C++ 代码底层使用 Highway,它会看起来非常相似,它为单个可移植内部函数使用了与 Load 类似易于理解的名称。

上面 C 版本中的 @sfx 是类型标识符的模板变量,例如:#sfx = u8, s8, u16, s16, u32, s32, u64, s64, f32, f64#。像这样明确使用位大小编码的类型不适用于无大小的 SIMD 指令集。使用 C++ 处理起来更容易;PR gh-21057 展示了如何处理,并包含了更多完整的 C++ 代码示例。

本 NEP 的范围包括讨论采用 Google Highway 替代我们当前 Universal Intrinsics 框架的大多数相关方面,包括但不限于

  • 可维护性、领域专业知识可用性、新贡献者入职便利性及其他社会方面,

  • 可能影响 NumPy 内部设计或性能的关键技术差异和限制,

  • 构建系统相关方面,

  • 发布时间相关方面。

目前不在范围之内的,是对我们当前 SIMD 支持策略其他方面的重新审视:

  • 为函数添加 SIMD 支持时的精度与性能权衡

  • SVML 和 x86-simd-sort 的使用(以及可能存在的 aarch64 等效项)

  • 引入 Highway 的单个模块或算法(如 gh-24018 中所示)或 SLEEF(如在同一 PR 中讨论的)

用法与影响#

不适用 - 不会有显著的用户可见更改。

向后兼容性#

用户可见的 Python 或 C API 将不会有任何变化:所有控制编译和运行时 CPU 特性选择的方法都应保留,尽管由于迁移到 C++(无论是否选择 Highway/Universal Intrinsics)可能会有一些变化。

Highway 中 CPU 特性的命名与 Universal Intrinsics 不同(参见下文“支持的特性/目标”)

在 Windows 上,可能需要避免使用 MSVC,因为 Highway 使用的 pragmas 对 MSVC 的支持较差。这意味着我们可能必须使用 clang-cl 或 Mingw-w64 构建我们的 wheel。这两种方式都应该可行——我们之前已经合并了 clang-cl 支持(参见 gh-20866),并且 SciPy 使用 Mingw-w64 构建。然而,这可能会影响其他分发者或在 Windows 上从源代码构建的最终用户。

为了回应之前关于本 NEP 的讨论,Highway 现已采用 Apache 2 / BSD-3 双重许可。

高级考量#

注意

目前,本节尝试单独涵盖每个主题,并比较未来使用 NumPy 特定的 C++ 实现与使用 Google Highway 并在此基础上构建我们自己的数值例程。它尚未(或还没有)假定已做出决定或拟议决定。因此,本 NEP 并非“提议如此”,并在“替代方案”部分提供另一个选项,而是一种并排比较。

开发工作量与长期可维护性#

迁移到 Highway 可能需要投入大量开发工作。从长远来看,这有望通过 Highway 自身拥有更多维护者带宽来处理编译器支持中的持续问题和添加新平台而得到弥补。

Highway 被其他项目使用,如 Chromium 和 JPEG XL(参见 Highway 文档中的此更完整列表),这意味着可能会受益于更广泛的测试和错误报告/修复。

一个担忧是可能需要添加新的指令,而这通常最好在开发需要该指令的数值核心的过程中完成。如果指令存在于 Highway 中(它是 NumPy 仓库中的一个 git submodule),这将变得有些笨拙——需要首先实现一个临时/通用版本,然后在将新的内部函数上游后更新 submodule。

从文档方面来看,Highway 将是一个明显的优势。NumPy 的 CPU/SIMD 优化文档与 Highway 文档相比相当稀疏。

迁移策略——能否循序渐进?#

这是一个双重故事。迁移到 Highway 的静态调度内部函数可以逐步完成,如 PR gh-24018 中所示。然而,采用 Highway 的运行时调度方式必须一次性完成——我们不能(或不应该)有两种方式来做这件事。

Highway 的编译器和平台支持策略#

在添加新指令时,Highway 有一项策略,即它们必须以在 CPU 架构之间公平平衡的方式实现。

关于支持状态以及所有当前支持的架构是否将继续受支持,Jan 表示 Highway 可以承诺以下几点:

  1. 如果它能与 Clang 交叉编译并可通过标准 QEMU 进行测试,则可以纳入 Highway 的 CI。

  2. 如果它能通过 clang/gcc 交叉编译并可使用新的 QEMU(可能带有额外标志)进行测试,那么它可以在每个 Highway 版本发布前通过手动测试获得支持。

  3. 只要现有目标能在 QEMU 中编译/运行,就将继续得到支持。

Highway 不受 Google“不再支持”策略的约束(或者,如其 README 中所述,这不是一个官方支持的 Google 产品)。这不是一件坏事;这意味着它不太可能因为 Google 关于该项目的业务决策而失去支持。在 google GitHub 组织下,许多知名的开源项目都声明了这一点,例如 JAXtcmalloc

支持的特性/目标#

这两个框架都支持大量的平台和 SIMD 指令集,以及通用的标量/回退版本。目前的主要区别在于:

  • NumPy 支持 IBM Z-system (s390x, VX/VXE/VXE2),而 Highway 支持 Z14, Z15。

  • Highway 支持 ARM SVE/SVE2 和 RISC-V RVV(无大小指令),而 NumPy 不支持。

    • NumPy 中对无大小 SIMD 支持的基础工作已在 gh-21057 中完成,但 SVE/SVE2 和 RISC-V 尚未在那里实现。

指令集组的粒度也存在差异:NumPy 支持比 Highway 更细粒度的架构集。请参见 Highway 的目标列表此处(大致按 CPU 系列划分)和 NumPy 的目标列表此处(大致按 SIMD 指令集划分)。因此,使用 Highway 我们会失去一些粒度——但这可能没问题,我们并不真正需要这种级别的粒度,而且没有太多证据表明用户会明确地通过此操作来榨取他们自己的 CPU 的最后一点性能。

多目标编译策略和运行时调度#

Highway 仅编译一次,同时使用预处理技巧在同一个编译单元内为每个 CPU 特性生成多个代码段(参见 foreach_target.h 的用法和动态调度文档以了解其实现方式)。Universal Intrinsics 为每个 CPU 特性组生成多个编译单元,并多次编译,将它们(以不同的名称)全部链接在一起以进行运行时调度。Highway 的技术在 MSVC 上可能无法可靠工作,而 Universal Intrinsic 的技术在 MSVC 上确实可以工作。

哪种更健壮?专家们意见不一。Jan 认为 Highway 方法更健壮,特别是在避免链接器将带有过新指令的函数拉入最终二进制文件方面。Sayed 认为当前的 NumPy 方法(也被 OpenCV 使用)更健壮,并且更不容易遇到编译器特定的错误或更早地发现它们。双方都同意 Meson 构建系统允许指定对象链接顺序,这会产生更一致的构建。然而,这确实将 NumPy 绑定到 Meson。

Matti 和 Ralf 认为当前的构建策略对 NumPy 来说运行良好,并且改变构建和运行时调度可能带来的未知不稳定性所带来的劣势,大于采用 Highway 动态调度可能带来的优势。

我们过去四年的经验表明,“无效指令”类型的崩溃错误总是由于特性检测问题造成的——最常见的原因是用户在仿真环境下运行,有时是因为我们的 CPU 特性检测代码存在实际问题。我们几乎没有发现链接器将为不同架构多次编译的函数拉入,并选择其中包含不受支持指令的函数的证据。为了避免这个问题,建议将数值核心保留在源代码中,并避免在可缓存对象中定义非内联函数。

C++ 重构考量#

我们希望从 C 迁移到 C++,这将自然地涉及大量的重构,主要原因有二:

  • 摆脱 NumPy 特有的模板语言,以获得更具表达力的 C++

  • 这将使使用无大小内部函数(如 SVE)变得更容易。

此外,我们还有以下考量:

  • 如果我们使用 Highway,我们需要将 C++ 封装器从通用内部函数切换到 Highway。另一方面,迁移到 C++ 的工作尚未完成。

  • 如果我们使用 Highway,我们需要使用 Highway 内部函数重写现有核心。但话说回来,迁移到 C++ 无论如何都需要改动所有这些核心。

  • 关于 Highway 的一个担忧是,是否有可能获取特定架构函数的函数指针,而不是直接调用该函数。这样我们可以确保在一个 Python API 调用中多次调用一维内循环不会多次产生调度开销。对此进行了调查:Highway 也可以做到这一点。

  • 第二个担忧是,使用 Highway 是否有可能在运行时允许用户选择或禁用对某些指令集的调度。这是可能的。

  • 在 Highway 的 C++ 实现中使用标签减少了代码重复,但增加的模板使得 C 级别的测试和跟踪更加复杂。

_simd 单元测试模块#

_simd testing 模块重写为使用 C++ 的工作最近已在 PR gh-24069 中完成。它依赖于迁移到 C++ 的主要 PR gh-21057。它允许用户从 Python 访问 C++ 内部函数,签名几乎相同。这不仅是测试的好方法,也是设计新 SIMD 核心的好方法。

Highway(使用纯 googletest)可能可以添加类似的测试和原型功能,但目前 NumPy 的方式要好得多。

数学例程#

数学或数值例程的抽象级别高于本 NEP 主要关注的通用内部函数。Highway 只有有限的数学例程,并且它们对于 NumPy 的需求来说不够精确。因此,无论选择哪种方式,NumPy 现有的例程(使用通用内部函数)都将保留,如果选择 Highway 路线,它们只需内部使用 Highway 原语。我们仍然可以使用 Highway 的排序例程。如果我们确实接受低精度例程(通过用户提供的选择,即扩展 errstate 以允许精度选项),我们可以使用 Highway 原生的例程。

可能还有其他库包含可在 NumPy 中重用的数值例程(例如,来自 SLEEF,或者可能来自 JPEG XL 或其他使用 Highway 的库)。这可能带来一点好处,但很可能影响不大。

支持和缺失的内部函数#

NumPy 需要的一些特定内部函数可能在 Highway 中缺失。同样,NumPy 需要实现例程的一些内部函数已在 Highway 中实现,但 NumPy 中缺失。

Highway 拥有比 NumPy 通用内部函数更多的指令,因此 NumPy 核心的某些未来需求可能已经在那里得到满足。

无论哪种方式,我们都始终需要在任何一种解决方案中实现内部函数。

实现#

TODO

替代方案#

使用 Google Highway 进行动态调度。其他替代方案包括:不采取任何行动,继续使用 C 通用内部函数;使用 Xsimd 作为 SIMD 框架(不如 Highway 全面——例如不支持 SVE 或 PowerPC);或者使用/集成 SLEEF(一个不错的库,但维护不一致)。这些替代方案似乎都不吸引人。

讨论#

参考文献和脚注#