libeigen/eigen!2557 Co-authored-by: Rasmus Munk Larsen <rmlarsen@gmail.com> Co-authored-by: Rasmus Munk Larsen <rlarsen@nvidia.com>
229 lines
8.3 KiB
C++
229 lines
8.3 KiB
C++
// 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
|
|
|
|
#define EIGEN_USE_THREADS 1
|
|
#include "sparse.h"
|
|
|
|
// Pulls in the ThreadedSparseProduct header via the SparseCore module.
|
|
// (Eigen/Sparse -> Eigen/SparseCore conditional include under EIGEN_USE_THREADS.)
|
|
#include <Eigen/SparseCore>
|
|
|
|
template <typename SparseMatrixType, typename DenseVectorType>
|
|
void verify_threaded_spmv(Index rows, Index cols, double density) {
|
|
typedef typename SparseMatrixType::Scalar Scalar;
|
|
typedef Matrix<Scalar, Dynamic, Dynamic> DenseMatrix;
|
|
|
|
DenseMatrix refMat(rows, cols);
|
|
SparseMatrixType A(rows, cols);
|
|
initSparse<Scalar>(density, refMat, A);
|
|
A.makeCompressed();
|
|
|
|
DenseVectorType x_fwd = DenseVectorType::Random(cols);
|
|
DenseVectorType x_adj = DenseVectorType::Random(rows);
|
|
|
|
// Reference results via the existing dense path.
|
|
DenseVectorType y_fwd_ref = refMat * x_fwd;
|
|
DenseVectorType y_adj_ref = refMat.adjoint() * x_adj;
|
|
|
|
ThreadedSparseProduct<SparseMatrixType> op(A);
|
|
VERIFY(op.rows() == A.rows());
|
|
VERIFY(op.cols() == A.cols());
|
|
|
|
// Overwriting forms.
|
|
{
|
|
DenseVectorType y(rows);
|
|
y.setRandom();
|
|
op.apply(x_fwd, y);
|
|
VERIFY_IS_APPROX(y, y_fwd_ref);
|
|
}
|
|
{
|
|
DenseVectorType y(cols);
|
|
y.setRandom();
|
|
op.applyAdjoint(x_adj, y);
|
|
VERIFY_IS_APPROX(y, y_adj_ref);
|
|
}
|
|
|
|
// Accumulating forms: y += alpha * A * x, with both alpha = 1 and a
|
|
// non-trivial alpha.
|
|
for (Scalar alpha : {Scalar(1), Scalar(internal::random<Scalar>())}) {
|
|
{
|
|
DenseVectorType y0 = DenseVectorType::Random(rows);
|
|
DenseVectorType y_ref = y0 + alpha * y_fwd_ref;
|
|
DenseVectorType y = y0;
|
|
op.applyAddTo(x_fwd, y, alpha);
|
|
VERIFY_IS_APPROX(y, y_ref);
|
|
}
|
|
{
|
|
DenseVectorType y0 = DenseVectorType::Random(cols);
|
|
DenseVectorType y_ref = y0 + alpha * y_adj_ref;
|
|
DenseVectorType y = y0;
|
|
op.applyAdjointAddTo(x_adj, y, alpha);
|
|
VERIFY_IS_APPROX(y, y_ref);
|
|
}
|
|
}
|
|
|
|
// Calling adjoint must have materialized the mirror; calling it again must
|
|
// not rebuild (no easy assertion for "not rebuilt", but verify the path is
|
|
// consistent across repeated calls).
|
|
VERIFY(op.hasMirror());
|
|
{
|
|
DenseVectorType y(cols);
|
|
op.applyAdjoint(x_adj, y);
|
|
VERIFY_IS_APPROX(y, y_adj_ref);
|
|
}
|
|
|
|
// Re-running analyzePattern must invalidate the mirror.
|
|
op.analyzePattern(A);
|
|
VERIFY(!op.hasMirror());
|
|
{
|
|
DenseVectorType y(cols);
|
|
op.applyAdjoint(x_adj, y);
|
|
VERIFY_IS_APPROX(y, y_adj_ref);
|
|
}
|
|
|
|
// Coefficient-only invalidation: mutate the bound matrix's stored values
|
|
// (same sparsity pattern), call refreshValues(), and verify the next
|
|
// mirror-using direction picks up the new coefficients. The mirror is
|
|
// materialized lazily by the direction that needs the transposed view --
|
|
// forward for ColMajor A, adjoint for RowMajor A -- so call both to warm
|
|
// it regardless of storage order.
|
|
{
|
|
DenseVectorType y_fwd(rows);
|
|
DenseVectorType y_adj(cols);
|
|
op.apply(x_fwd, y_fwd);
|
|
op.applyAdjoint(x_adj, y_adj);
|
|
VERIFY(op.hasMirror());
|
|
}
|
|
for (Index k = 0; k < A.nonZeros(); ++k) A.valuePtr()[k] *= Scalar(2);
|
|
refMat *= Scalar(2);
|
|
DenseVectorType y_fwd_ref_updated = refMat * x_fwd;
|
|
DenseVectorType y_adj_ref_updated = refMat.adjoint() * x_adj;
|
|
op.refreshValues();
|
|
VERIFY(!op.hasMirror());
|
|
{
|
|
DenseVectorType y(rows);
|
|
op.apply(x_fwd, y);
|
|
VERIFY_IS_APPROX(y, y_fwd_ref_updated);
|
|
}
|
|
{
|
|
DenseVectorType y(cols);
|
|
op.applyAdjoint(x_adj, y);
|
|
VERIFY_IS_APPROX(y, y_adj_ref_updated);
|
|
}
|
|
}
|
|
|
|
// IEEE-754 propagation through the ColMajor SpMV OMP scatter/reduce path
|
|
// (Eigen/src/SparseCore/SparseDenseProduct.h). With OpenMP enabled, the
|
|
// kernel sums per-thread scratch buffers into y; NaN/Inf in any thread's
|
|
// contribution must propagate to the reduced sum. Without OpenMP this still
|
|
// exercises the serial scatter, where the same propagation must hold.
|
|
template <typename Scalar>
|
|
void verify_threaded_scatter_ieee754() {
|
|
typedef SparseMatrix<Scalar, ColMajor> SpMat;
|
|
typedef Matrix<Scalar, Dynamic, 1> Vec;
|
|
typedef typename NumTraits<Scalar>::Real Real;
|
|
|
|
// Pin the OMP team size so the scatter/reduce gate (nnz >= threads * m)
|
|
// is deterministic across runs.
|
|
const int prev_threads = Eigen::nbThreads();
|
|
Eigen::setNbThreads(4);
|
|
|
|
// 300x300 with diagonal + (i+j)%4==0 entries -> ~22600 nnz, above the
|
|
// 20000-nnz threshold and well above threads*m = 4*300 for the gate.
|
|
const Index n = 300;
|
|
std::vector<Triplet<Scalar> > triplets;
|
|
triplets.reserve(static_cast<std::size_t>(n) * (n / 4 + 1));
|
|
// Simple deterministic LCG so we don't depend on <random> being included
|
|
// transitively. Modulo-bounded so we don't rely on a specific integer width.
|
|
unsigned rng = 0xC0FFEEu;
|
|
for (int j = 0; j < int(n); ++j) {
|
|
for (int i = 0; i < int(n); ++i) {
|
|
if (i == j || (i + j) % 4 == 0) {
|
|
rng = (rng * 1664525u + 1013904223u) & 0xFFFFu;
|
|
const Real v = Real(0.1) + Real(0.9) * Real(rng) / Real(0xFFFF);
|
|
triplets.push_back(Triplet<Scalar>(i, j, Scalar(v)));
|
|
}
|
|
}
|
|
}
|
|
SpMat A(n, n);
|
|
A.setFromTriplets(triplets.begin(), triplets.end());
|
|
A.makeCompressed();
|
|
VERIFY(A.nonZeros() > 20000);
|
|
|
|
// Inject NaN/Inf into three rows of column 0 by overwriting valuePtr().
|
|
// Pick rows that are guaranteed in column 0's pattern: row 0 (diagonal),
|
|
// row 4 ((4+0)%4==0), row 8 ((8+0)%4==0).
|
|
const Real nan_r = std::numeric_limits<Real>::quiet_NaN();
|
|
const Real inf_r = std::numeric_limits<Real>::infinity();
|
|
const Index i_nan = 0, i_pinf = 4, i_ninf = 8;
|
|
bool set_nan = false, set_pinf = false, set_ninf = false;
|
|
for (Index k = A.outerIndexPtr()[0]; k < A.outerIndexPtr()[1]; ++k) {
|
|
const Index i = A.innerIndexPtr()[k];
|
|
if (i == i_nan) {
|
|
A.valuePtr()[k] = Scalar(nan_r);
|
|
set_nan = true;
|
|
} else if (i == i_pinf) {
|
|
A.valuePtr()[k] = Scalar(inf_r);
|
|
set_pinf = true;
|
|
} else if (i == i_ninf) {
|
|
A.valuePtr()[k] = Scalar(-inf_r);
|
|
set_ninf = true;
|
|
}
|
|
}
|
|
VERIFY(set_nan && set_pinf && set_ninf);
|
|
|
|
Vec x = Vec::Random(n);
|
|
// Ensure x[0] is finite and strictly positive (real part), so the injected
|
|
// ±Inf contributions don't cancel against a zero-multiplier or flip sign.
|
|
x[0] = Scalar(1);
|
|
|
|
Vec y = A * x;
|
|
|
|
VERIFY((numext::isnan)(numext::real(y[i_nan])));
|
|
VERIFY((numext::isinf)(numext::real(y[i_pinf])) && numext::real(y[i_pinf]) > Real(0));
|
|
VERIFY((numext::isinf)(numext::real(y[i_ninf])) && numext::real(y[i_ninf]) < Real(0));
|
|
|
|
Eigen::setNbThreads(prev_threads);
|
|
}
|
|
|
|
template <typename Scalar>
|
|
void run_grid() {
|
|
typedef Matrix<Scalar, Dynamic, 1> Vec;
|
|
// Cover both storage orders, square and rectangular, small (serial path,
|
|
// nnz < threshold) and larger (threaded path).
|
|
for (int order = 0; order < 2; ++order) {
|
|
if (order == 0) {
|
|
verify_threaded_spmv<SparseMatrix<Scalar, ColMajor>, Vec>(8, 8, 0.5);
|
|
verify_threaded_spmv<SparseMatrix<Scalar, ColMajor>, Vec>(13, 21, 0.3);
|
|
verify_threaded_spmv<SparseMatrix<Scalar, ColMajor>, Vec>(500, 500, 0.15);
|
|
verify_threaded_spmv<SparseMatrix<Scalar, ColMajor>, Vec>(800, 400, 0.10);
|
|
// Large enough that nnz > kThreadingThreshold (20000) to exercise the
|
|
// multi-threaded dispatch path.
|
|
verify_threaded_spmv<SparseMatrix<Scalar, ColMajor>, Vec>(1500, 1500, 0.02);
|
|
} else {
|
|
verify_threaded_spmv<SparseMatrix<Scalar, RowMajor>, Vec>(8, 8, 0.5);
|
|
verify_threaded_spmv<SparseMatrix<Scalar, RowMajor>, Vec>(21, 13, 0.3);
|
|
verify_threaded_spmv<SparseMatrix<Scalar, RowMajor>, Vec>(500, 500, 0.15);
|
|
verify_threaded_spmv<SparseMatrix<Scalar, RowMajor>, Vec>(400, 800, 0.10);
|
|
verify_threaded_spmv<SparseMatrix<Scalar, RowMajor>, Vec>(1500, 1500, 0.02);
|
|
}
|
|
}
|
|
}
|
|
|
|
EIGEN_DECLARE_TEST(sparse_threaded_product) {
|
|
for (int i = 0; i < g_repeat; ++i) {
|
|
CALL_SUBTEST_1(run_grid<float>());
|
|
CALL_SUBTEST_2(run_grid<double>());
|
|
CALL_SUBTEST_3(run_grid<std::complex<double> >());
|
|
CALL_SUBTEST_4(verify_threaded_scatter_ieee754<double>());
|
|
CALL_SUBTEST_4(verify_threaded_scatter_ieee754<float>());
|
|
}
|
|
}
|