blob: 5d9a892fe10c5593e8c70920361b6c40830fe932 [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
// Tests for gpu::SparseContext with BlockSparseMatrix input: GPU BSR SpMV/SpMM
// via cuSPARSE.
#define EIGEN_USE_GPU
#include "main.h"
#include <Eigen/Sparse>
#include <contrib/Eigen/GPU>
#include "gpu_test_helpers.h"
using namespace Eigen;
#if EIGEN_HAS_CUSPARSE_BSR
template <typename Scalar, int Options, int B>
using Bsm = BlockSparseMatrix<Scalar, Options, B, B, int>;
// Random block pattern with random dense blocks; Random() fills both parts of
// a complex scalar, so conjugation is observable.
template <typename Scalar, int Options, int B>
Bsm<Scalar, Options, B> make_block_sparse(Index block_rows, Index block_cols, double density = 0.3) {
using Triplet = typename Bsm<Scalar, Options, B>::TripletType;
using Block = typename Bsm<Scalar, Options, B>::BlockType;
std::vector<Triplet> triplets;
for (Index bi = 0; bi < block_rows; ++bi) {
for (Index bj = 0; bj < block_cols; ++bj) {
if (internal::random<double>(0.0, 1.0) < density) triplets.emplace_back(int(bi), int(bj), Block::Random());
}
}
Bsm<Scalar, Options, B> A(block_rows, block_cols);
A.setFromTriplets(triplets.begin(), triplets.end());
return A;
}
// Relative bound for a product by A: each entry sums at most
// max(A.rows(), A.cols()) terms, so 10 * max(A.rows(), A.cols()) * eps covers
// the accumulation error.
template <typename Lhs, typename Rhs, typename MatrixType>
void verify_product(const Lhs& gpu, const Rhs& cpu, const MatrixType& A) {
using Scalar = typename Lhs::Scalar;
using RealScalar = typename NumTraits<Scalar>::Real;
const RealScalar tol = RealScalar(10) * RealScalar((std::max)(A.rows(), A.cols())) * NumTraits<Scalar>::epsilon();
VERIFY_IS_EQUAL(gpu.rows(), cpu.rows());
VERIFY_IS_EQUAL(gpu.cols(), cpu.cols());
VERIFY((gpu - cpu).norm() / (cpu.norm() + RealScalar(1)) < tol);
}
// ---- Host SpMV: y = op(A) * x for every op -----------------------------------
template <typename Scalar, int Options, int B>
void test_bsr_spmv(Index block_rows, Index block_cols) {
using Vec = Matrix<Scalar, Dynamic, 1>;
const Bsm<Scalar, Options, B> A = make_block_sparse<Scalar, Options, B>(block_rows, block_cols);
const Vec x = Vec::Random(A.cols());
const Vec xt = Vec::Random(A.rows());
gpu::SparseContext<Scalar> ctx;
const Vec y = ctx.multiply(A, x);
verify_product(y, Vec(A * x), A);
const Vec yt = ctx.multiplyT(A, xt);
verify_product(yt, Vec(A.transpose() * xt), A);
const Vec yh = ctx.multiplyAdjoint(A, xt);
verify_product(yh, Vec(A.adjoint() * xt), A);
}
// ---- Host SpMV in place: y = alpha * op(A) * x + beta * y ---------------------
template <typename Scalar, int Options, int B>
void test_bsr_spmv_alpha_beta(Index block_rows, Index block_cols) {
using Vec = Matrix<Scalar, Dynamic, 1>;
const Bsm<Scalar, Options, B> A = make_block_sparse<Scalar, Options, B>(block_rows, block_cols);
const Scalar alpha(2);
const Scalar beta(3);
gpu::SparseContext<Scalar> ctx;
const Vec x = Vec::Random(A.cols());
const Vec y_init = Vec::Random(A.rows());
Vec y = y_init;
ctx.multiply(A, x, y, alpha, beta);
verify_product(y, Vec(alpha * (A * x) + beta * y_init), A);
const Vec xt = Vec::Random(A.rows());
const Vec yt_init = Vec::Random(A.cols());
Vec yt = yt_init;
ctx.multiply(A, xt, yt, alpha, beta, gpu::GpuOp::ConjTrans);
verify_product(yt, Vec(alpha * (A.adjoint() * xt) + beta * yt_init), A);
}
// ---- Host SpMM: Y = op(A) * X -------------------------------------------------
template <typename Scalar, int Options, int B>
void test_bsr_spmm(Index block_rows, Index block_cols, Index nrhs) {
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
const Bsm<Scalar, Options, B> A = make_block_sparse<Scalar, Options, B>(block_rows, block_cols);
const Mat X = Mat::Random(A.cols(), nrhs);
const Mat Xt = Mat::Random(A.rows(), nrhs);
gpu::SparseContext<Scalar> ctx;
verify_product(ctx.multiplyMat(A, X), Mat(A * X), A);
verify_product(ctx.multiplyMat(A, Xt, gpu::GpuOp::Trans), Mat(A.transpose() * Xt), A);
verify_product(ctx.multiplyMat(A, Xt, gpu::GpuOp::ConjTrans), Mat(A.adjoint() * Xt), A);
}
// ---- DeviceMatrix in/out with an explicit op ----------------------------------
template <typename Scalar, int Options, int B>
void test_bsr_device_multiply(Index block_rows, Index block_cols) {
using Vec = Matrix<Scalar, Dynamic, 1>;
const Bsm<Scalar, Options, B> A = make_block_sparse<Scalar, Options, B>(block_rows, block_cols);
gpu::Context gctx;
gpu::SparseContext<Scalar> ctx(gctx);
const Vec x = Vec::Random(A.cols());
auto d_x = gpu::DeviceMatrix<Scalar>::fromHost(x, gctx.stream());
gpu::DeviceMatrix<Scalar> d_y;
ctx.multiply(A, d_x, d_y);
verify_product(d_y.toHost(gctx.stream()), Vec(A * x), A);
const Vec xt = Vec::Random(A.rows());
auto d_xt = gpu::DeviceMatrix<Scalar>::fromHost(xt, gctx.stream());
gpu::DeviceMatrix<Scalar> d_yt;
ctx.multiply(A, d_xt, d_yt, Scalar(1), Scalar(0), gpu::GpuOp::Trans);
verify_product(d_yt.toHost(gctx.stream()), Vec(A.transpose() * xt), A);
ctx.multiply(A, d_xt, d_yt, Scalar(1), Scalar(0), gpu::GpuOp::ConjTrans);
verify_product(d_yt.toHost(gctx.stream()), Vec(A.adjoint() * xt), A);
}
// ---- deviceView: upload once, SpMV and SpMM by expression ----------------------
template <typename Scalar, int Options, int B>
void test_bsr_device_view(Index block_rows, Index block_cols, Index nrhs) {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
const Bsm<Scalar, Options, B> A = make_block_sparse<Scalar, Options, B>(block_rows, block_cols);
gpu::Context gctx;
gpu::SparseContext<Scalar> ctx(gctx);
auto view = ctx.deviceView(A);
VERIFY(view.generation() == ctx.uploadGeneration());
VERIFY_IS_EQUAL(view.rows(), A.rows());
VERIFY_IS_EQUAL(view.cols(), A.cols());
const Vec x = Vec::Random(A.cols());
auto d_x = gpu::DeviceMatrix<Scalar>::fromHost(x, gctx.stream());
gpu::DeviceMatrix<Scalar> d_y = view * d_x;
verify_product(d_y.toHost(gctx.stream()), Vec(A * x), A);
const Mat X = Mat::Random(A.cols(), nrhs);
auto d_X = gpu::DeviceMatrix<Scalar>::fromHost(X, gctx.stream());
gpu::DeviceMatrix<Scalar> d_Y;
d_Y.noalias() = view * d_X;
verify_product(d_Y.toHost(gctx.stream()), Mat(A * X), A);
// cuSPARSE runs no transposed BSR product; the exec entry points reject any
// other op against a BSR upload before queuing work.
VERIFY_RAISES_ASSERT(ctx.spmv_device_exec(d_x, d_y, Scalar(1), Scalar(0), gpu::GpuOp::Trans));
VERIFY_RAISES_ASSERT(ctx.spmm_device_exec(d_X, d_Y, Scalar(1), Scalar(0), gpu::GpuOp::Trans));
}
// ---- Descriptor cache across formats ------------------------------------------
// A CSC matrix with the same shape and nonzero count as a BSR one must not
// reuse its descriptor: S = (A^T as scalar sparse) has both, and a different
// value for every x unless A is symmetric.
template <typename Scalar, int Options, int B>
void test_bsr_format_switch(Index block_n) {
using Vec = Matrix<Scalar, Dynamic, 1>;
using SpMat = SparseMatrix<Scalar, ColMajor, int>;
const Bsm<Scalar, Options, B> A = make_block_sparse<Scalar, Options, B>(block_n, block_n);
const SpMat S = SpMat(A.transpose().toSparse());
VERIFY_IS_EQUAL(S.nonZeros(), A.nonZeros());
const Vec x = Vec::Random(A.cols());
gpu::SparseContext<Scalar> ctx;
verify_product(ctx.multiply(A, x), Vec(A * x), A);
verify_product(ctx.multiply(S, x), Vec(S * x), A);
verify_product(ctx.multiply(A, x), Vec(A * x), A);
}
// ---- Pattern rewrite at unchanged host pointers -------------------------------
// The index arrays are re-uploaded on every host-input call, so rewriting the
// block-column indices in place is picked up. Blocks sit at (i, i) in an
// n x (n + 1) grid and move to (i, i + 1), which keeps every block row sorted.
template <typename Scalar, int Options, int B>
void test_bsr_pattern_rewrite(Index n) {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Triplet = typename Bsm<Scalar, Options, B>::TripletType;
using Block = typename Bsm<Scalar, Options, B>::BlockType;
std::vector<Triplet> triplets;
for (Index i = 0; i < n; ++i) triplets.emplace_back(int(i), int(i), Block::Random());
Bsm<Scalar, Options, B> A(n, n + 1);
A.setFromTriplets(triplets.begin(), triplets.end());
const Vec x = Vec::Random(A.cols());
gpu::SparseContext<Scalar> ctx;
verify_product(ctx.multiply(A, x), Vec(A * x), A);
// RowMajor: inner index = block column, shift it. ColMajor: inner index =
// block row; shifting the *outer* boundaries instead moves block i to
// column i + 1, so both orders end at the same pattern.
if (Options & RowMajor) {
for (Index k = 0; k < A.nonZeroBlocks(); ++k) A.innerIndexPtr()[k] += 1;
} else {
for (Index j = n; j >= 1; --j) A.outerIndexPtr()[j] = A.outerIndexPtr()[j - 1];
A.outerIndexPtr()[0] = 0;
A.outerIndexPtr()[n + 1] = int(n);
}
VERIFY_IS_EQUAL(A.nonZeroBlocks(), n);
verify_product(ctx.multiply(A, x), Vec(A * x), A);
}
// ---- Empty matrices ------------------------------------------------------------
template <typename Scalar, int Options, int B>
void test_bsr_empty() {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
gpu::Context gctx;
gpu::SparseContext<Scalar> ctx(gctx);
const Bsm<Scalar, Options, B> A0(0, 0);
VERIFY_IS_EQUAL(ctx.multiply(A0, Vec(0)).size(), 0);
VERIFY_IS_EQUAL(ctx.multiplyMat(A0, Mat(0, 3)).cols(), 3);
// Nonzero dimensions, no stored blocks: y <- beta * y on the host and
// y <- 0 on the device.
const Bsm<Scalar, Options, B> A(3, 2);
const Vec x = Vec::Random(A.cols());
const Vec y_init = Vec::Random(A.rows());
Vec y = y_init;
ctx.multiply(A, x, y, Scalar(1), Scalar(2));
VERIFY_IS_APPROX(y, Vec(Scalar(2) * y_init));
VERIFY(ctx.multiply(A, x).isZero());
auto d_x = gpu::DeviceMatrix<Scalar>::fromHost(x, gctx.stream());
gpu::DeviceMatrix<Scalar> d_y;
ctx.multiply(A, d_x, d_y);
VERIFY_IS_EQUAL(d_y.rows(), A.rows());
VERIFY(d_y.toHost(gctx.stream()).isZero());
}
// ---- Driver ---------------------------------------------------------------------
template <typename Scalar, int Options, int B>
void test_order() {
CALL_SUBTEST((test_bsr_spmv<Scalar, Options, B>(7, 5)));
CALL_SUBTEST((test_bsr_spmv_alpha_beta<Scalar, Options, B>(5, 7)));
CALL_SUBTEST((test_bsr_spmm<Scalar, Options, B>(6, 4, 3)));
CALL_SUBTEST((test_bsr_device_multiply<Scalar, Options, B>(4, 6)));
CALL_SUBTEST((test_bsr_device_view<Scalar, Options, B>(6, 6, 4)));
CALL_SUBTEST((test_bsr_format_switch<Scalar, Options, B>(5)));
CALL_SUBTEST((test_bsr_pattern_rewrite<Scalar, Options, B>(4)));
CALL_SUBTEST((test_bsr_empty<Scalar, Options, B>()));
}
template <typename Scalar>
void test_scalar() {
// RowMajor binds without a host copy for NoTrans, ColMajor for Trans; the
// other op / order combinations take the transposing copy.
test_order<Scalar, RowMajor, 2>();
test_order<Scalar, ColMajor, 2>();
test_order<Scalar, RowMajor, 3>();
test_order<Scalar, ColMajor, 3>();
}
#endif // EIGEN_HAS_CUSPARSE_BSR
EIGEN_DECLARE_TEST(gpu_cusparse_bsr) {
#if EIGEN_HAS_CUSPARSE_BSR
gpu_test::require_cusparse_context();
// Split by scalar so each part compiles in parallel.
CALL_SUBTEST_1(test_scalar<float>());
CALL_SUBTEST_2(test_scalar<double>());
CALL_SUBTEST_3(test_scalar<std::complex<float>>());
CALL_SUBTEST_4(test_scalar<std::complex<double>>());
// Block size 4, in one scalar type to bound compile time.
CALL_SUBTEST_5((test_order<double, RowMajor, 4>()));
CALL_SUBTEST_5((test_order<double, ColMajor, 4>()));
#else
std::cout << "SKIP: BSR products need cuSPARSE >= 12.6.3 (CUDA 13.0 Update 1); this build has CUSPARSE_VERSION "
<< CUSPARSE_VERSION << std::endl;
std::exit(77);
#endif
}