-
Notifications
You must be signed in to change notification settings - Fork 681
Use scikit-learn's array-api to accelerate StandardScaler
#8020
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rapids-bot
merged 9 commits into
NVIDIA:main
from
jcrist:array-api-dispatch-preprocessing
Apr 28, 2026
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
21fd19d
Add `in_internal_context`
jcrist 105725b
Add patch disabling array-api check
jcrist cef6e3d
Add ArrayAPIProxyBase
jcrist af4eb29
Use array api for StandardScaler, add tests
jcrist 620e978
Update xfail list
jcrist 992bacf
Update docs
jcrist 130d475
Only accelerate on scikit-learn >= 1.8
jcrist 17f57c5
Skip pipeline data transfer tests on sklearn < 1.8
jcrist 77f4c98
Update xfail list
jcrist File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,73 +2,17 @@ | |
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
|
|
||
| import cupyx.scipy.sparse as cupy_sparse | ||
| import numpy as np | ||
| from scipy import sparse as sp_sparse | ||
|
|
||
| import cuml.preprocessing | ||
| from cuml.accel.estimator_proxy import ProxyBase | ||
| from cuml.accel.estimator_proxy import ArrayAPIProxyBase, ProxyBase | ||
| from cuml.internals.interop import UnsupportedOnGPU | ||
|
|
||
| __all__ = ("StandardScaler", "TargetEncoder") | ||
|
|
||
|
|
||
| def _check_standardscaler_unsupported_inputs(X, **kwargs): | ||
| """Check if inputs are supported by cuML's StandardScaler on GPU. | ||
|
|
||
| Raises UnsupportedOnGPU for unsupported cases to trigger CPU fallback. | ||
| """ | ||
| if kwargs.get("sample_weight") is not None: | ||
| raise UnsupportedOnGPU("sample_weight is not supported") | ||
|
|
||
| # Reject complex, object, and float16 dtypes | ||
| if hasattr(X, "dtype"): | ||
| if np.issubdtype(X.dtype, np.complexfloating): | ||
| raise UnsupportedOnGPU("complex dtype is not supported") | ||
| if X.dtype == np.object_: | ||
| raise UnsupportedOnGPU("object dtype is not supported") | ||
| if X.dtype == np.float16: | ||
| raise UnsupportedOnGPU("float16 dtype is not supported") | ||
|
|
||
| # Check for sparse matrices with unsupported properties | ||
| if sp_sparse.issparse(X): | ||
| if np.issubdtype(X.dtype, np.integer): | ||
| raise UnsupportedOnGPU( | ||
| "sparse matrix with integer dtype is not supported" | ||
| ) | ||
| # cuML's StandardScaler algorithm only supports CSR/CSC formats. | ||
| if X.format not in ("csr", "csc"): | ||
| raise UnsupportedOnGPU( | ||
| f"sparse matrix format '{X.format}' is not supported" | ||
| ) | ||
| elif cupy_sparse.issparse(X): | ||
| if np.issubdtype(X.dtype, np.integer): | ||
| raise UnsupportedOnGPU( | ||
| "sparse matrix with integer dtype is not supported" | ||
| ) | ||
| # cuML's StandardScaler algorithm only supports CSR/CSC formats. | ||
| if X.format not in ("csr", "csc"): | ||
| raise UnsupportedOnGPU( | ||
| f"sparse matrix format '{X.format}' is not supported" | ||
| ) | ||
|
|
||
|
|
||
| class StandardScaler(ProxyBase): | ||
| _gpu_class = cuml.preprocessing.StandardScaler | ||
|
|
||
| def _gpu_fit(self, X, y=None, sample_weight=None): | ||
| kwargs = {"sample_weight": sample_weight} | ||
| _check_standardscaler_unsupported_inputs(X, **kwargs) | ||
| return self._gpu.fit(X, y) | ||
|
|
||
| def _gpu_fit_transform(self, X, y=None, **fit_params): | ||
| _check_standardscaler_unsupported_inputs(X, **fit_params) | ||
| return self._gpu.fit_transform(X, y, **fit_params) | ||
|
|
||
| def _gpu_partial_fit(self, X, y=None, sample_weight=None): | ||
| """partial_fit not supported on GPU - always fall back to CPU.""" | ||
| raise UnsupportedOnGPU("partial_fit not supported on GPU") | ||
| class StandardScaler(ArrayAPIProxyBase): | ||
| _cpu_class_path = "sklearn.preprocessing.StandardScaler" | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Other array-api backed estimators would be easy to wrap this same way. I've limited this PR to just |
||
|
|
||
|
|
||
| def _check_unsupported_inputs(X, y, cpu_model): | ||
|
|
||
File renamed without changes.
22 changes: 22 additions & 0 deletions
22
python/cuml/cuml/accel/_patches/sklearn/utils/_array_api.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| import functools | ||
|
|
||
| from sklearn.utils._array_api import ( | ||
| _check_array_api_dispatch as _orig_check_array_api_dispatch, | ||
| ) | ||
|
|
||
| from cuml.internals.outputs import in_internal_context | ||
|
|
||
| __all__ = ("_check_array_api_dispatch",) | ||
|
|
||
|
|
||
| @functools.wraps(_orig_check_array_api_dispatch) | ||
| def _check_array_api_dispatch(array_api_dispatch): | ||
| # sklearn's array-api support requires setting SCIPY_ARRAY_API=1, even | ||
| # though all uses we need it for don't rely on scipy. To work around this, | ||
| # we patch sklearn to disable the check when running within a cuml | ||
| # estimator. Usage outside of cuml estimators will still result in the | ||
| # proper error. | ||
| if not in_internal_context(): | ||
| _orig_check_array_api_dispatch(array_api_dispatch) | ||
|
jcrist marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The array-api doesn't support sparse data, so no array-api accelerated estimators will work here. That said, while
StandardScalercan support sparse inputs, doing so withoutwith_mean=Falsewould remove the sparsity. It's kind of a weird operation to do on sparse data anyway.Given that, I'm not concerned about this limitation, and don't think this should prevent us from moving forward with this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit unfortunate that we are losing function, but I agree that we should move forward here. I think for some pre-processors that make more sense for sparse data (like MaxAbsScaler), we might have to revisit this.