| // This file is part of Eigen, a lightweight C++ template library | |
| // for linear algebra. | |
| // | |
| // Copyright (C) 2026 Rasmus Munk Larsen <rmlarsen@gmail.com> | |
| // | |
| // This Source Code Form is subject to the terms of the Mozilla | |
| // Public License v. 2.0. If a copy of the MPL was not distributed | |
| // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // Tests for GpuQR: GPU QR decomposition via cuSOLVER. | |
| #define EIGEN_USE_GPU | |
| #include "main.h" | |
| #include <Eigen/QR> | |
| #include <Eigen/SVD> | |
| #include <contrib/Eigen/GPU> | |
| #include "./gpu_test_helpers.h" | |
| using namespace Eigen; | |
| // ---- Solve square system: A * X = B ----------------------------------------- | |
| template <typename Scalar> | |
| void test_qr_solve_square(Index n, Index nrhs) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(n, n); | |
| Mat B = Mat::Random(n, nrhs); | |
| gpu::QR<Scalar> qr(A); | |
| VERIFY_IS_EQUAL(qr.info(), Success); | |
| Mat X = qr.solve(B); | |
| RealScalar residual = (A * X - B).norm() / (A.norm() * X.norm()); | |
| VERIFY(residual < RealScalar(10) * RealScalar(n) * NumTraits<Scalar>::epsilon()); | |
| } | |
| // ---- Solve overdetermined system: m > n (least-squares) --------------------- | |
| template <typename Scalar> | |
| void test_qr_solve_overdetermined(Index m, Index n, Index nrhs) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| eigen_assert(m >= n); | |
| Mat A = Mat::Random(m, n); | |
| Mat B = Mat::Random(m, nrhs); | |
| gpu::QR<Scalar> qr(A); | |
| VERIFY_IS_EQUAL(qr.info(), Success); | |
| Mat X = qr.solve(B); | |
| VERIFY_IS_EQUAL(X.rows(), n); | |
| VERIFY_IS_EQUAL(X.cols(), nrhs); | |
| // For an overdetermined system the residual r = A X - B is generally large | |
| // (O(||B||) when B is not in col(A)). What backward-stable least-squares | |
| // makes small is the gradient of the LS objective, A^H r, which is zero at | |
| // the optimum. Higham's bound for QR-based LS gives ||A^H r|| <= O(m * eps) | |
| // * ||A|| * (||A|| ||X|| + ||B||), regardless of kappa(A). | |
| Mat X_cpu = HouseholderQR<Mat>(A).solve(B); | |
| RealScalar tol = RealScalar(10) * RealScalar(m) * NumTraits<Scalar>::epsilon(); | |
| RealScalar A_norm = A.norm(); | |
| RealScalar denom_gpu = A_norm * (A_norm * X.norm() + B.norm()); | |
| RealScalar denom_cpu = A_norm * (A_norm * X_cpu.norm() + B.norm()); | |
| VERIFY((A.adjoint() * (A * X - B)).norm() / denom_gpu < tol); | |
| VERIFY((A.adjoint() * (A * X_cpu - B)).norm() / denom_cpu < tol); | |
| } | |
| // ---- Solve with DeviceMatrix input ------------------------------------------ | |
| template <typename Scalar> | |
| void test_qr_solve_device(Index n, Index nrhs) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(n, n); | |
| Mat B = Mat::Random(n, nrhs); | |
| auto d_A = gpu::DeviceMatrix<Scalar>::fromHost(A); | |
| auto d_B = gpu::DeviceMatrix<Scalar>::fromHost(B); | |
| gpu::QR<Scalar> qr; | |
| qr.compute(d_A); | |
| VERIFY_IS_EQUAL(qr.info(), Success); | |
| gpu::DeviceMatrix<Scalar> d_X = qr.solve(d_B); | |
| Mat X = d_X.toHost(); | |
| RealScalar residual = (A * X - B).norm() / (A.norm() * X.norm()); | |
| VERIFY(residual < RealScalar(10) * RealScalar(n) * NumTraits<Scalar>::epsilon()); | |
| // Context-bound rvalue constructor adopts a square A instead of copying it. | |
| gpu::Context ctx; | |
| auto d_A_ctx = gpu::DeviceMatrix<Scalar>::fromHost(A, ctx.stream()); | |
| gpu::QR<Scalar> qr_ctx(ctx, std::move(d_A_ctx)); | |
| VERIFY(d_A_ctx.empty()); | |
| VERIFY_IS_EQUAL(qr_ctx.info(), Success); | |
| VERIFY(qr_ctx.stream() == ctx.stream()); | |
| Mat X_ctx = qr_ctx.solve(B); | |
| residual = (A * X_ctx - B).norm() / (A.norm() * X_ctx.norm()); | |
| VERIFY(residual < RealScalar(10) * RealScalar(n) * NumTraits<Scalar>::epsilon()); | |
| } | |
| // ---- Solve overdetermined via device path ----------------------------------- | |
| template <typename Scalar> | |
| void test_qr_solve_overdetermined_device(Index m, Index n, Index nrhs) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| eigen_assert(m >= n); | |
| Mat A = Mat::Random(m, n); | |
| Mat B = Mat::Random(m, nrhs); | |
| auto d_A = gpu::DeviceMatrix<Scalar>::fromHost(A); | |
| auto d_B = gpu::DeviceMatrix<Scalar>::fromHost(B); | |
| gpu::QR<Scalar> qr; | |
| qr.compute(d_A); | |
| VERIFY_IS_EQUAL(qr.info(), Success); | |
| gpu::DeviceMatrix<Scalar> d_X = qr.solve(d_B); | |
| VERIFY_IS_EQUAL(d_X.rows(), n); | |
| VERIFY_IS_EQUAL(d_X.cols(), nrhs); | |
| Mat X = d_X.toHost(); | |
| Mat X_cpu = HouseholderQR<Mat>(A).solve(B); | |
| // See test_qr_solve_overdetermined: backward error for LS is bounded on | |
| // the gradient A^H r, not on r itself. | |
| RealScalar tol = RealScalar(10) * RealScalar(m) * NumTraits<Scalar>::epsilon(); | |
| RealScalar A_norm = A.norm(); | |
| RealScalar denom_gpu = A_norm * (A_norm * X.norm() + B.norm()); | |
| RealScalar denom_cpu = A_norm * (A_norm * X_cpu.norm() + B.norm()); | |
| VERIFY((A.adjoint() * (A * X - B)).norm() / denom_gpu < tol); | |
| VERIFY((A.adjoint() * (A * X_cpu - B)).norm() / denom_cpu < tol); | |
| } | |
| // ---- Solve underdetermined system: m < n (minimum norm) --------------------- | |
| template <typename Scalar> | |
| void test_qr_solve_underdetermined(Index m, Index n, Index nrhs) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| eigen_assert(m < n); | |
| Mat A = Mat::Random(m, n); | |
| Mat B = Mat::Random(m, nrhs); | |
| gpu::QR<Scalar> qr(A); | |
| VERIFY_IS_EQUAL(qr.info(), Success); | |
| Mat X = qr.solve(B); | |
| VERIFY_IS_EQUAL(X.rows(), n); | |
| VERIFY_IS_EQUAL(X.cols(), nrhs); | |
| // The minimum-norm solution exactly satisfies A X = B (m equations, n > m | |
| // unknowns). Backward error is O(m * eps) * ||A|| * ||X||. | |
| RealScalar tol = RealScalar(20) * RealScalar(n) * NumTraits<Scalar>::epsilon(); | |
| VERIFY((A * X - B).norm() / (A.norm() * X.norm() + B.norm()) < tol); | |
| // Min-norm property: X ⟂ null(A), so X ∈ row space(A) = col space(A^H). | |
| // ||X|| <= ||X_any|| for any other solution. Compare to the SVD min-norm reference. | |
| Mat X_svd = BDCSVD<Mat, ComputeThinU | ComputeThinV>(A).solve(B); | |
| // Both should have the same norm (up to rounding). | |
| VERIFY(numext::abs(X.norm() - X_svd.norm()) / X_svd.norm() < | |
| RealScalar(50) * RealScalar(n) * NumTraits<Scalar>::epsilon()); | |
| } | |
| template <typename Scalar> | |
| void test_qr_solve_underdetermined_device(Index m, Index n, Index nrhs) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| eigen_assert(m < n); | |
| Mat A = Mat::Random(m, n); | |
| Mat B = Mat::Random(m, nrhs); | |
| auto d_A = gpu::DeviceMatrix<Scalar>::fromHost(A); | |
| auto d_B = gpu::DeviceMatrix<Scalar>::fromHost(B); | |
| gpu::QR<Scalar> qr; | |
| qr.compute(d_A); | |
| VERIFY_IS_EQUAL(qr.info(), Success); | |
| gpu::DeviceMatrix<Scalar> d_X = qr.solve(d_B); | |
| VERIFY_IS_EQUAL(d_X.rows(), n); | |
| VERIFY_IS_EQUAL(d_X.cols(), nrhs); | |
| Mat X = d_X.toHost(); | |
| RealScalar tol = RealScalar(20) * RealScalar(n) * NumTraits<Scalar>::epsilon(); | |
| VERIFY((A * X - B).norm() / (A.norm() * X.norm() + B.norm()) < tol); | |
| } | |
| // ---- matrixR() returns the upper-triangular factor -------------------------- | |
| template <typename Scalar> | |
| void test_qr_matrixR(Index m, Index n) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| eigen_assert(m >= n); | |
| Mat A = Mat::Random(m, n); | |
| gpu::QR<Scalar> qr(A); | |
| VERIFY_IS_EQUAL(qr.info(), Success); | |
| Mat R = qr.matrixR(); | |
| VERIFY_IS_EQUAL(R.rows(), n); | |
| VERIFY_IS_EQUAL(R.cols(), n); | |
| // Verify R is upper-triangular (lower triangle exactly zero). | |
| for (Index j = 0; j < n; ++j) { | |
| for (Index i = j + 1; i < n; ++i) { | |
| VERIFY_IS_EQUAL(R(i, j), Scalar(0)); | |
| } | |
| } | |
| // Verify that the diagonal of R matches the absolute values of CPU R diagonal | |
| // up to sign (Householder QR has free diagonal sign). | |
| Mat R_cpu = HouseholderQR<Mat>(A).matrixQR().topRows(n).template triangularView<Upper>(); | |
| RealScalar tol = RealScalar(20) * RealScalar(m) * NumTraits<Scalar>::epsilon() * A.norm(); | |
| for (Index i = 0; i < n; ++i) { | |
| VERIFY(numext::abs(numext::abs(R(i, i)) - numext::abs(R_cpu(i, i))) < tol); | |
| } | |
| } | |
| // ---- matrixR() waits only for the solver's stream --------------------------- | |
| // A download through the legacy default stream (cudaMemcpy) would also wait | |
| // for the unrelated blocking stream parked below. | |
| template <typename Scalar> | |
| void test_qr_matrixR_ignores_other_streams(Index m, Index n) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| Mat A = Mat::Random(m, n); | |
| gpu::QR<Scalar> qr(A); | |
| VERIFY_IS_EQUAL(qr.info(), Success); | |
| gpu::Context other; | |
| gpu_test::ParkedStream parked(other.stream()); | |
| (void)qr.matrixR(); | |
| VERIFY(parked.held()); | |
| } | |
| // ---- Multiple solves reuse the factorization -------------------------------- | |
| template <typename Scalar> | |
| void test_qr_multiple_solves(Index n) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(n, n); | |
| gpu::QR<Scalar> qr(A); | |
| VERIFY_IS_EQUAL(qr.info(), Success); | |
| RealScalar tol = RealScalar(10) * RealScalar(n) * NumTraits<Scalar>::epsilon(); | |
| for (int k = 0; k < 5; ++k) { | |
| Mat B = Mat::Random(n, 3); | |
| Mat X = qr.solve(B); | |
| RealScalar residual = (A * X - B).norm() / (A.norm() * X.norm()); | |
| VERIFY(residual < tol); | |
| } | |
| } | |
| // ---- Per-scalar driver ------------------------------------------------------ | |
| template <typename Scalar> | |
| void test_scalar() { | |
| CALL_SUBTEST(test_qr_solve_square<Scalar>(1, 1)); | |
| CALL_SUBTEST(test_qr_solve_square<Scalar>(64, 1)); | |
| CALL_SUBTEST(test_qr_solve_square<Scalar>(64, 4)); | |
| CALL_SUBTEST(test_qr_solve_square<Scalar>(256, 8)); | |
| CALL_SUBTEST(test_qr_solve_overdetermined<Scalar>(128, 64, 4)); | |
| CALL_SUBTEST(test_qr_solve_overdetermined<Scalar>(256, 128, 1)); | |
| CALL_SUBTEST(test_qr_solve_underdetermined<Scalar>(64, 128, 4)); | |
| CALL_SUBTEST(test_qr_solve_underdetermined<Scalar>(64, 128, 1)); | |
| CALL_SUBTEST(test_qr_solve_underdetermined_device<Scalar>(64, 128, 4)); | |
| CALL_SUBTEST(test_qr_matrixR<Scalar>(64, 64)); | |
| CALL_SUBTEST(test_qr_matrixR<Scalar>(128, 64)); | |
| CALL_SUBTEST(test_qr_matrixR_ignores_other_streams<Scalar>(96, 64)); | |
| CALL_SUBTEST(test_qr_solve_device<Scalar>(64, 4)); | |
| CALL_SUBTEST(test_qr_solve_overdetermined_device<Scalar>(128, 64, 4)); | |
| CALL_SUBTEST(test_qr_multiple_solves<Scalar>(64)); | |
| } | |
| void test_qr_empty() { | |
| ArrayXXd empty(0, 0); | |
| gpu::QR<double> qr(empty); | |
| VERIFY_IS_EQUAL(qr.info(), Success); | |
| VERIFY_IS_EQUAL(qr.rows(), 0); | |
| VERIFY_IS_EQUAL(qr.cols(), 0); | |
| VERIFY_IS_EQUAL(qr.solve(MatrixXd(0, 2)).cols(), 2); | |
| VERIFY_IS_EQUAL(qr.solve(gpu::DeviceMatrix<double>::fromHost(MatrixXd(0, 2))).cols(), 2); | |
| // An empty factor still solves: X is cols() x nrhs, zero for the minimum-norm case. | |
| for (Index m : {5, 0}) { | |
| const MatrixXd B = MatrixXd::Random(m, 2); | |
| qr.compute(MatrixXd(m, 5 - m)); | |
| const MatrixXd X = qr.solve(B); | |
| VERIFY_IS_EQUAL(X.rows(), 5 - m); | |
| VERIFY_IS_EQUAL(X.cols(), 2); | |
| VERIFY_IS_EQUAL(X.norm(), 0.0); | |
| VERIFY_IS_EQUAL(qr.solve(gpu::DeviceMatrix<double>::fromHost(B)).toHost().rows(), 5 - m); | |
| } | |
| } | |
| EIGEN_DECLARE_TEST(gpu_cusolver_qr) { | |
| gpu_test::require_cuda_device(); | |
| // Split by scalar so each part compiles in parallel. | |
| CALL_SUBTEST_1(test_scalar<float>()); | |
| CALL_SUBTEST_2(test_scalar<double>()); | |
| CALL_SUBTEST_3(test_scalar<std::complex<float>>()); | |
| CALL_SUBTEST_4(test_scalar<std::complex<double>>()); | |
| CALL_SUBTEST_5(test_qr_empty()); | |
| } |