Skip to content

Commit ab3559f

Browse files
committed
ENH: Add SIMD sin/cos implementation with numpy-simd-routines
numpy-simd-routines added as subrepo in meson subprojects directory and the current FP configuration is static, ~1ulp used for double-precision ~4ulp for single-precision with handling floating-point errors, special-cases extended precision for large arguments, subnormals are enabled by default too. numpy-simd-routines supports all SIMD extensions that are supported by Google Highway including non-FMA extensions and is fully independent from libm to guarantee unified results across all compilers and platforms. Full benchmarks will be provided within the pull-request, the following benchmark was tested against clang-19 and x86 CPU (Ryzen7 7700X) with AVX512 enabled. Note: that there was no SIMD optimization enabled for sin/cos for double-precision, only single-precision. | Before | After | Ratio | Benchmark (Parameter) | |---------------|-------------|--------|------------------------------------------| | 713±6μs | 633±6μs | 0.89 | UnaryFP(<ufunc 'cos'>, 1, 2, 'f') | | 717±9μs | 637±6μs | 0.89 | UnaryFP(<ufunc 'cos'>, 4, 1, 'f') | | 705±3μs | 607±10μs | 0.86 | UnaryFP(<ufunc 'sin'>, 4, 1, 'f') | | 714±10μs | 595±0.5μs | 0.83 | UnaryFP(<ufunc 'sin'>, 1, 2, 'f') | | 370±0.3μs | 277±4μs | 0.75 | UnaryFP(<ufunc 'cos'>, 1, 1, 'f') | | 373±2μs | 236±0.6μs | 0.63 | UnaryFP(<ufunc 'sin'>, 1, 1, 'f') | | 1.06±0.01ms | 648±3μs | 0.61 | UnaryFP(<ufunc 'cos'>, 4, 2, 'f') | | 1.06±0.01ms | 617±30μs | 0.58 | UnaryFP(<ufunc 'sin'>, 4, 2, 'f') | | 5.06±0.06ms | 2.61±0.3ms | 0.52 | UnaryFPSpecial(<ufunc 'cos'>, 4, 2, 'd') | | 1.48±0ms | 715±5μs | 0.48 | UnaryFPSpecial(<ufunc 'sin'>, 1, 2, 'f') | | 1.50±0.01ms | 639±6μs | 0.43 | UnaryFPSpecial(<ufunc 'cos'>, 1, 2, 'f') | | 5.15±0.1ms | 1.96±0.01ms | 0.38 | UnaryFPSpecial(<ufunc 'cos'>, 4, 1, 'd') | | 5.72±0.02ms | 2.09±0.1ms | 0.37 | UnaryFP(<ufunc 'cos'>, 4, 2, 'd') | | 5.76±0.01ms | 2.03±0.08ms | 0.35 | UnaryFP(<ufunc 'sin'>, 4, 2, 'd') | | 5.07±0.08ms | 1.76±0.2ms | 0.35 | UnaryFPSpecial(<ufunc 'cos'>, 1, 2, 'd') | | 6.04±0.04ms | 2.05±0.09ms | 0.34 | UnaryFPSpecial(<ufunc 'sin'>, 4, 2, 'd') | | 5.79±0.03ms | 1.90±0.2ms | 0.33 | UnaryFP(<ufunc 'sin'>, 4, 1, 'd') | | 2.29±0.1ms | 762±40μs | 0.33 | UnaryFPSpecial(<ufunc 'sin'>, 4, 1, 'f') | | 5.72±0.1ms | 1.75±0.07ms | 0.31 | UnaryFP(<ufunc 'cos'>, 4, 1, 'd') | | 6.04±0.03ms | 1.82±0.2ms | 0.3 | UnaryFPSpecial(<ufunc 'sin'>, 4, 1, 'd') | | 2.49±0.1ms | 748±30μs | 0.3 | UnaryFPSpecial(<ufunc 'sin'>, 4, 2, 'f') | | 2.23±0.1ms | 634±6μs | 0.28 | UnaryFPSpecial(<ufunc 'cos'>, 4, 1, 'f') | | 1.31±0.03ms | 367±5μs | 0.28 | UnaryFPSpecial(<ufunc 'sin'>, 1, 1, 'f') | | 2.55±0.09ms | 654±30μs | 0.26 | UnaryFPSpecial(<ufunc 'cos'>, 4, 2, 'f') | | 4.97±0.03ms | 1.14±0ms | 0.23 | UnaryFPSpecial(<ufunc 'cos'>, 1, 1, 'd') | | 5.67±0.01ms | 1.22±0.03ms | 0.22 | UnaryFP(<ufunc 'cos'>, 1, 2, 'd') | | 5.76±0.03ms | 1.28±0.06ms | 0.22 | UnaryFP(<ufunc 'sin'>, 1, 2, 'd') | | 1.26±0.01ms | 272±2μs | 0.22 | UnaryFPSpecial(<ufunc 'cos'>, 1, 1, 'f') | | 7.03±0.02ms | 1.31±0.01ms | 0.19 | UnaryFPSpecial(<ufunc 'sin'>, 1, 2, 'd') | | 5.67±0.01ms | 810±9μs | 0.14 | UnaryFP(<ufunc 'cos'>, 1, 1, 'd') | | 5.71±0.01ms | 817±40μs | 0.14 | UnaryFP(<ufunc 'sin'>, 1, 1, 'd') | | 7.05±0.03ms | 915±4μs | 0.13 | UnaryFPSpecial(<ufunc 'sin'>, 1, 1, 'd') |
1 parent 5e40326 commit ab3559f

