Skip to content

Commit cdf7be3

Browse files
committed
Added suitesparse backend for row reduction algorithm
It is faster than the dense solver dgeqp3 for constraint matrix reduction
1 parent 946fb64 commit cdf7be3

2 files changed

Lines changed: 113 additions & 1 deletion

File tree

alm/least_squares.cpp

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,90 @@ static auto solve_least_squares_spqr(const Eigen::SparseMatrix<double> &A, const
107107
cholmod_l_finish(&cc);
108108
return status;
109109
}
110+
111+
// Rank-revealing reduction of a HOMOGENEOUS constraint matrix C (rows define C x = 0) via SuiteSparseQR
112+
// -- a sparse, multithreaded replacement for the densifying LAPACK dgeqp3 path. We factorize the TALL
113+
// matrix C^T (N x P) with rank detection and column pivoting: the rank-revealing pivot deflates the
114+
// dependent columns of C^T (= dependent rows of C) to the end of the permutation E, so E[0..rank-1] are
115+
// the independent rows of C. C_red is then those ORIGINAL (sparse) rows of C -- NOT the R factor, which
116+
// is densely filled for these constraints. R (only rank x P here) and Q are discarded.
117+
// Only the homogeneous case is handled (every invariance subset, and the merged matrix when no
118+
// FC2FIX/FC3FIX value is imposed); inhomogeneous d != 0 and any SuiteSparseQR failure fall back to
119+
// dgeqp3. Returns 0 on success, 1 on failure.
120+
static auto get_independent_rows_spqr(const Eigen::SparseMatrix<double> &C, const int verbosity,
121+
const double tolerance, Eigen::SparseMatrix<double> &C_red, int &r) -> int
122+
{
123+
const SuiteSparse_long P = C.rows();
124+
const SuiteSparse_long Ncol = C.cols();
125+
126+
Eigen::SparseMatrix<double> Ct = C.transpose(); // N x P; its columns are the rows of C
127+
Ct.makeCompressed();
128+
const SuiteSparse_long nnz = Ct.nonZeros();
129+
130+
// Widen Eigen's 32-bit CSC indices to the 64-bit indices the long CHOLMOD interface expects
131+
// (same pattern as solve_least_squares_spqr). C^T has P columns, so outer has P+1 entries.
132+
std::vector<SuiteSparse_long> outer(P + 1), inner(nnz);
133+
for (SuiteSparse_long j = 0; j <= P; ++j) outer[j] = Ct.outerIndexPtr()[j];
134+
for (SuiteSparse_long k = 0; k < nnz; ++k) inner[k] = Ct.innerIndexPtr()[k];
135+
136+
cholmod_common cc;
137+
cholmod_l_start(&cc);
138+
139+
cholmod_sparse A; // A = C^T (N x P)
140+
std::memset(&A, 0, sizeof(A));
141+
A.nrow = Ncol;
142+
A.ncol = P;
143+
A.nzmax = nnz;
144+
A.p = outer.data();
145+
A.i = inner.data();
146+
A.x = const_cast<double *>(Ct.valuePtr());
147+
A.stype = 0;
148+
A.itype = CHOLMOD_LONG;
149+
A.xtype = CHOLMOD_REAL;
150+
A.dtype = CHOLMOD_DOUBLE;
151+
A.sorted = 1;
152+
A.packed = 1;
153+
154+
// Map the caller's auto sentinel (tol < 0, i.e. rank_tolerance_auto = -1) to SuiteSparseQR's default
155+
// tolerance SPQR_DEFAULT_TOL (= -2). Passing -1 literally would be SPQR_NO_TOL (rank detection off).
156+
// An explicit positive tolerance is passed through. NOTE: SuiteSparseQR thresholds column 2-norms
157+
// whereas dgeqp3 thresholds R-diagonals, so the numerical rank may differ by a few rows on a
158+
// borderline (badly-scaled) constraint set.
159+
const double spqr_tol = (tolerance < 0.0) ? SPQR_DEFAULT_TOL : tolerance;
160+
161+
cholmod_sparse *R = nullptr;
162+
SuiteSparse_long *E = nullptr; // size P column permutation of C^T = row permutation of C
163+
const SuiteSparse_long rank = SuiteSparseQR_C(SPQR_ORDERING_DEFAULT, spqr_tol, /*econ=*/0, /*getCTX=*/0,
164+
&A, /*Bsparse=*/nullptr, /*Bdense=*/nullptr,
165+
/*Zsparse=*/nullptr, /*Zdense=*/nullptr, &R, &E,
166+
/*H=*/nullptr, /*HPinv=*/nullptr, /*HTau=*/nullptr, &cc);
167+
168+
int status = 1;
169+
if (rank >= 0 && cc.status == CHOLMOD_OK) {
170+
r = static_cast<int>(rank);
171+
// E[0..rank-1] are the independent columns of C^T = independent rows of C (E == NULL => identity).
172+
// Build C_red from those ORIGINAL sparse rows of C.
173+
Eigen::SparseMatrix<double, Eigen::RowMajor> Crow = C;
174+
std::vector<Eigen::Triplet<double>> tri;
175+
tri.reserve(static_cast<size_t>(C.nonZeros()));
176+
for (int k = 0; k < r; ++k) {
177+
const SuiteSparse_long orig_row = E ? E[k] : k;
178+
for (Eigen::SparseMatrix<double, Eigen::RowMajor>::InnerIterator it(Crow, orig_row); it; ++it) {
179+
tri.emplace_back(k, static_cast<int>(it.col()), it.value());
180+
}
181+
}
182+
C_red.resize(r, static_cast<int>(Ncol));
183+
C_red.setFromTriplets(tri.begin(), tri.end());
184+
C_red.makeCompressed();
185+
status = 0;
186+
LOG_IF(verbosity, 1, "SuiteSparseQR reduction: rank ", r, " of ", static_cast<int>(P),
187+
" rows, C_red nnz=", C_red.nonZeros(), ".\n");
188+
}
189+
if (R) cholmod_l_free_sparse(&R, &cc);
190+
if (E) cholmod_l_free(P, sizeof(SuiteSparse_long), E, &cc);
191+
cholmod_l_finish(&cc);
192+
return status;
193+
}
110194
#endif
111195

