diff --git a/dependencies.yaml b/dependencies.yaml index f35cd7d6a6..1ce8523a31 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -664,7 +664,7 @@ dependencies: dependencies: "oldest" cuda: "12.*" packages: - - cuda-bindings==12.9.2 + - cuda-bindings==12.9.3 - matrix: dependencies: "oldest" cuda: "13.*" @@ -712,7 +712,7 @@ dependencies: matrices: - matrix: {dependencies: "latest"} packages: - - scikit-learn==1.9.0 + - scikit-learn==1.9.1 - matrix: {dependencies: "intermediate"} packages: - scikit-learn==1.8.0 diff --git a/docs/source/cuml-accel/compatibility.rst b/docs/source/cuml-accel/compatibility.rst index 0343b1caad..8297a6aee4 100644 --- a/docs/source/cuml-accel/compatibility.rst +++ b/docs/source/cuml-accel/compatibility.rst @@ -15,7 +15,7 @@ General Behavior ---------------- **Compatibility** - The accelerator is tested with ``scikit-learn`` versions 1.6 through 1.9, + The accelerator is tested with ``scikit-learn`` versions 1.6 through 1.9.1, ``umap-learn`` versions 0.5.7 through 0.5.12, and ``hdbscan`` versions 0.8.39 through 0.8.44. When ``cuml.accel`` detects a version outside these ranges, it issues a runtime warning and continues. The untested version will likely diff --git a/python/cuml/cuml/_thirdparty/sklearn/preprocessing/_data.py b/python/cuml/cuml/_thirdparty/sklearn/preprocessing/_data.py index 7ad7bfd88b..17da1654b4 100644 --- a/python/cuml/cuml/_thirdparty/sklearn/preprocessing/_data.py +++ b/python/cuml/cuml/_thirdparty/sklearn/preprocessing/_data.py @@ -2463,17 +2463,17 @@ def _sparse_fit(self, X, random_state): column_nnz_data = X.data[X.indptr[feature_idx]: X.indptr[feature_idx + 1]] if len(column_nnz_data) > self.subsample: - column_subsample = (self.subsample * len(column_nnz_data) // - n_samples) - if self.ignore_implicit_zeros: - column_data = np.zeros(shape=column_subsample, - dtype=X.dtype) - else: - column_data = np.zeros(shape=self.subsample, dtype=X.dtype) + column_data = np.zeros(shape=self.subsample, dtype=X.dtype) + column_subsample = ( + self.subsample + if self.ignore_implicit_zeros + else self.subsample * len(column_nnz_data) // n_samples + ) column_data[:column_subsample] = np.array( - random_state.choice(column_nnz_data.get(), - size=column_subsample, - replace=False)) + random_state.choice( + column_nnz_data.get(), size=column_subsample, replace=False + ) + ) else: if self.ignore_implicit_zeros: column_data = np.zeros(shape=len(column_nnz_data), @@ -2491,6 +2491,7 @@ def _sparse_fit(self, X, random_state): cpu_np.nanpercentile(np.asnumpy(column_data), np.asnumpy(references))) self.quantiles_ = cpu_np.transpose(np.asnumpy(self.quantiles_)) + # due to floating-point precision error in `np.nanpercentile`, # make sure the quantiles are monotonically increasing # Upstream issue in numpy: diff --git a/python/cuml/cuml/accel/core.py b/python/cuml/cuml/accel/core.py index 5f3056c9d3..3b2f922de0 100644 --- a/python/cuml/cuml/accel/core.py +++ b/python/cuml/cuml/accel/core.py @@ -1,5 +1,5 @@ # -# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # from __future__ import annotations @@ -119,7 +119,7 @@ def __call__(self) -> bool: _CONSTRAINTS = { - "sklearn": CheckConstraint("scikit-learn>=1.6.0,<=1.9.0"), + "sklearn": CheckConstraint("scikit-learn>=1.6.0,<=1.9.1"), "hdbscan": CheckConstraint("hdbscan>=0.8.39,<=0.8.44"), "umap": CheckConstraint("umap-learn>=0.5.7,<=0.5.12"), } diff --git a/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml b/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml index 8168cc5811..4e1f0c1028 100644 --- a/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml +++ b/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml @@ -1195,3 +1195,8 @@ - "sklearn.linear_model.tests.test_logistic::test_liblinear_dual_random_state[42]" - "sklearn.linear_model.tests.test_logistic::test_liblinear_with_large_values" - "sklearn.svm.tests.test_svm::test_liblinear_set_coef[42]" +- reason: sklearn >= 1.9.1 test failures + condition: scikit-learn>=1.9.1 + tests: + - "sklearn.neighbors.tests.test_neighbors::test_radius_neighbors_parallel_array_radius[ball_tree]" + - "sklearn.neighbors.tests.test_neighbors::test_radius_neighbors_parallel_array_radius[kd_tree]" diff --git a/python/cuml/tests/test_preprocessing.py b/python/cuml/tests/test_preprocessing.py index 02b2bb5c4d..aa1ee314b6 100644 --- a/python/cuml/tests/test_preprocessing.py +++ b/python/cuml/tests/test_preprocessing.py @@ -74,6 +74,8 @@ sparse_nan_filled_positive, ) +SKLEARN_VERSION = Version(sklearn.__version__) + @pytest.mark.parametrize("feature_range", [(0, 1), (0.1, 0.8)]) def test_minmax_scaler( @@ -821,9 +823,7 @@ def test_kbinsdiscretizer( assert type(r_X) is type(t_X) sklearn_kwargs = {} - if strategy == "quantile" and Version(sklearn.__version__) >= Version( - "1.7" - ): + if strategy == "quantile" and SKLEARN_VERSION >= Version("1.7"): # cuML uses linear percentile interpolation. Scikit-learn exposed the # method in 1.7 and changed its default in 1.9. sklearn_kwargs["quantile_method"] = "linear" @@ -1061,7 +1061,23 @@ def test_quantile_transformer( @pytest.mark.parametrize("n_quantiles", [30, 100]) @pytest.mark.parametrize("output_distribution", ["uniform", "normal"]) -@pytest.mark.parametrize("ignore_implicit_zeros", [False, True]) +@pytest.mark.parametrize( + "ignore_implicit_zeros", + [ + False, + pytest.param( + True, + marks=pytest.mark.xfail( + SKLEARN_VERSION < Version("1.9.1"), + reason=( + "sklearn bug in sparse quantiles with ignore_implicit_zeros " + "in sklearn <= 1.9.0" + ), + strict=True, + ), + ), + ], +) @pytest.mark.parametrize("subsample", [100]) def test_quantile_transformer_sparse( failure_logger, @@ -1116,6 +1132,36 @@ def test_quantile_transformer_sparse( assert_allclose(r_X, sk_r_X) +def test_quantile_transformer_sparse_subsampling_ignore_implicit_zeros(): + subsample = 500 + kws = dict(subsample=subsample, n_quantiles=50, random_state=42) + + # A very sparse X matrix with two similar columns. + # One with nnz `subsample - 1`, the other with nnz `subsample + 1` + row = cp.arange(subsample * 2) + col = cp.asarray([0, 1]).repeat([subsample - 1, subsample + 1]) + data = cp.concatenate( + ( + cp.linspace(1, 2, num=subsample - 1), + cp.linspace(1, 2, num=subsample + 1), + ) + ) + X = cpx.scipy.sparse.csc_array( + (data, (row, col)), + shape=(2 * subsample**2, 2), + ) + + qt = cuQuantileTransformer(ignore_implicit_zeros=True, **kws).fit(X) + quantiles = qt.quantiles_.T + assert (qt.quantiles_ > 0).all() + assert not cp.all(quantiles[1] == quantiles[1][0]) + + # if ignore_implicit_zeros=False, quantiles are mostly zeros + qt = cuQuantileTransformer(ignore_implicit_zeros=False, **kws).fit(X) + quantiles = qt.fit(X).quantiles_ + assert cp.isclose(quantiles, 0).mean() > 0.9 + + @pytest.mark.filterwarnings( "ignore:'ignore_implicit_zeros' takes effect only with sparse matrix.*:UserWarning" )