Skip to content

feat(solver): exact fused-block solve for collinearity-warned term groups (#281) - #288

Open
schroedk wants to merge 18 commits into
mainfrom
feat/281-fused-exact-block
Open

feat(solver): exact fused-block solve for collinearity-warned term groups (#281)#288
schroedk wants to merge 18 commits into
mainfrom
feat/281-fused-exact-block

Conversation

@schroedk

Copy link
Copy Markdown
Collaborator

Fixes the #281 iteration blow-up that #283's screen only detects. Stacked on #283.

  • Solver::new builds 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 reloaded Preconditioner stays self-contained).
  • Low-mobility AKM stress (50k workers / 21k firms, near-shared slope covariate): 3000-iteration cap unconverged → 26 iterations, fused-side cost < 1 s (#[ignore] stress test).
  • Null pivots are regularized with a large pivot: a tiny delta amplifies the block's exact nulls ~1e12× and fakes convergence. Correctness is pinned by reference-match tests against the unpreconditioned solve, including exact sharing.
  • The collinearity screen now shares one per-term Gram pass across all candidate covariates (bitwise-identical residuals).
  • within gains a faer dependency; algorithmic write-up in docs part 3, §5.

@schroedk
schroedk force-pushed the fix/280-282-slope-pair-hardening branch from 9796e6f to 2c3be19 Compare August 20, 2026 20:28
@schroedk
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.
Verdicts are recorded (IC dead, FSAI productionized in 9b66950, fill
scaling and BLR incompressibility measured); the probes stay retrievable
at 780a5da. solver/tests.rs keeps only the permanent suite.
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
schroedk force-pushed the feat/281-fused-exact-block branch from 8563fc6 to 512d95e Compare August 21, 2026 08:26
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant