Skip to content

Add LSMR solver to Ridge - #7922

Merged
rapids-bot[bot] merged 7 commits into
NVIDIA:mainfrom
jcrist:ridge-sparse
Mar 24, 2026
Merged

Add LSMR solver to Ridge#7922
rapids-bot[bot] merged 7 commits into
NVIDIA:mainfrom
jcrist:ridge-sparse

Conversation

@jcrist

@jcrist jcrist commented Mar 20, 2026

Copy link
Copy Markdown
Member

This adds a new solver="lsmr" to Ridge, based on cupyx.scipy.sparse.linalg.lsmr. This solver supports both sparse and dense inputs, multi-target regressions, and is typically much faster than either existing solver.

For now we keep the solver="auto" default the same as before, with the exception that for sparse inputs we now default to 'lsmr' instead of erroring.

For cuml-accel, we map solver="lsqr" to solver="lsmr", since they're closely related.

Fixes #7911.

@jcrist jcrist self-assigned this Mar 20, 2026
@jcrist
jcrist requested a review from a team as a code owner March 20, 2026 22:10
@jcrist jcrist added feature request New feature or request non-breaking Non-breaking change labels Mar 20, 2026
@jcrist
jcrist requested a review from viclafargue March 20, 2026 22:10
@github-actions github-actions Bot added the Cython / Python Cython or Python issue label Mar 20, 2026
@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2730a506-74d2-415f-bae3-308f77c9144e

📥 Commits

Reviewing files that changed from the base of the PR and between cfd5d55 and d42d4a7.

📒 Files selected for processing (1)
  • python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Ridge regression: native sparse-input support, automatic sparse-aware solver selection, new LSMR solver option, tunable tol/max_iter, and public n_iter_ (may be None for some solvers).
  • Bug Fixes

    • Documentation updated to reflect CPU fallback behavior for sparse inputs; runtime validation now rejects unsupported sparse+solver combinations.
  • Tests

    • Expanded sparse/solver coverage, added sparse-input tests, adjusted tolerances/xfails, and narrowed exception-test scope.

Walkthrough

Adds sparse-input support and an LSMR solver to Ridge, introduces a center_and_scale utility, exposes tol/max_iter and n_iter_, updates sklearn-compat GPU validation for sparse/solver combos, adjusts prediction indexing for converted inputs, and updates tests and docs accordingly.

Changes

Cohort / File(s) Summary
Documentation
docs/source/cuml-accel/limitations.rst
Removed the documented CPU-fallback condition for sparse X in Ridge; remaining fallback cases only mention positive=True or solver="lbfgs".
sklearn-compat overrides
python/cuml/cuml/accel/_overrides/sklearn/linear_model.py
Removed n_iter_ from Ridge _not_implemented_attributes; added runtime GPU validation that raises UnsupportedOnGPU when X is sparse and solver is unsupported for sparse inputs; copyright year update.
Linear model utilities & mixins
python/cuml/cuml/linear_model/base.py
Added center_and_scale(...) handling dense/sparse and sample weights; updated LinearPredictMixin.predict and LinearClassifierMixin.decision_function to handle sparse inputs via SparseCumlArray, compute with cupy-backed arrays, and return CumlArray indexed by X_m.index.
Ridge implementation
python/cuml/cuml/linear_model/ridge.pyx
Added sparse-input support, new lsmr solver and mapping to scikit "lsqr", solver selection defaulting to lsmr for sparse X, added tol/max_iter params, unified fit internals (_fit_cupy, _solve_lsmr, _solve_svd), and set/export n_iter_ and solver_.
Tests: Ridge & compatibility
python/cuml/tests/test_ridge.py, python/cuml/tests/test_sklearn_compatibility.py
Added "lsmr" coverage, added sparse-input tests (test_ridge_sparse), adjusted tolerances, updated solver auto-selection expectations, and added xfail for n_iter_ check.
Tests: exceptions
python/cuml/tests/test_exceptions.py
Removed Ridge and other estimators from the sparse-unsupported TypeError test and simplified the test flow to exercise remaining unsupported estimators via fit_transform/fit.
Upstream test config
python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml
Added a flaky upstream test entry and set strict: false for Ridge solver-consistency xfail block under scikit-learn>=1.8.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

improvement, cuml-accel, sklearn-api-compat

