perf(linalg): slim solve_ols marshalling - 15.3->7.8 GB rust-side footprint, -21% firm-panel fits, outputs preserved#607
Merged
Conversation
….8 GB rust-side high-water, CV-clear speed wins, outputs preserved Closes the solver-memory investigation with the census fix, an allocation-level measurement instrument, and the corrected diagnosis on record: - rust/src/linalg.rs::solve_ols: column norms computed from the borrowed numpy view (bit-identical scan order), equilibration fused into the SINGLE owned faer copy (replaces defensive copy + scaled clone + conversion copy), U'y computed directly off the faer SVD factor via deterministic column-order accumulation (the old path materialized a full ndarray copy of U for one k-vector dot), and the factorization + input dropped before the fitted/residual/vcov stage so thin-SVD U never coexists with the cluster-vcov scores block. Shared ndarray_to_faer helper untouched (trop.rs still uses it). - diff_diff/linalg.py: _detect_rank_deficiency uses qr(mode="r") - same dgeqp3 R and pivot as mode="economic" (locked by a cross-mode equality test), skipping the discarded-Q formation (dorgqr) that cost ~20% of county-class fits; sibling in _rank_guarded_inv. _equilibrated_lstsq materializes its scaled temporary F-ORDER and passes overwrite_a=True (scipy honors overwrite only for F-contiguous input - a C-order temp would be a silent no-op), letting gelsd consume the copy we already own; bit-equality locked by test. - rust/Cargo.toml + rust/src/alloc_profile.rs: feature-gated counting global allocator (--features alloc-profile, never in wheels) exposing reset_alloc_high_water / alloc_high_water_bytes for allocation-level before/after claims - resident-set metrics are unreliable for this on macOS (the compressor evicts cold pages; the historical "13.4 vs 21 GB backend gap" was a run-order artifact - interleaved A/B measured no systematic backend RSS difference). Measured (2.4M x 130 clustered solve, firm_churn-class): - rust-side allocator high-water 15.32 GB (6.14 n x k blocks) -> 7.81 GB (3.13 blocks); remaining floor = fused input copy + thin-SVD U transient + vcov scores, matching the code census exactly. - Frozen-code benchmark arms (3 repeats, identity-gated vs committed baselines): python-arm estimates BIT-IDENTICAL (deltas exactly 0.0 on every scenario); rust-arm at the usual 1e-13..1e-16. No scenario regresses; CV-clear improvements: firm rust 25.84 -> 20.34 s (-21%), county 1.76 -> 1.55 s rust / 2.39 -> 2.09 s python (skipped dorgqr), geo rust -6%, survey rust -2%. Committed after/rust baselines updated (identical estimates, fresh timings) + findings tables regenerated. Tests: cross-mode QR lock + F-order/overwrite bit-equality lock (test_linalg.py), underdetermined n<k raw-kernel parity (test_rust_backend.py), existing rust-vs-numpy parity suites unchanged. Sweeps: 350 rust-mode + 480 python-mode green; 45 cargo tests; clippy clean on touched files. TODO: solver-memory row resolved -> narrow Parked floor note; stale ndarray-to-faer row removed; REGISTRY solve_ols Note gains the explicit cross-backend tolerance sentence; full diagnosis in docs/performance-plan.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Overall Assessment✅ Looks good — no unmitigated P0/P1 findings. Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
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.
Summary
solve_olshot path on both backends - the memory census from the solver-phase diagnosis, implemented. Rust (rust/src/linalg.rs): norms from the borrowed view, equilibration fused into the single owned faer copy (replacing a defensive copy + scaled clone + conversion copy),U'ycomputed directly off the faer SVD factor via deterministic column-order accumulation (the old path materialized a full ndarray copy of U to compute one k-vector), and factors dropped before the fitted/residual/vcov stage so thin-SVD U never coexists with the cluster-vcov scores block. Python (diff_diff/linalg.py):_detect_rank_deficiencyusesqr(mode="r")- same dgeqp3 R/pivot, skips forming the discarded Q (sibling fixed in_rank_guarded_inv);_equilibrated_lstsqmaterializes its scaled temporary F-order and passesoverwrite_a=Trueso gelsd consumes the copy we already own (scipy silently ignoresoverwrite_afor C-order input - the F-order materialization is what makes it real).--features alloc-profile, never compiled into wheels) because resident-set metrics cannot verify this class of fix on macOS: the compressor evicts exactly the cold wasted copies, and an interleaved A/B showed the historical "13.4 vs 21 GB backend RSS gap" was a run-order artifact, not a backend property (full corrected diagnosis indocs/performance-plan.md).mode="r"/overwrite_achanges are bit-identical by construction - same LAPACK factorizations on the same values - and now locked by tests); the rust arm sits at the usual 1e-13..1e-16 cross-backend level. TODO's solver-memory row is resolved into a narrow Parked floor note (remaining residency is inherent to SVD-based lstsq; further reduction = the tall-skinny-QR change parked with the QR-reuse row), and the stale ndarray-to-faer conversion row is removed (its cited site no longer exists).Methodology references (required if estimator / math changes)
mode="r"returns the same dgeqp3 R and pivot); python solve outputs are bit-identical; rust outputs move only at the documented cross-backend parity tolerances.docs/methodology/REGISTRY.mdsolve_ols scale-invariance Note (extended with an explicit sentence: the Python gelsd and Rust faer thin-SVD backends implement the same contract and agree at parity-suite tolerances, never bit-identically).Validation
tests/test_linalg.py- cross-mode QR lock (mode="r" vs mode="economic": identical R diagonal + pivot on collinear and scale-artifact fixtures; rank/dropped hard-coded, pivots locked by same-session equality since dgeqp3 tie-breaks are BLAS-dependent) and an F-order/overwrite_abit-equality lock (like-for-like reference, caller's X proven unmutated).tests/test_rust_backend.py- underdetermined n < k parity through the slimmed raw-kernel path (residual/exact-fit contract; the publicsolve_olsrejects n < k). Existing rust-vs-numpy parity suites and cargo tests pass unchanged (350 rust-mode + 480 python-mode sweep, 45 cargo tests).docs/performance-plan.md.Security / privacy
Generated with Claude Code