From 54b6e3f87a58ba7f1da138e729c4d312c531d5c1 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 15 May 2026 11:58:42 -0500 Subject: [PATCH 1/6] Update `libcuml` wheel to depend on `librmm` (#8110) This is required to import `libcuml`, but wasn't explicitly listed as a dependency. The parent main user-facing package (`cuml`) does have a `rmm` dependency, which in turn depends on `librmm`, so everything would be installed properly normally anyway. Just adding an explicit link here for tidyness. Part of #7845. Authors: - Jim Crist-Harif (https://github.com/jcrist) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) URL: https://github.com/rapidsai/cuml/pull/8110 --- dependencies.yaml | 1 + python/libcuml/pyproject.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/dependencies.yaml b/dependencies.yaml index f1d04fec4a..2f5ceb9f9c 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -248,6 +248,7 @@ files: - cuda_wheels - depends_on_libcuvs - depends_on_libraft + - depends_on_librmm - depends_on_rapids_logger channels: - rapidsai-nightly diff --git a/python/libcuml/pyproject.toml b/python/libcuml/pyproject.toml index cc7a86a1ab..523df2d06f 100644 --- a/python/libcuml/pyproject.toml +++ b/python/libcuml/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ dependencies = [ "cuda-toolkit[cublas,cufft,curand,cusolver,cusparse]==13.*", "libraft==26.6.*,>=0.0.0a0", + "librmm==26.6.*,>=0.0.0a0", "nvidia-nvjitlink>=13.0,<14", "rapids-logger==0.2.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. From da8e0cd567c98a831968ff8142b9409e0abc3931 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 15 May 2026 13:36:50 -0500 Subject: [PATCH 2/6] Fix wrapping metaestimators in `Pipeline` in `cuml.accel` (#8115) Our pipeline data transfer optimization didn't work if any of the steps were other compositional metaestimators that wrapped accelerated estimators (since these could then accidentally use the accelerated versions, resulting in a mix of `cupy` and `numpy` results). This PR patches the other two compositional estimators (`FeatureUnion` and `ColumnTransformer`) so they always run within a `numpy` output-type context. Fixes #8112. Fixes a few sklearn examples as well (yay!) Authors: - Jim Crist-Harif (https://github.com/jcrist) Approvers: - Simon Adorf (https://github.com/csadorf) URL: https://github.com/rapidsai/cuml/pull/8115 --- .../cuml/accel/_patches/sklearn/compose.py | 25 +++++++++ .../cuml/accel/_patches/sklearn/pipeline.py | 24 ++++++-- python/cuml/cuml/accel/core.py | 1 + python/cuml/cuml_accel_tests/test_pipeline.py | 55 ++++++++++++++++++- .../upstream/scikit-learn/xfail-examples.yaml | 5 -- 5 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 python/cuml/cuml/accel/_patches/sklearn/compose.py diff --git a/python/cuml/cuml/accel/_patches/sklearn/compose.py b/python/cuml/cuml/accel/_patches/sklearn/compose.py new file mode 100644 index 0000000000..8f8f9dec54 --- /dev/null +++ b/python/cuml/cuml/accel/_patches/sklearn/compose.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 +import functools + +from sklearn.compose import ColumnTransformer + +from cuml.internals.outputs import using_output_type + +__all__ = ("ColumnTransformer",) + + +def patch_method(name): + """Patch a ColumnTransformer method to ensure results returned as numpy.""" + orig_method = getattr(ColumnTransformer, name) + + @functools.wraps(orig_method) + def method(self, *args, **kwargs): + with using_output_type("numpy"): + return orig_method(self, *args, **kwargs) + + setattr(ColumnTransformer, name, method) + + +for method_name in ["fit", "fit_transform", "transform"]: + patch_method(method_name) diff --git a/python/cuml/cuml/accel/_patches/sklearn/pipeline.py b/python/cuml/cuml/accel/_patches/sklearn/pipeline.py index 629f0ea86a..a032f290a0 100644 --- a/python/cuml/cuml/accel/_patches/sklearn/pipeline.py +++ b/python/cuml/cuml/accel/_patches/sklearn/pipeline.py @@ -5,14 +5,14 @@ import cupy as cp from cupyx.scipy.sparse import issparse as is_cp_sparse -from sklearn.pipeline import Pipeline +from sklearn.pipeline import FeatureUnion, Pipeline from sklearn.utils.metaestimators import available_if from cuml.accel.estimator_proxy import is_proxy from cuml.internals.global_settings import GlobalSettings from cuml.internals.outputs import using_output_type -__all__ = ("Pipeline",) +__all__ = ("Pipeline", "FeatureUnion") def get_output_type(pipeline, reverse=False): @@ -54,7 +54,7 @@ def flat_steps(pipeline): return "cupy" -def patch_method(name): +def patch_pipeline_method(name): """Patch a sklearn Pipeline method to reduce device<->host transfers.""" orig_method = inspect.getattr_static(Pipeline, name) # Unwrap @available_if decorated methods @@ -105,4 +105,20 @@ def method(self, *args, **kwargs): "score_samples", "transform", ]: - patch_method(method_name) + patch_pipeline_method(method_name) + + +def patch_feature_union_method(name): + """Patch a FeatureUnion method to ensure results returned as numpy.""" + orig_method = getattr(FeatureUnion, name) + + @functools.wraps(orig_method) + def method(self, *args, **kwargs): + with using_output_type("numpy"): + return orig_method(self, *args, **kwargs) + + setattr(FeatureUnion, name, method) + + +for method_name in ["fit", "fit_transform", "transform"]: + patch_feature_union_method(method_name) diff --git a/python/cuml/cuml/accel/core.py b/python/cuml/cuml/accel/core.py index 22d15868e4..8ea280d054 100644 --- a/python/cuml/cuml/accel/core.py +++ b/python/cuml/cuml/accel/core.py @@ -92,6 +92,7 @@ def debug(self, msg: str) -> None: _PATCHES = { "sklearn.pipeline", + "sklearn.compose", "sklearn.utils", "sklearn.utils._array_api", "sklearn.utils.discovery", diff --git a/python/cuml/cuml_accel_tests/test_pipeline.py b/python/cuml/cuml_accel_tests/test_pipeline.py index cb0935d554..814b114ad2 100644 --- a/python/cuml/cuml_accel_tests/test_pipeline.py +++ b/python/cuml/cuml_accel_tests/test_pipeline.py @@ -13,6 +13,7 @@ from packaging.version import Version from sklearn.base import BaseEstimator from sklearn.cluster import DBSCAN, KMeans +from sklearn.compose import ColumnTransformer from sklearn.datasets import make_classification, make_regression from sklearn.decomposition import PCA, TruncatedSVD from sklearn.linear_model import ( @@ -28,8 +29,8 @@ KNeighborsRegressor, NearestNeighbors, ) -from sklearn.pipeline import Pipeline, make_pipeline -from sklearn.preprocessing import StandardScaler +from sklearn.pipeline import FeatureUnion, Pipeline, make_pipeline +from sklearn.preprocessing import RobustScaler, StandardScaler from umap import UMAP SKLEARN_18 = Version(sklearn.__version__) >= Version("1.8.0.dev0") @@ -356,3 +357,53 @@ def test_pipeline_classifier_predict_non_numeric_labels(patch_methods): assert isinstance(LogisticRegression.predict.args[0], cp.ndarray) # User-facing output is always numpy assert isinstance(out, np.ndarray) + + +@requires_sklearn_18 +def test_column_transfomer_in_pipeline_works(): + """Ensure outputs of steps in `ColumnTransformer` return as numpy""" + rng = np.random.default_rng(0) + X = rng.standard_normal((200, 20)).astype(np.float32) + y = rng.standard_normal(200).astype(np.float32) + + ct = ColumnTransformer( + [ + ("svd", TruncatedSVD(n_components=5), slice(0, 10)), + ("pass", "passthrough", slice(10, 20)), + ] + ) + + pipe = Pipeline( + [ + ("ct", ct), # Shouldn't be accelerated + ("scaler", RobustScaler()), # Not accelerated + ("ridge", Ridge()), # Accelerated + ] + ) + + pipe.fit(X, y) + + +@requires_sklearn_18 +def test_feature_union_in_pipeline_works(): + """Ensure outputs of steps in `FeatureUnion` return as numpy""" + rng = np.random.default_rng(0) + X = rng.standard_normal((200, 20)).astype(np.float32) + y = rng.standard_normal(200).astype(np.float32) + + union = FeatureUnion( + [ + ("svd", TruncatedSVD(n_components=2)), + ("pca", PCA(n_components=2)), + ] + ) + + pipe = Pipeline( + [ + ("features", union), # Shouldn't be accelerated + ("scaler", RobustScaler()), # Not accelerated + ("ridge", Ridge()), # Accelerated + ] + ) + + pipe.fit(X, y) diff --git a/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-examples.yaml b/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-examples.yaml index e19cee5cbf..0027602b97 100644 --- a/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-examples.yaml +++ b/python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-examples.yaml @@ -3,11 +3,6 @@ strict: false tests: - "decomposition::plot_faces_decomposition" -- reason: 'cuml.accel bug: implicit CuPy-to-NumPy conversion not handled in ColumnTransformer/Pipeline' - marker: cuml_accel_bugs - tests: - - "linear_model::plot_poisson_regression_non_normal_loss" - - "release_highlights::plot_release_highlights_1_1_0" - reason: 'cuml.accel bug: native crash in cuml PCA' marker: cuml_accel_bugs tests: From 0095e1e2edf2ac9be9a30185be155ca80f231504 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 15 May 2026 14:32:21 -0500 Subject: [PATCH 3/6] Use the PTDS for (most) cupy operations (#8086) This configures cupy to use the per-thread default stream (PTDS) for _most_ operations. This avoids usage of the default legacy stream in more of the codebase, allowing for improved parallelism when running across multiple threads. **This is a breaking change.** Previously any cupy operations in `cuml` ran in cupy's default stream (the legacy stream). We didn't synchronize the stream before returning, but that didn't matter due to the synchronization behavior of the legacy stream. With this PR we've moved to running (most) cupy operations in the PTDS. Depending on the operation, we may not synchronize the PTDS before returning. **Most users shouldn't notice a difference and should have no issues.** Users not using threads, custom streams, or only working with host memory (e.g. numpy in/numpy out) should see no difference. Likewise any users that only use cupy's default stream (the legacy stream) in their code should see no issues. Users doing tricky things with custom streams or threads may run into issues and require a manual sync of the PTDS (can be done with `cupy.cuda.Stream.ptds.synchronize()`. For example, the following workflow _may_ run into issues: - Run a cuml operation based on cupy in thread A, returning a cupy array - Consume that output in thread B as a cupy array using a stream other than the legacy stream (e.g. a different PTDS or a custom stream) For safety, you probably want to add a call to `cupy.cuda.Stream.ptds.synchronize()` in thread A before returning to ensure the output array is fully populated before consuming it in thread B. Fixes #7909. Authors: - Jim Crist-Harif (https://github.com/jcrist) Approvers: - Simon Adorf (https://github.com/csadorf) - Dante Gama Dessavre (https://github.com/dantegd) URL: https://github.com/rapidsai/cuml/pull/8086 --- python/cuml/cuml/internals/outputs.py | 7 +++-- python/cuml/cuml/preprocessing/label.py | 4 --- python/cuml/tests/test_prims.py | 8 ----- python/cuml/tests/test_reflection.py | 40 +++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/python/cuml/cuml/internals/outputs.py b/python/cuml/cuml/internals/outputs.py index 2b91978f81..0bcdb535c3 100644 --- a/python/cuml/cuml/internals/outputs.py +++ b/python/cuml/cuml/internals/outputs.py @@ -7,6 +7,7 @@ import inspect import numpy as np +from cupy.cuda import Stream # TODO: Try to resolve circular import that makes this necessary: from cuml.internals import input_utils as iu @@ -212,7 +213,8 @@ def enter_internal_context(): gs._external_output_type = gs.output_type gs.output_type = "mirror" try: - yield True + with Stream.ptds: + yield True finally: gs.output_type = gs._external_output_type gs._external_output_type = False @@ -451,6 +453,7 @@ def inner(*args, **kwargs): # We're internal, return as cuml output_type = "cuml" - return coerce_arrays(res, output_type) + with enter_internal_context(): + return coerce_arrays(res, output_type) return inner diff --git a/python/cuml/cuml/preprocessing/label.py b/python/cuml/cuml/preprocessing/label.py index 590e5fc458..a042130c8e 100644 --- a/python/cuml/cuml/preprocessing/label.py +++ b/python/cuml/cuml/preprocessing/label.py @@ -49,8 +49,6 @@ def label_binarize( dtype=cp.float32, ) - cp.cuda.Stream.null.synchronize() - is_binary = classes.shape[0] == 2 if sparse_output: @@ -185,8 +183,6 @@ def fit(self, y) -> "LabelBinarizer": else: self.classes_ = cp.unique(y).astype(y.dtype) - cp.cuda.Stream.null.synchronize() - return self @cuml.internals.reflect diff --git a/python/cuml/tests/test_prims.py b/python/cuml/tests/test_prims.py index 30e4fcccca..5f497bb279 100644 --- a/python/cuml/tests/test_prims.py +++ b/python/cuml/tests/test_prims.py @@ -24,8 +24,6 @@ def test_monotonic_without_classes(arr_type, dtype, copy): monotonic, returned_classes = make_monotonic(arr, copy=copy) - cp.cuda.Stream.null.synchronize() - # Verify monotonic mapping: [0, 15, 10, 50, 20, 50] -> [0, 2, 1, 4, 3, 4] # (sorted unique: 0->0, 10->1, 15->2, 20->3, 50->4) expected_monotonic = cp.array([0, 2, 1, 4, 3, 4], dtype=dtype) @@ -56,8 +54,6 @@ def test_monotonic_inversion(dtype): # Invert: use classes array to map indices back to original values inverted = classes[monotonic] - cp.cuda.Stream.null.synchronize() - assert array_equal(inverted, original) @@ -74,8 +70,6 @@ def test_monotonic_with_explicit_classes(dtype, copy): labels, classes=classes, copy=copy ) - cp.cuda.Stream.null.synchronize() - # Labels should map to their position in the original classes array # 5 -> 0, 2 -> 1, 8 -> 2 expected = cp.array([2, 1, 0, 1, 2], dtype=dtype) @@ -100,8 +94,6 @@ def test_monotonic_unknown_labels(dtype): monotonic, _ = make_monotonic(labels, classes=classes, copy=True) - cp.cuda.Stream.null.synchronize() - # Unknown labels (999, -1) should map to len(classes) = 3 # 1 -> 0, 999 -> 3, 2 -> 1, 3 -> 2, -1 -> 3 expected = cp.array([0, 3, 1, 2, 3], dtype=dtype) diff --git a/python/cuml/tests/test_reflection.py b/python/cuml/tests/test_reflection.py index 9931e4e432..08b2560400 100644 --- a/python/cuml/tests/test_reflection.py +++ b/python/cuml/tests/test_reflection.py @@ -394,3 +394,43 @@ def test_array_descriptor_cache_behavior(): assert b"pandas" not in msg assert_output_type(model2.X_, "cupy") assert len(model2.__dict__["X_"].values) == 2 # cuml + cupy + + +def test_decorators_set_cupy_ptds(): + class MyEstimator(Base): + @reflect(reset="type") + def fit(self, X, y=None): + assert cp.cuda.get_current_stream() is cp.cuda.Stream.ptds + return self + + @reflect + def direct_call(self, X): + assert cp.cuda.get_current_stream() is cp.cuda.Stream.ptds + return cp.zeros(3) + + @reflect + def nested_call(self, X): + assert cp.cuda.get_current_stream() is cp.cuda.Stream.ptds + return self.direct_call(X) + + @run_in_internal_context + def no_reflection(self, X): + assert cp.cuda.get_current_stream() is cp.cuda.Stream.ptds + return cp.zeros(3) + + X = cp.ones(3) + + # Check that ptds is used instead of the default stream + model = MyEstimator() + model.fit(X) + model.direct_call(X) + model.nested_call(X) + model.no_reflection(X) + + # Check that ptds is used instead of a custom stream + with cp.cuda.Stream(): + model = MyEstimator() + model.fit(X) + model.direct_call(X) + model.nested_call(X) + model.no_reflection(X) From 4ad59e410a6908bb4ca8db2f64043b07fc1b49e7 Mon Sep 17 00:00:00 2001 From: Tim Head Date: Fri, 15 May 2026 21:33:34 +0200 Subject: [PATCH 4/6] DOC Add third-party app example for cuml.accel (#8094) This adds to the cuml.accel documentation and documents the "third party application" use-case. Authors: - Tim Head (https://github.com/betatim) Approvers: - Jim Crist-Harif (https://github.com/jcrist) URL: https://github.com/rapidsai/cuml/pull/8094 --- docs/source/cuml-accel/examples/index.rst | 1 + .../cuml-accel/examples/third-party-apps.rst | 117 ++++++++++++++++++ docs/source/cuml-accel/index.rst | 6 + 3 files changed, 124 insertions(+) create mode 100644 docs/source/cuml-accel/examples/third-party-apps.rst diff --git a/docs/source/cuml-accel/examples/index.rst b/docs/source/cuml-accel/examples/index.rst index de65734403..3ea9b53186 100644 --- a/docs/source/cuml-accel/examples/index.rst +++ b/docs/source/cuml-accel/examples/index.rst @@ -11,4 +11,5 @@ examples in this section is available in the cuML GitHub repository at `examples getting_started.ipynb profiling.ipynb plot_kmeans_digits.ipynb + third-party-apps.rst onnx_export.ipynb diff --git a/docs/source/cuml-accel/examples/third-party-apps.rst b/docs/source/cuml-accel/examples/third-party-apps.rst new file mode 100644 index 0000000000..85449af0ee --- /dev/null +++ b/docs/source/cuml-accel/examples/third-party-apps.rst @@ -0,0 +1,117 @@ +Accelerating Third-Party Applications +====================================== + +The ``CUML_ACCEL_ENABLED`` environment variable lets you GPU-accelerate any +Python application that uses ``sklearn``, ``umap``, or ``hdbscan``. +Even applications whose code you cannot modify. This is useful for +installed CLI tools, applications, and third-party libraries. + +.. code-block:: console + + CUML_ACCEL_ENABLED=1 some-third-party-tool [args...] + +When :ref:`CUML_ACCEL_ENABLED=1 is defined `, +`cuml.accel` will be enabled as part of the normal Python interpreter +startup, letting you accelerate Python applications without modification + +This means you do not need access to an application's source code: set the +environment variable and the acceleration applies automatically. + +Example: Embedding Visualization with embedding-atlas +----------------------------------------------------- + +`embedding-atlas `_ is Apple's +open-source tool for interactive visualization of large embedding datasets. +Given a text dataset, it computes sentence embeddings, projects them to 2D +using `UMAP `_, and launches a +browser-based explorer. + +Install it alongside ``cuml``: + +.. code-block:: console + + pip install embedding-atlas + +Run it on a Hugging Face dataset. The example below uses +`TinyStories `_, +a dataset of 2M+ short stories: + +.. code-block:: console + + # CPU -- UMAP runs on CPU + embedding-atlas roneneldan/TinyStories --text text \ + --split train --sample 1000000 + + # GPU -- set environment variable; no other changes needed + CUML_ACCEL_ENABLED=1 embedding-atlas roneneldan/TinyStories --text text \ + --split train --sample 1000000 + +The only change between the two commands is the environment variable. +``embedding-atlas`` computes embeddings with sentence-transformers (which +already uses the GPU), then runs UMAP for dimensionality reduction. +``cuml.accel`` intercepts the ``umap.UMAP`` call inside ``embedding-atlas`` +and dispatches ``fit_transform`` to cuML's GPU implementation. + +Use a smaller ``--sample`` value (e.g. 250000) for a quicker test run. +The UMAP speedup grows with dataset size. + +To confirm GPU dispatch, add ``CUML_ACCEL_LOG_LEVEL=info``: + +.. code-block:: console + + CUML_ACCEL_ENABLED=1 CUML_ACCEL_LOG_LEVEL=info embedding-atlas \ + roneneldan/TinyStories --text text --split train --sample 1000000 + +You should see the following messages amongst the other output: + +.. code-block:: text + + [cuml.accel] Accelerator installed. + [cuml.accel] `UMAP.fit_transform` ran on GPU + +Results +~~~~~~~ + +At the time of writing and on the hardware the author used the +``fit_transform`` step saw a roughly **~4x speedup** because cuML's GPU +UMAP replaces the CPU optimization. The KNN step (``nearest_neighbors``) +is a standalone function call that ``cuml.accel`` does not currently +intercept, so it runs on CPU in both cases. Despite this, the overall +UMAP step is still **~2x faster**. + +At smaller scales (< 100K rows) the UMAP step is already fast on CPU and +the speedup is less pronounced. The benefit grows with dataset size. + + +Identifying Acceleratable Applications +--------------------------------------- + +Any Python tool that calls one of the following is a candidate for +``CUML_ACCEL_ENABLED``: + +- ``sklearn`` estimators (KMeans, PCA, DBSCAN, RandomForest, + LogisticRegression, NearestNeighbors, and + :doc:`many more <../faq>`) +- ``umap.UMAP`` +- ``hdbscan.HDBSCAN`` + +A quick way to check: search an application's dependencies for +``scikit-learn``, ``umap-learn``, or ``hdbscan``, or run with +``CUML_ACCEL_LOG_LEVEL=info`` and look for ``ran on GPU`` messages +in the output. + +Checking for CPU Fallbacks +-------------------------- + +Not all parameter combinations are supported on the GPU. When +``cuml.accel`` encounters an unsupported configuration, it silently +falls back to CPU execution. To detect this, set the log level to +``info`` or ``debug``: + +.. code-block:: console + + CUML_ACCEL_ENABLED=1 CUML_ACCEL_LOG_LEVEL=info python app.py + +Lines containing ``ran on GPU`` confirm GPU execution. Lines +containing ``falling back to CPU`` indicate a fallback, along with +the reason. See :doc:`../logging-and-profiling` for more detail. diff --git a/docs/source/cuml-accel/index.rst b/docs/source/cuml-accel/index.rst index 4caadb1e28..0f1ca42196 100644 --- a/docs/source/cuml-accel/index.rst +++ b/docs/source/cuml-accel/index.rst @@ -58,6 +58,8 @@ executing the following line magic at the top (before other imports): You can see an example of this in :doc:`this example `. +.. _cuml-accel-env-var: + Environment Variable ~~~~~~~~~~~~~~~~~~~~ @@ -75,6 +77,10 @@ environment variable to ``1`` or ``true`` (case insensitive). Note that any python program running with the environment defined this way will load the accelerator, which may result in a measurable startup overhead. +This approach is especially useful for accelerating +:doc:`third-party applications ` whose code you do not +control. + Additionally, if ``cuml`` is not installed properly in your environment, the ``CUML_ACCEL_ENABLED`` environment variable will be silently ignored (and normal CPU execution will occur). For this reason one of the other methods From 87d7b8ea0f099b10f72e097a8a1ab3088650d831 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 15 May 2026 15:50:26 -0500 Subject: [PATCH 5/6] Numpy 1.x compatibility fixes (#8118) This: - Bumps our minimum supported `numpy` version to 1.26, to match that of `cudf`. Since `cudf` is a required dependency, we were effectively pinned at that already. - Adds `numpy` to our oldest deps test runs. This also effectively adds `cupy==13.6`, since `cupy==14` requires `numpy>=2.0`. Explicitly specifying `cupy==13.6` in an oldest-deps run is tricky since the pypi packages require cuda suffixes as well. I'm skipping that for now. - Fixes a few incompatibilities with numpy 1.x Authors: - Jim Crist-Harif (https://github.com/jcrist) Approvers: - Gil Forsyth (https://github.com/gforsyth) - Simon Adorf (https://github.com/csadorf) URL: https://github.com/rapidsai/cuml/pull/8118 --- .../all_cuda-129_arch-aarch64.yaml | 2 +- .../all_cuda-129_arch-x86_64.yaml | 2 +- .../all_cuda-132_arch-aarch64.yaml | 2 +- .../all_cuda-132_arch-x86_64.yaml | 2 +- conda/recipes/cuml/recipe.yaml | 2 +- dependencies.yaml | 3 +- python/cuml/cuml/internals/validation.py | 29 ++++++++++++------- .../cuml/linear_model/linear_regression.pyx | 2 +- python/cuml/cuml/linear_model/ridge.pyx | 2 +- python/cuml/pyproject.toml | 2 +- .../cuml/tests/explainer/test_gpu_treeshap.py | 6 ++++ python/cuml/tests/test_validation.py | 5 ++-- 12 files changed, 38 insertions(+), 21 deletions(-) diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 699e9872f3..f70b17a786 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -47,7 +47,7 @@ dependencies: - nltk - numba-cuda>=0.22.2,<0.29.0 - numba>=0.60.0,<0.65.0 -- numpy>=1.23,<3.0 +- numpy>=1.26,<3.0 - numpydoc - numpydoc<1.9 - nvidia-ml-py>=12 diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 644e0132e7..130a281624 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -46,7 +46,7 @@ dependencies: - nltk - numba-cuda>=0.22.2,<0.29.0 - numba>=0.60.0,<0.65.0 -- numpy>=1.23,<3.0 +- numpy>=1.26,<3.0 - numpydoc - numpydoc<1.9 - nvidia-ml-py>=12 diff --git a/conda/environments/all_cuda-132_arch-aarch64.yaml b/conda/environments/all_cuda-132_arch-aarch64.yaml index 692f50dbd6..40981932cf 100644 --- a/conda/environments/all_cuda-132_arch-aarch64.yaml +++ b/conda/environments/all_cuda-132_arch-aarch64.yaml @@ -47,7 +47,7 @@ dependencies: - nltk - numba-cuda>=0.22.2,<0.29.0 - numba>=0.60.0,<0.65.0 -- numpy>=1.23,<3.0 +- numpy>=1.26,<3.0 - numpydoc - numpydoc<1.9 - nvidia-ml-py>=12 diff --git a/conda/environments/all_cuda-132_arch-x86_64.yaml b/conda/environments/all_cuda-132_arch-x86_64.yaml index 2eda3604e5..b879ce0d48 100644 --- a/conda/environments/all_cuda-132_arch-x86_64.yaml +++ b/conda/environments/all_cuda-132_arch-x86_64.yaml @@ -46,7 +46,7 @@ dependencies: - nltk - numba-cuda>=0.22.2,<0.29.0 - numba>=0.60.0,<0.65.0 -- numpy>=1.23,<3.0 +- numpy>=1.26,<3.0 - numpydoc - numpydoc<1.9 - nvidia-ml-py>=12 diff --git a/conda/recipes/cuml/recipe.yaml b/conda/recipes/cuml/recipe.yaml index 2c526f86ca..960d4347df 100644 --- a/conda/recipes/cuml/recipe.yaml +++ b/conda/recipes/cuml/recipe.yaml @@ -100,7 +100,7 @@ requirements: - libcuml =${{ version }} - numba >=0.60.0,<0.65.0 - numba-cuda >=0.22.2,<0.29.0 - - numpy >=1.23,<3.0 + - numpy >=1.26,<3.0 - scikit-learn >=1.4 - scipy >=1.14.0 - packaging diff --git a/dependencies.yaml b/dependencies.yaml index 2f5ceb9f9c..12705a4401 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -324,7 +324,7 @@ dependencies: packages: - joblib>=0.11 - numba>=0.60.0,<0.65.0 - - &numpy numpy>=1.23,<3.0 + - &numpy numpy>=1.26,<3.0 - scipy>=1.14.0 - packaging - rich @@ -531,6 +531,7 @@ dependencies: - scikit-learn==1.5.0 - umap-learn==0.5.7 - hdbscan==0.8.39 + - numpy==1.26 - matrix: {dependencies: "intermediate"} packages: - scikit-learn==1.7.2 diff --git a/python/cuml/cuml/internals/validation.py b/python/cuml/cuml/internals/validation.py index 2e00a2c623..e8afe0aa55 100644 --- a/python/cuml/cuml/internals/validation.py +++ b/python/cuml/cuml/internals/validation.py @@ -351,7 +351,7 @@ def check_all_finite(array, *, allow_nan=False, input_name=None) -> None: input_name : str or None, default=None The input parameter name to use in error messages. """ - if not np.isdtype(array.dtype, "real floating"): + if not array.dtype.kind == "f": # No-op for non floating inputs return @@ -484,6 +484,19 @@ def _index_as_mem_type(index, mem_type=None): return index +if np.lib.NumpyVersion(np.__version__) >= "2.0.0b1": + np_asarray = np.asarray +else: + + def np_asarray(x, dtype=None, order=None, copy=None): + """A compatibility shim for `np.asarray`. + + numpy 2.0 added the `copy` arg to `np.asarray`, as well as changed the + meaning of copy=False to "error if a copy required" rather than "only + copy if needed" (which is now `copy=None`).""" + return np.array(x, dtype=dtype, order=order or "K", copy=bool(copy)) + + def check_array( array, *, @@ -600,7 +613,7 @@ def check_array( # Infer proper output dtype if array_dtype is not None: # Check for complex inputs before conversion when possible - if np.isdtype(array_dtype, "complex floating"): + if array_dtype.kind == "c": raise ValueError("Complex data not supported") if dtype is None: dtype = array_dtype @@ -703,7 +716,7 @@ def check_array( elif ( mem_type is None and cudf.pandas.LOADED - and np.isdtype(array.dtype, ("numeric", "bool")) + and array.dtype.kind in "iufb" ): # We treat pandas objects with supported dtypes as device # memory when running under cudf.pandas. Note that the output @@ -732,8 +745,7 @@ def check_array( array, dtype=dtype, order=order, copy=(copy or None) ) else: - # XXX: using np.array for compat with numpy < 2 - array = np.array( + array = np_asarray( array, dtype=dtype, order=order, copy=(copy or None) ) @@ -761,7 +773,7 @@ def check_array( ) # Check for complex inputs after conversion for cases when `dtype=None` - if np.isdtype(array.dtype, "complex floating"): + if array.dtype.kind == "c": raise ValueError("Complex data not supported") # Validate data meets expected value requirements @@ -1052,10 +1064,7 @@ def check_y( input_dtype = y.dtype if mem_type is None: mem_type = "host" if isinstance(y, np.ndarray) else "device" - if ( - np.isdtype(y.dtype, ("numeric", "bool")) - and return_classes is True - ): + if y.dtype.kind in "iufb" and return_classes is True: y = cp.asarray(y) elif ( y.dtype == "object" diff --git a/python/cuml/cuml/linear_model/linear_regression.pyx b/python/cuml/cuml/linear_model/linear_regression.pyx index 450db871a1..387a0dd437 100644 --- a/python/cuml/cuml/linear_model/linear_regression.pyx +++ b/python/cuml/cuml/linear_model/linear_regression.pyx @@ -241,7 +241,7 @@ class LinearRegression(Base, ) # All libcuml solvers require F-ordered X, and mutate the inputs. - X = cp.asarray(X, order="F", copy=None if may_mutate_X else True) + X = cp.array(X, order="F", copy=None if may_mutate_X else True) if not may_mutate_y: y = y.copy() if sample_weight is not None and not may_mutate_sample_weight: diff --git a/python/cuml/cuml/linear_model/ridge.pyx b/python/cuml/cuml/linear_model/ridge.pyx index c9e196cbfd..085474e848 100644 --- a/python/cuml/cuml/linear_model/ridge.pyx +++ b/python/cuml/cuml/linear_model/ridge.pyx @@ -285,7 +285,7 @@ class Ridge(Base, # The `eig` solver requires X be F-contiguous. Additionally, all inputs # are mutated when weighted or `fit_intercept=True`. mutates = self.fit_intercept or sample_weight is not None - X = cp.asarray(X, order="F", copy=True if mutates and not may_mutate_X else None) + X = cp.array(X, order="F", copy=True if mutates and not may_mutate_X else None) if mutates and not may_mutate_y: y = y.copy() if sample_weight is not None and mutates and not may_mutate_sample_weight: diff --git a/python/cuml/pyproject.toml b/python/cuml/pyproject.toml index 18a5aa8a25..04ae556bb7 100644 --- a/python/cuml/pyproject.toml +++ b/python/cuml/pyproject.toml @@ -87,7 +87,7 @@ dependencies = [ "libcuml==26.6.*,>=0.0.0a0", "numba-cuda>=0.22.2,<0.29.0", "numba>=0.60.0,<0.65.0", - "numpy>=1.23,<3.0", + "numpy>=1.26,<3.0", "nvidia-nvjitlink>=13.0,<14", "packaging", "pylibraft==26.6.*,>=0.0.0a0", diff --git a/python/cuml/tests/explainer/test_gpu_treeshap.py b/python/cuml/tests/explainer/test_gpu_treeshap.py index 3459a30ead..10b27525b7 100644 --- a/python/cuml/tests/explainer/test_gpu_treeshap.py +++ b/python/cuml/tests/explainer/test_gpu_treeshap.py @@ -124,6 +124,9 @@ def count_categorical_split(tl_model): ) def test_xgb_regressor(objective): xgb = pytest.importorskip("xgboost") + pytest.importorskip( + "numpy", minversion="2.0", reason="Test fails on numpy < 2" + ) n_samples = 100 X, y = make_regression( @@ -197,6 +200,9 @@ def test_xgb_regressor(objective): ) def test_xgb_classifier(objective, n_classes): xgb = pytest.importorskip("xgboost") + pytest.importorskip( + "numpy", minversion="2.0", reason="Test fails on numpy < 2" + ) n_samples = 100 X, y = make_classification( diff --git a/python/cuml/tests/test_validation.py b/python/cuml/tests/test_validation.py index 7d67d1c603..95a97ca113 100644 --- a/python/cuml/tests/test_validation.py +++ b/python/cuml/tests/test_validation.py @@ -841,8 +841,9 @@ def test_check_array_dataframe_mixed_dtypes(kind, mem_type): ) # Non-numeric columns -> object dtype by default if is_cuda_output(mem_type, df): - # cupy doesn't support object dtypes - with pytest.raises((ValueError, TypeError), match="object"): + # cupy doesn't support object dtypes. We don't care what the exception + # is here, just that one is raised. + with pytest.raises(Exception, match="object"): check_array(df, mem_type=mem_type) else: # dtype=None does no conversion by default From a4c824be458ea73af2abb4afa2efb21d5c963a17 Mon Sep 17 00:00:00 2001 From: James Lamb Date: Sat, 16 May 2026 07:13:52 -0500 Subject: [PATCH 6/6] loosen threshold in test_mbsgd_regressor test (#8122) Closes #8121 Proposes slightly reducing the threshold in the R-squared check for `MBSGDRegressor` tests. Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Simon Adorf (https://github.com/csadorf) URL: https://github.com/rapidsai/cuml/pull/8122 --- python/cuml/tests/test_mbsgd_regressor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/cuml/tests/test_mbsgd_regressor.py b/python/cuml/tests/test_mbsgd_regressor.py index 6d3da2b9ef..02a2619605 100644 --- a/python/cuml/tests/test_mbsgd_regressor.py +++ b/python/cuml/tests/test_mbsgd_regressor.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 import cupy as cp import numpy as np @@ -138,7 +138,7 @@ def test_mbsgd_regressor(lrate, penalty, make_dataset): cu_pred = model.predict(X_test) cu_r2 = r2_score(cu_pred, y_test) - assert cu_r2 >= 0.88 + assert cu_r2 >= 0.87 def test_mbsgd_regressor_default(make_dataset):