Suggested reviewers

  • dantegd
  • csadorf
  • gforsyth
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a new LSMR solver option to the Ridge estimator.
Description check ✅ Passed The description is related to the changeset, explaining the new LSMR solver, its benefits, default behavior changes, and how it addresses the linked issue.
Linked Issues check ✅ Passed The PR fully implements sparse matrix support for Ridge [#7911] by adding the LSMR solver that handles sparse inputs, enabling Ridge to operate on sparse matrices without error.
Out of Scope Changes check ✅ Passed Changes remain focused on Ridge sparse support [#7911]: LSMR solver implementation, sparse input handling, test updates, and related documentation/configuration changes are all within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/cuml/cuml/linear_model/ridge.pyx`:
- Around line 558-562: The ValueError message in the conditional that checks
X_is_sparse and solver != "lsmr" in ridge.pyx is missing the f-string prefix so
"{solver!r}" won't be interpolated; update the string literal in that raise
inside the if block to use an f-string (prefix with f) so solver is formatted
into the message (refer to the conditional using X_is_sparse and the variable
solver to find the exact spot).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7278859d-49d5-48ac-af07-c64ecad6fd13

📥 Commits

Reviewing files that changed from the base of the PR and between 095b50e and db0f8b6.

📒 Files selected for processing (5)
  • docs/source/cuml-accel/limitations.rst
  • python/cuml/cuml/accel/_overrides/sklearn/linear_model.py
  • python/cuml/cuml/linear_model/base.py
  • python/cuml/cuml/linear_model/ridge.pyx
  • python/cuml/tests/test_ridge.py
💤 Files with no reviewable changes (1)
  • docs/source/cuml-accel/limitations.rst

Comment thread python/cuml/cuml/linear_model/ridge.pyx
@jcrist

jcrist commented Mar 20, 2026

Copy link
Copy Markdown
Member Author

A quick benchmark on dense inputs:

from time import perf_counter

import cuml
from cuml.datasets import make_regression


for n_samples in [5_000, 10_000, 20_000]:
    for n_features in [1_000, 5_000, 10_000]:
        X, y = make_regression(
            n_samples, n_features, random_state=42, dtype="float"
        )
        print(f"shape: {X.shape}")
        for solver in ["eig", "svd", "lsmr"]:
            start = perf_counter()
            cuml.Ridge(solver=solver, tol=1e-6).fit(X, y)
            duration = perf_counter() - start
            print(f"- {solver}: {duration:.3f} s")

Output

shape: (5000, 1000)                                                                           
- eig: 0.071 s                                                                                
- svd: 0.192 s                                                                                
- lsmr: 0.056 s                                                                               
shape: (5000, 5000)                                                                           
- eig: 0.485 s                                                                                
- svd: 3.636 s                                                                                
- lsmr: 0.855 s                                                                               
shape: (5000, 10000)                                                                          
- eig: 2.291 s                                                                                
- svd: 4.559 s                                                                                
- lsmr: 0.113 s                                                                               
shape: (10000, 1000)                                                                          
- eig: 0.055 s                                                                                
- svd: 0.155 s                                                                                
- lsmr: 0.035 s                                                                               
shape: (10000, 5000)                                                                          
- eig: 0.475 s                                                                                
- svd: 4.489 s                                                                                
- lsmr: 0.113 s                                                                               
shape: (10000, 10000)                                                                         
- eig: 2.702 s                                                                                
- svd: 25.029 s                                                                               
- lsmr: 1.470 s                                                                               
shape: (20000, 1000)                                                                          
- eig: 0.041 s                                                                                
- svd: 0.180 s                                                                                
- lsmr: 0.031 s                                                                               
shape: (20000, 5000)                                                                          
- eig: 0.604 s                                                                                
- svd: 4.791 s                                                                                
- lsmr: 0.085 s                                                                               
shape: (20000, 10000)                                                                         
- eig: 3.164 s                                                                                
- svd: 31.893 s                                                                               
- lsmr: 0.212 s

LSMR is typically the fastest across dimensionality. For small n_features eig may be close, but as n_features increases LSMR takes the lead.

I'm not sure if we'd want to make solver="auto" default to always using solver='lsmr', but it seems likely that we might want to always prefer it to solver="svd".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
python/cuml/tests/test_ridge.py (2)

140-141: Consider clarifying n_targets=0 semantics.

Using n_targets=0 to represent the 1D-y (single-target) case is functional but could be confusing to readers. A brief comment explaining this convention would improve clarity.

 `@pytest.mark.parametrize`("n_targets", [0, 1, 3])
 def test_ridge_sparse(dtype, fit_intercept, weighted, n_targets):
+    # n_targets=0 tests 1D y (single target), n_targets=1 tests 2D y with 1 column
     n_samples, n_features = 3000, 500
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/tests/test_ridge.py` around lines 140 - 141, The test
parameterization uses n_targets with a special convention where n_targets=0
denotes the 1D-y (single-target) case; add a brief clarifying comment above the
pytest.mark.parametrize or at the start of test_ridge_sparse explaining that
n_targets==0 is used to represent a single-target (1D) response to avoid
confusion, and reference the parameter name n_targets and test function
test_ridge_sparse so readers understand the mapping and why 0 is used.

159-166: Consider matching tol between cuML and sklearn for fair comparison.

cuml.Ridge is initialized with tol=1e-6, but sklearn.linear_model.Ridge uses its default tol=1e-4. While the atol=1e-2 assertion tolerance is permissive enough to pass, matching tolerances would make the comparison more meaningful.

     cu_model = cuml.Ridge(tol=1e-6, fit_intercept=fit_intercept)
     ...
-    sk_model = sklearn.linear_model.Ridge(
-        solver="lsqr", fit_intercept=fit_intercept
-    )
+    sk_model = sklearn.linear_model.Ridge(
+        solver="lsqr", fit_intercept=fit_intercept, tol=1e-6
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/tests/test_ridge.py` around lines 159 - 166, The test uses
different tolerances between cuml.Ridge (cu_model with tol=1e-6) and
sklearn.linear_model.Ridge (sk_model using default tol=1e-4); update the sklearn
model initialization to pass tol=1e-6 so sk_model and cu_model use the same
convergence tolerance for a fair comparison (i.e., set tol in the
sklearn.linear_model.Ridge constructor to match the tol used when creating
cu_model).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@python/cuml/tests/test_ridge.py`:
- Around line 140-141: The test parameterization uses n_targets with a special
convention where n_targets=0 denotes the 1D-y (single-target) case; add a brief
clarifying comment above the pytest.mark.parametrize or at the start of
test_ridge_sparse explaining that n_targets==0 is used to represent a
single-target (1D) response to avoid confusion, and reference the parameter name
n_targets and test function test_ridge_sparse so readers understand the mapping
and why 0 is used.
- Around line 159-166: The test uses different tolerances between cuml.Ridge
(cu_model with tol=1e-6) and sklearn.linear_model.Ridge (sk_model using default
tol=1e-4); update the sklearn model initialization to pass tol=1e-6 so sk_model
and cu_model use the same convergence tolerance for a fair comparison (i.e., set
tol in the sklearn.linear_model.Ridge constructor to match the tol used when
creating cu_model).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 26d709a0-3115-4013-8134-8a756ec12f07

📥 Commits

Reviewing files that changed from the base of the PR and between db0f8b6 and e4329db.

📒 Files selected for processing (2)
  • python/cuml/cuml/linear_model/ridge.pyx
  • python/cuml/tests/test_ridge.py

@viclafargue viclafargue 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! LGTM

Comment thread python/cuml/cuml/linear_model/base.py Outdated
Comment thread python/cuml/cuml/linear_model/ridge.pyx
Comment thread python/cuml/cuml/linear_model/ridge.pyx Outdated
jcrist added 5 commits March 23, 2026 17:28
This function will be reused later on for other linear models, so taking
care of it first.
This adds a new LSMR solver to `Ridge`. The solver supports sparse and
dense inputs, multi-target regressions, and is typically much faster
than either existing solvers (eig or svd).

For now we kept `'auto'` defaulting the same as before, except in the
case of sparse inputs we now use `lsmr` instead of erroring.
@jcrist

jcrist commented Mar 24, 2026

Copy link
Copy Markdown
Member Author

/merge

@rapids-bot
rapids-bot Bot merged commit 5e954f7 into NVIDIA:main Mar 24, 2026
93 checks passed
@jcrist
jcrist deleted the ridge-sparse branch March 24, 2026 01:53
rapids-bot Bot pushed a commit that referenced this pull request Apr 1, 2026
This is a followup to #7922. It adds an LSMR solver to `LinearRegression`. The primary motivation here is adding sparse input support, but the LSMR solver is also much faster than our existing solvers for most inputs. For now I kept the default behavior the same (except we use LSMR for sparse inputs) - in the future we _might_ consider preferring LSMR over SVD.

To accomplish this, I split the cupy-based solvers out of `cuml/linear_models/ridge.pyx` into `cuml/linear_models/base.py` as a standalone `fit_least_squares` function. This feels a _bit_ weird to have it there, but it's nice to have pure-python functions in a `.py` file since we get much better linting/formatting there than we do in cython files. I'm happy with this location for now. Since `LinearRegression` is effectively a special-case of `Ridge` with `alpha=0.0`, sharing this functionality across the models makes sense.

Fixes #3105.

Authors:
  - Jim Crist-Harif (https://github.com/jcrist)

Approvers:
  - Victor Lafargue (https://github.com/viclafargue)

URL: #7927
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Cython / Python Cython or Python issue feature request New feature or request non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sparse matrix support for Ridge models

4 participants