NEP 38 — 使用 SIMD 优化指令来提高性能#

作者

赛义德·阿德尔、马蒂·皮库斯、拉尔夫·戈默斯

地位

最终的

类型

标准

创建

2019-11-25

解决

https://mail.python.org/archives/list/numpy-discussion@python.org/thread/PVWJ74UVBRZ5ZWF6MDU7EUSJXVNILAQB/#PVWJ74UVBRZ5ZWF6MDU7EUSJXVNILAQB

抽象的

虽然编译器越来越擅长使用特定于硬件的例程来优化代码,但有时它们不会产生最佳结果。此外,我们希望能够将二进制优化的 C 扩展模块从一台机器复制到另一台具有相同基础架构(x86、ARM 或 PowerPC)但具有不同功能的机器,而无需重新编译。

我们在 ufunc 机制中有一个机制来构建 按 CPU 功能名称索引的替代循环。在导入时(在 中),从候选中选择InitOperators与运行时 CPU 信息相匹配的循环函数。此NEP 提出了一种机制,可以在此基础上构建更多功能和架构。提议的步骤是:

  • 建立一组定义明确、与体系结构无关的通用内在函数,捕获跨体系结构可用的功能。

  • 在一组 C 宏中捕获这些通用内在函数,并使用这些宏为从基线到该架构上可用的最大功能集的功能集构建代码路径。将它们作为有限数量的已编译替代代码路径提供。

  • 在运行时,发现哪些 CPU 功能可用,并从可能的代码路径中进行相应的选择。

动机和范围#

传统上,NumPy 依赖编译器专门针对目标架构生成最佳代码。然而,如今很少有用户在他们的机器上本地编译 NumPy。大多数使用二进制包,它们必须为最低公分母 CPU 架构提供运行时支持。因此,NumPy 无法利用其 CPU 处理器的更高级功能,因为它们可能不适用于所有用户的系统。

传统上,CPU 功能是通过内在函数公开的,这些内在函数是直接映射到汇编指令的编译器特定指令。最近有关于添加更多内在函数的有效性的讨论(例如,用于浮点数 AVX 优化的gh-11113 )。过go,特定于体系结构的代码被添加到 NumPy 中,以实现各种 ufunc 中的快速 avx512 例程,使用上述机制为体系结构选择最佳循环。然而,该代码不是通用的,并且不能推广到其他架构。

最近,OpenCV 开始在硬件抽象层 (HAL) 中使用通用内在函数,这为常见共享单指令多数据 (SIMD) 结构提供了良好的抽象。该 NEP 为 NumPy 提出了类似的机制。使用该机制分为三个阶段:

  • 代码中为抽象内在函数提供了基础结构。 ufunc 机制将使用这些抽象内在函数集进行扩展,以便单个 ufunc 将表示为一组循环,从可能可用的内在函数的最小集到最大集。

  • 在编译时,编译器宏和 CPU 检测用于将抽象内在函数转换为具体的内在函数调用。平台上不可用的任何内在函数,要么因为 CPU 不支持它们(因此无法测试),要么因为抽象内在函数在平台上没有并行的具体内在函数,都不会出错,而是不会生成相应的循环并添加到可能性集中。

  • 在运行时,CPU 检测代码将进一步限制可用循环集,并为 ufunc 选择最佳循环。

当前的 NEP 只建议对 ufunc 使用运行时特征检测和最优循环选择机制。未来的 NEPS 可能会为所提议的解决方案提出其他用途。

ufunc 机器已经能够在运行时为特定可用的 CPU 功能选择最佳循环,当前使用 foravx2fma循环avx512f(在生成的__umath_generated.c文件中);通用内在函数将扩展生成的代码以包含更多循环变体。

使用和影响#

最终用户将能够获得可用于其平台和编译器的内部函数列表。可选地,用户可能能够指定将使用运行时可用的循环中的哪一个,也许通过环境变量来启用对不同循环的影响进行基准测试。对天真的最终用户不应该有直接影响,所有循环的结果应该与少量(1-3?)ULP 内相同。另一方面,拥有更强大机器的用户应该会注意到性能的显着提升。

二进制版本 - PyPI 和 conda 包上的轮子#

此过程发布的二进制文件会更大,因为它们包含该架构的所有可能的循环。一些打包者可能更愿意限制循环数量以限制二进制文件的大小,我们希望他们仍然支持广泛的架构系列。请注意,此问题已存在于 Intel MKL 产品中,其中二进制包包含一组广泛的用于各种 CPU 替代方案的替代共享对象 (DLL)。

源代码构建#

请参阅下面的“详细说明”。打包程序了解目标机器详细信息的源构建理论上可以通过选择通过命令行参数仅编译目标所需的循环来生成较小的二进制文件。

如何运行基准测试来评估性能优势#

添加更多使用内部函数的代码将使代码更难以维护。因此,只有在产生显着的性能优势时才应添加此类代码。评估这种性能优势并非易事。为了帮助实现这一点,此 NEP 的实现将添加一种方法来选择可以通过环境变量在运行时使用哪些指令集。 (名称待定)。这种能力对于 CI 代码验证至关重要。

诊断#

__cpu_features__python 将可以使用新的字典。键是可用的功能,值是布尔值,无论该功能是否可用。各种新的私有 C 函数将在内部使用来查询可用功能。这些可能通过特定的 c 扩展模块公开以进行测试。

