diff --git a/python/cuml/cuml/manifold/spectral_embedding.pyx b/python/cuml/cuml/manifold/spectral_embedding.pyx index 69589e87a5..9c8118ad51 100644 --- a/python/cuml/cuml/manifold/spectral_embedding.pyx +++ b/python/cuml/cuml/manifold/spectral_embedding.pyx @@ -19,7 +19,7 @@ from cuml.internals.interop import ( ) from cuml.internals.mixins import CMajorInputTagMixin from cuml.internals.outputs import reflect -from cuml.internals.validation import check_random_seed +from cuml.internals.validation import check_inputs, check_random_seed from libc.stdint cimport int64_t, uint64_t, uintptr_t from libcpp cimport bool @@ -399,7 +399,7 @@ class SpectralEmbedding(Base, InteropMixin, CMajorInputTagMixin): self.fit(X, y) return self.embedding_ - @reflect(reset=True) + @reflect(reset="type") def fit(self, X, y=None) -> "SpectralEmbedding": """Fit the model from data in X. @@ -420,6 +420,13 @@ class SpectralEmbedding(Base, InteropMixin, CMajorInputTagMixin): self : object Returns the instance itself. """ + X = check_inputs( + self, + X, + dtype="float32", + accept_sparse=(self.affinity == "precomputed"), + reset=True, + ) # Store n_neighbors_ for sklearn compatibility self.n_neighbors_ = ( diff --git a/python/cuml/cuml/manifold/t_sne.pyx b/python/cuml/cuml/manifold/t_sne.pyx index 660a5e874b..a5c36f8787 100644 --- a/python/cuml/cuml/manifold/t_sne.pyx +++ b/python/cuml/cuml/manifold/t_sne.pyx @@ -7,13 +7,11 @@ import numpy as np import sklearn from packaging.version import Version -from cuml.common import input_to_cuml_array from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.common.doc_utils import generate_docstring from cuml.common.sparse_utils import is_sparse from cuml.common.sparsefuncs import extract_knn_graph from cuml.internals.array import CumlArray -from cuml.internals.array_sparse import SparseCumlArray from cuml.internals.base import Base, get_handle from cuml.internals.interop import ( InteropMixin, @@ -23,7 +21,7 @@ from cuml.internals.interop import ( ) from cuml.internals.mixins import CMajorInputTagMixin, SparseInputTagMixin from cuml.internals.outputs import reflect -from cuml.internals.validation import check_random_seed +from cuml.internals.validation import check_inputs, check_random_seed from libc.stdint cimport int64_t, uintptr_t from libcpp cimport bool @@ -565,7 +563,7 @@ class TSNE(Base, @generate_docstring(skip_parameters_heading=True, X='dense_sparse', convert_dtype_cast='np.float32') - @reflect(reset=True) + @reflect(reset="type") def fit(self, X, y=None, *, convert_dtype=True, knn_graph=None) -> "TSNE": """ Fit X into an embedded space. @@ -589,20 +587,26 @@ class TSNE(Base, cdef int X_nnz = 0 cdef bool sparse_fit = is_sparse(X) - # Normalize input X + X, index = check_inputs( + self, + X, + dtype="float32", + convert_dtype=convert_dtype, + order="F", + accept_sparse="csr", + reset=True, + return_index=True, + ) + if sparse_fit: - X_m = SparseCumlArray(X, convert_to_dtype=cupy.float32) - n_samples, n_features = X_m.shape - X_ptr = X_m.data.ptr - X_indptr_ptr = X_m.indptr.ptr - X_indices_ptr = X_m.indices.ptr - X_nnz = X_m.nnz + n_samples, n_features = X.shape + X_ptr = X.data.data.ptr + X_indptr_ptr = X.indptr.data.ptr + X_indices_ptr = X.indices.data.ptr + X_nnz = X.nnz else: - X_m, n_samples, n_features, _ = input_to_cuml_array( - X, order='F', check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None) - ) - X_ptr = X_m.ptr + n_samples, n_features = X.shape + X_ptr = X.data.ptr # Initialize TSNEParams cdef TSNEParams params @@ -616,23 +620,26 @@ class TSNE(Base, if knn_graph is not None: knn_indices, knn_dists = extract_knn_graph(knn_graph, params.n_neighbors) + knn_dists_cp = knn_dists.to_output("cupy") + if sparse_fit: # Sparse fitting requires the indices to be int32 - knn_indices = input_to_cuml_array( - knn_indices, convert_to_dtype=np.int32 - ).array + knn_indices_cp = cupy.asarray( + knn_indices.to_output("cupy"), dtype=np.int32 + ) + else: + knn_indices_cp = knn_indices.to_output("cupy") - knn_dists_ptr = knn_dists.ptr - knn_indices_ptr = knn_indices.ptr + knn_dists_ptr = knn_dists_cp.data.ptr + knn_indices_ptr = knn_indices_cp.data.ptr # Allocate output array - embedding = CumlArray.zeros( + embedding = cupy.zeros( (n_samples, self.n_components), order="F", dtype=np.float32, - index=X_m.index, ) - cdef uintptr_t embed_ptr = embedding.ptr + cdef uintptr_t embed_ptr = embedding.data.ptr # Execute fit handle = get_handle() @@ -676,7 +683,7 @@ class TSNE(Base, self._kl_divergence_ = kl_divergence self.n_iter_ = n_iter self.learning_rate_ = params.pre_learning_rate - self.embedding_ = embedding + self.embedding_ = CumlArray(data=embedding, index=index) return self diff --git a/python/cuml/cuml/manifold/umap/umap.pyx b/python/cuml/cuml/manifold/umap/umap.pyx index bf3ca6d293..85b6385301 100644 --- a/python/cuml/cuml/manifold/umap/umap.pyx +++ b/python/cuml/cuml/manifold/umap/umap.pyx @@ -28,10 +28,10 @@ from cuml.internals.interop import ( to_cpu, to_gpu, ) -from cuml.internals.mem_type import MemoryType from cuml.internals.mixins import CMajorInputTagMixin, SparseInputTagMixin from cuml.internals.validation import ( - check_features, + check_array, + check_inputs, check_is_fitted, check_random_seed, ) @@ -1158,7 +1158,7 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): X="dense_sparse", skip_parameters_heading=True, ) - @reflect(reset=True) + @reflect(reset="type") def fit(self, X, y=None, *, convert_dtype=True, knn_graph=None) -> "UMAP": """ Fit X into an embedded space. @@ -1183,65 +1183,66 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): cdef int n_rows = X.shape[0] cdef int n_dims = X.shape[1] - if n_rows < 2: - raise ValueError( - f"Found an array with {n_rows} sample(s) (shape={X.shape}) " - f"while a minimum of 2 is required." - ) - if n_dims < 1: - raise ValueError( - f"Found an array with 0 feature(s) (shape={X.shape}) " - f"while a minimum of 1 is required." - ) - cdef bool X_is_sparse = is_sparse(X) cdef lib.UMAPParams params init_params(self, params, n_rows=n_rows, is_sparse=X_is_sparse) - cdef uintptr_t X_ptr = 0, X_indices_ptr = 0, X_indptr_ptr = 0 - cdef size_t X_nnz = 0 + # Don't coerce to device memory for dense case when using a precomputed + # KNN, so that X may be dropped earlier if passed on host. + if knn_graph is None and self.precomputed_knn is None: + base_mem_type = "device" + else: + base_mem_type = None - # Don't coerce to device memory when using a precomputed KNN, so - # that X may be dropped earlier if passed on host. - mem_type = ( - MemoryType.device - if knn_graph is None and self.precomputed_knn is None - else False + if X_is_sparse: + mem_type = base_mem_type + elif ( + params.build_algo == lib.graph_build_algo.NN_DESCENT + or ( + params.build_algo == lib.graph_build_algo.BRUTE_FORCE_KNN + and params.build_params.n_clusters > 1 + ) + ): + mem_type = "host" + else: + mem_type = base_mem_type + + check_kwargs = dict( + dtype="float32", + y_dtype="float32", + convert_dtype=convert_dtype, + order="C", + accept_sparse="csr", + mem_type=mem_type, + reset=True, + return_index=True, + ensure_min_samples=2, ) + if y is not None: + X, y, index = check_inputs(self, X, y, **check_kwargs) + # `y` needs to be on GPU but `check_inputs` doesn't have separate + # mem_type handling for X and y. + y = cp.asarray(y) + else: + X, index = check_inputs(self, X, **check_kwargs) + + cdef uintptr_t X_ptr = 0, X_indices_ptr = 0, X_indptr_ptr = 0 + cdef size_t X_nnz = 0 if X_is_sparse: - X_m = SparseCumlArray(X, convert_to_dtype=cp.float32, convert_to_mem_type=mem_type) + X_m = SparseCumlArray(X) X_ptr = X_m.data.ptr X_indices_ptr = X_m.indices.ptr X_indptr_ptr = X_m.indptr.ptr X_nnz = X_m.nnz else: - X_m = input_to_cuml_array( - X, - order="C", - check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None), - convert_to_mem_type=( - MemoryType.host - if params.build_algo == lib.graph_build_algo.NN_DESCENT or - (params.build_algo == lib.graph_build_algo.BRUTE_FORCE_KNN - and params.build_params.n_clusters > 1) - else mem_type - ) - ).array + X_m = CumlArray(data=X, index=index) X_ptr = X_m.ptr cdef uintptr_t y_ptr = 0 if y is not None: - y_m = input_to_cuml_array( - y, - check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None), - check_rows=n_rows, - check_cols=1, - ).array - y_ptr = y_m.ptr + y_ptr = y.data.ptr cdef uintptr_t knn_dists_ptr = 0, knn_indices_ptr = 0 if knn_graph is not None or self.precomputed_knn is not None: @@ -1256,12 +1257,17 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): self._n_neighbors, mem_type=False, # mirrors the input graph mem type ) + knn_dists_cp = knn_dists.to_output("cupy") if X_is_sparse: - knn_indices = input_to_cuml_array( - knn_indices, convert_to_dtype=np.int32 - ).array - knn_indices_ptr = knn_indices.ptr - knn_dists_ptr = knn_dists.ptr + knn_indices_cp = cp.asarray( + knn_indices.to_output("cupy"), dtype=np.int32 + ) + # Drop the int64 original and keep only the int32 copy used by the kernel. + knn_indices = CumlArray(data=knn_indices_cp) + else: + knn_indices_cp = knn_indices.to_output("cupy") + knn_indices_ptr = knn_indices_cp.data.ptr + knn_dists_ptr = knn_dists_cp.data.ptr else: knn_indices = knn_dists = None @@ -1296,13 +1302,13 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): cdef uintptr_t sigmas_ptr = 0 cdef uintptr_t rhos_ptr = 0 if not X_is_sparse: - sigmas_arr = CumlArray.zeros(n_rows, dtype=np.float32, order="C") - rhos_arr = CumlArray.zeros(n_rows, dtype=np.float32, order="C") - sigmas_ptr = sigmas_arr.ptr - rhos_ptr = rhos_arr.ptr + sigmas_cp = cp.zeros(n_rows, dtype=np.float32) + rhos_cp = cp.zeros(n_rows, dtype=np.float32) + sigmas_ptr = sigmas_cp.data.ptr + rhos_ptr = rhos_cp.data.ptr else: - sigmas_arr = None - rhos_arr = None + sigmas_cp = None + rhos_cp = None with nogil: if X_is_sparse: @@ -1347,15 +1353,19 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): ), order="C" ) - self.embedding_ = CumlArray(data=embedding, index=X_m.index) + self.embedding_ = CumlArray(data=embedding, index=index) self.graph_ = copy_raft_host_coo_to_scipy_coo(fss_graph) self._raw_data = X_m self._sparse_data = X_is_sparse self._supervised = y is not None self._knn_indices = knn_indices self._knn_dists = knn_dists - self._sigmas = sigmas_arr - self._rhos = rhos_arr + self._sigmas = ( + CumlArray(data=sigmas_cp) if sigmas_cp is not None else None + ) + self._rhos = ( + CumlArray(data=rhos_cp) if rhos_cp is not None else None + ) if self.hash_input: self._input_hash = _joblib_hash(X_m.to_output("numpy")) @@ -1428,86 +1438,71 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): https://github.com/lmcinnes/umap/issues/158 """ check_is_fitted(self) - check_features(self, X) - if len(X.shape) != 2: - raise ValueError("Reshape your data: X should be two dimensional") + X, index = check_inputs( + self, + X, + dtype="float32", + convert_dtype=convert_dtype, + order="C", + accept_sparse="csr", + return_index=True, + ) - if is_sparse(X): - X = SparseCumlArray(X, convert_to_dtype=cp.float32) - index = None - else: - X = input_to_cuml_array( - X, - order="C", - check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None), - ).array - index = X.index + X_input_sparse = is_sparse(X) - if self._sparse_data and not isinstance(X, SparseCumlArray): + if self.hash_input: + if X_input_sparse: + X_for_hash = X.get() + else: + X_for_hash = cp.asnumpy(X) + if _joblib_hash(X_for_hash) == self._input_hash: + return self.embedding_ + + if self._sparse_data and not X_input_sparse: logger.warn( "Model was trained on sparse data but dense data was provided to " "transform(). Converting to sparse." ) - X = SparseCumlArray( - cupyx.scipy.sparse.csr_matrix(X.to_output("cupy")), - convert_to_dtype=cp.float32 - ) - elif not self._sparse_data and isinstance(X, SparseCumlArray): + X = cupyx.scipy.sparse.csr_matrix(X) + elif not self._sparse_data and X_input_sparse: logger.warn( "Model was trained on dense data but sparse data was provided to " "transform(). Converting to dense." ) - X = input_to_cuml_array( - X.to_output("cupy").todense(), - order="C", - check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None), - ).array + X = cp.ascontiguousarray(X.toarray()) cdef bool X_is_sparse = self._sparse_data cdef int n_rows = X.shape[0] cdef int n_cols = X.shape[1] cdef int orig_n_rows = self._raw_data.shape[0] - if n_cols != self.n_features_in_: - raise ValueError( - f"X has {n_cols} features, but UMAP is expecting " - f"{self.n_features_in_} features as input" - ) - - if self.hash_input: - if _joblib_hash(X.to_output("numpy")) == self._input_hash: - return self.embedding_ - cdef lib.UMAPParams params init_params(self, params, n_rows=n_rows, is_sparse=X_is_sparse, is_fit=False) - out = CumlArray.zeros( + out = cp.zeros( (n_rows, self.n_components), order="C", dtype=np.float32, - index=index ) cdef uintptr_t X_ptr, X_indptr_ptr, X_indices_ptr cdef uintptr_t orig_ptr, orig_indptr_ptr, orig_indices_ptr cdef size_t X_nnz, orig_nnz if X_is_sparse: - X_indptr_ptr = X.indptr.ptr - X_indices_ptr = X.indices.ptr - X_ptr = X.data.ptr + X_indptr_ptr = X.indptr.data.ptr + X_indices_ptr = X.indices.data.ptr + X_ptr = X.data.data.ptr X_nnz = X.nnz orig_indptr_ptr = self._raw_data.indptr.ptr orig_indices_ptr = self._raw_data.indices.ptr orig_ptr = self._raw_data.data.ptr orig_nnz = self._raw_data.nnz else: - X_ptr = X.ptr + X_ptr = X.data.ptr orig_ptr = self._raw_data.ptr - cdef uintptr_t out_ptr = out.ptr + cdef uintptr_t out_ptr = out.data.ptr cdef uintptr_t embedding_ptr = self.embedding_.ptr handle = get_handle(device_ids=self.device_ids) cdef handle_t* handle_ = handle.getHandle() @@ -1547,7 +1542,7 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): ) handle.sync() - return out + return CumlArray(data=out, index=index) @generate_docstring( convert_dtype_cast="np.float32", @@ -1577,13 +1572,14 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): " autoencoder." ) - X = input_to_cuml_array( + # skip n_features_in_ validation + X, index = check_array( X, + dtype="float32", + convert_dtype=convert_dtype, order="C", - check_dtype=np.float32, - convert_to_dtype=(np.float32 if convert_dtype else None), - ).array - index = X.index + return_index=True, + ) n_samples = X.shape[0] if X.shape[1] != self.n_components: @@ -1594,7 +1590,7 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): # Get numpy arrays for preprocessing embedding_np = self.embedding_.to_output("numpy") - X_np = X.to_output("numpy") + X_np = cp.asnumpy(X) raw_data_np = self._raw_data.to_output("numpy") # Phase 1: Compute neighborhoods via Delaunay triangulation + BFS (CPU) 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 c61010e69c..d59eb10e42 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 @@ -971,11 +971,6 @@ - "sklearn.tests.test_common::test_estimators[PCA()-check_fit2d_1sample]" - "sklearn.tests.test_common::test_estimators[RandomForestClassifier()-check_classifiers_multilabel_output_format_decision_function]" - "sklearn.tests.test_common::test_estimators[RandomForestRegressor()-check_regressor_data_not_an_array]" - - "sklearn.tests.test_common::test_estimators[SpectralEmbedding()-check_dtype_object]" - - "sklearn.tests.test_common::test_estimators[SpectralEmbedding()-check_estimators_nan_inf]" - - "sklearn.tests.test_common::test_estimators[TSNE()-check_dtype_object]" - - "sklearn.tests.test_common::test_estimators[TSNE()-check_estimators_empty_data_messages]" - - "sklearn.tests.test_common::test_estimators[TSNE()-check_estimators_nan_inf]" - "sklearn.tests.test_common::test_estimators[TruncatedSVD()-check_fit2d_1feature]" - "sklearn.tests.test_common::test_estimators[TruncatedSVD()-check_fit2d_1sample]" - reason: test_estimators checks fail diff --git a/python/cuml/tests/test_sklearn_compatibility.py b/python/cuml/tests/test_sklearn_compatibility.py index 252b6bc12e..939784ec22 100644 --- a/python/cuml/tests/test_sklearn_compatibility.py +++ b/python/cuml/tests/test_sklearn_compatibility.py @@ -182,10 +182,7 @@ TSNE: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", "check_dont_overwrite_parameters": "TSNE overwrites parameters during fit", - "check_dtype_object": "TSNE does not handle object dtype", - "check_estimators_empty_data_messages": "TSNE does not handle empty data", "check_pipeline_consistency": "TSNE results are not deterministic", - "check_estimators_nan_inf": "TSNE does not check for NaN and inf", "check_methods_sample_order_invariance": "TSNE results depend on sample order", "check_methods_subset_invariance": "TSNE results depend on data subset", "check_fit2d_1sample": "TSNE does not handle single sample", @@ -194,7 +191,6 @@ }, UMAP: { "check_estimator_tags_renamed": "No support for modern tags infrastructure", - "check_dtype_object": "UMAP does not handle object dtype", "check_transformer_data_not_an_array": "UMAP does not handle non-array data", "check_methods_sample_order_invariance": "UMAP results depend on sample order", "check_transformer_general": "UMAP does not have consistent fit_transform and transform outputs",