// This file is part of Eigen, a lightweight C++ template library // for linear algebra. // // Copyright (C) 2026 Rasmus Munk Larsen // // 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 template void verify_threaded_spmv(Index rows, Index cols, double density) { typedef typename SparseMatrixType::Scalar Scalar; typedef Matrix DenseMatrix; DenseMatrix refMat(rows, cols); SparseMatrixType A(rows, cols); initSparse(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 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())}) { { 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 void verify_threaded_scatter_ieee754() { typedef SparseMatrix SpMat; typedef Matrix Vec; typedef typename NumTraits::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 > triplets; triplets.reserve(static_cast(n) * (n / 4 + 1)); // Simple deterministic LCG so we don't depend on 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(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::quiet_NaN(); const Real inf_r = std::numeric_limits::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 void run_grid() { typedef Matrix 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, Vec>(8, 8, 0.5); verify_threaded_spmv, Vec>(13, 21, 0.3); verify_threaded_spmv, Vec>(500, 500, 0.15); verify_threaded_spmv, Vec>(800, 400, 0.10); // Large enough that nnz > kThreadingThreshold (20000) to exercise the // multi-threaded dispatch path. verify_threaded_spmv, Vec>(1500, 1500, 0.02); } else { verify_threaded_spmv, Vec>(8, 8, 0.5); verify_threaded_spmv, Vec>(21, 13, 0.3); verify_threaded_spmv, Vec>(500, 500, 0.15); verify_threaded_spmv, Vec>(400, 800, 0.10); verify_threaded_spmv, Vec>(1500, 1500, 0.02); } } } EIGEN_DECLARE_TEST(sparse_threaded_product) { for (int i = 0; i < g_repeat; ++i) { CALL_SUBTEST_1(run_grid()); CALL_SUBTEST_2(run_grid()); CALL_SUBTEST_3(run_grid >()); CALL_SUBTEST_4(verify_threaded_scatter_ieee754()); CALL_SUBTEST_4(verify_threaded_scatter_ieee754()); } }