112196

@@ -289,6 +373,20 @@ auto get_independent_rows_lapack_sparse(const Eigen::SparseMatrix<double> &C_spa
289373

290374
LOG_IF(verbosity, 1, "P = ", P, ", N = ", N, ", nnz = ", C_sparse.nonZeros(), ".\n");
291375

376+
#ifdef USE_SUITESPARSE_BACKEND
377+
// SuiteSparseQR rank-revealing reduction for HOMOGENEOUS constraints (every invariance subset, and
378+
// the merged matrix when no FC2FIX/FC3FIX value is imposed): sparse + multithreaded, avoiding the
379+
// dense P x N densification and dgeqp3 below. Inhomogeneous d (d != 0, from FC2FIX/FC3FIX) and any
380+
// SuiteSparseQR failure fall through to the dgeqp3 path.
381+
if (dvec.isZero(0)) {
382+
if (get_independent_rows_spqr(C_sparse, verbosity, tolerance, C_red, r) == 0) {
383+
d_red = Eigen::VectorXd::Zero(r);
384+
return 0;
385+
}
386+
LOG_ERR_IF(verbosity, 0, "SuiteSparseQR reduction failed; falling back to dgeqp3.\n");
387+
}
388+
#endif
389+
292390
// 1) Copy sparse→dense column-major buffer C_mat (size P×N)
293391
std::vector<double> C_mat(static_cast<size_t>(P_i) * static_cast<size_t>(N_i), 0.0);
294392
for (int col = 0; col < N; ++col) {

docs/source/install.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,26 @@ least-squares problems, especially ``LMODEL = ols``, ``SPARSE = 1`` with
185185
numerical constraints ``ICONST = 1, 2, 3, 4``, a native build with optimized
186186
sparse direct solvers can be much faster and more memory-scalable.
187187

188-
The recommended priority is:
188+
For the **KKT solver** (the symmetric-indefinite system of the numerically
189+
constrained sparse fit) the recommended priority is:
189190

190191
1. **Intel MKL / PARDISO** on Linux or Intel oneAPI systems.
191192
2. **SuiteSparse / SPQR + CHOLMOD** when SuiteSparse is available.
192193
3. **Apple Accelerate** on macOS when MKL is unavailable.
193194

195+
These three are mutually exclusive *as the KKT solver*. **SuiteSparse, however,
196+
is an orthogonal add-on, not just a KKT-solver choice:** ``-DUSE_SUITESPARSE_BACKEND=yes``
197+
can be combined with ``-DUSE_MKL_BACKEND=yes`` (or ``-DUSE_ACCEL_BACKEND=yes``).
198+
In that combined build the LDLT backend (PARDISO / Accelerate) still solves the
199+
KKT system, while the multithreaded **SuiteSparseQR** replaces the dense LAPACK
200+
``dgeqp3`` for the rank-revealing **constraint-matrix reduction** -- which can
201+
otherwise dominate the runtime on large numerically-constrained fits (e.g. a
202+
254k-parameter ``ICONST = 2`` reduction dropped from ~9 min to ~10 s). The
203+
recommended high-performance configuration on Intel systems is therefore
204+
``-DUSE_MKL_BACKEND=yes -DUSE_SUITESPARSE_BACKEND=yes`` together. (Build
205+
SuiteSparse against the same BLAS as the solver backend -- e.g. MKL -- to avoid
206+
linking two BLAS implementations.)
207+
194208
.. raw:: html
195209

196210
<details>

0 commit comments

Comments
 (0)