Skip to content

Add LSMR solver to LinearRegression - #7927

Merged
rapids-bot[bot] merged 8 commits into
NVIDIA:mainfrom
jcrist:linear-regression-improvements
Apr 1, 2026
Merged

Add LSMR solver to LinearRegression#7927
rapids-bot[bot] merged 8 commits into
NVIDIA:mainfrom
jcrist:linear-regression-improvements

Conversation

@jcrist

@jcrist jcrist commented Mar 24, 2026

Copy link
Copy Markdown
Member

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.

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

coderabbitai Bot commented Mar 24, 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
📝 Walkthrough

Walkthrough

Removed CPU-fallback for sparse X in docs; added a shared GPU least-squares implementation (fit_least_squares) with SVD and LSMR paths; enabled sparse-input and LSMR support in LinearRegression and Ridge; refactored solver dispatch, attribute descriptors/shapes, and updated tests/xfail entries.

Changes

Cohort / File(s) Summary
Documentation
docs/source/cuml-accel/limitations.rst
Removed the bullet "If X is sparse" from the LinearRegression CPU-fallback conditions.
Linear model utilities
python/cuml/cuml/linear_model/base.py
Added fit_least_squares(...) (CuPy ElementwiseKernels, SVD and LSMR implementations, centering/scaling, per-target alpha handling, intercept computation).
LinearRegression implementation
python/cuml/cuml/linear_model/...linear_regression.pyx
Added SparseInputTagMixin, detect/wrap sparse X, refactored fit to dispatch to libcuml OLS or fit_least_squares (svd/lsmr); changed coef_/intercept_ descriptors/shapes and docstrings; removed prior _select_algo/_fit_multi_target SVD path.
Ridge implementation
python/cuml/cuml/linear_model/...ridge.pyx
Removed local CuPy SVD/LSMR ridge solvers; now delegates to fit_least_squares(...); updated post-fit wrapping/flattening of coef/intercept.
Tests — unit & behavior
python/cuml/tests/test_linear_model.py, python/cuml/tests/test_exceptions.py
Added test_linear_regression_sparse, expanded solver (lsmr) and test coverage, adjusted mutation/assume logic; removed LinearRegression from sparse-exception negative test.
Tests — sklearn-compat / xfail config
python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml, python/cuml/tests/test_sklearn_compatibility.py
Pruned specific LinearRegression xfail entries (check_estimators_empty_data_messages, check_fit2d_1sample, check_fit1d) in YAML and strict-xfail config.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Add LSMR solver to Ridge #7922: Adds LSMR-based least-squares support and related sparse-input/center-and-scale changes touching linear_model/base.py, Ridge, and LinearRegression.
  • CI Update xfail list #7768: Adjusts scikit-learn xfail mappings for LinearRegression (test/xfail changes overlapping with this PR).

Suggested labels

improvement, sklearn-api-compat

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 primary change: adding an LSMR solver to LinearRegression.
Description check ✅ Passed The description is directly related to the changeset, explaining the LSMR solver addition, sparse input support, and code refactoring.
Linked Issues check ✅ Passed The PR successfully addresses issue #3105 by implementing sparse input support for LinearRegression through the new LSMR solver.
Out of Scope Changes check ✅ Passed All changes are directly related to adding LSMR solver support and sparse input handling; no unrelated modifications detected.

✏️ 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: 2

