Date: 2026-01-16 Module: sparse Purpose: Comprehensive guide to BELFEM's sparse matrix and solver API Revision: 2026-01-16 - Initial version
Overview
The src/sparse module provides sparse matrix storage and direct/iterative solver interfaces for BELFEM. It wraps multiple high-performance solver libraries:
- UMFPACK (SuiteSparse): Sequential sparse direct solver
- MUMPS: Parallel sparse direct solver (MPI)
- STRUMPACK: Parallel sparse direct/iterative solver (MPI, low-rank compression)
- PARDISO: Shared-memory parallel direct solver (Intel MKL)
- PETSc: Scalable iterative solver framework (MPI, Krylov methods)
The abstraction allows switching solver backends at compile time without changing user code, enabling portability across different HPC systems.
Key Features
- Multiple Solver Backends: Compile-time selection via CMake flags
- Sparse Matrix Formats: CSR (Compressed Sparse Row) and CSC (Compressed Sparse Column)
- MPI Support: Distributed sparse matrices and parallel solvers
- Flexible Indexing: C++ (0-based) or Fortran (1-based) indexing
- Solver Parameters: Unified interface for reordering, preconditioners, tolerances
- Graph-Based Construction: Build sparse matrices from connectivity graphs
Critical: Solver Selection Solver availability is determined at compile time via the CMake options (the BELFEM_* spellings are the generated compile definitions, not cache entries):
- USE_SUITESPARSE=ON → UMFPACK available (default OFF)
- USE_MUMPS=ON → MUMPS available (default ON)
- USE_STRUMPACK=ON → STRUMPACK available (preferred, default ON)
- USE_PARDISO=ON → PARDISO available (default OFF; ON under the SCLS mkl flavor, in parity with USE_MKL)
- USE_PETSC=ON → PETSc available (default ON)
Multiple solvers can be enabled in a single build. Runtime selection allows switching between available solvers:
Solver solver1(SolverType::UMFPACK);
Solver solver2(SolverType::MUMPS);
Use the default solver (automatically selected at compile time) or specify explicitly.
Common Pitfalls
1. Indexing Base Mismatch
SpMatrix A(graph, SpMatrixType::CSR);
int base = A.indexing_base();
A.set_indexing_base(SpMatrixIndexingBase::Fortran);
Solution: Check solver requirements. MUMPS needs Fortran indexing; UMFPACK uses C++ indexing.
2. CSR vs. CSC Format
SpMatrix A(graph);
SpMatrix A_csc(graph, SpMatrixType::CSC);
SpMatrix A_csr(graph, SpMatrixType::CSR);
Solution: Always specify format explicitly, or use preferred_matrix_format():
SpMatrix A(graph, preferred_matrix_format(SolverType::MUMPS));
Note: SpMatrix default constructor uses CSC format. For solvers that prefer CSR (MUMPS, STRUMPACK, PARDISO, PETSc), always specify SpMatrixType::CSR explicitly to avoid performance loss.
3. COO Indices for MUMPS
MUMPS needs explicit row/column index arrays, but the solver creates them itself — SolverMUMPS calls create_coo_indices() on the matrix before it solves (cl_SolverMUMPS.cpp:716-725). A caller does not have to:
SpMatrix A(graph, SpMatrixType::CSR);
A.free_coo_indices();
4. Overwriting Matrix During Solve
SpMatrix A(graph, SpMatrixType::CSR);
Vector<real> x(n), b(n);
Solver solver(SolverType::MUMPS);
solver.solve(A, x, b);
SpMatrix A_copy;
A_copy = A;
Solution: None needed for the values: they survive the solve. Only the indexing base may have been flipped and restored; copy only if you rely on a specific base afterwards.
5. MPI Distribution
A whole SpMatrix object is not sent over MPI, and there is no distribute( SpMatrix* ) overload. The matrix lives on the master rank only; the solver wrappers handle distribution internally:
- MUMPS takes the host-centralized matrix from rank 0 and distributes inside the library.
- STRUMPACK/PETSc use sparse::DistMatrix, which extracts the pointer, index, and value arrays on rank 0 and scatters raw array slices to workers.
SpMatrix A;
A = SpMatrix(graph, SpMatrixType::CSR);
}
solver.solve(A, x, b);
belfem::Communicator gComm
Definition belfem.cpp:35
proc_t rank() const
Definition cl_Communicator.hpp:192
Solution: Build the matrix on rank 0 and let the solver wrapper distribute.
Core Types
SpMatrix - Sparse Matrix
Files: cl_SpMatrix.hpp
Description
Compressed sparse matrix storage (CSR or CSC format) with element access, matrix-vector multiplication, and I/O capabilities.
Sparse Matrix Formats
CSR (Compressed Sparse Row):
Row-major storage
Pointers[i]: Start of row i in Values array
Indices[j]: Column index of Values[j]
CSC (Compressed Sparse Column):
Column-major storage
Pointers[j]: Start of column j in Values array
Indices[i]: Row index of Values[i]
COO (Coordinate) format (for MUMPS):
Explicit row and column index arrays
Rows[k]: Row index of Values[k]
Cols[k]: Column index of Values[k]
Construction
Graph graph;
SpMatrix A(graph, SpMatrixType::CSR);
SpMatrix A(graph, SpMatrixType::CSC, num_rows, num_cols);
SpMatrix A(SpMatrixType::CSR, n_rows, n_cols, nnz, indices, pointers);
Matrix<real> dense = {{4, 0, 1},
{0, 2, 0},
{1, 0, 3}};
SpMatrix A(dense, SpMatrixType::CSR);
SpMatrix A("matrix.h5", "StiffnessMatrix");
SpMatrix A;
Element Access
SpMatrix A(graph, SpMatrixType::CSR);
real value = A(0, 0);
A(2, 3) = 1.5;
real zero = A(1, 5);
real* values = A.data();
int_t* indices = A.indices();
int_t* pointers = A.pointers();
Size and Properties
index_t rows = A.n_rows();
index_t cols = A.n_cols();
index_t nnz = A.number_of_nonzeros();
SpMatrixType type = A.type();
size_t mem = A.memory();
bool have_coo = A.have_coo_indices();
Matrix-Vector Multiplication
SpMatrix A(graph, SpMatrixType::CSR);
Vector<real> x(n), y(n);
A.multiply(x, y);
A.multiply(x, y, 2.0, 1.0);
A.multiply(x, y, 1.0, 0.0, true);
Vector<real> y = A * x;
Utilities
A.fill(0.0);
A.transpose();
A.set_indexing_base(SpMatrixIndexingBase::Fortran);
A.set_indexing_base(SpMatrixIndexingBase::Cpp);
A.create_coo_indices();
A.free_coo_indices();
A.print("MyMatrix");
A.print2("MyMatrix");
Saving and Loading
A.save("matrix.h5", "StiffnessMatrix", FileMode::NEW);
SpMatrix B;
B.load("matrix.h5", "StiffnessMatrix");
hid_t group = ...;
herr_t status;
A.save(group, status);
Parent/Child Structure Sharing
Two matrices with the same sparsity pattern can share their structure arrays (pointers, indices, and, when present, the COO index array) to save memory. The second matrix is constructed as a child of the first:
SpMatrix* tK = new SpMatrix(graph, SpMatrixType::CSR, n, n, false);
SpMatrix* tM = new SpMatrix(tK);
This is how SolverData pairs mSystemMatrix/mJacobianMatrix and mFullMassMatrix/mFullStiffnessMatrix. Memory saved per pair is roughly (nnz + n + 1) * sizeof(int_t), plus the COO array when MUMPS is used.
Ownership and lifecycle rules:
- While linked, the parent owns the pointer, index, and optional COO index arrays; the child owns only its values. memory() reports that split.
- One child per parent; chains (a child of a child) are refused.
- Either destruction order is safe: destroying the child unlinks it from the parent; destroying the parent first transfers structure ownership to the child.
- set_indexing_base() on either side converts the shared arrays once and updates both index functions. create_coo_indices()/free_coo_indices() keep both sides in sync in either call order.
- load() on a linked matrix (child, or a parent with an attached child, as in SolverData::load_system) never replaces the structure: it verifies the file's pattern against the existing arrays (tolerating an indexing-base offset) and loads the values only. A former child that inherited the structure after its parent's destruction is an ordinary unlinked matrix again, and load() rebuilds it from the file as usual. save() needs no special handling.
Refused on linked matrices (BELFEM_ERROR): transpose(), sort_entries(), set_type(), copy-assignment onto a linked matrix, and move-assignment from or onto a linked matrix. Assigning from a linked matrix into an unlinked matrix is allowed and produces an ordinary deep copy.
When to Use
- Primary sparse matrix type for all BELFEM FEM assembly
- Large sparse linear systems (> 1000 DOFs)
- Graph-based connectivity (mesh elements, finite differences)
- Matrix-free iterative methods (via multiply)
See: SpMatrix class in cl_SpMatrix.hpp
Solver - Unified Solver Interface
Files: cl_Solver.hpp
Description
Unified interface to multiple sparse direct and iterative solver libraries. Automatically selects the solver backend compiled into BELFEM.
Construction
Solver solver;
Solver solver(SolverType::MUMPS);
Solver solver(SolverType::STRUMPACK);
Solver solver(SolverType::PARDISO);
Solver solver(SolverType::UMFPACK);
Solver solver(SolverType::PETSc);
SolverParameters params(SolverType::MUMPS);
params.set_reordering_method(ReorderingMethod::METIS);
Solver solver(params);
Solving Linear Systems
SpMatrix A(graph, SpMatrixType::CSR);
Vector<real> x(n), b(n);
Solver solver(SolverType::MUMPS);
solver.set_symmetry_mode(SymmetryMode::Unsymmetric);
solver.solve(A, x, b);
Matrix<real> X(n, m), B(n, m);
solver.solve(A, X, B);
Solver-Specific Configuration
PETSc (Iterative Solvers):
Solver solver(SolverType::PETSc);
solver.set_petsc(
Preconditioner::GAMG,
KrylovMethod::GMRES,
1e-8
);
MUMPS (Parallel Direct Solver):
Solver solver(SolverType::MUMPS);
solver.set_mumps_reordering(
MumpsSerialReodrdering::METIS,
MumpsParallelReodrdering::PARMETIS
);
solver.set_mumps_blr(
MumpsBlockLowRanking::FactorizationAndSolution,
1e-4
);
solver.set_mumps_error_analysis(MumpsErrorAnalysis::Full);
Command-Line Pass-Through (PETSc and STRUMPACK)
Both libraries read their own options directly from the command line of any BELFEM executable — no code is required in the executable:
- PETSc: Communicator::init hands the full argc/argv to PetscInitialize (cl_Communicator.cpp, search PetscInitialize), which loads every argument into the PETSc options database. All PETSc options work as documented by PETSc (-ksp_type gmres, -pc_type hypre, -ksp_monitor, -options_file petsc.opts, -snes_*, …). Unknown arguments are ignored; the warning about unused options is suppressed via -options_left 0.
- STRUMPACK: the wrapper copies the arguments from the global communicator and calls options().set_from_command_line() after applying the settings derived from input.conf (cl_SolverSTRUMPACK.cpp, search set_from_command_line; both the serial and the distributed path do this). The command line therefore overrides the input file. STRUMPACK flags carry the --sp_ prefix (--sp_compression NONE, --sp_reordering_method metis, …); run any STRUMPACK-enabled executable with --help not intercepted by BELFEM, i.e. consult the STRUMPACK manual for the full list.
STRUMPACK's own progress output is tied to the logger: it is enabled when the info level is at least 5, e.g. by passing --verbose (see the core module's Arguments class).
Example:
mpirun -np 4 belfem -v 3 --sp_compression NONE -ksp_monitor
Each parser skips flags it does not recognize, so BELFEM, PETSc, and STRUMPACK options can be mixed freely on one command line.
Cleanup
When to Use
- All sparse linear system solves in BELFEM
- FEM stiffness matrix solves
- Eigenvalue problems (via iterative methods)
- Nonlinear problems (repeated solves with updated matrices)
See: Solver class in cl_Solver.hpp
SolverParameters - Solver Configuration
Files: cl_SolverParameters.hpp
Description
Configuration object for solver settings, supporting MPI broadcasting and input file parsing.
Construction
SolverParameters params(SolverType::MUMPS);
input::Section* input = ...;
SolverParameters params(input);
SolverParameters params2(params);
Configuration
SolverParameters params(SolverType::PETSc);
params.set_distributed_matrix_type(DistributedMatrixType::AIJ);
params.set_reordering_method(ReorderingMethod::METIS);
params.set_compression_method(CompressionMethod::BLR);
params.set_preconditioner(Preconditioner::GAMG);
params.set_krylov_method(KrylovMethod::GMRES);
params.set_relative_tolerance(1e-8);
params.set_use_initial_guess(true);
Querying
SolverType type = params.type();
DistributedMatrixType mat_type = params.distributed_matrix_type();
Preconditioner prec = params.preconditioner();
KrylovMethod krylov = params.krylov_method();
ReorderingMethod reorder = params.reordering_method();
CompressionMethod compress = params.compression_method();
real tol = params.relative_tolerance();
bool use_guess = params.use_initial_guess();
MPI Synchronization
SolverParameters params(SolverType::MUMPS);
params.set_reordering_method(ReorderingMethod::METIS);
}
params.synchronize();
See: SolverParameters class in cl_SolverParameters.hpp
Solver Backend Comparison
| Backend | Type | MPI | Shared-Mem | Compression | When to Use |
| UMFPACK | Direct | No | No | No | Sequential, moderate size (< 100k DOFs) |
| MUMPS | Direct | Yes | Yes | BLR | Parallel, general matrices, robust |
| STRUMPACK | Direct/Iterative | Yes | Yes | BLR, HSS | Parallel, best performance, modern |
| PARDISO | Direct | No | Yes (OpenMP) | No | Shared-memory parallel, Intel CPUs |
| PETSc | Iterative | Yes | No | N/A | Very large systems, SPD matrices |
Performance Characteristics
UMFPACK (SuiteSparse):
- Sequential only
- Excellent for unsymmetric matrices
- Mature and stable
- Best for < 100k DOFs on single node
MUMPS:
- MPI + OpenMP hybrid parallelism
- Robust for ill-conditioned systems
- Good for general unsymmetric matrices
- Block low-rank compression available
- Reliable fallback when STRUMPACK fails
STRUMPACK (Recommended):
- MPI + OpenMP hybrid parallelism
- Best overall performance in BELFEM
- Low-rank compression (BLR, HSS)
- Both direct and iterative solvers
- Can be 2× faster than MUMPS (Messe et al. 2023)
- Preferred for production runs
PARDISO (Intel MKL):
- Shared-memory parallelism via OpenMP
- Excellent single-node performance
- Optimized for Intel CPUs
- Limited to shared-memory systems
PETSc:
- Scalable to very large systems (millions of DOFs)
- Flexible Krylov method selection
- Requires good preconditioner for fast convergence
- Best for SPD matrices with CG
- GAMG preconditioner works well for elliptic problems
Default Solver Selection
The default solver is determined at compile time (see en_SolverEnums.hpp:193-203):
#ifdef BELFEM_STRUMPACK
gDefaultSolver = SolverType::STRUMPACK;
#elif BELFEM_MUMPS
gDefaultSolver = SolverType::MUMPS;
#elif BELFEM_PARDISO
gDefaultSolver = SolverType::PARDISO;
#elif BELFEM_SUITESPARSE
gDefaultSolver = SolverType::UMFPACK;
#elif BELFEM_SUPERLU
gDefaultSolver = SolverType::SUPERLU;
#endif
Recommendation: Compile with STRUMPACK for production, MUMPS for robustness, PETSc for extreme scale.
Free Functions
preferred_matrix_format(solver_type) - Optimal Matrix Format
Returns the preferred sparse matrix format for a given solver.
SolverType solver = SolverType::MUMPS;
SpMatrixType format = preferred_matrix_format(solver);
SpMatrix A(graph, preferred_matrix_format(solver));
Mapping:
- UMFPACK → CSC
- MUMPS → CSR
- STRUMPACK → CSR
- PARDISO → CSR
- PETSc → CSR (converted to AIJ internally)
File: fn_preferred_matrix_format.hpp
matrix_type(solver_type) - Storage Format for a Solver
Returns SpMatrixType::CSC for UMFPACK, SuperLU and MUMPS, CSR for PARDISO, PETSc and STRUMPACK. It never sees a matrix and does not detect symmetry. Note that preferred_matrix_format() answers the same question with CSR for MUMPS; use one consistently.
SpMatrixType tType = matrix_type(SolverType::MUMPS);
File: fn_matrix_type.hpp
compute_permutation(A, graph, forward, backward, index) - METIS Permutation
Builds a graph from A, runs METIS nested dissection on it and fills three Cell<int_t> permutations (row forward/backward, and new→old nonzero positions). No method argument: METIS only. graph must be empty on entry.
SpMatrix A(graph_in, SpMatrixType::CSR);
Graph graph;
Cell<int_t> forward, backward, index;
sparse::compute_permutation(A, graph, forward, backward, index);
File: fn_compute_permutation.hpp
create_graph_from_matrix(A) - Extract Connectivity Graph
Extracts connectivity graph from sparse matrix structure.
SpMatrix A(graph_in, SpMatrixType::CSR);
Graph graph_out;
sparse::create_graph_from_matrix(A, graph_out);
File: fn_create_graph_from_matrix.hpp
rcond(A) - Reciprocal Condition Number
Estimates the reciprocal condition number via CHOLMOD (requires USE_SUITESPARSE=ON, default OFF, and an OpenMP-enabled build; aborts otherwise).
SpMatrix A(graph, SpMatrixType::CSR);
real rc = rcond(A);
if (rc < 1e-12) {
message(InfoLevel::Minimal,
"Matrix is ill-conditioned");
}
void message(const belfem::InfoLevel aInfoLevel, const std::string &aFormat, const Args ... aArgs)
Definition cl_Logger.hpp:144
Note: Expensive operation (requires factorization). Use for diagnostics only.
File: fn_rcond.hpp
Common Patterns
Pattern 1: Solving a Sparse Linear System
Graph graph = build_dof_graph(
mesh);
SpMatrix K(graph, SpMatrixType::CSR);
for (Element* e :
mesh->elements()) {
Matrix<real> Ke = e->stiffness();
Cell<index_t> dofs = e->dof_indices();
for (index_t i = 0; i < dofs.size(); ++i) {
for (index_t j = 0; j < dofs.size(); ++j) {
K(dofs(i), dofs(j)) += Ke(i, j);
}
}
}
Vector<real> f(n_dofs, 0.0);
Vector<real> u(n_dofs);
Solver solver;
solver.solve(K, u, f);
Pattern 2: Symmetric Positive Definite System
SpMatrix K(graph, SpMatrixType::CSR);
Solver solver;
solver.set_symmetry_mode(SymmetryMode::Unsymmetric);
Vector<real> u(n), f(n);
solver.solve(K, u, f);
Pattern 3: Multiple Solves with Same Matrix
SpMatrix K(graph, SpMatrixType::CSR);
Solver solver;
Vector<real> u1(n), f1(n);
solver.solve(K, u1, f1);
Vector<real> u2(n), f2(n);
solver.solve(K, u2, f2);
Pattern 4: Iterative Solve with PETSc
SpMatrix A(graph, SpMatrixType::CSR);
Vector<real> x(n), b(n);
Solver solver(SolverType::PETSc);
solver.set_petsc(
Preconditioner::GAMG,
KrylovMethod::CG,
1e-10
);
x.fill(0.0);
solver.solve(A, x, b);
Pattern 5: MPI Parallel Solve
SpMatrix A;
Graph graph = build_dof_graph(
mesh);
A = SpMatrix(graph, SpMatrixType::CSR);
}
Vector<real> x(n), b(n);
Solver solver(SolverType::MUMPS);
solver.solve(A, x, b);
Pattern 6: Saving and Reusing Matrix
SpMatrix K(graph, SpMatrixType::CSR);
K.save("stiffness.h5", "K", FileMode::NEW);
SpMatrix K_loaded("stiffness.h5", "K");
Solver solver;
Vector<real> u(n), f(n);
solver.solve(K_loaded, u, f);
Performance Tips
1. Choose the Right Solver
Solver solver(SolverType::UMFPACK);
Solver solver(SolverType::STRUMPACK);
Solver solver(SolverType::MUMPS);
Solver solver(SolverType::PETSc);
solver.set_petsc(Preconditioner::GAMG, KrylovMethod::CG, 1e-8);
2. Enable Compression for Large Systems
Solver solver(SolverType::MUMPS);
solver.set_mumps_blr(
MumpsBlockLowRanking::FactorizationAndSolution,
1e-4
);
3. Use Optimal Reordering
Solver solver(SolverType::MUMPS);
solver.set_mumps_reordering(
MumpsSerialReodrdering::METIS,
MumpsParallelReodrdering::PARMETIS
);
4. Exploit Symmetry
Not available with MUMPS. A symmetric mode would cut factor time and memory substantially, but under SYM != 0 MUMPS wants exactly ONE representative of each symmetric coordinate. Either triangle is acceptable — it does not insist on the lower one — and what is fatal is supplying BOTH: (i,j) and (j,i) are then summed as duplicates. BELFEM stores and hands over the FULL matrix and nothing extracts a triangle, so every off-diagonal would arrive twice and MUMPS would factorize a different matrix – without failing. Measured on a matrix with a known spectrum: a first-solve residual of 7.4e16 and a converged eigenvalue of -2.5e-19 against a true 2.46e-6.
MUMPS::initialize() therefore rejects any symmetric mode with an always-active error rather than accepting one it cannot honor. To enable it, supply one triangle from the wrapper — see todo/mumps_symmetric_triangle_extraction.md.
SuperLU is unaffected: its SymmetricMode is a pivoting and ordering heuristic, not a triangle-storage contract, so it reads the full matrix as intended.
solver.set_symmetry_mode(SymmetryMode::Unsymmetric);
5. Use Optimal Matrix Format
SolverType solver_type = SolverType::MUMPS;
SpMatrix A(graph, preferred_matrix_format(solver_type));
SpMatrixType format = (solver_type == SolverType::UMFPACK)
? SpMatrixType::CSC : SpMatrixType::CSR;
SpMatrix A(graph, format);
6. Minimize Matrix Copies
SpMatrix A(graph, SpMatrixType::CSR);
solver.solve(A, x, b);
SpMatrix A_backup;
A_backup = A;
MPI Considerations
Matrix Distribution
BELFEM uses host-centralized sparse-matrix input for MPI-capable solvers: rank 0 owns the full SpMatrix, and the solver wrapper or backend distributes the work internally. MUMPS consumes the rank-0 matrix through the library interface; STRUMPACK and PETSc build sparse::DistMatrix slices from the rank-0 pointer, index, and value arrays.
SpMatrix A;
A = SpMatrix(graph, SpMatrixType::CSR);
}
solver.solve(A, x, b);
Solver Parallelism
| Solver | MPI | OpenMP | Best Use Case |
| UMFPACK | No | No | Sequential |
| MUMPS | Yes | Yes | Hybrid parallel |
| STRUMPACK | Yes | Yes | Hybrid parallel (best) |
| PARDISO | No | Yes | Shared-memory |
| PETSc | Yes | No | Pure MPI |
Hybrid Parallel Example (STRUMPACK/MUMPS):
# 4 MPI ranks, 8 OpenMP threads each = 32 cores total
export OMP_NUM_THREADS=8
mpirun -np 4 ./my_belfem_app
BELFEM's Own OpenMP Kernels: USE_BELFEM_OPENMP
The OpenMP threads listed above belong to the third-party solvers. BELFEM's own sparse kernels use a separate switch. That switch is OFF by default:
| CMake option | Default | Governs |
| USE_OPENMP | ON | -fopenmp for the whole build; the solvers need it, and the thread-budget queries (hatch_turtle() in cl_SolverWrapper.cpp, the banner's Threads Used line, PARDISO's gParameters(3)) key off the plain OMP define |
| USE_BELFEM_OPENMP | OFF | only the !$omp directives in splinalg.f90 (matvec_csr, matvec_csc), arpacktools.f90 and parpacktools.f90, through the BELFEM_OMP define (CMakeLists.txt, appended under USE_OPENMP AND USE_BELFEM_OPENMP) |
These switches are not interchangeable. Moving the thread-budget queries to BELFEM_OMP would remove the oversubscription warning and silently serialize PARDISO on every default build. The three sites have comments that state this.
Why the kernels are OFF. matvec_csc parallelizes its column scatter with an OpenMP array reduction, !$omp reduction(+:y) (splinalg.f90, matvec_csc). y is an explicit-shape dummy, so gfortran creates a private copy of the entire result vector in each worker thread's stack frame. A libgomp worker gets 2 MiB on Darwin. As a result, a threaded CSC product dies with SIGBUS once
n > 2 MiB / 8 bytes = 262,144 rows
Ordinary 3D meshes can exceed that size easily. The threading also provides no measurable benefit: both call paths for these kernels run master-only (the Newton residual in cl_FEM_DofMgr_SolverData.cpp and the ARPACK inverse iteration in cl_FEM_DofMgr_EigenValues.cpp). The kernels cost about two flops per nonzero and run alongside a full assembly and a direct factorization at each step. No threaded-versus-serial timing for them exists anywhere in the tree. The parallelism was therefore switched off instead of repaired, as stated in the kernel comment above the reduction. One useful side effect is that, with the switch OFF, matvec_csc is bitwise deterministic across thread counts.
Under an MKL build the switch is moot. The two-argument SpMatrix::multiply forwards to the five-argument overload (cl_SpMatrix.cpp, SpMatrix::multiply). Its BELFEM_MKL branch calls mkl_sparse_d_mv; both remaining matvec_csc/matvec_csr call sites are inside its #else. An MKL build therefore never reaches the Fortran kernels on these paths.
When to turn it ON. Use it only in the following two situations, never in a production or CI build:
- To measure whether the threading ever pays. Build once with -DUSE_BELFEM_OPENMP=ON and once without it. Run the same deck with the same OMP_NUM_THREADS, then compare per-phase timings instead of step wall-time. Raise OMP_STACKSIZE for the ON run (OMP_STACKSIZE=64M carries a 295,315-dof case that crashes at the default). If the measurement shows a gain, the private copy must still move off the worker stack before ON can become a supported configuration.
- To prove the switch is a two-way toggle: make one build with the token ON and run make check.
What ON does not do. It does not change the third-party solvers' threading. STRUMPACK, MKL and a threaded BLAS spawn their own workers regardless, and a libgomp worker gets the same 2 MiB default stack on Darwin. Therefore, OMP_STACKSIZE remains a useful diagnostic knob with the switch OFF, and a fault inside a gomp_thread_start frame is not automatically a BELFEM bug. The switch also has no effect on an MKL build, for the reason given above.
OMP_NUM_THREADS=1 hides the failure rather than fixing it — the work then runs on the 8 MiB main-thread stack. Use this as a diagnostic, not as a configuration.
See doc/parallel_execution.md for the wider picture. The matrix backends (Armadillo, Blaze) parallelize their own expression evaluation whenever OpenMP is available. Neither uses an OpenMP reduction, so neither can hit this failure.
Thread Safety
Thread Safety: Like other BELFEM modules, sparse matrices and solvers are not thread-safe.
- Reading: Safe from multiple threads (const operations)
- Writing: Requires external synchronization
- Solving: One solve per solver instance at a time
Example with OpenMP:
SpMatrix K(graph, SpMatrixType::CSR);
#pragma omp parallel for
for (int e = 0; e < n_elements; ++e) {
Matrix<real> Ke = compute_element_stiffness(e);
Cell<index_t> dofs = get_element_dofs(e);
#pragma omp critical
{
for (index_t i = 0; i < dofs.size(); ++i) {
for (index_t j = 0; j < dofs.size(); ++j) {
K(dofs(i), dofs(j)) += Ke(i, j);
}
}
}
}
Solver solver;
solver.solve(K, u, f);
Enumerations Reference
SolverType
enum class SolverType {
UMFPACK,
SUPERLU,
MUMPS,
STRUMPACK,
PARDISO,
PETSc,
UNDEFINED
};
SpMatrixType
enum class SpMatrixType {
CSC,
CSR,
UNDEFINED
};
SymmetryMode
enum class SymmetryMode {
Unsymmetric = 0,
PositiveDefiniteSymmetric = 1,
GeneralSymmetric = 2,
UNDEFINED
};
Preconditioner (PETSc only)
enum class Preconditioner {
NONE,
JACOBI,
BJACOBI,
ASM,
GAMG,
ILU,
ICC,
LU,
UNDEFINED
};
KrylovMethod (PETSc, STRUMPACK)
enum class KrylovMethod {
PREONLY,
CG,
GMRES,
BCGS,
CGS,
TFQMR,
AUTO,
UNDEFINED
};
ReorderingMethod
enum class ReorderingMethod {
NATURAL = 0,
METIS = 1,
SCOTCH = 2,
AUTOMATIC = 3,
PARMETIS = 4,
PTSCOTCH = 5,
UNDEFINED = 6
};
CompressionMethod
enum class CompressionMethod {
OFF = 0,
BLR = 1,
AUTOMATIC = 2,
UNDEFINED = 3
};
Related Modules
- linalg: Dense Matrix<T> and Vector<T> for element-level operations
- fem/kernel: Uses sparse module for global system assembly and solve
- mesh: Provides connectivity graphs for sparse matrix construction
- comm: MPI utilities for distributed sparse matrices
Debugging Tips
1. Check Matrix Symmetry
SpMatrix A(graph, SpMatrixType::CSR);
for (index_t i = 0; i < A.n_rows(); ++i) {
for (index_t j = i+1; j < A.n_cols(); ++j) {
real Aij = A(i, j);
real Aji = A(j, i);
if (std::abs(Aij - Aji) > 1e-12) {
"Asymmetry at (%lu,%lu): %e vs %e", i, j, Aij, Aji);
}
}
}
2. Visualize Sparsity Pattern
SpMatrix A(graph, SpMatrixType::CSR);
A.print("Stiffness");
"Matrix: %lu × %lu, nnz = %lu, fill = %.2f%%",
A.n_rows(), A.n_cols(), A.number_of_nonzeros(),
100.0 * A.number_of_nonzeros() / (A.n_rows() * A.n_cols()));
3. Check Conditioning
real rc = rcond(A);
if (rc < 1e-12) {
"Matrix is ill-conditioned (rcond = %e)", rc);
}
4. Enable Solver Diagnostics
Solver solver(SolverType::MUMPS);
solver.set_mumps_error_analysis(MumpsErrorAnalysis::Full);
Vector<real> residual = A * x - b;
real res_norm = norm(residual);
message(InfoLevel::Default,
"Residual norm: %e", res_norm);
See Also
- Sparse Solver Libraries:
- BELFEM Modules:
- Build System:
- CLAUDE.md (repository root) - Build configuration and solver selection
- CMakeLists.txt - Compile-time solver flags
Revision History:
- 2026-01-16: Initial comprehensive documentation
Contributors:
- Based on BELFEM sparse module by Christian Messe and Gregory Giard
- Documentation synthesized from code analysis and usage patterns
Prepared by the BELFEM documentation team.