| // 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 sparse matrix-vector (SpMV) and sparse matrix-dense matrix (SpMM) multiply | |
| // via cuSPARSE. | |
| // | |
| // SparseContext owns the cuSPARSE descriptors and device buffers. It takes | |
| // SparseMatrix<Scalar, ColMajor> (CSC), implicitly converting RowMajor input, and | |
| // can borrow a gpu::Context so that sparse products share a stream with BLAS-1 | |
| // operations — which removes the cross-stream event waits in solvers like CG. | |
| // | |
| // Caching: host-input calls re-upload the values *and* index arrays on every | |
| // call. Host pointer identity cannot detect a sparsity pattern rewritten in | |
| // place or assigned into the same allocations (SparseMatrix reuses them for | |
| // same-shape assignments), so the structure is never assumed unchanged. What | |
| // is cached are the cuSPARSE descriptors (sparse descriptor keyed on | |
| // dimensions + nonzero count, dense descriptors keyed on shape) and the | |
| // SpMV/SpMM workspace-size queries. For repeated products with no re-upload | |
| // at all, use deviceView(). | |
| // | |
| // Not thread-safe: concurrent multiply* calls on one instance race on the | |
| // cuSPARSE handle, the bound stream, and the cached buffers. Use one per thread. | |
| #ifndef EIGEN_GPU_SPARSE_CONTEXT_H | |
| #define EIGEN_GPU_SPARSE_CONTEXT_H | |
| // IWYU pragma: private | |
| #include "./InternalHeaderCheck.h" | |
| #include <cstdint> | |
| #include "./CuSparseSupport.h" | |
| #include "./FwdDecl.h" | |
| namespace Eigen { | |
| namespace gpu { | |
| /** Sparse product expression: DeviceSparseView * DeviceMatrix → SpMVExpr. | |
| * Evaluated by DeviceMatrix::operator=(SpMVExpr): dispatches to cusparseSpMV | |
| * when the dense operand has one column, cusparseSpMM otherwise. */ | |
| template <typename Scalar_> | |
| class SpMVExpr { | |
| public: | |
| using Scalar = Scalar_; | |
| SpMVExpr(const DeviceSparseView<Scalar>& view, const DeviceMatrix<Scalar>& x) : view_(view), x_(x) {} | |
| const DeviceSparseView<Scalar>& view() const { return view_; } | |
| const DeviceMatrix<Scalar>& x() const { return x_; } | |
| private: | |
| const DeviceSparseView<Scalar>& view_; | |
| const DeviceMatrix<Scalar>& x_; | |
| }; | |
| /** Device-resident sparse matrix view. Returned by SparseContext::deviceView(). | |
| * Lightweight handle referencing the context's cached device data. | |
| * | |
| * \warning One SparseContext caches one sparse matrix at a time. Any later | |
| * upload through the same context — a second deviceView() or any host-input | |
| * multiply — replaces the cached data and invalidates earlier views; a stale | |
| * view is caught by an assert at evaluation time via a generation counter. | |
| * For multiple simultaneous sparse matrices, use separate SparseContext | |
| * instances (they can share a Context for same-stream execution). | |
| * | |
| * Supports `d_y = d_A * d_x` (SpMV) and `d_Y = d_A * d_X` (SpMM). */ | |
| template <typename Scalar_> | |
| class DeviceSparseView { | |
| public: | |
| using Scalar = Scalar_; | |
| using SpMat = SparseMatrix<Scalar, ColMajor, int>; | |
| DeviceSparseView(SparseContext<Scalar>& ctx, Index rows, Index cols, uint64_t generation) | |
| : ctx_(ctx), rows_(rows), cols_(cols), generation_(generation) {} | |
| /** Sparse product expression: d_A * d_x. Evaluated by DeviceMatrix::operator=. */ | |
| SpMVExpr<Scalar> operator*(const DeviceMatrix<Scalar>& x) const { return SpMVExpr<Scalar>(*this, x); } | |
| Index rows() const { return rows_; } | |
| Index cols() const { return cols_; } | |
| const SparseContext<Scalar>& context() const { return ctx_; } | |
| /** Upload generation this view was created against. Used to detect stale | |
| * views after the context has cached a different matrix. */ | |
| uint64_t generation() const { return generation_; } | |
| private: | |
| SparseContext<Scalar>& ctx_; | |
| Index rows_; | |
| Index cols_; | |
| uint64_t generation_; | |
| }; | |
| template <typename Scalar_> | |
| class SparseContext { | |
| public: | |
| using Scalar = Scalar_; | |
| using RealScalar = typename NumTraits<Scalar>::Real; | |
| using StorageIndex = int; | |
| using SpMat = SparseMatrix<Scalar, ColMajor, StorageIndex>; | |
| using DenseVector = Matrix<Scalar, Dynamic, 1>; | |
| using DenseMatrix = Matrix<Scalar, Dynamic, Dynamic, ColMajor>; | |
| /** Standalone: creates own stream and cuSPARSE handle. */ | |
| SparseContext() : owns_handle_(true) { | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamCreate(&stream_)); | |
| owns_stream_ = true; | |
| EIGEN_CUSPARSE_CHECK(cusparseCreate(&handle_)); | |
| EIGEN_CUSPARSE_CHECK(cusparseSetStream(handle_, stream_)); | |
| } | |
| /** Borrow a Context: shares stream and cuSPARSE handle. | |
| * The Context must outlive this SparseContext. */ | |
| explicit SparseContext(Context& ctx) | |
| : stream_(ctx.stream()), handle_(ctx.cusparseHandle()), owns_stream_(false), owns_handle_(false) {} | |
| ~SparseContext() { | |
| destroy_spmat_descriptor(/*checked=*/false); | |
| destroy_dense_descriptors(); | |
| if (owns_handle_ && handle_) (void)cusparseDestroy(handle_); | |
| if (owns_stream_ && stream_) (void)cudaStreamDestroy(stream_); | |
| } | |
| SparseContext(const SparseContext&) = delete; | |
| SparseContext& operator=(const SparseContext&) = delete; | |
| /** Upload a sparse matrix to device and return a lightweight view. | |
| * The sparse data is uploaded immediately and cached in this context. | |
| * The returned view can be used for repeated SpMV/SpMM without re-uploading. | |
| * If the matrix values change, call deviceView() again to re-upload. | |
| * | |
| * \warning One context caches one matrix. Any later upload — another | |
| * deviceView() or any host-input multiply — overwrites the previous upload | |
| * and invalidates earlier views (asserted at evaluation time). For multiple | |
| * simultaneous matrices, use separate SparseContext instances sharing the | |
| * same Context. | |
| * | |
| * Supports `d_y = d_A * d_x` (SpMV) and `d_Y = d_A * d_X` (SpMM). */ | |
| DeviceSparseView<Scalar> deviceView(const SpMat& A) { | |
| eigen_assert(A.isCompressed()); | |
| upload_sparse(A); | |
| return DeviceSparseView<Scalar>(*this, A.rows(), A.cols(), generation_); | |
| } | |
| /** Generation counter of the currently cached sparse matrix. Bumped on | |
| * every sparse upload (deviceView() or a host-input multiply). */ | |
| uint64_t uploadGeneration() const { return generation_; } | |
| /** Compute y = A * x. Returns y as a new dense vector. */ | |
| template <typename InputType, typename Rhs> | |
| DenseVector multiply(const SparseMatrixBase<InputType>& A, const MatrixBase<Rhs>& x) { | |
| return multiply_host_return(A, x, GpuOp::NoTrans); | |
| } | |
| /** Compute y = alpha * op(A) * x + beta * y (in-place, host vectors). */ | |
| template <typename InputType, typename Rhs, typename Dest> | |
| void multiply(const SparseMatrixBase<InputType>& A, const MatrixBase<Rhs>& x, MatrixBase<Dest>& y, | |
| Scalar alpha = Scalar(1), Scalar beta = Scalar(0), GpuOp op = GpuOp::NoTrans) { | |
| const InputType& input = A.derived(); | |
| internal::check_storage_index_bounds<StorageIndex>(input.rows(), input.cols(), input.nonZeros()); | |
| SpMat storage; | |
| const SpMat& mat = internal::bind_sparse<SpMat>(input, storage); | |
| multiply_host_impl(mat, x.derived(), y.derived(), alpha, beta, internal::to_cusparse_op<Scalar>(op)); | |
| } | |
| /** Compute d_y = A * d_x. Device-resident dense vectors, no host transfer | |
| * for x/y. The sparse matrix (values and index arrays) is re-uploaded on | |
| * each call; for a fully device-resident sparse matrix use deviceView(). */ | |
| template <typename InputType> | |
| void multiply(const SparseMatrixBase<InputType>& A, const DeviceMatrix<Scalar>& d_x, DeviceMatrix<Scalar>& d_y) { | |
| multiply(A, d_x, d_y, Scalar(1), Scalar(0), GpuOp::NoTrans); | |
| } | |
| /** Compute d_y = alpha * op(A) * d_x + beta * d_y (DeviceMatrix, in-place). */ | |
| template <typename InputType> | |
| void multiply(const SparseMatrixBase<InputType>& A, const DeviceMatrix<Scalar>& d_x, DeviceMatrix<Scalar>& d_y, | |
| Scalar alpha, Scalar beta, GpuOp op = GpuOp::NoTrans) { | |
| const InputType& input = A.derived(); | |
| internal::check_storage_index_bounds<StorageIndex>(input.rows(), input.cols(), input.nonZeros()); | |
| SpMat storage; | |
| const SpMat& mat = internal::bind_sparse<SpMat>(input, storage); | |
| upload_sparse(mat); | |
| spmv_device_exec(d_x, d_y, alpha, beta, op); | |
| } | |
| /** Compute y = A^T * x (host vectors). */ | |
| template <typename InputType, typename Rhs> | |
| DenseVector multiplyT(const SparseMatrixBase<InputType>& A, const MatrixBase<Rhs>& x) { | |
| return multiply_host_return(A, x, GpuOp::Trans); | |
| } | |
| /** Compute y = A^H * x (conjugate transpose). For real Scalar this is equivalent to multiplyT. */ | |
| template <typename InputType, typename Rhs> | |
| DenseVector multiplyAdjoint(const SparseMatrixBase<InputType>& A, const MatrixBase<Rhs>& x) { | |
| return multiply_host_return(A, x, GpuOp::ConjTrans); | |
| } | |
| /** Compute Y = op(A) * X where X is a dense matrix (multiple RHS). Returns Y. */ | |
| template <typename InputType, typename Rhs> | |
| DenseMatrix multiplyMat(const SparseMatrixBase<InputType>& A, const MatrixBase<Rhs>& X, GpuOp op = GpuOp::NoTrans) { | |
| const InputType& input = A.derived(); | |
| internal::check_storage_index_bounds<StorageIndex>(input.rows(), input.cols(), input.nonZeros()); | |
| SpMat storage; | |
| const SpMat& mat = internal::bind_sparse<SpMat>(input, storage); | |
| const DenseMatrix rhs(X.derived()); | |
| const cusparseOperation_t cu_op = internal::to_cusparse_op<Scalar>(op); | |
| const Index m = (op == GpuOp::NoTrans) ? mat.rows() : mat.cols(); | |
| const Index k = (op == GpuOp::NoTrans) ? mat.cols() : mat.rows(); | |
| eigen_assert(k == rhs.rows()); | |
| const Index n = rhs.cols(); | |
| if (m == 0 || n == 0 || mat.nonZeros() == 0) return DenseMatrix::Zero(m, n); | |
| DenseMatrix Y = DenseMatrix::Zero(m, n); | |
| spmm_impl(mat, rhs, Y, Scalar(1), Scalar(0), cu_op); | |
| return Y; | |
| } | |
| cudaStream_t stream() const { return stream_; } | |
| private: | |
| cudaStream_t stream_ = nullptr; | |
| cusparseHandle_t handle_ = nullptr; | |
| bool owns_stream_ = false; | |
| bool owns_handle_ = false; | |
| // Cached device buffers for sparse matrix (grow-only). | |
| internal::DeviceBuffer d_outerPtr_; | |
| internal::DeviceBuffer d_innerIdx_; | |
| internal::DeviceBuffer d_values_; | |
| // Cached device buffers for host-API dense vectors (grow-only). | |
| internal::DeviceBuffer d_x_; | |
| internal::DeviceBuffer d_y_; | |
| mutable internal::DeviceBuffer d_workspace_; | |
| // Cached cuSPARSE sparse matrix descriptor. | |
| cusparseSpMatDescr_t spmat_desc_ = nullptr; | |
| Index cached_rows_ = -1; | |
| Index cached_cols_ = -1; | |
| Index cached_nnz_ = -1; | |
| // Bumped on every sparse upload; DeviceSparseViews record it at creation so | |
| // a stale view (its data replaced by a later upload) asserts at evaluation. | |
| uint64_t generation_ = 0; | |
| // Cached dense-vector/matrix descriptors, re-pointed per call and recreated | |
| // only when the shape changes. | |
| mutable cusparseDnVecDescr_t x_vec_desc_ = nullptr; | |
| mutable cusparseDnVecDescr_t y_vec_desc_ = nullptr; | |
| mutable int64_t x_vec_size_ = -1; | |
| mutable int64_t y_vec_size_ = -1; | |
| mutable cusparseDnMatDescr_t x_mat_desc_ = nullptr; | |
| mutable cusparseDnMatDescr_t y_mat_desc_ = nullptr; | |
| mutable int64_t x_mat_rows_ = -1, x_mat_cols_ = -1; | |
| mutable int64_t y_mat_rows_ = -1, y_mat_cols_ = -1; | |
| // Cached workspace-size query results, indexed by cusparseOperation_t. | |
| // Invalidated when the sparse descriptor or a dense-descriptor shape changes. | |
| static constexpr size_t kWsUnknown = static_cast<size_t>(-1); | |
| mutable size_t spmv_ws_size_[3] = {kWsUnknown, kWsUnknown, kWsUnknown}; | |
| mutable size_t spmm_ws_size_[3] = {kWsUnknown, kWsUnknown, kWsUnknown}; | |
| // Shared host-input SpMV entry: y = op(A) * x into a fresh vector. | |
| template <typename InputType, typename Rhs> | |
| DenseVector multiply_host_return(const SparseMatrixBase<InputType>& A, const MatrixBase<Rhs>& x, GpuOp op) { | |
| const InputType& input = A.derived(); | |
| internal::check_storage_index_bounds<StorageIndex>(input.rows(), input.cols(), input.nonZeros()); | |
| SpMat storage; | |
| const SpMat& mat = internal::bind_sparse<SpMat>(input, storage); | |
| DenseVector y((op == GpuOp::NoTrans) ? mat.rows() : mat.cols()); | |
| y.setZero(); | |
| multiply_host_impl(mat, x.derived(), y, Scalar(1), Scalar(0), internal::to_cusparse_op<Scalar>(op)); | |
| return y; | |
| } | |
| template <typename RhsDerived, typename DestDerived> | |
| void multiply_host_impl(const SpMat& A, const RhsDerived& x, DestDerived& y, Scalar alpha, Scalar beta, | |
| cusparseOperation_t op) { | |
| eigen_assert(A.isCompressed()); | |
| const Index m = A.rows(); | |
| const Index n = A.cols(); | |
| const Index nnz = A.nonZeros(); | |
| const Index x_size = (op == CUSPARSE_OPERATION_NON_TRANSPOSE) ? n : m; | |
| const Index y_size = (op == CUSPARSE_OPERATION_NON_TRANSPOSE) ? m : n; | |
| eigen_assert(x.size() == x_size); | |
| eigen_assert(y.size() == y_size); | |
| if (m == 0 || n == 0 || nnz == 0) { | |
| if (beta == Scalar(0)) | |
| y.setZero(); | |
| else | |
| y *= beta; | |
| return; | |
| } | |
| upload_sparse(A); | |
| ensure_buffer(d_x_, static_cast<size_t>(x_size) * sizeof(Scalar)); | |
| // Ref binds in place when x is already a contiguous vector; only genuine | |
| // expressions are evaluated into the Ref's internal temporary. | |
| const Ref<const DenseVector> x_ref(x); | |
| EIGEN_CUDA_RUNTIME_CHECK( | |
| cudaMemcpyAsync(d_x_.get(), x_ref.data(), x_size * sizeof(Scalar), cudaMemcpyHostToDevice, stream_)); | |
| ensure_buffer(d_y_, static_cast<size_t>(y_size) * sizeof(Scalar)); | |
| if (beta != Scalar(0)) { | |
| const Ref<const DenseVector> y_ref(y); | |
| EIGEN_CUDA_RUNTIME_CHECK( | |
| cudaMemcpyAsync(d_y_.get(), y_ref.data(), y_size * sizeof(Scalar), cudaMemcpyHostToDevice, stream_)); | |
| } | |
| exec_spmv(x_size, y_size, d_x_.get(), d_y_.get(), alpha, beta, op); | |
| EIGEN_CUDA_RUNTIME_CHECK( | |
| cudaMemcpyAsync(y.data(), d_y_.get(), y_size * sizeof(Scalar), cudaMemcpyDeviceToHost, stream_)); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(stream_)); | |
| } | |
| public: | |
| /** Execute SpMV using the already-uploaded sparse matrix (no re-upload). | |
| * Used by SpMVExpr (d_y = d_A * d_x) for cached deviceView() paths. | |
| * The sparse matrix must have been uploaded via deviceView() or multiply(). */ | |
| void spmv_device_exec(const DeviceMatrix<Scalar>& d_x, DeviceMatrix<Scalar>& d_y, Scalar alpha = Scalar(1), | |
| Scalar beta = Scalar(0), GpuOp op = GpuOp::NoTrans) const { | |
| eigen_assert(spmat_desc_ && "sparse matrix not uploaded — call deviceView() or multiply() first"); | |
| // cuSPARSE SpMV: y must not alias x (undefined behavior). | |
| eigen_assert(d_x.data() != d_y.data() && "SpMV: output aliases input vector"); | |
| const cusparseOperation_t cu_op = internal::to_cusparse_op<Scalar>(op); | |
| const Index m = cached_rows_; | |
| const Index n = cached_cols_; | |
| const Index x_size = (cu_op == CUSPARSE_OPERATION_NON_TRANSPOSE) ? n : m; | |
| const Index y_size = (cu_op == CUSPARSE_OPERATION_NON_TRANSPOSE) ? m : n; | |
| eigen_assert(d_x.rows() * d_x.cols() == x_size); | |
| if (m == 0 || n == 0 || cached_nnz_ == 0) { | |
| // Empty A reduces SpMV to y <- beta*y; SparseContext owns no cuBLAS | |
| // handle for the scale, so the beta != 0 case must be handled by the caller. | |
| eigen_assert(beta == Scalar(0) && "SpMV with empty A and beta != 0 is unsupported; scale d_y externally"); | |
| if (d_y.rows() * d_y.cols() != y_size) d_y.resize(y_size, 1); | |
| d_y.setZero(stream_); | |
| return; | |
| } | |
| // Ensure d_y is allocated. | |
| if (d_y.rows() * d_y.cols() != y_size) { | |
| d_y.resize(y_size, 1); | |
| } | |
| // Wait for input data to be ready on this stream. | |
| d_x.waitReady(stream_); | |
| d_y.waitReady(stream_); | |
| exec_spmv(x_size, y_size, const_cast<void*>(static_cast<const void*>(d_x.data())), static_cast<void*>(d_y.data()), | |
| alpha, beta, cu_op); | |
| d_y.recordReady(stream_); | |
| } | |
| /** Execute SpMM (d_Y = alpha * op(A) * d_X + beta * d_Y) using the | |
| * already-uploaded sparse matrix. d_X / d_Y are device-resident dense | |
| * column-major matrices. Used by SpMVExpr when the RHS has > 1 column. */ | |
| void spmm_device_exec(const DeviceMatrix<Scalar>& d_X, DeviceMatrix<Scalar>& d_Y, Scalar alpha = Scalar(1), | |
| Scalar beta = Scalar(0), GpuOp op = GpuOp::NoTrans) const { | |
| eigen_assert(spmat_desc_ && "sparse matrix not uploaded — call deviceView() or multiply() first"); | |
| eigen_assert(d_X.data() != d_Y.data() && "SpMM: output aliases input matrix"); | |
| const cusparseOperation_t cu_op = internal::to_cusparse_op<Scalar>(op); | |
| const bool transposed = (cu_op != CUSPARSE_OPERATION_NON_TRANSPOSE); | |
| const Index m_op = transposed ? cached_cols_ : cached_rows_; | |
| const Index k_op = transposed ? cached_rows_ : cached_cols_; | |
| const Index n = d_X.cols(); | |
| eigen_assert(d_X.rows() == k_op); | |
| if (m_op == 0 || n == 0 || cached_nnz_ == 0) { | |
| eigen_assert(beta == Scalar(0) && "SpMM with empty A and beta != 0 is unsupported; scale d_Y externally"); | |
| if (d_Y.rows() != m_op || d_Y.cols() != n) d_Y.resize(m_op, n); | |
| d_Y.setZero(stream_); | |
| return; | |
| } | |
| if (d_Y.rows() != m_op || d_Y.cols() != n) { | |
| d_Y.resize(m_op, n); | |
| } | |
| d_X.waitReady(stream_); | |
| d_Y.waitReady(stream_); | |
| exec_spmm(m_op, k_op, n, const_cast<void*>(static_cast<const void*>(d_X.data())), static_cast<void*>(d_Y.data()), | |
| alpha, beta, cu_op); | |
| d_Y.recordReady(stream_); | |
| } | |
| private: | |
| // cuSPARSE 11.x's cusparseSpMM rejects CSC for matA (CSC support landed in | |
| // CUDA 12.0). On 11.x we register the same buffers as CSR-of-A^T (dims | |
| // swapped) and invert the user-facing op before each cuSPARSE call. On 12+ | |
| // we keep the natural CSC path so users pay no extra cost. | |
| #if !defined(CUSPARSE_VERSION) || CUSPARSE_VERSION < 12000 | |
| static constexpr bool kUseCsrOfTranspose = true; | |
| static constexpr cusparseSpMMAlg_t kSpMMAlg = CUSPARSE_SPMM_CSR_ALG2; | |
| #else | |
| static constexpr bool kUseCsrOfTranspose = false; | |
| static constexpr cusparseSpMMAlg_t kSpMMAlg = CUSPARSE_SPMM_ALG_DEFAULT; | |
| #endif | |
| // Map a user-facing op on A to the cuSPARSE op on the cached descriptor. | |
| // Identity on cuSPARSE 12+ (descriptor is CSC of A); inverted on 11.x | |
| // (descriptor is CSR of A^T). | |
| static cusparseOperation_t descriptor_op(cusparseOperation_t user_op) { | |
| EIGEN_IF_CONSTEXPR (!kUseCsrOfTranspose) return user_op; | |
| switch (user_op) { | |
| case CUSPARSE_OPERATION_NON_TRANSPOSE: | |
| return CUSPARSE_OPERATION_TRANSPOSE; | |
| case CUSPARSE_OPERATION_TRANSPOSE: | |
| return CUSPARSE_OPERATION_NON_TRANSPOSE; | |
| default: | |
| // CONJUGATE_TRANSPOSE on the CSR-of-A^T descriptor would compute | |
| // conj(A) * x, not A^H * x — not supported via this representation. | |
| eigen_assert(false && "CUSPARSE_OPERATION_CONJUGATE_TRANSPOSE not supported on cuSPARSE < 12.0"); | |
| return user_op; | |
| } | |
| } | |
| static int op_index(cusparseOperation_t op) { | |
| switch (op) { | |
| case CUSPARSE_OPERATION_TRANSPOSE: | |
| return 1; | |
| case CUSPARSE_OPERATION_CONJUGATE_TRANSPOSE: | |
| return 2; | |
| default: | |
| return 0; | |
| } | |
| } | |
| void invalidate_ws_caches() const { | |
| for (int i = 0; i < 3; ++i) { | |
| spmv_ws_size_[i] = kWsUnknown; | |
| spmm_ws_size_[i] = kWsUnknown; | |
| } | |
| } | |
| // Recreate the descriptor when the size changes; otherwise just re-point it | |
| // at the new device buffer (cusparseDnVecSetValues is a host-side pointer | |
| // update, no GPU work). | |
| void update_dnvec(cusparseDnVecDescr_t& desc, int64_t& cur_size, int64_t size, void* ptr) const { | |
| if (!desc || cur_size != size) { | |
| if (desc) EIGEN_CUSPARSE_CHECK(cusparseDestroyDnVec(desc)); | |
| EIGEN_CUSPARSE_CHECK(cusparseCreateDnVec(&desc, size, ptr, internal::cuda_data_type<Scalar>::value)); | |
| cur_size = size; | |
| invalidate_ws_caches(); | |
| } else { | |
| EIGEN_CUSPARSE_CHECK(cusparseDnVecSetValues(desc, ptr)); | |
| } | |
| } | |
| void update_dnmat(cusparseDnMatDescr_t& desc, int64_t& cur_rows, int64_t& cur_cols, int64_t rows, int64_t cols, | |
| void* ptr) const { | |
| if (!desc || cur_rows != rows || cur_cols != cols) { | |
| if (desc) EIGEN_CUSPARSE_CHECK(cusparseDestroyDnMat(desc)); | |
| // Column-major with ld = rows. | |
| EIGEN_CUSPARSE_CHECK(cusparseCreateDnMat(&desc, rows, cols, rows, ptr, internal::cuda_data_type<Scalar>::value, | |
| CUSPARSE_ORDER_COL)); | |
| cur_rows = rows; | |
| cur_cols = cols; | |
| invalidate_ws_caches(); | |
| } else { | |
| EIGEN_CUSPARSE_CHECK(cusparseDnMatSetValues(desc, ptr)); | |
| } | |
| } | |
| void destroy_dense_descriptors() { | |
| if (x_vec_desc_) (void)cusparseDestroyDnVec(x_vec_desc_); | |
| if (y_vec_desc_) (void)cusparseDestroyDnVec(y_vec_desc_); | |
| if (x_mat_desc_) (void)cusparseDestroyDnMat(x_mat_desc_); | |
| if (y_mat_desc_) (void)cusparseDestroyDnMat(y_mat_desc_); | |
| x_vec_desc_ = y_vec_desc_ = nullptr; | |
| x_mat_desc_ = y_mat_desc_ = nullptr; | |
| } | |
| void exec_spmv(Index x_size, Index y_size, void* d_x_ptr, void* d_y_ptr, Scalar alpha, Scalar beta, | |
| cusparseOperation_t op) const { | |
| constexpr cudaDataType_t dtype = internal::cuda_data_type<Scalar>::value; | |
| const cusparseOperation_t cu_op = descriptor_op(op); | |
| update_dnvec(x_vec_desc_, x_vec_size_, x_size, d_x_ptr); | |
| update_dnvec(y_vec_desc_, y_vec_size_, y_size, d_y_ptr); | |
| size_t& ws_size = spmv_ws_size_[op_index(cu_op)]; | |
| if (ws_size == kWsUnknown) { | |
| EIGEN_CUSPARSE_CHECK(cusparseSpMV_bufferSize(handle_, cu_op, &alpha, spmat_desc_, x_vec_desc_, &beta, y_vec_desc_, | |
| dtype, CUSPARSE_SPMV_ALG_DEFAULT, &ws_size)); | |
| } | |
| ensure_buffer(d_workspace_, ws_size); | |
| EIGEN_CUSPARSE_CHECK(cusparseSpMV(handle_, cu_op, &alpha, spmat_desc_, x_vec_desc_, &beta, y_vec_desc_, dtype, | |
| CUSPARSE_SPMV_ALG_DEFAULT, d_workspace_.get())); | |
| } | |
| void exec_spmm(Index m_op, Index k_op, Index n, void* d_x_ptr, void* d_y_ptr, Scalar alpha, Scalar beta, | |
| cusparseOperation_t op) const { | |
| constexpr cudaDataType_t dtype = internal::cuda_data_type<Scalar>::value; | |
| const cusparseOperation_t cu_op = descriptor_op(op); | |
| // X is k_op x n, Y is m_op x n (column-major, post-op shapes). | |
| update_dnmat(x_mat_desc_, x_mat_rows_, x_mat_cols_, k_op, n, d_x_ptr); | |
| update_dnmat(y_mat_desc_, y_mat_rows_, y_mat_cols_, m_op, n, d_y_ptr); | |
| size_t& ws_size = spmm_ws_size_[op_index(cu_op)]; | |
| if (ws_size == kWsUnknown) { | |
| EIGEN_CUSPARSE_CHECK(cusparseSpMM_bufferSize(handle_, cu_op, CUSPARSE_OPERATION_NON_TRANSPOSE, &alpha, | |
| spmat_desc_, x_mat_desc_, &beta, y_mat_desc_, dtype, kSpMMAlg, | |
| &ws_size)); | |
| } | |
| ensure_buffer(d_workspace_, ws_size); | |
| EIGEN_CUSPARSE_CHECK(cusparseSpMM(handle_, cu_op, CUSPARSE_OPERATION_NON_TRANSPOSE, &alpha, spmat_desc_, | |
| x_mat_desc_, &beta, y_mat_desc_, dtype, kSpMMAlg, d_workspace_.get())); | |
| } | |
| void spmm_impl(const SpMat& A, const DenseMatrix& X, DenseMatrix& Y, Scalar alpha, Scalar beta, | |
| cusparseOperation_t op) { | |
| eigen_assert(A.isCompressed()); | |
| // For op != NON_TRANSPOSE, Y = op(A) * X. The dense X / Y descriptors must | |
| // describe the *post-op* shapes: X has k_op rows (= input dim of op(A)), | |
| // Y has m_op rows (= output dim of op(A)). | |
| const bool transposed = (op != CUSPARSE_OPERATION_NON_TRANSPOSE); | |
| const Index m_op = transposed ? A.cols() : A.rows(); | |
| const Index k_op = transposed ? A.rows() : A.cols(); | |
| const Index n = X.cols(); | |
| const Index nnz = A.nonZeros(); | |
| if (m_op == 0 || n == 0 || k_op == 0 || nnz == 0) { | |
| if (beta == Scalar(0)) | |
| Y.setZero(); | |
| else | |
| Y *= beta; | |
| return; | |
| } | |
| upload_sparse(A); | |
| // Upload X to device. X is k_op x n, Y is m_op x n (column-major). | |
| const size_t x_bytes = static_cast<size_t>(k_op) * static_cast<size_t>(n) * sizeof(Scalar); | |
| const size_t y_bytes = static_cast<size_t>(m_op) * static_cast<size_t>(n) * sizeof(Scalar); | |
| ensure_buffer(d_x_, x_bytes); | |
| ensure_buffer(d_y_, y_bytes); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_x_.get(), X.data(), x_bytes, cudaMemcpyHostToDevice, stream_)); | |
| if (beta != Scalar(0)) { | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_y_.get(), Y.data(), y_bytes, cudaMemcpyHostToDevice, stream_)); | |
| } | |
| exec_spmm(m_op, k_op, n, d_x_.get(), d_y_.get(), alpha, beta, op); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(Y.data(), d_y_.get(), y_bytes, cudaMemcpyDeviceToHost, stream_)); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(stream_)); | |
| } | |
| void upload_sparse(const SpMat& A) { | |
| // cuSPARSE 12.0+ accepts CSC directly. On cuSPARSE 11.x, cusparseSpMM | |
| // rejects CSC and CONJUGATE_TRANSPOSE on CSC+complex SpMV silently | |
| // demotes to TRANSPOSE. We register the same CSC buffers as CSR-of-A^T | |
| // (dims swapped) on 11.x and invert the op at exec time via | |
| // descriptor_op() — no transpose-copy required. | |
| upload_compressed_arrays(A.rows(), A.cols(), A.nonZeros(), | |
| /*outer_count=*/A.cols() + 1, A.outerIndexPtr(), A.innerIndexPtr(), A.valuePtr()); | |
| } | |
| void upload_compressed_arrays(Index m, Index n, Index nnz, Index outer_count, const StorageIndex* host_outer, | |
| const StorageIndex* host_inner, const Scalar* host_values) { | |
| const size_t outer_bytes = static_cast<size_t>(outer_count) * sizeof(StorageIndex); | |
| const size_t inner_bytes = static_cast<size_t>(nnz) * sizeof(StorageIndex); | |
| const size_t val_bytes = static_cast<size_t>(nnz) * sizeof(Scalar); | |
| // Values *and* index arrays are re-uploaded unconditionally: host pointer | |
| // identity cannot detect a same-shape/same-nnz pattern rewritten in place | |
| // or assigned into the same allocations (SparseMatrix reuses them), so a | |
| // structure cache keyed on pointers would silently serve stale indices. | |
| // Only the cuSPARSE descriptor and the workspace-size queries are cached, | |
| // keyed on (rows, cols, nnz). Every upload invalidates outstanding | |
| // DeviceSparseViews via the generation counter. | |
| ++generation_; | |
| ensure_buffer(d_values_, val_bytes); | |
| ensure_buffer(d_outerPtr_, outer_bytes); | |
| ensure_buffer(d_innerIdx_, inner_bytes); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_values_.get(), host_values, val_bytes, cudaMemcpyHostToDevice, stream_)); | |
| EIGEN_CUDA_RUNTIME_CHECK( | |
| cudaMemcpyAsync(d_outerPtr_.get(), host_outer, outer_bytes, cudaMemcpyHostToDevice, stream_)); | |
| EIGEN_CUDA_RUNTIME_CHECK( | |
| cudaMemcpyAsync(d_innerIdx_.get(), host_inner, inner_bytes, cudaMemcpyHostToDevice, stream_)); | |
| // Same shape and nnz: the grow-only device buffers cannot have been | |
| // reallocated, so the existing descriptor still points at the freshly | |
| // written data. | |
| if (m == cached_rows_ && n == cached_cols_ && nnz == cached_nnz_) return; | |
| destroy_spmat_descriptor(/*checked=*/true); | |
| constexpr cusparseIndexType_t idx_type = (sizeof(StorageIndex) == 4) ? CUSPARSE_INDEX_32I : CUSPARSE_INDEX_64I; | |
| constexpr cudaDataType_t val_type = internal::cuda_data_type<Scalar>::value; | |
| EIGEN_IF_CONSTEXPR (kUseCsrOfTranspose) { | |
| // cuSPARSE 11.x: cusparseSpMM rejects CSC for matA. CSC of A and CSR of | |
| // A^T share the same buffers, so register the data as CSR-of-A^T (dims | |
| // swapped) and invert the op in exec_spmv / spmm_impl via descriptor_op. | |
| EIGEN_CUSPARSE_CHECK(cusparseCreateCsr(&spmat_desc_, n, m, nnz, d_outerPtr_.get(), d_innerIdx_.get(), | |
| d_values_.get(), idx_type, idx_type, CUSPARSE_INDEX_BASE_ZERO, val_type)); | |
| } else { | |
| EIGEN_CUSPARSE_CHECK(cusparseCreateCsc(&spmat_desc_, m, n, nnz, d_outerPtr_.get(), d_innerIdx_.get(), | |
| d_values_.get(), idx_type, idx_type, CUSPARSE_INDEX_BASE_ZERO, val_type)); | |
| } | |
| cached_rows_ = m; | |
| cached_cols_ = n; | |
| cached_nnz_ = nnz; | |
| } | |
| // Destroy the sparse-matrix descriptor and reset the cache identity. | |
| // `checked` selects assert-on-failure (mid-lifetime rebuilds) vs swallow | |
| // (noexcept destructor). | |
| void destroy_spmat_descriptor(bool checked) { | |
| if (spmat_desc_) { | |
| cusparseStatus_t s = cusparseDestroySpMat(spmat_desc_); | |
| eigen_assert((!checked || s == CUSPARSE_STATUS_SUCCESS) && "cusparseDestroySpMat failed"); | |
| EIGEN_UNUSED_VARIABLE(s); | |
| EIGEN_UNUSED_VARIABLE(checked); | |
| spmat_desc_ = nullptr; | |
| } | |
| cached_rows_ = -1; | |
| cached_cols_ = -1; | |
| cached_nnz_ = -1; | |
| invalidate_ws_caches(); | |
| } | |
| void ensure_buffer(internal::DeviceBuffer& buf, size_t needed) const { | |
| if (needed > buf.size()) { | |
| if (buf) EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(stream_)); | |
| buf = internal::DeviceBuffer(needed); | |
| } | |
| } | |
| }; | |
| // Defined here because it needs the full SparseContext definition. | |
| template <typename Scalar_> | |
| DeviceMatrix<Scalar_>& DeviceMatrix<Scalar_>::operator=(const SpMVExpr<Scalar_>& expr) { | |
| // Uses the sparse matrix already uploaded by deviceView() — no re-upload on | |
| // repeated products with the same view. A stale view (the context has since | |
| // uploaded again, replacing the cached data) is caught here. | |
| eigen_assert(expr.view().generation() == expr.view().context().uploadGeneration() && | |
| "DeviceSparseView is stale: its SparseContext has since uploaded another sparse matrix"); | |
| if (expr.x().cols() <= 1) { | |
| expr.view().context().spmv_device_exec(expr.x(), *this, Scalar_(1), Scalar_(0), GpuOp::NoTrans); | |
| } else { | |
| expr.view().context().spmm_device_exec(expr.x(), *this, Scalar_(1), Scalar_(0), GpuOp::NoTrans); | |
| } | |
| return *this; | |
| } | |
| template <typename Scalar_> | |
| DeviceMatrix<Scalar_>::DeviceMatrix(const SpMVExpr<Scalar_>& expr) : DeviceMatrix() { | |
| *this = expr; | |
| } | |
| } // namespace gpu | |
| } // namespace Eigen | |
| #endif // EIGEN_GPU_SPARSE_CONTEXT_H |