blob: 600a8737fb6091ee2b48fd1a8db7a1f9e64fafc5 [file]
// 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). */
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);
}
~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. */
SVD& compute(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 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);
EIGEN_CUDA_RUNTIME_CHECK(
cudaMemcpy(S.data(), d_S_.get(), static_cast<size_t>(k) * sizeof(RealScalar), cudaMemcpyDeviceToHost));
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);
EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpy(U.data(), d_U_.get(),
static_cast<size_t>(m_) * static_cast<size_t>(ucols) * sizeof(Scalar),
cudaMemcpyDeviceToHost));
return U;
} else {
const Index vtrows = (options_ & ComputeFullU) ? m_orig : k;
PlainMatrix VT_stored(vtrows, n_);
EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpy(VT_stored.data(), d_VT_.get(),
static_cast<size_t>(vtrows) * static_cast<size_t>(n_) * sizeof(Scalar),
cudaMemcpyDeviceToHost));
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_);
EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpy(VT.data(), d_VT_.get(),
static_cast<size_t>(vtrows) * static_cast<size_t>(n_) * sizeof(Scalar),
cudaMemcpyDeviceToHost));
return VT;
} else {
const Index ucols = (options_ & ComputeFullV) ? n_orig : k;
PlainMatrix U_stored(m_, ucols);
EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpy(U_stored.data(), d_U_.get(),
static_cast<size_t>(m_) * static_cast<size_t>(ucols) * sizeof(Scalar),
cudaMemcpyDeviceToHost));
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