Skip to content
2 changes: 2 additions & 0 deletions docs/source/cuml-accel/limitations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ RandomForestClassifier
- If ``class_weight`` is not ``None``.
- If ``sample_weight`` is passed to ``fit`` or ``score``.
- If ``X`` is sparse.
- If ``X`` contains missing values (represented as ``NaN``).
- If ``y`` is a multi-output target.

RandomForestRegressor
Expand All @@ -213,6 +214,7 @@ RandomForestRegressor
- If ``ccp_alpha`` is not ``0``.
- If ``sample_weight`` is passed to ``fit`` or ``score``.
- If ``X`` is sparse.
- If ``X`` contains missing values (represented as ``NaN``).
- If ``y`` is a multi-output target.


Expand Down
83 changes: 47 additions & 36 deletions python/cuml/cuml/accel/_overrides/sklearn/ensemble.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,67 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0
#

import cuml.ensemble
from cuml.accel.estimator_proxy import ProxyBase
from cuml.internals.input_utils import input_to_cuml_array
from cuml.internals.interop import UnsupportedOnGPU
from cuml.internals.validation import check_array

__all__ = ("RandomForestRegressor", "RandomForestClassifier")


class RandomForestRegressor(ProxyBase):
_gpu_class = cuml.ensemble.RandomForestRegressor
class _RandomForestMixin:
def _check_inputs(self, X, y=None, sample_weight=None):
# Fallback to CPU if NaN in X
try:
check_array(
X, mem_type=None, order=None, ensure_2d=False, input_name="X"
)
except ValueError as exc:
if "NaN" in str(exc):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we control this error so this is fine, just gave me a bit of paus thinking about an obscure bug if we change say to "nan" instead of "NaN" or another change. Probably not worth mulling much about, but still gave me a bit of pause.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I didn't love it, but it was quick to do and there's tests that will start failing if behavior ever changes. Seems better than nothing.

raise UnsupportedOnGPU(
"Missing values are not supported"
) from None
raise
Comment thread
jcrist marked this conversation as resolved.

def _gpu_fit(self, X, y, sample_weight=None):
if sample_weight is not None:
raise UnsupportedOnGPU("`sample_weight` is not supported")

try:
y = input_to_cuml_array(y, convert_to_mem_type=False)[0]
except ValueError:
raise
else:
if y is not None:
y = check_array(
y,
mem_type=None,
order=None,
ensure_2d=False,
ensure_all_finite=False,
input_name="y",
)
if len(y.shape) > 1 and y.shape[1] > 1:
if isinstance(self, RandomForestClassifier) and self.oob_score:
raise ValueError(
"The type of target cannot be used to compute OOB estimates"
)
raise UnsupportedOnGPU(
"Multi-output targets are not supported"
)

def _gpu_fit(self, X, y, sample_weight=None):
self._check_inputs(X, y, sample_weight=sample_weight)
return self._gpu.fit(X, y)

def _gpu_predict(self, X):
self._check_inputs(X)
return self._gpu.predict(X)

def _gpu_score(self, X, y, sample_weight=None):
if sample_weight is not None:
raise UnsupportedOnGPU("`sample_weight` is not supported")
self._check_inputs(X, y, sample_weight=sample_weight)
return self._gpu.score(X, y)


class RandomForestRegressor(ProxyBase, _RandomForestMixin):
_gpu_class = cuml.ensemble.RandomForestRegressor

def __len__(self):
return self._call_method("__len__")

Expand All @@ -44,33 +72,16 @@ def __getitem__(self, index):
return self._call_method("__getitem__", index)


class RandomForestClassifier(ProxyBase):
class RandomForestClassifier(ProxyBase, _RandomForestMixin):
_gpu_class = cuml.ensemble.RandomForestClassifier

def _gpu_fit(self, X, y, sample_weight=None):
if sample_weight is not None:
raise UnsupportedOnGPU("`sample_weight` is not supported")
def _gpu_predict_proba(self, X):
self._check_inputs(X)
return self._gpu.predict_proba(X)

try:
y = input_to_cuml_array(y, convert_to_mem_type=False)[0]
except ValueError:
raise
else:
if len(y.shape) > 1 and y.shape[1] > 1:
if self.oob_score:
raise ValueError(
"The type of target cannot be used to compute OOB estimates"
)
else:
raise UnsupportedOnGPU(
"Multi-output targets are not supported"
)
return self._gpu.fit(X, y)

def _gpu_score(self, X, y, sample_weight=None):
if sample_weight is not None:
raise UnsupportedOnGPU("`sample_weight` is not supported")
return self._gpu.score(X, y)
def _gpu_predict_log_proba(self, X):
self._check_inputs(X)
return self._gpu.predict_log_proba(X)

