From 5ae497c4cbe8d4a35f5a1f13e349a2eb8d22a827 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 12 Aug 2026 13:32:27 +0200 Subject: [PATCH 1/3] Handle empty boolean reductions without launching a kernel dpnp.all / dpnp.any submitted the reduction kernel unconditionally. When the input has no elements to reduce over - e.g. all(zeros((0, 3, 4))) or a reduction along a zero-length axis - the reduction extent is zero and the kernel is launched with a zero-sized nd_range. That is a silent no-op on runtimes built with NDEBUG, but aborts on an assertions-enabled SYCL runtime (adjustNDRangePerKernel asserts NDR.LocalSize[0] == 0 when GlobalSize is 0). Short-circuit in _boolean_reduction when the (permuted) input is empty: build the result directly with the reduction identity (True for all, False for any). This is correct for both empty sub-cases - an empty output (fill value is irrelevant) and a zero-length reduced axis (identity is the answer) - and submits no kernel. --- dpnp/tensor/_utility_functions.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/dpnp/tensor/_utility_functions.py b/dpnp/tensor/_utility_functions.py index 651ce0830266..3b4158abf56c 100644 --- a/dpnp/tensor/_utility_functions.py +++ b/dpnp/tensor/_utility_functions.py @@ -48,7 +48,7 @@ ) -def _boolean_reduction(x, axis, keepdims, func): +def _boolean_reduction(x, axis, keepdims, func, identity): if not isinstance(x, dpt.usm_ndarray): raise TypeError(f"Expected dpnp.tensor.usm_ndarray, got {type(x)}") @@ -77,11 +77,27 @@ def _boolean_reduction(x, axis, keepdims, func): exec_q = x.sycl_queue res_usm_type = x.usm_type + if x_tmp.size == 0: + # nothing to reduce over: the result is either empty (a non-reduced + # dimension is zero) or filled with the reduction identity (a reduced + # dimension is zero, e.g. all([]) is True and any([]) is False) + res = dpt.full( + res_shape, + identity, + dtype=dpt.bool, + usm_type=res_usm_type, + sycl_queue=exec_q, + ) + if keepdims: + res_shape = res_shape + (1,) * red_nd + inv_perm = sorted(range(nd), key=lambda d: perm[d]) + res = dpt.permute_dims(dpt.reshape(res, res_shape), inv_perm) + return res + _manager = du.SequentialOrderManager[exec_q] dep_evs = _manager.submitted_events - # always allocate the temporary as - # int32 and usm-device to ensure that atomic updates - # are supported + # always allocate the temporary as int32 and usm-device to ensure + # that atomic updates are supported res_tmp = dpt.empty( res_shape, dtype=dpt.int32, @@ -142,7 +158,7 @@ def all(x, /, *, axis=None, keepdims=False): An array with a data type of `bool` containing the results of the logical AND reduction. """ - return _boolean_reduction(x, axis, keepdims, tri._all) + return _boolean_reduction(x, axis, keepdims, tri._all, True) def any(x, /, *, axis=None, keepdims=False): @@ -171,7 +187,7 @@ def any(x, /, *, axis=None, keepdims=False): An array with a data type of `bool` containing the results of the logical OR reduction. """ - return _boolean_reduction(x, axis, keepdims, tri._any) + return _boolean_reduction(x, axis, keepdims, tri._any, False) def _validate_diff_shape(sh1, sh2, axis): From 99bda422ea6a80714964421df2fe654a391d6700 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 12 Aug 2026 14:05:33 +0200 Subject: [PATCH 2/3] Update changelog for empty boolean reduction fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d01978e60df..8defd2a707a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ This release is compatible with NumPy 2.5. * Fixed `dpnp.interp` returning `nan` when querying at an exact knot point whose adjacent `fp` value is `inf` [#2986](https://github.com/IntelPython/dpnp/pull/2986) * Fixed missing strides validation in `dpnp.tensor.usm_ndarray` constructor when allocating new memory [#2927](https://github.com/IntelPython/dpnp/pull/2927) * Fixed `dpnp.bincount` raising a `ValueError` on an empty input array instead of returning an empty `intp` array [#3018](https://github.com/IntelPython/dpnp/pull/3018) +* Fixed `dpnp.all` and `dpnp.any` aborting when reducing over an empty axis (e.g. an array with a zero-length dimension) [#3021](https://github.com/IntelPython/dpnp/pull/3021) ### Security From a90dafd635146eac3d01d9bccfd8944ce331be55 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 12 Aug 2026 14:13:32 +0200 Subject: [PATCH 3/3] Deduplicate keepdims handling in _boolean_reduction Use an if/else so the empty-input and reduction paths share the single trailing keepdims block instead of repeating it. --- dpnp/tensor/_utility_functions.py | 67 ++++++++++++++----------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/dpnp/tensor/_utility_functions.py b/dpnp/tensor/_utility_functions.py index 3b4158abf56c..8d0be2d85028 100644 --- a/dpnp/tensor/_utility_functions.py +++ b/dpnp/tensor/_utility_functions.py @@ -88,42 +88,37 @@ def _boolean_reduction(x, axis, keepdims, func, identity): usm_type=res_usm_type, sycl_queue=exec_q, ) - if keepdims: - res_shape = res_shape + (1,) * red_nd - inv_perm = sorted(range(nd), key=lambda d: perm[d]) - res = dpt.permute_dims(dpt.reshape(res, res_shape), inv_perm) - return res - - _manager = du.SequentialOrderManager[exec_q] - dep_evs = _manager.submitted_events - # always allocate the temporary as int32 and usm-device to ensure - # that atomic updates are supported - res_tmp = dpt.empty( - res_shape, - dtype=dpt.int32, - usm_type="device", - sycl_queue=exec_q, - ) - hev0, ev0 = func( - src=x_tmp, - trailing_dims_to_reduce=red_nd, - dst=res_tmp, - sycl_queue=exec_q, - depends=dep_evs, - ) - _manager.add_event_pair(hev0, ev0) - - # copy to boolean result array - res = dpt.empty( - res_shape, - dtype=dpt.bool, - usm_type=res_usm_type, - sycl_queue=exec_q, - ) - hev1, ev1 = ti._copy_usm_ndarray_into_usm_ndarray( - src=res_tmp, dst=res, sycl_queue=exec_q, depends=[ev0] - ) - _manager.add_event_pair(hev1, ev1) + else: + _manager = du.SequentialOrderManager[exec_q] + dep_evs = _manager.submitted_events + # always allocate the temporary as int32 and usm-device to ensure + # that atomic updates are supported + res_tmp = dpt.empty( + res_shape, + dtype=dpt.int32, + usm_type="device", + sycl_queue=exec_q, + ) + hev0, ev0 = func( + src=x_tmp, + trailing_dims_to_reduce=red_nd, + dst=res_tmp, + sycl_queue=exec_q, + depends=dep_evs, + ) + _manager.add_event_pair(hev0, ev0) + + # copy to boolean result array + res = dpt.empty( + res_shape, + dtype=dpt.bool, + usm_type=res_usm_type, + sycl_queue=exec_q, + ) + hev1, ev1 = ti._copy_usm_ndarray_into_usm_ndarray( + src=res_tmp, dst=res, sycl_queue=exec_q, depends=[ev0] + ) + _manager.add_event_pair(hev1, ev1) if keepdims: res_shape = res_shape + (1,) * red_nd