Repository navigation
Add dpnp.minmax with a fused min/max reduction kernel - #3135
antonwolfy wants to merge 8 commits into
Conversation
Cover float16, NaN at the first, middle and last position (also a complex NaN in the imaginary part only), integer limits of all integer types, multi-pass reductions of several rows, axis=() with out, and read-only and other-queue out errors. Link dpnp.minmax from dpnp.min and dpnp.max.
There was a problem hiding this comment.
🟡 Changes recommended
The public docstring omits two intentional out incompatibilities: narrowing integer outputs can produce different extrema, and even disjoint interleaved output views are rejected.
1 open issue
What changed in this PR
Adds dpnp.minmax to compute minimum and maximum together using a fused SYCL reduction, extending dpnp’s NumPy-compatible statistics API.
Changes:
- Adds fused kernels for contiguous and strided inputs, including multi-pass reductions.
- Integrates the public API, output handling, bindings, exports, and documentation.
- Adds tests for values, layouts, output buffers, errors, queues, and USM allocation types.
| File | Description |
|---|---|
dpnp/tests/test_usm_type.py |
Checks output USM allocation types. |
dpnp/tests/test_sycl_queue.py |
Checks output queue placement. |
dpnp/tests/test_statistics.py |
Adds functional and edge-case tests for minmax. |
dpnp/tensor/libtensor/source/reductions/reduction_common.cpp |
Registers the new reduction binding. |
dpnp/tensor/libtensor/source/reductions/minmax.hpp |
Declares binding initialization. |
dpnp/tensor/libtensor/source/reductions/minmax.cpp |
Adds validation, dispatch, and Python binding. |
dpnp/tensor/libtensor/include/kernels/reductions.hpp |
Implements fused minimum/maximum kernels. |
dpnp/tensor/CMakeLists.txt |
Includes the new binding source in the build. |
dpnp/tensor/_reduction.py |
Adds private axis and output handling. |
dpnp/dpnp_iface_statistics.py |
Adds the public API and docstring. |
dpnp/__init__.py |
Exports dpnp.minmax. |
doc/reference/math.rst |
Lists minmax among extrema functions. |
CHANGELOG.md |
Records the new functionality. |
Review effort: Balanced
|
Array API standard conformance tests for dpnp=0.22.0dev0=np2py314h8d9cdd5_20 ran successfully. |
|
View rendered docs @ https://intelpython.github.io/dpnp/pull/3135/index.html |
There was a problem hiding this comment.
🟡 Changes recommended
Casting a read-only output to a different dtype hides its read-only flag, allowing the reduction to mutate the input before failing; validate original output writability before reduction and add a mixed-dtype regression test.
1 open issue
1 resolved since last review
Review effort: Balanced
|
Follow-up proposal: the ASV suite has no reduction benchmarks yet, so nothing tracks the gain of the fused
Keeping it out of this PR means the benchmark never runs against a dpnp build without |


NumPy 2.6 adds
numpy.minmax, which returns the(min, max)tuple of an array in a single pass. This PR addsdpnp.minmaxwith the same signature. It is backed by a new fused reduction kernel that reads the input once and writes both extrema, instead of runningdpnp.minanddpnp.maxone after another.Changes
kernels/reductions.hpp: a new tree reduction (no atomics) that keeps the running minimum and maximum per work-item and reduces both over the work-group. It uses native group algorithms for integers,all_of/any_offor bool, and NaN-aware native reductions for real floats: if any work-item holds a NaN, the NaN of the first such work-item is broadcast. Complex types use the custom SLM reduction. There are sequential, one-work-group and multi-pass variants with strided, axis-0 and axis-1 contiguous entry points, and the two outputs have independent strides.source/reductions/minmax.cpp: new binding_tensor_reductions_impl._minmax_over_axis(src, trailing_dims_to_reduce, dst_min, dst_max, sycl_queue, depends).dpnp/tensor/_reduction.py: private_minmaxhelper, modelled on_comparison_over_axis.dpnp.minmax(a, axis=None, out=None, keepdims=False, initial=None, where=True):outis a tuple of two arrays, and an entry may beNone.initialandwhereare only supported with their default values, as indpnp.minanddpnp.max.test_statistics.py,test_sycl_queue.pyandtest_usm_type.py.Two behaviours follow
dpnp.min,dpnp.maxanddpnp.modfrather than NumPy, and are not changed here:outof another dtype receives the result cast after the reduction. NumPy casts the input first.outentries that overlap in memory are rejected, including interleaved views such as(r[:, 0], r[:, 1]).Performance
Measured on an Intel Core Ultra 7 366H (Panther Lake H) with its integrated GPU on Level Zero and its CPU on OpenCL. The timing is the end-to-end time of
dpnp.minmax(a)againstdpnp.min(a), dpnp.max(a), each followed byqueue.wait(). Runs are interleaved and the median is reported. The speedup is the separate time divided by the fused time; the results of both were checked to be equal in every case.dpnp.minanddpnp.maxuse the single-kernel atomic path, and complex128 with 1e3 elements (0.96x).Cost
Build times are for
-j6on 16 CPUs._tensor_impllink remains the last step to finish._tensor_reductions_impltarget alone goes from 488 s to 511 s (+23 s), measured branch/master/master/branch. The device link takes +20.6 s andminmax.cppcompiles in about 12 s. Compile times of the other reduction sources are unchanged._tensor_reductions_implshared object grows from 35.38 MB to 37.27 MB (+1.89 MB, +5.4%). Of that, 1.0 MB is SPIR-V and 0.45 MB is host code. Compressed with zstd -19 it grows by 166 KB, with gzip -9 by 282 KB, and all dpnp shared objects together grow by 1.0%.import dpnpgrows by about 1.6 MB. GPU memory is unchanged, and so is the first-call JIT time of the existingdpnp.minanddpnp.max.Checklist