def __len__(self):
return self._call_method("__len__")
Expand Down
23 changes: 14 additions & 9 deletions python/cuml/cuml/accel/estimator_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def _reconstruct(self):
# will always serialize it by value rather than by reference. This allows
# the saved model to be loaded in an environment without cuml installed,
# where it will load as the CPU model directly.
def reconstruct(cls_path, cpu_model):
def reconstruct(cls_path, cpu_model, load_on_gpu):
"""Reconstruct a serialized estimator.

Returns a proxy estimator if `cuml.accel` is installed, falling back
Expand All @@ -82,7 +82,7 @@ def reconstruct(cls_path, cpu_model):
# Return the CPU estimator directly
return cpu_model
# `cuml.accel` is installed, reconstruct a proxy estimator
return cls._reconstruct_from_cpu(cpu_model)
return cls._reconstruct_from_cpu(cpu_model, load_on_gpu)

return reconstruct

Expand All @@ -96,8 +96,8 @@ def __reduce__(self):

return (pickle.loads, (cloudpickle.dumps(self._reconstruct),))

def __call__(self, cls_path, cpu):
return self._reconstruct(cls_path, cpu)
def __call__(self, cls_path, cpu, load_on_gpu):
return self._reconstruct(cls_path, cpu, load_on_gpu)


_reconstruct_proxy = _ReconstructProxy()
Expand Down Expand Up @@ -241,16 +241,17 @@ def _sync_attrs_to_cpu(self) -> None:
)

@classmethod
def _reconstruct_from_cpu(cls, cpu):
def _reconstruct_from_cpu(cls, cpu, load_on_gpu=True):
"""Reconstruct a proxy estimator from its CPU counterpart.

Primarily used when unpickling serialized proxy estimators."""
assert type(cpu) is cls._cpu_class
self = cls.__new__(cls)
self._cpu = cpu
self._synced = False
if is_fitted(self._cpu):
# This is a fit estimator. Try to convert model back to GPU
if load_on_gpu and is_fitted(self._cpu):
# This estimator is fit and should be loaded on GPU. Try to convert
# model back to GPU.
try:
self._gpu = self._gpu_class.from_sklearn(self._cpu)
except UnsupportedOnGPU:
Expand All @@ -259,7 +260,7 @@ def _reconstruct_from_cpu(cls, cpu):
# Supported on GPU, clear fit attributes from CPU to release host memory
self._cpu = sklearn.clone(self._cpu)
else:
# Estimator is unfit, delay GPU init until needed
# Estimator is unfit or should remain on CPU
self._gpu = None
return self

Expand Down Expand Up @@ -427,7 +428,11 @@ def __reduce__(self):
self._sync_attrs_to_cpu()
return (
_reconstruct_proxy,
(self._gpu_class._cpu_class_path, self._cpu),
(
self._gpu_class._cpu_class_path,
self._cpu,
self._gpu is not None,
),
)

def __getattr__(self, name: str) -> Any:
Expand Down
5 changes: 3 additions & 2 deletions python/cuml/cuml/ensemble/randomforest_common.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -408,14 +408,15 @@ class BaseRandomForestModel(Base, InteropMixin):
layout=layout,
default_chunk_size=default_chunk_size,
align_bytes=align_bytes,
ensure_all_finite=True,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Quick question for the accel path here, with ensure_all_finite=True getting passed to ForestInference from _predict_model_on_gpu, and the proxy's _check_inputs already running check_array(..., ensure_all_finite=True) on X to detect NaN, aren't we now traversing X twice during accel predict? Once to translate NaN -> UnsupportedOnGPU, once inside FIL. Another small thing probably, but just wanted to ask about it more than block on it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Indeed we are in cuml.accel alone. These checks run at ~600GiBs (on my machine) though, so I don't anticipate the double traversal being a measurable perf issue. Users using cuml proper also won't run into that issue.

)

def _fit_forest(self, X, y):
cdef bool is_classifier = self._estimator_type == "classifier"
cdef bool is_float32 = X.dtype == np.float32

cdef uintptr_t X_ptr = X.ptr
cdef uintptr_t y_ptr = y.ptr
cdef uintptr_t X_ptr = X.data.ptr
cdef uintptr_t y_ptr = y.data.ptr
cdef int n_rows = X.shape[0]
cdef int n_cols = X.shape[1]
cdef level_enum verbose = <level_enum> self._verbose_level
Expand Down
78 changes: 63 additions & 15 deletions python/cuml/cuml/ensemble/randomforestclassifier.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0

import cupy as cp
import numpy as np

import cuml.internals
import cuml.internals.nvtx as nvtx
Expand All @@ -11,10 +9,9 @@
from cuml.common.doc_utils import generate_docstring, insert_into_docstring
from cuml.ensemble.randomforest_common import BaseRandomForestModel
from cuml.internals.array import CumlArray
from cuml.internals.input_utils import input_to_cuml_array
from cuml.internals.interop import UnsupportedOnGPU
from cuml.internals.mixins import ClassifierMixin
from cuml.internals.validation import check_features, check_y
from cuml.internals.validation import check_features, check_inputs
from cuml.metrics import accuracy_score


Expand Down Expand Up @@ -225,19 +222,20 @@ def fit(self, X, y, *, convert_dtype=True) -> "RandomForestClassifier":
y to be of dtype int32. This will increase memory used for
the method.
"""
y, classes = check_y(y, dtype=cp.int32, return_classes=True)
X_m = input_to_cuml_array(
X, y, classes = check_inputs(
self,
X,
convert_to_dtype=(np.float32 if convert_dtype else None),
check_dtype=[np.float32, np.float64],
y,
dtype=("float32", "float64"),
convert_dtype=convert_dtype,
order="F",
check_rows=y.shape[0],
).array
y_dtype="int32",
return_classes=True,
reset=True,
)
Comment thread
jcrist marked this conversation as resolved.
self.classes_ = classes
self.n_classes_ = len(classes)
y_m = CumlArray(data=y)

