From d76dbb88a757b6e7dda6b82e70d65c4b863fbc1f Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 18 Jun 2026 11:15:02 +0200 Subject: [PATCH 1/4] Always return unitary vh even when input array is singular --- dpnp/linalg/dpnp_utils_linalg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dpnp/linalg/dpnp_utils_linalg.py b/dpnp/linalg/dpnp_utils_linalg.py index ee2023befc2..2a4600a5973 100644 --- a/dpnp/linalg/dpnp_utils_linalg.py +++ b/dpnp/linalg/dpnp_utils_linalg.py @@ -914,7 +914,8 @@ def _hermitian_svd(a, compute_uv): # the eigenvalues and related arrays to have the correct order if compute_uv: s, u = dpnp_eigh(a, eigen_mode="V") - sgn = dpnp.sign(s) + # avoid zero sign + sgn = dpnp.copysign(1.0, s) s = dpnp.abs(s, out=s) sidx = dpnp.argsort(s)[..., ::-1] # Rearrange the signs according to sorted indices From c71fc84e6ae5ac1b8b084c9c9ce9e24892427eba Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 18 Jun 2026 11:36:15 +0200 Subject: [PATCH 2/4] Add dedicated test to cover the use case --- dpnp/tests/test_linalg.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/dpnp/tests/test_linalg.py b/dpnp/tests/test_linalg.py index 723b1dcedc4..6fb9e911aaf 100644 --- a/dpnp/tests/test_linalg.py +++ b/dpnp/tests/test_linalg.py @@ -27,6 +27,8 @@ from .qr_helper import check_qr from .third_party.cupy import testing +ALL_DTYPES_NO_BOOL = get_all_dtypes(no_none=True, no_bool=True) + def vvsort(val, vec, size, xp): val_kwargs = {} @@ -464,8 +466,6 @@ def test_det_errors(self): class TestEigenvalue: - ALL_DTYPES_NO_BOOL = get_all_dtypes(no_none=True, no_bool=True) - # Eigenvalue decomposition of a matrix or a batch of matrices # by checking if the eigen equation A*v=w*v holds for given eigenvalues(w) # and eigenvectors(v). @@ -3974,7 +3974,7 @@ def get_tol(self, dtype): # Additionally checks for equality of singular values # between dpnp and numpy decompositions def check_decomposition( - self, dp_a, dp_u, dp_s, dp_vt, np_u, np_s, np_vt, compute_vt + self, dp_a, dp_u, dp_s, dp_vt, np_u, np_s, np_vt, compute_vt=False ): tol = self._tol if compute_vt: @@ -4006,13 +4006,13 @@ def check_decomposition( atol=tol, ) - @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("dtype", ALL_DTYPES_NO_BOOL) @pytest.mark.parametrize( "shape", [(2, 2), (3, 4), (5, 3), (16, 16)], ids=["(2, 2)", "(3, 4)", "(5, 3)", "(16, 16)"], ) - def test_svd(self, dtype, shape): + def test_basic(self, dtype, shape): a = numpy.arange(shape[0] * shape[1], dtype=dtype).reshape(shape) dp_a = dpnp.array(a) @@ -4030,7 +4030,7 @@ def test_svd(self, dtype, shape): @pytest.mark.parametrize( "shape", [(2, 2), (16, 16)], ids=["(2, 2)", "(16, 16)"] ) - def test_svd_hermitian(self, dtype, compute_vt, shape): + def test_hermitian(self, dtype, compute_vt, shape): a = generate_random_numpy_array(shape, dtype, hermitian=True) dp_a = dpnp.array(a) @@ -4053,6 +4053,15 @@ def test_svd_hermitian(self, dtype, compute_vt, shape): dp_a, dp_u, dp_s, dp_vh, np_u, np_s, np_vh, compute_vt ) + def test_hermitian_singular(self): + a = numpy.array([[1, 0], [0, 0]]) + dp_a = dpnp.array(a) + + np_u, _, np_vh = numpy.linalg.svd(a, hermitian=True) + u, _, vh = dpnp.linalg.svd(dp_a, hermitian=True) + assert_allclose(u, np_u) + assert_allclose(vh, np_vh) + def test_svd_errors(self): a_dp = dpnp.array([[1, 2], [3, 4]], dtype="float32") From 4507d5856e1fdf0fa51ee83a611cf26384f90ae6 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 18 Jun 2026 11:37:43 +0200 Subject: [PATCH 3/4] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f98f0f4108..5162e4fcdfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ This release is compatible with NumPy 2.5. * Fixed incorrect `dpnp.tensor.expm1` result for `complex(±0, 0)` special case on CPU to match the Python Array API specification [#2926](https://github.com/IntelPython/dpnp/pull/2926) * Fixed tests which expected lists from `dpctl` functions which now return tuples (i.e., `dpctl.SyclDevice.create_sub_devices`) [#2945](https://github.com/IntelPython/dpnp/pull/2945) * Fixed `PytestRemovedIn10Warning` raised by `pytest` 9.1.0 by converting class-scoped fixtures to class methods [#2952](https://github.com/IntelPython/dpnp/pull/2952) +* Fixed `dpnp.linalg.svd(..., hermitian=True)` returning a non-unitary `vh` for singular input arrays due to a zero sign appearing [#2954](https://github.com/IntelPython/dpnp/pull/2954) ### Security From c6c3c413a373d587cb6d7f708bd51cc3e607995b Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 18 Jun 2026 11:42:48 +0200 Subject: [PATCH 4/4] Limit test to run only with NumPy 2.5 where the fix is present --- dpnp/tests/test_linalg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dpnp/tests/test_linalg.py b/dpnp/tests/test_linalg.py index 6fb9e911aaf..302a722bd1b 100644 --- a/dpnp/tests/test_linalg.py +++ b/dpnp/tests/test_linalg.py @@ -4053,6 +4053,7 @@ def test_hermitian(self, dtype, compute_vt, shape): dp_a, dp_u, dp_s, dp_vh, np_u, np_s, np_vh, compute_vt ) + @testing.with_requires("numpy>=2.5") def test_hermitian_singular(self): a = numpy.array([[1, 0], [0, 0]]) dp_a = dpnp.array(a)