The boosting loop got faster without changing what it computes. Measured
end-to-end on real preprocessed scRNA-seq (20,000 cells, `latent_dim=10`):
**1.86x at p=2,000, 2.20x at p=3,000, 1.80x at p=8,000**, and 1.46x on a
batch-integrated fit with 20 covariate dummies.
**This is not a methodological improvement.** Across eighteen configurations —
plain, all three `batch_integration_mode` values, flat and per-dimension
`mandatory_genes`, `balance_obs`, `split_softmax`, both disentanglement methods,
`standardize_targets`, frozen/anchored/zero-dimension transfers and both
stability modes — the selected gene set is *identical* and the reconstruction
loss agrees to the sixth decimal. Nothing here recovers a marker that was
previously missed. What it buys is more iterations and more stability runs for
the same budget, which is what a method whose encoder support keeps drifting
actually needs.
Stability selection benefits too, and for the default `n_runs=300` that is the
larger absolute saving: **1.79–1.88x** for iteration mode and **1.31–1.39x** for
subsample mode at p=2,000–3,000, with peak memory during a subsample run falling
from 360 MB to 5 MB at p=3,000 — that figure is exactly the float64 copy of the
expression matrix described below. Subsample mode gains less by construction:
every run draws a different subset of cells, so its column norms genuinely change
and cannot be hoisted, and it uses the lazy column cache rather than the full
matrix.
Precomputing the covariance matrix *inside* subsample mode was measured and
rejected. It runs 3.07x faster at p=2,000 and 2.69x at p=3,000, but 0.73–0.85x
**slower** from p=6,000 upwards: the number of distinct columns a subsample
selects stays roughly flat as p grows, so the full p x p product stops paying for
itself. A memory guard cannot separate those cases — the 800 MB matrix at
p=10,000 fits comfortably and still loses — so that path stays lazy at every p.
`boosting_precompute_covcache` now defaults to `"auto"` and applies to every
boosting path, including iteration-mode stability selection, which previously
ignored the setting entirely. `"auto"` builds the full p x p covariance matrix
whenever it fits a conservative share of system memory; `True` and `False` are
still honoured exactly, and the resolved decision is recorded in
`adata.uns["bae"]["boosting_precompute_covcache"]`.
The flag was documented as a memory-versus-recomputation trade, which undersold
it. Building all `p` columns at once is one compute-bound matrix product near
hardware peak; fetching them one at a time is a sequence of memory-bound
matrix-vector products. Precomputing wins from roughly `p/59` distinct selected
features onwards — a threshold a fit passes in its first iteration.
Three quantities that never change were being recomputed. `col_norms_sq` was
rebuilt on every training iteration although the design matrix is fixed for the
whole fit, allocating a full-size temporary each time; the mandatory-covariate
block was rebuilt on every *boosting step* although it depends only on the
target; and the covariance cache's NaN check re-scanned columns that had already
been verified. The starting residual correlations are now formed for all latent
dimensions in one matrix product rather than one per dimension.
A float64 target passed against float32 predictors used to make NumPy promote
the *design matrix*, materializing a full float64 copy — measured 2.1x slower,
and it silently produced a different answer. The target is now aligned to the
predictors instead.
The three entry points to the same boosting problem ran at two different
precisions: `fit` in float32, both stability paths in float64 on top of a
float32 copy they had already made. They now agree on float32, which removes two
full-size copies of the expression matrix. The precision this gives up was
measured at about one gene in 380 — far inside the run-to-run support variation
this method documents for itself.
Results are **not bit-identical to 0.1.2**. Coefficients move by ~1e-6 relative
and the selected support does not change; a fixed seed no longer reproduces
0.1.2 output exactly.