perf(linalg): per-cell solver fast paths - IRLS Cholesky inner solve + rank-detection certification (CS covariate fits 1.5-2x)#634
Conversation
…+ rank-detection certification Two pure-Python fast paths in the shared solvers (CallawaySantAnna covariate fits and every estimator routing through them): 1. solve_logit's IRLS inner step - previously a full gelsd SVD on the tall weighted design per iteration - now solves equilibrated normal equations by Cholesky under an explicit dpocon reciprocal-condition guard (1e-6), falling back to the exact legacy lstsq line for any iteration whose normal matrix cannot be certified well-conditioned (working weights can crush a column's effective scale on near-separated subgroups, so full column rank pre-loop does not imply a chol-safe normal matrix). Columns equilibrated ONCE; IRLS state, tol, and warnings stay in the raw basis. Fallback count exposed via diagnostics_out["irls_chol_fallback_iters"]. 2. _detect_rank_deficiency stage-0 certification - the always-run pivoted QR on the tall design is short-circuited by a Gram/eigvalsh certification at the _rank_guarded_inv 1e-10 Gram threshold (two orders stricter than the 1e-7 QR boundary, so it never reports full rank where the QR path would report a deficiency); n < k, non-finite, looser-rcond, and uncertifiable designs fall through to the existing two-stage QR verbatim (all drop decisions, pivots, and rank counts unchanged - incl. dCDH residual-df and the rust solve_ols routing boolean, proven by arm instrumentation). Measured (3-rep medians, CS 100k units x 20p = 2M rows, 95 cells): dr cov10 1.53->1.03s (1.49x) | cov20 2.64->1.61s (1.64x) dr cov40 5.69->3.07s (1.85x) | ipw cov20 1.79->0.90s (1.98x) survey dr cov20 2.73->1.68s (1.63x); python-backend pairs equal or better. Stages at cov40: IRLS solve 6.8x, rank detection 16.5x. Deltas: overall ATT/SE exactly 0, per-cell max ~7e-15; golden ipw R-parity and dCDH bit-identity baselines green unmodified; fallback 0% / certification 100% on all bench scenarios; maxrss flat. New tests: TestIRLSCholeskyFastPath (property parity vs legacy IRLS at the tol-bounded 1e-8 gate, always-raise byte-identity lock, natural guard-trip warning equivalence, convergence-iteration semantics) and TestRankDetectionStage0Certification (zero-QR cert spy, declined paths incl. the cond~1e6 cert-vs-QR boundary, Kahan characterization, rust solve_ols dispatch composition) in tests/test_linalg.py. REGISTRY notes for both fast paths; CHANGELOG + performance-plan tables; TODO rows for the RC pscore cache and rust solve_ols follow-ups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X4AzrFUMqJxcUumSH31mSr
PR Review ReportOverall Assessment✅ Looks good — I found no unmitigated P0/P1 issues in the PR diff. Executive Summary
MethodologyFinding: P3 informational — Documented solver fast pathsSeverity: P3 Code QualityNo P0/P1 findings. The Cholesky path in PerformanceFinding: P3 informational — Remaining performance work is trackedSeverity: P3 MaintainabilityNo findings. The new tests in Tech DebtNo untracked blocking tech debt found. New deferrable performance items are tracked in SecurityNo findings. The diff only changes numerical solver code, tests, and documentation; I saw no secrets or sensitive-data handling changes. Documentation/TestsFinding: P3 informational — Local tests not run in this review environmentSeverity: P3 |
Summary
diff_diff/linalg.py), hit by CallawaySantAnna ipw/dr covariate fits and every estimator routing through them (StaggeredTripleDifference, TripleDifference, ContinuousDiD, WooldridgeDiD logit; solve_ols pre-check, solve_poisson, dCDH residual-df).solve_logit's per-iteration tall-matrixnp.linalg.lstsq(gelsd SVD) becomes equilibrated normal equations + Cholesky under an explicit LAPACKdpoconreciprocal-condition guard (1e-6), with a per-iteration fallback that reproduces the legacy solve byte-for-byte when the normal matrix cannot be certified well-conditioned (working weights can crush a dummy column's effective scale on near-separated subgroups, so pre-loop full rank does not imply a chol-safe normal matrix;cho_factoralone can succeed with garbage at cond ~1e10-1e16). Columns are equilibrated once; IRLS state, tol, and warnings stay in the raw basis. The prior removal of an un-equilibratedcho_solve(X'X)OR fast path is cited in-code, and this path differs on exactly that axis._detect_rank_deficiencyshort-circuits the common full-rank case with a Gram/eigvalsh certification at the documented_rank_guarded_inv1e-10 Gram threshold - two orders stricter than the pivoted-QR 1e-7 boundary, so it never reports full rank where the QR path would report a deficiency. All n < k, non-finite, looser-rcond, and uncertifiable designs fall through to the existing two-stage QR verbatim: every drop decision, pivot selection, and rank count is unchanged (including dCDH's residual-df and the Rust solve_ols routing boolean, proven by arm instrumentation).Methodology references (required if estimator / math changes)
glm(family=binomial)IRLS convention and Rqr()/lm()rank handling perdocs/methodology/REGISTRY.md(IRLS inner-solver Note and rank-detection scale-invariance Notes extended in this PR)Validation
tests/test_linalg.py- newTestIRLSCholeskyFastPath(property parity vs a legacy-IRLS reimplementation at the tol-bounded 1e-8 gate over 20 varied datasets incl. zero/tiny weights and separation pressure; monkeypatched always-raise fallback byte-identity lock; natural dpocon-trip warning-set equivalence +diagnostics_out["irls_chol_fallback_iters"]counter; convergence-iteration semantics lock) andTestRankDetectionStage0Certification(zero-QR certification spy incl. a 1e8-scale column; declined paths - collinear, n<k structural, zero column, NaN still raises, looser rcond, and a between-columns cond~1e6 boundary locking cert-strictly-stricter-than-QR; Kahan characterization asserting certification can only disagree with QR by being more correct; Rust solve_ols dispatch composition lock). All existing rank-selection, logit, golden ipw/dr (R-parity SE abs<1e-6), survey self-consistency (atol=1e-10), and dCDH bit-identity baselines pass UNMODIFIED on both backends (1714 rust / 1594 python targeted sweep; full suite 8567 passed with only the known pre-existing order-dependent dCDH failure, which passes in isolation).docs/performance-plan.md(BEFORE = pristine main via baseline worktree, per-backend arm-pairs, BLAS threads pinned, instrumented fallback/certification/dispatch counters).Security / privacy
Generated with Claude Code