feat(solver): exact fused-block solve for collinearity-warned term groups (#281) - #288
Open
schroedk wants to merge 18 commits into
Open
feat(solver): exact fused-block solve for collinearity-warned term groups (#281)#288schroedk wants to merge 18 commits into
schroedk wants to merge 18 commits into
Conversation
schroedk
force-pushed
the
fix/280-282-slope-pair-hardening
branch
from
August 20, 2026 20:28
9796e6f to
2c3be19
Compare
schroedk
changed the base branch from
fix/280-282-slope-pair-hardening
to
main
August 21, 2026 08:21
…rms (#281) An additive sparse LDLT (faer, AMD-ordered, symbolic-fill-gated) over each connected component of warned term pairs resolves the cross-term near-null continuum that no pair subdomain contains; low-mobility shared-covariate panels drop from a 3000-iteration cap (unconverged) to 26 iterations.
Promoting faer to a normal dependency of within pulled its tree into the bans graph; skip exactly the introduced duplicate versions so any new duplicate still fails.
…nk decay) IC(tau) plateaus ~316 at any tau (breakdown shift flattens the near-null tail); static pattern-A FSAI cures the delta=0.2 band (1192 -> 64 iters, reference-verified) but fails at delta=0.05 where the exact path arms. Fill/row doubles per scale doubling at fixed delta=0.05 (~n^2 total); frontal blocks of the exact factor are incompressible (rank 511/512 at 1e-8), closing the BLR/HSS avenue empirically. Splits assemble_gram out of FusedBlockSolve::build so the probes can assemble the gated matrix.
…281) FusedBlockSolve::build becomes infallible: exact sparse LDLT under the 40M fill gate as before, and a degree-capped static-pattern FSAI (GtG on the prescaled Gram, pattern lower(A)) otherwise, in the same additive FusedPreconditioner slot. Middle-band stress (delta=0.2, fill 539M): 1192 Schwarz iterations -> 64 through the real gate; the exact rung is bitwise-undisturbed at delta=0.05 (26 iterations). FSAI is fill-free, breakdown-free (row subsystems are principal submatrices), and costs ~2*nnz(A) flops per apply.
Singular FSAI row subsystems now get a LARGE pivot (bounded ~1 response, mirroring the exact rung's d_max policy) instead of a 1e-12 clamp; the predicted fake convergence did not reproduce in the new aliased-pair regression test, so this ships as policy hardening. Cap truncation ties break by column index (restores single-thread bitwise reproducibility); the fill gate runs on a pattern-only symbolic before numeric staging; the scaled `entries` duplicate and `unit_diag` are gone; probe-era pub(crate) on FusedGram/assemble_gram is retracted; the low-mobility stress asserts the exact rung armed. Stress re-pinned: 64/26 unchanged.
schroedk
force-pushed
the
feat/281-fused-exact-block
branch
from
August 21, 2026 08:26
8563fc6 to
512d95e
Compare
Take main's rewritten collinearity screen (#296) wholesale: its screened_covariates already carries this branch's Loading::covariate() cleanup, and its relocated tests are a superset of the inline ones. Re-apply the shared-RNG dedup to the moved test module.
The approximate-inverse rung won nothing in the 48 armed cells of the topology sweep, and the middle band it was built for is empty at the scales it was measured for. The fill gate now accepts a term group for exact factorization or declines it, leaving the group to the pairwise decomposition.
…f by default (#303) LocalSolverConfig::fused_block_budget caps the factor's bytes per warned term group; the gate declines a group above it. The default is None, so no design gets a sparse direct solve unless the caller asks for one. Wire format v16 -> v17: the serialized Preconditioner carries the config.
Collapses the three-tuple and the mid-match wildcard in Solver::new, so a deserialized preconditioner honours the budget its own config carries instead of silently discarding it. Also drops FusedGram's duplicate n_local, unboxes the symbolic factor, and inlines a single-use test helper.
nnz(L) sets both the resident bytes and the per-iteration triangular solve, so a byte budget names only half of what fill costs; measured 3.65x per-iteration (3.82 -> 13.96 ms) against 0.345s one-time. fused_block_max_fill caps nnz(L)/nnz(triu(A)), matching the unit the field converged on (PETSc 5.0, Ifpack2 1.0, SuperLU 10.0). Being a ratio it is also scale-free, so it needs no per-group total.
Resolving the fused Gram's null directions grows |L| with the dimension (31 at 500 dofs, 4.8e3 at 5k, overflow at 2.5e4). The resulting inf made M⁻¹p̃ non-finite, and mlsmr read that as a solved system: `vp.max(0.0)` returns the non-NaN operand, so α came out 0 and the solver reported `converged` with `x = 0`.
faer pulled rand, rand_distr, and npyz into within's normal dependency graph; none are reachable (every call site is Par::Seq, so even faer's rayon is dead weight) and they forced 7 of the 11 duplicate-crate skips added with the fused block. std + sparse-linalg is what the factorization actually uses.
The fill *ratio* named a quantity the gate never measured: `SymbolicCholesky:: len_val` is padded dense-block storage on faer's supernodal path, so a threshold tuned at one size could flip accept/decline at another with no change in true fill. A ratio also cannot bound the assembly that precedes it, and `f64` let a NaN budget disable the gate outright. An absolute count of stored LDLᵀ values is what we allocate, so the gate now measures its own currency, the `usize` rules out NaN and negatives by type, and `|L| >= nnz(triu(A))` lets the same budget abandon an over-budget Gram before it is complete. A decline is no longer silent: `BuildWarning::FusedBlockDeclined` carries the reason. Rides the unreleased v17 wire bump that introduced the field.
`FusedBlockDecline::Budget` carried the budget the caller had just set, and the assembly bail could not report the same quantity as the factor gate — one name, two meanings. It becomes a unit variant. The overflow test had copy-pasted the unpreconditioned comparison; both call a shared assertion now. Three budgets through `build` exercised one outcome, so one remains. Comments trimmed to the one-line cap.
faer's `LdltError` has one variant, `ZeroPivot`, raised on `d == 0 || !d .is_finite()`, and the dynamic regularization already replaces zero pivots. The only reachable numeric failure is therefore the same non-finite event the value scan reports five lines later, so it stops being a separate reason and the remaining `Factorization` sites are uniformly structural.
`beta == 0.0` and `alpha > 0.0` are both false for NaN, so an operator that overflowed left the vector unscaled and the NaN propagating through the recurrence — the same laundering `alpha_from_vp` already guards on the preconditioned side, on the four unguarded squared-norm sites. Also pins the fused block's fill gate, which no test reached: every budget the suite declined bailed during assembly first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the #281 iteration blow-up that #283's screen only detects. Stacked on #283.
Solver::newbuilds an exact sparse LDLᵀ (faer, AMD ordering) of the Gram over each connected component of warned term pairs, applied additively on top of the base preconditioner; declined when the symbolic factor exceeds a 40M-nnz fill budget, and never serialized (a reloadedPreconditionerstays self-contained).#[ignore]stress test).withingains afaerdependency; algorithmic write-up in docs part 3, §5.