From 74df9894882f9b3b814686dd36fc0b0c84ddd296 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Mon, 19 Oct 2026 20:41:09 +0200 Subject: [PATCH 1/3] Fix repeat with uint64 repeats above int64 max `dpnp.tensor.repeat` checked a `uint64` repeats array for negative values before casting it to `int64`, so values above the `int64` maximum passed the check and wrapped only later. A multi-element array silently gave a wrong result and a single element raised an `OverflowError`. Cast `uint64` repeats to `int64` first so such values become negative and raise a `ValueError`. Also fix the casting rule named in the `TypeError` message, which is `same_kind`, not `safe`. --- CHANGELOG.md | 1 + dpnp/tensor/_manipulation_functions.py | 5 ++++- dpnp/tests/tensor/test_usm_ndarray_manipulation.py | 8 ++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaaf3c68fafa..c9a5ecbe5592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,7 @@ This release is compatible with NumPy 2.5. * Fixed `dpnp.median` and `dpnp.nanmedian` raising a `ValueError` for a tuple `axis` when a kept dimension has size 0 [#3081](https://github.com/IntelPython/dpnp/pull/3081) * Fixed a `RuntimeError` raised by `dpnp.tensor` kernels launched over more than `INT_MAX` work-items when built with DPC++ compiler 2026.2 or newer, by also passing `-fno-sycl-id-queries-fit-in-int` to the linker [#3089](https://github.com/IntelPython/dpnp/pull/3089) * Fixed `dpnp.__all__` missing `ndarray` and `__array_namespace_info__` and listing `mean`, `median` and `min` twice [#3100](https://github.com/IntelPython/dpnp/pull/3100) +* Fixed `dpnp.repeat` and `dpnp.tensor.repeat` silently returning wrong results for `uint64` repeats above the `int64` maximum [#3115](https://github.com/IntelPython/dpnp/pull/3115) ### Security diff --git a/dpnp/tensor/_manipulation_functions.py b/dpnp/tensor/_manipulation_functions.py index 166a6cb5a526..235d81dcd9fe 100644 --- a/dpnp/tensor/_manipulation_functions.py +++ b/dpnp/tensor/_manipulation_functions.py @@ -638,8 +638,11 @@ def repeat(x, repeats, /, *, axis=None): if not dpt.can_cast(repeats.dtype, dpt.int64, casting="same_kind"): raise TypeError( f"'repeats' data type {repeats.dtype} cannot be cast to " - "'int64' according to the casting rule ''safe.''" + "'int64' according to the casting rule ''same_kind.''" ) + if repeats.dtype == dpt.uint64: + # cast first so values above int64 max wrap to negative + repeats = dpt.astype(repeats, dpt.int64) if repeats.size == 1: scalar = True # bring the single element to the host diff --git a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py index cd5962615fb9..76b6f9739cd9 100644 --- a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py +++ b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py @@ -1509,6 +1509,14 @@ def test_repeat_arg_validation(): with pytest.raises(ValueError): dpt.repeat(x, reps) + # u8 repeats above i8 max wrap to negative + reps = dpt.asarray(2**64 - 1, dtype="u8") + with pytest.raises(ValueError): + dpt.repeat(x, reps) + reps = dpt.asarray([1, 1, 2**64 - 1, 1, 1], dtype="u8") + with pytest.raises(ValueError): + dpt.repeat(x, reps) + # repeats must broadcastable to axis size reps = dpt.arange(10, dtype="i8") with pytest.raises(ValueError): From ed02c9b948cc4bb1c59c880b029a924b341083b5 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Tue, 20 Oct 2026 18:54:46 +0200 Subject: [PATCH 2/3] Clarify repeat error for uint64 repeats above int64 max A uint64 value above the int64 maximum wraps to negative on the cast to int64, so it was reported as "must be positive" although it is positive. Since uint64 is never negative, keep the cast and the existing sign check but raise a message about the int64 maximum for uint64 repeats, so no extra kernel or host sync is needed. The message is now also the same for single- and multi-element repeats arrays. --- dpnp/tensor/_manipulation_functions.py | 9 ++++++--- dpnp/tests/tensor/test_usm_ndarray_manipulation.py | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/dpnp/tensor/_manipulation_functions.py b/dpnp/tensor/_manipulation_functions.py index 235d81dcd9fe..fc75a86d6764 100644 --- a/dpnp/tensor/_manipulation_functions.py +++ b/dpnp/tensor/_manipulation_functions.py @@ -640,9 +640,12 @@ def repeat(x, repeats, /, *, axis=None): f"'repeats' data type {repeats.dtype} cannot be cast to " "'int64' according to the casting rule ''same_kind.''" ) + err_msg = "`repeats` elements must be positive" if repeats.dtype == dpt.uint64: - # cast first so values above int64 max wrap to negative + # u8 is never negative, so a value negative after the cast was + # above i8 max: reuse the sign check with a matching message repeats = dpt.astype(repeats, dpt.int64) + err_msg = "`repeats` elements must not exceed the int64 maximum" if repeats.size == 1: scalar = True # bring the single element to the host @@ -653,7 +656,7 @@ def repeat(x, repeats, /, *, axis=None): # since non-0D arrays can not be converted to scalars repeats = int(repeats[0]) if repeats < 0: - raise ValueError("`repeats` elements must be positive") + raise ValueError(err_msg) else: if repeats.size != axis_size: raise ValueError( @@ -661,7 +664,7 @@ def repeat(x, repeats, /, *, axis=None): "the repeated axis" ) if not dpt.all(repeats >= 0): - raise ValueError("'repeats' elements must be positive") + raise ValueError(err_msg) elif isinstance(repeats, (tuple, list, range)): usm_type = x.usm_type diff --git a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py index 76b6f9739cd9..6d82816b5453 100644 --- a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py +++ b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py @@ -1509,12 +1509,12 @@ def test_repeat_arg_validation(): with pytest.raises(ValueError): dpt.repeat(x, reps) - # u8 repeats above i8 max wrap to negative + # u8 repeats above i8 max reps = dpt.asarray(2**64 - 1, dtype="u8") - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="int64 maximum"): dpt.repeat(x, reps) reps = dpt.asarray([1, 1, 2**64 - 1, 1, 1], dtype="u8") - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="int64 maximum"): dpt.repeat(x, reps) # repeats must broadcastable to axis size From e3475c109ffb5651a99d7c5d6ad2de941ef09b14 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Tue, 20 Oct 2026 18:59:55 +0200 Subject: [PATCH 3/3] Drop comment on the uint64 repeats cast --- dpnp/tensor/_manipulation_functions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dpnp/tensor/_manipulation_functions.py b/dpnp/tensor/_manipulation_functions.py index fc75a86d6764..7e3ffa05c57a 100644 --- a/dpnp/tensor/_manipulation_functions.py +++ b/dpnp/tensor/_manipulation_functions.py @@ -642,8 +642,6 @@ def repeat(x, repeats, /, *, axis=None): ) err_msg = "`repeats` elements must be positive" if repeats.dtype == dpt.uint64: - # u8 is never negative, so a value negative after the cast was - # above i8 max: reuse the sign check with a matching message repeats = dpt.astype(repeats, dpt.int64) err_msg = "`repeats` elements must not exceed the int64 maximum" if repeats.size == 1: