From d5bdcef600193472879576c93f60be0cf54f1ab9 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Fri, 13 Mar 2026 20:27:17 +0000 Subject: [PATCH 1/9] Support unlimited depth for the RandomForest estimators. --- python/cuml/cuml/ensemble/randomforest_common.pyx | 15 ++++++++++++--- .../cuml/cuml/ensemble/randomforestclassifier.py | 13 +++++++------ .../cuml/cuml/ensemble/randomforestregressor.py | 13 +++++++------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/python/cuml/cuml/ensemble/randomforest_common.pyx b/python/cuml/cuml/ensemble/randomforest_common.pyx index 657d6d7625..0708dceee9 100644 --- a/python/cuml/cuml/ensemble/randomforest_common.pyx +++ b/python/cuml/cuml/ensemble/randomforest_common.pyx @@ -418,8 +418,17 @@ class BaseRandomForestModel(Base, InteropMixin): cdef level_enum verbose = self._verbose_level cdef int n_classes = self.n_classes_ if is_classifier else 0 - if self.max_depth <= 0: - raise ValueError("Must specify max_depth > 0") + # None/-1 mean unlimited; translate to INT32_MAX just like sklearn. + cdef int max_depth_c + if self.max_depth is None or self.max_depth == -1: + max_depth_c = np.iinfo(np.int32).max + elif self.max_depth <= 0: + raise ValueError( + f"max_depth must be a positive integer, None, or -1 (unlimited); " + f"got {self.max_depth!r}" + ) + else: + max_depth_c = self.max_depth # Validate OOB score parameter if callable(self.oob_score): @@ -456,7 +465,7 @@ class BaseRandomForestModel(Base, InteropMixin): n_bins = self.n_bins cdef RF_params params = set_rf_params( - self.max_depth, + max_depth_c, self.max_leaves, max_features, n_bins, diff --git a/python/cuml/cuml/ensemble/randomforestclassifier.py b/python/cuml/cuml/ensemble/randomforestclassifier.py index ab5d30895f..19c948b44b 100644 --- a/python/cuml/cuml/ensemble/randomforestclassifier.py +++ b/python/cuml/cuml/ensemble/randomforestclassifier.py @@ -70,12 +70,13 @@ class RandomForestClassifier(BaseRandomForestModel, ClassifierMixin): * If ``False``, the whole dataset is used to build each tree. max_samples : float (default = 1.0) Ratio of dataset rows used while fitting each tree. - max_depth : int (default = 16) - Maximum tree depth. Must be greater than 0. - Unlimited depth (i.e, until leaves are pure) - is not supported.\n - .. note:: This default differs from scikit-learn's - random forest, which defaults to unlimited depth. + max_depth : int or None (default = 16) + Maximum tree depth. Use ``None`` or ``-1`` for unlimited depth + (trees grow until all leaves are pure). Must be a positive integer, + ``None``, or ``-1``. + + .. note:: This default differs from scikit-learn's random forest, + which defaults to unlimited depth. max_leaves : int (default = -1) Maximum leaf nodes per tree. Soft constraint. Unlimited, If ``-1``. diff --git a/python/cuml/cuml/ensemble/randomforestregressor.py b/python/cuml/cuml/ensemble/randomforestregressor.py index c4294f99f2..6b5ee95729 100644 --- a/python/cuml/cuml/ensemble/randomforestregressor.py +++ b/python/cuml/cuml/ensemble/randomforestregressor.py @@ -65,12 +65,13 @@ class RandomForestRegressor(BaseRandomForestModel, RegressorMixin): * If ``False``, the whole dataset is used to build each tree. max_samples : float (default = 1.0) Ratio of dataset rows used while fitting each tree. - max_depth : int (default = 16) - Maximum tree depth. Must be greater than 0. - Unlimited depth (i.e, until leaves are pure) - is not supported.\n - .. note:: This default differs from scikit-learn's - random forest, which defaults to unlimited depth. + max_depth : int or None (default = 16) + Maximum tree depth. Use ``None`` or ``-1`` for unlimited depth + (trees grow until all leaves are pure). Must be a positive integer, + ``None``, or ``-1``. + + .. note:: This default differs from scikit-learn's random forest, + which defaults to unlimited depth. max_leaves : int (default = -1) Maximum leaf nodes per tree. Soft constraint. Unlimited, If ``-1``. From 625526a46cf1b3f605ff59b86a820e5414de6594 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Fri, 13 Mar 2026 20:34:06 +0000 Subject: [PATCH 2/9] Implement tests --- python/cuml/tests/test_random_forest.py | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/python/cuml/tests/test_random_forest.py b/python/cuml/tests/test_random_forest.py index b9b067813d..3a07cd43d4 100644 --- a/python/cuml/tests/test_random_forest.py +++ b/python/cuml/tests/test_random_forest.py @@ -776,6 +776,41 @@ def test_create_classification_model( assert params["n_bins"] == verfiy_params["n_bins"] +@pytest.mark.parametrize("max_depth", [-1, None]) +def test_unlimited_max_depth_classifier(max_depth): + X, y = make_classification(n_samples=500, n_features=10, random_state=42) + + clf = curfc(n_estimators=10, max_depth=max_depth, random_state=42) + clf.fit(X, y) + preds = clf.predict(X) + assert len(preds) == len(y) + + params = clf.get_params() + assert params["max_depth"] == max_depth + clf2 = curfc() + clf2.set_params(**params) + assert clf2.get_params()["max_depth"] == max_depth + + shallow = curfc(n_estimators=10, max_depth=2, random_state=42) + shallow.fit(X, y) + assert accuracy_score(y, preds) >= accuracy_score(y, shallow.predict(X)) + + +@pytest.mark.parametrize("max_depth", [-1, None]) +def test_unlimited_max_depth_regressor(max_depth): + X, y = make_regression(n_samples=500, n_features=10, random_state=42) + + reg = curfr(n_estimators=10, max_depth=max_depth, random_state=42) + reg.fit(X, y) + assert len(reg.predict(X)) == len(y) + + params = reg.get_params() + assert params["max_depth"] == max_depth + reg2 = curfr() + reg2.set_params(**params) + assert reg2.get_params()["max_depth"] == max_depth + + @pytest.mark.parametrize("n_estimators", [10, 20, 100]) @pytest.mark.parametrize("n_bins", [8, 9, 10]) def test_multiple_fits_classification(large_clf, n_estimators, n_bins): From c5fc11a329e46bbd50da2d07cdcf797853c25c78 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Fri, 13 Mar 2026 20:37:41 +0000 Subject: [PATCH 3/9] Remove max_depth parameter from kernel where not actually used. --- cpp/src/decisiontree/batched-levelalgo/builder.cuh | 5 ++--- .../batched-levelalgo/kernels/builder_kernels.cuh | 5 ++--- .../kernels/builder_kernels_impl.cuh | 12 ++++-------- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/cpp/src/decisiontree/batched-levelalgo/builder.cuh b/cpp/src/decisiontree/batched-levelalgo/builder.cuh index 509eb1e4e1..4e454de4ba 100644 --- a/cpp/src/decisiontree/batched-levelalgo/builder.cuh +++ b/cpp/src/decisiontree/batched-levelalgo/builder.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ @@ -468,8 +468,7 @@ struct Builder { // create child nodes (or make the current ones leaf) raft::common::nvtx::push_range("nodeSplitKernel @builder.cuh [batched-levelalgo]"); - launchNodeSplitKernel(params.max_depth, - params.min_samples_leaf, + launchNodeSplitKernel(params.min_samples_leaf, params.min_samples_split, params.max_leaves, params.min_impurity_decrease, diff --git a/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh b/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh index bd384f3eb9..45f52a4e6b 100644 --- a/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh +++ b/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ @@ -64,8 +64,7 @@ DI OutT* alignPointer(InT dataset) } template -void launchNodeSplitKernel(const IdxT max_depth, - const IdxT min_samples_leaf, +void launchNodeSplitKernel(const IdxT min_samples_leaf, const IdxT min_samples_split, const IdxT max_leaves, const DataT min_impurity_decrease, diff --git a/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh b/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh index 638bf523e7..07270c4042 100644 --- a/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh +++ b/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ #pragma once @@ -78,8 +78,7 @@ DI void partitionSamples(const Dataset& dataset, } } template -static __global__ void nodeSplitKernel(const IdxT max_depth, - const IdxT min_samples_leaf, +static __global__ void nodeSplitKernel(const IdxT min_samples_leaf, const IdxT min_samples_split, const IdxT max_leaves, const DataT min_impurity_decrease, @@ -98,8 +97,7 @@ static __global__ void nodeSplitKernel(const IdxT max_depth, } template -void launchNodeSplitKernel(const IdxT max_depth, - const IdxT min_samples_leaf, +void launchNodeSplitKernel(const IdxT min_samples_leaf, const IdxT min_samples_split, const IdxT max_leaves, const DataT min_impurity_decrease, @@ -111,8 +109,7 @@ void launchNodeSplitKernel(const IdxT max_depth, { auto constexpr smem_size = 2 * sizeof(IdxT) * TPB; nodeSplitKernel - <<>>(max_depth, - min_samples_leaf, + <<>>(min_samples_leaf, min_samples_split, max_leaves, min_impurity_decrease, @@ -377,7 +374,6 @@ void launchComputeSplitKernel(BinT* histograms, } template void launchNodeSplitKernel<_DataT, _LabelT, _IdxT, TPB_DEFAULT>( - const _IdxT max_depth, const _IdxT min_samples_leaf, const _IdxT min_samples_split, const _IdxT max_leaves, From 977107ae5252a57c448e0bdc498a4bb35f08ff3e Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Fri, 13 Mar 2026 21:08:13 +0000 Subject: [PATCH 4/9] Adjust the dask versions. --- .../dask/ensemble/randomforestclassifier.py | 9 ++--- .../dask/ensemble/randomforestregressor.py | 9 ++--- .../tests/dask/test_dask_random_forest.py | 33 ++++++++++++++++++- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/python/cuml/cuml/dask/ensemble/randomforestclassifier.py b/python/cuml/cuml/dask/ensemble/randomforestclassifier.py index 0257b0827d..4c94a99438 100755 --- a/python/cuml/cuml/dask/ensemble/randomforestclassifier.py +++ b/python/cuml/cuml/dask/ensemble/randomforestclassifier.py @@ -68,10 +68,11 @@ class RandomForestClassifier( * If ``False``, the whole dataset is used to build each tree. max_samples : float (default = 1.0) Ratio of dataset rows used while fitting each tree. - max_depth : int (default = 16) - Maximum tree depth. Must be greater than 0. - Unlimited depth (i.e, until leaves are pure) - is not supported.\n + max_depth : int or None (default = 16) + Maximum tree depth. Use ``None`` or ``-1`` for unlimited depth + (trees grow until all leaves are pure). Must be a positive integer, + ``None``, or ``-1``. + .. note:: This default differs from scikit-learn's random forest, which defaults to unlimited depth. max_leaves : int (default = -1) diff --git a/python/cuml/cuml/dask/ensemble/randomforestregressor.py b/python/cuml/cuml/dask/ensemble/randomforestregressor.py index 7524c9f934..dd5bcc4de6 100755 --- a/python/cuml/cuml/dask/ensemble/randomforestregressor.py +++ b/python/cuml/cuml/dask/ensemble/randomforestregressor.py @@ -58,10 +58,11 @@ class RandomForestRegressor( * If ``False``, the whole dataset is used to build each tree. max_samples : float (default = 1.0) Ratio of dataset rows used while fitting each tree. - max_depth : int (default = 16) - Maximum tree depth. Must be greater than 0. - Unlimited depth (i.e, until leaves are pure) - is not supported.\n + max_depth : int or None (default = 16) + Maximum tree depth. Use ``None`` or ``-1`` for unlimited depth + (trees grow until all leaves are pure). Must be a positive integer, + ``None``, or ``-1``. + .. note:: This default differs from scikit-learn's random forest, which defaults to unlimited depth. max_leaves : int (default = -1) diff --git a/python/cuml/tests/dask/test_dask_random_forest.py b/python/cuml/tests/dask/test_dask_random_forest.py index 6094ba6a56..f5c08e0dcc 100644 --- a/python/cuml/tests/dask/test_dask_random_forest.py +++ b/python/cuml/tests/dask/test_dask_random_forest.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 import json @@ -357,6 +357,37 @@ def check_count(node, nodes): check_count(node, nodes) +@pytest.mark.parametrize("max_depth", [-1, None]) +def test_unlimited_max_depth_classifier(client, max_depth): + n_workers = len(client.scheduler_info(n_workers=-1)["workers"]) + X, y = make_classification( + n_samples=n_workers * 200, n_features=10, random_state=42 + ) + y = y.astype(np.int32) + + X_dask, y_dask = _prep_training_data(client, X, y, partitions_per_worker=1) + clf = cuRFC_mg(n_estimators=n_workers * 5, max_depth=max_depth) + clf.fit(X_dask, y_dask) + preds = cp.asnumpy(cp.array(clf.predict(X_dask).compute())) + assert len(preds) == len(y) + + +@pytest.mark.parametrize("max_depth", [-1, None]) +def test_unlimited_max_depth_regressor(client, max_depth): + n_workers = len(client.scheduler_info(n_workers=-1)["workers"]) + X, y = make_regression( + n_samples=n_workers * 200, n_features=10, random_state=42 + ) + X = X.astype(np.float32) + y = y.astype(np.float32) + + X_dask, y_dask = _prep_training_data(client, X, y, partitions_per_worker=1) + reg = cuRFR_mg(n_estimators=n_workers * 5, max_depth=max_depth) + reg.fit(X_dask, y_dask) + preds = cp.asnumpy(cp.array(reg.predict(X_dask).compute())) + assert len(preds) == len(y) + + @pytest.mark.parametrize("estimator_type", ["regression", "classification"]) def test_rf_get_combined_model_right_aftter_fit(client, estimator_type): max_depth = 3 From d29771df992acafe2d14ff0d135c286cc8f4ec3b Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Fri, 13 Mar 2026 21:17:00 +0000 Subject: [PATCH 5/9] Do not support -1 at the Python API layer. None indicates unlimited. --- .../cuml/cuml/dask/ensemble/randomforestclassifier.py | 5 ++--- .../cuml/cuml/dask/ensemble/randomforestregressor.py | 5 ++--- python/cuml/cuml/ensemble/randomforest_common.pyx | 10 ++++------ python/cuml/cuml/ensemble/randomforestclassifier.py | 5 ++--- python/cuml/cuml/ensemble/randomforestregressor.py | 5 ++--- python/cuml/tests/dask/test_dask_random_forest.py | 11 +++++------ 6 files changed, 17 insertions(+), 24 deletions(-) diff --git a/python/cuml/cuml/dask/ensemble/randomforestclassifier.py b/python/cuml/cuml/dask/ensemble/randomforestclassifier.py index 4c94a99438..bb89bb1fcb 100755 --- a/python/cuml/cuml/dask/ensemble/randomforestclassifier.py +++ b/python/cuml/cuml/dask/ensemble/randomforestclassifier.py @@ -69,9 +69,8 @@ class RandomForestClassifier( max_samples : float (default = 1.0) Ratio of dataset rows used while fitting each tree. max_depth : int or None (default = 16) - Maximum tree depth. Use ``None`` or ``-1`` for unlimited depth - (trees grow until all leaves are pure). Must be a positive integer, - ``None``, or ``-1``. + Maximum tree depth. Use ``None`` for unlimited depth (trees grow + until all leaves are pure). Must be a positive integer or ``None``. .. note:: This default differs from scikit-learn's random forest, which defaults to unlimited depth. diff --git a/python/cuml/cuml/dask/ensemble/randomforestregressor.py b/python/cuml/cuml/dask/ensemble/randomforestregressor.py index dd5bcc4de6..fb45e14a2e 100755 --- a/python/cuml/cuml/dask/ensemble/randomforestregressor.py +++ b/python/cuml/cuml/dask/ensemble/randomforestregressor.py @@ -59,9 +59,8 @@ class RandomForestRegressor( max_samples : float (default = 1.0) Ratio of dataset rows used while fitting each tree. max_depth : int or None (default = 16) - Maximum tree depth. Use ``None`` or ``-1`` for unlimited depth - (trees grow until all leaves are pure). Must be a positive integer, - ``None``, or ``-1``. + Maximum tree depth. Use ``None`` for unlimited depth (trees grow + until all leaves are pure). Must be a positive integer or ``None``. .. note:: This default differs from scikit-learn's random forest, which defaults to unlimited depth. diff --git a/python/cuml/cuml/ensemble/randomforest_common.pyx b/python/cuml/cuml/ensemble/randomforest_common.pyx index 0708dceee9..3c1a756a2e 100644 --- a/python/cuml/cuml/ensemble/randomforest_common.pyx +++ b/python/cuml/cuml/ensemble/randomforest_common.pyx @@ -220,8 +220,7 @@ class BaseRandomForestModel(Base, InteropMixin): elif model.max_samples is not None: conditional_params["max_samples"] = model.max_samples - if model.max_depth is not None: - conditional_params["max_depth"] = model.max_depth + conditional_params["max_depth"] = model.max_depth return { "n_estimators": model.n_estimators, @@ -418,13 +417,12 @@ class BaseRandomForestModel(Base, InteropMixin): cdef level_enum verbose = self._verbose_level cdef int n_classes = self.n_classes_ if is_classifier else 0 - # None/-1 mean unlimited; translate to INT32_MAX just like sklearn. cdef int max_depth_c - if self.max_depth is None or self.max_depth == -1: + if self.max_depth is None: max_depth_c = np.iinfo(np.int32).max - elif self.max_depth <= 0: + elif not isinstance(self.max_depth, int) or self.max_depth <= 0: raise ValueError( - f"max_depth must be a positive integer, None, or -1 (unlimited); " + f"max_depth must be a positive integer or None (unlimited); " f"got {self.max_depth!r}" ) else: diff --git a/python/cuml/cuml/ensemble/randomforestclassifier.py b/python/cuml/cuml/ensemble/randomforestclassifier.py index 19c948b44b..1df2edde04 100644 --- a/python/cuml/cuml/ensemble/randomforestclassifier.py +++ b/python/cuml/cuml/ensemble/randomforestclassifier.py @@ -71,9 +71,8 @@ class RandomForestClassifier(BaseRandomForestModel, ClassifierMixin): max_samples : float (default = 1.0) Ratio of dataset rows used while fitting each tree. max_depth : int or None (default = 16) - Maximum tree depth. Use ``None`` or ``-1`` for unlimited depth - (trees grow until all leaves are pure). Must be a positive integer, - ``None``, or ``-1``. + Maximum tree depth. Use ``None`` for unlimited depth (trees grow + until all leaves are pure). Must be a positive integer or ``None``. .. note:: This default differs from scikit-learn's random forest, which defaults to unlimited depth. diff --git a/python/cuml/cuml/ensemble/randomforestregressor.py b/python/cuml/cuml/ensemble/randomforestregressor.py index 6b5ee95729..aacac40579 100644 --- a/python/cuml/cuml/ensemble/randomforestregressor.py +++ b/python/cuml/cuml/ensemble/randomforestregressor.py @@ -66,9 +66,8 @@ class RandomForestRegressor(BaseRandomForestModel, RegressorMixin): max_samples : float (default = 1.0) Ratio of dataset rows used while fitting each tree. max_depth : int or None (default = 16) - Maximum tree depth. Use ``None`` or ``-1`` for unlimited depth - (trees grow until all leaves are pure). Must be a positive integer, - ``None``, or ``-1``. + Maximum tree depth. Use ``None`` for unlimited depth (trees grow + until all leaves are pure). Must be a positive integer or ``None``. .. note:: This default differs from scikit-learn's random forest, which defaults to unlimited depth. diff --git a/python/cuml/tests/dask/test_dask_random_forest.py b/python/cuml/tests/dask/test_dask_random_forest.py index f5c08e0dcc..5c67bcc0b5 100644 --- a/python/cuml/tests/dask/test_dask_random_forest.py +++ b/python/cuml/tests/dask/test_dask_random_forest.py @@ -357,23 +357,22 @@ def check_count(node, nodes): check_count(node, nodes) -@pytest.mark.parametrize("max_depth", [-1, None]) -def test_unlimited_max_depth_classifier(client, max_depth): +def test_unlimited_max_depth_classifier(client): n_workers = len(client.scheduler_info(n_workers=-1)["workers"]) X, y = make_classification( n_samples=n_workers * 200, n_features=10, random_state=42 ) + X = X.astype(np.float32) y = y.astype(np.int32) X_dask, y_dask = _prep_training_data(client, X, y, partitions_per_worker=1) - clf = cuRFC_mg(n_estimators=n_workers * 5, max_depth=max_depth) + clf = cuRFC_mg(n_estimators=n_workers * 5, max_depth=None) clf.fit(X_dask, y_dask) preds = cp.asnumpy(cp.array(clf.predict(X_dask).compute())) assert len(preds) == len(y) -@pytest.mark.parametrize("max_depth", [-1, None]) -def test_unlimited_max_depth_regressor(client, max_depth): +def test_unlimited_max_depth_regressor(client): n_workers = len(client.scheduler_info(n_workers=-1)["workers"]) X, y = make_regression( n_samples=n_workers * 200, n_features=10, random_state=42 @@ -382,7 +381,7 @@ def test_unlimited_max_depth_regressor(client, max_depth): y = y.astype(np.float32) X_dask, y_dask = _prep_training_data(client, X, y, partitions_per_worker=1) - reg = cuRFR_mg(n_estimators=n_workers * 5, max_depth=max_depth) + reg = cuRFR_mg(n_estimators=n_workers * 5, max_depth=None) reg.fit(X_dask, y_dask) preds = cp.asnumpy(cp.array(reg.predict(X_dask).compute())) assert len(preds) == len(y) From 19d42682d802047b1c37fcfaa22d1bebec2514f3 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Fri, 13 Mar 2026 21:21:30 +0000 Subject: [PATCH 6/9] Clean up interop. --- python/cuml/cuml/ensemble/randomforest_common.pyx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/cuml/cuml/ensemble/randomforest_common.pyx b/python/cuml/cuml/ensemble/randomforest_common.pyx index 3c1a756a2e..454f665db6 100644 --- a/python/cuml/cuml/ensemble/randomforest_common.pyx +++ b/python/cuml/cuml/ensemble/randomforest_common.pyx @@ -220,11 +220,10 @@ class BaseRandomForestModel(Base, InteropMixin): elif model.max_samples is not None: conditional_params["max_samples"] = model.max_samples - conditional_params["max_depth"] = model.max_depth - return { "n_estimators": model.n_estimators, "split_criterion": split_criterion, + "max_depth": model.max_depth, "min_samples_split": model.min_samples_split, "min_samples_leaf": model.min_samples_leaf, "max_features": model.max_features, From d1d6b7d59bafe9a110da50cf09190e85ff24541e Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Fri, 13 Mar 2026 21:37:57 +0000 Subject: [PATCH 7/9] Clean up cpp layer. --- cpp/include/cuml/tree/decisiontree.hpp | 5 +++-- cpp/src/decisiontree/batched-levelalgo/builder.cuh | 1 - .../batched-levelalgo/kernels/builder_kernels.cuh | 1 - .../batched-levelalgo/kernels/builder_kernels_impl.cuh | 4 ---- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/cpp/include/cuml/tree/decisiontree.hpp b/cpp/include/cuml/tree/decisiontree.hpp index 74374cfab0..f617737154 100644 --- a/cpp/include/cuml/tree/decisiontree.hpp +++ b/cpp/include/cuml/tree/decisiontree.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2023, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ @@ -17,7 +17,8 @@ namespace DT { struct DecisionTreeParams { /** - * Maximum tree depth. Unlimited (e.g., until leaves are pure), If `-1`. + * Maximum tree depth. Set to INT32_MAX for unlimited depth + * (i.e., until leaves are pure or other stopping criteria are met). */ int max_depth; /** diff --git a/cpp/src/decisiontree/batched-levelalgo/builder.cuh b/cpp/src/decisiontree/batched-levelalgo/builder.cuh index 4e454de4ba..e9a7996b65 100644 --- a/cpp/src/decisiontree/batched-levelalgo/builder.cuh +++ b/cpp/src/decisiontree/batched-levelalgo/builder.cuh @@ -524,7 +524,6 @@ struct Builder { raft::common::nvtx::range kernel_scope("computeSplitKernel @builder.cuh [batched-levelalgo]"); launchComputeSplitKernel(histograms, params.max_n_bins, - params.max_depth, params.min_samples_split, params.max_leaves, dataset, diff --git a/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh b/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh index 45f52a4e6b..ce3dc8b79c 100644 --- a/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh +++ b/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh @@ -387,7 +387,6 @@ template void launchComputeSplitKernel(BinT* histograms, IdxT n_bins, - IdxT max_depth, IdxT min_samples_split, IdxT max_leaves, const Dataset& dataset, diff --git a/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh b/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh index 07270c4042..59677b6caf 100644 --- a/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh +++ b/cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh @@ -201,7 +201,6 @@ template static __global__ void computeSplitKernel(BinT* histograms, IdxT max_n_bins, - IdxT max_depth, IdxT min_samples_split, IdxT max_leaves, const Dataset dataset, @@ -334,7 +333,6 @@ template void launchComputeSplitKernel(BinT* histograms, IdxT max_n_bins, - IdxT max_depth, IdxT min_samples_split, IdxT max_leaves, const Dataset& dataset, @@ -356,7 +354,6 @@ void launchComputeSplitKernel(BinT* histograms, computeSplitKernel <<>>(histograms, max_n_bins, - max_depth, min_samples_split, max_leaves, dataset, @@ -397,7 +394,6 @@ template void launchLeafKernel<_DatasetT, _NodeT, _ObjectiveT, _DataT>( template void launchComputeSplitKernel<_DataT, _LabelT, _IdxT, TPB_DEFAULT, _ObjectiveT, _BinT>( _BinT* histograms, _IdxT n_bins, - _IdxT max_depth, _IdxT min_samples_split, _IdxT max_leaves, const Dataset<_DataT, _LabelT, _IdxT>& dataset, From 42b1174c35972632729b91600a2d81ad5d719ffa Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Mon, 23 Mar 2026 17:35:37 +0000 Subject: [PATCH 8/9] Fixup max_depth tests --- python/cuml/tests/test_random_forest.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/python/cuml/tests/test_random_forest.py b/python/cuml/tests/test_random_forest.py index 3a07cd43d4..3e93eb077c 100644 --- a/python/cuml/tests/test_random_forest.py +++ b/python/cuml/tests/test_random_forest.py @@ -776,39 +776,37 @@ def test_create_classification_model( assert params["n_bins"] == verfiy_params["n_bins"] -@pytest.mark.parametrize("max_depth", [-1, None]) -def test_unlimited_max_depth_classifier(max_depth): +def test_unlimited_max_depth_classifier(): X, y = make_classification(n_samples=500, n_features=10, random_state=42) - clf = curfc(n_estimators=10, max_depth=max_depth, random_state=42) + clf = curfc(n_estimators=10, max_depth=None, random_state=42) clf.fit(X, y) preds = clf.predict(X) assert len(preds) == len(y) params = clf.get_params() - assert params["max_depth"] == max_depth + assert params["max_depth"] is None clf2 = curfc() clf2.set_params(**params) - assert clf2.get_params()["max_depth"] == max_depth + assert clf2.get_params()["max_depth"] is None shallow = curfc(n_estimators=10, max_depth=2, random_state=42) shallow.fit(X, y) assert accuracy_score(y, preds) >= accuracy_score(y, shallow.predict(X)) -@pytest.mark.parametrize("max_depth", [-1, None]) -def test_unlimited_max_depth_regressor(max_depth): +def test_unlimited_max_depth_regressor(): X, y = make_regression(n_samples=500, n_features=10, random_state=42) - reg = curfr(n_estimators=10, max_depth=max_depth, random_state=42) + reg = curfr(n_estimators=10, max_depth=None, random_state=42) reg.fit(X, y) assert len(reg.predict(X)) == len(y) params = reg.get_params() - assert params["max_depth"] == max_depth + assert params["max_depth"] is None reg2 = curfr() reg2.set_params(**params) - assert reg2.get_params()["max_depth"] == max_depth + assert reg2.get_params()["max_depth"] is None @pytest.mark.parametrize("n_estimators", [10, 20, 100]) From 3237c3fe34e1f873f49b07dc1888d9f07f1a9d25 Mon Sep 17 00:00:00 2001 From: Hyunsu Cho Date: Wed, 1 Apr 2026 06:32:58 -0700 Subject: [PATCH 9/9] Fix pytests --- .../cuml/tests/test_sklearn_import_export.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/python/cuml/tests/test_sklearn_import_export.py b/python/cuml/tests/test_sklearn_import_export.py index 9762ade6aa..7c07cc4aee 100644 --- a/python/cuml/tests/test_sklearn_import_export.py +++ b/python/cuml/tests/test_sklearn_import_export.py @@ -772,9 +772,13 @@ def test_random_forest_classifier(random_state, oob_score): n_samples=200, n_features=5, n_informative=3, random_state=random_state ) - cu_model = cuml.RandomForestClassifier(oob_score=oob_score).fit(X, y) + cu_model = cuml.RandomForestClassifier( + oob_score=oob_score, + max_depth=None, + ).fit(X, y) sk_model = sklearn.ensemble.RandomForestClassifier( - oob_score=oob_score + oob_score=oob_score, + max_depth=None, ).fit(X, y) sk_model2 = cu_model.as_sklearn() @@ -819,10 +823,14 @@ def test_random_forest_regressor(random_state, oob_score): X, y = make_regression(n_samples=200, random_state=random_state) X = X.astype("float32") - cu_model = cuml.RandomForestRegressor(oob_score=oob_score).fit(X, y) - sk_model = sklearn.ensemble.RandomForestRegressor(oob_score=oob_score).fit( - X, y - ) + cu_model = cuml.RandomForestRegressor( + oob_score=oob_score, + max_depth=None, + ).fit(X, y) + sk_model = sklearn.ensemble.RandomForestRegressor( + oob_score=oob_score, + max_depth=None, + ).fit(X, y) sk_model2 = cu_model.as_sklearn() cu_model2 = cuml.RandomForestRegressor.from_sklearn(sk_model)