From 80040a57231d68b3efa7ff3ac7fe31a4e4a79f7b Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 5 Jun 2026 08:48:16 -0500 Subject: [PATCH 1/8] Add `infer_output_type` This adds a new `infer_output_type` method for mapping an input to its corresponding output type. This new method is: - Simpler than the old one in `input_utils.py` - Maps better to how the new ingest `check_array` function treats inputs - Properly handles array-like inputs, rather than returning `None` --- python/cuml/cuml/internals/outputs.py | 57 +++++++++++++++++++++++ python/cuml/tests/test_reflection.py | 66 +++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/python/cuml/cuml/internals/outputs.py b/python/cuml/cuml/internals/outputs.py index 0bcdb535c3..7ae78bcc6c 100644 --- a/python/cuml/cuml/internals/outputs.py +++ b/python/cuml/cuml/internals/outputs.py @@ -6,11 +6,17 @@ import functools import inspect +import cudf +import cupy as cp +import cupyx.scipy.sparse as cp_sp import numpy as np +import pandas as pd +import scipy.sparse as sp from cupy.cuda import Stream # TODO: Try to resolve circular import that makes this necessary: from cuml.internals import input_utils as iu +from cuml.internals.array import CumlArray from cuml.internals.array_sparse import SparseCumlArray from cuml.internals.global_settings import GlobalSettings from cuml.internals.validation import check_features @@ -269,6 +275,57 @@ def _get_param(sig, name_or_index): return param.name +def infer_output_type(array, array_like="numpy"): + """Infer the corresponding ``output_type`` given an input array-like. + + Parameters + ---------- + array : array-like + The array-like value to infer from. + array_like : Any, default="numpy" + The value to return if `array` is not an array but is array-like. + + Returns + ------- + output_type : {"cupy", "numpy", "pandas", "cudf", "numba", "cuml", None} + The inferred ``output_type``, or ``None`` if not an array-like input. + """ + if isinstance(array, np.ndarray) or sp.issparse(array): + return "numpy" + elif isinstance(array, cp.ndarray) or cp_sp.issparse(array): + return "cupy" + elif isinstance(array, (CumlArray, SparseCumlArray)): + return "cuml" + elif isinstance(array, (cudf.Series, cudf.DataFrame)): + return "cudf" + elif isinstance(array, (pd.Series, pd.DataFrame)): + return "pandas" + elif hasattr(array, "__cuda_ndarray__"): + return "numba" + elif hasattr(array, "__cuda_array_interface__"): + return "cupy" + + # Explicitly exclude a few common collections that aren't array-likes. This + # matches those also explicitly excluded in our validation routines. + if isinstance(array, (str, bytes, dict)): + return None + + # Exclude numpy scalars, which also implement `__array__` + if np.isscalar(array): + return None + + # Types with any of these attributes _may_ be coerced to an array by our + # validation methods (e.g. `check_array`). The actual instance may error at + # that point, but that's fine, this is just a best effort inference to + # exclude non-array-like things like `None`/`1`/... + for name in ["__array__", "__array_interface__", "__len__"]: + if hasattr(array, name): + return array_like + + # Not an array-like input, just return None + return None + + def coerce_arrays(res, output_type): """Traverse a result, converting it to the proper output type""" if isinstance(res, tuple): diff --git a/python/cuml/tests/test_reflection.py b/python/cuml/tests/test_reflection.py index 08b2560400..3230bf9238 100644 --- a/python/cuml/tests/test_reflection.py +++ b/python/cuml/tests/test_reflection.py @@ -19,6 +19,7 @@ from cuml.internals.array_sparse import SparseCumlArray from cuml.internals.base import Base from cuml.internals.global_settings import GlobalSettings +from cuml.internals.outputs import infer_output_type OUTPUT_TYPES = ["numpy", "numba", "cupy", "cudf", "pandas"] @@ -136,6 +137,71 @@ def test_using_output_type(): pass +@pytest.mark.parametrize("input_type", OUTPUT_TYPES) +def test_infer_output_type(input_type): + X = rand_array(input_type) + output_type = infer_output_type(X) + assert output_type == input_type + + +def test_infer_output_type_cuml(): + a = CumlArray(cp.array([[1, 2], [3, 4]])) + b = SparseCumlArray(cupyx.scipy.sparse.random(5, 5, random_state=42)) + assert infer_output_type(a) == "cuml" + assert infer_output_type(b) == "cuml" + + +class ImplementsArray: + def __init__(self, x): + self.x = x + + def __array__(self, dtype=None, copy=None): + return self.x + + +class ImplementsArrayInterface: + def __init__(self, x): + self.x = x + + @property + def __array_interface__(self): + return self.x.__array_interface__ + + +@pytest.mark.parametrize( + "obj", + [ + pytest.param([1, 2], id="list"), + pytest.param((1, 2), id="tuple"), + pytest.param([[1, 2], [3, 4]], id="nested-list"), + pytest.param(ImplementsArray(np.array([1, 2])), id="__array__"), + pytest.param( + ImplementsArrayInterface(np.array([1, 2])), + id="__array_interface__", + ), + ], +) +def test_infer_output_type_array_like(obj): + assert infer_output_type(obj) == "numpy" + assert infer_output_type(obj, array_like="fizz") == "fizz" + + +@pytest.mark.parametrize( + "obj", + [ + pytest.param(None, id="none"), + pytest.param(1, id="scalar"), + pytest.param(np.int32(1), id="numpy-scalar"), + pytest.param("abc", id="string"), + pytest.param(b"abc", id="bytes"), + pytest.param({"a": 1, "b": 2}, id="dict"), + pytest.param(object(), id="arbitrary-object"), + ], +) +def test_infer_output_type_non_arrays(obj): + assert infer_output_type(obj) is None + + @pytest.mark.parametrize("input_type", OUTPUT_TYPES) @pytest.mark.filterwarnings("ignore::UserWarning") def test_default_output_type(input_type): From e37f514729359f691753293d688d19f87d9aef5f Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 5 Jun 2026 09:07:36 -0500 Subject: [PATCH 2/8] Use `infer_output_type` in `coerce_arrays` --- python/cuml/cuml/internals/outputs.py | 14 +++++--- python/cuml/tests/test_reflection.py | 52 ++++++++++++++++++--------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/python/cuml/cuml/internals/outputs.py b/python/cuml/cuml/internals/outputs.py index 7ae78bcc6c..35b3666bcb 100644 --- a/python/cuml/cuml/internals/outputs.py +++ b/python/cuml/cuml/internals/outputs.py @@ -14,8 +14,6 @@ import scipy.sparse as sp from cupy.cuda import Stream -# TODO: Try to resolve circular import that makes this necessary: -from cuml.internals import input_utils as iu from cuml.internals.array import CumlArray from cuml.internals.array_sparse import SparseCumlArray from cuml.internals.global_settings import GlobalSettings @@ -336,18 +334,24 @@ def coerce_arrays(res, output_type): return {k: coerce_arrays(v, output_type) for k, v in res.items()} # Get the output type - arr_type, is_sparse = iu.determine_array_type_full(res) + arr_type = infer_output_type(res, array_like=None) if arr_type is None: # Not an array, just return return res + is_sparse = ( + cp_sp.issparse(res) + or sp.issparse(res) + or isinstance(res, SparseCumlArray) + ) + # If we are a supported array and not already cuml, convert to cuml if arr_type != "cuml": if is_sparse: res = SparseCumlArray(res, convert_index=False) else: - res = iu.input_to_cuml_array(res, order="K").array + res = CumlArray.from_input(res, order="K") if output_type == "cuml": # Return CumlArray/SparseCumlArray directly @@ -501,7 +505,7 @@ def inner(*args, **kwargs): output_type = gs.output_type if output_type in ("input", None): if array is not None: - output_type = iu.determine_array_type(array_arg) + output_type = infer_output_type(array_arg) if output_type in ("input", None): # Nothing to infer from and no explicit type set, # default to cupy diff --git a/python/cuml/tests/test_reflection.py b/python/cuml/tests/test_reflection.py index 3230bf9238..622dc19c61 100644 --- a/python/cuml/tests/test_reflection.py +++ b/python/cuml/tests/test_reflection.py @@ -61,6 +61,23 @@ def rand_array(output_type, *, shape=(8, 4), seed=42): return cudf.DataFrame(X) +class ImplementsArray: + def __init__(self, x): + self.x = x + + def __array__(self, dtype=None, copy=None): + return self.x + + +class ImplementsArrayInterface: + def __init__(self, x): + self.x = x + + @property + def __array_interface__(self): + return self.x.__array_interface__ + + class DummyEstimator(Base): X_ = CumlArrayDescriptor() @@ -151,23 +168,6 @@ def test_infer_output_type_cuml(): assert infer_output_type(b) == "cuml" -class ImplementsArray: - def __init__(self, x): - self.x = x - - def __array__(self, dtype=None, copy=None): - return self.x - - -class ImplementsArrayInterface: - def __init__(self, x): - self.x = x - - @property - def __array_interface__(self): - return self.x.__array_interface__ - - @pytest.mark.parametrize( "obj", [ @@ -336,6 +336,24 @@ def make_sparse(): assert scipy.sparse.issparse(res) +@pytest.mark.parametrize( + "obj", + [ + pytest.param([1, 2, 3], id="list"), + pytest.param((1, 2, 3), id="tuple"), + pytest.param(ImplementsArray(np.array([1, 2, 3])), id="__array__"), + ], +) +def test_dont_convert_array_like(obj): + @reflect + def make_array_like(): + return obj + + cuml.set_global_output_type("numpy") + res = make_array_like() + assert type(res) is type(obj) + + @pytest.mark.parametrize("output_type", [None, *OUTPUT_TYPES]) def test_functions(output_type): cuml.set_global_output_type(output_type) From 29852162a179c48e2530e5e2d624e683e845af02 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 5 Jun 2026 09:15:50 -0500 Subject: [PATCH 3/8] Use `infer_output_type` in `CumlArrayDescriptor` --- python/cuml/cuml/common/array_descriptor.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/python/cuml/cuml/common/array_descriptor.py b/python/cuml/cuml/common/array_descriptor.py index 2e102e6440..87e2e5a80a 100644 --- a/python/cuml/cuml/common/array_descriptor.py +++ b/python/cuml/cuml/common/array_descriptor.py @@ -7,10 +7,7 @@ import cuml from cuml.internals.array import CumlArray -from cuml.internals.input_utils import ( - determine_array_type, - input_to_cuml_array, -) +from cuml.internals.outputs import infer_output_type @dataclass @@ -80,9 +77,9 @@ def _to_output(self, instance, to_output_type, to_output_dtype=None): # If the input type was anything but CumlArray, need to create one now if "cuml" not in existing.values: - existing.values["cuml"] = input_to_cuml_array( + existing.values["cuml"] = CumlArray.from_input( existing.get_input_value(), order="K" - ).array + ) cuml_arr: CumlArray = existing.values["cuml"] @@ -128,7 +125,7 @@ def __set__(self, instance, value): existing = self._get_meta(instance) # Determine the type - existing.input_type = determine_array_type(value) + existing.input_type = infer_output_type(value, array_like=None) # Clear any existing values existing.values.clear() From b705ace8daa6b2e9225ce148959a0b9f8d9441c4 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 5 Jun 2026 09:16:25 -0500 Subject: [PATCH 4/8] Use `infer_output_type` in `cuml.Base` --- python/cuml/cuml/internals/base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/python/cuml/cuml/internals/base.py b/python/cuml/cuml/internals/base.py index 1004bcc4ee..4012a6211e 100644 --- a/python/cuml/cuml/internals/base.py +++ b/python/cuml/cuml/internals/base.py @@ -11,11 +11,10 @@ import cuml import cuml.common import cuml.internals -import cuml.internals.input_utils import cuml.internals.logger as logger import cuml.internals.nvtx as nvtx -from cuml.internals.input_utils import determine_array_type from cuml.internals.mixins import TagsMixin +from cuml.internals.outputs import infer_output_type _THREAD_STATE = threading.local() @@ -192,7 +191,7 @@ def set_params(self, **params): return self def _set_output_type(self, inp): - self._input_type = determine_array_type(inp) + self._input_type = infer_output_type(inp) def _get_output_type(self, inp=None): """ @@ -214,7 +213,7 @@ class output type and global output type. output_type = self._input_type else: # Determine the output from the input - output_type = determine_array_type(inp) + output_type = infer_output_type(inp) return output_type From a3dfb23c555fb6d06d9f27ea6820d88df4b8d7d5 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 5 Jun 2026 09:54:51 -0500 Subject: [PATCH 5/8] Expand test coverage --- python/cuml/tests/test_reflection.py | 39 ++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/python/cuml/tests/test_reflection.py b/python/cuml/tests/test_reflection.py index 622dc19c61..0562688112 100644 --- a/python/cuml/tests/test_reflection.py +++ b/python/cuml/tests/test_reflection.py @@ -20,6 +20,7 @@ from cuml.internals.base import Base from cuml.internals.global_settings import GlobalSettings from cuml.internals.outputs import infer_output_type +from cuml.internals.validation import check_inputs OUTPUT_TYPES = ["numpy", "numba", "cupy", "cudf", "pandas"] @@ -78,12 +79,22 @@ def __array_interface__(self): return self.x.__array_interface__ +class ImplementsCudaArrayInterface: + def __init__(self, x): + self.x = x + + @property + def __cuda_array_interface__(self): + return self.x.__cuda_array_interface__ + + class DummyEstimator(Base): X_ = CumlArrayDescriptor() - @reflect(reset=True) + @reflect(reset="type") def fit(self, X, y=None): - self.X_ = CumlArray.from_input(X) + X = check_inputs(self, X, reset=True) + self.X_ = CumlArray(data=X) return self @reflect @@ -168,6 +179,11 @@ def test_infer_output_type_cuml(): assert infer_output_type(b) == "cuml" +def test_infer_output_type_cuda_array_interface(): + x = ImplementsCudaArrayInterface(cp.array([1, 2, 3])) + assert infer_output_type(x) == "cupy" + + @pytest.mark.parametrize( "obj", [ @@ -435,6 +451,25 @@ def test_estimator_method_with_array_input(): assert_output_type(model.example(X2), "cupy") +def test_array_like_inputs_treated_as_numpy_by_reflection(): + X_cupy = rand_array("cupy", shape=(10, 5)) + X_list = rand_array("numpy", shape=(10, 5)).tolist() + + model_fit_list = DummyEstimator().fit(X_list) + model_fit_cupy = DummyEstimator().fit(X_cupy) + + # Fitting on array-likes stores `numpy` as input-type + assert model_fit_list._input_type == "numpy" + + # Inferring on array-likes uses `numpy` as output type + assert_output_type(model_fit_list.example(X_list), "numpy") + assert_output_type(model_fit_cupy.example(X_list), "numpy") + + # Methods with no args use input type + assert_output_type(model_fit_list.example_no_args(), "numpy") + assert_output_type(model_fit_cupy.example_no_args(), "cupy") + + def test_estimator_method_with_no_array_input(): X = rand_array("numpy", shape=(10, 5)) model = DummyEstimator().fit(X) From cf9ed6ec1941b265bab19b569d11e270ef806927 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 5 Jun 2026 09:28:41 -0500 Subject: [PATCH 6/8] Update sklearn compat tests --- .../cuml/tests/test_sklearn_compatibility.py | 29 ++----------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/python/cuml/tests/test_sklearn_compatibility.py b/python/cuml/tests/test_sklearn_compatibility.py index cd7666bdfc..91bb6caa54 100644 --- a/python/cuml/tests/test_sklearn_compatibility.py +++ b/python/cuml/tests/test_sklearn_compatibility.py @@ -182,43 +182,35 @@ def _all_cuml_estimators(): KMeans: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", "check_sample_weight_equivalence_on_dense_data": "KMeans sample weight equivalence not implemented", - "check_transformer_data_not_an_array": "KMeans does not handle non-array data", }, KernelRidge: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_regressor_data_not_an_array": "KernelRidge does not handle non-array data", }, LogisticRegression: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", "check_sample_weight_equivalence_on_dense_data": "LogisticRegression sample weight equivalence not implemented", "check_sample_weight_equivalence_on_sparse_data": "LogisticRegression does not handle sparse data", "check_class_weight_classifiers": "LogisticRegression does not handle class weights properly", - "check_classifier_data_not_an_array": "LogisticRegression does not handle non-array data", }, LinearRegression: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_regressor_data_not_an_array": "LinearRegression does not handle non-array data", }, Ridge: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_regressor_data_not_an_array": "Ridge does not handle non-array data", "check_non_transformer_estimators_n_iter": "Ridge `n_iter_` may be `None`", }, RandomForestRegressor: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_regressor_data_not_an_array": "RandomForestRegressor does not handle non-array data", + "check_regressor_data_not_an_array": "float32 and float64 inputs yield different-enough values", }, RandomForestClassifier: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_classifier_data_not_an_array": "RandomForestClassifier does not handle non-array data", }, KNeighborsClassifier: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_classifier_data_not_an_array": "KNeighborsClassifier does not handle non-array data", }, KNeighborsRegressor: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_regressor_data_not_an_array": "KNeighborsRegressor does not handle non-array data", "check_supervised_y_2d": "KNeighborsRegressor does not handle 2D y", }, NearestNeighbors: { @@ -226,39 +218,32 @@ def _all_cuml_estimators(): }, LinearSVC: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_classifier_data_not_an_array": "LinearSVC does not handle non-array data", "check_sample_weight_equivalence_on_dense_data": "LinearSVC sample weight equivalence not implemented", }, LinearSVR: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", "check_sample_weight_equivalence_on_dense_data": "LinearSVR sample weight equivalence not implemented", - "check_regressor_data_not_an_array": "LinearSVR does not handle non-array data", }, SVC: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", "check_sample_weight_equivalence_on_dense_data": "SVC sample weight equivalence not implemented", "check_sample_weight_equivalence_on_sparse_data": "SVC does not handle sparse data", - "check_classifier_data_not_an_array": "SVC does not handle non-array data", }, SVR: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", "check_sample_weight_equivalence_on_dense_data": "SVR sample weight equivalence not implemented", "check_sample_weight_equivalence_on_sparse_data": "SVR does not handle sparse data", - "check_regressor_data_not_an_array": "SVR does not handle non-array data", }, PCA: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_transformer_data_not_an_array": "PCA does not handle non-array data", "check_fit2d_1sample": "PCA does not handle single sample", "check_fit2d_1feature": "PCA does not handle single feature", }, IncrementalPCA: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_transformer_data_not_an_array": "IncrementalPCA does not handle non-array data", }, TruncatedSVD: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_transformer_data_not_an_array": "TruncatedSVD does not handle non-array data", "check_fit2d_1sample": "TruncatedSVD does not handle single sample", "check_fit2d_1feature": "TruncatedSVD does not handle single feature", }, @@ -274,19 +259,17 @@ def _all_cuml_estimators(): }, UMAP: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_transformer_data_not_an_array": "UMAP does not handle non-array data", + "check_transformer_data_not_an_array": "float32 and float64 inputs yield different-enough values", "check_methods_sample_order_invariance": "UMAP results depend on sample order", "check_transformer_general": "UMAP does not have consistent fit_transform and transform outputs", "check_methods_subset_invariance": "UMAP results depend on data subset", }, Lasso: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_regressor_data_not_an_array": "Lasso does not handle non-array data", "check_sample_weight_equivalence_on_sparse_data": "Lasso QN solver has issues with sample weights", }, ElasticNet: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_regressor_data_not_an_array": "ElasticNet does not handle non-array data", "check_sample_weight_equivalence_on_sparse_data": "ElasticNet QN solver has issues with sample weights", }, KernelDensity: { @@ -316,35 +299,27 @@ def _all_cuml_estimators(): "check_fit_score_takes_y": "AttributeError: 'int' object has no attribute 'repeat'", "check_do_not_raise_errors_in_init_or_set_params": "StandardScaler(**params) raises an exception", "check_estimator_sparse_tag": "Sparse tag inconsistent with with_mean=True default", - "check_transformer_data_not_an_array": "Non-array data leads to an exception", }, GaussianRandomProjection: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_transformer_data_not_an_array": "GaussianRandomProjection does not handle non-array data", }, SparseRandomProjection: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_transformer_data_not_an_array": "SparseRandomProjection does not handle non-array data", }, GaussianNB: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_classifier_data_not_an_array": "GaussianNB does not handle non-array data", }, BernoulliNB: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_classifier_data_not_an_array": "bug in reflection prevents this", }, ComplementNB: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_classifier_data_not_an_array": "bug in reflection prevents this", }, CategoricalNB: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_classifier_data_not_an_array": "bug in reflection prevents this", }, MultinomialNB: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_classifier_data_not_an_array": "bug in reflection prevents this", }, } From fd1a135ebda39de5a3a006a8d472447bd377909a Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 5 Jun 2026 09:34:54 -0500 Subject: [PATCH 7/8] Delete now unused code --- python/cuml/cuml/common/__init__.py | 1 - python/cuml/cuml/internals/input_utils.py | 193 ---------------------- 2 files changed, 194 deletions(-) diff --git a/python/cuml/cuml/common/__init__.py b/python/cuml/cuml/common/__init__.py index a557b90e63..bb5f0b1140 100644 --- a/python/cuml/cuml/common/__init__.py +++ b/python/cuml/cuml/common/__init__.py @@ -9,7 +9,6 @@ from cuml.internals.input_utils import ( input_to_cuml_array, input_to_host_array, - input_to_host_array_with_sparse_support, sparse_scipy_to_cp, ) from cuml.internals.outputs import set_global_output_type, using_output_type diff --git a/python/cuml/cuml/internals/input_utils.py b/python/cuml/cuml/internals/input_utils.py index 05b6e57b25..fbaf8564a7 100644 --- a/python/cuml/cuml/internals/input_utils.py +++ b/python/cuml/cuml/internals/input_utils.py @@ -2,7 +2,6 @@ # SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # - from collections import namedtuple import cudf @@ -18,160 +17,12 @@ import cuml.internals.nvtx as nvtx from cuml.internals.array import CumlArray from cuml.internals.array_sparse import SparseCumlArray -from cuml.internals.global_settings import GlobalSettings from cuml.internals.mem_type import MemoryType -global_settings = GlobalSettings() PANDAS_VERSION = Version(pd.__version__) cuml_array = namedtuple("cuml_array", "array n_rows n_cols dtype") -_input_type_to_str = { - CumlArray: "cuml", - SparseCumlArray: "cuml", - np.ndarray: "numpy", - pd.Series: "pandas", - pd.DataFrame: "pandas", - pd.Index: "pandas", - cp.ndarray: "cupy", - cudf.Series: "cudf", - cudf.DataFrame: "cudf", - cudf.Index: "cudf", - numba_cuda.devicearray.DeviceNDArrayBase: "numba", - cupyx.scipy.sparse.spmatrix: "cupy", - scipy.sparse.spmatrix: "numpy", - scipy.sparse.sparray: "numpy", -} - -_input_type_to_mem_type = { - np.ndarray: MemoryType.host, - pd.Series: MemoryType.host, - pd.DataFrame: MemoryType.host, - scipy.sparse.spmatrix: MemoryType.host, - scipy.sparse.sparray: MemoryType.host, - cp.ndarray: MemoryType.device, - cudf.Series: MemoryType.device, - cudf.DataFrame: MemoryType.device, - numba_cuda.devicearray.DeviceNDArrayBase: MemoryType.device, - cupyx.scipy.sparse.spmatrix: MemoryType.device, -} - -_SPARSE_TYPES = [ - SparseCumlArray, - cupyx.scipy.sparse.spmatrix, - scipy.sparse.spmatrix, - scipy.sparse.sparray, -] - - -def get_supported_input_type(X): - """ - Determines if the input object is a supported input array-like object or - not. If supported, the type is returned. Otherwise, `None` is returned. - - Parameters - ---------- - X : object - Input object to test - - Notes - ----- - To closely match the functionality of - :func:`~cuml.internals.input_utils.input_to_cuml_array`, this method will - return `cupy.ndarray` for any object supporting - `__cuda_array_interface__` and `numpy.ndarray` for any object supporting - `__array_interface__`. - - Returns - ------- - array-like type or None - If the array-like object is supported, the type is returned. - Otherwise, `None` is returned. - """ - # Check CumlArray first to shorten search time - if isinstance(X, CumlArray): - return CumlArray - - if isinstance(X, SparseCumlArray): - return SparseCumlArray - - if isinstance(X, cudf.Series): - if X.null_count != 0: - return None - else: - return cudf.Series - - if isinstance(X, pd.DataFrame): - return pd.DataFrame - - if isinstance(X, pd.Series): - return pd.Series - - if isinstance(X, pd.Index): - return pd.Index - - if isinstance(X, cudf.DataFrame): - return cudf.DataFrame - - if isinstance(X, cudf.Index): - return cudf.Index - - # A cudf.pandas wrapped Numpy array defines `__cuda_array_interface__` - # which means without this we'd always return a cupy array. We don't want - # to match wrapped cupy arrays, they get dealt with later - if getattr(X, "_fsproxy_slow_type", None) is np.ndarray: - return np.ndarray - - if numba_cuda.devicearray.is_cuda_ndarray(X): - return numba_cuda.devicearray.DeviceNDArrayBase - - if hasattr(X, "__cuda_array_interface__"): - return cp.ndarray - - if hasattr(X, "__array_interface__"): - # For some reason, numpy scalar types also implement - # `__array_interface__`. See numpy.generic.__doc__. Exclude those types - # as well as np.dtypes - if not isinstance(X, np.generic) and not isinstance(X, type): - return np.ndarray - - if cupyx.scipy.sparse.issparse(X): - return cupyx.scipy.sparse.spmatrix - - if scipy.sparse.isspmatrix(X): - return scipy.sparse.spmatrix - - if scipy.sparse.issparse(X) and X.ndim == 2: - return scipy.sparse.sparray - - # Return None if this type is not supported - return None - - -def determine_array_type(X): - if X is None: - return None - - # Get the generic type - gen_type = get_supported_input_type(X) - - return _input_type_to_str.get(gen_type, None) - - -def determine_df_obj_type(X): - if X is None: - return None - - # Get the generic type - gen_type = get_supported_input_type(X) - - if gen_type in (cudf.DataFrame, pd.DataFrame): - return "dataframe" - elif gen_type in (cudf.Series, pd.Series): - return "series" - - return None - def determine_array_dtype(X): if X is None: @@ -194,32 +45,6 @@ def determine_array_dtype(X): return dtype -def determine_array_type_full(X): - """ - Returns a tuple of the array type, and a boolean if it is sparse - - Parameters - ---------- - X : array-like - Input array to test - - Returns - ------- - (string, bool) Returns a tuple of the array type string and a boolean if it - is a sparse array. - """ - if X is None: - return None, None - - # Get the generic type - gen_type = get_supported_input_type(X) - - if gen_type is None: - return None, None - - return _input_type_to_str[gen_type], gen_type in _SPARSE_TYPES - - def is_array_like(X, accept_lists=False): """Check if X is array-like. @@ -473,24 +298,6 @@ def input_to_host_array( return out_data._replace(array=out_data.array.to_output("numpy")) -def input_to_host_array_with_sparse_support(X): - if X is None: - return None - if scipy.sparse.issparse(X): - return X - _array_type, is_sparse = determine_array_type_full(X) - if is_sparse: - if _array_type == "cupy": - return SparseCumlArray(X).to_output(output_type="scipy") - elif _array_type == "cuml": - return X.to_output(output_type="scipy") - elif _array_type == "numpy": - return X - else: - raise ValueError(f"Unsupported sparse array type: {_array_type}.") - return input_to_host_array(X).array - - def convert_dtype(X, to_dtype=np.float32, legacy=True, safe_dtype=True): """ Convert X to be of dtype `dtype`, raising a TypeError From 45671150499e8058f36be08ec0093c07a7599ac9 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Fri, 5 Jun 2026 10:16:00 -0500 Subject: [PATCH 8/8] Fixup dataframe support --- python/cuml/cuml/internals/outputs.py | 4 ++-- python/cuml/tests/test_reflection.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/python/cuml/cuml/internals/outputs.py b/python/cuml/cuml/internals/outputs.py index 35b3666bcb..39b3bcf85e 100644 --- a/python/cuml/cuml/internals/outputs.py +++ b/python/cuml/cuml/internals/outputs.py @@ -294,9 +294,9 @@ def infer_output_type(array, array_like="numpy"): return "cupy" elif isinstance(array, (CumlArray, SparseCumlArray)): return "cuml" - elif isinstance(array, (cudf.Series, cudf.DataFrame)): + elif isinstance(array, (cudf.DataFrame, cudf.Series, cudf.Index)): return "cudf" - elif isinstance(array, (pd.Series, pd.DataFrame)): + elif isinstance(array, (pd.DataFrame, pd.Series, pd.Index)): return "pandas" elif hasattr(array, "__cuda_ndarray__"): return "numba" diff --git a/python/cuml/tests/test_reflection.py b/python/cuml/tests/test_reflection.py index 0562688112..c69462a7a3 100644 --- a/python/cuml/tests/test_reflection.py +++ b/python/cuml/tests/test_reflection.py @@ -179,6 +179,15 @@ def test_infer_output_type_cuml(): assert infer_output_type(b) == "cuml" +@pytest.mark.parametrize("kind", ["cudf", "pandas"]) +def test_infer_output_type_dataframes(kind): + ns = cudf if kind == "cudf" else pd + df = ns.DataFrame({"x": [1, 2, 3]}, index=[10, 20, 30]) + assert infer_output_type(df) == kind + assert infer_output_type(df.x) == kind + assert infer_output_type(df.index) == kind + + def test_infer_output_type_cuda_array_interface(): x = ImplementsCudaArrayInterface(cp.array([1, 2, 3])) assert infer_output_type(x) == "cupy"