diff --git a/python/cuml/cuml/neighbors/kernel_density.py b/python/cuml/cuml/neighbors/kernel_density.py index 9a3a5d7541..8acaedbf9b 100644 --- a/python/cuml/cuml/neighbors/kernel_density.py +++ b/python/cuml/cuml/neighbors/kernel_density.py @@ -11,12 +11,12 @@ from cuml.internals.array import CumlArray from cuml.internals.base import Base -from cuml.internals.input_utils import input_to_cuml_array, input_to_cupy_array from cuml.internals.interop import InteropMixin, UnsupportedOnGPU from cuml.internals.outputs import reflect, run_in_internal_context from cuml.internals.validation import ( - check_features, + check_inputs, check_is_fitted, + check_non_negative, check_random_seed, ) from cuml.metrics import pairwise_distances @@ -267,7 +267,7 @@ def __init__( self.metric = metric self.metric_params = metric_params - @reflect(reset=True) + @reflect(reset="type") def fit( self, X, y=None, sample_weight=None, *, convert_dtype=True ) -> "KernelDensity": @@ -288,55 +288,40 @@ def fit( self Returns the instance itself. """ + if self.kernel not in VALID_KERNELS: + raise ValueError(f"kernel={self.kernel!r} is not supported") + if isinstance(self.bandwidth, str): - if self.bandwidth == "scott": - self.bandwidth_ = X.shape[0] ** (-1 / (X.shape[1] + 4)) - elif self.bandwidth == "silverman": - self.bandwidth_ = (X.shape[0] * (X.shape[1] + 2) / 4) ** ( - -1 / (X.shape[1] + 4) - ) - else: + if self.bandwidth not in ("scott", "silverman"): raise ValueError( f"Expected bandwidth in ['scott', 'silverman'], got {self.bandwidth!r}" ) elif self.bandwidth <= 0: raise ValueError(f"Expected bandwidth > 0, got {self.bandwidth}") - else: - self.bandwidth_ = self.bandwidth - if self.kernel not in VALID_KERNELS: - raise ValueError(f"kernel={self.kernel!r} is not supported") - - self._X, n_rows, n_cols, _ = input_to_cupy_array( + self._X, self._sample_weight = check_inputs( + self, X, + sample_weight=sample_weight, + dtype=("float32", "float64"), + convert_dtype=convert_dtype, order="C", - convert_to_dtype=(np.float32 if convert_dtype else None), - check_dtype=[cp.float32, cp.float64], + reset=True, ) + if self._sample_weight is not None: + check_non_negative(self._sample_weight, input_name="sample_weight") - if n_rows < 1: - raise ValueError( - f"Found array with 0 sample(s) (shape={self._X.shape}) while " - f"a minimum of 1 is required by KernelDensity" - ) - if n_cols < 1: - raise ValueError( - f"Found array with 0 feature(s) (shape={self._X.shape}) while " - f"a minimum of 1 is required by KernelDensity" - ) - - if sample_weight is not None: - self._sample_weight = input_to_cupy_array( - sample_weight, - convert_to_dtype=(np.float32 if convert_dtype else None), - check_dtype=[cp.float32, cp.float64], - check_cols=1, - check_rows=self._X.shape[0], - ).array - if self._sample_weight.min() < 0: - raise ValueError("sample_weight must have positive values") + if isinstance(self.bandwidth, str): + if self.bandwidth == "scott": + self.bandwidth_ = self._X.shape[0] ** ( + -1 / (self._X.shape[1] + 4) + ) + else: # silverman + self.bandwidth_ = ( + self._X.shape[0] * (self._X.shape[1] + 2) / 4 + ) ** (-1 / (self._X.shape[1] + 4)) else: - self._sample_weight = None + self.bandwidth_ = self.bandwidth return self @@ -358,14 +343,13 @@ def score_samples(self, X, *, convert_dtype=True) -> CumlArray: data. """ check_is_fitted(self) - check_features(self, X) - - X = input_to_cuml_array( + X = check_inputs( + self, X, - convert_to_dtype=(self._X.dtype if convert_dtype else None), - check_dtype=[self._X.dtype], - check_cols=self.n_features_in_, - ).array + dtype=[self._X.dtype], + convert_dtype=convert_dtype, + order="C", + ) if self.metric_params: if len(self.metric_params) != 1: raise ValueError( diff --git a/python/cuml/cuml/neighbors/kneighbors_classifier.pyx b/python/cuml/cuml/neighbors/kneighbors_classifier.pyx index 67d2e8a1ae..9189618b13 100644 --- a/python/cuml/cuml/neighbors/kneighbors_classifier.pyx +++ b/python/cuml/cuml/neighbors/kneighbors_classifier.pyx @@ -8,7 +8,6 @@ import cupy as cp import numpy as np import cuml -from cuml.common import input_to_cuml_array from cuml.common.classification import decode_labels from cuml.common.doc_utils import generate_docstring from cuml.internals import get_handle @@ -140,14 +139,14 @@ class KNeighborsClassifier(ClassifierMixin, FMajorInputTagMixin, NeighborsBase): def _attrs_from_cpu(self, model): return { "classes_": model.classes_, - "_y": cp.asarray(model._y, order="F", dtype=np.int32), + "_y": cp.asarray(model._y, dtype=np.int32, order="F"), **super()._attrs_from_cpu(model), } def _attrs_to_cpu(self, model): return { "classes_": self.classes_, - "_y": self._y.get(), + "_y": cp.asnumpy(self._y), "outputs_2d_": self.outputs_2d_, **super()._attrs_to_cpu(model), } @@ -164,7 +163,7 @@ class KNeighborsClassifier(ClassifierMixin, FMajorInputTagMixin, NeighborsBase): self.weights = weights @generate_docstring(convert_dtype_cast='np.float32') - @reflect(reset=True) + @reflect(reset="type") def fit(self, X, y, *, convert_dtype=True) -> "KNeighborsClassifier": """ Fit a GPU index for k-nearest neighbors classifier model. @@ -178,12 +177,13 @@ class KNeighborsClassifier(ClassifierMixin, FMajorInputTagMixin, NeighborsBase): super().fit(X, convert_dtype=convert_dtype) y, classes = check_y( y, + dtype="int32", + convert_dtype=convert_dtype, order="F", - dtype=np.int32, accept_multi_output=True, return_classes=True, ) - check_consistent_length(X, y) + check_consistent_length(self._fit_X, y) self.classes_ = classes self._y = y return self @@ -210,20 +210,11 @@ class KNeighborsClassifier(ClassifierMixin, FMajorInputTagMixin, NeighborsBase): X, return_distance=True, convert_dtype=convert_dtype ) - cdef size_t n_rows - inds, n_rows, _, _ = input_to_cuml_array( - knn_indices, - order='C', - check_dtype=np.int64, - convert_to_dtype=(np.int64 if convert_dtype else None), - ) - - dists, _, _, _ = input_to_cuml_array( - knn_distances, - order='C', - check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None), + inds_cp = cp.ascontiguousarray( + knn_indices.to_output("cupy"), dtype=np.int64 ) + dists_cp = knn_distances.to_output("cupy") + cdef size_t n_rows = inds_cp.shape[0] # Allocate array for predictions out_cols = self._y.shape[1] if self._y.ndim == 2 else 1 @@ -238,14 +229,14 @@ class KNeighborsClassifier(ClassifierMixin, FMajorInputTagMixin, NeighborsBase): y_vec.push_back(col.data.ptr) # Compute weights (returns None for uniform weights) - weights_cp = compute_weights(dists.to_output('cupy'), self.weights) + weights_cp = compute_weights(dists_cp, self.weights) cdef float* weights_ptr = ( 0 if weights_cp is None else weights_cp.data.ptr ) handle = get_handle() cdef handle_t* handle_ = handle.getHandle() - cdef int64_t* inds_ptr = inds.ptr + cdef int64_t* inds_ptr = inds_cp.data.ptr cdef size_t n_samples_fit = self._y.shape[0] cdef int n_neighbors = self.n_neighbors with nogil: @@ -283,20 +274,12 @@ class KNeighborsClassifier(ClassifierMixin, FMajorInputTagMixin, NeighborsBase): X, return_distance=True, convert_dtype=convert_dtype ) - cdef size_t n_rows - inds, n_rows, _, _ = input_to_cuml_array( - knn_indices, - order='C', - check_dtype=np.int64, - convert_to_dtype=(np.int64 if convert_dtype else None) - ) - - dists, _, _, _ = input_to_cuml_array( - knn_distances, - order='C', - check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None) + inds_cp = cp.ascontiguousarray( + knn_indices.to_output("cupy"), dtype=np.int64 ) + dists_cp = knn_distances.to_output("cupy") + cdef size_t n_rows = inds_cp.shape[0] + index = knn_indices.index if self._y.ndim == 1 or self._y.shape[1] == 1: n_classes = [len(self.classes_)] @@ -311,21 +294,21 @@ class KNeighborsClassifier(ClassifierMixin, FMajorInputTagMixin, NeighborsBase): cdef vector[int*] y_vec for n, y in zip(n_classes, ys): proba = CumlArray.zeros( - (n_rows, n), dtype=np.float32, order="C", index=inds.index + (n_rows, n), dtype=np.float32, order="C", index=index ) probas.append(proba) out_vec.push_back(proba.ptr) y_vec.push_back(y.data.ptr) # Compute weights (returns None for uniform weights) - weights_cp = compute_weights(dists.to_output('cupy'), self.weights) + weights_cp = compute_weights(dists_cp, self.weights) cdef float* weights_ptr = ( 0 if weights_cp is None else weights_cp.data.ptr ) handle = get_handle() cdef handle_t* handle_ = handle.getHandle() - cdef int64_t* inds_ptr = inds.ptr + cdef int64_t* inds_ptr = inds_cp.data.ptr cdef size_t n_samples_fit = self._y.shape[0] cdef int n_neighbors = self.n_neighbors with nogil: diff --git a/python/cuml/cuml/neighbors/kneighbors_regressor.pyx b/python/cuml/cuml/neighbors/kneighbors_regressor.pyx index 3326bb76da..648f6b8fa4 100644 --- a/python/cuml/cuml/neighbors/kneighbors_regressor.pyx +++ b/python/cuml/cuml/neighbors/kneighbors_regressor.pyx @@ -2,14 +2,15 @@ # SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # +import cupy as cp import numpy as np -from cuml.common import input_to_cuml_array from cuml.common.doc_utils import generate_docstring from cuml.internals import get_handle, reflect from cuml.internals.array import CumlArray -from cuml.internals.interop import UnsupportedOnGPU, to_cpu, to_gpu +from cuml.internals.interop import UnsupportedOnGPU from cuml.internals.mixins import FMajorInputTagMixin, RegressorMixin +from cuml.internals.validation import check_consistent_length, check_y from cuml.neighbors.nearest_neighbors import NeighborsBase from cuml.neighbors.weights import compute_weights @@ -142,13 +143,13 @@ class KNeighborsRegressor(RegressorMixin, FMajorInputTagMixin, NeighborsBase): def _attrs_from_cpu(self, model): return { - "_y": to_gpu(model._y, order="F", dtype=np.float32), + "_y": cp.asarray(model._y, dtype=np.float32, order="F"), **super()._attrs_from_cpu(model), } def _attrs_to_cpu(self, model): return { - "_y": to_cpu(self._y), + "_y": cp.asnumpy(self._y), **super()._attrs_to_cpu(model), } @@ -164,7 +165,7 @@ class KNeighborsRegressor(RegressorMixin, FMajorInputTagMixin, NeighborsBase): self.weights = weights @generate_docstring(convert_dtype_cast='np.float32') - @reflect(reset=True) + @reflect(reset="type") def fit(self, X, y, *, convert_dtype=True) -> "KNeighborsRegressor": """ Fit a GPU index for k-nearest neighbors regression model. @@ -176,13 +177,15 @@ class KNeighborsRegressor(RegressorMixin, FMajorInputTagMixin, NeighborsBase): ) super().fit(X, convert_dtype=convert_dtype) - self._y = input_to_cuml_array( + y = check_y( y, - order='F', - check_rows=self.n_samples_fit_, - check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None), - ).array + dtype="float32", + convert_dtype=convert_dtype, + order="F", + accept_multi_output=True, + ) + check_consistent_length(self._fit_X, y) + self._y = y return self @@ -203,28 +206,18 @@ class KNeighborsRegressor(RegressorMixin, FMajorInputTagMixin, NeighborsBase): X, return_distance=True, convert_dtype=convert_dtype ) - cdef size_t n_rows - inds, n_rows, _, _ = input_to_cuml_array( - knn_indices, - order='C', - check_dtype=np.int64, - convert_to_dtype=(np.int64 if convert_dtype else None), + inds_cp = cp.ascontiguousarray( + knn_indices.to_output("cupy"), dtype=np.int64 ) - - dists = input_to_cuml_array( - knn_distances, - order='C', - check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None), - ).array - - cdef int64_t* inds_ctype = inds.ptr + dists_cp = knn_distances.to_output("cupy") + cdef size_t n_rows = inds_cp.shape[0] + cdef int64_t* inds_ctype = inds_cp.data.ptr res_cols = 1 if self._y.ndim == 1 else self._y.shape[1] res_shape = n_rows if res_cols == 1 else (n_rows, res_cols) out = CumlArray.zeros( - res_shape, dtype=np.float32, order="C", index=inds.index + res_shape, dtype=np.float32, order="C", index=knn_indices.index ) cdef float* out_ptr = out.ptr @@ -233,14 +226,14 @@ class KNeighborsRegressor(RegressorMixin, FMajorInputTagMixin, NeighborsBase): cdef float* y_ptr for col_num in range(res_cols): col = self._y if res_cols == 1 else self._y[:, col_num] - y_ptr = col.ptr + y_ptr = col.data.ptr y_vec.push_back(y_ptr) handle = get_handle() cdef handle_t* handle_ = handle.getHandle() # Compute weights (returns None for uniform weights) - weights_cp = compute_weights(dists.to_output('cupy'), self.weights) + weights_cp = compute_weights(dists_cp, self.weights) cdef float* weights_ctype = ( 0 if weights_cp is None else weights_cp.data.ptr ) diff --git a/python/cuml/cuml/neighbors/nearest_neighbors.pyx b/python/cuml/cuml/neighbors/nearest_neighbors.pyx index 2376ca2058..43af59d66b 100644 --- a/python/cuml/cuml/neighbors/nearest_neighbors.pyx +++ b/python/cuml/cuml/neighbors/nearest_neighbors.pyx @@ -17,11 +17,15 @@ from cuml.common.sparse_utils import is_dense, is_sparse from cuml.internals.array import CumlArray from cuml.internals.array_sparse import SparseCumlArray from cuml.internals.base import Base, get_handle -from cuml.internals.input_utils import input_to_cuml_array from cuml.internals.interop import InteropMixin, UnsupportedOnGPU, to_gpu from cuml.internals.mixins import CMajorInputTagMixin, SparseInputTagMixin -from cuml.internals.outputs import reflect, using_output_type -from cuml.internals.validation import check_features, check_is_fitted +from cuml.internals.outputs import reflect +from cuml.internals.validation import ( + check_array, + check_features, + check_inputs, + check_is_fitted, +) from libc.stdint cimport int64_t, uint32_t, uintptr_t from libcpp cimport bool @@ -191,12 +195,8 @@ void swap_kernel(long long int* I, float* D, int n_rows, int n_cols) { ''', 'swap_kernel') -def _drop_self_edges(distances: CumlArray, indices: CumlArray): +def _drop_self_edges(distances_cp, indices_cp): """Drop edges between a point and itself in the knn graph""" - index = indices.index - distances_cp = distances.to_output('cupy') - indices_cp = indices.to_output('cupy') - rows, cols = indices_cp.shape # Launch config @@ -227,10 +227,7 @@ def _drop_self_edges(distances: CumlArray, indices: CumlArray): indices_cp = cp.ascontiguousarray(indices_cp[:, 1:], dtype=cp.int64) distances_cp = cp.ascontiguousarray(distances_cp[:, 1:], dtype=cp.float32) - distances = CumlArray(distances_cp, index=index) - indices = CumlArray(indices_cp, index=index) - - return distances, indices + return distances_cp, indices_cp METRICS = { @@ -284,7 +281,7 @@ cdef class RBCIndex: handle = get_handle() cdef handle_t* handle_ = handle.getHandle() - cdef float* X_ptr = X.ptr + cdef float* X_ptr = X.data.ptr cdef int64_t n_rows = X.shape[0] cdef int64_t n_cols = X.shape[1] cdef DistanceType distance_type = _metric_to_distance_type(metric) @@ -313,7 +310,7 @@ cdef class RBCIndex: cdef int64_t n_query = X.shape[0] indptr = cp.empty(n_query + 1, dtype=np.int64) - cdef float* X_ptr = X.ptr + cdef float* X_ptr = X.data.ptr cdef int64_t n_rows = X.shape[0] cdef int64_t n_cols = X.shape[1] cdef int64_t* indptr_ptr = indptr.data.ptr @@ -360,25 +357,16 @@ cdef class RBCIndex: raise ValueError( "The rbc algorithm is not supported for >3 dimensions currently." ) - distances = CumlArray.zeros( - (X.shape[0], n_neighbors), - dtype=np.float32, - order="C", - index=X.index, - ) - indices = CumlArray.zeros( - (X.shape[0], n_neighbors), - dtype=np.int64, - order="C", - index=X.index, - ) + distances_cp = cp.empty((X.shape[0], n_neighbors), dtype=np.float32, order="C") + indices_cp = cp.empty((X.shape[0], n_neighbors), dtype=np.int64, order="C") + handle = get_handle() cdef handle_t* handle_ = handle.getHandle() - cdef float* X_ptr = X.ptr + cdef float* X_ptr = X.data.ptr cdef uint32_t n_rows = X.shape[0] cdef int64_t n_cols = X.shape[1] - cdef int64_t* indices_ptr = indices.ptr - cdef float* distances_ptr = distances.ptr + cdef int64_t* indices_ptr = indices_cp.data.ptr + cdef float* distances_ptr = distances_cp.data.ptr with nogil: rbc_knn_query( @@ -392,7 +380,7 @@ cdef class RBCIndex: distances_ptr, ) handle.sync() - return distances, indices + return distances_cp, indices_cp cdef class ApproxIndex: @@ -446,7 +434,7 @@ cdef class ApproxIndex: handle = get_handle() cdef DistanceType distance_type = _metric_to_distance_type(metric) cdef handle_t* handle_ = handle.getHandle() - cdef float* X_ptr = X.ptr + cdef float* X_ptr = X.data.ptr cdef int n_rows = X.shape[0] cdef int n_cols = X.shape[1] @@ -468,24 +456,14 @@ cdef class ApproxIndex: def kneighbors(ApproxIndex self, X, int n_neighbors): """Query the index for the k nearest neighbors.""" - distances = CumlArray.zeros( - (X.shape[0], n_neighbors), - dtype=np.float32, - order="C", - index=X.index, - ) - indices = CumlArray.zeros( - (X.shape[0], n_neighbors), - dtype=np.int64, - order="C", - index=X.index, - ) + distances_cp = cp.empty((X.shape[0], n_neighbors), dtype=np.float32, order="C") + indices_cp = cp.empty((X.shape[0], n_neighbors), dtype=np.int64, order="C") handle = get_handle() cdef handle_t* handle_ = handle.getHandle() - cdef float* distances_ptr = distances.ptr - cdef int64_t* indices_ptr = indices.ptr - cdef float* X_ptr = X.ptr + cdef float* distances_ptr = distances_cp.data.ptr + cdef int64_t* indices_ptr = indices_cp.data.ptr + cdef float* X_ptr = X.data.ptr cdef int n_rows = X.shape[0] with nogil: @@ -499,7 +477,7 @@ cdef class ApproxIndex: n_rows, ) handle.sync() - return distances, indices + return distances_cp, indices_cp class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): @@ -547,9 +525,12 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin def _attrs_from_cpu(self, model): if scipy.sparse.issparse(model._fit_X): fit_X = SparseCumlArray( - model._fit_X, - convert_to_dtype=np.float32, - convert_format=True + check_array( + model._fit_X, + dtype="float32", + accept_sparse=["csr"], + convert_dtype=True, + ) ) else: fit_X = to_gpu(model._fit_X, order="C", dtype=np.float32) @@ -614,13 +595,13 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin if (fit_method := state.get("_fit_method")) in ("rbc", "ivfpq", "ivfflat"): # TODO: These index types currently aren't pickleable. For now we # recreate them on load. - with using_output_type("cuml"): - X = getattr(self, "_fit_X", None) + fit_X = getattr(self, "_fit_X", None) + X_cp = cp.asarray(fit_X) if fit_X is not None else None if fit_method == "rbc": - self._index = RBCIndex.build(X, self.effective_metric_) + self._index = RBCIndex.build(X_cp, self.effective_metric_) else: self._index = ApproxIndex.build( - X, + X_cp, self.effective_metric_, fit_method, params=self.algo_params, @@ -628,25 +609,33 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin ) @generate_docstring(X='dense_sparse') - @reflect(reset=True) + @reflect(reset="type") def fit(self, X, y=None, *, convert_dtype=True) -> "NearestNeighbors": """ Fit GPU index for performing nearest neighbor queries. """ sparse = is_sparse(X) + valid_metrics = ( + cuml.neighbors.VALID_METRICS_SPARSE if sparse else cuml.neighbors.VALID_METRICS + ) + X_processed, index = check_inputs( + self, + X, + dtype="float32", + accept_sparse=["csr"], + convert_dtype=convert_dtype, + order="C", + return_index=True, + reset=True, + ) if sparse: - valid_metrics = cuml.neighbors.VALID_METRICS_SPARSE - self._fit_X = SparseCumlArray(X, convert_to_dtype=cp.float32) + self._fit_X = SparseCumlArray(X_processed) + X_cp = None else: - valid_metrics = cuml.neighbors.VALID_METRICS - self._fit_X, _, _, _ = input_to_cuml_array( - X, - order='C', - check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None), - ) + X_cp = X_processed + self._fit_X = CumlArray(X_cp, index=index) # Normalize metric, and simplify for common cases self.effective_metric_ = self.metric @@ -665,7 +654,7 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin else: self.effective_metric_params_["p"] = p - self.n_samples_fit_, self.n_features_in_ = self._fit_X.shape + self.n_samples_fit_ = self._fit_X.shape[0] if self.algorithm == "auto": if ( @@ -696,14 +685,14 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin if self._fit_method in ('ivfflat', 'ivfpq'): self._index = ApproxIndex.build( - self._fit_X, + X_cp, self.effective_metric_, self._fit_method, params=self.algo_params, p=self._effective_p, ) elif self._fit_method == "rbc": - self._index = RBCIndex.build(self._fit_X, self.effective_metric_) + self._index = RBCIndex.build(X_cp, self.effective_metric_) return self @@ -772,7 +761,10 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin n_neighbors = self.n_neighbors if n_neighbors is None else n_neighbors if use_training_data := (X is None): - X = self._fit_X + if isinstance(self._fit_X, SparseCumlArray): + X = self._fit_X.to_output("cupy") + else: + X = self._fit_X n_neighbors += 1 else: check_features(self, X) @@ -784,15 +776,18 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin raise ValueError("n_neighbors must be <= number of samples in index") if isinstance(self._fit_X, SparseCumlArray): - distances, indices = self._kneighbors_sparse(X, n_neighbors) + distances_cp, indices_cp = self._kneighbors_sparse(X, n_neighbors) + index = None else: - distances, indices = self._kneighbors_dense( + distances_cp, indices_cp, index = self._kneighbors_dense( X, n_neighbors, convert_dtype, two_pass_precision ) if use_training_data: - distances, indices = _drop_self_edges(distances, indices) + distances_cp, indices_cp = _drop_self_edges(distances_cp, indices_cp) + distances = CumlArray(distances_cp, index=index) + indices = CumlArray(indices_cp, index=index) return (distances, indices) if return_distance else indices def _kneighbors_dense( @@ -802,14 +797,18 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin raise ValueError("A NearestNeighbors model trained on dense " "data requires dense input to kneighbors()") - cdef int n_rows, n_cols - X_m, n_rows, n_cols, _ = input_to_cuml_array( + X_cp, index = check_array( X, + dtype="float32", + convert_dtype=convert_dtype, order="C", - check_dtype=np.float32, - check_cols=self.n_features_in_, - convert_to_dtype=(np.float32 if convert_dtype else False), + return_index=True, + input_name="X", ) + if index is None: # Special case if X is a CumlArray (self._fit_X forwarded) + index = getattr(X, "index", None) + cdef int n_rows = X_cp.shape[0] + cdef int n_cols = X_cp.shape[1] use_index = self._fit_method != "brute" if self._fit_method == "rbc" and n_neighbors > self.n_samples_fit_**0.5: @@ -821,14 +820,11 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin use_index = False if use_index: - return self._index.kneighbors(X_m, n_neighbors) + distances_cp, indices_cp = self._index.kneighbors(X_cp, n_neighbors) + return distances_cp, indices_cp, index - distances = CumlArray.zeros( - (X_m.shape[0], n_neighbors), dtype=np.float32, order="C", index=X_m.index, - ) - indices = CumlArray.zeros( - (X_m.shape[0], n_neighbors), dtype=np.int64, order="C", index=X_m.index, - ) + distances_cp = cp.empty((n_rows, n_neighbors), dtype=np.float32, order="C") + indices_cp = cp.empty((n_rows, n_neighbors), dtype=np.int64, order="C") handle = get_handle() cdef handle_t* handle_ = handle.getHandle() @@ -837,9 +833,9 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin inputs.push_back(self._fit_X.ptr) sizes.push_back(self.n_samples_fit_) cdef DistanceType distance_type = _metric_to_distance_type(self.effective_metric_) - cdef float* X_ptr = X_m.ptr - cdef int64_t* indices_ptr = indices.ptr - cdef float* distances_ptr = distances.ptr + cdef float* X_ptr = X_cp.data.ptr + cdef int64_t* indices_ptr = indices_cp.data.ptr + cdef float* distances_ptr = distances_cp.data.ptr cdef float metric_arg = self._effective_p with nogil: @@ -861,11 +857,11 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin handle.sync() if two_pass_precision: - distances, indices = self._maybe_apply_two_pass_precision( - X_m, distances, indices + distances_cp, indices_cp = self._maybe_apply_two_pass_precision( + X_cp, distances_cp, indices_cp ) - return distances, indices + return distances_cp, indices_cp, index def _maybe_apply_two_pass_precision(self, X, distances, indices): # FAISS employs imprecise distance algorithm only for L2-based @@ -880,27 +876,19 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin # Nothing to do return distances, indices - index = indices.index - X_cp = X.to_output("cupy", output_dtype=cp.float32) - indices_cp = indices.to_output('cupy') + self_diff = X[indices] - X[:, cp.newaxis, :] + distances = cp.sum(self_diff * self_diff, axis=2) + correct_order = cp.argsort(distances, axis=1) - self_diff = X_cp[indices_cp] - X_cp[:, cp.newaxis, :] - distances_cp = cp.sum(self_diff * self_diff, axis=2) - correct_order = cp.argsort(distances_cp, axis=1) + distances = cp.take_along_axis(distances, correct_order, axis=1) + indices = cp.take_along_axis(indices, correct_order, axis=1) - distances_cp = cp.take_along_axis(distances_cp, correct_order, axis=1) - indices_cp = cp.take_along_axis(indices_cp, correct_order, axis=1) - - distances = CumlArray( - data=cp.ascontiguousarray(distances_cp, dtype=cp.float32), index=index - ) - indices = CumlArray( - data=cp.ascontiguousarray(indices_cp, dtype=cp.int64), index=index - ) + distances = cp.ascontiguousarray(distances, dtype=cp.float32) + indices = cp.ascontiguousarray(indices, dtype=cp.int64) return distances, indices def _kneighbors_sparse(self, X, int n_neighbors): - if isinstance(self._fit_X, SparseCumlArray) and not is_sparse(X): + if not is_sparse(X): raise ValueError("A NearestNeighbors model trained on sparse " "data requires sparse input to kneighbors()") @@ -911,16 +899,20 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin cdef DistanceType metric = _metric_to_distance_type(self.effective_metric_) cdef float metric_arg = self._effective_p - # Extract query input components - X_m = SparseCumlArray(X, convert_to_dtype=cp.float32) - cdef int* X_indptr = X_m.indptr.ptr - cdef int* X_indices = X_m.indices.ptr - cdef float* X_data = X_m.data.ptr - cdef size_t X_nnz = X_m.nnz - cdef int X_n_rows = X_m.shape[0] - cdef int X_n_cols = X_m.shape[1] + X_cp = check_array( + X, + dtype="float32", + accept_sparse=["csr"], + convert_dtype=True, + input_name="X", + ) + cdef int* X_indptr = X_cp.indptr.data.ptr + cdef int* X_indices = X_cp.indices.data.ptr + cdef float* X_data = X_cp.data.data.ptr + cdef size_t X_nnz = X_cp.nnz + cdef int X_n_rows = X_cp.shape[0] + cdef int X_n_cols = X_cp.shape[1] - # Extract index components cdef int* idx_indptr = self._fit_X.indptr.ptr cdef int* idx_indices = self._fit_X.indices.ptr cdef float* idx_data = self._fit_X.data.ptr @@ -929,14 +921,10 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin cdef int idx_n_cols = self._fit_X.shape[1] # Allocate outputs - indices = CumlArray.zeros( - (X_m.shape[0], n_neighbors), dtype=np.int32, order="C" - ) - distances = CumlArray.zeros( - (X_m.shape[0], n_neighbors), dtype=np.float32, order="C" - ) - cdef int* indices_ptr = indices.ptr - cdef float* distances_ptr = distances.ptr + indices_cp = cp.empty((X_n_rows, n_neighbors), dtype=np.int32, order="C") + distances_cp = cp.empty((X_n_rows, n_neighbors), dtype=np.float32, order="C") + cdef int* indices_ptr = indices_cp.data.ptr + cdef float* distances_ptr = distances_cp.data.ptr handle = get_handle() cdef handle_t* handle_ = handle.getHandle() @@ -965,7 +953,7 @@ class NeighborsBase(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin ) handle.sync() - return distances, indices + return distances_cp, indices_cp @insert_into_docstring(parameters=[('dense', '(n_samples, n_features)')]) @reflect @@ -1278,22 +1266,24 @@ class NearestNeighbors(NeighborsBase): else: check_features(self, X) - X_m = input_to_cuml_array( + X_cp = check_array( X, + dtype="float32", + convert_dtype=True, order="C", - check_dtype=np.float32, - check_cols=self.n_features_in_, - convert_to_dtype=np.float32, - ).array + input_name="X", + ) if hasattr(self, "_index") and isinstance(self._index, RBCIndex): # Already fit with RBC, reuse the index index = self._index else: # Fit with another method, build a temporary index - index = RBCIndex.build(self._fit_X, self.effective_metric_) + index = RBCIndex.build( + self._fit_X.to_output("cupy"), self.effective_metric_ + ) - out = index.radius_neighbors_graph(X_m, radius) + out = index.radius_neighbors_graph(X_cp, radius) if using_fit_X: # When using the training data, the diagonal elements aren't included out.setdiag(np.int64(0)) 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 dd63ea8a9a..9abb55b2d3 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 @@ -970,22 +970,7 @@ - reason: test_estimators checks fail marker: cuml_accel_test_estimators tests: - - "sklearn.tests.test_common::test_estimators[KNeighborsClassifier()-check_classifier_data_not_an_array]" - - "sklearn.tests.test_common::test_estimators[KNeighborsClassifier()-check_dtype_object]" - - "sklearn.tests.test_common::test_estimators[KNeighborsClassifier()-check_estimators_empty_data_messages]" - - "sklearn.tests.test_common::test_estimators[KNeighborsClassifier()-check_estimators_nan_inf]" - - "sklearn.tests.test_common::test_estimators[KNeighborsRegressor()-check_dtype_object]" - - "sklearn.tests.test_common::test_estimators[KNeighborsRegressor()-check_estimators_empty_data_messages]" - - "sklearn.tests.test_common::test_estimators[KNeighborsRegressor()-check_estimators_nan_inf]" - - "sklearn.tests.test_common::test_estimators[KNeighborsRegressor()-check_regressor_data_not_an_array]" - - "sklearn.tests.test_common::test_estimators[KNeighborsRegressor()-check_requires_y_none]" - "sklearn.tests.test_common::test_estimators[KNeighborsRegressor()-check_supervised_y_no_nan]" - - "sklearn.tests.test_common::test_estimators[KernelDensity()-check_dtype_object]" - - "sklearn.tests.test_common::test_estimators[KernelDensity()-check_estimators_nan_inf]" - - "sklearn.tests.test_common::test_estimators[KernelDensity()-check_sample_weights_not_an_array]" - - "sklearn.tests.test_common::test_estimators[NearestNeighbors()-check_dtype_object]" - - "sklearn.tests.test_common::test_estimators[NearestNeighbors()-check_estimators_empty_data_messages]" - - "sklearn.tests.test_common::test_estimators[NearestNeighbors()-check_estimators_nan_inf]" - "sklearn.tests.test_common::test_estimators[PCA()-check_fit2d_1feature]" - "sklearn.tests.test_common::test_estimators[PCA()-check_fit2d_1sample]" - "sklearn.tests.test_common::test_estimators[RandomForestClassifier()-check_classifiers_multilabel_output_format_decision_function]" diff --git a/python/cuml/tests/test_kernel_density.py b/python/cuml/tests/test_kernel_density.py index 59a37cfe81..93c48a849d 100644 --- a/python/cuml/tests/test_kernel_density.py +++ b/python/cuml/tests/test_kernel_density.py @@ -226,10 +226,13 @@ def test_bad_sample_weight_errors(): kde = KernelDensity() X = np.array([[0.0, 1.0], [2.0, 0.5]]) - with pytest.raises(ValueError, match="Expected 2 rows but got 3 rows."): + with pytest.raises( + ValueError, + match="inconsistent number of samples", + ): kde.fit(X, sample_weight=np.array([1, 2, 3])) with pytest.raises( - ValueError, match="Expected 1 columns but got 2 columns." + ValueError, match="Sample weights must be 1D array or scalar" ): kde.fit(X, sample_weight=np.array([[1, 2], [3, 4]])) diff --git a/python/cuml/tests/test_sklearn_compatibility.py b/python/cuml/tests/test_sklearn_compatibility.py index d271b4e5ed..d5936ae322 100644 --- a/python/cuml/tests/test_sklearn_compatibility.py +++ b/python/cuml/tests/test_sklearn_compatibility.py @@ -130,31 +130,16 @@ KNeighborsClassifier: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", "check_do_not_raise_errors_in_init_or_set_params": "KNeighborsClassifier raises errors in init or set_params", - "check_dtype_object": "KNeighborsClassifier does not handle object dtype", - "check_estimators_empty_data_messages": "KNeighborsClassifier does not handle empty data", - "check_estimators_nan_inf": "KNeighborsClassifier does not check for NaN and inf", "check_classifier_data_not_an_array": "KNeighborsClassifier does not handle non-array data", - "check_classifiers_train": "KNeighborsClassifier does not validate input data properly", }, KNeighborsRegressor: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", "check_do_not_raise_errors_in_init_or_set_params": "KNeighborsRegressor raises errors in init or set_params", - "check_dtype_object": "KNeighborsRegressor does not handle object dtype", - "check_estimators_empty_data_messages": "KNeighborsRegressor does not handle empty data", - "check_estimators_nan_inf": "KNeighborsRegressor does not check for NaN and inf", - "check_regressors_train": "KNeighborsRegressor does not handle list inputs", - "check_regressors_train(readonly_memmap=True)": "KNeighborsRegressor does not handle readonly memmap", - "check_regressors_train(readonly_memmap=True,X_dtype=float32)": "KNeighborsRegressor does not handle readonly memmap with float32", "check_regressor_data_not_an_array": "KNeighborsRegressor does not handle non-array data", "check_supervised_y_2d": "KNeighborsRegressor does not handle 2D y", - "check_supervised_y_no_nan": "KNeighborsRegressor does not check for NaN in y", - "check_requires_y_none": "KNeighborsRegressor does not handle y=None", }, NearestNeighbors: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_dtype_object": "NearestNeighbors does not handle object dtype", - "check_estimators_empty_data_messages": "NearestNeighbors does not handle empty data", - "check_estimators_nan_inf": "NearestNeighbors does not check for NaN and inf", }, LinearSVC: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", @@ -227,11 +212,7 @@ }, KernelDensity: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_sample_weights_not_an_array": "KernelDensity does not handle non-array sample weights", - "check_sample_weights_list": "KernelDensity does not handle list sample weights", "check_all_zero_sample_weights_error": "KernelDensity does not validate all-zero sample weights", - "check_dtype_object": "KernelDensity does not handle object dtype", - "check_estimators_nan_inf": "KernelDensity does not check for NaN and inf", }, LedoitWolf: { "check_estimator_tags_renamed": "No support for modern tags infrastructure",