blob: 24ed2a16636d4ddcadc57a3eb921aedf22e69bb2 [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 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