#32307·numpy

ENH: use existing non-contiguous SIMD path for integer np.minimum/np.maximum (currently float-only)

Author: samrudh-nuxCreated Aug 16, 2026Updated Sep 17, 2026

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.

bash
$ 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_u64

Identical 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

bash
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 how IS_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 (reusing NPY_BITSOF_@BTYPE@, already used elsewhere in this file for TO_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.src template 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.