🤖 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/base.py`:
- Around line 205-210: The kernel _ridge_transform_zero_alpha currently returns
x when abs(y) < 1e-10, which preserves components corresponding to near-zero
singular values; change its behavior to zero those components instead (set z = 0
when abs(y) < 1e-10) so it matches the pseudo-inverse semantics and is
consistent with the behavior in _ridge_transform; update the ElementwiseKernel
body in _ridge_transform_zero_alpha to assign z = 0 for the near-zero branch and
keep z = x / y otherwise, ensuring the same numeric threshold and symbol names
(_ridge_transform_zero_alpha, _ridge_transform) are used for clarity.

In `@python/cuml/cuml/linear_model/linear_regression.pyx`:
- Around line 366-368: The fallback message is incorrect: when checking
X_m.shape[0] == 1 (single row/sample) the code sets fallback_reason =
"single-column X"; change the string to accurately reflect a single row/sample
(e.g., "single-row X" or "single-sample X") so that the variables solver and
fallback_reason in linear_regression.pyx (around the X_m.shape[0] == 1 branch)
correctly describe the condition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dc082eb9-328e-4968-a380-3dca6814772c

📥 Commits

Reviewing files that changed from the base of the PR and between 5e954f7 and d6af1d0.

📒 Files selected for processing (8)
  • docs/source/cuml-accel/limitations.rst
  • python/cuml/cuml/linear_model/base.py
  • python/cuml/cuml/linear_model/linear_regression.pyx
  • python/cuml/cuml/linear_model/ridge.pyx
  • python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml
  • python/cuml/tests/test_exceptions.py
  • python/cuml/tests/test_linear_model.py
  • python/cuml/tests/test_sklearn_compatibility.py
💤 Files with no reviewable changes (3)
  • docs/source/cuml-accel/limitations.rst
  • python/cuml/tests/test_sklearn_compatibility.py
  • python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml

Comment thread python/cuml/cuml/linear_model/base.py
Comment thread python/cuml/cuml/linear_model/linear_regression.pyx Outdated
@jcrist
jcrist force-pushed the linear-regression-improvements branch 2 times, most recently from 59b1fda to f5d112f Compare March 24, 2026 21:02

@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 (1)
python/cuml/cuml/linear_model/linear_regression.pyx (1)

313-316: Consider validating sparse input dtype for consistency.

The sparse input path only checks dtype.kind != "f" to decide on conversion, which would allow float16 through unchanged. The dense path explicitly validates check_dtype=[np.float32, np.float64]. For consistency and to avoid potential downstream issues with unsupported dtypes, consider validating the sparse dtype similarly.

♻️ Suggested change
         if X_is_sparse := is_sparse(X):
+            if X.dtype not in (np.float32, np.float64):
+                convert_dtype = np.float32
+            else:
+                convert_dtype = None
             X_m = SparseCumlArray(
-                X, convert_to_dtype=np.float32 if X.dtype.kind != "f" else None
+                X, convert_to_dtype=convert_dtype
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/cuml/linear_model/linear_regression.pyx` around lines 313 - 316,
The sparse-input branch currently only checks dtype.kind != "f" and thus lets
float16 through; update the handling around is_sparse and SparseCumlArray
creation to validate sparse X dtype like the dense path (allowed check_dtype =
[np.float32, np.float64]) and convert any unsupported sparse dtypes (e.g.,
float16 or non-floats) to np.float32 before constructing X_m; ensure the same
validation logic/utility used for dense paths (or replicate the check) is
applied so X_m is always float32/float64 consistent with the rest of the
pipeline.
🤖 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/cuml/linear_model/linear_regression.pyx`:
- Around line 313-316: The sparse-input branch currently only checks dtype.kind
!= "f" and thus lets float16 through; update the handling around is_sparse and
SparseCumlArray creation to validate sparse X dtype like the dense path (allowed
check_dtype = [np.float32, np.float64]) and convert any unsupported sparse
dtypes (e.g., float16 or non-floats) to np.float32 before constructing X_m;
ensure the same validation logic/utility used for dense paths (or replicate the
check) is applied so X_m is always float32/float64 consistent with the rest of
the pipeline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1eb69be7-0de0-46bb-a4da-c4e55535f4cc

📥 Commits

Reviewing files that changed from the base of the PR and between d6af1d0 and 59b1fda.

📒 Files selected for processing (6)
  • docs/source/cuml-accel/limitations.rst
  • python/cuml/cuml/linear_model/linear_regression.pyx
  • python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml
  • python/cuml/tests/test_exceptions.py
  • python/cuml/tests/test_linear_model.py
  • python/cuml/tests/test_sklearn_compatibility.py
💤 Files with no reviewable changes (3)
  • docs/source/cuml-accel/limitations.rst
  • python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml
  • python/cuml/tests/test_sklearn_compatibility.py
✅ Files skipped from review due to trivial changes (1)
  • python/cuml/tests/test_exceptions.py

@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: 4

🤖 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/base.py`:
- Around line 273-324: Normalize and validate alpha before the solver branches:
convert any array-like alpha to a cupy array of shape (y.shape[1],) with dtype
X.dtype (use cp.asarray and reshape/broadcast as needed), reject negative values
(raise ValueError) and ensure scalar inputs are expanded to length n_targets;
then use this validated alpha for the SVD branch checks ((alpha == 0).all()) and
for computing damp = cp.sqrt(alpha) in the "lsmr" branch so sqrt gets only
non-negative cupy arrays. Reference symbols: alpha, X, y, solver,
_ridge_transform/_ridge_transform_zero_alpha, damp, and the "lsmr" branch loop.

