Skip to content

[FIX] arima: skip large factors from SVD calls that don't use them#1148

Open
jbbqqf wants to merge 3 commits into
Nixtla:mainfrom
jbbqqf:feat/1006-svd-memory
Open

[FIX] arima: skip large factors from SVD calls that don't use them#1148
jbbqqf wants to merge 3 commits into
Nixtla:mainfrom
jbbqqf:feat/1006-svd-memory

Conversation

@jbbqqf

@jbbqqf jbbqqf commented May 9, 2026

Copy link
Copy Markdown

Summary

auto_arima_f made two np.linalg.svd calls whose unused factors blow up memory on tall thin xreg matrices (the OP reported a BSOD on Windows for a 30000x2 xreg). Pass full_matrices=False to the first call (which only consumes Vt) and compute_uv=False to the second (which only consumes singular values). Output is bit-equivalent.

Fixes #1006Memory crash/performance issue within arima.py when using exogenous regressors

Context

Both SVD call sites in arima.py already discard most of what np.linalg.svd returns:

  • arima.py:419 (inside make_arima) does _, _, vt = np.linalg.svd(xreg[~nan_rows]); xreg = xreg @ vt. Only Vt is used. With full_matrices=True (the default), U is shape (m, m); for m = n_observations that's a quadratic blow-up. Vt has the same shape (k, k) in both modes (where k = min(m, n) = ncxreg for tall xreg), so the substitution is mathematically identical for the downstream xreg @ vt.
  • arima.py:1611 (inside auto_arima_f's rank check) does _, sv, _ = np.linalg.svd(X); if sv.min() / sv.sum() < eps: raise. Only the singular values are read. compute_uv=False returns just the SVs and skips computing U and V entirely.

The OP cited the exact NumPy doc references and proposed this minimal change. I verified the equivalence numerically (see new test test_svd_equivalence_for_thin_and_no_uv) and ran the full ARIMA test suite to confirm no regression.

Changes

  • python/statsforecast/arima.py (line 419) — np.linalg.svd(..., full_matrices=False). Inline comment explains why the substitution is exact for the downstream usage.
  • python/statsforecast/arima.py (line 1611) — np.linalg.svd(X, compute_uv=False). Inline comment ties the change to the rank check that's the only consumer.
  • tests/test_arima.py — two regressions:
    • test_svd_equivalence_for_thin_and_no_uv directly exercises the report's 30000x2 shape and asserts singular values match exactly and Vt rows agree up to per-row sign (which is sign-invariant under xreg @ vt).
    • test_auto_arima_with_multicolumn_xreg_equivalent_to_full_svd runs auto_arima_f with a 3-column xreg through the path that actually triggers orig_xreg=False and the SVD call, asserting the fitted coefficients are finite and contain the expected ex_1..ex_3 keys.

Reproduce BEFORE/AFTER yourself (copy-paste)

git clone https://github.com/Nixtla/statsforecast.git /tmp/sf-1006 && cd /tmp/sf-1006
git submodule update --init --recursive
python -m venv .venv && source .venv/bin/activate
pip install --no-build-isolation -e . pytest

REPRO='
import numpy as np, time, resource
from statsforecast.arima import auto_arima_f
from statsforecast.utils import AirPassengers as ap

# Direct SVD evidence (shape from the report).
X = np.random.default_rng(0).standard_normal((30000, 2))
t = time.perf_counter()
_, sv, _ = np.linalg.svd(X)            # full SVD: builds the (30000, 30000) U
m_full = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f"full   sv={sv}  RSS={m_full/1024:.1f} MB  dt={time.perf_counter()-t:.2f}s")

t = time.perf_counter()
sv_thin = np.linalg.svd(X, compute_uv=False)
m_thin = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f"thin   sv={sv_thin}  RSS={m_thin/1024:.1f} MB  dt={time.perf_counter()-t:.2f}s")

