| // 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 || B.cols() == 0) return DenseMatrix(n_, 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 |