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

作者:

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

状态:

草稿

类型:

标准跟踪

创建:

2023-07-06

决议:

待办事项

摘要#

我们正在将 SIMD 本征框架 Universal Intrinsics 从 C 迁移到 C++。我们也迁移到了 Meson 作为构建系统。Google Highway 本征项目建议我们使用 Highway 来代替我们目前在 NEP 38 中描述的 Universal Intrinsics。这是一个复杂且多方面的决策 - 此 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 中讨论的那样)

用法和影响#

N/A - 不会有显着的用户可见更改。

向后兼容性#

用户界面 Python 或 C API 不会有任何更改:控制编译和运行时 CPU 特性选择的所有方法都应该保留,尽管由于迁移到 C++ 而可能有一些更改,与 Highway/Universal Intrinsics 选择无关。

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

在 Windows 上,由于 Highway 使用了 MSVC 支持不佳的编译指示,因此可能需要避免使用 MSVC。这意味着我们可能必须使用 clang-cl 或 Mingw-w64 来构建我们的轮子。这两个都应该可以工作 - 我们在一段时间前合并了 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 子模块),这将稍微笨拙一些 - 需要首先实现一个临时/通用版本,然后在将新的本征上游之后更新子模块。

从文档方面来看,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 系统(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 级别的测试和跟踪变得更加复杂。

The _simd 单元测试模块#

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

可能可以为 Highway 添加类似的测试和原型功能(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 内核的某些未来需求可能已经在那里得到满足。

无论哪种方式,我们都必须在这两种解决方案中实现内联函数。

实现#

待办事项

替代方案#

使用 Google Highway 进行动态调度。其他替代方案包括:不做任何操作并保留 C 通用内联函数,使用 Xsimd 作为 SIMD 框架(不如 Highway 完整,例如不支持 SVE 或 PowerPC),或使用/厂商 SLEEF(一个不错的库,但维护不一致)。这些替代方案都不太吸引人。

讨论#

参考文献和脚注#