blob: f3ba8f36656e93e29defbe70f6f9ef92425e3aeb [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
// Common base for the GPU sparse direct solvers (LLT, LDLT, LU) via cuDSS.
//
// All three share one analyzePattern → factorize → solve workflow and differ only
// in the cudssMatrixType_t and cudssMatrixViewType_t they pass to cuDSS, so this
// CRTP base implements the workflow and derived classes supply the two types.
//
// Not thread-safe: concurrent calls race on the cuDSS handle, the bound stream,
// and the cached scratch buffers. This includes concurrent solve() calls on one
// instance, const though solve() is. Use one solver per thread.
#ifndef EIGEN_GPU_SPARSE_SOLVER_BASE_H
#define EIGEN_GPU_SPARSE_SOLVER_BASE_H
// IWYU pragma: private
#include "./InternalHeaderCheck.h"
#include "./CuSparseSupport.h"
#include "./CuDssSupport.h"
namespace Eigen {
namespace gpu {
// The named algorithms carry the cuDSS values, so apply_config() can
// static_cast them. cuDSS < 0.8 names none of them, so there they are not
// declared at all: selecting an algorithm the linked cuDSS cannot honor is a
// compile error rather than a runtime rejection, and EIGEN_HAS_CUDSS_SOLVER_CONFIG
// lets a caller branch on which case a build is in.
// Default is declared either way and is a sentinel rather than a cuDSS value:
// apply_config() skips its cudssConfigSet(), leaving the cuDSS default in place
// without depending on its numeric value.
/** Fill-reducing reordering algorithm, applied during analyzePattern().
* Default lets cuDSS choose; Natural disables reordering. BtfColamd and
* Colamd are valid for general (SparseLU) matrices only. The named
* algorithms are declared only for cuDSS >= 0.8. */
enum class SparseReordering : int {
Default = -1,
#if EIGEN_HAS_CUDSS_SOLVER_CONFIG
BtfColamd = CUDSS_REORDERING_ALG_BTF_COLAMD,
Colamd = CUDSS_REORDERING_ALG_COLAMD,
Amd = CUDSS_REORDERING_ALG_AMD,
NestedDissection = CUDSS_REORDERING_ALG_NESTED_DISSECTION,
Natural = CUDSS_REORDERING_ALG_NONE,
#endif
};
/** Matching algorithm, applied during analyzePattern() to improve numerical
* robustness. Off by cuDSS default; primarily useful for SparseLU on
* indefinite or badly scaled systems. Auto lets cuDSS pick. The named
* algorithms are declared only for cuDSS >= 0.8. */
enum class SparseMatching : int {
Default = -1,
#if EIGEN_HAS_CUDSS_SOLVER_CONFIG
None = CUDSS_MATCHING_ALG_NONE,
MaxDiagCount = CUDSS_MATCHING_ALG_MAX_DIAG_COUNT,
MaxMinDiag = CUDSS_MATCHING_ALG_MAX_MIN_DIAG,
MaxMinDiagAlt = CUDSS_MATCHING_ALG_MAX_MIN_DIAG_ALT,
MaxDiagSum = CUDSS_MATCHING_ALG_MAX_DIAG_SUM,
MaxDiagProduct = CUDSS_MATCHING_ALG_MAX_DIAG_PRODUCT,
Auto = CUDSS_MATCHING_ALG_AUTO,
#endif
};
/** Pivoting strategy, applied during factorize(). Default resolves per
* matrix type. Validity of the other values depends on the matrix type and
* reordering algorithm; see the cuDSS documentation for cudssPivotType_t.
* The named strategies are declared only for cuDSS >= 0.8. */
enum class SparsePivoting : int {
Default = -1,
#if EIGEN_HAS_CUDSS_SOLVER_CONFIG
None = CUDSS_PIVOT_NONE,
GlobalCol = CUDSS_PIVOT_GLOBAL_COL,
GlobalRow = CUDSS_PIVOT_GLOBAL_ROW,
Diagonal = CUDSS_PIVOT_DIAGONAL,
LocalBlock = CUDSS_PIVOT_LOCAL_BLOCK,
#endif
};
/** Pass-through configuration for the cuDSS-backed sparse direct solvers
* (SparseLLT, SparseLDLT, SparseLU). Fields left at their defaults keep the
* corresponding cuDSS default, which is tuned for performance rather than
* maximum robustness (e.g. matching is off). Non-default fields require
* cuDSS >= 0.8, whose cudssReorderingAlg_t etc. name the algorithms with
* stable values; earlier versions declare no algorithm enumerator and accept
* only a default config, which EIGEN_HAS_CUDSS_SOLVER_CONFIG reports. */
struct SparseSolverConfig {
SparseReordering reordering = SparseReordering::Default;
SparseMatching matching = SparseMatching::Default;
SparsePivoting pivoting = SparsePivoting::Default;
/** Pivot admissibility threshold in [0, 1]; negative keeps the cuDSS default. */
double pivotThreshold = -1.0;
/** Perturbation applied to near-zero pivots; negative keeps the cuDSS default. */
double pivotEpsilon = -1.0;
/** Iterative-refinement steps during solve(); negative keeps the cuDSS default (0). */
int refinementSteps = -1;
/** Iterative-refinement stopping tolerance; negative keeps the cuDSS default. */
double refinementTolerance = -1.0;
/** Let factor data spill to host memory when device memory is insufficient.
* Changes what analyzePattern() builds — set it before that phase. */
bool hybridMemory = false;
/** Device-memory budget in bytes for hybridMemory; negative keeps the cuDSS
* default heuristic, which is what cuDSS itself spells as -1. Zero is a
* budget of its own and is passed through. */
int64_t hybridMemoryDeviceLimit = -1;
/** Split factorization/solve work between host and device. cuDSS requires
* this before analysis, which it changes — set it before analyzePattern(). */
bool hybridExecute = false;
/** True when every field keeps the cuDSS default. */
bool isDefault() const {
return reordering == SparseReordering::Default && matching == SparseMatching::Default &&
pivoting == SparsePivoting::Default && pivotThreshold < 0 && pivotEpsilon < 0 && refinementSteps < 0 &&
refinementTolerance < 0 && !hybridMemory && hybridMemoryDeviceLimit < 0 && !hybridExecute;
}
};
namespace internal {
/** CRTP base for GPU sparse direct solvers.
*
* \tparam Scalar_ Element type (passed explicitly to avoid incomplete-type issues with CRTP).
* \tparam Derived The concrete solver class (SparseLLT, SparseLDLT, SparseLU).
* Must provide:
* - `static constexpr cudssMatrixType_t cudss_matrix_type()`
* - `static constexpr cudssMatrixViewType_t cudss_matrix_view()`
* - `static constexpr bool needs_csr_conversion()`
*/
template <typename Scalar_, typename Derived>
class SparseSolverBase {
public:
using Scalar = Scalar_;
using RealScalar = typename NumTraits<Scalar>::Real;
using StorageIndex = int;
using SpMat = SparseMatrix<Scalar, ColMajor, StorageIndex>;
using CsrMat = SparseMatrix<Scalar, RowMajor, StorageIndex>;
using DenseVector = Matrix<Scalar, Dynamic, 1>;
using DenseMatrix = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
SparseSolverBase() { init_context(/*stream=*/nullptr, /*borrow_stream=*/false); }
/** Borrow \p ctx's stream: solver work runs on the same stream as the
* caller's other GPU operations (device-resident solves chain with SpMV /
* cuBLAS work without cross-stream event waits). The cuDSS handle itself is
* always owned by this solver. \p ctx must outlive this object. */
explicit SparseSolverBase(Context& ctx) { init_context(ctx.stream(), /*borrow_stream=*/true); }
~SparseSolverBase() {
destroy_cudss_objects();
if (handle_) (void)cudssDestroy(handle_);
if (owns_stream_ && stream_) (void)cudaStreamDestroy(stream_);
}
SparseSolverBase(const SparseSolverBase&) = delete;
SparseSolverBase& operator=(const SparseSolverBase&) = delete;
/** Apply \p cfg to this solver. Most knobs are consumed by the phase they
* affect — reordering and matching by analyzePattern(), pivoting by
* factorize(), refinement by solve() — so call setConfig() before the
* first phase whose behavior it changes; phases already executed are
* unaffected.
*
* The two hybrid *modes* are the exception. cuDSS builds different analysis
* state for them, and documents that analysis has to be redone when hybrid
* memory is enabled afterwards, so hybridMemory and hybridExecute must be
* set before analyzePattern(). Changing either one after analysis
* invalidates it: analyzePattern() has to be called again before
* factorize(), which would otherwise consume analysis state built for a
* different execution and memory mode. hybridMemoryDeviceLimit is only a
* budget within hybridMemory and may be changed between factorizations.
*
* Non-default fields require cuDSS >= 0.8, which EIGEN_HAS_CUDSS_SOLVER_CONFIG
* reports. Below that, the algorithm enumerators are not declared, so
* selecting one does not compile; a non-default value of the remaining
* fields is rejected rather than applied, asserting and leaving info()
* reporting InvalidInput until the config is reset to default. */
Derived& setConfig(const SparseSolverConfig& cfg) {
#if !EIGEN_HAS_CUDSS_SOLVER_CONFIG
eigen_assert(cfg.isDefault() && "SparseSolverConfig knobs require cuDSS >= 0.8");
if (!cfg.isDefault()) {
// The assert above is compiled out in release builds. Record what was
// asked for and fail through info() rather than factorizing with the
// cuDSS defaults this call meant to replace. analyzePattern() keeps
// failing while the stored config is one this cuDSS cannot honor.
config_opts_ = cfg;
analysis_done_ = false;
info_ = InvalidInput;
return derived();
}
#endif
const bool hybrid_mode_changed =
cfg.hybridMemory != config_opts_.hybridMemory || cfg.hybridExecute != config_opts_.hybridExecute;
eigen_assert((!hybrid_mode_changed || !analysis_done_) &&
"hybridMemory/hybridExecute change what analyzePattern() builds, so they must be set before it");
config_opts_ = cfg;
apply_config();
if (hybrid_mode_changed && analysis_done_) {
// The assert above is compiled out in release builds. Fail through
// info() rather than factorizing against the wrong analysis.
analysis_done_ = false;
info_ = InvalidInput;
}
return derived();
}
/** The configuration most recently passed to setConfig(). */
const SparseSolverConfig& config() const { return config_opts_; }
/** Symbolic analysis + numeric factorization. */
template <typename InputType>
Derived& compute(const SparseMatrixBase<InputType>& A) {
analyzePattern(A);
if (info_ == Success) {
factorize(A);
}
return derived();
}
/** Symbolic analysis only. Uploads sparsity structure to device.
* This phase is synchronous (blocks until complete). */
template <typename InputType>
Derived& analyzePattern(const SparseMatrixBase<InputType>& A) {
#if !EIGEN_HAS_CUDSS_SOLVER_CONFIG
if (!config_opts_.isDefault()) {
// setConfig() refused this config; analyzing as if it had been applied
// would hand factorize() state built for settings the caller replaced.
info_ = InvalidInput;
analysis_done_ = false;
return derived();
}
#endif
const InputType& input = A.derived();
check_storage_index_bounds<StorageIndex>(input.rows(), input.cols(), input.nonZeros());
eigen_assert(input.rows() == input.cols() && "GpuSparseSolver requires a square matrix");
n_ = input.rows();
info_ = InvalidInput;
analysis_done_ = false;
if (n_ == 0) {
nnz_ = 0;
info_ = Success;
analysis_done_ = true;
return derived();
}
// For symmetric solvers, ColMajor CSC can be reinterpreted as CSR with
// swapped triangle view — zero copy, except that Hermitian matrix types
// need the value array conjugated (see csc_upload_values). For general
// solvers, we must convert to actual RowMajor CSR so cuDSS sees the
// correct matrix, not A^T.
if (Derived::needs_csr_conversion()) {
const CsrMat csr(input);
nnz_ = csr.nonZeros();
upload_compressed(csr.outerIndexPtr(), csr.innerIndexPtr(), csr.valuePtr());
} else {
// Bind by reference when the input is already a compressed CSC matrix —
// no host copy.
SpMat storage;
const SpMat& csc = bind_sparse<SpMat>(input, storage);
nnz_ = csc.nonZeros();
upload_compressed(csc.outerIndexPtr(), csc.innerIndexPtr(), csc_upload_values(csc.valuePtr(), nnz_));
}
create_cudss_matrix();
if (data_) EIGEN_CUDSS_CHECK(cudssDataDestroy(handle_, data_));
EIGEN_CUDSS_CHECK(cudssDataCreate(handle_, &data_));
create_placeholder_dense();
EIGEN_CUDSS_CHECK(cudssExecute(handle_, CUDSS_PHASE_ANALYSIS, config_, data_, d_A_cudss_, d_x_cudss_, d_b_cudss_));
analysis_done_ = true;
info_ = Success;
return derived();
}
/** Numeric factorization using the symbolic analysis from analyzePattern.
*
* \warning The sparsity pattern (outerIndexPtr, innerIndexPtr) must be
* identical to the one passed to analyzePattern(). Only the numerical
* values may change. Passing a different pattern is undefined behavior.
* This matches the contract of CHOLMOD, UMFPACK, and cuDSS's own API.
*
* This phase is asynchronous — info() lazily synchronizes. */
template <typename InputType>
Derived& factorize(const SparseMatrixBase<InputType>& A) {
eigen_assert(analysis_done_ && "factorize() requires analyzePattern() first");
if (!analysis_done_) {
// Reachable in release builds whenever setConfig() invalidated the
// analysis rather than applying the request; see setConfig().
info_ = InvalidInput;
return derived();
}
if (n_ == 0) {
info_ = Success;
return derived();
}
// Convert to the same format used in analyzePattern. When the input is
// already a compressed CSC matrix (symmetric solvers), it is bound by
// reference — no host copy or conversion on refactorize (Hermitian types
// re-conjugate the value array; the pattern stays zero-copy).
// The temporaries must outlive the async memcpy (pageable H2D is actually
// synchronous w.r.t. the host, but keep them alive for clarity).
const InputType& input = A.derived();
check_storage_index_bounds<StorageIndex>(input.rows(), input.cols(), input.nonZeros());
eigen_assert(input.rows() == n_ && input.cols() == n_);
const Scalar* value_ptr;
Index value_nnz;
CsrMat csr_tmp;
SpMat csc_storage;
if (Derived::needs_csr_conversion()) {
csr_tmp = CsrMat(input);
value_ptr = csr_tmp.valuePtr();
value_nnz = csr_tmp.nonZeros();
} else {
const SpMat& csc = bind_sparse<SpMat>(input, csc_storage);
value_ptr = csc_upload_values(csc.valuePtr(), csc.nonZeros());
value_nnz = csc.nonZeros();
}
eigen_assert(value_nnz == nnz_);
EIGEN_UNUSED_VARIABLE(value_nnz);
EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_values_.get(), value_ptr, static_cast<size_t>(nnz_) * sizeof(Scalar),
cudaMemcpyHostToDevice, stream_));
EIGEN_CUDSS_CHECK(cudssMatrixSetValues(d_A_cudss_, d_values_.get()));
info_ = InvalidInput;
info_synced_ = false;
EIGEN_CUDSS_CHECK(
cudssExecute(handle_, CUDSS_PHASE_FACTORIZATION, config_, data_, d_A_cudss_, d_x_cudss_, d_b_cudss_));
return derived();
}
/** Solve A * X = B (host → host). Returns X as a dense matrix.
* Supports single or multiple right-hand sides. */
template <typename Rhs>
DenseMatrix solve(const MatrixBase<Rhs>& B) const {
// Debug builds verify the factorization (info() synchronizes on the first
// call after factorize()); release builds skip both the check and the
// sync — use info() explicitly when failure must be detected.
eigen_assert(info() == Success && "GpuSparseSolver::solve requires a successful factorization");
eigen_assert(B.rows() == n_);
if (n_ == 0) return DenseMatrix(0, B.cols());
const Ref<const DenseMatrix> rhs(B.derived());
const int64_t nrhs = static_cast<int64_t>(rhs.cols());
// Reuse cached d_b/d_x scratch to avoid cudaMalloc/cudaFree per solve.
const size_t rhs_bytes = static_cast<size_t>(n_) * static_cast<size_t>(nrhs) * sizeof(Scalar);
ensure_solve_buffer(d_b_solve_, rhs_bytes);
ensure_solve_buffer(d_x_solve_, rhs_bytes);
internal::upload_host_matrix(static_cast<Scalar*>(d_b_solve_.get()), n_, rhs.data(), rhs.outerStride(), n_, nrhs,
stream_);
update_solve_descriptors(nrhs, d_b_solve_.get(), d_x_solve_.get());
EIGEN_CUDSS_CHECK(
cudssExecute(handle_, CUDSS_PHASE_SOLVE, config_, data_, d_A_cudss_, x_solve_cudss_, b_solve_cudss_));
DenseMatrix X(n_, rhs.cols());
EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(X.data(), d_x_solve_.get(), rhs_bytes, cudaMemcpyDeviceToHost, stream_));
EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(stream_));
return X;
}
/** Solve A * X = B with device-resident RHS. Returns an n × nrhs
* DeviceMatrix that stays on device — no H2D/D2H transfer and no host
* synchronization (debug builds verify the factorization status first,
* which syncs once after each factorize()). Chain the result directly into
* SpMV / cuBLAS work, e.g. for iterative refinement. */
DeviceMatrix<Scalar> solve(const DeviceMatrix<Scalar>& d_B) const {
eigen_assert(info() == Success && "GpuSparseSolver::solve requires a successful factorization");
eigen_assert(d_B.rows() == n_);
const int64_t nrhs = static_cast<int64_t>(d_B.cols());
DeviceMatrix<Scalar> X(n_, d_B.cols());
if (n_ == 0 || nrhs == 0) return X;
d_B.waitReady(stream_);
update_solve_descriptors(nrhs, const_cast<Scalar*>(d_B.data()), X.data());
EIGEN_CUDSS_CHECK(
cudssExecute(handle_, CUDSS_PHASE_SOLVE, config_, data_, d_A_cudss_, x_solve_cudss_, b_solve_cudss_));
X.recordReady(stream_);
return X;
}
ComputationInfo info() const {
sync_info();
return info_;
}
Index rows() const { return n_; }
Index cols() const { return n_; }
cudaStream_t stream() const { return stream_; }
protected:
cudaStream_t stream_ = nullptr;
bool owns_stream_ = true;
cudssHandle_t handle_ = nullptr;
cudssConfig_t config_ = nullptr;
cudssData_t data_ = nullptr;
cudssMatrix_t d_A_cudss_ = nullptr;
cudssMatrix_t d_x_cudss_ = nullptr;
cudssMatrix_t d_b_cudss_ = nullptr;
DeviceBuffer d_rowPtr_;
DeviceBuffer d_colIdx_;
DeviceBuffer d_values_;
// Host staging for the conjugated value array (Hermitian zero-copy path
// only; see csc_upload_values). Kept as a member so it outlives the async
// H2D copy.
DenseVector conj_values_;
mutable DeviceBuffer d_b_solve_;
mutable DeviceBuffer d_x_solve_;
// Cached cuDSS dense descriptors for solve, re-pointed per call and
// recreated only when nrhs changes.
mutable cudssMatrix_t b_solve_cudss_ = nullptr;
mutable cudssMatrix_t x_solve_cudss_ = nullptr;
mutable int64_t solve_desc_nrhs_ = -1;
int64_t n_ = 0;
int64_t nnz_ = 0;
mutable ComputationInfo info_ = InvalidInput;
mutable bool info_synced_ = true;
bool analysis_done_ = false;
SparseSolverConfig config_opts_;
private:
Derived& derived() { return static_cast<Derived&>(*this); }
const Derived& derived() const { return static_cast<const Derived&>(*this); }
void init_context(cudaStream_t stream, bool borrow_stream) {
if (borrow_stream) {
// nullptr is CUDA's valid legacy default stream, so ownership cannot be
// inferred from the stream value.
stream_ = stream;
owns_stream_ = false;
} else {
EIGEN_CUDA_RUNTIME_CHECK(cudaStreamCreate(&stream_));
owns_stream_ = true;
}
EIGEN_CUDSS_CHECK(cudssCreate(&handle_));
EIGEN_CUDSS_CHECK(cudssSetStream(handle_, stream_));
EIGEN_CUDSS_CHECK(cudssConfigCreate(&config_));
}
// Rebuild config_ from config_opts_. Recreating rather than mutating lets a
// field reset to Default restore the cuDSS default, whose numeric value is
// not part of the cuDSS API contract.
void apply_config() {
#if EIGEN_HAS_CUDSS_SOLVER_CONFIG
if (config_) {
// cuDSS reads the config during execution, and both factorize() and the
// device-resident solve() return with their phase still queued. Retire
// that work before the config it is reading goes away.
EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(stream_));
(void)cudssConfigDestroy(config_);
}
config_ = nullptr;
EIGEN_CUDSS_CHECK(cudssConfigCreate(&config_));
const SparseSolverConfig& c = config_opts_;
if (c.reordering != SparseReordering::Default) {
const cudssReorderingAlg_t v = static_cast<cudssReorderingAlg_t>(c.reordering);
EIGEN_CUDSS_CHECK(cudssConfigSet(config_, CUDSS_CONFIG_REORDERING_ALG, &v, sizeof(v)));
}
if (c.matching != SparseMatching::Default) {
const cudssMatchingAlg_t v = static_cast<cudssMatchingAlg_t>(c.matching);
EIGEN_CUDSS_CHECK(cudssConfigSet(config_, CUDSS_CONFIG_MATCHING_ALG, &v, sizeof(v)));
}
if (c.pivoting != SparsePivoting::Default) {
const cudssPivotType_t v = static_cast<cudssPivotType_t>(c.pivoting);
EIGEN_CUDSS_CHECK(cudssConfigSet(config_, CUDSS_CONFIG_PIVOT_TYPE, &v, sizeof(v)));
}
if (c.pivotThreshold >= 0) {
EIGEN_CUDSS_CHECK(
cudssConfigSet(config_, CUDSS_CONFIG_PIVOT_THRESHOLD, &c.pivotThreshold, sizeof(c.pivotThreshold)));
}
if (c.pivotEpsilon >= 0) {
EIGEN_CUDSS_CHECK(cudssConfigSet(config_, CUDSS_CONFIG_PIVOT_EPSILON, &c.pivotEpsilon, sizeof(c.pivotEpsilon)));
}
if (c.refinementSteps >= 0) {
EIGEN_CUDSS_CHECK(
cudssConfigSet(config_, CUDSS_CONFIG_IR_N_STEPS, &c.refinementSteps, sizeof(c.refinementSteps)));
}
if (c.refinementTolerance >= 0) {
EIGEN_CUDSS_CHECK(
cudssConfigSet(config_, CUDSS_CONFIG_IR_TOL, &c.refinementTolerance, sizeof(c.refinementTolerance)));
}
if (c.hybridMemory) {
const int v = 1;
EIGEN_CUDSS_CHECK(cudssConfigSet(config_, CUDSS_CONFIG_HYBRID_MEMORY_MODE, &v, sizeof(v)));
if (c.hybridMemoryDeviceLimit >= 0) {
const int64_t limit = c.hybridMemoryDeviceLimit;
EIGEN_CUDSS_CHECK(cudssConfigSet(config_, CUDSS_CONFIG_HYBRID_DEVICE_MEMORY_LIMIT, &limit, sizeof(limit)));
}
}
if (c.hybridExecute) {
const int v = 1;
EIGEN_CUDSS_CHECK(cudssConfigSet(config_, CUDSS_CONFIG_HYBRID_EXECUTE_MODE, &v, sizeof(v)));
}
#else
// setConfig() returns early for anything else, so there is nothing to apply.
eigen_internal_assert(config_opts_.isDefault());
#endif
}
void ensure_solve_buffer(DeviceBuffer& buf, size_t needed) const {
if (needed > buf.size()) {
if (buf) EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(stream_));
buf = DeviceBuffer(needed);
}
}
// Recreate the solve descriptors when nrhs changes; otherwise just re-point
// them (cudssMatrixSetValues is a host-side pointer update).
void update_solve_descriptors(int64_t nrhs, void* b_ptr, void* x_ptr) const {
constexpr cudss_value_type_t dtype = to_cudss_data_type(cuda_data_type<Scalar>::value);
if (!b_solve_cudss_ || solve_desc_nrhs_ != nrhs) {
destroy_solve_descriptors();
EIGEN_CUDSS_CHECK(cudssMatrixCreateDn(&b_solve_cudss_, n_, nrhs, n_, b_ptr, dtype, CUDSS_LAYOUT_COL_MAJOR));
EIGEN_CUDSS_CHECK(cudssMatrixCreateDn(&x_solve_cudss_, n_, nrhs, n_, x_ptr, dtype, CUDSS_LAYOUT_COL_MAJOR));
solve_desc_nrhs_ = nrhs;
} else {
EIGEN_CUDSS_CHECK(cudssMatrixSetValues(b_solve_cudss_, b_ptr));
EIGEN_CUDSS_CHECK(cudssMatrixSetValues(x_solve_cudss_, x_ptr));
}
}
void destroy_solve_descriptors() const {
if (b_solve_cudss_) {
(void)cudssMatrixDestroy(b_solve_cudss_);
b_solve_cudss_ = nullptr;
}
if (x_solve_cudss_) {
(void)cudssMatrixDestroy(x_solve_cudss_);
x_solve_cudss_ = nullptr;
}
solve_desc_nrhs_ = -1;
}
void sync_info() const {
if (!info_synced_) {
// cudssDataGet for CUDSS_DATA_INFO synchronizes the stream internally,
// so an explicit cudaStreamSynchronize would be redundant.
int cudss_info = 0;
EIGEN_CUDSS_CHECK(cudssDataGet(handle_, data_, CUDSS_DATA_INFO, &cudss_info, sizeof(cudss_info), nullptr));
info_ = (cudss_info == 0) ? Success : NumericalIssue;
info_synced_ = true;
}
}
// Destructor-only cleanup: there is no useful recovery path for failures.
void destroy_cudss_objects() {
// Same lifetime rule as apply_config(): the queued phase reads the data
// handle and the matrix descriptors as well as the config. Unchecked
// because this runs from the destructor.
(void)cudaStreamSynchronize(stream_);
destroy_solve_descriptors();
if (d_A_cudss_) {
(void)cudssMatrixDestroy(d_A_cudss_);
d_A_cudss_ = nullptr;
}
if (d_x_cudss_) {
(void)cudssMatrixDestroy(d_x_cudss_);
d_x_cudss_ = nullptr;
}
if (d_b_cudss_) {
(void)cudssMatrixDestroy(d_b_cudss_);
d_b_cudss_ = nullptr;
}
if (data_) {
(void)cudssDataDestroy(handle_, data_);
data_ = nullptr;
}
if (config_) {
(void)cudssConfigDestroy(config_);
config_ = nullptr;
}
}
// The zero-copy CSC-as-CSR reinterpretation hands cuDSS the stored triangle
// of A^T. Real symmetric types are unaffected (A^T = A), but the Hermitian
// types need the triangle of A, whose entries are the conjugates of A^T's.
static constexpr bool needs_value_conjugation() {
return Derived::cudss_matrix_type() == CUDSS_MTYPE_HPD || Derived::cudss_matrix_type() == CUDSS_MTYPE_HERMITIAN;
}
// Value array for the zero-copy CSC-as-CSR path: conjugated into
// conj_values_ for Hermitian matrix types, passed through otherwise. The
// pattern arrays remain zero-copy either way.
const Scalar* csc_upload_values(const Scalar* values, Index nnz) {
if (!needs_value_conjugation()) return values;
conj_values_ = Map<const DenseVector>(values, nnz).conjugate();
return conj_values_.data();
}
void upload_compressed(const StorageIndex* outer, const StorageIndex* inner, const Scalar* values) {
const size_t rowptr_bytes = static_cast<size_t>(n_ + 1) * sizeof(StorageIndex);
const size_t colidx_bytes = static_cast<size_t>(nnz_) * sizeof(StorageIndex);
const size_t values_bytes = static_cast<size_t>(nnz_) * sizeof(Scalar);
d_rowPtr_ = DeviceBuffer(rowptr_bytes);
d_colIdx_ = DeviceBuffer(colidx_bytes);
d_values_ = DeviceBuffer(values_bytes);
EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_rowPtr_.get(), outer, rowptr_bytes, cudaMemcpyHostToDevice, stream_));
EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_colIdx_.get(), inner, colidx_bytes, cudaMemcpyHostToDevice, stream_));
EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_values_.get(), values, values_bytes, cudaMemcpyHostToDevice, stream_));
}
void create_cudss_matrix() {
if (d_A_cudss_) EIGEN_CUDSS_CHECK(cudssMatrixDestroy(d_A_cudss_));
constexpr cudss_value_type_t idx_type = to_cudss_data_type(cudss_index_type<StorageIndex>::value);
constexpr cudss_value_type_t val_type = to_cudss_data_type(cuda_data_type<Scalar>::value);
constexpr cudssMatrixType_t mtype = Derived::cudss_matrix_type();
constexpr cudssMatrixViewType_t mview = Derived::cudss_matrix_view();
#if defined(CUDSS_VERSION) && CUDSS_VERSION >= 800
// cuDSS 0.8 split the index type into separate offset/index type params.
EIGEN_CUDSS_CHECK(cudssMatrixCreateCsr(&d_A_cudss_, n_, n_, nnz_, d_rowPtr_.get(),
/*rowEnd=*/nullptr, d_colIdx_.get(), d_values_.get(), idx_type, idx_type,
val_type, mtype, mview, CUDSS_BASE_ZERO));
#else
EIGEN_CUDSS_CHECK(cudssMatrixCreateCsr(&d_A_cudss_, n_, n_, nnz_, d_rowPtr_.get(),
/*rowEnd=*/nullptr, d_colIdx_.get(), d_values_.get(), idx_type, val_type,
mtype, mview, CUDSS_BASE_ZERO));
#endif
}
void create_placeholder_dense() {
// A new analysis may change n_, so the cached solve descriptors are stale.
destroy_solve_descriptors();
if (d_x_cudss_) EIGEN_CUDSS_CHECK(cudssMatrixDestroy(d_x_cudss_));
if (d_b_cudss_) EIGEN_CUDSS_CHECK(cudssMatrixDestroy(d_b_cudss_));
constexpr cudss_value_type_t dtype = to_cudss_data_type(cuda_data_type<Scalar>::value);
EIGEN_CUDSS_CHECK(cudssMatrixCreateDn(&d_x_cudss_, n_, 1, n_, nullptr, dtype, CUDSS_LAYOUT_COL_MAJOR));
EIGEN_CUDSS_CHECK(cudssMatrixCreateDn(&d_b_cudss_, n_, 1, n_, nullptr, dtype, CUDSS_LAYOUT_COL_MAJOR));
}
};
} // namespace internal
} // namespace gpu
} // namespace Eigen
#endif // EIGEN_GPU_SPARSE_SOLVER_BASE_H