NumPy SIMD 基础设施演进:从 C Universal Intrinsics 到 Google Highway 的 C++ 迁移(NEP 54 全解析)
科学计算数据分析【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址https://gitcode.com/gh_mirrors/nu/numpy点击查看免费下载导读本文以 NumPy 官方 NEPNumPy Enhancement Proposal54《SIMD infrastructure evolution: adopting Google Highway when moving to C》为主体系统讲解 NumPy 将 SIMD 内建框架Universal Intrinsics从 C 迁移到 C、并评估采用 Google Highway 作为底层实现的完整决策过程。文中将结合当前仓库中已经落地的 Highway 封装源码numpy/_core/src/common/simd/、基于 Highway 重写的数值内核如tanh与排序模块帮助读者理解 NumPy 的 SIMD 架构演进脉络、关键技术权衡以及如何在新代码中直接使用这套 Highway 封装编写 SIMD 内核。关联文档NEP 54doc/neps/nep-0054-simd-cpp-highway.rst状态 Accepted作者为 Sayed Adel、Jan Wassenberg、Matti Picus、Ralf Gommers、Chris Sidebottom创建于 2023-07-06。NEP 54 的核心动机为什么要把 Universal Intrinsics 迁到 CNumPy 的 SIMD 优化基础框架被称为Universal Intrinsics最早在 NEP 38 中定义。它是一套用 C 语言编写、通过 NumPy 自研的sfx模板语言.c.src文件中的占位符为不同数据类型批量生成代码的抽象层。NEP 54 提出的核心动作是把这一框架从 C 迁移到 C同时评估是否用 Google 开源的Highway项目直接取代 NumPy 自研的 Universal Intrinsics。迁移动机主要有两点代码可读性与开发效率C 版本中每个操作都带npyv_前缀和sfx类型后缀冗长且难以阅读支持无尺寸sizelessSIMD 指令集ARM SVE/SVE2 与 RISC-V RVV 的向量宽度在编译期不确定以位宽编码类型的旧接口u8/s8/u16/s16/u32/s32/u64/s64/f32/f64无法直接表达这类指令集。代码风格对比一行代码看懂差异NEP 54 给出了一个典型的 C Universal Intrinsics 代码行来自当前仓库的numpy/_core/src/umath/loops_arithmetic.dispatch.c.src等*.dispatch.c.src文件族// name 是 NumPy 在 .c.src 文件中的专用模板变量 npyv_sfx a5 npyv_load_sfx(src1 npyv_nlanes_sfx * 4);在迁到 C 后NEP 引用了 PRgh-21057的实现同样的逻辑变为auto a5 Load(src1 nlanes * 4);其中sfx是类型标识模板变量展开后为u8, s8, u16, s16, u32, s32, u64, s64, f32, f64之一。这种按位宽硬编码类型的做法在 sizeless 指令集上无法工作——因为 SVE/RVV 的寄存器宽度是运行时或编译目标决定的npyv_nlanes_sfx这类编译期常量失去了意义。C 的模板与类型推导机制则天然可以处理这种宽度未知的向量类型这正是迁到 C 的根本原因之一。NEP 54 的讨论范围一个多方位的权衡分析NEP 54 明确指出它尚未做出最终决定而是以并排对比side-by-side comparison的方式分析两个方向NumPy 自研 C 实现 vs. 采用 Google Highway。讨论范围包括可维护性与社区因素领域专家可得性、新贡献者上手难度等社会性因素关键技术差异可能影响 NumPy 内部设计或性能的约束构建系统相关方面此时 NumPy 已迁移到 Meson 构建系统发布节奏相关方面。同时明确不在本次范围内的事项在给某个函数添加 SIMD 支持时精度 vs 性能的取舍使用 SVML 和 x86-simd-sort以及可能的 aarch64 等价物单独抽取 Highway 的某些算法片段如 PRgh-24018中讨论的或 SLEEF。兼容性承诺对用户零可见变化NEP 54 明确声明不会带来任何显著的用户可见变化Python API 与 C API 均保持不变所有控制编译和运行时 CPU 特性选择的机制都会保留CPU 特性在 Highway 中的命名与 Universal Intrinsics 不同详见下文支持的平台与特性粒度一节在 Windows 上由于 Highway 使用的 pragma 在 MSVC 上支持不佳可能需要避免 MSVC改用 clang-cl 或 Mingw-w64 构建 wheel——NumPy 此前已合入 clang-cl 支持PRgh-20866SciPy 也用 Mingw-w64 构建两种方式理论上都可行但会影响 Windows 上从源码构建的重分发者与终端用户针对早期讨论的反馈Highway 现已采用Apache 2 / BSD-3 双许可扫清了许可证层面的障碍。高层考量逐项对比两大方案开发投入与长期可维护性迁移到 Highway 是一次重大的开发投入但长期看Highway 自身维护者有更多带宽处理编译器兼容问题与新增平台支持可以摊薄 NumPy 团队的维护成本Highway 被 Chromium、JPEG XL 等知名项目使用意味着有更广泛的测试与缺陷报告/修复网络一个现实顾虑是为新增指令集编写支持最好伴随需要该指令的数值内核开发一起进行。若该指令位于作为 NumPy 仓库 git 子模块的 Highway 中就需要先实现临时/通用版本待上游合入后再更新子模块流程会更笨重文档方面 Highway 是明显胜出者NumPy 的 CPU/SIMD Optimizations 文档 相对稀疏而 Highway 文档更完备。迁移策略能否渐进式推进NEP 54 的判断是两半的故事静态分派的 intrinsics 迁移可以渐进进行PRgh-24018已展示了这种做法但Highway 的运行时派发方式必须一步到位——不能同时存在两套运行时派发机制。这意味着要么彻底切换到 Highway 的派发模型要么保留 NumPy 现有的多编译单元派发模型没有折中态。Highway 的编译器与平台支持策略Highway 维护者 Jan Wassenberg 对支持状态的承诺NEP 54 记录只要能通过 Clang 交叉编译、并能用标准 QEMU 测试就可进入 Highway 的 CI若能通过 clang/gcc 交叉编译、并可用带额外参数的新 QEMU 测试则可在每次 Highway 发布前通过手动测试提供支持现有目标只要能在 QEMU 中编译/运行就会持续支持。另外Highway 不受 Google不再支持策略的约束——其 README 明示This is not an officially supported Google product。NEP 54 认为这反而是好事项目不太可能因为 Google 的商业决策而被废弃Google org 下不少知名开源项目如 JAX、tcmalloc 都有类似声明。支持的平台与特性粒度差异两大框架都支持大量平台与 SIMD 指令集并提供标量/回退版本。当前主要差异维度NumPy Universal IntrinsicsGoogle HighwayIBM Zs390x支持 VX/VXE/VXE2支持 Z14、Z15ARM SVE/SVE2、RISC-V RVVsizeless不支持gh-21057已做基础工作尚未实现支持特性粒度更细大致按 SIMD 指令集划分更粗大致按 CPU 家族划分NEP 54 的观点是采用 Highway 会丢失一些粒度但这大概率无伤大雅——NumPy 并不真正需要这么细的粒度也缺乏用户显式利用这种粒度榨取最后一分性能的证据。多目标编译与运行时派发策略关键分歧点这是 NEP 54 中最实质的技术分歧Highway 方式只编译一次在同一编译单元内通过预处理技巧foreach_target.h与 dynamic dispatch 文档所描述的做法为每个 CPU 特性生成多个代码段stanzaUniversal Intrinsics 方式为每个 CPU 特性组生成多个编译单元、编译多次再以不同符号名链接到一起供运行时派发。两者的稳健性争论Jan 认为 Highway 方式更稳健尤其能避免链接器把包含过新指令的函数拉进最终二进制Sayed 认为 NumPy 现有方式OpenCV 也在用更稳健更不容易踩到编译器特定 bug、或能更早暴露问题Matti 与 Ralf 认为当前构建策略对 NumPy 运行良好更换构建与运行时派发可能引入未知不稳定收益不抵风险双方都认同Meson 构建系统允许指定目标文件链接顺序能产出更一致的构建——但这也会把 NumPy 更深地绑定到 Meson。NEP 54 还分享了 NumPy 过去四年的实践经验invalid instruction 类崩溃几乎都源于特性检测问题——最常见是用户运行在模拟/仿真环境下其次是 CPU 特性检测代码本身有缺陷几乎没有证据表明链接器错误地选中了带不支持指令的多版本编译函数。建议做法是将数值内核保留在源码内并避免在可缓存对象中定义非内联函数。从当前仓库源码看NumPy 实际保留了其NPY_CPU_DISPATCH多编译单元派发机制并在其上叠加使用 Highway 的静态 intrinsics——见 highway_qsort.hpp 中NPY_CPU_DISPATCH_DECLARE与 Highway 的qsort_simd命名空间的组合方式。这与 NEP 54 中静态 intrinsics 可渐进迁移、运行时派发保持单一的判断一致。C 重构相关考量迁到 C 本身是巨大重构动机是摆脱 NumPy 专用模板语言.c.src中的sfx以及让 sizeless intrinsics 更易用无论是否采用 HighwayC 迁移都要求触碰全部现有内核因此用 Highway 重写内核的额外成本被 C 迁移本身覆盖两个曾被质疑的技术点都得到正面结论可以获取架构特定函数的函数指针而非直接调用从而保证单次 Python API 调用内多次执行 1-D 内层循环时不会重复付出派发开销可以在运行时允许用户选择或禁用特定指令集的派发Highway C 实现中的 tag 用法减少了代码重复但额外的模板化使 C 层面的测试与追踪更复杂。落地实践仓库中的 Highway 封装np::simdNEP 54 描述的是 2023 年的决策过程从当前仓库源码看这一方向已经实际落地。numpy/_core/src/common/simd/目录包含一个为 NumPy 量身定制的轻量 C 封装其 README 明确说明该目录下的 C 接口 universal intrinsicssimd.h已不再受支持所有新 SIMD 代码都应使用 Highway 封装。封装的设计哲学用 lane 类型取代 class tagHighway 原生接口以tag 类作为模板参数例如VecScalableTagfloat且大量函数要求显式传入 tag 实例作为第一个参数相当啰嗦。NumPy 封装的核心设计是去掉 class tag、直接使用 lane 类型如float并尽量从参数中推导。头文件结构simd.hppnp::simd命名空间使用hn::ScalableTagTLane代表平台支持的最大 SIMD 宽度可扩展np::simd128命名空间使用hn::Full128TLane代表固定的 128 位 SIMD 宽度hn是hwy::HWY_NAMESPACE的别名实现细节在 simd.inc.hpp 中被simd.hpp以不同命名空间多次包含。基础用法对比来自 README// 原生 Highway 的写法需要 tag 实例 namespace hn hwy::HWY_NAMESPACE; ScalableTagfloat df; Vecdecltype(df) v LoadU(df, data); // LoadU 需要 tag 实例 StoreU(v, df, data); // NumPy 封装的写法直接使用 lane 类型 using namespace np::simd; Vecfloat v LoadU(data); // 类型由参数推导 StoreU(v, data);编译期能力检测宏封装定义了四个关键宏simd.hpp宏含义NPY_HWYHighway SIMD 可用HWY_TARGET既非HWY_SCALAR也非HWY_EMU128时为真NPY_HWY_F16SIMD 可用且支持 float16HWY_HAVE_FLOAT16NPY_HWY_F64SIMD 可用且支持 float64HWY_HAVE_FLOAT64NPY_HWY_FMASIMD 可用且原生支持 FMAHWY_NATIVE_FMA这些宏在NPY_DISABLE_OPTIMIZATION或NPY_DISABLE_HIGHWAY定义时统一置 0代码自动回退到标量实现。设计上刻意避免使用 Highway 的标量操作EMU128原因有三NumPy 已有优化好的标量内核Highway 标量模式部分 intrinsics 支持不完整NumPy 严格的 IEEE 754 浮点合规要求使直接标量实现行为更可预测。封装还提供kSupportLaneTLaneSFINAE 编译期类型检查与kMaxLanesTLane等约束用于在模板上下文中按 lane 类型能力开关代码路径。常用操作一览封装在 simd.inc.hpp 中通过using hn::...直接导入大量 Highway intrinsics向量创建Zero、Set、Undefined、内存操作LoadU、StoreU、类型转换BitCast、VecFromMask、比较Eq/Ne/Lt/Le/Gt/Ge、算术Add/Sub/Mul/Div/Min/Max/Abs/Sqrt、逻辑And/Or/Xor/AndNot等。需要 tag 的操作则以模板函数包装内部统一传入_TagTLane()。如需扩展只需在simd.inc.hpp中增加using hn::FunctionName;或对应包装函数。实际采用 Highway 的数值内核与模块数学内核tanh等超越函数的 Highway 重写loops_hyperbolic.dispatch.cpp.src 是 C 迁移后直接使用 Highway 编写数学内核的实例#include hwy/highway.h namespace hn hwy::HWY_NAMESPACE; const hn::ScalableTagfloat f32; const hn::ScalableTagint32_t s32; using vec_f32 hn::Vecdecltype(f32);该文件中的tanh(f32, f64)实现是从 Intel SVML 汇编转换而来原代码位于 numpy/SVML 仓库的 avx512svml_z0_tanh_*汇编算法上利用tanh是奇函数的性质处理绝对值、按子区间查表做多项式逼近、对 IEEE 特殊值±0、±Inf、QNaN、SNaN逐一处理。NEP 54 中的Math routines一节指出数学例程抽象层级高于 universal intrinsicsHighway 自带数学函数数量有限且精度不足以满足 NumPy 需求因此NumPy 现有数学例程会保留只是内部改用 Highway 原语实现排序例程VQSort则可以直接复用。排序模块Highway VQSort 的接入numpy/_core/src/npysort/ 下的highway_qsort.hpp、highway_qsort.dispatch.cpp、highway_qsort_16bit.dispatch.cpp展示了将 Highway 排序VQSort与 NumPy 既有NPY_CPU_DISPATCH派发机制结合的模式——这与 NEP 54 主张的静态 intrinsics 渐进迁移完全吻合。此外loops_trigonometric.dispatch.cpp、loops_logical.dispatch.cpp、loops_arithmetic_timedelta.dispatch.cpp等文件也已迁到 Highway 风格的 C 实现。_simd单元测试与原型模块NEP 54 专门介绍了_simd测试模块重写完成于 PRgh-24069依赖 C 迁移主 PRgh-21057它允许从 Python 以几乎相同的签名访问 C intrinsics不仅是测试利器更是设计新 SIMD 内核的原型工具。Highway 本身使用 googletest也可以添加类似的测试/原型特性但 NEP 54 认为当时 NumPy 的做法明显更优雅。从当前仓库看_simd模块的构建逻辑在 numpy/_core/src/_simd/_simd.dispatch.c.src它通过/**begin repeat*/模板为u8/s8/u16/s16/u32/s32/u64/s64/f32/f64各类型生成load/store/load_till/store_till/...等指令的 Python 可调用封装并按 CPU 特性分组NPY_MTARGETS_CONF_DISPATCH为每个目标生成独立子模块见 _simd.c上层测试与原型代码位于 numpy/_core/tests/test_simd.py 与 test_simd_module.py。数学例程与精度策略NEP 54 指出数学/数值例程的抽象层级高于 universal intrinsics不是本 NEP 的焦点Highway 的数学例程有限且精度不足以满足 NumPy 需求因此 NumPy 自研例程必须保留若走 Highway 路线则内部改用 Highway 原语排序例程可直接使用 Highway 的 VQSort若未来接受低精度例程例如扩展errstate引入精度选项由用户选择则可使用 Highway 原生例程其他库SLEEF、JPEG XL 等的数值例程也可能有可复用价值但收益有限。支持与缺失的 intrinsicsNEP 54 的结论是NumPy 需要的某些 intrinsics 在 Highway 中缺失反之 Highway 已有的某些 intrinsics 在 NumPy 中也缺失Highway 的指令集比 Universal Intrinsics 更丰富未来 NumPy 内核的新需求可能已经在那里被满足。无论选择哪条路线都必然要自行实现部分 intrinsics——这是绕不开的工作量。相关研究与替代方案NEP 54 列举的相关工作Google HighwayXsimdxtensor-stack 的 SIMD 框架OpenCV 的 SIMD 框架core/hal/intrin系列 APIC 标准提案std::experimental::simd更早的相关工作可参考 NEP 382019 年的 Related Work 一节。替代方案方面NEP 54 的评估结论是其他选项都不太有吸引力——留在 C Universal Intrinsics 原地不动丧失 C 迁移与 sizeless 支持的机会采用 Xsimd功能不如 Highway 全面例如不支持 SVE 与 PowerPC使用/内置 SLEEF是好库但维护不太稳定。结论与当前仓库状态NEP 54 以并排对比的方式完整呈现了NumPy 自研 C SIMD与采用 Google Highway两条路线的权衡并在可维护性、渐进迁移可行性、运行时派发稳健性、特性粒度、文档、社区生态等多个维度给出了详实分析。从当前仓库的实际状态看该方向已经落地为C 迁移完成numpy/_core/src/umath/下大量*.dispatch.cpp.src内核已迁到 CHighway 封装就绪numpy/_core/src/common/simd/simd.hppsimd.inc.hpp提供了去掉 class tag 的轻量 C 接口np::simd最大宽度与np::simd128固定 128 位双命名空间设计配套编译期能力检测宏NPY_HWY / NPY_HWY_F16 / NPY_HWY_F64 / NPY_HWY_FMA既有派发机制保留运行时 CPU 特性派发仍使用 NumPy 的NPY_CPU_DISPATCH多编译单元机制Highway 静态 intrinsics 在其中渐进使用面向新代码的指引仓库明确要求所有新 SIMD 代码使用 Highway 封装旧 C 接口 universal intrinsics 仅作兼容保留。对于希望在 NumPy 中编写或理解新 SIMD 内核的开发者推荐路径是阅读 simd.hpp 与 simd.inc.hpp 了解可用操作参考 loops_hyperbolic.dispatch.cpp.src 这类已迁移内核的写法并用_simd模块从 Python 侧原型化验证后再落地为 C 内核。赞分享科学计算数据分析【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址https://gitcode.com/gh_mirrors/nu/numpy点击查看免费下载相关推荐NumPy C API 演进指南NEP 53 如何设计 NPY_TARGET_VERSION 与 NumPy 2.0 的 ABI 断裂NumPy C API 演进指南NEP 53 如何设计 NPY_TARGET_VERSION 与 NumPy 2.0 的 ABI 断裂 本文围绕 NEP 53科学计算数据分析NumPy 2.0.0 发布全解析从 NEP 50 标量提升到 StringDType 与 C-API 大改版的实战迁移指南NumPy 2.0.0 发布全解析从 NEP 50 标量提升到 StringDType 与 C API 大改版的实战迁移指南 NumPy 2.0.0 是该开源科学计算数据分析NumPy 广义 ufuncGeneralized Universal Functions完全指南从 NEP 5 签名语法到 C 级实现原理NumPy 广义 ufuncGeneralized Universal Functions完全指南从 NEP 5 签名语法到 C 级实现原理 本篇指南以科学计算数据分析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考