Skip to content

Navigation Menu

Sign in
Sign up

Add cuml.accel support for sklearn.ensemble.IsolationForest #8477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
adityaanikam wants to merge 9 commits into NVIDIA:main
base: main
Choose a base branch
Loading
from adityaanikam:fea-accel-isolation-forest
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
9 commits
Select commit Hold shift + click to select a range
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
20 changes: 20 additions & 0 deletions docs/source/cuml-accel/compatibility.rst
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,26 @@ To compare results between estimators, we recommend comparing scores like
- If ``y`` is a multi-output target.


.. dropdown:: ``IsolationForest``
:name: isolationforest

``IsolationForest`` will fall back to CPU in the following cases:

- If ``warm_start=True``.
- If ``sample_weight`` is passed to ``fit`` or ``fit_predict``.
- If ``X`` is sparse.
- If ``X`` contains missing or non-finite values.

Additional notes:

- Conversion of a fitted GPU ``IsolationForest`` back to a CPU estimator
is supported. Accessing fit attributes (``offset_``, ``max_samples_``,
``estimators_``, and others) or pickling a GPU-fitted model triggers
this conversion automatically.
- ``estimators_samples_`` is not available on the converted model,
since cuML does not record per-tree sample indices.


sklearn.kernel_ridge
~~~~~~~~~~~~~~~~~~~~

Expand Down
54 changes: 52 additions & 2 deletions python/cuml/cuml/accel/_overrides/sklearn/ensemble.py
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#

Expand All @@ -8,7 +8,11 @@
from cuml.internals.interop import UnsupportedOnGPU
from cuml.internals.validation import check_array

__all__ = ("RandomForestRegressor", "RandomForestClassifier")
__all__ = (
"RandomForestRegressor",
"RandomForestClassifier",
"IsolationForest",
)


class _RandomForestMixin:
Expand Down Expand Up @@ -88,3 +92,49 @@ def __iter__(self):

def __getitem__(self, index):
return self._call_method("__getitem__", index)


class IsolationForest(ProxyBase):
_gpu_class = cuml.ensemble.IsolationForest

@staticmethod
def _validate_input(X):
# cuML's IsolationForest requires dense, finite input and raises
# ValueError (NaN/inf) or TypeError (sparse) otherwise. Convert
# those into UnsupportedOnGPU so callers fall back to CPU instead
# of crashing.
try:
check_array(
X, mem_type=None, order=None, ensure_2d=False, input_name="X"
)
except (ValueError, TypeError) as exc:
raise UnsupportedOnGPU(str(exc)) from None

def _gpu_fit(self, X, y=None, sample_weight=None):
self._validate_input(X)
if sample_weight is not None:
raise UnsupportedOnGPU("sample_weight is not supported")
return self._gpu.fit(X, y=y)

def _gpu_fit_predict(self, X, y=None, **kwargs):
# IsolationForest.fit_predict() doesn't declare sample_weight itself;
# it inherits OutlierMixin.fit_predict(self, X, y=None, **kwargs),
# which forwards kwargs straight to fit(). Match that signature here
# (rather than declaring sample_weight explicitly) so the proxy stays
# signature-compatible with the CPU method.
self._validate_input(X)
if kwargs.get("sample_weight") is not None:
raise UnsupportedOnGPU("sample_weight is not supported")
return self._gpu.fit_predict(X, y=y)

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

def _gpu_decision_function(self, X):
self._validate_input(X)
return self._gpu.decision_function(X)

def _gpu_score_samples(self, X):
self._validate_input(X)
return self._gpu.score_samples(X)
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import numpy as np
import pytest
from sklearn.datasets import make_blobs
from sklearn.ensemble import IsolationForest

CPUIsolationForest = IsolationForest._cpu_class


@pytest.fixture(scope="module")
def blobs_with_outliers():
X, _ = make_blobs(
n_samples=200,
centers=1,
cluster_std=0.5,
random_state=42,
)
rng = np.random.RandomState(42)
outliers = rng.uniform(low=-10, high=10, size=(20, X.shape[1]))
return np.vstack([X, outliers])


def test_isolation_forest_fit_predict_agreement(blobs_with_outliers):
X = blobs_with_outliers
params = {"n_estimators": 100, "random_state": 0}

expected = CPUIsolationForest(**params).fit(X)
result = IsolationForest(**params).fit(X)

assert result._gpu is not None

expected_labels = expected.predict(X)
result_labels = result.predict(X)
assert set(np.unique(result_labels)) <= {-1, 1}
assert np.mean(expected_labels == result_labels) >= 0.9


def test_isolation_forest_fit_sample_weight_falls_back_to_cpu(
blobs_with_outliers,
):
# cuml.ensemble.IsolationForest.fit() has no sample_weight parameter,
# so a non-None sample_weight cannot be honored on GPU.
X = blobs_with_outliers
sample_weight = np.ones(len(X))

result = IsolationForest(n_estimators=10, random_state=0).fit(
X, sample_weight=sample_weight
)

assert result._gpu is None


def test_isolation_forest_fit_predict_sample_weight_falls_back_to_cpu(
blobs_with_outliers,
):
# Same as above, for fit_predict().
X = blobs_with_outliers
sample_weight = np.ones(len(X))

result = IsolationForest(n_estimators=10, random_state=0)
result.fit_predict(X, sample_weight=sample_weight)

assert result._gpu is None


def test_isolation_forest_gpu_fit_attrs_available_after_conversion(
blobs_with_outliers,
):
# Regression test: conversion of a fitted GPU IsolationForest back to a
# CPU estimator is now supported (landed in #8483, tracked by #8420).
# Accessing fit attributes on a GPU-fitted proxy must trigger that
# conversion and expose the real synced values instead of raising.
Comment on lines +71 to +74

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.

(nit) I don't think this in-line comment is adding valuable context.

@adityaanikam adityaanikam Aug 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Trimmed kept the #8483/#8420 reference so the "why" is still clear, dropped the rest.

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.

I don't see any changes. Did you forget to push? Either way, not a big deal. I'm just not a fan of overly verbose comments that seem to explain code evolution instead of just providing necessary context.

X = blobs_with_outliers
result = IsolationForest(n_estimators=50, random_state=0).fit(X)
assert result._gpu is not None

gpu_scores = result.decision_function(X) # dispatched to GPU

assert len(result.estimators_) == 50
assert result.offset_ == pytest.approx(float(result._gpu.offset_))

np.testing.assert_allclose(
result._cpu.decision_function(X), gpu_scores, atol=1e-5
)
Loading

AltStyle によって変換されたページ (->オリジナル) /