| // 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 | |
| } |