9 files changed

Lines changed: 196 additions & 290 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ build
5050
build-*
5151
# meson python output
5252
.mesonpy-native-file.ini
53+
subprojects/.wraplock
5354
# sphinx build directory
5455
_build
5556
# dist directory is where sdist/wheel end up

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@
1919
[submodule "numpy/_core/src/common/pythoncapi-compat"]
2020
path = numpy/_core/src/common/pythoncapi-compat
2121
url = https://github.com/python/pythoncapi-compat
22+
[submodule "subprojects/numpy-simd-routines/src"]
23+
path = subprojects/numpy-simd-routines/src
24+
url = https://github.com/numpy/numpy-simd-routines

numpy/_core/include/numpy/npy_common.h

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,24 @@
117117
#endif
118118
#endif
119119

120-
#if defined(_MSC_VER)
121-
#define NPY_NOINLINE static __declspec(noinline)
122-
#elif defined(__GNUC__) || defined(__clang__)
123-
#define NPY_NOINLINE static __attribute__((noinline))
120+
#ifdef _MSC_VER
121+
#ifdef __cplusplus
122+
#define NPY_NOINLINE __declspec(noinline)
123+
#else
124+
#define NPY_NOINLINE static __declspec(noinline)
125+
#endif
126+
#elif defined(__GNUC__)
127+
#ifdef __cplusplus
128+
#define NPY_NOINLINE __attribute__((noinline))
129+
#else
130+
#define NPY_NOINLINE static __attribute__((noinline))
131+
#endif
124132
#else
125-
#define NPY_NOINLINE static
133+
#ifdef __cplusplus
134+
#define NPY_NOINLINE inline
135+
#else
136+
#define NPY_NOINLINE static inline
137+
#endif
126138
#endif
127139

128140
#ifdef __cplusplus

numpy/_core/meson.build

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,9 @@ umath_gen_headers = [
981981
src_file.process('src/umath/loops_utils.h.src'),
982982
]
983983

984+
npsr = subproject('numpy-simd-routines')
985+
npsr_dep = npsr.get_variable('npsr_dep')
986+
984987
foreach gen_mtargets : [
985988
[
986989
'loops_arithm_fp.dispatch.h',
@@ -1145,7 +1148,7 @@ foreach gen_mtargets : [
11451148
dispatch: gen_mtargets[2],
11461149
baseline: CPU_BASELINE,
11471150
prefix: 'NPY_',
1148-
dependencies: [py_dep, np_core_dep],
1151+
dependencies: [py_dep, np_core_dep, npsr_dep],
11491152
c_args: c_args_common + max_opt,
11501153
cpp_args: cpp_args_common + max_opt,
11511154
include_directories: [

numpy/_core/src/common/simd/simd.hpp

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#ifndef NUMPY__CORE_SRC_COMMON_SIMD_SIMD_HPP_
22
#define NUMPY__CORE_SRC_COMMON_SIMD_SIMD_HPP_
3-
43
/**
54
* This header provides a thin wrapper over Google's Highway SIMD library.
65
*
@@ -19,7 +18,9 @@
1918
*/
2019
#ifndef NPY_DISABLE_OPTIMIZATION
2120
#include <hwy/highway.h>
22-
21+
#include <npsr/npsr.h>
22+
#include <type_traits>
23+
#include <limits>
2324
/**
2425
* We avoid using Highway scalar operations for the following reasons:
2526
*
@@ -67,6 +68,37 @@ namespace hn = hwy::HWY_NAMESPACE;
6768
// internally used by the template header
6869
template <typename TLane>
6970
using _Tag = hn::ScalableTag<TLane>;
71+
72+
/// NumPy SIMD Routines namespace alias
73+
/// npsr is tag free by design so we only include it within main namespace (np::simd)
74+
namespace sr = npsr::HWY_NAMESPACE;
75+
76+
/// Default precision configrations for NumPy SIMD Routines
77+
// TODO: Allow user to configure the default precision at build time.
78+
namespace detail {
79+
using PreciseHigh = decltype(npsr::Precise{});
80+
using PreciseLow = decltype(npsr::Precise{npsr::kLowAccuracy});
81+
struct PresiceDummy {};
82+
template <typename T>
83+
struct PreciseByType {};
84+
template <>
85+
struct PreciseByType<float> { using Type = PreciseLow; };
86+
template <>
87+
struct PreciseByType<double> {
88+
#if NPY_HWY_F64
89+
using Type = PreciseHigh;
90+
#else
91+
// If float64 SIMD isn’t available, use a dummy type.
92+
// The scalar path will run, but `Type` must still be defined.
93+
// The dummy is never passed; it only satisfies interfaces.
94+
// This also avoids spurious FP exceptions during RAII.
95+
using Type = PresiceDummy;
96+
#endif
97+
};
98+
} // namespace detail
99+
template <typename T>
100+
using Precise = typename detail::PreciseByType<T>::Type;
101+
70102
#endif
71103
#include "simd.inc.hpp"
72104
} // namespace simd

numpy/_core/src/common/simd/simd.inc.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ LoadU(const TLane *ptr)
5151
return hn::LoadU(_Tag<TLane>(), ptr);
5252
}
5353

54+
template <typename TLane>
55+
HWY_API Vec<TLane>
56+
LoadN(const TLane *ptr, size_t n)
57+
{
58+
return hn::LoadN(_Tag<TLane>(), ptr, n);
59+
}
60+
5461
/// Unaligned store of a vector to memory.
5562
template <typename TLane>
5663
HWY_API void
@@ -59,6 +66,13 @@ StoreU(const Vec<TLane> &a, TLane *ptr)
5966
hn::StoreU(a, _Tag<TLane>(), ptr);
6067
}
6168

69+
template <typename TLane>
70+
HWY_API void
71+
StoreN(const Vec<TLane> &a, TLane *ptr, size_t n)
72+
{
73+
hn::StoreN(a, _Tag<TLane>(), ptr, n);
74+
}
75+
6276
/// Returns the number of vector lanes based on the lane type.
6377
template <typename TLane>
6478
HWY_API HWY_LANES_CONSTEXPR size_t

0 commit comments

Comments
 (0)