| // 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 GpuSVD: GPU SVD via cuSOLVER. | |
| #define EIGEN_USE_GPU | |
| #include "main.h" | |
| #include <Eigen/SVD> | |
| #include <contrib/Eigen/GPU> | |
| #include "./gpu_test_helpers.h" | |
| using namespace Eigen; | |
| // ---- SVD reconstruction: U * diag(S) * VT ≈ A ------------------------------ | |
| template <typename Scalar, unsigned int Options> | |
| void test_svd_reconstruction(Index m, Index n) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(m, n); | |
| gpu::SVD<Scalar> svd(A, Options); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| auto S = svd.singularValues(); | |
| Mat U = svd.matrixU(); | |
| Mat VT = svd.matrixVT(); | |
| const Index k = (std::min)(m, n); | |
| // Reconstruct: A_hat = U[:,:k] * diag(S) * VT[:k,:]. | |
| Mat A_hat = U.leftCols(k) * S.asDiagonal() * VT.topRows(k); | |
| RealScalar tol = RealScalar(5) * std::sqrt(static_cast<RealScalar>(k)) * NumTraits<Scalar>::epsilon() * A.norm(); | |
| VERIFY((A_hat - A).norm() < tol); | |
| // Orthogonality: U^H * U ≈ I. | |
| Mat UtU = U.adjoint() * U; | |
| Mat I_u = Mat::Identity(U.cols(), U.cols()); | |
| VERIFY((UtU - I_u).norm() < tol); | |
| // Orthogonality: VT * VT^H ≈ I. | |
| Mat VtVh = VT * VT.adjoint(); | |
| Mat I_v = Mat::Identity(VT.rows(), VT.rows()); | |
| VERIFY((VtVh - I_v).norm() < tol); | |
| } | |
| // ---- Singular values match CPU BDCSVD --------------------------------------- | |
| template <typename Scalar> | |
| void test_svd_singular_values(Index m, Index n) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(m, n); | |
| gpu::SVD<Scalar> svd(A, 0); // values only | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| auto S_gpu = svd.singularValues(); | |
| auto S_cpu = BDCSVD<Mat>(A).singularValues(); | |
| // Weyl's perturbation bound (Higham, Accuracy and Stability of Numerical | |
| // Algorithms, 2nd ed., §10.2.3): |σ_i(A) - σ_i(A+δA)| ≤ ||δA||. Both cuSOLVER | |
| // and BDCSVD have backward error ||δA|| / ||A|| ≤ p(m,n) · u; the difference | |
| // of the two computed singular value vectors is bounded by 2·p(m,n)·u·S_max, | |
| // and √(min) · ||·||_∞ ≥ ||·||_2. Across 5k trials per case in | |
| // {float, double, complex<float>, complex<double>} × {(64,64), (128,64)}, | |
| // worst observed err / (√min · u · S_max) is ≈ 6.2; we use 12 for headroom | |
| // against future runs hitting the tail of the distribution. | |
| RealScalar tol = | |
| RealScalar(12) * std::sqrt(static_cast<RealScalar>((std::min)(m, n))) * NumTraits<Scalar>::epsilon() * S_cpu(0); | |
| VERIFY((S_gpu - S_cpu).norm() < tol); | |
| } | |
| // ---- Solve: pseudoinverse --------------------------------------------------- | |
| template <typename Scalar> | |
| void test_svd_solve(Index m, Index n, Index nrhs) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(m, n); | |
| Mat B = Mat::Random(m, nrhs); | |
| gpu::SVD<Scalar> svd(A, ComputeThinU | ComputeThinV); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| Mat X = svd.solve(B); | |
| VERIFY_IS_EQUAL(X.rows(), n); | |
| VERIFY_IS_EQUAL(X.cols(), nrhs); | |
| // Compare with CPU BDCSVD solve. Wedin's perturbation theorem (Higham, | |
| // Accuracy and Stability of Numerical Algorithms, 2nd ed., §20.1) bounds | |
| // the forward error of a backward-stable SVD-based pseudoinverse solve by | |
| // c · κ(A) · u with c = O(1). Comparing two such solvers doubles the | |
| // constant. Across 6k trials over {float, double, complex<float>, | |
| // complex<double>} and {square, over-/underdetermined} shapes, the worst | |
| // observed err / (κ · u) is 5.3. | |
| auto cpu_svd = BDCSVD<Mat, ComputeThinU | ComputeThinV>(A); | |
| Mat X_cpu = cpu_svd.solve(B); | |
| auto S = cpu_svd.singularValues(); | |
| const RealScalar cond = S(0) / S(S.size() - 1); | |
| const RealScalar tol = RealScalar(8) * cond * NumTraits<Scalar>::epsilon(); | |
| VERIFY((X - X_cpu).norm() / X_cpu.norm() < tol); | |
| } | |
| // ---- Solve: Context-bound --------------------------------------------------- | |
| // A Context-bound SVD shares the Context's stream and cuBLASLt plan cache / | |
| // workspace; its pseudoinverse solve must agree with the standalone solver. | |
| template <typename Scalar> | |
| void test_svd_solve_context(Index m, Index n, Index nrhs) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(m, n); | |
| Mat B = Mat::Random(m, nrhs); | |
| gpu::Context ctx; | |
| gpu::SVD<Scalar> svd(ctx, A, ComputeThinU | ComputeThinV); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| Mat X = svd.solve(B); | |
| gpu::SVD<Scalar> svd_standalone(A, ComputeThinU | ComputeThinV); | |
| Mat X_ref = svd_standalone.solve(B); | |
| // Same algorithm on both paths; only the cuBLASLt plan selection may | |
| // differ, so bound the difference by κ(A)·u as in test_svd_solve. | |
| auto S = svd_standalone.singularValues(); | |
| const RealScalar cond = S(0) / S(S.size() - 1); | |
| const RealScalar tol = RealScalar(8) * cond * NumTraits<Scalar>::epsilon(); | |
| VERIFY((X - X_ref).norm() / X_ref.norm() < tol); | |
| // Context-bound rvalue constructor adopts A when m >= n (a wide A is | |
| // transposed into a copy). | |
| auto d_A = gpu::DeviceMatrix<Scalar>::fromHost(A, ctx.stream()); | |
| gpu::SVD<Scalar> svd_adopt(ctx, std::move(d_A), ComputeThinU | ComputeThinV); | |
| VERIFY(m < n || d_A.empty()); | |
| VERIFY_IS_EQUAL(svd_adopt.info(), Success); | |
| VERIFY(svd_adopt.stream() == ctx.stream()); | |
| Mat X_adopt = svd_adopt.solve(B); | |
| VERIFY((X_adopt - X_ref).norm() / X_ref.norm() < tol); | |
| } | |
| // ---- Solve: truncated ------------------------------------------------------- | |
| template <typename Scalar> | |
| void test_svd_solve_truncated(Index m, Index n) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(m, n); | |
| Mat B = Mat::Random(m, 1); | |
| const Index k = (std::min)(m, n); | |
| const Index trunc = k / 2; | |
| eigen_assert(trunc > 0); | |
| gpu::SVD<Scalar> svd(A, ComputeThinU | ComputeThinV); | |
| Mat X_trunc = svd.solve(B, trunc); | |
| // Build CPU reference: truncated pseudoinverse. | |
| auto cpu_svd = BDCSVD<Mat, ComputeThinU | ComputeThinV>(A); | |
| auto S = cpu_svd.singularValues(); | |
| Mat U = cpu_svd.matrixU(); | |
| Mat V = cpu_svd.matrixV(); | |
| // D_ii = 1/S_i for i < trunc, 0 otherwise. | |
| Matrix<RealScalar, Dynamic, 1> D = Matrix<RealScalar, Dynamic, 1>::Zero(k); | |
| for (Index i = 0; i < trunc; ++i) D(i) = RealScalar(1) / S(i); | |
| Mat X_ref = V * D.asDiagonal() * U.adjoint() * B; | |
| RealScalar tol = RealScalar(100) * RealScalar(k) * NumTraits<Scalar>::epsilon(); | |
| VERIFY((X_trunc - X_ref).norm() / X_ref.norm() < tol); | |
| } | |
| // ---- Solve: Tikhonov regularized -------------------------------------------- | |
| template <typename Scalar> | |
| void test_svd_solve_regularized(Index m, Index n) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(m, n); | |
| Mat B = Mat::Random(m, 1); | |
| RealScalar lambda = RealScalar(0.1); | |
| const Index k = (std::min)(m, n); | |
| gpu::SVD<Scalar> svd(A, ComputeThinU | ComputeThinV); | |
| Mat X_reg = svd.solve(B, lambda); | |
| // CPU reference: D_ii = S_i / (S_i^2 + lambda^2). | |
| auto cpu_svd = BDCSVD<Mat, ComputeThinU | ComputeThinV>(A); | |
| auto S = cpu_svd.singularValues(); | |
| Mat U = cpu_svd.matrixU(); | |
| Mat V = cpu_svd.matrixV(); | |
| Matrix<RealScalar, Dynamic, 1> D(k); | |
| for (Index i = 0; i < k; ++i) D(i) = S(i) / (S(i) * S(i) + lambda * lambda); | |
| Mat X_ref = V * D.asDiagonal() * U.adjoint() * B; | |
| RealScalar tol = RealScalar(100) * RealScalar(k) * NumTraits<Scalar>::epsilon(); | |
| VERIFY((X_reg - X_ref).norm() / X_ref.norm() < tol); | |
| } | |
| // ---- Solve: rank-deficient (exercise drop_threshold pseudoinverse) ---------- | |
| template <typename Scalar> | |
| void test_svd_solve_rank_deficient(Index m, Index n, Index r) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using Vec = Matrix<typename NumTraits<Scalar>::Real, Dynamic, 1>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| eigen_assert(r > 0 && r < (std::min)(m, n)); | |
| // Build A of rank exactly r: A = U[:,:r] * diag(s) * V[:,:r]^H. | |
| Mat U_seed = Mat::Random(m, m); | |
| Mat U_full = HouseholderQR<Mat>(U_seed).householderQ(); | |
| Mat V_seed = Mat::Random(n, n); | |
| Mat V_full = HouseholderQR<Mat>(V_seed).householderQ(); | |
| Vec sigma(r); | |
| for (Index i = 0; i < r; ++i) sigma(i) = RealScalar(1) + RealScalar(i); // distinct, well-spaced | |
| Mat A = U_full.leftCols(r) * sigma.asDiagonal() * V_full.leftCols(r).adjoint(); | |
| Mat B = Mat::Random(m, 1); | |
| gpu::SVD<Scalar> svd(A, ComputeThinU | ComputeThinV); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| Mat X_gpu = svd.solve(B); | |
| // Reference: CPU BDCSVD with the same drop_threshold semantics (its default solve() | |
| // also drops near-zero singular values relative to S(0) * (m, n)_max * epsilon). | |
| Mat X_cpu = BDCSVD<Mat, ComputeThinU | ComputeThinV>(A).solve(B); | |
| // Both should produce the minimum-norm least-squares solution; compare via the | |
| // "useful" residual A^H r: zero up to the rank-r component plus rounding. | |
| RealScalar tol = RealScalar(50) * RealScalar((std::max)(m, n)) * NumTraits<Scalar>::epsilon(); | |
| VERIFY((X_gpu - X_cpu).norm() / X_cpu.norm() < tol); | |
| // Norm of X should also be minimum (X has no component in null(A) up to rounding). | |
| VERIFY(numext::abs(X_gpu.norm() - X_cpu.norm()) / X_cpu.norm() < tol); | |
| } | |
| // ---- Reconstruction: full U / full V on a wide matrix (m < n) --------------- | |
| // | |
| // Exercises the transposed-internal-representation path through gesvd combined | |
| // with the matrixU/matrixVT swap logic. | |
| template <typename Scalar> | |
| void test_svd_reconstruction_full_wide(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::SVD<Scalar> svd(A, ComputeFullU | ComputeFullV); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| auto S = svd.singularValues(); | |
| Mat U = svd.matrixU(); // m × m | |
| Mat VT = svd.matrixVT(); // n × n | |
| Mat V = svd.matrixV(); // n × n (matrixV alias should match matrixVT().adjoint()) | |
| VERIFY_IS_EQUAL(U.rows(), m); | |
| VERIFY_IS_EQUAL(U.cols(), m); | |
| VERIFY_IS_EQUAL(VT.rows(), n); | |
| VERIFY_IS_EQUAL(VT.cols(), n); | |
| VERIFY_IS_EQUAL(V.rows(), n); | |
| VERIFY_IS_EQUAL(V.cols(), n); | |
| VERIFY_IS_APPROX(V, VT.adjoint()); | |
| const Index k = (std::min)(m, n); | |
| Mat A_hat = U.leftCols(k) * S.asDiagonal() * VT.topRows(k); | |
| RealScalar tol = RealScalar(5) * std::sqrt(static_cast<RealScalar>(k)) * NumTraits<Scalar>::epsilon() * A.norm(); | |
| VERIFY((A_hat - A).norm() < tol); | |
| Mat UtU = U.adjoint() * U; | |
| VERIFY((UtU - Mat::Identity(m, m)).norm() < tol); | |
| Mat VtVh = VT * VT.adjoint(); | |
| VERIFY((VtVh - Mat::Identity(n, n)).norm() < tol); | |
| } | |
| // ---- Device-side accessors -------------------------------------------------- | |
| // | |
| // Verify that d_singularValues / d_matrixU / d_matrixVT return views over the | |
| // SVD's internal storage that agree with the host accessors after toHost(). | |
| template <typename Scalar> | |
| void test_svd_device_accessors(Index m, Index n) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(m, n); | |
| gpu::SVD<Scalar> svd(A, ComputeThinU | ComputeThinV); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| auto d_S = svd.d_singularValues(); | |
| auto d_U = svd.d_matrixU(); | |
| auto d_VT = svd.d_matrixVT(); | |
| Matrix<RealScalar, Dynamic, 1> S_host = d_S.toHost(); | |
| Mat U_host = d_U.toHost(); | |
| Mat VT_host = d_VT.toHost(); | |
| VERIFY_IS_APPROX(S_host, svd.singularValues()); | |
| VERIFY_IS_APPROX(U_host, svd.matrixU()); | |
| VERIFY_IS_APPROX(VT_host, svd.matrixVT()); | |
| // Multiple invocations must keep returning views without disturbing the SVD's state: | |
| // call again, then verify the SVD's host accessors still produce correct results. | |
| (void)svd.d_matrixU(); | |
| (void)svd.d_matrixVT(); | |
| VERIFY_IS_APPROX(svd.matrixU(), U_host); | |
| VERIFY_IS_APPROX(svd.matrixVT(), VT_host); | |
| } | |
| template <typename Scalar> | |
| void test_svd_device_accessors_full_wide(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::SVD<Scalar> svd(A, ComputeFullU | ComputeFullV); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| auto d_S = svd.d_singularValues(); | |
| auto d_U = svd.d_matrixU(); | |
| auto d_VT = svd.d_matrixVT(); | |
| VERIFY_IS_EQUAL(d_U.rows(), m); | |
| VERIFY_IS_EQUAL(d_U.cols(), m); | |
| VERIFY_IS_EQUAL(d_VT.rows(), n); | |
| VERIFY_IS_EQUAL(d_VT.cols(), n); | |
| Matrix<RealScalar, Dynamic, 1> S_host = d_S.toHost(); | |
| Mat U_host = d_U.toHost(); | |
| Mat VT_host = d_VT.toHost(); | |
| VERIFY_IS_APPROX(S_host, svd.singularValues()); | |
| VERIFY_IS_APPROX(U_host, svd.matrixU()); | |
| VERIFY_IS_APPROX(VT_host, svd.matrixVT()); | |
| } | |
| // ---- Chain device views into a downstream cuBLAS GEMM (no D2D copy) --------- | |
| // | |
| // d_matrixU() returns a non-owning view over the SVD's internal d_U_ buffer. | |
| // Feeding the view straight into a Context-driven GEMM exercises cross-stream | |
| // event sync and confirms the borrow-deleter does not double-free on temporary | |
| // destruction. | |
| template <typename Scalar> | |
| void test_svd_chain_orthogonality(Index m, Index n) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| Mat A = Mat::Random(m, n); | |
| gpu::SVD<Scalar> svd(A, ComputeThinU | ComputeThinV); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| const Index k = (std::min)(m, n); | |
| // U^H * U = I_k, all on device, no host roundtrip on U. | |
| gpu::Context ctx; | |
| gpu::DeviceMatrix<Scalar> d_UtU; | |
| { | |
| auto d_U = svd.d_matrixU(); | |
| d_UtU.device(ctx) = d_U.adjoint() * d_U; | |
| } | |
| Mat UtU = d_UtU.toHost(); | |
| RealScalar tol = RealScalar(20) * std::sqrt(static_cast<RealScalar>(k)) * NumTraits<Scalar>::epsilon(); | |
| VERIFY((UtU - Mat::Identity(k, k)).norm() < tol); | |
| // VT * VT^H = I_k. | |
| gpu::DeviceMatrix<Scalar> d_VVt; | |
| { | |
| auto d_VT = svd.d_matrixVT(); | |
| d_VVt.device(ctx) = d_VT * d_VT.adjoint(); | |
| } | |
| Mat VVt = d_VVt.toHost(); | |
| VERIFY((VVt - Mat::Identity(k, k)).norm() < tol); | |
| // After view destruction, SVD's state must remain valid (the deleter is a no-op | |
| // for views, so the underlying d_U_ / d_VT_ are not freed). | |
| Mat U_again = svd.matrixU(); | |
| Mat VT_again = svd.matrixVT(); | |
| VERIFY_IS_EQUAL(U_again.rows(), m); | |
| VERIFY_IS_EQUAL(VT_again.cols(), n); | |
| } | |
| // ---- Host getters wait 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_svd_getters_ignore_other_streams(Index m, Index n) { | |
| using Mat = Matrix<Scalar, Dynamic, Dynamic>; | |
| Mat A = Mat::Random(m, n); | |
| gpu::SVD<Scalar> svd(A, ComputeThinU | ComputeThinV); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| gpu::Context other; | |
| gpu_test::ParkedStream parked(other.stream()); | |
| (void)svd.singularValues(); | |
| (void)svd.matrixU(); | |
| (void)svd.matrixVT(); | |
| VERIFY(parked.held()); | |
| } | |
| // ---- Empty matrix ----------------------------------------------------------- | |
| void test_svd_empty() { | |
| ArrayXXd empty(0, 0); | |
| gpu::SVD<double> svd(empty, 0); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| VERIFY_IS_EQUAL(svd.rows(), 0); | |
| VERIFY_IS_EQUAL(svd.cols(), 0); | |
| svd.compute(MatrixXd::Random(4, 6), ComputeThinU | ComputeThinV); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| svd.compute(MatrixXd(0, 7), 0); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| VERIFY_IS_EQUAL(svd.rows(), 0); | |
| VERIFY_IS_EQUAL(svd.cols(), 7); | |
| VERIFY_IS_EQUAL(svd.singularValues().size(), 0); | |
| svd.compute(MatrixXd(5, 0), 0); | |
| VERIFY_IS_EQUAL(svd.info(), Success); | |
| VERIFY_IS_EQUAL(svd.rows(), 5); | |
| VERIFY_IS_EQUAL(svd.cols(), 0); | |
| VERIFY_IS_EQUAL(svd.singularValues().size(), 0); | |
| } | |
| // ---- Device-resident solve ---------------------------------------------------- | |
| template <typename Scalar> | |
| void test_device_solve(Index m, Index n, Index nrhs) { | |
| using Mat = Eigen::Matrix<Scalar, Dynamic, Dynamic>; | |
| Mat A = Mat::Random(m, n); | |
| Mat B = Mat::Random(m, nrhs); | |
| gpu::SVD<Scalar> svd(A); | |
| VERIFY(svd.info() == Success); | |
| Mat X_host = svd.solve(B); | |
| auto d_B = gpu::DeviceMatrix<Scalar>::fromHost(B, svd.stream()); | |
| gpu::DeviceMatrix<Scalar> d_X = svd.solve(d_B); | |
| VERIFY_IS_APPROX(d_X.toHost(), X_host); | |
| // Second device solve reuses the cached inverse diagonal (no host sync). | |
| gpu::DeviceMatrix<Scalar> d_X2 = svd.solve(d_B); | |
| VERIFY_IS_APPROX(d_X2.toHost(), X_host); | |
| // Truncated variant matches its host counterpart too. | |
| const Index trunc = (std::min)(m, n) / 2 + 1; | |
| Mat Xt_host = svd.solve(B, trunc); | |
| gpu::DeviceMatrix<Scalar> d_Xt = svd.solve(d_B, trunc); | |
| VERIFY_IS_APPROX(d_Xt.toHost(), Xt_host); | |
| } | |
| // ---- Per-scalar driver ------------------------------------------------------ | |
| template <typename Scalar> | |
| void test_scalar() { | |
| CALL_SUBTEST(test_device_solve<Scalar>(64, 48, 4)); | |
| CALL_SUBTEST(test_device_solve<Scalar>(48, 64, 3)); | |
| // Reconstruction + orthogonality (thin and full, identical test logic). | |
| CALL_SUBTEST((test_svd_reconstruction<Scalar, ComputeThinU | ComputeThinV>(64, 64))); | |
| CALL_SUBTEST((test_svd_reconstruction<Scalar, ComputeThinU | ComputeThinV>(128, 64))); | |
| CALL_SUBTEST((test_svd_reconstruction<Scalar, ComputeThinU | ComputeThinV>(64, 128))); // wide (m < n) | |
| CALL_SUBTEST((test_svd_reconstruction<Scalar, ComputeFullU | ComputeFullV>(64, 64))); | |
| CALL_SUBTEST((test_svd_reconstruction<Scalar, ComputeFullU | ComputeFullV>(128, 64))); | |
| // Singular values. | |
| CALL_SUBTEST(test_svd_singular_values<Scalar>(64, 64)); | |
| CALL_SUBTEST(test_svd_singular_values<Scalar>(128, 64)); | |
| // Solve. | |
| CALL_SUBTEST(test_svd_solve<Scalar>(64, 64, 4)); | |
| CALL_SUBTEST(test_svd_solve<Scalar>(128, 64, 4)); | |
| CALL_SUBTEST(test_svd_solve<Scalar>(64, 128, 4)); // wide (m < n) | |
| CALL_SUBTEST(test_svd_solve_context<Scalar>(64, 48, 3)); | |
| // Truncated and regularized solve. | |
| CALL_SUBTEST(test_svd_solve_truncated<Scalar>(64, 64)); | |
| CALL_SUBTEST(test_svd_solve_regularized<Scalar>(64, 64)); | |
| // Rank-deficient solve (exercises drop_threshold pseudoinverse). | |
| CALL_SUBTEST(test_svd_solve_rank_deficient<Scalar>(64, 64, 32)); | |
| CALL_SUBTEST(test_svd_solve_rank_deficient<Scalar>(96, 64, 16)); | |
| CALL_SUBTEST(test_svd_solve_rank_deficient<Scalar>(64, 96, 16)); | |
| // Wide matrix with full U/V (transposed-internal path). | |
| CALL_SUBTEST((test_svd_reconstruction_full_wide<Scalar>(64, 96))); | |
| // Device accessors. | |
| CALL_SUBTEST(test_svd_device_accessors<Scalar>(64, 64)); | |
| CALL_SUBTEST(test_svd_device_accessors<Scalar>(96, 64)); | |
| CALL_SUBTEST(test_svd_device_accessors<Scalar>(64, 96)); | |
| CALL_SUBTEST(test_svd_device_accessors_full_wide<Scalar>(64, 96)); | |
| // Chain device views into a downstream GEMM (orthogonality check). | |
| CALL_SUBTEST(test_svd_chain_orthogonality<Scalar>(64, 64)); | |
| CALL_SUBTEST(test_svd_chain_orthogonality<Scalar>(96, 64)); | |
| CALL_SUBTEST(test_svd_getters_ignore_other_streams<Scalar>(64, 48)); | |
| CALL_SUBTEST(test_svd_getters_ignore_other_streams<Scalar>(48, 64)); // wide: U and V^T swap roles | |
| } | |
| EIGEN_DECLARE_TEST(gpu_cusolver_svd) { | |
| 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_svd_empty()); | |
| } |