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 来代替我们的 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 中讨论)。
用法和影响#
N/A — 用户可见的更改不会很大。
向后兼容性#
用户界面 Python 或 C API 不会有任何更改:控制编译和运行时 CPU 功能选择的所有方法都应该保留,尽管由于迁移到 C++(与 Highway/Universal Intrinsics 的选择无关)可能会有一些更改。
Highway 中 CPU 功能的命名与 Universal Intrinsics 不同(参见下面的“支持的功能/目标”)。
在 Windows 上,由于 Highway 使用 MSVC 支持较差的 pragma,因此可能必须避免使用 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 将是一个明显的优势。与 Highway 文档 相比,NumPy 的 CPU/SIMD 优化 文档相当稀疏。
迁移策略 - 它可以逐步进行吗?#
这是一个由两部分组成的故事。正如PR gh-24018中已经看到的那样,逐步迁移到Highway的静态分派内在函数是可以实现的。但是,采用Highway执行运行时分派的方式必须一次完成——我们不能(也不应该)同时使用两种方法。
Highway的编译器和平台支持策略#
在添加新的指令时,Highway有一项策略,即必须以在CPU架构之间公平平衡的方式实现这些指令。
关于支持状态以及是否所有当前支持的架构都将继续得到支持,Jan声明Highway可以承诺以下几点:
如果它可以通过Clang交叉编译,并且可以通过标准QEMU进行测试,那么它就可以进入Highway的CI。
如果它可以通过clang/gcc交叉编译,并且可以使用新的QEMU(可能需要额外的标志)进行测试,那么它可以在每次Highway发布之前通过手动测试来支持。
只要现有目标可以在QEMU中编译/运行,它们就会继续得到支持。
Highway不受Google的“不再支持”策略的约束(或者,正如其README中所写的那样,这不是 Google 官方支持的产品)。这并不是一件坏事;这意味着它不太可能由于Google关于该项目的业务决策而得不到支持。在google
GitHub组织下,相当多的知名开源项目也声明了这一点,例如JAX和tcmalloc。
支持的功能/目标#
这两个框架都支持大量的平台和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
的使用和动态分派文档,了解它是如何实现的)。通用内在函数为每个CPU特性组生成多个编译单元,并多次编译,将它们全部链接在一起(使用不同的名称)以进行运行时分派。Highway技术可能无法在MSVC上可靠地工作,而通用内在函数技术可以在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
单元测试模块#
最近在PR gh-24069中完成了将_simd testing
模块重写为使用C++的工作。它依赖于迁移到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内核的一些未来需求可能已经在那里得到满足。
无论哪种方式,我们都必须在两种解决方案中实现内在函数。
实现#
待办事项
替代方案#
使用Google Highway进行动态分派。其他替代方案包括:不做任何更改并保留C通用内在函数,使用Xsimd作为SIMD框架(不如Highway全面——例如,没有SVE或PowerPC支持),或使用/销售SLEEF(一个不错的库,但维护不一致)。这些替代方案似乎都不吸引人。
讨论#
参考文献和脚注#
版权#
本文件已置于公共领域。[1]