Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ This release is compatible with NumPy 2.5.
* `dpnp` uses pybind11 3.1.0 [#3015](https://github.com/IntelPython/dpnp/pull/3015)
* Reworked the ASV benchmarks and added end-to-end workload benchmarks derived from dpBench [#2996](https://github.com/IntelPython/dpnp/pull/2996)
* Reduced allocations in `dpnp.linalg.norm` by reusing the reduction result as the `sqrt` output buffer in the 2-norm and Frobenius-norm branches [#3062](https://github.com/IntelPython/dpnp/pull/3062)
* Changed `dpnp.sort`, `dpnp.argsort`, and their `dpnp.ndarray`/`dpnp.tensor` counterparts to place `NaN` values last instead of first when sorting in descending order [#3066](https://github.com/IntelPython/dpnp/pull/3066)
* Changed `dpnp.tensor.top_k` with `mode="largest"` to no longer return `NaN` values (or complex values with a `NaN` component) ahead of finite values, matching the `NaN`-last order of `dpnp.tensor.sort` [#3066](https://github.com/IntelPython/dpnp/pull/3066)

### Deprecated

Expand Down
6 changes: 4 additions & 2 deletions dpnp/dpnp_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,8 @@ def argsort(
descending : bool, optional
Sort order. If ``True``, the array must be sorted in descending
order (by value). If ``False``, the array must be sorted in
ascending order (by value).
ascending order (by value). NaN values (and complex values with a
NaN component) are ordered to the end regardless of `descending`.

Default: ``False``.
stable : {None, bool}, optional
Expand Down Expand Up @@ -1967,7 +1968,8 @@ def sort(
descending : bool, optional
Sort order. If ``True``, the array must be sorted in descending
order (by value). If ``False``, the array must be sorted in
ascending order (by value).
ascending order (by value). NaN values (and complex values with a
NaN component) are ordered to the end regardless of `descending`.

Default: ``False``.
stable : {None, bool}, optional
Expand Down
6 changes: 4 additions & 2 deletions dpnp/dpnp_iface_sorting.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ def argsort(
descending : bool, optional
Sort order. If ``True``, the array must be sorted in descending order
(by value). If ``False``, the array must be sorted in ascending order
(by value).
(by value). NaN values (and complex values with a NaN component) are
ordered to the end regardless of `descending`.
Comment thread
antonwolfy marked this conversation as resolved.

Default: ``False``.
stable : {None, bool}, optional
Expand Down Expand Up @@ -348,7 +349,8 @@ def sort(a, axis=-1, kind=None, order=None, *, descending=False, stable=None):
descending : bool, optional
Sort order. If ``True``, the array must be sorted in descending order
(by value). If ``False``, the array must be sorted in ascending order
(by value).
(by value). NaN values (and complex values with a NaN component) are
ordered to the end regardless of `descending`.

Default: ``False``.
stable : {None, bool}, optional
Expand Down
12 changes: 10 additions & 2 deletions dpnp/tensor/_sorting.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ def sort(x, /, *, axis=-1, descending=False, stable=True, kind=None):
descending (Optional[bool]):
sort order. If `True`, the array must be sorted in descending
order (by value). If `False`, the array must be sorted in
ascending order (by value). Default: `False`.
ascending order (by value). NaN values (and complex values with
a NaN component) are ordered to the end regardless of
`descending`. Default: `False`.
stable (Optional[bool]):
sort stability. If `True`, the returned array must maintain the
relative order of `x` values which compare as equal. If `False`,
Expand Down Expand Up @@ -185,7 +187,9 @@ def argsort(x, axis=-1, descending=False, stable=True, kind=None):
descending (Optional[bool]):
sort order. If `True`, the array must be sorted in descending
order (by value). If `False`, the array must be sorted in
ascending order (by value). Default: `False`.
ascending order (by value). NaN values (and complex values with
a NaN component) are ordered to the end regardless of
`descending`. Default: `False`.
stable (Optional[bool]):
sort stability. If `True`, the returned array must maintain the
relative order of `x` values which compare as equal. If `False`,
Expand Down Expand Up @@ -315,6 +319,10 @@ def top_k(x, k, /, *, axis=None, mode="largest"):
- `"largest"`: return the `k` largest elements.
- `"smallest"`: return the `k` smallest elements.

NaN values (and complex values with a NaN component) are ordered
last for both modes, so they are not returned ahead of finite
values.

Default: `"largest"`.

Returns:
Expand Down
22 changes: 15 additions & 7 deletions dpnp/tensor/libtensor/include/kernels/sorting/radix_sort.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,11 @@ std::uint16_t order_preserving_cast(sycl::half val)
{
using UIntT = std::uint16_t;

const UIntT uint_val = sycl::bit_cast<UIntT>(
(sycl::isnan(val)) ? std::numeric_limits<sycl::half>::quiet_NaN()
: val);
// NaNs sort to the end for both orders
if (sycl::isnan(val))
return std::numeric_limits<UIntT>::max();

const UIntT uint_val = sycl::bit_cast<UIntT>(val);
UIntT mask;

// test the sign bit of the original value
Expand Down Expand Up @@ -203,8 +205,11 @@ std::uint32_t order_preserving_cast(FloatT val)
{
using UIntT = std::uint32_t;

UIntT uint_val = sycl::bit_cast<UIntT>(
(sycl::isnan(val)) ? std::numeric_limits<FloatT>::quiet_NaN() : val);
// NaNs sort to the end for both orders
if (sycl::isnan(val))
return std::numeric_limits<UIntT>::max();

const UIntT uint_val = sycl::bit_cast<UIntT>(val);

UIntT mask;

Expand All @@ -231,8 +236,11 @@ std::uint64_t order_preserving_cast(FloatT val)
{
using UIntT = std::uint64_t;

UIntT uint_val = sycl::bit_cast<UIntT>(
(sycl::isnan(val)) ? std::numeric_limits<FloatT>::quiet_NaN() : val);
// NaNs sort to the end for both orders
if (sycl::isnan(val))
return std::numeric_limits<UIntT>::max();

const UIntT uint_val = sycl::bit_cast<UIntT>(val);
UIntT mask;

// test the sign bit of the original value
Expand Down
8 changes: 5 additions & 3 deletions dpnp/tensor/libtensor/include/utils/rich_comparisons.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ struct ExtendedRealFPLess
template <typename fpT>
struct ExtendedRealFPGreater
{
/* [R, nan] — NaNs sort to the end, as in ascending order */
bool operator()(const fpT v1, const fpT v2) const
{
return (!std::isnan(v2) && (std::isnan(v1) || (v2 < v1)));
return (!std::isnan(v1) && (std::isnan(v2) || (v2 < v1)));
}
};

Expand Down Expand Up @@ -106,10 +107,11 @@ struct ExtendedComplexFPLess
template <typename cT>
struct ExtendedComplexFPGreater

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExtendedComplexFPGreater line by line copy of ExtendedComplexFPLess with operands swapped
Probably easier to use something like return ExtendedComplexFPLess<cT>{}(-v1, -v2); below

@antonwolfy antonwolfy Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, done — ExtendedComplexFPGreater now delegates to ExtendedComplexFPLess<cT>{}(-v1, -v2). Negation preserves NaN-ness, so the NaN grouping (and its ordering to the end) is unchanged while the finite comparison is reversed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One perf note for the record: this form constructs two std::complex temporaries (4 float negations) per comparison, whereas the previous hand-written body compared existing references. It's safe to ignore in practice — complex sort goes through the merge path (complex isn't radix-eligible), the per-compare cost is dominated by the branchy NaN-group logic inside ExtendedComplexFPLess, the negations are trivial and typically optimized out, and complex isn't a hot dtype for large sorts. The readability win of dropping the duplicated ~30-line body outweighs the negligible cost; we can revisit if profiling ever flags complex descending sort.

{
/* Negating both operands reverses the finite comparison but preserves
NaN-ness, so NaN groups stay ordered to the end. */
bool operator()(const cT &v1, const cT &v2) const
{
auto less_ = ExtendedComplexFPLess<cT>{};
return less_(v2, v1);
return ExtendedComplexFPLess<cT>{}(-v1, -v2);
}
};

Expand Down
19 changes: 18 additions & 1 deletion dpnp/tests/tensor/test_usm_ndarray_sorting.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from numpy.testing import assert_array_equal

import dpnp.tensor as dpt
from dpnp.tests.helper import numpy_version

from .helper import (
get_queue_or_skip,
Expand Down Expand Up @@ -309,8 +310,9 @@ def test_sort_real_fp_nan(dtype, kind):

s = dpt.sort(x, descending=True, kind=kind)

# NaNs sort to the end for descending order too matching NumPy
expected = dpt.asarray(
[dpt.nan, dpt.nan, 0.2, 0.1, -0.0, 0.0, -0.1, -0.3], dtype=dtype
[0.2, 0.1, -0.0, 0.0, -0.1, -0.3, dpt.nan, dpt.nan], dtype=dtype
)

assert dpt.allclose(s, expected, equal_nan=True)
Expand Down Expand Up @@ -356,6 +358,21 @@ def test_sort_complex_fp_nan(dtype):
r1.view(np.int64), r2.view(np.int64)
), f"Failed for {i} and {j}"

# complex values with a NaN component sort to the end for descending
# order too, matching NumPy (`descending` requires numpy>=2.5)
if numpy_version() >= "2.5.0":
s = dpt.sort(inp, descending=True)
expected = np.sort(dpt.asnumpy(inp), descending=True)
assert np.allclose(dpt.asnumpy(s), expected, equal_nan=True)

m1 = dpt.asnumpy(dpt.sort(sub_arrs, axis=1, descending=True))
m2 = np.sort(dpt.asnumpy(sub_arrs), axis=1, descending=True)
for k in range(len(pairs)):
i, j = pairs[k]
assert np.array_equal(
m1[k].view(np.int64), m2[k].view(np.int64)
), f"Failed for {i} and {j}"


def test_radix_sort_size_1_axis():
get_queue_or_skip()
Expand Down
33 changes: 33 additions & 0 deletions dpnp/tests/tensor/test_usm_ndarray_top_k.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,39 @@ def test_top_k_1d_smallest(dtype, n):
assert dpt.all(s.indices == expected_inds), (s.indices, expected_inds)


@pytest.mark.parametrize("dtype", ["f2", "f4", "f8", "c8", "c16"])
@pytest.mark.parametrize("mode", ["largest", "smallest"])
def test_top_k_nan(dtype, mode):
# NaNs (and complex values with a NaN component) are ordered to the end
# for both modes, so top_k excludes them until k reaches the NaN region
q = get_queue_or_skip()
skip_if_dtype_not_supported(dtype, q)

is_complex = dtype in ("c8", "c16")
nan = complex(dpt.nan, dpt.nan) if is_complex else dpt.nan

def has_nan(a):
if is_complex:
return dpt.any(dpt.isnan(dpt.real(a)) | dpt.isnan(dpt.imag(a)))
return dpt.any(dpt.isnan(a))

# 5 distinct finite values followed by 2 NaNs, then rolled to interleave
x = dpt.roll(dpt.asarray([3, 1, 5, 2, 4, nan, nan], dtype=dtype), 3)

# k within the finite region: NaNs are excluded from the result
r = dpt.top_k(x, 3, mode=mode)
assert not has_nan(r.values)
assert dpt.all(r.values == x[r.indices])
extreme = [5, 4, 3] if mode == "largest" else [1, 2, 3]
expected = dpt.asarray(extreme, dtype=dtype)
assert dpt.all(dpt.sort(r.values) == dpt.sort(expected))

# k reaching into the NaN region: the 2 NaNs are ordered last
r = dpt.top_k(x, 7, mode=mode)
assert has_nan(r.values[-2:])
assert not has_nan(r.values[:5])


@pytest.mark.parametrize(
"dtype",
[
Expand Down
78 changes: 55 additions & 23 deletions dpnp/tests/test_sort.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,26 @@ def test_kind(self, kind):
expected = numpy.argsort(a, kind="stable")
assert_array_equal(result, expected)

@testing.with_requires("numpy>=2.5")
@pytest.mark.parametrize("descending", [False, True])
def test_descending(self, descending):
a = numpy.repeat(numpy.arange(10), 10)
@pytest.mark.parametrize(
"dtype", get_integer_dtypes(all_int_types=True) + [dpnp.bool]
)
def test_descending_duplicates(self, dtype, descending):
# a stable argsort keeps the original relative order of equal
# elements in both ascending and descending order
if dtype == dpnp.bool:
values = [False, True]
else:
info = numpy.iinfo(dtype)
values = [info.min, 1, info.max]
a = numpy.array(values * 2, dtype=dtype)
ia = dpnp.array(a)

result = dpnp.argsort(ia, descending=descending)
if not descending:
expected = numpy.argsort(a, kind="stable")
else:
expected = numpy.flip(numpy.argsort(numpy.flip(a), kind="stable"))
expected = (a.shape[0] - 1) - expected
assert_array_equal(result, expected)

# test ndarray method
result = ia.argsort(descending=descending)
if not descending:
expected = a.argsort(kind="stable")
else:
a = numpy.flip(a)
expected = numpy.flip(a.argsort(kind="stable"))
expected = (a.shape[0] - 1) - expected
expected = numpy.argsort(a, stable=True, descending=descending)
assert_array_equal(result, expected)
assert_array_equal(dpnp.sort(ia, descending=descending), a[expected])

# `stable` keyword is supported in numpy 2.0 and above
@testing.with_requires("numpy>=2.0")
Expand Down Expand Up @@ -545,24 +543,58 @@ def test_kind(self, kind):
expected = numpy.sort(a, kind="stable")
assert_array_equal(result, expected)

@testing.with_requires("numpy>=2.5")
@pytest.mark.parametrize("descending", [False, True])
def test_descending(self, descending):
a = numpy.repeat(numpy.arange(10), 10)
ia = dpnp.array(a)

result = dpnp.sort(ia, descending=descending)
expected = numpy.sort(a, kind="stable")
if descending:
expected = numpy.flip(expected)
expected = numpy.sort(a, stable=True, descending=descending)
assert_array_equal(result, expected)

# test ndarray method
ia.sort(descending=descending)
a.sort(kind="stable")
if descending:
a = numpy.flip(a)
a.sort(stable=True, descending=descending)
assert_array_equal(ia, a)

@testing.with_requires("numpy>=2.5")
@pytest.mark.parametrize("kind", [None, "stable", "mergesort", "radixsort"])
@pytest.mark.parametrize("descending", [False, True])
@pytest.mark.parametrize("dtype", get_float_dtypes(no_float16=False))
Comment thread
antonwolfy marked this conversation as resolved.
def test_descending_nan(self, dtype, descending, kind):
# NaNs are sorted to the end for both ascending and descending order
a = numpy.linspace(-50, 50, 101).astype(dtype)
a[::10] = numpy.nan
ia = dpnp.array(a)

result = dpnp.sort(ia, descending=descending, kind=kind)
expected = numpy.sort(a, stable=True, descending=descending)
assert_array_equal(result, expected)

@testing.with_requires("numpy>=2.5")
@pytest.mark.parametrize("descending", [False, True])
@pytest.mark.parametrize("dtype", get_complex_dtypes())
def test_descending_complex_nan(self, dtype, descending):
# NaN-containing complex values sort to the end in groups
# (no nan) -> (imag nan) -> (real nan) -> (all nan) for both orders;
# finite values keep lexicographic order (real part more significant)
arange = numpy.tile(numpy.arange(25), 4)
no_nans = arange + 1j * arange
im_nans = arange + complex(0, numpy.nan)
re_nans = complex(numpy.nan, 0) + 1j * arange
all_nans = numpy.full(100, complex(numpy.nan, numpy.nan))
a = numpy.concatenate((no_nans, im_nans, re_nans, all_nans))
a = a.astype(dtype)

rng = numpy.random.default_rng(0)
rng.shuffle(a)
ia = dpnp.array(a)

result = dpnp.sort(ia, descending=descending)
expected = numpy.sort(a, stable=True, descending=descending)
assert_array_equal(result, expected)

# `stable` keyword is supported in numpy 2.0 and above
@testing.with_requires("numpy>=2.0")
@pytest.mark.parametrize("stable", [None, False, True])
Expand Down
Loading
Loading