ENH: use existing non-contiguous SIMD path for integer np.minimum/np.maximum (currently float-only)
Proposed new feature or change:
np.minimum/np.maximum have a vectorized fast path for non-contiguous
operands (e.g. a[::-1], a[::2]), but it's compiled in for
floating-point dtypes only. Integer dtypes fall back to the scalar loop
unconditionally, regardless of CPU SIMD capability. This looks like a
dtype-based restriction with no corresponding hardware or correctness
constraint behind it — the intrinsics needed for the integer case already
exist and are already exercised by other loops in the same file. Opening
this to get maintainer/community input before attempting a fix.
File: numpy/_core/src/umath/loops_minmax.dispatch.c.src
// non-contiguous for float 32/64-bit memory access
#if @is_fp@ && !defined(NPY_HAVE_NEON)
static inline void
simd_binary_@intrin@_@sfx@(...)@is_fp@ here is a template-level dtype flag, not a capability check.
Because of it, simd_binary_@intrin@_@sfx@ — the function that performs
strided SIMD load/compare/store via npyv_loadn_@sfx@ / npyv_storen_@sfx@
— is never instantiated for any integer suffix. The call site inherits the
same restriction, so integer dtypes fall straight through to the generic
4-wide scalar unroll further down the file, skipping the vectorized tier
entirely.
Why this looks like a gap rather than a design decision
1. The intrinsics already support the integer case, on every backend NumPy ships.
$ grep -o "npyv_loadn_[a-z0-9]*" \
numpy/_core/src/common/simd/{sse,avx2,avx512,neon,vec,lsx}/memory.h
npyv_loadn_f32 npyv_loadn_f64 npyv_loadn_s32 npyv_loadn_s64 npyv_loadn_u32 npyv_loadn_u64Identical signature set across SSE, AVX2, AVX512, NEON, VSX, and LSX — no backend where 32/64-bit integer strided load/store is unavailable while the float equivalent is available. (8/16-bit lanes are the one place a real constraint exists — no backend implements a strided variant below 32 bits — so any fix here should scope to 32/64-bit lanes specifically, not "all integers.")
2. Integer min/max has no ordering hazard that would justify the caution.
Floating-point min/max carries NaN-propagation semantics that can make strict ordering stride-sensitive enough to warrant care. Integer min/max is a total order with no such hazard — there's no correctness argument for excluding integers here that applies symmetrically to floats.
3. This isn't a novel pattern relative to the rest of the codebase.
numpy/_core/src/umath/loops_unary.dispatch.c.src already gates its own
non-contiguous SIMD path on lane width rather than float-ness:
* #supports_ncontig = 0*2, 1*3, 0*2, 1*3, 1*3#— 1 for every 32/64-bit type, int or float; 0 only for 8/16-bit. That rule
is already in production for np.negative and its siblings. Would be good
to hear whether there's a reason loops_minmax diverges from that, or
whether it's simply an unaddressed follow-up.
Root cause, as far as I can trace it
git blame numpy/_core/src/umath/loops_minmax.dispatch.c.src
-> 5fca2bfccf7 "ENH, SIMD: several improvements for max/min" (Dec 2021)The commit that introduced this function's non-contiguous path did so for a specific fp workload at the time (building on the contiguous-path work in gh-13207). Neither the diff nor the commit message gives a rationale for excluding integers — nothing there suggests it was a deliberate scope decision rather than a follow-up that never happened. @seiko2plus, since this traces back to your commit — did I miss a reason integers were left out, or is this genuinely open?
Discussion points
- Is there a correctness or portability reason for the
@is_fp@gate here that isn't obvious from the code/history — e.g. something specific to howIS_BINARY_CONT/overlap detection interacts with integer strides that doesn't apply to floats? - If not, is
@is_fp@ || NPY_BITSOF_@BTYPE@ == 32 || NPY_BITSOF_@BTYPE@ == 64(reusingNPY_BITSOF_@BTYPE@, already used elsewhere in this file forTO_SIMD_SFX) the right shape for the fix, or is there a preferred pattern given the ongoing C++/Highway migration work (gh-29528) that this should align with instead of extending the existing.c.srctemplate approach? - Worth noting:
numpy-minmax(a third-party package doing similar SIMD min/max work outside NumPy core) deliberately disabled vectorization for negative-stride arrays in a recent release "for improved compatibility." That's a different codebase/mechanism, but flagging it in case it points at a platform-specific subtlety worth considering here.
Happy to attempt a fix once there's agreement on approach — wanted to raise it for discussion first rather than show up withan unsolicited diff.
Source: numpy/numpy