In `@python/cuml/cuml/linear_model/linear_regression.pyx`:
- Around line 313-316: The sparse branch currently forces non-floating sparse
inputs to float32 and lets float16 bypass dense checks; update the X_is_sparse
handling so it respects the convert_dtype flag and uses the same dense dtype
contract as the dense path: when convert_dtype is False preserve X.dtype (i.e.,
pass convert_to_dtype=None into SparseCumlArray), and when convert_dtype is True
choose the promoted floating dtype using the same logic used for dense inputs
(promote to float32/float64 according to dtype.kind and the dense-path rules),
also ensure float16 is handled consistently with dense conversion; locate
X_is_sparse, SparseCumlArray and mirror the dense conversion decision (or call
the common conversion helper used for dense arrays) rather than unconditionally
passing np.float32.
- Around line 359-384: Before selecting a solver in the solver-determination
block, validate self.algorithm against the allowed values (e.g. "auto",
"libcuml", "lsmr", "svd") and raise a ValueError for any other string so typos
like algorithm="eg" error out early; add this check at the top of the method
containing the shown logic (the same scope that references self.algorithm and
_fit_libcuml()) so fallback paths (sparse X, single-column X, multi-column y) no
longer silently accept invalid algorithm values and preserve existing behavior
for valid values.
- Around line 329-333: The code currently raises for n_rows == 1 (checking
X_m.shape[0] < 2) which breaks compatibility with scikit-learn; instead remove
the hard raise and, when X_m.shape[0] == 1 or when the libcuml path cannot
handle a single sample, route execution to the file's existing SVD fallback path
used for unstable libcuml cases (i.e., replace the raise with a conditional that
invokes the SVD fallback routine for the single-row case or when libcuml signals
inability). Ensure you use the same inputs (X_m, n_rows, etc.) when calling the
fallback so behavior matches scikit-learn.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aca308e7-8fbb-4e78-bd0a-9d5cfcf97803

📥 Commits

Reviewing files that changed from the base of the PR and between 59b1fda and f5d112f.

📒 Files selected for processing (7)
  • docs/source/cuml-accel/limitations.rst
  • python/cuml/cuml/linear_model/base.py
  • python/cuml/cuml/linear_model/linear_regression.pyx
  • python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml
  • python/cuml/tests/test_exceptions.py
  • python/cuml/tests/test_linear_model.py
  • python/cuml/tests/test_sklearn_compatibility.py
💤 Files with no reviewable changes (3)
  • docs/source/cuml-accel/limitations.rst
  • python/cuml/tests/test_sklearn_compatibility.py
  • python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml
✅ Files skipped from review due to trivial changes (1)
  • python/cuml/tests/test_exceptions.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/cuml/tests/test_linear_model.py

Comment thread python/cuml/cuml/linear_model/base.py
Comment thread python/cuml/cuml/linear_model/linear_regression.pyx
Comment thread python/cuml/cuml/linear_model/linear_regression.pyx
Comment thread python/cuml/cuml/linear_model/linear_regression.pyx

@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.

♻️ Duplicate comments (2)
python/cuml/cuml/linear_model/linear_regression.pyx (1)

313-323: ⚠️ Potential issue | 🟠 Major

Sparse X still ignores convert_dtype=False.

Unlike the dense path in Lines 319-324, Line 315 always casts non-floating sparse inputs to float32, and float16 also slips past the dense branch’s float32/float64 gate. That makes sparse fits silently diverge from the dense contract and can feed unsupported dtypes into fit_least_squares.

🔧 Suggested fix
         if X_is_sparse := is_sparse(X):
+            allowed_dtypes = (np.dtype(np.float32), np.dtype(np.float64))
+            x_dtype = np.dtype(X.dtype)
             X_m = SparseCumlArray(
-                X, convert_to_dtype=np.float32 if X.dtype.kind != "f" else None
+                X,
+                convert_to_dtype=(
+                    np.float32
+                    if convert_dtype and x_dtype not in allowed_dtypes
+                    else None
+                ),
             )
+            if np.dtype(X_m.dtype) not in allowed_dtypes:
+                raise TypeError(
+                    "X must have dtype float32 or float64 when convert_dtype=False"
+                )
             X_is_copy = False

