qdldl: replay a precomputed schedule on numeric refactorization - #6
qdldl: replay a precomputed schedule on numeric refactorization#6batterseapower wants to merge 1 commit into
Conversation
After the first factorization the sparsity pattern of L is fixed, but _factor_inner re-derives the per-column update lists on every refactor: an elimination-tree walk with marker arrays, a work buffer and a reversal step. Interior-point use refactors the same pattern once per iteration, so this control flow is pure overhead. This change records the discovered control flow on the first refactor as a flat list of (column, position) update steps, then replays the identical floating-point operations in the identical order on subsequent refactorizations. L, D, Dinv, the inertia count and the dynamic-regularization decisions are bit-for-bit identical to the original path -- enforced by new tests that compare a refactorization against a fresh factorization bitwise, including a case where dynamic regularization fires. This is the standard symbolic/numeric phase separation of sparse direct solvers (T. Davis, "Direct Methods for Sparse Linear Systems", SIAM 2006, ch. 4). The schedule is built lazily on the first refactor() call, so one-shot factorizations pay nothing. The build self-verifies against the existing pattern of L (every recorded position must satisfy Li[pos] == k, and the step count must equal nnz(L)); on any mismatch, or if indices would not fit the u32 storage used to halve replay memory traffic, refactorization falls back to the original path. Measured on an interleaved A/B benchmark (Apple M-series, min of 3 rounds x 2 reps, objectives bitwise-equal throughout): -2.1% total wall time over nine portfolio-rebalance QP/SOCPs (n=9.5k-18k), and -3.9% total over seven Netlib-Kennington/Mittelmann/Maros-Meszaros/ conic problems (all 16 problems individually faster, -1.2% to -5.4%). The win is modest because these factors are flop-bound; the removed overhead grows in relative terms the sparser the factor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqGdm2vZZ6HA5HT8DsRePa
Public-corpus timing, measured per trackAdded because the earlier corpus timings in this description used a narrow 7-problem set. Re-measured on 14 timing-relevant public problems (Netlib-Kennington
All 14 problems individually faster with the full stack (−6.9% to −56.3%; Worth stating explicitly since the original motivation was a portfolio workload: this is not a workload-specific optimisation. The public corpus gains (−26.3%) are of the same order as the portfolio set (−42.6%), and the largest single component (#6 + #9, −21.8%) changes no floating-point result at all. |
|
Submitted upstream as oxfordcontrol#230 (both commits of this track together, since a cross-repo PR must target upstream |
What
After the first factorization the sparsity pattern of
Lis fixed, but_factor_innerre-derives the per-column update lists on every refactor: an elimination-tree walk with marker arrays, a work buffer, and a reversal step. Interior-point use refactors the same pattern once per IP iteration, so that control flow is pure overhead.This PR records the control flow once — a flat list of
(cidx, pos)update steps per column, exactly as_factor_innerexecutes them — and replays it on subsequent refactorizations. The update range of a step isLp[cidx]..posbecause a column holds exactly its rows< kwhen step(k, cidx)runs, andpos(the write position ofL[k,cidx]) is a fixed index once the pattern is fixed. This is the standard symbolic/numeric phase separation of sparse direct solvers (T. Davis, Direct Methods for Sparse Linear Systems, SIAM 2006, ch. 4); QDLDL's single-phase design favours one-shot factorization, which Clarabel's refactor-heavy usage inverts.Why it is correct
The replay performs the identical floating-point operations in the identical order as
_factor_inner(..., logical_factor=false)— same A-column scatter, same update loop bounds, same pivot regularization tests on the same values. SoL,D,Dinv, the inertia count and the dynamic-regularization decisions are bit-for-bit identical.Defensive design: the schedule is built lazily on the first
refactor()(one-shot factorizations pay nothing), the build self-verifies every recorded position against the existing pattern (Li[pos] == kper step, total =nnz(L)), and any mismatch — or a pattern too large for theu32step storage — falls back to the original path.No tunable constants. The only magic number is the
u32index width, which is a storage choice guarded by an explicit overflow check and fallback.Validation suites used throughout this series
All timing is on one Apple M-series core, single-threaded,
--releasewithdebug symbols, and every comparison is interleaved: the two binaries are
run alternately problem-by-problem within each round, so thermal drift and
machine noise affect both sides equally. Reported times are the minimum over
rounds. (Session-to-session noise on this machine is large — up to 20% between
identical binaries at different times — so non-interleaved comparisons are not
trustworthy and none are quoted.)
1. Unit / integration tests.
cargo test— 20 test binaries, all passing,plus the specific new tests listed per PR below.
cargo clippyclean on thetouched files.
2. Portfolio-rebalance set (9 problems, the motivating workload).
Real conic problems from a production portfolio-rebalance backtest:
n = 9,448–18,424, m = 22,329–43,273, nnz(A) = 271k–538k, diagonal
P, twolarge nonnegative cones and one second-order cone of dimension 3,279–6,271
(a factor-model risk constraint: ~156 dense-ish factor rows plus a diagonal
idiosyncratic block). Used for the headline timings.
3. In-sample public corpus (224 problems). Every tiny/small/medium
non-PSD problem in a locally converted corpus: 101 Maros–Mészáros QPs,
85 Netlib LPs, 24 structured conic problems (SOCP/EXP/POW built from UCI
data), 12 Netlib-Kennington LPs, 2 Mittelmann LPs. Each has an
independently verified reference objective. Used as the status/objective
regression gate for every change.
4. Out-of-sample suites (92 problems, fetched fresh from the internet).
Because suite 3 became a tuning gate, two further suites were added that
were never used to guide any decision:
MIPLIB 2017 benchmark set as LP relaxations — 45 instances. Downloaded
from
miplib.zib.de; the MPS reader ignores INTORG/INTEND markers, soreading a MIPLIB file yields exactly the continuous relaxation. These are
substantially harder for an interior-point method than the portfolio
problems. 90s time limit per solve.
CBLIB (Conic Benchmark Library) — 47 instances, sampled across families
from
cblib.zib.de, including the DIMACS classicsnb,nb_L1,nb_L2,nql30/60/180,qssp30/60,sched_*. Converted with a new CBF reader;integer instances are taken as continuous relaxations, and rotated
quadratic cones are mapped to second-order cones by the orthogonal
rotation
u=(x1+x2)/√2, v=(x1−x2)/√2.Conversion validated independently:
nbsolves to −5.0703094644e-2,matching its published DIMACS optimum (−0.05070309), and HiGHS (installed
for the purpose) agrees on the LP-only conversions, e.g.
gen_ip0546765.209042728 vs Clarabel 6765.2090428.
Tests specific to this PR
test_refactor_matches_fresh_factor_exactly— refactor after value updates must equal a fresh factorization bitwise (L.nzval,D,Dinv, inertia, regularize count) on a 70×70 quasidefinite matrix with genuine fill-in, checked twice (first replay and a subsequent one), and asserts the replay path was actually engaged rather than silently falling back.test_refactor_matches_fresh_factor_with_regularization— the same, on a matrix constructed so dynamic regularization actually fires (asserted).Performance: before → after
Interleaved, min of 3 rounds × 2 reps. Objectives are bitwise-equal and statuses and iteration counts identical on every problem below, which is the strongest possible quality result: the solver follows exactly the same trajectory, only faster.
Portfolio-rebalance set (9 problems)
Public corpus (7 timing-relevant problems)
All 16 problems individually faster. The win is modest because these factors are flop-bound (measured Σᵢlᵢ² ≈ 42M on the largest problem, dominated by a dense trapezoid from ~156 dense-ish rows); the removed overhead grows in relative terms the sparser the factor, which is why the sparse LPs gain more.
Status/objective regression gate
Bit-identity makes this vacuous but it was run anyway: 224-problem in-sample corpus — 0 status changes, 0 iteration-count changes, 0 objective changes.
Cost
nnz(L)×8B +(n+1)×4B for the schedule (≈4.7MB on the largest problem here), allocated on first refactor only.Note for reviewers
#9 is stacked on this branch and adds the run-decomposition kernels; the combination is −12.7% on the portfolio set. This PR's test helper
test_matrix_quasidefand #10'stest_matrix_kkt_likeare near-duplicate random-KKT generators on independent branches — worth deduping into one helper once both land.🤖 Generated with Claude Code
https://claude.ai/code/session_01CqGdm2vZZ6HA5HT8DsRePa