NEP 54 — SIMD 基础设施演变:转向 C++ 时采用 Google Highway?#

作者

赛义德·阿德尔、扬·瓦森伯格、马蒂·皮库斯、拉尔夫·戈默斯、克里斯·塞德博顿

地位

草稿

类型

标准轨道

创建

2023-07-06

解决

go做

抽象的

我们正在将 SIMD 内在框架 Universal Intrinsics 从 C 迁移到 C++。我们还转向使用 Meson 作为构建系统。 Google Highway 内在函数项目建议我们使用 Highway 而不是NEP 38中描述的通用内在函数。这是一个复杂且多方面的决定 - 该新经济政策试图描述所涉及的权衡以及需要做什么。

动机和范围#

我们希望将基于 C 的通用内联(参见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是类型标识符的模板变量,例如:。像这样显式使用位大小编码类型对于无大小的 SIMD 指令集不起作用。对于 C++,这更容易处理; PR gh-21057展示了 C++ 代码的方式并包含更完整的示例。#sfx = u8, s8, u16, s16, u32, s32, u64, s64, f32, f64#

该 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 上,可能必须避免使用 MSVC,因为 Highway 使用的编译指示不太受 MSVC 支持。这意味着我们可能必须使用 clang-cl 或 Mingw-w64 来构建我们的轮子。这两个都应该有效 - 我们不久前合并了 clang-cl 支持(请参阅gh-20866),并且 SciPy 与 Mingw-w64 一起构建。但是,它可能会影响在 Windows 上从源代码构建的其他再发行商或最终用户。

为了回应之前围绕此 NEP 的讨论,Highway 现在获得了 Apache 2 / BSD-3 的双重许可。

高层考虑#

笔记

目前,本节尝试分别介绍每个主题,并将未来使用特定于 NumPy 的 C++ 实现与使用 Google Highway 以及我们自己的数值例程进行比较。它(尚未)假设已做出决定或提议的决定。因此,这个新经济政策并不是“建议”与替代方案部分中的另一个选项,而是并排比较。

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

转向高速公路可能是一项重大的开发工作。从长远来看,这有望被 Highway 本身拥有更多维护者带宽来抵消,以处理编译器支持中持续出现的问题并添加新平台。

其他项目(例如 Chromium 和JPEG XL)正在使用的 Highway (请参阅 Highway 文档中的更完整列表 )确实意味着更广泛的测试和错误报告/修复可能会带来好处。

一个问题是可能需要添加新指令,而这通常最好作为开发需要指令的数字内核过程的一部分来完成。如果指令位于 NumPy 存储库内的 git 子模块 Highway 中,这会有点笨拙 - 需要首先实现临时/通用版本,然后在上游新的内在函数后更新子模块。

从文档角度来看,Highway 将是一个明显的胜利。与Highway 文档相比, NumPy 的 CPU/SIMD 优化文档相当稀疏 。

迁移策略——可以循序渐进吗?#

这是一个分为两半的故事。正如 PR gh-24018中所见,可以逐步转向 Highway 的静态调度内在函数。然而,采用 Highway 执行运行时调度的方式必须一次性完成 - 我们不能(或不应该)有两种方法来做到这一点。

编译器和平台支持的高速公路政策#

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

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

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

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

  3. 只要现有目标在 QEMU 中编译/运行,它们就会保持受支持。

Highway 不受 Google“不再支持”策略的约束(或者,如其自述文件中所述,这不是 Google 官方支持的产品)。这并不是一件坏事。这意味着它不太可能因为谷歌对该项目的商业决策而得不到支持。 GitHub 组织下的不少知名开源项目都google表明了这一点,例如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,我们会失go一些粒度 - 但这可能很好,我们并不真正需要这种级别的粒度,并且没有太多证据表明用户明确地使用它来挤出他们的最后一点性能自己的CPU。

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

Highway 编译一次,同时使用预处理技巧为同一编译单元中的每个 CPU 功能生成多个节( foreach_target.h有关如何完成的信息,请参阅使用和动态调度文档)。 Universal Intrinsics 生成多个编译单元,每个编译单元对应一个 CPU 功能组,并进行多次编译,将它们全部链接在一起(使用不同的名称)以进行运行时调度。 Highway 技术可能无法在 MSVC 上可靠地工作,而 Universal Intrinsic 技术却可以在 MSVC 上可靠地工作。

哪一个更坚固?专家们不同意。 Jan 认为 Highway 方法更加稳健,特别是可以避免链接器将具有太新指令的函数拉入最终的二进制文件中。 Sayed 认为当前的 NumPy 方法(OpenCV 也使用)更加稳健,特别是不太可能遇到特定于编译器的错误或较早捕获它们。两者都同意介子构建系统允许指定对象链接顺序,这会产生更一致的构建。然而,这确实将 NumPy 与介子联系起来。

Matti 和 Ralf 认为当前的构建策略对于 NumPy 来说效果很好,并且改变构建和运行时调度的优势(可能存在未知的不稳定性)超过了采用 Highway 动态调度可能带来的优势。

我们过go四年的经验表明,“无效指令”类型崩溃的错误总是由于功能检测问题造成的 - 最常见的是因为用户在仿真下运行,有时是因为我们的 CPU 功能检测代码存在实际问题。几乎没有证据表明我们知道链接器拉入了针对不同架构多次编译的函数,并选择了具有不受支持的指令的函数。为了确保避免此问题,建议将数字内核保留在源代码中,并避免在可缓存对象中定义非内联函数。

C++ 重构注意事项#

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

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

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

此外,我们还看到以下考虑因素:

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

  • 如果我们使用 Highway,我们需要使用 Highway 内在函数重写现有内核。但同样,迁移到 C++ 无论如何都需要接触所有这些内核。

  • 关于 Highway 的一个问题是是否可以获取特定于体系结构的函数的函数指针,而不是直接调用该函数。这样我们就可以确保为单个 Python API 调用多次调用一维内部循环不会多次产生调度开销。对此进行了调查:这也可以通过 Highway 来完成。

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

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

单元_simd测试模块#

最近在 PR gh-24069中完成了使用 C++重写模块。这取决于转向 C++ 的主要 PR gh-21057。它允许人们使用几乎相同的签名来访问 C++ 内在函数,但来自 Python。这不仅是测试的好方法,也是设计新 SIMD 内核的好方法。_simd testing

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

数学例程#

数学或数值例程是在比通用内在函数更高的抽象级别上编写的,而通用内在函数是该 NEP 的主要焦点。 Highway 的数学例程数量有限,而且不够精确,无法满足 NumPy 的需求。因此,无论哪种方式,NumPy 的现有例程(使用通用内在函数)都将保留,如果我们走 Highway 路线,他们只需在内部使用 Highway 原语。我们仍然可以使用高速公路排序例程。如果我们确实接受较低精度的例程(通过用户提供的选择,即扩展errstate以允许精度选项),我们可以使用 Highway 本地例程。

可能还有其他库具有可以在 NumPy 中重用的数值例程(例如,来自 SLEEF,或者可能来自 JPEG XL 或一些其他使用 Highway 的库)。这里可能有一点好处,但可能并不重要。

支持和缺少的内在函数#

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

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

无论哪种方式,我们总是必须在任一解决方案中实现内在函数。

执行

go做

备择方案

使用 Google Highway 进行动态调度。其他替代方案包括:什么都不做并保留 C 通用内在函数,使用Xsimd作为 SIMD 框架(不如 Highway 全面 - 例如不支持 SVE 或 PowerPC),或使用/供应商SLEEF(一个很好的库,但维护不一致)。这两种选择似乎都没有吸引力。

讨论

参考文献和脚注#