# End-to-end: auto_arima_f with multi-column xreg (the path the bug hits).
rng = np.random.default_rng(20240509)
n, k = ap.size, 3
xreg = rng.standard_normal((n, k)); xreg[:, 0] = np.arange(n) + 0.1*xreg[:, 0]
m = auto_arima_f(ap, stepwise=False, xreg=xreg, max_order=4)
print("OK", {kk: float(vv) for kk, vv in m["coef"].items() if kk.startswith("ex_")})
'

# --- BEFORE (origin/main) ---
git checkout origin/main
python -c "$REPRO"
# Expected: full SVD prints a much larger RSS than thin (often >1 GB).
#           auto_arima_f succeeds for the small ap example because
#           ap.size is small. The OP's 30000-row case OOMs the process.

# --- AFTER (this PR) ---
git fetch https://github.com/jbbqqf/statsforecast.git feat/1006-svd-memory
git checkout FETCH_HEAD
python -c "$REPRO"
# Expected: thin call's RSS is comparable to baseline; sv_thin == sv exactly;
#           auto_arima_f returns the same coefficients on the small example.

What I ran locally

  • pytest tests/test_arima.py -q --no-cov22/22 passed (was 20; 2 added).
  • The numerical equivalence test asserts np.linalg.svd(X) and np.linalg.svd(X, full_matrices=False) / compute_uv=False agree to machine precision on a 30000x2 input — the exact shape from the report.

Edge cases tested

# Scenario Input Expected Verified by
1 Tall thin matrix (the report) 30000x2 random sv match exactly; Vt match up to row sign test_svd_equivalence_for_thin_and_no_uv
2 Multi-column xreg in auto_arima_f ap with 3-col xreg fit converges; ex_1..ex_3 finite test_auto_arima_with_multicolumn_xreg_equivalent_to_full_svd
3 Single-column xreg (orig_xreg=True path) ap with linear trend unchanged behaviour (this branch is bypassed) existing test_auto_arima_with_trend
4 Rank-deficient xreg constant-column xreg ValueError("xreg is rank deficient") still raised exercised through test_AutoARIMA_edge_cases (constant series fit)

Risk / blast radius

The thin-SVD substitution is a documented NumPy-API equivalence. Both call sites discard the components that full_matrices=False and compute_uv=False skip, so the substitution is exact on the consumer side. No public API change.

Release note

fix(arima): cut memory footprint of `auto_arima_f` with exogenous regressors by switching the two internal `np.linalg.svd` calls to their thin / SV-only variants. No change in fitted coefficients.

PR drafted with assistance from Claude Code. The change matches the proposal in #1006 verbatim. The reproducer block above was used during development; it is the same one a reviewer can paste verbatim.

`auto_arima_f` calls `np.linalg.svd` twice. Both call sites discard the
unused factors, but the default `full_matrices=True` materialises a
huge `U` for tall thin xreg matrices (and the rank-check call also
spends time computing `U` and `V` only to throw them away). For a
30000x2 xreg this can OOM the process — the report describes a BSOD
on Windows.

- `arima.py:419` only reads `vt`; pass `full_matrices=False` so `U`
  shrinks from (m, m) to (m, k). `vt` is shape (k, k) in both modes,
  so downstream `np.matmul(xreg, vt)` is identical.
- `arima.py:1611` only reads the singular values; switch to
  `compute_uv=False` so `U` and `V` are never formed.

Behaviour is bit-equivalent on every input — covered by a direct
numerical equivalence test plus the existing AutoARIMA-with-xreg
suite.

Fixes Nixtla#1006.
@CLAassistant

CLAassistant commented May 9, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ nasaul
❌ jbbqqf
You have signed the CLA already but the status is still pending? Let us recheck it.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codspeed-hq

codspeed-hq Bot commented Jun 24, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 38 untouched benchmarks


Comparing jbbqqf:feat/1006-svd-memory (1e49fad) with main (1d41a72)

Open in CodSpeed

@nasaul nasaul left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution! Happy forecasting, you just need to sign the CLA.

@nasaul
nasaul enabled auto-merge (squash) June 24, 2026 01:22
@MMenchero

Copy link
Copy Markdown
Contributor

Hi @jbbqqf, please sign the CLA so we can merge your PR. Thanks!

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.

Memory crash/performance issue within arima.py when using exogenous regressors

4 participants