Skip to content

perf: BLAS projection, per-width caching, chunked fits, and accuracy fixes for ExtremeLearningMachine#48

Merged
CyrilJl merged 2 commits into
mainfrom
perf/elm-blas-projection
Jul 4, 2026
Merged

perf: BLAS projection, per-width caching, chunked fits, and accuracy fixes for ExtremeLearningMachine#48
CyrilJl merged 2 commits into
mainfrom
perf/elm-blas-projection

Conversation

@CyrilJl

@CyrilJl CyrilJl commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Rewrites ExtremeLearningMachine for speed and memory, fixes a correctness bug that corrupted its output when used as the imputer regressor, and fixes a second, separate accuracy issue that made it systematically underperform (and sometimes actively hurt) compared to FastRidge in typical imputation workloads:

  • BLAS projection: the naive numba triple-loop matmul kernel is replaced with np.matmul + in-place bias/ReLU on float32 (numba dependency removed from elm.py).
  • Per-width projection cache (bug fix): fit previously initialized the projection matrix once and reused it forever. Inside MultivariateImputer, the same instance is refit on column subsets of varying widths (one per missingness pattern); refits with more input features read past the projection matrix (numba, no bounds check) and produced non-finite imputations, while narrower refits silently used a truncated projection. Projections are now sampled lazily per input width and cached, seeded identically — reproducible across instances, and identical values to before for the first width. set_params invalidates the cache when n_features/random_state change.
  • Chunked large fits/predicts: above 65,536 rows, fit accumulates the augmented Gram matrix [H, y, 1] chunk by chunk and solves via the existing fit_ridge_from_gram; predict projects into a reusable chunk buffer. The full (n_samples, n_features) hidden matrix is never materialized.
  • Fan-in scaling + sample-aware hidden width (accuracy fix): MultivariateImputer refits a fresh model per missingness pattern on a small, pattern-specific row subset (often tens of rows). Two compounding issues made ExtremeLearningMachine a poor fit for that regime: (1) projection weights/bias were drawn ~N(0,1) with no fan-in scaling, so pre-activation variance — and therefore the effective strength of alpha — grew with the number of predictor columns per pattern; (2) the hidden width (n_features, default 100) was fixed regardless of how many samples a given pattern's fit had, turning the internal ridge fit into a severely underdetermined (parameters > samples) problem. Weights/bias are now scaled by 1/sqrt(n_input_features), and a new min_samples_per_feature parameter (default 5) caps the hidden width to n_samples // min_samples_per_feature at fit time (set to 0 to opt out and keep the old fixed-width behavior).

Measurements

Same machine, warm runs (local perf/elm_benchmark.py):

Scenario Before After
Standalone fit, 20k rows × 35 → 100 components 8.7 ms 3.3 ms
Standalone predict, 5k rows 1.9 ms 0.7 ms
MultivariateImputer(regressor=ELM), 10k × 60 panel, 3 targets 0.67 s, non-finite output 0.46 s, finite output
Fit + predict, 500k rows × 35 0.32 s / 204 MB peak 0.18 s / 29 MB peak

Accuracy: ExtremeLearningMachine vs FastRidge as the imputer regressor

Benchmarked via MultivariateImputer(regressor=...) across 10 seeds, 10% MAR missingness (mean r² on numeric targets):

Dataset FastRidge ELM before accuracy fix ELM after accuracy fix
Diabetes 0.541 0.295 0.490
Titanic (mixed types) 0.447 -0.092 0.532
SyntheticMixed 0.748 0.725 0.737
Nonlinear-relationship synthetic* 0.618 0.918 0.918

*constructed so every column is a nonlinear transform of shared latent factors — the one case ELM should structurally beat a linear model, kept to confirm the fix doesn't just regress ELM toward FastRidge everywhere.

Root cause on Titanic: per-pattern fits use a median of 38 rows against a 100-unit hidden layer (parameters > samples by 2.6x), which is what produced the negative r² before the fix. On typical tabular/time-series data FastRidge remains the stronger default; ExtremeLearningMachine is best reserved for imputation contexts with known nonlinear structure and enough samples per pattern.

Tests

  • Two regression tests from the original perf work: varying-width refits stay finite and match a fresh instance; chunked fit matches single-shot at float32 rounding level.
  • Five new tests for the accuracy fix: hidden-width capping by sample count, disabling the cap via min_samples_per_feature=0, predict/fit width consistency after capping, and fan-in variance stability across input widths.
  • Full suite passes (46 tests).

Docs: docs/api.rst autodocs ExtremeLearningMachine via :members:, so the updated docstring (documenting fan-in scaling and min_samples_per_feature) is already reflected on the API page — verified with a clean local sphinx-build (no warnings).

Note: imputation results with an ELM regressor will differ from previous releases whenever pattern widths varied — the old outputs were garbage (out-of-bounds reads) or badly overfit (fixed hidden width vs. pattern sample size), so no parity is intended.

🤖 Generated with Claude Code

CyrilJl and others added 2 commits July 4, 2026 00:50
Replace the naive numba matmul kernel in ExtremeLearningMachine with a
BLAS-backed float32 projection, sample and cache the random projection
per input width, and solve large fits (>65536 rows) from a chunked
augmented Gram matrix so the hidden matrix is never materialized.

The per-width cache also fixes an out-of-bounds read: one instance
refit with more input features than its first fit (which happens for
every missingness pattern inside the imputers) read past the projection
matrix and produced non-finite predictions.

Measured: standalone fit 8.7->3.3 ms (20k rows, 35->100 features),
imputer run with ELM regressor 0.67->0.46 s, 500k-row fit+predict
0.32 s / 204 MB -> 0.18 s / 29 MB peak.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

MultivariateImputer refits a fresh model per missingness pattern on a
small, pattern-specific row subset (often tens of rows). A fixed hidden
width regardless of that subset size turned ExtremeLearningMachine's
internal ridge fit into a severely underdetermined problem, causing high
variance and frequent underperformance versus FastRidge. Scale the random
projection by 1/sqrt(n_input_features) so alpha's effective strength no
longer drifts with the number of predictor columns per pattern, and cap
the hidden width to n_samples // min_samples_per_feature so it never
outgrows the data it's fit on.
@CyrilJl CyrilJl changed the title perf: BLAS projection, per-width caching, and chunked fits for ExtremeLearningMachine perf: BLAS projection, per-width caching, chunked fits, and accuracy fixes for ExtremeLearningMachine Jul 4, 2026
@CyrilJl CyrilJl merged commit 40833c3 into main Jul 4, 2026
3 checks passed
@CyrilJl CyrilJl deleted the perf/elm-blas-projection branch July 4, 2026 22:06
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