diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 7727b278ac..f1b8dba2e3 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 65af4e4f2e..4373395c39 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 18310d403c..efd0f07f8d 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 d72d3f3a28..4f5aa5a2bb 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 fdadaff091..e7cd918cd2 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 @@ -323,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 @@ -530,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/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 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/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/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/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/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: diff --git a/python/cuml/pyproject.toml b/python/cuml/pyproject.toml index 738c980e16..b9f593909c 100644 --- a/python/cuml/pyproject.toml +++ b/python/cuml/pyproject.toml @@ -87,7 +87,7 @@ dependencies = [ "libcuml==26.8.*,>=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.8.*,>=0.0.0a0", diff --git a/python/cuml/tests/explainer/test_gpu_treeshap.py b/python/cuml/tests/explainer/test_gpu_treeshap.py index c956397bf2..754f05e54b 100644 --- a/python/cuml/tests/explainer/test_gpu_treeshap.py +++ b/python/cuml/tests/explainer/test_gpu_treeshap.py @@ -125,6 +125,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( @@ -198,6 +201,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_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): 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) 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 diff --git a/python/libcuml/pyproject.toml b/python/libcuml/pyproject.toml index b08c55c78a..9d11cdd7b0 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.8.*,>=0.0.0a0", + "librmm==26.8.*,>=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`.