Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 36 additions & 40 deletions python/cuml/cuml/neighbors/kernel_density.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@

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_array,
check_consistent_length,
check_features,
check_is_fitted,
check_random_seed,
check_sample_weight,
)
from cuml.metrics import pairwise_distances
from cuml.metrics.pairwise_distances import (
Expand Down Expand Up @@ -267,7 +269,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":
Expand All @@ -288,53 +290,46 @@ 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(
check_features(self, X, reset=True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use check_inputs here instead of check_features + check_array + check_sample_weight + check_consistent_length. A bit shorter code, and ensures things are handled consistently across our estimators.

@viclafargue viclafargue May 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check_inputs function does not have a sample_weight_ensure_non_negative argument making it impossible to merge everything into a single operation. But, I could merge check_features + check_array.

@jcrist jcrist May 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also use check_inputs, and call check_non_negative on sample weights after too. Could also add the plumbing for ensure_sample_weight_non_negative if ya want. Up to you.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing out the existence of this function! Just updated the code with it.

self._X = check_array(
X,
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],
input_name="X",
)

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 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.bandwidth_ = self.bandwidth

if sample_weight is not None:
self._sample_weight = input_to_cupy_array(
self._sample_weight = check_sample_weight(
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")
dtype=self._X.dtype,
convert_dtype=convert_dtype,
ensure_non_negative=True,
)
check_consistent_length(self._X, self._sample_weight)
else:
self._sample_weight = None

Expand All @@ -360,12 +355,13 @@ def score_samples(self, X, *, convert_dtype=True) -> CumlArray:
check_is_fitted(self)
check_features(self, X)

X = input_to_cuml_array(
X = check_array(
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",
input_name="X",
)
Comment thread
viclafargue marked this conversation as resolved.
Resolved
if self.metric_params:
if len(self.metric_params) != 1:
raise ValueError(
Expand Down
65 changes: 24 additions & 41 deletions python/cuml/cuml/neighbors/kneighbors_classifier.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ 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
from cuml.internals.array import CumlArray
from cuml.internals.interop import UnsupportedOnGPU
from cuml.internals.interop import UnsupportedOnGPU, to_cpu, to_gpu
from cuml.internals.mixins import ClassifierMixin, FMajorInputTagMixin
from cuml.internals.outputs import reflect, run_in_internal_context
from cuml.internals.validation import check_consistent_length, check_y
Expand Down Expand Up @@ -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": to_gpu(model._y, order="F", dtype=np.int32),
**super()._attrs_from_cpu(model),
}

def _attrs_to_cpu(self, model):
return {
"classes_": self.classes_,
"_y": self._y.get(),
"_y": to_cpu(self._y),
"outputs_2d_": self.outputs_2d_,
**super()._attrs_to_cpu(model),
}
Expand All @@ -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.
Expand All @@ -178,14 +177,15 @@ 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
self._y = CumlArray(y)
Comment thread
viclafargue marked this conversation as resolved.
Resolved
return self

@property
Expand All @@ -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
Expand All @@ -235,17 +226,17 @@ class KNeighborsClassifier(ClassifierMixin, FMajorInputTagMixin, NeighborsBase):
cdef vector[int*] y_vec
for i in range(out_cols):
col = self._y if out_cols == 1 else self._y[:, i]
y_vec.push_back(<int*><uintptr_t>col.data.ptr)
y_vec.push_back(<int*><uintptr_t>col.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 = <float*><uintptr_t>(
0 if weights_cp is None else weights_cp.data.ptr
)

handle = get_handle()
cdef handle_t* handle_ = <handle_t*><size_t>handle.getHandle()
cdef int64_t* inds_ptr = <int64_t*><uintptr_t>inds.ptr
cdef int64_t* inds_ptr = <int64_t*><uintptr_t>inds_cp.data.ptr
cdef size_t n_samples_fit = self._y.shape[0]
cdef int n_neighbors = self.n_neighbors
with nogil:
Expand Down Expand Up @@ -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_)]
Expand All @@ -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(<float*><uintptr_t>proba.ptr)
y_vec.push_back(<int*><uintptr_t>y.data.ptr)
y_vec.push_back(<int*><uintptr_t>y.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 = <float*><uintptr_t>(
0 if weights_cp is None else weights_cp.data.ptr
)

handle = get_handle()
cdef handle_t* handle_ = <handle_t*><size_t>handle.getHandle()
cdef int64_t* inds_ptr = <int64_t*><uintptr_t>inds.ptr
cdef int64_t* inds_ptr = <int64_t*><uintptr_t>inds_cp.data.ptr
cdef size_t n_samples_fit = self._y.shape[0]
cdef int n_neighbors = self.n_neighbors
with nogil:
Expand Down
43 changes: 18 additions & 25 deletions python/cuml/cuml/neighbors/kneighbors_regressor.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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.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

Expand Down Expand Up @@ -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.
Expand All @@ -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 = CumlArray(y)
Comment thread
viclafargue marked this conversation as resolved.
Resolved

return self

Expand All @@ -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 = <int64_t*><uintptr_t>inds.ptr
dists_cp = knn_distances.to_output("cupy")
cdef size_t n_rows = inds_cp.shape[0]
cdef int64_t* inds_ctype = <int64_t*><uintptr_t>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 = <float*><uintptr_t>out.ptr
Expand All @@ -240,7 +233,7 @@ class KNeighborsRegressor(RegressorMixin, FMajorInputTagMixin, NeighborsBase):
cdef handle_t* handle_ = <handle_t*><size_t>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 = <float*><uintptr_t>(
0 if weights_cp is None else weights_cp.data.ptr
)
Expand Down
Loading
Loading