A small regression test for sparse int32 / float16 with both convert_dtype settings would help lock this down. As per coding guidelines, "Silent data corruption from type coercion, incorrect handling of cuDF vs pandas vs NumPy inputs, or missing validation causing crashes on invalid input must be addressed".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/cuml/linear_model/linear_regression.pyx` around lines 313 - 323,
The sparse branch currently always casts non-floating inputs to float32 via
SparseCumlArray(X, convert_to_dtype=np.float32 if X.dtype.kind != "f" else
None), which ignores the convert_dtype flag and allows float16 through; change
the SparseCumlArray invocation to mirror the dense path by using
convert_to_dtype=(np.float32 if convert_dtype else None) and enforce
check_dtype=[np.float32, np.float64] (matching input_to_cuml_array) so float16
is converted or rejected consistently, and add regression tests for sparse
int32/float16 with both convert_dtype True/False; update references:
X_is_sparse, SparseCumlArray, convert_dtype, input_to_cuml_array, and
fit_least_squares.
python/cuml/cuml/linear_model/base.py (1)

273-275: ⚠️ Potential issue | 🟠 Major

Normalize non-scalar alpha before entering the solver branches.

Only scalar alpha values are normalized today. A list/tuple still makes Line 280 evaluate to a plain bool, so .all() fails, and mismatched or negative arrays still flow through to Line 310 as sqrt(alpha). Please coerce alpha to a 1D CuPy array of length y.shape[1] and reject negatives up front.

🔧 Suggested fix
-    # Normalize alpha to a cupy array of shape (n_targets,)
-    if cp.isscalar(alpha):
-        alpha = cp.full(y.shape[1], alpha, dtype=X.dtype)
+    # Normalize alpha to a cupy array of shape (n_targets,)
+    alpha = cp.asarray(alpha, dtype=X.dtype)
+    if alpha.ndim == 0:
+        alpha = cp.full(y.shape[1], alpha.item(), dtype=X.dtype)
+    else:
+        alpha = alpha.ravel()
+        if alpha.size != y.shape[1]:
+            raise ValueError(
+                f"alpha must have size {y.shape[1]} for {y.shape[1]} target(s), "
+                f"got {alpha.size}"
+            )
+    if (alpha < 0).any():
+        raise ValueError("alpha must be non-negative")

As per coding guidelines, "Logic errors in ML algorithm implementations, incorrect distance metrics, kernels, or loss function implementations, numerical instability causing wrong results, and incorrect model parameter initialization must be corrected".

Also applies to: 280-285, 309-310

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/cuml/linear_model/base.py` around lines 273 - 275, Normalize and
validate non-scalar alpha before entering solver branches: coerce alpha (if not
a scalar) into a 1-D CuPy array of length y.shape[1] with dtype matching X.dtype
(use cp.asarray(..., dtype=X.dtype) and ravel if needed), check that its shape
matches y.shape[1], and raise ValueError for negative entries or length
mismatch; then proceed to use cp.sqrt(alpha) in the solver. Apply these checks
at the same place where scalar alpha is handled (around the current alpha
normalization block) so downstream code (e.g., where cp.sqrt(alpha) is used)
always receives a validated 1-D CuPy array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@python/cuml/cuml/linear_model/base.py`:
- Around line 273-275: Normalize and validate non-scalar alpha before entering
solver branches: coerce alpha (if not a scalar) into a 1-D CuPy array of length
y.shape[1] with dtype matching X.dtype (use cp.asarray(..., dtype=X.dtype) and
ravel if needed), check that its shape matches y.shape[1], and raise ValueError
for negative entries or length mismatch; then proceed to use cp.sqrt(alpha) in
the solver. Apply these checks at the same place where scalar alpha is handled
(around the current alpha normalization block) so downstream code (e.g., where
cp.sqrt(alpha) is used) always receives a validated 1-D CuPy array.

In `@python/cuml/cuml/linear_model/linear_regression.pyx`:
- Around line 313-323: The sparse branch currently always casts non-floating
inputs to float32 via SparseCumlArray(X, convert_to_dtype=np.float32 if
X.dtype.kind != "f" else None), which ignores the convert_dtype flag and allows
float16 through; change the SparseCumlArray invocation to mirror the dense path
by using convert_to_dtype=(np.float32 if convert_dtype else None) and enforce
check_dtype=[np.float32, np.float64] (matching input_to_cuml_array) so float16
is converted or rejected consistently, and add regression tests for sparse
int32/float16 with both convert_dtype True/False; update references:
X_is_sparse, SparseCumlArray, convert_dtype, input_to_cuml_array, and
fit_least_squares.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4ebeadf8-639f-4256-b8e7-ad939231e224

📥 Commits

Reviewing files that changed from the base of the PR and between f5d112f and 890ad12.

📒 Files selected for processing (2)
  • python/cuml/cuml/linear_model/base.py
  • python/cuml/cuml/linear_model/linear_regression.pyx

@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! The refactor seems correct to me, but additional reviews would be welcome.

Comment thread python/cuml/cuml/linear_model/linear_regression.pyx
Comment thread python/cuml/cuml/linear_model/base.py
Comment thread python/cuml/cuml/linear_model/linear_regression.pyx
Comment thread python/cuml/cuml/linear_model/linear_regression.pyx
Comment thread python/cuml/tests/test_linear_model.py Outdated
@jcrist
jcrist force-pushed the linear-regression-improvements branch from ec5ebc0 to 3da0db7 Compare March 31, 2026 19:45
@jcrist
jcrist force-pushed the linear-regression-improvements branch from 3da0db7 to cbfbda1 Compare March 31, 2026 21:10
@jcrist

jcrist commented Mar 31, 2026

Copy link
Copy Markdown
Member Author

/merge

@rapids-bot
rapids-bot Bot merged commit 0d5f6d2 into NVIDIA:main Apr 1, 2026
172 of 174 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

algo: linear-model 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.

[FEA] Sparse inputs to linear regression

4 participants