// This file is part of Eigen, a lightweight C++ template library // for linear algebra. // // Copyright (C) 2026 Rasmus Munk Larsen // // 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 GpuSelfAdjointEigenSolver: GPU symmetric/Hermitian eigenvalue // decomposition via cuSOLVER. #define EIGEN_USE_GPU #include "main.h" #include #include using namespace Eigen; // ---- Reconstruction: V * diag(W) * V^H ≈ A --------------------------------- template void test_eigen_reconstruction(Index n) { using Mat = Matrix; using RealScalar = typename NumTraits::Real; // Build a symmetric/Hermitian matrix. Mat R = Mat::Random(n, n); Mat A = R + R.adjoint(); gpu::SelfAdjointEigenSolver es(A); VERIFY_IS_EQUAL(es.info(), Success); auto W = es.eigenvalues(); Mat V = es.eigenvectors(); VERIFY_IS_EQUAL(W.size(), n); VERIFY_IS_EQUAL(V.rows(), n); VERIFY_IS_EQUAL(V.cols(), n); // Reconstruct: A_hat = V * diag(W) * V^H. Mat A_hat = V * W.asDiagonal() * V.adjoint(); RealScalar tol = RealScalar(8) * static_cast(n) * NumTraits::epsilon() * A.norm(); VERIFY((A_hat - A).norm() < tol); // Orthogonality: V^H * V ≈ I. Mat VhV = V.adjoint() * V; Mat eye = Mat::Identity(n, n); VERIFY((VhV - eye).norm() < tol); } // ---- Eigenvalues match CPU SelfAdjointEigenSolver --------------------------- template void test_eigen_values(Index n) { using Mat = Matrix; using RealScalar = typename NumTraits::Real; Mat R = Mat::Random(n, n); Mat A = R + R.adjoint(); gpu::SelfAdjointEigenSolver gpu_es(A); VERIFY_IS_EQUAL(gpu_es.info(), Success); auto W_gpu = gpu_es.eigenvalues(); SelfAdjointEigenSolver cpu_es(A); auto W_cpu = cpu_es.eigenvalues(); RealScalar tol = RealScalar(2) * static_cast(n) * NumTraits::epsilon() * W_cpu.cwiseAbs().maxCoeff(); VERIFY((W_gpu - W_cpu).norm() < tol); } // ---- Eigenvalues-only mode -------------------------------------------------- template void test_eigen_values_only(Index n) { using Mat = Matrix; using RealScalar = typename NumTraits::Real; Mat R = Mat::Random(n, n); Mat A = R + R.adjoint(); gpu::SelfAdjointEigenSolver gpu_es(A, EigenvaluesOnly); VERIFY_IS_EQUAL(gpu_es.info(), Success); auto W_gpu = gpu_es.eigenvalues(); SelfAdjointEigenSolver cpu_es(A, EigenvaluesOnly); auto W_cpu = cpu_es.eigenvalues(); RealScalar tol = RealScalar(2) * static_cast(n) * NumTraits::epsilon() * W_cpu.cwiseAbs().maxCoeff(); VERIFY((W_gpu - W_cpu).norm() < tol); } // ---- DeviceMatrix input path ------------------------------------------------ template void test_eigen_device_matrix(Index n) { using Mat = Matrix; using RealScalar = typename NumTraits::Real; Mat R = Mat::Random(n, n); Mat A = R + R.adjoint(); auto d_A = gpu::DeviceMatrix::fromHost(A); gpu::SelfAdjointEigenSolver es; es.compute(d_A); VERIFY_IS_EQUAL(es.info(), Success); auto W_gpu = es.eigenvalues(); Mat V = es.eigenvectors(); // Verify reconstruction. Mat A_hat = V * W_gpu.asDiagonal() * V.adjoint(); RealScalar tol = RealScalar(8) * static_cast(n) * NumTraits::epsilon() * A.norm(); VERIFY((A_hat - A).norm() < tol); } // ---- Recompute (reuse solver object) ---------------------------------------- template void test_eigen_recompute(Index n) { using Mat = Matrix; using RealScalar = typename NumTraits::Real; gpu::SelfAdjointEigenSolver es; for (int trial = 0; trial < 3; ++trial) { Mat R = Mat::Random(n, n); Mat A = R + R.adjoint(); es.compute(A); VERIFY_IS_EQUAL(es.info(), Success); auto W = es.eigenvalues(); Mat V = es.eigenvectors(); Mat A_hat = V * W.asDiagonal() * V.adjoint(); RealScalar tol = RealScalar(8) * static_cast(n) * NumTraits::epsilon() * A.norm(); VERIFY((A_hat - A).norm() < tol); } } // ---- Repeated eigenvalues --------------------------------------------------- // // Builds A with a degenerate eigenvalue cluster. Eigenvectors within the cluster // are not uniquely determined, but eigenvalues, reconstruction, and orthogonality // must still hold. template void test_eigen_repeated_values(Index n) { using Mat = Matrix; using Vec = Matrix::Real, Dynamic, 1>; using RealScalar = typename NumTraits::Real; // Build a unitary V via QR of a random matrix. Mat seed = Mat::Random(n, n); Mat V = HouseholderQR(seed).householderQ(); // Spectrum: a 4-fold cluster at value 1, then increasing distinct values. Vec eigs(n); for (Index i = 0; i < n; ++i) { eigs(i) = (i < 4) ? RealScalar(1) : RealScalar(i); } Mat A = V * eigs.asDiagonal() * V.adjoint(); Mat A_sym = (A + A.adjoint()) * RealScalar(0.5); // symmetrize numerically gpu::SelfAdjointEigenSolver es(A_sym); VERIFY_IS_EQUAL(es.info(), Success); // Eigenvalues must match the constructed spectrum (cuSOLVER returns ascending). Vec W_gpu = es.eigenvalues(); Vec W_expected = eigs; std::sort(W_expected.data(), W_expected.data() + n); RealScalar tol_w = RealScalar(2) * static_cast(n) * NumTraits::epsilon() * W_expected.cwiseAbs().maxCoeff(); VERIFY((W_gpu - W_expected).norm() < tol_w); // Reconstruction must hold even with a degenerate cluster (eigenvectors form a // valid orthonormal basis for the cluster's invariant subspace). Mat V_gpu = es.eigenvectors(); Mat A_hat = V_gpu * W_gpu.asDiagonal() * V_gpu.adjoint(); RealScalar tol = RealScalar(20) * static_cast(n) * NumTraits::epsilon() * A.norm(); VERIFY((A_hat - A_sym).norm() < tol); // Orthogonality of computed eigenvectors must hold. VERIFY((V_gpu.adjoint() * V_gpu - Mat::Identity(n, n)).norm() < tol); } // ---- Device-side accessors -------------------------------------------------- // // d_eigenvalues / d_eigenvectors return non-owning views over the solver's // internal d_W_ / d_A_ buffers. Verify they agree with the host accessors and // that repeated invocations leave the solver state intact. template void test_eigen_device_accessors(Index n) { using Mat = Matrix; Mat R = Mat::Random(n, n); Mat A = R + R.adjoint(); gpu::SelfAdjointEigenSolver es(A); VERIFY_IS_EQUAL(es.info(), Success); auto d_W = es.d_eigenvalues(); auto d_V = es.d_eigenvectors(); VERIFY_IS_APPROX(d_W.toHost(), es.eigenvalues()); VERIFY_IS_APPROX(d_V.toHost(), es.eigenvectors()); // Re-fetch must keep working (view destruction must not free the underlying buffers). (void)es.d_eigenvalues(); (void)es.d_eigenvectors(); VERIFY_IS_APPROX(es.eigenvalues(), d_W.toHost()); VERIFY_IS_APPROX(es.eigenvectors(), d_V.toHost()); } // ---- Chain device views into a downstream cuBLAS GEMM (no D2D copy) --------- template void test_eigen_chain_orthogonality(Index n) { using Mat = Matrix; using RealScalar = typename NumTraits::Real; Mat R = Mat::Random(n, n); Mat A = R + R.adjoint(); gpu::SelfAdjointEigenSolver es(A); VERIFY_IS_EQUAL(es.info(), Success); // V^H * V = I — V is the device view of d_A_; the GEMM consumes the view // without an intervening D2D copy. gpu::Context ctx; gpu::DeviceMatrix d_VtV; { auto d_V = es.d_eigenvectors(); d_VtV.device(ctx) = d_V.adjoint() * d_V; } Mat VtV = d_VtV.toHost(); RealScalar tol = RealScalar(20) * static_cast(n) * NumTraits::epsilon(); VERIFY((VtV - Mat::Identity(n, n)).norm() < tol); // After view destruction, the solver's state must remain valid. Mat V_again = es.eigenvectors(); VERIFY_IS_EQUAL(V_again.rows(), n); } // ---- Move support ------------------------------------------------------------- template void test_eigen_move(Index n) { using Mat = Matrix; using RealScalar = typename NumTraits::Real; Mat R = Mat::Random(n, n); Mat A = R + R.adjoint(); gpu::SelfAdjointEigenSolver es(A); VERIFY_IS_EQUAL(es.info(), Success); gpu::SelfAdjointEigenSolver moved(std::move(es)); VERIFY_IS_EQUAL(moved.info(), Success); Mat V = moved.eigenvectors(); auto W = moved.eigenvalues(); RealScalar tol = RealScalar(8) * static_cast(n) * NumTraits::epsilon() * A.norm(); VERIFY((V * W.asDiagonal() * V.adjoint() - A).norm() < tol); gpu::SelfAdjointEigenSolver assigned; assigned = std::move(moved); VERIFY_IS_EQUAL(assigned.info(), Success); V = assigned.eigenvectors(); W = assigned.eigenvalues(); VERIFY((V * W.asDiagonal() * V.adjoint() - A).norm() < tol); } // ---- Empty matrix ----------------------------------------------------------- void test_eigen_empty() { gpu::SelfAdjointEigenSolver es(MatrixXd(0, 0)); VERIFY_IS_EQUAL(es.info(), Success); VERIFY_IS_EQUAL(es.rows(), 0); VERIFY_IS_EQUAL(es.cols(), 0); } // ---- Per-scalar driver ------------------------------------------------------ template void test_scalar() { // Reconstruction + orthogonality. CALL_SUBTEST(test_eigen_reconstruction(64)); CALL_SUBTEST(test_eigen_reconstruction(128)); // Eigenvalues match CPU. CALL_SUBTEST(test_eigen_values(64)); CALL_SUBTEST(test_eigen_values(128)); // Values-only mode. CALL_SUBTEST(test_eigen_values_only(64)); // DeviceMatrix input. CALL_SUBTEST(test_eigen_device_matrix(64)); // Recompute. CALL_SUBTEST(test_eigen_recompute(32)); // Repeated eigenvalues (degenerate cluster). CALL_SUBTEST(test_eigen_repeated_values(64)); // Device accessors. CALL_SUBTEST(test_eigen_device_accessors(64)); // Chain device view into a downstream GEMM. CALL_SUBTEST(test_eigen_chain_orthogonality(64)); // Move constructor/assignment. CALL_SUBTEST(test_eigen_move(32)); } EIGEN_DECLARE_TEST(gpu_cusolver_eigen) { // Split by scalar so each part compiles in parallel. CALL_SUBTEST_1(test_scalar()); CALL_SUBTEST_2(test_scalar()); CALL_SUBTEST_3(test_scalar>()); CALL_SUBTEST_4(test_scalar>()); CALL_SUBTEST_5(test_eigen_empty()); }