添加新的 CPU 架构特定优化的工作流程#

对于任何可能成为 SIMD 矢量化候选的代码,NumPy 将始终具有基线 C 实现。如果贡献者想要为某些架构添加 SIMD 支持(通常是他们最感兴趣的架构),此评论是有关如何执行此操作的教程的开始: numpy/numpy#13516

截至目前,NumPy对于许多 ufunc有许多avx512fandavx2和SIMD 循环。fma这些可能是第一个被移植到通用内在函数的候选者。预计新的实现可能会导致基准测试回归,但不会增加二进制文件的大小。如果回归不是最小的,我们可以选择保留该平台的 X86 特定代码,并使用其他平台的通用内部代码。

任何使用内在函数实现 ufunc 的新 PR 都应该使用通用内在函数。如果可以证明通用内在函数的使用太尴尬或性能不够,则也可以接受特定于平台的代码。在极少数情况下,仅单一平台的 PR 可能会被接受,但必须在首选使用通用内在函数的解决方案的框架内进行检查。

接受新循环的主观标准是:

  • 正确性:即使在算法的边缘点,新代码的准确度降低也不得超过 1-3 个 ULP。

  • 代码膨胀:源代码大小,尤其是编译轮的二进制大小。

  • 可维护性:代码的可读性如何

  • 性能:基准测试必须显示出显着的性能提升

添加新的内在#

如果贡献者想要使用尚未作为通用内在函数支持的特定于平台的 SIMD 指令,则:

  1. 应该将其添加为所有平台的通用内在函数

  2. 如果它在其他平台上没有等效的指令(例如 _mm512_mask_i32gather_psAVX512),则不应添加通用内在函数,而ufunc应编写特定于平台的或简短的辅助函数。如果使用这样的辅助函数,则必须用功能宏包装它,并且默认使用合理的非内在后备。

我们预计 (2) 是例外。贡献者和维护者应该考虑与使用最佳可用的基于通用内在函数的实现相比,单一平台内在函数是否值得。

被其他项目重用#

如果通用内在函数可用于其他也构建 ufunc 的库(例如 SciPy 或 Astropy),那就太好了,但这并不是此 NEP 首次实现的明确目标。

向后兼容性#

对向后兼容性应该没有影响。

详细说明

CPU 特定的映射到通用内在函数,这与所有 x86 SIMD 变体、ARM SIMD 变体等类似。例如,NumPy 通用内在函数npyv_load_u32映射到:

  • vld1q_u32适用于基于 ARM 的 NEON

  • _mm256_loadu_si256适用于基于 x86 的 AVX2

  • _mm512_loadu_si512适用于基于 x86 的 AVX-512

任何编写 SIMD 循环的人都会使用npyv_load_u32宏而不是特定于体系结构的内在函数。该代码还为编译和运行时提供保护宏,以便可以选择正确的循环。

runtests.pysetup.py: --cpu-baseline和可以使用两个新的构建选项--cpu-dispatch。编译所需的绝对最低功能由 --cpu-baseline.例如,x86_64默认为SSE3.如果编译器支持,则将启用最少的功能。可以检测到并用作分派的要求集的附加内在函数集由 设定--cpu-dispatch。例如, x86_64默认为.这些功能都映射到一个c级布尔数组,一个c级便利函数 查询这个数组,并在运行时将结果存储在其中。[SSSE3, SSE41, POPCNT, SSE42, AVX, F16C, XOP, FMA4, FMA3, AVX2, AVX512F, AVX512CD, AVX512_KNL, AVX512_KNM, AVX512_SKX, AVX512_CLX, AVX512_CNL, AVX512_ICL]npy__cpu_havenpy_cpu_have(int feature_id)__cpu_features__

导入 ufunc 时,可用编译循环的所需功能与发现的功能相匹配。具有最佳匹配的循环被标记为由 ufunc 调用。

执行

当前 PR:

编译时和运行时代码基础结构由第一个 PR 提供。第二个添加了循环基础设施的使用演示。一旦 NEP 获得批准,就需要做更多的工作来使用 NEP 提供的机制编写循环。

备择方案

gh-13516中提出的替代方案是手动为每个 CPU 架构单独实现循环,而不尝试抽象 SIMD 内在函数中的常见模式(例如,有loops.avx512.c.srcloops.avx2.c.srcloops) .sse.c.srcloops.vsx.c.srcloops.neon.c.src等)。这与 PIXMAX 的做法更相似。不过,这里存在大量重复,并且手动代码重复需要一位致力于实现和维护该平台循环代码的拥护者。

讨论

大多数讨论是在 PR gh-15228上进行的,以接受此 NEP。邮件列表的讨论提到了VOLK,它被添加到相关工作的部分。可维护性问题也在邮件列表和gh-15228中提出并解决如下:

  • 如果贡献者想要利用特定的 SIMD 指令,他们是否也需要为所有其他架构添加该指令的软件实现? (请参阅工作流程的new-intrinsics部分)。

  • 谁有责任验证所有架构的代码和基准?如果添加通用 ufunc 代替特定于体系结构的代码有助于一种体系结构,但会损害另一种体系结构的性能,会发生什么情况? (在工作流程的权衡部分中回答)。

参考文献和脚注#