From 8cb6d296d1dd7dc85d461925c84f24fa7cfcf29d Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Tue, 24 Feb 2026 01:58:05 -0800 Subject: [PATCH 1/6] Unskip qr tests --- .../third_party/cupy/linalg_tests/test_decomposition.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py b/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py index c7ff275cac0..bbcdcea0de7 100644 --- a/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py +++ b/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py @@ -202,9 +202,6 @@ def test_mode(self): self.check_mode(numpy.random.randn(3, 3), mode=self.mode) self.check_mode(numpy.random.randn(5, 4), mode=self.mode) - @pytest.mark.skipif( - is_lts_driver(version=LTS_VERSION.V1_6), reason="SAT-8375" - ) @testing.with_requires("numpy>=1.22") @testing.fix_random() def test_mode_rank3(self): @@ -212,9 +209,6 @@ def test_mode_rank3(self): self.check_mode(numpy.random.randn(4, 3, 3), mode=self.mode) self.check_mode(numpy.random.randn(2, 5, 4), mode=self.mode) - @pytest.mark.skipif( - is_lts_driver(version=LTS_VERSION.V1_6), reason="SAT-8375" - ) @testing.with_requires("numpy>=1.22") @testing.fix_random() def test_mode_rank4(self): From 6c894c9eecdab3a1ad287b6ebb5c4753290b8e0e Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Thu, 26 Feb 2026 05:30:39 -0800 Subject: [PATCH 2/6] Update QR tests to avoid element-wise comparisons --- dpnp/tests/test_linalg.py | 177 +++++++++--------- .../cupy/linalg_tests/test_decomposition.py | 80 ++++++-- 2 files changed, 159 insertions(+), 98 deletions(-) diff --git a/dpnp/tests/test_linalg.py b/dpnp/tests/test_linalg.py index 31d99d71ce4..1b41e5b7970 100644 --- a/dpnp/tests/test_linalg.py +++ b/dpnp/tests/test_linalg.py @@ -3135,6 +3135,85 @@ def test_error(self): class TestQr: + def gram(self, X, xp): + # Return Gram matrix: X^H @ X + return xp.conjugate(X).swapaxes(-1, -2) @ X + + def get_R_from_raw(self, h, m, n, xp): + # Get reduced R from NumPy-style raw QR: + # R = triu((tril(h))^T), shape (..., k, n) + k = min(m, n) + Rt = xp.tril(h) + R = xp.swapaxes(Rt, -1, -2) + R = xp.triu(R[..., :m, :n]) + + return R[..., :k, :] + + # QR is not unique: + # element-wise comparison with NumPy may differ by sign/phase. + # To verify correctness use mode-dependent functional checks: + # complete/reduced: check decomposition Q @ R = A + # raw/r: check invariant R^H @ R = A^H @ A + def check_qr(self, a_np, a_dp, mode): + if mode in ("complete", "reduced"): + res = dpnp.linalg.qr(a_dp, mode) + assert dpnp.allclose(res.Q @ res.R, a_dp, atol=1e-5) + + # Since QR satisfies A = Q @ R with orthonormal Q (Q^H @ Q = I), + # validate correctness via the invariant R^H @ R == A^H @ A + # for raw/r modes + elif mode == "raw": + h_np, tau_np = numpy.linalg.qr(a_np, mode=mode) + h_dp, tau_dp = dpnp.linalg.qr(a_dp, mode=mode) + + m, n = a_np.shape[-2], a_np.shape[-1] + Rraw_np = self.get_R_from_raw(h_np, m, n, numpy) + Rraw_dp = self.get_R_from_raw(h_dp, m, n, dpnp) + + # Use reduced QR as a reference: + # reduced is validated via Q @ R == A + exp_res = dpnp.linalg.qr(a_dp, mode="reduced") + exp_R = exp_res.R + assert_allclose(Rraw_dp, exp_R, atol=1e-4, rtol=1e-4) + + exp_dp = self.gram(a_dp, dpnp).astype(Rraw_dp.dtype) + exp_np = self.gram(a_np, numpy).astype(Rraw_np.dtype) + + # compare R^H @ R == A^H @ A + assert_allclose( + self.gram(Rraw_dp, dpnp), exp_dp, atol=1e-4, rtol=1e-4 + ) + assert_allclose( + self.gram(Rraw_np, numpy), exp_np, atol=1e-4, rtol=1e-4 + ) + + assert tau_dp.shape == tau_np.shape + if has_support_aspect64(tau_dp.sycl_device): + if tau_np.dtype == numpy.float64: + tau_np = tau_np.astype("float32") + elif tau_np.dtype == numpy.complex128: + tau_np = tau_np.astype("complex64") + assert tau_dp.dtype == tau_np.dtypes + + else: # mode == "r" + R_np = numpy.linalg.qr(a_np, mode="r") + R_dp = dpnp.linalg.qr(a_dp, mode="r") + + # Use reduced QR as a reference: + # reduced is validated via Q @ R == A + exp_res = dpnp.linalg.qr(a_dp, mode="reduced") + exp_R = exp_res.R + assert_allclose(R_dp, exp_R, atol=1e-4, rtol=1e-4) + + exp_dp = self.gram(a_dp, dpnp).astype(R_dp.dtype) + exp_np = self.gram(a_np, numpy).astype(R_np.dtype) + + # compare R^H @ R == A^H @ A + assert_allclose(self.gram(R_dp, dpnp), exp_dp, atol=1e-4, rtol=1e-4) + assert_allclose( + self.gram(R_np, numpy), exp_np, atol=1e-4, rtol=1e-4 + ) + @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) @pytest.mark.parametrize( "shape", @@ -3161,30 +3240,12 @@ class TestQr: "(2, 2, 4)", ], ) - @pytest.mark.parametrize("mode", ["r", "raw", "complete", "reduced"]) + @pytest.mark.parametrize("mode", ["complete", "reduced", "r", "raw"]) def test_qr(self, dtype, shape, mode): - a = generate_random_numpy_array(shape, dtype, seed_value=81) - ia = dpnp.array(a) - - if mode == "r": - np_r = numpy.linalg.qr(a, mode) - dpnp_r = dpnp.linalg.qr(ia, mode) - else: - np_q, np_r = numpy.linalg.qr(a, mode) - - # check decomposition - if mode in ("complete", "reduced"): - result = dpnp.linalg.qr(ia, mode) - dpnp_q, dpnp_r = result.Q, result.R - assert dpnp.allclose( - dpnp.matmul(dpnp_q, dpnp_r), ia, atol=1e-05 - ) - else: # mode=="raw" - dpnp_q, dpnp_r = dpnp.linalg.qr(ia, mode) - assert_dtype_allclose(dpnp_q, np_q, factor=24) + a = generate_random_numpy_array(shape, dtype, seed_value=None) + ia = dpnp.array(a, dtype=dtype) - if mode in ("raw", "r"): - assert_dtype_allclose(dpnp_r, np_r, factor=24) + self.check_qr(a, ia, mode) @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) @pytest.mark.parametrize( @@ -3192,27 +3253,12 @@ def test_qr(self, dtype, shape, mode): [(32, 32), (8, 16, 16)], ids=["(32, 32)", "(8, 16, 16)"], ) - @pytest.mark.parametrize("mode", ["r", "raw", "complete", "reduced"]) + @pytest.mark.parametrize("mode", ["complete", "reduced", "r", "raw"]) def test_qr_large(self, dtype, shape, mode): a = generate_random_numpy_array(shape, dtype, seed_value=81) ia = dpnp.array(a) - if mode == "r": - np_r = numpy.linalg.qr(a, mode) - dpnp_r = dpnp.linalg.qr(ia, mode) - else: - np_q, np_r = numpy.linalg.qr(a, mode) - - # check decomposition - if mode in ("complete", "reduced"): - result = dpnp.linalg.qr(ia, mode) - dpnp_q, dpnp_r = result.Q, result.R - assert dpnp.allclose(dpnp.matmul(dpnp_q, dpnp_r), ia, atol=1e-5) - else: # mode=="raw" - dpnp_q, dpnp_r = dpnp.linalg.qr(ia, mode) - assert_allclose(dpnp_q, np_q, atol=1e-4) - if mode in ("raw", "r"): - assert_allclose(dpnp_r, np_r, atol=1e-4) + self.check_qr(a, ia, mode) @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) @pytest.mark.parametrize( @@ -3227,65 +3273,22 @@ def test_qr_large(self, dtype, shape, mode): "(0, 2, 3)", ], ) - @pytest.mark.parametrize("mode", ["r", "raw", "complete", "reduced"]) + @pytest.mark.parametrize("mode", ["complete", "reduced", "r", "raw"]) def test_qr_empty(self, dtype, shape, mode): a = numpy.empty(shape, dtype=dtype) ia = dpnp.array(a) - if mode == "r": - np_r = numpy.linalg.qr(a, mode) - dpnp_r = dpnp.linalg.qr(ia, mode) - else: - np_q, np_r = numpy.linalg.qr(a, mode) + self.check_qr(a, ia, mode) - if mode in ("complete", "reduced"): - result = dpnp.linalg.qr(ia, mode) - dpnp_q, dpnp_r = result.Q, result.R - else: - dpnp_q, dpnp_r = dpnp.linalg.qr(ia, mode) - - assert_dtype_allclose(dpnp_q, np_q) - - assert_dtype_allclose(dpnp_r, np_r) - - @pytest.mark.parametrize("mode", ["r", "raw", "complete", "reduced"]) + @pytest.mark.parametrize("mode", ["complete", "reduced", "r", "raw"]) def test_qr_strides(self, mode): a = generate_random_numpy_array((5, 5)) ia = dpnp.array(a) # positive strides - if mode == "r": - np_r = numpy.linalg.qr(a[::2, ::2], mode) - dpnp_r = dpnp.linalg.qr(ia[::2, ::2], mode) - else: - np_q, np_r = numpy.linalg.qr(a[::2, ::2], mode) - - if mode in ("complete", "reduced"): - result = dpnp.linalg.qr(ia[::2, ::2], mode) - dpnp_q, dpnp_r = result.Q, result.R - else: - dpnp_q, dpnp_r = dpnp.linalg.qr(ia[::2, ::2], mode) - - assert_dtype_allclose(dpnp_q, np_q) - - assert_dtype_allclose(dpnp_r, np_r) - + self.check_qr(a[::2, ::2], ia[::2, ::2], mode) # negative strides - if mode == "r": - np_r = numpy.linalg.qr(a[::-2, ::-2], mode) - dpnp_r = dpnp.linalg.qr(ia[::-2, ::-2], mode) - else: - np_q, np_r = numpy.linalg.qr(a[::-2, ::-2], mode) - - if mode in ("complete", "reduced"): - result = dpnp.linalg.qr(ia[::-2, ::-2], mode) - dpnp_q, dpnp_r = result.Q, result.R - else: - dpnp_q, dpnp_r = dpnp.linalg.qr(ia[::-2, ::-2], mode) - - assert_dtype_allclose(dpnp_q, np_q) - - assert_dtype_allclose(dpnp_r, np_r) + self.check_qr(a[::-2, ::-2], ia[::-2, ::-2], mode) def test_qr_errors(self): a_dp = dpnp.array([[1, 2], [3, 5]], dtype="float32") diff --git a/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py b/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py index bbcdcea0de7..43e7db06ced 100644 --- a/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py +++ b/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py @@ -12,9 +12,7 @@ # from cupy.cuda import runtime # from cupy.linalg import _util from dpnp.tests.helper import ( - LTS_VERSION, has_support_aspect64, - is_lts_driver, ) from dpnp.tests.third_party.cupy import testing from dpnp.tests.third_party.cupy.testing import _condition @@ -169,6 +167,18 @@ def test_decomposition(self, dtype): ) ) class TestQRDecomposition(unittest.TestCase): + def _gram(self, x, xp): + # Gram matrix: X^H @ X + return xp.conjugate(x).swapaxes(-1, -2) @ x + + def _get_R_from_raw(self, h, m, n, xp): + # Get reduced R from NumPy-style raw QR: + # R = triu((tril(h))^T), shape (..., k, n) + k = min(m, n) + Rt = xp.tril(h) + R = xp.swapaxes(Rt, -1, -2) + R = xp.triu(R[..., :m, :n]) + return R[..., :k, :] @testing.for_dtypes("fdFD") def check_mode(self, array, mode, dtype): @@ -184,16 +194,64 @@ def check_mode(self, array, mode, dtype): or numpy.lib.NumpyVersion(numpy.__version__) >= "1.22.0rc1" ): result_cpu = numpy.linalg.qr(a_cpu, mode=mode) - self._check_result(result_cpu, result_gpu) + self._check_result(result_cpu, result_gpu, a_cpu, a_gpu, mode) + + # def _check_result(self, result_cpu, result_gpu): + # if isinstance(result_cpu, tuple): + # for b_cpu, b_gpu in zip(result_cpu, result_gpu): + # assert b_cpu.dtype == b_gpu.dtype + # testing.assert_allclose(b_cpu, b_gpu, atol=1e-4) + # else: + # assert result_cpu.dtype == result_gpu.dtype + # testing.assert_allclose(result_cpu, result_gpu, atol=1e-4) + + # QR is not unique: + # element-wise comparison with NumPy may differ by sign/phase. + # To verify correctness use mode-dependent functional checks: + # complete/reduced: check decomposition Q @ R = A + # raw/r: check invariant R^H @ R = A^H @ A + def _check_result(self, result_cpu, result_gpu, a_cpu, a_gpu, mode): + + if mode in ("complete", "reduced"): + q_gpu, r_gpu = result_gpu + testing.assert_allclose(q_gpu @ r_gpu, a_gpu, atol=1e-4) + + elif mode == "raw": + h_gpu, tau_gpu = result_gpu + h_cpu, tau_cpu = result_cpu + m, n = a_gpu.shape[-2], a_gpu.shape[-1] + r_gpu = self._get_R_from_raw(h_gpu, m, n, cupy) + r_cpu = self._get_R_from_raw(h_cpu, m, n, numpy) + + exp_gpu = self._gram(a_gpu, cupy) + exp_cpu = self._gram(a_cpu, numpy) + + testing.assert_allclose( + self._gram(r_gpu, cupy), exp_gpu, atol=1e-4, rtol=1e-4 + ) + testing.assert_allclose( + self._gram(r_cpu, numpy), exp_cpu, atol=1e-4, rtol=1e-4 + ) - def _check_result(self, result_cpu, result_gpu): - if isinstance(result_cpu, tuple): - for b_cpu, b_gpu in zip(result_cpu, result_gpu): - assert b_cpu.dtype == b_gpu.dtype - testing.assert_allclose(b_cpu, b_gpu, atol=1e-4) - else: - assert result_cpu.dtype == result_gpu.dtype - testing.assert_allclose(result_cpu, result_gpu, atol=1e-4) + assert tau_gpu.shape == tau_cpu.shape + if not has_support_aspect64(tau_gpu.sycl_device): + if tau_cpu.dtype == numpy.float64: + tau_cpu = tau_cpu.astype("float32") + elif tau_cpu.dtype == numpy.complex128: + tau_cpu = tau_cpu.astype("complex64") + assert tau_gpu.dtype == tau_cpu.dtype + + else: # mode == "r" + r_gpu = result_gpu + r_cpu = result_cpu + exp_gpu = self._gram(a_gpu, cupy) + exp_cpu = self._gram(a_cpu, numpy) + testing.assert_allclose( + self._gram(r_gpu, cupy), exp_gpu, atol=1e-4, rtol=1e-4 + ) + testing.assert_allclose( + self._gram(r_cpu, numpy), exp_cpu, atol=1e-4, rtol=1e-4 + ) @testing.fix_random() @_condition.repeat(3, 10) From 26d213a675b4ad8202a6660d02fbd983c8082c36 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Thu, 26 Feb 2026 06:30:20 -0800 Subject: [PATCH 3/6] Fix typos --- dpnp/tests/test_linalg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dpnp/tests/test_linalg.py b/dpnp/tests/test_linalg.py index 1b41e5b7970..2aaeb185074 100644 --- a/dpnp/tests/test_linalg.py +++ b/dpnp/tests/test_linalg.py @@ -3188,12 +3188,12 @@ def check_qr(self, a_np, a_dp, mode): ) assert tau_dp.shape == tau_np.shape - if has_support_aspect64(tau_dp.sycl_device): + if not has_support_aspect64(tau_dp.sycl_device): if tau_np.dtype == numpy.float64: tau_np = tau_np.astype("float32") elif tau_np.dtype == numpy.complex128: tau_np = tau_np.astype("complex64") - assert tau_dp.dtype == tau_np.dtypes + assert tau_dp.dtype == tau_np.dtype else: # mode == "r" R_np = numpy.linalg.qr(a_np, mode="r") From 706b3e2f18e8d483dea55ea6a3b78c0703970755 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Thu, 26 Feb 2026 09:50:18 -0800 Subject: [PATCH 4/6] Run TestQr only for float and complex dtypes --- dpnp/tests/test_linalg.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dpnp/tests/test_linalg.py b/dpnp/tests/test_linalg.py index 2aaeb185074..a693299d854 100644 --- a/dpnp/tests/test_linalg.py +++ b/dpnp/tests/test_linalg.py @@ -3205,8 +3205,8 @@ def check_qr(self, a_np, a_dp, mode): exp_R = exp_res.R assert_allclose(R_dp, exp_R, atol=1e-4, rtol=1e-4) - exp_dp = self.gram(a_dp, dpnp).astype(R_dp.dtype) - exp_np = self.gram(a_np, numpy).astype(R_np.dtype) + exp_dp = self.gram(a_dp, dpnp) + exp_np = self.gram(a_np, numpy) # compare R^H @ R == A^H @ A assert_allclose(self.gram(R_dp, dpnp), exp_dp, atol=1e-4, rtol=1e-4) @@ -3214,7 +3214,7 @@ def check_qr(self, a_np, a_dp, mode): self.gram(R_np, numpy), exp_np, atol=1e-4, rtol=1e-4 ) - @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) @pytest.mark.parametrize( "shape", [ @@ -3247,7 +3247,7 @@ def test_qr(self, dtype, shape, mode): self.check_qr(a, ia, mode) - @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) @pytest.mark.parametrize( "shape", [(32, 32), (8, 16, 16)], @@ -3260,7 +3260,7 @@ def test_qr_large(self, dtype, shape, mode): self.check_qr(a, ia, mode) - @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) @pytest.mark.parametrize( "shape", [(0, 0), (0, 2), (2, 0), (2, 0, 3), (2, 3, 0), (0, 2, 3)], From 707c5a0fa39c8d8d2cf39c01ac61c4129cb7d169 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 9 Mar 2026 05:32:50 -0700 Subject: [PATCH 5/6] Move QR helper functions to qr_helper.py --- dpnp/tests/qr_helper.py | 70 +++++++++++++++ dpnp/tests/test_linalg.py | 90 ++----------------- .../cupy/linalg_tests/test_decomposition.py | 75 +++------------- 3 files changed, 87 insertions(+), 148 deletions(-) create mode 100644 dpnp/tests/qr_helper.py diff --git a/dpnp/tests/qr_helper.py b/dpnp/tests/qr_helper.py new file mode 100644 index 00000000000..640e2ecbaea --- /dev/null +++ b/dpnp/tests/qr_helper.py @@ -0,0 +1,70 @@ +import numpy + +from .helper import has_support_aspect64 + + +def gram(x, xp): + # Return Gram matrix: X^H @ X + return xp.conjugate(x).swapaxes(-1, -2) @ x + + +def get_R_from_raw(h, m, n, xp): + # Get reduced R from NumPy-style raw QR: + # R = triu((tril(h))^T), shape (..., k, n) + k = min(m, n) + rt = xp.tril(h) + r = xp.swapaxes(rt, -1, -2) + r = xp.triu(r[..., :m, :n]) + return r[..., :k, :] + + +def check_qr(a_np, a_xp, mode, xp): + # QR is not unique: + # element-wise comparison with NumPy may differ by sign/phase. + # To verify correctness use mode-dependent functional checks: + # complete/reduced: check decomposition Q @ R = A + # raw/r: check invariant R^H @ R = A^H @ A + if mode in ("complete", "reduced"): + res = xp.linalg.qr(a_xp, mode) + assert xp.allclose(res.Q @ res.R, a_xp, atol=1e-5) + + # Since QR satisfies A = Q @ R with orthonormal Q (Q^H @ Q = I), + # validate correctness via the invariant R^H @ R == A^H @ A + # for raw/r modes + elif mode == "raw": + _, tau_np = numpy.linalg.qr(a_np, mode=mode) + h_xp, tau_xp = xp.linalg.qr(a_xp, mode=mode) + + m, n = a_np.shape[-2], a_np.shape[-1] + Rraw_xp = get_R_from_raw(h_xp, m, n, xp) + + # Use reduced QR as a reference: + # reduced is validated via Q @ R == A + exp_res = xp.linalg.qr(a_xp, mode="reduced") + exp_r = exp_res.R + assert xp.allclose(Rraw_xp, exp_r, atol=1e-4, rtol=1e-4) + + exp_xp = gram(a_xp, xp) + + # Compare R^H @ R == A^H @ A + assert xp.allclose(gram(Rraw_xp, xp), exp_xp, atol=1e-4, rtol=1e-4) + + assert tau_xp.shape == tau_np.shape + if not has_support_aspect64(tau_xp.sycl_device): + assert tau_xp.dtype.kind == tau_np.dtype.kind + else: + assert tau_xp.dtype == tau_np.dtype + + else: # mode == "r" + r_xp = xp.linalg.qr(a_xp, mode="r") + + # Use reduced QR as a reference: + # reduced is validated via Q @ R == A + exp_res = xp.linalg.qr(a_xp, mode="reduced") + exp_r = exp_res.R + assert xp.allclose(r_xp, exp_r, atol=1e-4, rtol=1e-4) + + exp_xp = gram(a_xp, xp) + + # Compare R^H @ R == A^H @ A + assert xp.allclose(gram(r_xp, xp), exp_xp, atol=1e-4, rtol=1e-4) diff --git a/dpnp/tests/test_linalg.py b/dpnp/tests/test_linalg.py index a693299d854..17184136c26 100644 --- a/dpnp/tests/test_linalg.py +++ b/dpnp/tests/test_linalg.py @@ -25,6 +25,7 @@ has_support_aspect64, numpy_version, ) +from .qr_helper import check_qr from .third_party.cupy import testing @@ -3135,85 +3136,6 @@ def test_error(self): class TestQr: - def gram(self, X, xp): - # Return Gram matrix: X^H @ X - return xp.conjugate(X).swapaxes(-1, -2) @ X - - def get_R_from_raw(self, h, m, n, xp): - # Get reduced R from NumPy-style raw QR: - # R = triu((tril(h))^T), shape (..., k, n) - k = min(m, n) - Rt = xp.tril(h) - R = xp.swapaxes(Rt, -1, -2) - R = xp.triu(R[..., :m, :n]) - - return R[..., :k, :] - - # QR is not unique: - # element-wise comparison with NumPy may differ by sign/phase. - # To verify correctness use mode-dependent functional checks: - # complete/reduced: check decomposition Q @ R = A - # raw/r: check invariant R^H @ R = A^H @ A - def check_qr(self, a_np, a_dp, mode): - if mode in ("complete", "reduced"): - res = dpnp.linalg.qr(a_dp, mode) - assert dpnp.allclose(res.Q @ res.R, a_dp, atol=1e-5) - - # Since QR satisfies A = Q @ R with orthonormal Q (Q^H @ Q = I), - # validate correctness via the invariant R^H @ R == A^H @ A - # for raw/r modes - elif mode == "raw": - h_np, tau_np = numpy.linalg.qr(a_np, mode=mode) - h_dp, tau_dp = dpnp.linalg.qr(a_dp, mode=mode) - - m, n = a_np.shape[-2], a_np.shape[-1] - Rraw_np = self.get_R_from_raw(h_np, m, n, numpy) - Rraw_dp = self.get_R_from_raw(h_dp, m, n, dpnp) - - # Use reduced QR as a reference: - # reduced is validated via Q @ R == A - exp_res = dpnp.linalg.qr(a_dp, mode="reduced") - exp_R = exp_res.R - assert_allclose(Rraw_dp, exp_R, atol=1e-4, rtol=1e-4) - - exp_dp = self.gram(a_dp, dpnp).astype(Rraw_dp.dtype) - exp_np = self.gram(a_np, numpy).astype(Rraw_np.dtype) - - # compare R^H @ R == A^H @ A - assert_allclose( - self.gram(Rraw_dp, dpnp), exp_dp, atol=1e-4, rtol=1e-4 - ) - assert_allclose( - self.gram(Rraw_np, numpy), exp_np, atol=1e-4, rtol=1e-4 - ) - - assert tau_dp.shape == tau_np.shape - if not has_support_aspect64(tau_dp.sycl_device): - if tau_np.dtype == numpy.float64: - tau_np = tau_np.astype("float32") - elif tau_np.dtype == numpy.complex128: - tau_np = tau_np.astype("complex64") - assert tau_dp.dtype == tau_np.dtype - - else: # mode == "r" - R_np = numpy.linalg.qr(a_np, mode="r") - R_dp = dpnp.linalg.qr(a_dp, mode="r") - - # Use reduced QR as a reference: - # reduced is validated via Q @ R == A - exp_res = dpnp.linalg.qr(a_dp, mode="reduced") - exp_R = exp_res.R - assert_allclose(R_dp, exp_R, atol=1e-4, rtol=1e-4) - - exp_dp = self.gram(a_dp, dpnp) - exp_np = self.gram(a_np, numpy) - - # compare R^H @ R == A^H @ A - assert_allclose(self.gram(R_dp, dpnp), exp_dp, atol=1e-4, rtol=1e-4) - assert_allclose( - self.gram(R_np, numpy), exp_np, atol=1e-4, rtol=1e-4 - ) - @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) @pytest.mark.parametrize( "shape", @@ -3245,7 +3167,7 @@ def test_qr(self, dtype, shape, mode): a = generate_random_numpy_array(shape, dtype, seed_value=None) ia = dpnp.array(a, dtype=dtype) - self.check_qr(a, ia, mode) + check_qr(a, ia, mode, dpnp) @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) @pytest.mark.parametrize( @@ -3258,7 +3180,7 @@ def test_qr_large(self, dtype, shape, mode): a = generate_random_numpy_array(shape, dtype, seed_value=81) ia = dpnp.array(a) - self.check_qr(a, ia, mode) + check_qr(a, ia, mode, dpnp) @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) @pytest.mark.parametrize( @@ -3278,7 +3200,7 @@ def test_qr_empty(self, dtype, shape, mode): a = numpy.empty(shape, dtype=dtype) ia = dpnp.array(a) - self.check_qr(a, ia, mode) + check_qr(a, ia, mode, dpnp) @pytest.mark.parametrize("mode", ["complete", "reduced", "r", "raw"]) def test_qr_strides(self, mode): @@ -3286,9 +3208,9 @@ def test_qr_strides(self, mode): ia = dpnp.array(a) # positive strides - self.check_qr(a[::2, ::2], ia[::2, ::2], mode) + check_qr(a[::2, ::2], ia[::2, ::2], mode, dpnp) # negative strides - self.check_qr(a[::-2, ::-2], ia[::-2, ::-2], mode) + check_qr(a[::-2, ::-2], ia[::-2, ::-2], mode, dpnp) def test_qr_errors(self): a_dp = dpnp.array([[1, 2], [3, 5]], dtype="float32") diff --git a/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py b/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py index 43e7db06ced..697e4ee7988 100644 --- a/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py +++ b/dpnp/tests/third_party/cupy/linalg_tests/test_decomposition.py @@ -14,6 +14,7 @@ from dpnp.tests.helper import ( has_support_aspect64, ) +from dpnp.tests.qr_helper import check_qr from dpnp.tests.third_party.cupy import testing from dpnp.tests.third_party.cupy.testing import _condition @@ -167,19 +168,6 @@ def test_decomposition(self, dtype): ) ) class TestQRDecomposition(unittest.TestCase): - def _gram(self, x, xp): - # Gram matrix: X^H @ X - return xp.conjugate(x).swapaxes(-1, -2) @ x - - def _get_R_from_raw(self, h, m, n, xp): - # Get reduced R from NumPy-style raw QR: - # R = triu((tril(h))^T), shape (..., k, n) - k = min(m, n) - Rt = xp.tril(h) - R = xp.swapaxes(Rt, -1, -2) - R = xp.triu(R[..., :m, :n]) - return R[..., :k, :] - @testing.for_dtypes("fdFD") def check_mode(self, array, mode, dtype): # if runtime.is_hip and driver.get_build_version() < 307: @@ -188,13 +176,20 @@ def check_mode(self, array, mode, dtype): a_cpu = numpy.asarray(array, dtype=dtype) a_gpu = cupy.asarray(array, dtype=dtype) - result_gpu = cupy.linalg.qr(a_gpu, mode=mode) + # QR is not unique: + # element-wise comparison with NumPy may differ by sign/phase. + # To verify correctness use mode-dependent functional checks: + # complete/reduced: check decomposition Q @ R = A + # raw/r: check invariant R^H @ R = A^H @ A + + # result_gpu = cupy.linalg.qr(a_gpu, mode=mode) if ( mode != "raw" or numpy.lib.NumpyVersion(numpy.__version__) >= "1.22.0rc1" ): - result_cpu = numpy.linalg.qr(a_cpu, mode=mode) - self._check_result(result_cpu, result_gpu, a_cpu, a_gpu, mode) + # result_cpu = numpy.linalg.qr(a_cpu, mode=mode) + # self._check_result(result_cpu, result_gpu, a_gpu, mode) + check_qr(a_cpu, a_gpu, mode, cupy) # def _check_result(self, result_cpu, result_gpu): # if isinstance(result_cpu, tuple): @@ -205,54 +200,6 @@ def check_mode(self, array, mode, dtype): # assert result_cpu.dtype == result_gpu.dtype # testing.assert_allclose(result_cpu, result_gpu, atol=1e-4) - # QR is not unique: - # element-wise comparison with NumPy may differ by sign/phase. - # To verify correctness use mode-dependent functional checks: - # complete/reduced: check decomposition Q @ R = A - # raw/r: check invariant R^H @ R = A^H @ A - def _check_result(self, result_cpu, result_gpu, a_cpu, a_gpu, mode): - - if mode in ("complete", "reduced"): - q_gpu, r_gpu = result_gpu - testing.assert_allclose(q_gpu @ r_gpu, a_gpu, atol=1e-4) - - elif mode == "raw": - h_gpu, tau_gpu = result_gpu - h_cpu, tau_cpu = result_cpu - m, n = a_gpu.shape[-2], a_gpu.shape[-1] - r_gpu = self._get_R_from_raw(h_gpu, m, n, cupy) - r_cpu = self._get_R_from_raw(h_cpu, m, n, numpy) - - exp_gpu = self._gram(a_gpu, cupy) - exp_cpu = self._gram(a_cpu, numpy) - - testing.assert_allclose( - self._gram(r_gpu, cupy), exp_gpu, atol=1e-4, rtol=1e-4 - ) - testing.assert_allclose( - self._gram(r_cpu, numpy), exp_cpu, atol=1e-4, rtol=1e-4 - ) - - assert tau_gpu.shape == tau_cpu.shape - if not has_support_aspect64(tau_gpu.sycl_device): - if tau_cpu.dtype == numpy.float64: - tau_cpu = tau_cpu.astype("float32") - elif tau_cpu.dtype == numpy.complex128: - tau_cpu = tau_cpu.astype("complex64") - assert tau_gpu.dtype == tau_cpu.dtype - - else: # mode == "r" - r_gpu = result_gpu - r_cpu = result_cpu - exp_gpu = self._gram(a_gpu, cupy) - exp_cpu = self._gram(a_cpu, numpy) - testing.assert_allclose( - self._gram(r_gpu, cupy), exp_gpu, atol=1e-4, rtol=1e-4 - ) - testing.assert_allclose( - self._gram(r_cpu, numpy), exp_cpu, atol=1e-4, rtol=1e-4 - ) - @testing.fix_random() @_condition.repeat(3, 10) def test_mode(self): From e9973e6298e43b34fe7329b6f823d98e9b7e1114 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 9 Mar 2026 05:38:27 -0700 Subject: [PATCH 6/6] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61cde1ddfef..abd4f19b580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Also, that release drops support for Python 3.9, making Python 3.10 the minimum * Changed `dpnp.partition` implementation to reuse `dpnp.sort` where it brings the performance benefit [#2766](https://github.com/IntelPython/dpnp/pull/2766) * `dpnp` uses pybind11 3.0.2 [#27734](https://github.com/IntelPython/dpnp/pull/2773) * Modified CMake files for the extension to explicitly mark DPC++ compiler and dpctl headers as system ones and so to suppress the build warning generated inside them [#2770](https://github.com/IntelPython/dpnp/pull/2770) +* Updated QR tests to avoid element-wise comparisons for `raw` and `r` modes [#2785](https://github.com/IntelPython/dpnp/pull/2785) ### Deprecated