拓冰建站拓冰建站
首页 / 资讯中心 / 正文

NumPy SIMD 优化指南:NEP 38 通用 intrinsics、CPU 特性检测与运行时循环分派

NumPy SIMD 优化指南NEP 38 通用 intrinsics、CPU 特性检测与运行时循环分派【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址: https://gitcode.com/gh_mirrors/nu/numpy本篇技术指南基于 NumPy 官方 NEPNumPy Enhancement Proposal38《Using SIMD optimization instructions for performance》展开系统讲解 NumPy 如何通过通用 intrinsics 编译期宏展开 运行期 CPU 特性检测三层机制为 x86、ARM、PowerPC 等不同架构生成并挑选最优的 SIMD 计算循环。读完本文你将掌握--cpu-baseline/--cpu-dispatch构建选项的含义、__cpu_features__与NPY_ENABLE_CPU_FEATURES/NPY_DISABLE_CPU_FEATURES的用法以及新增 SIMD 循环时必须满足的正确性、体积、可维护性与性能标准。NEP 38 要解决什么问题NEP 38作者 Sayed Adel、Matti Picus、Ralf Gommers状态 Final创建于 2019-11-25见 doc/neps/nep-0038-SIMD-optimizations.rst针对两个现实痛点提出编译器优化不够理想现代编译器虽然越来越擅长利用硬件特性但有时并不能生成最优代码二进制分发的最低公共分母困境绝大多数用户并不在本机编译 NumPy而是使用 PyPI wheel 或 conda 等预编译二进制包。这些包必须面向最低公共分母的 CPU 架构提供运行支持因此即使你的 CPU 支持更高级的指令集如 AVX-512预编译的 NumPy 也无法充分利用。传统上CPU 特性通过 intrinsics 暴露——即编译器提供的、直接映射到汇编指令的特定函数。NumPy 此前曾为 AVX-512 编写过若干 ufunc 快速循环并通过 ufunc 机制按 CPU 特性名选择循环见generate_umath.py中构建候选循环与在InitOperators中选择匹配循环的逻辑但这些代码不可泛化无法推广到其他架构。NEP 38 的初衷正是借鉴 OpenCV Hardware Abstraction LayerHAL中 universal intrinsics 的思路为 NumPy 建立一套架构无关的 SIMD 抽象。三阶段机制从抽象 intrinsic 到最优循环NEP 38 提出的机制分为三个阶段基础设施阶段在代码中提供抽象 intrinsics 的基础设施。ufunc 机制将被扩展使单个 ufunc 表达为一组循环loops从最少的 intrinsics 集合一直覆盖到架构上可能的最大集合编译期阶段通过编译器宏与 CPU 检测将抽象 intrinsics 转化为具体的 intrinsic 调用。任何在当前平台不可用的 intrinsic——无论是 CPU 不支持还是该平台没有对应的具体 intrinsic——都不会报错而是对应的循环不会被生成和加入候选集合运行期阶段CPU 检测代码进一步缩小可用循环集合并为 ufunc 挑选最优循环。NEP 38 当前只把运行期特性检测与最优循环选择机制用于 ufunc未来可能通过新的 NEP 扩展其他用途。通用 intrinsics 的映射示例通用 intrinsics 的核心思想是对 x86 的各类 SIMD 变体、ARM 的各类 SIMD 变体等提供一套长相一致的抽象接口。以 NumPy 通用 intrinsicnpyv_load_u32为例它映射到通用 intrinsic平台具体 intrinsicnpyv_load_u32ARM NEONvld1q_u32npyv_load_u32x86 AVX2_mm256_loadu_si256npyv_load_u32x86 AVX-512_mm512_loadu_si512任何编写 SIMD 循环的开发者都使用npyv_load_u32这类宏而不是架构特定的 intrinsic。代码还附带编译期与运行期的 guard 宏以便正确选择循环。仓库中的落地实现simd/目录NEP 38 的构想已在当前仓库中落地为具体代码。通用 intrinsics 的 C 接口定义于 numpy/_core/src/common/simd/simd.h各平台的具体实现按目录划分sse/x86 SSE 系列avx2/x86 AVX2avx512/x86 AVX-512neon/ARM NEON / ASIMDlsx/LoongArch LSXvec/IBM PowerPC VSXVMX/VSX。从源码结构看这正是 NEP 38 所设想的一个抽象接口、多架构具体实现的格局。值得注意的是numpy/_core/src/common/simd/README.md 明确说明simd.h这套 C 接口已不再支持no longer supported新代码应改用基于 Google Highway 的 C 包装层simd.hpp/simd.inc.hpp其提供LoadU、StoreU、Add等与npyv_*对应的操作并通过NPY_HWY、NPY_HWY_F64、NPY_HWY_FMA等宏做编译期能力检查。也就是说通用 intrinsics 的思想被保留并演进到了 Highway 封装上。此外numpy/_core/src/_simd/ 目录提供了一套面向 Python 的_simd测试模块含_simd.c、_simd.dispatch.c.src、_simd_inc.h.src等用于在运行时验证各指令集目标的正确性test-simd构建选项见下文控制其测试目标集合。构建配置--cpu-baseline与--cpu-dispatchNEP 38 为构建系统引入两个核心选项--cpu-baseline定义编译所需的最绝对最小特性集合。例如在x86_64上默认为SSE3。只要编译器支持这些最小特性就会被启用--cpu-dispatch定义可以被检测并作为分派dispatch需求集合的额外 intrinsics 集合。例如在x86_64上默认为[SSSSE3, SSE41, POPCNT, SSE42, AVX, F16C, XOP, FMA4, FMA3, AVX2, AVX512F, AVX512CD, AVX512_KNL, AVX512_KNM, AVX512_SKX, AVX512_CLX, AVX512_CNL, AVX512_ICL]这些特性全部映射到一个 C 层的布尔数组npy__cpu_have并提供 C 层便捷函数npy_cpu_have(int feature_id)查询该数组运行期结果存放在 Python 可见的__cpu_features__字典中。当前 Meson 构建下的对应选项NEP 38 成文时 NumPy 使用runtests.py/setup.pysetuptools构建--cpu-baseline/--cpu-dispatch是命令行参数。当前仓库已迁移到 Meson 构建体系对应选项定义在 meson.options 中cpu-baseline字符串默认minMinimal set of required CPU featurescpu-baseline-detectfeature 类型默认auto是否从编译器标志检测 CPU baselinecpu-dispatch字符串默认maxAdditional CPU features to dispatch to (beyond baseline)disable-optimization布尔默认false禁用全部 CPU 优化分派、SIMD、循环展开disable-svml/disable-highway/disable-intel-sort分别控制 Intel SVML、Google Highway、Intel x86-simd-sort 相关 SIMD 优化test-simd数组默认包含BASELINE, X86_V2, X86_V3, X86_V4, VSX, VSX2, VSX3, VSX4, NEON, ASIMD, VX, VXE, VXE2, LSX即_simd测试模块需要覆盖的指令集目标。从默认值min/max可以看出默认构建会以最保守的 baseline 编译、并在运行期对尽可能多的指令集分派。源码构建时如果打包者清楚目标机器的细节可以通过命令行参数只编译目标需要的循环从而在理论上获得更小的二进制体积。运行期 CPU 特性检测与 ufunc 循环选择检测实现npy_cpu_features.c运行期检测的核心实现在 numpy/_core/src/common/npy_cpu_features.c约 1015 行全局静态数组npy__cpu_have[NPY_CPU_FEATURE_MAX]保存所有 CPU 特性的布尔值在模块初始化时被写入且之后不可变npy_cpu_have(int feature_id)为公共查询函数越界返回 0正常返回npy__cpu_have[feature_id]npy__cpu_init_features()通过cpuid指令逐位检测特性第一次调用功能号 0校验平台支持第二次功能号 1读取 SSE/SSE2/SSE3/SSSE3/SSE41/POPCNT/AVX/FMA3 等位扩展调用0x80000001读取 LAHF/LZCNT/XOP/FMA4 等第三次功能号 7读取 AVX2/BMI/BMI2/GFNI/VAES 等 AVX2 与 AVX-512 相关位。例如AVX的判定条件为 cpuid 位 OS 是否保存 YMM 状态avx_os这正是CPU 支持但操作系统未开启场景的兜底npy__cpu_validate_baseline()校验构建时选定的 baseline 特性在运行期确实可用。循环匹配与选择当导入 ufunc 时已编译循环所需的特性集合会与运行期检测到的特性进行匹配最佳匹配的循环被标记为由该 ufunc 调用。这意味着同一份二进制包内可以同时包含面向 SSE3、AVX2、AVX-512 等的多份循环实现运行期只激活最合适的一个。__cpu_features__诊断字典NEP 38 提出新增一个 Python 可访问的字典__cpu_features__键为可用特性名值为布尔是否可用。当前仓库中该字典在 numpy/_core/src/multiarray/multiarraymodule.c__cpu_features__设置处随_multiarray_umath模块导出与__cpu_baseline__、__cpu_dispatch__一同暴露。测试用例 numpy/_core/tests/test_cpu_features.py 直接导入并使用这三个对象from numpy._core._multiarray_umath import ( __cpu_baseline__, __cpu_dispatch__, __cpu_features__, )test_cpu_features.py中还包含失败时输出完整诊断信息的assert_features_equal会一并打印 NumPy 检测结果、/proc/cpuinfo内容与LD_SHOW_AUXV辅助向量方便定位检测差异同目录的 numpy/_core/tests/test_cpu_dispatcher.py 则验证分派机制本身的正确性。运行期选择指令集环境变量NEP 38 明确要求实现提供一种通过环境变量在运行期选择可用指令集的手段这对基准测试与 CI 代码验证至关重要原文档称名称待定现已落地为两个环境变量见 numpy/_core/src/common/npy_cpu_features.cNPY_ENABLE_CPU_FEATURES显式启用指定的 CPU 特性NPY_DISABLE_CPU_FEATURES显式禁用指定的 CPU 特性。用法要点依据npy__cpu_check_env的实现与注释多个特性之间用空格、逗号或制表符分隔两个变量不能同时设置否则导入时报ImportErrorBoth NPY_DISABLE_CPU_FEATURES and NPY_ENABLE_CPU_FEATURES environment variables cannot be set simultaneously.若解析失败、或指定的特性在运行期并不存在会抛出错误。典型用法是性能调优时对比不同指令集下的基准结果例如只允许 AVX2 路径NPY_ENABLE_CPU_FEATURESAVX2 python -m pytest numpy/_core/tests/... # 或禁用某个高开销特性观察回退路径 NPY_DISABLE_CPU_FEATURESAVX512F python -c import numpy; print(numpy.__cpu_features__)这类运行期开关对 CI 场景尤其关键可以在同一台机器上轮换启用/禁用各特性集合验证每一档循环路径的正确性。注意环境变量作用域与 Python 进程的导入时机相关需在导入 NumPy 前设置。性能评估与新循环的验收标准NEP 38 强调引入更多 intrinsic 代码会增加维护成本因此只有能带来显著性能收益时才应添加。为此实现提供了上述运行期指令集选择能力并给出验收新循环的四个主观标准正确性correctness新代码即使在算法边缘点精度损失也不得超过 1–3 ULP代码膨胀code bloat源码体积、尤其是编译后 wheel 的二进制体积都要受控可维护性maintainability代码可读性性能performance基准测试必须显示出显著的性能提升。NEP 38 同时给出一个务实的预期把现有的avx512f/avx2/fmaSIMD 循环迁移到通用 intrinsics 后可能出现基准回退但二进制体积不应增大若回退不可接受可以保留该平台的 x86 专属代码其他平台使用通用 intrinsic 实现。对最终用户而言所有循环的结果应一致到 1–3 ULP 以内更强的机器应能感受到明显的性能提升。添加新 intrinsic 的工作流当贡献者需要用到尚未被通用 intrinsic 支持的平台专属 SIMD 指令时NEP 38 规定优先将其作为全平台的通用 intrinsic 加入如果其他平台没有等价指令例如 AVX-512 的_mm512_mask_i32gather_ps则不应添加通用 intrinsic而是编写平台专属的 ufunc 或短小的辅助函数。若使用辅助函数必须用特性宏包裹并提供一个合理的、默认使用的非 intrinsic 回退实现。文档预期情形2属于例外贡献者与维护者应权衡该单平台 intrinsic 是否值得相比使用现有最佳通用 intrinsic 方案是否更优。任何新的 ufunc intrinsics PR 都被期望使用通用 intrinsics只有在能证明通用 intrinsics 过于笨拙或性能不足时才接受平台专属代码极端情况下可能接受单平台 PR但需在优先通用 intrinsics的框架内审查。二进制分发与源码构建的影响二进制发布PyPI wheels 与 conda 包由于包含架构下所有可能的循环二进制体积会变大。NEP 38 指出打包者可以限制循环数量以控制体积但希望仍能覆盖较广的架构家族——这一问题在 Intel MKL 中早已存在其二进制包即为各种 CPU 变体附带了一整套备选共享对象DLL源码构建见上文构建选项一节打包者可针对目标机器只编译所需循环理论上有望得到更小的二进制。同类方案与历史演进NEP 38 的相关研究部分梳理了业界已有的 SIMD 抽象方案可帮助理解其设计定位PixmanCairo/X 使用的像素处理库运行期用函数指针填充结构函数形态与 ufunc 循环类似EigenC 模板线性代数库抽象层次比 intrinsics 更高xsimd面向 x86 与 ARM 的 header-only C 库实现了 boost.SIMD 算法中的数学函数Simd面向图像处理与机器学习的跨平台优化库OpenCV此前是每架构一份实现现已迁移到与 NEP 38 非常相似的设计——顶层 dispatch 代码 编译期由 CMake 特化的通用头文件VOLKgnuradio 等使用的 GPL3 库为每个架构提供一套高层优化操作C 标准委员会通过 vector type 类模板与命名空间提案可移植 SIMD 编程。实现层面NEP 38 引用了两个关键 PRgh-13421改进 CPU 特性运行期检测与gh-13516启用多平台 SIMD 编译器优化并演示了基础设施在某个循环上的用法。原文档讨论的备选方案——为每个 CPU 架构手写循环如loops.avx512.c.src、loops.avx2.c.src、loops.sse.c.src、loops.vsx.c.src、loops.neon.c.src等类似 Pixman 的做法——因存在大量重复代码、需要专人长期维护而被否决最终选择了通用 intrinsics 路线。从当前仓库来看这套机制已完全融入现代构建链meson.options中的test-simd默认覆盖X86_V2/V3/V4、VSX系列、NEON/ASIMD、VX/VXE/VXE2、LSX等目标对应 meson_cpu 下的 x86、arm、ppc64、riscv64、s390x、loongarch64 检测文件numpy/__config__.py.in亦将 CPU 特性信息写入构建配置——这些都可以视为 NEP 38 设计在编译期与运行期的完整闭环。小结NEP 38 为 NumPy 确立了通用 intrinsics 编写循环 → 编译期按平台宏展开 → 运行期按 CPU 特性分派的 SIMD 优化路线图解决了预编译二进制无法充分利用用户 CPU 特性的根本矛盾。这一设计最终落地为当前仓库中的simd/各平台实现、npy_cpu_features.c的特性检测、__cpu_features__/NPY_ENABLE_CPU_FEATURES/NPY_DISABLE_CPU_FEATURES等运行期诊断与控制手段以及cpu-baseline/cpu-dispatch构建选项。理解这套机制无论对阅读 NumPy 源码、评估其数值性能还是参与 SIMD 循环的贡献与审查都是必要的知识基础。【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址: https://gitcode.com/gh_mirrors/nu/numpy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门