return self._fit_forest(X_m, y_m)
return self._fit_forest(X, y)

@nvtx.annotate(
message="predict RF-Classifier @randomforestclassifier.pyx",
Expand Down Expand Up @@ -309,8 +307,7 @@ def predict_proba(
align_bytes=None,
) -> CumlArray:
"""
Predicts class probabilities for X. This function uses the GPU
implementation of predict.
Predicts class probabilities for X.

Parameters
----------
Expand Down Expand Up @@ -343,6 +340,57 @@ def predict_proba(
check_features(self, X)
return fil.predict_proba(X)

@insert_into_docstring(
parameters=[("dense", "(n_samples, n_features)")],
return_values=[("dense", "(n_samples, 1)")],
)
@cuml.internals.reflect
def predict_log_proba(
self,
X,
*,
convert_dtype=True,
layout="depth_first",
default_chunk_size=None,
align_bytes=None,
) -> CumlArray:
"""
Predicts log class probabilities for X.

Parameters
----------
X : {}
convert_dtype : bool (default = True)
When True, automatically convert the input to the data type used
to train the model. This may increase memory usage.
layout : string (default = 'depth_first')
Specifies the in-memory layout of nodes in FIL forests. Options:
'depth_first', 'layered', 'breadth_first'.
default_chunk_size : int, optional (default = None)
Determines how batches are further subdivided for parallel processing.
The optimal value depends on hardware, model, and batch size.
If None, will be automatically determined.
align_bytes : int, optional (default = None)
If specified, trees will be padded such that their in-memory size is
a multiple of this value. This can improve performance by guaranteeing
that memory reads from trees begin on a cache line boundary.
Typical values are 0 or 128 on GPU and 0 or 64 on CPU.

Returns
-------
y : {}
"""
preds = self.predict_proba(
X,
convert_dtype=convert_dtype,
layout=layout,
default_chunk_size=default_chunk_size,
align_bytes=align_bytes,
)
out = preds.to_output("cupy")
cp.log(out, out=out)
return CumlArray(data=out, index=preds.index)

@nvtx.annotate(
message="score RF-Classifier @randomforestclassifier.pyx",
domain="cuml_python",
Expand Down
27 changes: 9 additions & 18 deletions python/cuml/cuml/ensemble/randomforestregressor.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
# SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0
import numpy as np

import cuml.internals.nvtx as nvtx
from cuml.common import input_to_cuml_array
from cuml.common.array_descriptor import CumlArrayDescriptor
from cuml.common.doc_utils import generate_docstring, insert_into_docstring
from cuml.ensemble.randomforest_common import BaseRandomForestModel
from cuml.internals.array import CumlArray
from cuml.internals.mixins import RegressorMixin
from cuml.internals.outputs import reflect, run_in_internal_context
from cuml.internals.validation import check_features
from cuml.internals.validation import check_features, check_inputs
from cuml.metrics import r2_score


Expand Down Expand Up @@ -187,22 +184,16 @@ def fit(self, X, y, *, convert_dtype=True) -> "RandomForestRegressor":
Perform Random Forest Regression on the input data

"""
X_m = input_to_cuml_array(
X, y = check_inputs(
self,
X,
convert_to_dtype=(np.float32 if convert_dtype else None),
check_dtype=[np.float32, np.float64],
order="F",
).array

y_m = input_to_cuml_array(
y,
convert_to_dtype=(X_m.dtype if convert_dtype else None),
check_dtype=X_m.dtype,
check_rows=X_m.shape[0],
check_cols=1,
).array

return self._fit_forest(X_m, y_m)
dtype=("float32", "float64"),
convert_dtype=convert_dtype,
order="F",
reset=True,
)
return self._fit_forest(X, y)

@nvtx.annotate(
message="predict RF-Regressor @randomforestclassifier.pyx",
Expand Down
Loading
Loading