| // 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 | |
| // GPU SVD using cuSOLVER's divide-and-conquer cusolverDnXgesvd. U, S, and VT | |
| // stay on device; solve() forms X = V * diag(D) * U^H * B with cuBLAS GEMM. | |
| // | |
| // cuSOLVER returns VT rather than V, so VT is what is stored; matrixV() adjoints | |
| // it to match JacobiSVD and BDCSVD. | |
| #ifndef EIGEN_GPU_SVD_H | |
| #define EIGEN_GPU_SVD_H | |
| // IWYU pragma: private | |
| #include "./InternalHeaderCheck.h" | |
| #include "./GpuSolverContext.h" | |
| namespace Eigen { | |
| namespace gpu { | |
| template <typename Scalar_> | |
| class SVD { | |
| public: | |
| using Scalar = Scalar_; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| using PlainMatrix = Eigen::Matrix<Scalar, Dynamic, Dynamic, ColMajor>; | |
| using PlainVector = Eigen::Matrix<Scalar, Dynamic, 1>; | |
| using RealVector = Eigen::Matrix<RealScalar, Dynamic, 1>; | |
| SVD() = default; | |
| /** Bind to \p ctx: run on its stream with its cuSOLVER/cuBLAS handles, so | |
| * solver work chains with other work on the same Context without | |
| * cross-stream event waits. \p ctx must outlive this object. */ | |
| explicit SVD(Context& ctx) : solver_ctx_(ctx) {} | |
| template <typename InputType> | |
| explicit SVD(const DenseBase<InputType>& A, unsigned int options = ComputeThinU | ComputeThinV) { | |
| compute(A, options); | |
| } | |
| explicit SVD(const DeviceMatrix<Scalar>& d_A, unsigned int options = ComputeThinU | ComputeThinV) { | |
| compute(d_A, options); | |
| } | |
| /** Decompose a device-resident A immediately (adopt when m >= n, no copy; a view is copied). */ | |
| explicit SVD(DeviceMatrix<Scalar>&& d_A, unsigned int options = ComputeThinU | ComputeThinV) { | |
| compute(std::move(d_A), options); | |
| } | |
| /** Bind to \p ctx and decompose A immediately. */ | |
| template <typename InputType> | |
| SVD(Context& ctx, const DenseBase<InputType>& A, unsigned int options = ComputeThinU | ComputeThinV) | |
| : solver_ctx_(ctx) { | |
| compute(A, options); | |
| } | |
| /** Bind to \p ctx and decompose a device-resident A (D2D copy). */ | |
| SVD(Context& ctx, const DeviceMatrix<Scalar>& d_A, unsigned int options = ComputeThinU | ComputeThinV) | |
| : solver_ctx_(ctx) { | |
| compute(d_A, options); | |
| } | |
| /** Bind to \p ctx and decompose a device-resident A (adopt when m >= n, no copy; a view is copied). */ | |
| SVD(Context& ctx, DeviceMatrix<Scalar>&& d_A, unsigned int options = ComputeThinU | ComputeThinV) : solver_ctx_(ctx) { | |
| compute(std::move(d_A), options); | |
| } | |
| ~SVD() = default; | |
| SVD(const SVD&) = delete; | |
| SVD& operator=(const SVD&) = delete; | |
| SVD(SVD&& o) noexcept | |
| : solver_ctx_(std::move(o.solver_ctx_)), | |
| d_A_(std::move(o.d_A_)), | |
| d_U_(std::move(o.d_U_)), | |
| d_S_(std::move(o.d_S_)), | |
| d_VT_(std::move(o.d_VT_)), | |
| d_D_(std::move(o.d_D_)), | |
| cached_diag_kk_(o.cached_diag_kk_), | |
| cached_diag_lambda_(o.cached_diag_lambda_), | |
| diag_valid_(o.diag_valid_), | |
| options_(o.options_), | |
| m_(o.m_), | |
| n_(o.n_), | |
| lda_(o.lda_), | |
| transposed_(o.transposed_) { | |
| o.diag_valid_ = false; | |
| o.options_ = 0; | |
| o.m_ = 0; | |
| o.n_ = 0; | |
| o.lda_ = 0; | |
| o.transposed_ = false; | |
| } | |
| SVD& operator=(SVD&& o) noexcept { | |
| if (this != &o) { | |
| solver_ctx_ = std::move(o.solver_ctx_); | |
| d_A_ = std::move(o.d_A_); | |
| d_U_ = std::move(o.d_U_); | |
| d_S_ = std::move(o.d_S_); | |
| d_VT_ = std::move(o.d_VT_); | |
| d_D_ = std::move(o.d_D_); | |
| cached_diag_kk_ = o.cached_diag_kk_; | |
| cached_diag_lambda_ = o.cached_diag_lambda_; | |
| diag_valid_ = o.diag_valid_; | |
| options_ = o.options_; | |
| m_ = o.m_; | |
| n_ = o.n_; | |
| lda_ = o.lda_; | |
| transposed_ = o.transposed_; | |
| o.diag_valid_ = false; | |
| o.options_ = 0; | |
| o.m_ = 0; | |
| o.n_ = 0; | |
| o.lda_ = 0; | |
| o.transposed_ = false; | |
| } | |
| return *this; | |
| } | |
| template <typename InputType> | |
| SVD& compute(const DenseBase<InputType>& A, unsigned int options = ComputeThinU | ComputeThinV) { | |
| // Upload to device, then delegate to the adopting overload — the freshly | |
| // uploaded matrix is consumed in place by gesvd, so no second device copy. | |
| // The wide-matrix transpose runs on the GPU (via cublasXgeam) inside the | |
| // device-input path; no host transpose. | |
| return compute(DeviceMatrix<Scalar>::fromHost(A.derived(), solver_ctx_.stream()), options); | |
| } | |
| SVD& compute(const DeviceMatrix<Scalar>& d_A, unsigned int options = ComputeThinU | ComputeThinV) { | |
| if (!begin_compute(d_A, options)) return *this; | |
| if (transposed_) { | |
| transpose_into_input(d_A); | |
| } else { | |
| const size_t mat_bytes = static_cast<size_t>(lda_) * static_cast<size_t>(n_) * sizeof(Scalar); | |
| d_A_ = internal::DeviceBuffer(mat_bytes); | |
| EIGEN_CUDA_RUNTIME_CHECK( | |
| cudaMemcpyAsync(d_A_.get(), d_A.data(), mat_bytes, cudaMemcpyDeviceToDevice, solver_ctx_.stream())); | |
| } | |
| factorize(); | |
| return *this; | |
| } | |
| /** Decompose a device matrix (move). For m >= n the buffer is adopted and | |
| * consumed in place by gesvd — no copy; for m < n a transposed copy is | |
| * unavoidable. A view is copied: its storage belongs to another object. */ | |
| SVD& compute(DeviceMatrix<Scalar>&& d_A, unsigned int options = ComputeThinU | ComputeThinV) { | |
| if (d_A.isView()) return compute(static_cast<const DeviceMatrix<Scalar>&>(d_A), options); | |
| if (!begin_compute(d_A, options)) return *this; | |
| if (transposed_) { | |
| transpose_into_input(d_A); | |
| } else { | |
| const size_t a_bytes = d_A.sizeInBytes(); | |
| d_A_ = internal::DeviceBuffer::adopt(static_cast<void*>(d_A.release()), a_bytes); | |
| } | |
| factorize(); | |
| return *this; | |
| } | |
| ComputationInfo info() const { return solver_ctx_.info(); } | |
| Index rows() const { return transposed_ ? n_ : m_; } | |
| Index cols() const { return transposed_ ? m_ : n_; } | |
| /** Singular values (always available). Downloads from device on each call. */ | |
| RealVector singularValues() const { | |
| eigen_assert(solver_ctx_.info() == Success); | |
| const Index k = (std::min)(m_, n_); | |
| RealVector S(k); | |
| solver_ctx_.download(S.data(), d_S_.get(), static_cast<size_t>(k) * sizeof(RealScalar)); | |
| return S; | |
| } | |
| /** Left singular vectors U. */ | |
| PlainMatrix matrixU() const { | |
| eigen_assert(solver_ctx_.info() == Success); | |
| eigen_assert((options_ & (ComputeThinU | ComputeFullU)) && "matrixU() requires ComputeThinU or ComputeFullU"); | |
| const Index m_orig = transposed_ ? n_ : m_; | |
| const Index n_orig = transposed_ ? m_ : n_; | |
| const Index k = (std::min)(m_orig, n_orig); | |
| if (!transposed_) { | |
| const Index ucols = (options_ & ComputeFullU) ? m_ : k; | |
| PlainMatrix U(m_, ucols); | |
| solver_ctx_.download(U.data(), d_U_.get(), static_cast<size_t>(m_) * static_cast<size_t>(ucols) * sizeof(Scalar)); | |
| return U; | |
| } else { | |
| const Index vtrows = (options_ & ComputeFullU) ? m_orig : k; | |
| PlainMatrix VT_stored(vtrows, n_); | |
| solver_ctx_.download(VT_stored.data(), d_VT_.get(), | |
| static_cast<size_t>(vtrows) * static_cast<size_t>(n_) * sizeof(Scalar)); | |
| return VT_stored.adjoint(); | |
| } | |
| } | |
| /** Right singular vectors V (matches host JacobiSVD/BDCSVD). */ | |
| PlainMatrix matrixV() const { return matrixVT().adjoint(); } | |
| /** Right singular vectors transposed V^T. */ | |
| PlainMatrix matrixVT() const { | |
| eigen_assert(solver_ctx_.info() == Success); | |
| eigen_assert((options_ & (ComputeThinV | ComputeFullV)) && "matrixVT() requires ComputeThinV or ComputeFullV"); | |
| const Index m_orig = transposed_ ? n_ : m_; | |
| const Index n_orig = transposed_ ? m_ : n_; | |
| const Index k = (std::min)(m_orig, n_orig); | |
| if (!transposed_) { | |
| const Index vtrows = (options_ & ComputeFullV) ? n_ : k; | |
| PlainMatrix VT(vtrows, n_); | |
| solver_ctx_.download(VT.data(), d_VT_.get(), | |
| static_cast<size_t>(vtrows) * static_cast<size_t>(n_) * sizeof(Scalar)); | |
| return VT; | |
| } else { | |
| const Index ucols = (options_ & ComputeFullV) ? n_orig : k; | |
| PlainMatrix U_stored(m_, ucols); | |
| solver_ctx_.download(U_stored.data(), d_U_.get(), | |
| static_cast<size_t>(m_) * static_cast<size_t>(ucols) * sizeof(Scalar)); | |
| return U_stored.adjoint(); | |
| } | |
| } | |
| // | |
| // These return non-owning DeviceMatrix views over the SVD's internal device storage. | |
| // The view borrows the pointer: destruction does not free; the SVD object must outlive | |
| // any view derived from it. For the common case (m >= n) all three accessors are pure | |
| // metadata: zero kernel launches, zero allocations. | |
| // | |
| // For wide matrices (m < n, internally factored as A^H), original U and V^T are the | |
| // adjoints of the stored buffers, so d_matrixU() / d_matrixVT() build them via a | |
| // cublasXgeam into an owning temporary. d_singularValues() remains zero-copy. | |
| /** Singular values as a k × 1 view on this solver's stream. */ | |
| DeviceMatrix<RealScalar> d_singularValues() const { | |
| eigen_assert(solver_ctx_.info() == Success); | |
| const Index k = (std::min)(m_, n_); | |
| auto v = DeviceMatrix<RealScalar>::view(static_cast<RealScalar*>(d_S_.get()), k, 1); | |
| v.recordReady(solver_ctx_.stream()); | |
| return v; | |
| } | |
| /** Left singular vectors U as a DeviceMatrix on this solver's stream. | |
| * For m >= n: zero-copy view. For m < n: owning (one cublasXgeam adjoint pass). */ | |
| DeviceMatrix<Scalar> d_matrixU() const { | |
| eigen_assert(solver_ctx_.info() == Success); | |
| eigen_assert((options_ & (ComputeThinU | ComputeFullU)) && "d_matrixU() requires ComputeThinU or ComputeFullU"); | |
| const Index m_orig = transposed_ ? n_ : m_; | |
| const Index n_orig = transposed_ ? m_ : n_; | |
| const Index k = (std::min)(m_orig, n_orig); | |
| if (!transposed_) { | |
| const Index ucols = (options_ & ComputeFullU) ? m_ : k; | |
| auto v = DeviceMatrix<Scalar>::view(static_cast<Scalar*>(d_U_.get()), m_, ucols); | |
| v.recordReady(solver_ctx_.stream()); | |
| return v; | |
| } | |
| // transposed: U_orig = VT_stored^H -> conjugate-transpose via cublasXgeam. | |
| const Index vtrows_stored = (options_ & ComputeFullU) ? n_ : k; | |
| DeviceMatrix<Scalar> result(n_, vtrows_stored); | |
| if (n_ > 0 && vtrows_stored > 0) { | |
| Scalar alpha_one(1), beta_zero(0); | |
| EIGEN_CUBLAS_CHECK(internal::cublasXgeam(solver_ctx_.cublasHandle(), CUBLAS_OP_C, CUBLAS_OP_N, n_, vtrows_stored, | |
| &alpha_one, static_cast<const Scalar*>(d_VT_.get()), vtrows_stored, | |
| &beta_zero, static_cast<const Scalar*>(nullptr), n_, result.data(), n_)); | |
| result.recordReady(solver_ctx_.stream()); | |
| } | |
| return result; | |
| } | |
| /** Right singular vectors transposed V^T as a DeviceMatrix on this solver's stream. | |
| * For m >= n: zero-copy view. For m < n: owning (one cublasXgeam adjoint pass). */ | |
| DeviceMatrix<Scalar> d_matrixVT() const { | |
| eigen_assert(solver_ctx_.info() == Success); | |
| eigen_assert((options_ & (ComputeThinV | ComputeFullV)) && "d_matrixVT() requires ComputeThinV or ComputeFullV"); | |
| const Index m_orig = transposed_ ? n_ : m_; | |
| const Index n_orig = transposed_ ? m_ : n_; | |
| const Index k = (std::min)(m_orig, n_orig); | |
| if (!transposed_) { | |
| const Index vtrows = (options_ & ComputeFullV) ? n_ : k; | |
| auto v = DeviceMatrix<Scalar>::view(static_cast<Scalar*>(d_VT_.get()), vtrows, n_); | |
| v.recordReady(solver_ctx_.stream()); | |
| return v; | |
| } | |
| // transposed: VT_orig = U_stored^H. | |
| const Index ucols = (options_ & ComputeFullV) ? n_orig : k; | |
| DeviceMatrix<Scalar> result(ucols, m_); | |
| if (ucols > 0 && m_ > 0) { | |
| Scalar alpha_one(1), beta_zero(0); | |
| EIGEN_CUBLAS_CHECK(internal::cublasXgeam(solver_ctx_.cublasHandle(), CUBLAS_OP_C, CUBLAS_OP_N, ucols, m_, | |
| &alpha_one, static_cast<const Scalar*>(d_U_.get()), m_, &beta_zero, | |
| static_cast<const Scalar*>(nullptr), ucols, result.data(), ucols)); | |
| result.recordReady(solver_ctx_.stream()); | |
| } | |
| return result; | |
| } | |
| /** Number of singular values above threshold. */ | |
| Index rank(RealScalar threshold = RealScalar(-1)) const { | |
| RealVector S = singularValues(); | |
| if (S.size() == 0) return 0; | |
| if (threshold < 0) { | |
| threshold = (std::max)(m_, n_) * S(0) * NumTraits<RealScalar>::epsilon(); | |
| } | |
| return (S.array() > threshold).count(); | |
| } | |
| /** Pseudoinverse solve: X = V * diag(1/S) * U^H * B. */ | |
| template <typename Rhs> | |
| PlainMatrix solve(const MatrixBase<Rhs>& B) const { | |
| return solve_impl(B, (std::min)(m_, n_), RealScalar(0)); | |
| } | |
| /** Truncated solve: use only top trunc singular triplets. */ | |
| template <typename Rhs> | |
| PlainMatrix solve(const MatrixBase<Rhs>& B, Index trunc) const { | |
| eigen_assert(trunc > 0 && trunc <= (std::min)(m_, n_)); | |
| return solve_impl(B, trunc, RealScalar(0)); | |
| } | |
| /** Tikhonov-regularized solve: D_ii = S_i / (S_i^2 + lambda^2). */ | |
| template <typename Rhs> | |
| PlainMatrix solve(const MatrixBase<Rhs>& B, RealScalar lambda) const { | |
| eigen_assert(lambda > 0); | |
| return solve_impl(B, (std::min)(m_, n_), lambda); | |
| } | |
| /** Pseudoinverse solve with device-resident RHS: returns an n × nrhs | |
| * DeviceMatrix that stays on device. The first solve for a given | |
| * (truncation, lambda) downloads the singular values (one host sync) to | |
| * build the inverse diagonal; subsequent solves reuse the cached device | |
| * diagonal and are fully asynchronous. */ | |
| DeviceMatrix<Scalar> solve(const DeviceMatrix<Scalar>& d_B) const { | |
| return solve_device_impl(d_B, (std::min)(m_, n_), RealScalar(0)); | |
| } | |
| /** Truncated device solve: use only top trunc singular triplets. */ | |
| DeviceMatrix<Scalar> solve(const DeviceMatrix<Scalar>& d_B, Index trunc) const { | |
| eigen_assert(trunc > 0 && trunc <= (std::min)(m_, n_)); | |
| return solve_device_impl(d_B, trunc, RealScalar(0)); | |
| } | |
| /** Tikhonov-regularized device solve: D_ii = S_i / (S_i^2 + lambda^2). */ | |
| DeviceMatrix<Scalar> solve(const DeviceMatrix<Scalar>& d_B, RealScalar lambda) const { | |
| eigen_assert(lambda > 0); | |
| return solve_device_impl(d_B, (std::min)(m_, n_), lambda); | |
| } | |
| cudaStream_t stream() const { return solver_ctx_.stream(); } | |
| private: | |
| mutable internal::GpuSolverContext solver_ctx_; | |
| internal::DeviceBuffer d_A_; // gesvd input scratch; released after factorize() | |
| internal::DeviceBuffer d_U_; // grow-only | |
| internal::DeviceBuffer d_S_; // grow-only | |
| internal::DeviceBuffer d_VT_; // grow-only | |
| // Cached inverse-diagonal for solve (built lazily, reused across solves; grow-only). | |
| mutable internal::DeviceBuffer d_D_; | |
| mutable Index cached_diag_kk_ = -1; | |
| mutable RealScalar cached_diag_lambda_ = RealScalar(-1); | |
| mutable bool diag_valid_ = false; | |
| unsigned int options_ = 0; | |
| int64_t m_ = 0; | |
| int64_t n_ = 0; | |
| int64_t lda_ = 0; | |
| bool transposed_ = false; | |
| // Common compute() prologue: record shape/options, reset info and cached | |
| // diagonal, wait on input. Returns false (clearing state) for empty input. | |
| bool begin_compute(const DeviceMatrix<Scalar>& d_A, unsigned int options) { | |
| options_ = options; | |
| m_ = d_A.rows(); | |
| n_ = d_A.cols(); | |
| lda_ = 0; | |
| transposed_ = false; | |
| diag_valid_ = false; | |
| if (!solver_ctx_.begin_compute(m_ != 0 && n_ != 0)) { | |
| d_A_ = internal::DeviceBuffer(); | |
| d_U_ = internal::DeviceBuffer(); | |
| d_S_ = internal::DeviceBuffer(); | |
| d_VT_ = internal::DeviceBuffer(); | |
| return false; | |
| } | |
| transposed_ = (m_ < n_); | |
| if (transposed_) { | |
| std::swap(m_, n_); | |
| lda_ = m_; | |
| } else { | |
| lda_ = static_cast<int64_t>(d_A.rows()); | |
| } | |
| d_A.waitReady(solver_ctx_.stream()); | |
| return true; | |
| } | |
| // Wide input (m < n): produce d_A_ = A^H on device via cuBLAS geam. | |
| void transpose_into_input(const DeviceMatrix<Scalar>& d_A) { | |
| const size_t mat_bytes = static_cast<size_t>(lda_) * static_cast<size_t>(n_) * sizeof(Scalar); | |
| d_A_ = internal::DeviceBuffer(mat_bytes); | |
| // geam: C(m×n) = alpha * op(A) + beta * op(B). beta=0, B=nullptr. | |
| Scalar alpha_one(1), beta_zero(0); | |
| EIGEN_CUBLAS_CHECK(internal::cublasXgeam(solver_ctx_.cublasHandle(), CUBLAS_OP_C, CUBLAS_OP_N, m_, n_, &alpha_one, | |
| d_A.data(), d_A.rows(), &beta_zero, static_cast<const Scalar*>(nullptr), | |
| m_, static_cast<Scalar*>(d_A_.get()), m_)); | |
| } | |
| // Swap U↔V flags for the transposed case. | |
| static unsigned int swap_uv_options(unsigned int opts) { | |
| unsigned int result = 0; | |
| if (opts & ComputeThinU) result |= ComputeThinV; | |
| if (opts & ComputeFullU) result |= ComputeFullV; | |
| if (opts & ComputeThinV) result |= ComputeThinU; | |
| if (opts & ComputeFullV) result |= ComputeFullU; | |
| return result; | |
| } | |
| static signed char jobu(unsigned int opts) { | |
| if (opts & ComputeFullU) return 'A'; | |
| if (opts & ComputeThinU) return 'S'; | |
| return 'N'; | |
| } | |
| static signed char jobvt(unsigned int opts) { | |
| if (opts & ComputeFullV) return 'A'; | |
| if (opts & ComputeThinV) return 'S'; | |
| return 'N'; | |
| } | |
| void factorize() { | |
| constexpr cudaDataType_t dtype = internal::cusolver_data_type<Scalar>::value; | |
| constexpr cudaDataType_t rtype = internal::cuda_data_type<RealScalar>::value; | |
| const Index k = (std::min)(m_, n_); | |
| solver_ctx_.mark_pending(); | |
| internal::ensure_sized(d_S_, static_cast<size_t>(k) * sizeof(RealScalar)); | |
| const unsigned int int_opts = transposed_ ? swap_uv_options(options_) : options_; | |
| const Index ucols = (int_opts & ComputeFullU) ? m_ : ((int_opts & ComputeThinU) ? k : 0); | |
| const Index vtrows = (int_opts & ComputeFullV) ? n_ : ((int_opts & ComputeThinV) ? k : 0); | |
| const int64_t ldu = m_; | |
| const int64_t ldvt = vtrows > 0 ? vtrows : 1; | |
| if (ucols > 0) { | |
| internal::ensure_sized(d_U_, static_cast<size_t>(m_) * static_cast<size_t>(ucols) * sizeof(Scalar)); | |
| } | |
| if (vtrows > 0) { | |
| internal::ensure_sized(d_VT_, static_cast<size_t>(vtrows) * static_cast<size_t>(n_) * sizeof(Scalar)); | |
| } | |
| eigen_assert(m_ >= n_ && "Internal error: m_ < n_ should have been handled by transpose in compute()"); | |
| size_t dev_ws = 0, host_ws = 0; | |
| EIGEN_CUSOLVER_CHECK(cusolverDnXgesvd_bufferSize( | |
| solver_ctx_.cusolverHandle(), solver_ctx_.params_.p, jobu(int_opts), jobvt(int_opts), m_, n_, dtype, d_A_.get(), | |
| lda_, rtype, d_S_.get(), dtype, ucols > 0 ? d_U_.get() : nullptr, ldu, dtype, | |
| vtrows > 0 ? d_VT_.get() : nullptr, ldvt, dtype, &dev_ws, &host_ws)); | |
| solver_ctx_.ensure_scratch(dev_ws); | |
| solver_ctx_.h_workspace_.resize(host_ws); | |
| EIGEN_CUSOLVER_CHECK(cusolverDnXgesvd( | |
| solver_ctx_.cusolverHandle(), solver_ctx_.params_.p, jobu(int_opts), jobvt(int_opts), m_, n_, dtype, d_A_.get(), | |
| lda_, rtype, d_S_.get(), dtype, ucols > 0 ? d_U_.get() : nullptr, ldu, dtype, | |
| vtrows > 0 ? d_VT_.get() : nullptr, ldvt, dtype, solver_ctx_.scratch_workspace(), dev_ws, | |
| host_ws > 0 ? solver_ctx_.h_workspace_.data() : nullptr, host_ws, solver_ctx_.scratch_info())); | |
| solver_ctx_.enqueue_info_copy(); | |
| // The input copy is pure gesvd scratch — release it now. The free is | |
| // stream-ordered (or synchronous on the fallback allocator), so it waits | |
| // for gesvd to retire; the memory returns to the pool instead of staying | |
| // resident for the solver's lifetime. | |
| d_A_ = internal::DeviceBuffer(); | |
| } | |
| // Ensure d_D_ holds the kk-entry inverse diagonal for (kk, lambda). | |
| // Downloads S and synchronizes only when the cached diagonal doesn't match; | |
| // repeated solves with the same truncation/regularization are then free of | |
| // host syncs and H2D traffic for the diagonal. | |
| // | |
| // For lambda == 0 we mirror Eigen's SVDBase::_solve_impl: drop singular | |
| // values below S(0) * k * eps (numerical-rank truncation), so this | |
| // pseudoinverse solve agrees with CPU BDCSVD::solve on near-singular A. | |
| // dgmm wants the diagonal in the matrix scalar type — for complex Scalar | |
| // the diagonal is still real, so we build the real values then cast. | |
| void build_diag(Index kk, RealScalar lambda) const { | |
| if (diag_valid_ && cached_diag_kk_ == kk && cached_diag_lambda_ == lambda) return; | |
| const Index k = (std::min)(m_, n_); | |
| RealVector S(k); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(S.data(), d_S_.get(), static_cast<size_t>(k) * sizeof(RealScalar), | |
| cudaMemcpyDeviceToHost, solver_ctx_.stream())); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(solver_ctx_.stream())); | |
| const RealScalar drop_threshold = S(0) * RealScalar(k) * NumTraits<RealScalar>::epsilon(); | |
| auto S_head = S.head(kk).array(); | |
| PlainVector D(kk); | |
| if (lambda == RealScalar(0)) { | |
| D = (S_head > drop_threshold).select(S_head.inverse(), RealScalar(0)).matrix().template cast<Scalar>(); | |
| } else { | |
| D = (S_head / (S_head.square() + lambda * lambda)).matrix().template cast<Scalar>(); | |
| } | |
| const size_t d_bytes = static_cast<size_t>(kk) * sizeof(Scalar); | |
| internal::ensure_sized(d_D_, d_bytes); | |
| EIGEN_CUDA_RUNTIME_CHECK( | |
| cudaMemcpyAsync(d_D_.get(), D.data(), d_bytes, cudaMemcpyHostToDevice, solver_ctx_.stream())); | |
| cached_diag_kk_ = kk; | |
| cached_diag_lambda_ = lambda; | |
| diag_valid_ = true; | |
| } | |
| // Shared pseudoinverse application: X = V_orig * diag(D) * U_orig^H * B, | |
| // entirely on device. Assumes build_diag(kk, ...) has run and B_dev/X_dev | |
| // are device pointers with leading dimensions m_orig / n_orig. | |
| void apply_pinv(const Scalar* B_dev, Index kk, Index nrhs, Scalar* X_dev) const { | |
| const Index m_orig = transposed_ ? n_ : m_; | |
| const Index n_orig = transposed_ ? m_ : n_; | |
| const Index k = (std::min)(m_, n_); | |
| auto* U_dev = static_cast<const Scalar*>(d_U_.get()); | |
| auto* VT_dev = static_cast<const Scalar*>(d_VT_.get()); | |
| Scalar scalars[2] = {Scalar(1), Scalar(0)}; | |
| // Step 1: tmp = U_orig^H * B (kk × nrhs). | |
| internal::DeviceBuffer d_tmp(static_cast<size_t>(kk) * static_cast<size_t>(nrhs) * sizeof(Scalar)); | |
| auto* tmp_dev = static_cast<Scalar*>(d_tmp.get()); | |
| if (!transposed_) { | |
| internal::cublaslt_gemm(solver_ctx_.cublasLtHandle(), solver_ctx_.cublasHandle(), CUBLAS_OP_C, CUBLAS_OP_N, kk, | |
| nrhs, m_, &scalars[0], U_dev, m_, B_dev, m_orig, &scalars[1], tmp_dev, kk, | |
| solver_ctx_.gemmWorkspace(), solver_ctx_.gemmPlanCache(), | |
| solver_ctx_.cublasLtMaxWorkspaceBytes(), solver_ctx_.stream()); | |
| } else { | |
| const Index vtrows_stored = (swap_uv_options(options_) & ComputeFullV) ? n_ : k; | |
| internal::cublaslt_gemm(solver_ctx_.cublasLtHandle(), solver_ctx_.cublasHandle(), CUBLAS_OP_N, CUBLAS_OP_N, kk, | |
| nrhs, m_orig, &scalars[0], VT_dev, vtrows_stored, B_dev, m_orig, &scalars[1], tmp_dev, kk, | |
| solver_ctx_.gemmWorkspace(), solver_ctx_.gemmPlanCache(), | |
| solver_ctx_.cublasLtMaxWorkspaceBytes(), solver_ctx_.stream()); | |
| } | |
| // Step 2: tmp = diag(D) * tmp on device via cublasXdgmm. | |
| EIGEN_CUBLAS_CHECK(internal::cublasXdgmm(solver_ctx_.cublasHandle(), CUBLAS_SIDE_LEFT, kk, nrhs, tmp_dev, kk, | |
| static_cast<const Scalar*>(d_D_.get()), 1, tmp_dev, kk)); | |
| // Step 3: X = V_orig * tmp (n_orig × nrhs). | |
| if (!transposed_) { | |
| const Index vtrows = (options_ & ComputeFullV) ? n_ : k; | |
| internal::cublaslt_gemm(solver_ctx_.cublasLtHandle(), solver_ctx_.cublasHandle(), CUBLAS_OP_C, CUBLAS_OP_N, | |
| n_orig, nrhs, kk, &scalars[0], VT_dev, vtrows, tmp_dev, kk, &scalars[1], X_dev, n_orig, | |
| solver_ctx_.gemmWorkspace(), solver_ctx_.gemmPlanCache(), | |
| solver_ctx_.cublasLtMaxWorkspaceBytes(), solver_ctx_.stream()); | |
| } else { | |
| internal::cublaslt_gemm(solver_ctx_.cublasLtHandle(), solver_ctx_.cublasHandle(), CUBLAS_OP_N, CUBLAS_OP_N, | |
| n_orig, nrhs, kk, &scalars[0], U_dev, m_, tmp_dev, kk, &scalars[1], X_dev, n_orig, | |
| solver_ctx_.gemmWorkspace(), solver_ctx_.gemmPlanCache(), | |
| solver_ctx_.cublasLtMaxWorkspaceBytes(), solver_ctx_.stream()); | |
| } | |
| } | |
| template <typename Rhs> | |
| PlainMatrix solve_impl(const MatrixBase<Rhs>& B, Index trunc, RealScalar lambda) const { | |
| eigen_assert(solver_ctx_.info() == Success && "SVD::solve called on a failed or uninitialized decomposition"); | |
| eigen_assert((options_ & (ComputeThinU | ComputeFullU)) && "solve requires U"); | |
| eigen_assert((options_ & (ComputeThinV | ComputeFullV)) && "solve requires V"); | |
| const Index m_orig = transposed_ ? n_ : m_; | |
| const Index n_orig = transposed_ ? m_ : n_; | |
| eigen_assert(B.rows() == m_orig); | |
| const Index k = (std::min)(m_, n_); | |
| const Index kk = (std::min)(trunc, k); | |
| const Index nrhs = B.cols(); | |
| // Empty problem: no rank, no RHS, or zero domain -> result is the zero matrix. | |
| if (kk == 0 || nrhs == 0 || n_orig == 0) { | |
| return PlainMatrix::Zero(n_orig, nrhs); | |
| } | |
| // Enqueue the B upload before build_diag: when the diagonal must be | |
| // (re)built, its S-download sync then also covers the in-flight upload — | |
| // one blocking wait instead of two. | |
| const Ref<const PlainMatrix> rhs(B.derived()); | |
| internal::DeviceBuffer d_B(static_cast<size_t>(m_orig) * static_cast<size_t>(nrhs) * sizeof(Scalar)); | |
| internal::upload_host_matrix(static_cast<Scalar*>(d_B.get()), m_orig, rhs.data(), rhs.outerStride(), m_orig, nrhs, | |
| solver_ctx_.stream()); | |
| build_diag(kk, lambda); | |
| PlainMatrix X(n_orig, nrhs); | |
| internal::DeviceBuffer d_X(static_cast<size_t>(n_orig) * static_cast<size_t>(nrhs) * sizeof(Scalar)); | |
| apply_pinv(static_cast<const Scalar*>(d_B.get()), kk, nrhs, static_cast<Scalar*>(d_X.get())); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(X.data(), d_X.get(), | |
| static_cast<size_t>(n_orig) * static_cast<size_t>(nrhs) * sizeof(Scalar), | |
| cudaMemcpyDeviceToHost, solver_ctx_.stream())); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(solver_ctx_.stream())); | |
| return X; | |
| } | |
| DeviceMatrix<Scalar> solve_device_impl(const DeviceMatrix<Scalar>& d_B, Index trunc, RealScalar lambda) const { | |
| eigen_assert(solver_ctx_.info() == Success && "SVD::solve called on a failed or uninitialized decomposition"); | |
| eigen_assert((options_ & (ComputeThinU | ComputeFullU)) && "solve requires U"); | |
| eigen_assert((options_ & (ComputeThinV | ComputeFullV)) && "solve requires V"); | |
| const Index m_orig = transposed_ ? n_ : m_; | |
| const Index n_orig = transposed_ ? m_ : n_; | |
| eigen_assert(d_B.rows() == m_orig); | |
| const Index k = (std::min)(m_, n_); | |
| const Index kk = (std::min)(trunc, k); | |
| const Index nrhs = d_B.cols(); | |
| if (kk == 0 || nrhs == 0 || n_orig == 0) { | |
| DeviceMatrix<Scalar> X(n_orig, nrhs); | |
| X.setZero(solver_ctx_.stream()); | |
| return X; | |
| } | |
| d_B.waitReady(solver_ctx_.stream()); | |
| build_diag(kk, lambda); | |
| DeviceMatrix<Scalar> X(n_orig, nrhs); | |
| apply_pinv(d_B.data(), kk, nrhs, X.data()); | |
| X.recordReady(solver_ctx_.stream()); | |
| return X; | |
| } | |
| }; | |
| } // namespace gpu | |
| } // namespace Eigen | |
| #endif // EIGEN_GPU_SVD_H |