Skip to content

Support unlimited_depth for the random forest estimators - #7895

Merged
rapids-bot[bot] merged 10 commits into
NVIDIA:release/26.04from
csadorf:rf-support-unlimited-depth
Apr 1, 2026
Merged

Support unlimited_depth for the random forest estimators#7895
rapids-bot[bot] merged 10 commits into
NVIDIA:release/26.04from
csadorf:rf-support-unlimited-depth

Conversation

@csadorf

@csadorf csadorf commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

This PR enables unlimited tree depth via max_depth=None, matching the scikit-learn API convention (previously cuML required a positive integer and had no unlimited option).

Implemented by mapping None to INT32_MAX before passing to the C++ layer — the same approach scikit-learn uses. Trees then grow until leaves are pure or other stopping criteria are met. Also removes max_depth from nodeSplitKernel and computeSplitKernel, where it was passed but never used; depth enforcement was always host-side in NodeQueue::IsExpandable.

Related to #6416; changing the default parameter will be done in a follow-up.

@csadorf
csadorf requested review from a team as code owners March 13, 2026 22:03
@github-actions github-actions Bot added Cython / Python Cython or Python issue CUDA/C++ labels Mar 13, 2026
@csadorf
csadorf changed the base branch from main to release/26.04 March 13, 2026 22:05
@csadorf csadorf added improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels Mar 13, 2026
@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request implements support for unlimited max_depth in Random Forest by allowing max_depth=None. It removes max_depth parameter from C++ kernel functions, updates Python APIs to accept None values, normalizes unlimited depth to INT32_MAX internally, and adds test coverage for the new behavior.

Changes

Cohort / File(s) Summary
C++ Kernel Function Signatures
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh, cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh
Removed max_depth parameter from launchNodeSplitKernel and launchComputeSplitKernel function signatures and kernel implementations. Updated copyright year range to 2019-2026.
C++ Kernel Invocations
cpp/src/decisiontree/batched-levelalgo/builder.cuh
Updated call sites to launchNodeSplitKernel and launchComputeSplitKernel to remove max_depth argument, adjusting parameter order accordingly. Updated copyright year range.
C++ Public Header
cpp/include/cuml/tree/decisiontree.hpp
Updated copyright year range to 2019-2026. Revised DecisionTreeParams::max_depth documentation to indicate unlimited depth is represented by setting max_depth to INT32_MAX.
Python Ensemble API Documentation
python/cuml/cuml/ensemble/randomforestclassifier.py, python/cuml/cuml/ensemble/randomforestregressor.py
Updated max_depth parameter documentation from int to int or None, replacing "unlimited depth not supported" with semantics that None enables unlimited growth until leaves are pure.
Python Dask API Documentation
python/cuml/cuml/dask/ensemble/randomforestclassifier.py, python/cuml/cuml/dask/ensemble/randomforestregressor.py
Updated max_depth parameter documentation to accept int or None, with semantics that None enables unlimited depth behavior.
Python Core Implementation
python/cuml/cuml/ensemble/randomforest_common.pyx
Implemented None value handling: normalizes max_depth=None to np.iinfo(np.int32).max internally; always includes max_depth in exported CPU parameters; validates input as non-negative integer or None.
Test Suite
python/cuml/tests/dask/test_dask_random_forest.py, python/cuml/tests/test_random_forest.py
Added parametrized test cases for both classifier and regressor with max_depth sentinels (-1 and None), verifying fit/predict execution, parameter propagation, and prediction correctness. Updated copyright year range.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Suggested labels

sklearn-api-compat

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature: enabling unlimited tree depth for random forest estimators via max_depth=None.
Description check ✅ Passed The description accurately explains the implementation approach, mapping None to INT32_MAX, removing unused max_depth parameters, and references the related issue.
Linked Issues check ✅ Passed The PR partially addresses issue #6416 by enabling unlimited max_depth support (a prerequisite for better defaults), but defers the actual default change to a follow-up PR.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing unlimited max_depth support: kernel signature updates, parameter documentation changes, and test additions are all in scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@csadorf
csadorf force-pushed the rf-support-unlimited-depth branch from 8d096f4 to 7acd941 Compare March 13, 2026 22:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh (1)

81-87: Trim the remaining dead node-split arguments.

nodeSplitKernel only consumes min_samples_leaf and min_impurity_decrease in Lines 89-97, so min_samples_split and max_leaves are still dead parameters after this cleanup. Removing them too would keep the internal CUDA interface aligned with the real host-side enforcement.

♻️ Suggested cleanup
 static __global__ void nodeSplitKernel(const IdxT min_samples_leaf,
-                                       const IdxT min_samples_split,
-                                       const IdxT max_leaves,
                                        const DataT min_impurity_decrease,
                                        const Dataset<DataT, LabelT, IdxT> dataset,
                                        const NodeWorkItem* work_items,
                                        const Split<DataT, IdxT>* splits)
@@
 void launchNodeSplitKernel(const IdxT min_samples_leaf,
-                           const IdxT min_samples_split,
-                           const IdxT max_leaves,
                            const DataT min_impurity_decrease,
                            const Dataset<DataT, LabelT, IdxT>& dataset,
                            const NodeWorkItem* work_items,
                            const size_t work_items_size,
                            const Split<DataT, IdxT>* splits,
                            cudaStream_t builder_stream)
 {
   auto constexpr smem_size = 2 * sizeof(IdxT) * TPB;
   nodeSplitKernel<DataT, LabelT, IdxT, TPB>
-    <<<work_items_size, TPB, smem_size, builder_stream>>>(min_samples_leaf,
-                                                          min_samples_split,
-                                                          max_leaves,
-                                                          min_impurity_decrease,
+    <<<work_items_size, TPB, smem_size, builder_stream>>>(min_samples_leaf,
+                                                          min_impurity_decrease,
                                                           dataset,
                                                           work_items,
                                                           splits);
 }

Update the matching declaration in cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh and the call site in cpp/src/decisiontree/batched-levelalgo/builder.cuh in the same sweep.

Also applies to: 100-118

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh`
around lines 81 - 87, The nodeSplitKernel signature still includes dead
parameters min_samples_split and max_leaves; remove these two parameters from
the device kernel declaration/definition for nodeSplitKernel (the signature
shown) and update the corresponding declaration in builder_kernels.cuh and the
invocation in builder.cuh so the CUDA kernel call and host-side declaration
match the new signature that only takes min_samples_leaf and
min_impurity_decrease (and the existing dataset, work_items, splits parameters).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cpp/include/cuml/tree/decisiontree.hpp`:
- Around line 20-21: The header has inconsistent sentinel docs for "Maximum tree
depth" (doc at top mentions INT32_MAX while other docs/defaults reference -1);
update the comments and any default values so they state one clear contract:
either document that the public API accepts -1 as "unlimited" which is
internally normalized to INT32_MAX, or change the comments/defaults to use
INT32_MAX everywhere. Adjust the Javadoc for the max-depth parameter in
decisiontree.hpp (the top comment and the entries around the helper at lines ~64
and the default at ~80) to explicitly mention the chosen sentinel and, if you
keep -1 as the API sentinel, add a short note that code normalizes -1 ->
INT32_MAX before use.

In `@python/cuml/tests/dask/test_dask_random_forest.py`:
- Around line 360-388: The two tests test_unlimited_max_depth_classifier and
test_unlimited_max_depth_regressor only check prediction length; change them to
evaluate numerical correctness by splitting X,y into train/test via a
deterministic split (use train_test_split with a fixed random_state), use
_prep_training_data on the training portion, fit cuRFC_mg / cuRFR_mg on the
training Dask arrays, predict on the held-out test set (not the training data),
and compare predictions to a scikit-learn baseline (e.g.,
sklearn.ensemble.RandomForestClassifier/Regressor with same
n_estimators/max_depth/random_state) or assert a numeric metric (accuracy/MSE)
is within an acceptable threshold; ensure types remain float32/int32 as before
and use the same n_workers-derived sizing so tests remain distributed.

---

Nitpick comments:
In `@cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh`:
- Around line 81-87: The nodeSplitKernel signature still includes dead
parameters min_samples_split and max_leaves; remove these two parameters from
the device kernel declaration/definition for nodeSplitKernel (the signature
shown) and update the corresponding declaration in builder_kernels.cuh and the
invocation in builder.cuh so the CUDA kernel call and host-side declaration
match the new signature that only takes min_samples_leaf and
min_impurity_decrease (and the existing dataset, work_items, splits parameters).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0a7ffdd2-f4c2-4011-99c3-3205c526b032

📥 Commits

Reviewing files that changed from the base of the PR and between 8d096f4 and 7acd941.

📒 Files selected for processing (11)
  • cpp/include/cuml/tree/decisiontree.hpp
  • cpp/src/decisiontree/batched-levelalgo/builder.cuh
  • cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh
  • cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh
  • python/cuml/cuml/dask/ensemble/randomforestclassifier.py
  • python/cuml/cuml/dask/ensemble/randomforestregressor.py
  • python/cuml/cuml/ensemble/randomforest_common.pyx
  • python/cuml/cuml/ensemble/randomforestclassifier.py
  • python/cuml/cuml/ensemble/randomforestregressor.py
  • python/cuml/tests/dask/test_dask_random_forest.py
  • python/cuml/tests/test_random_forest.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • python/cuml/tests/test_random_forest.py
  • python/cuml/cuml/ensemble/randomforestclassifier.py
  • python/cuml/cuml/dask/ensemble/randomforestclassifier.py
  • python/cuml/cuml/dask/ensemble/randomforestregressor.py
  • python/cuml/cuml/ensemble/randomforestregressor.py
  • python/cuml/cuml/ensemble/randomforest_common.pyx

Comment thread cpp/include/cuml/tree/decisiontree.hpp
Comment on lines +360 to +388
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=None)
clf.fit(X_dask, y_dask)
preds = cp.asnumpy(cp.array(clf.predict(X_dask).compute()))
assert len(preds) == len(y)


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
)
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=None)
reg.fit(X_dask, y_dask)
preds = cp.asnumpy(cp.array(reg.predict(X_dask).compute()))
assert len(preds) == len(y)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Strengthen unlimited-depth tests to verify correctness, not just output length.

Both new tests currently pass even if predictions are wrong, because they only assert len(preds) == len(y) and predict on training data. Please compare against a scikit-learn baseline (or at minimum a quality threshold on held-out data) with fixed random_state.

Suggested test hardening
+from sklearn.ensemble import RandomForestRegressor as skrfr
@@
 def test_unlimited_max_depth_classifier(client):
@@
-    X_dask, y_dask = _prep_training_data(client, X, y, partitions_per_worker=1)
-    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)
+    X_train, X_test, y_train, y_test = train_test_split(
+        X, y, test_size=n_workers * 40, random_state=42
+    )
+    X_train_dask, y_train_dask = _prep_training_data(
+        client, X_train, y_train, partitions_per_worker=1
+    )
+    X_test_dask = from_array(X_test)
+
+    clf = cuRFC_mg(n_estimators=n_workers * 5, max_depth=None, random_state=42)
+    clf.fit(X_train_dask, y_train_dask)
+    preds = cp.asnumpy(cp.array(clf.predict(X_test_dask).compute()))
+    acc = accuracy_score(y_test, preds)
+
+    sk_clf = skrfc(
+        n_estimators=n_workers * 5, max_depth=None, random_state=42, n_jobs=-1
+    )
+    sk_clf.fit(X_train, y_train)
+    sk_acc = accuracy_score(y_test, sk_clf.predict(X_test))
+    assert acc >= (sk_acc - 0.07)
@@
 def test_unlimited_max_depth_regressor(client):
@@
-    X_dask, y_dask = _prep_training_data(client, X, y, partitions_per_worker=1)
-    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)
+    X_train, X_test, y_train, y_test = train_test_split(
+        X, y, test_size=n_workers * 40, random_state=42
+    )
+    X_train_dask, y_train_dask = _prep_training_data(
+        client, X_train, y_train, partitions_per_worker=1
+    )
+    X_test_dask = from_array(X_test)
+
+    reg = cuRFR_mg(n_estimators=n_workers * 5, max_depth=None, random_state=42)
+    reg.fit(X_train_dask, y_train_dask)
+    preds = cp.asnumpy(cp.array(reg.predict(X_test_dask).compute()))
+    r2 = r2_score(y_test, preds)
+
+    sk_reg = skrfr(
+        n_estimators=n_workers * 5, max_depth=None, random_state=42, n_jobs=-1
+    )
+    sk_reg.fit(X_train, y_train)
+    sk_r2 = r2_score(y_test, sk_reg.predict(X_test))
+    assert r2 >= (sk_r2 - 0.10)

As per coding guidelines for python/**/test_*.py: “Test files must validate numerical correctness by comparing with scikit-learn…”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/tests/dask/test_dask_random_forest.py` around lines 360 - 388,
The two tests test_unlimited_max_depth_classifier and
test_unlimited_max_depth_regressor only check prediction length; change them to
evaluate numerical correctness by splitting X,y into train/test via a
deterministic split (use train_test_split with a fixed random_state), use
_prep_training_data on the training portion, fit cuRFC_mg / cuRFR_mg on the
training Dask arrays, predict on the held-out test set (not the training data),
and compare predictions to a scikit-learn baseline (e.g.,
sklearn.ensemble.RandomForestClassifier/Regressor with same
n_estimators/max_depth/random_state) or assert a numeric metric (accuracy/MSE)
is within an acceptable threshold; ensure types remain float32/int32 as before
and use the same n_workers-derived sizing so tests remain distributed.

@chyunsu3

Copy link
Copy Markdown
Contributor

I will review soon

@jcrist jcrist left a comment

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.

Overall approach LGTM! Do you have any thoughts on if/when/how we should change the default to max_depth=None?

@csadorf

csadorf commented Mar 18, 2026

Copy link
Copy Markdown
Contributor Author

Overall approach LGTM! Do you have any thoughts on if/when/how we should change the default to max_depth=None?

I think we should deprecate the current default in this PR and then switch to unlimited by default in 26.06 and let users decide on the desired accuracy vs compute cost trade-off themselves.

@viclafargue viclafargue left a comment

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.

Thanks, LGTM!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
python/cuml/tests/test_random_forest.py (1)

783-815: ⚠️ Potential issue | 🟠 Major

These unlimited-depth tests still don't prove max_depth=None works correctly.

The classifier path only checks training accuracy against a shallow forest, which is not a stable invariant for bagged models, and the regressor path only checks output length. Both can pass without showing scikit-learn-compatible unlimited-depth behavior. Please use a fixed train/test split and compare classifier accuracy / regressor R² against sklearn with max_depth=None.

As per coding guidelines for python/**/test_*.py: “Test files must validate numerical correctness by comparing with scikit-learn, include edge case coverage...”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/tests/test_random_forest.py` around lines 783 - 815, The tests
test_unlimited_max_depth_classifier and test_unlimited_max_depth_regressor are
insufficiently validating max_depth=None; replace the current checks with a
deterministic train/test split (use make_classification/make_regression to
create X,y then train_test_split), fit your estimator instances curfc and curfr
on the train set, compute classifier accuracy (accuracy_score) and regressor R²
on the test set, then fit
sklearn.ensemble.RandomForestClassifier/RandomForestRegressor with identical
parameters including max_depth=None and compare metrics (e.g., assert the custom
classifier accuracy and regressor R² are close to sklearn's within a small
tolerance) to prove scikit-learn-compatible unlimited-depth behavior.
python/cuml/tests/dask/test_dask_random_forest.py (1)

360-387: ⚠️ Potential issue | 🟠 Major

These unlimited-depth Dask tests still don't verify correctness.

Both tests train and predict on the same partitions and only assert len(preds) == len(y). They will still pass if max_depth=None silently behaves like the legacy depth or if the predictions are numerically wrong but correctly sized. Please switch to a deterministic train/test split, set random_state, and compare accuracy/R² against a scikit-learn max_depth=None baseline on held-out data.

As per coding guidelines for python/**/test_*.py: “Test files must validate numerical correctness by comparing with scikit-learn, include edge case coverage...”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/tests/dask/test_dask_random_forest.py` around lines 360 - 387,
Update both tests to validate numerical correctness by performing a
deterministic train/test split and comparing model performance to scikit-learn's
equivalent with max_depth=None: use train_test_split with a fixed random_state
to split X, y into train and test (do not train and predict on the same
partitions), prepare Dask partitions for the training set via
_prep_training_data (keep partitions_per_worker=1), instantiate
cuRFC_mg/cuRFR_mg with max_depth=None and fixed n_estimators, fit on the Dask
training data, compute predictions on the held-out test set (use .compute() and
convert to numpy), create a scikit-learn
RandomForestClassifier/RandomForestRegressor with max_depth=None and the same
random_state/n_estimators, fit on the same training arrays and compute baseline
metrics (accuracy for test_unlimited_max_depth_classifier, r2_score for
test_unlimited_max_depth_regressor), and assert the cuML metric is within a
small tolerance of scikit-learn's metric.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/cuml/tests/dask/test_dask_random_forest.py`:
- Around line 524-541: The test currently only checks the default sentinel;
after fitting each estimator you must also assert the worker-side params and
compare predictions to ensure explicit branches are used: after
clf_explicit.fit(X_dask, y_dask) and clf_none.fit(X_dask, y_dask) fetch their
worker_params (via the same get_params/inspection used for clf_default) and
assert that p["max_depth"] == 16 for clf_explicit and p["max_depth"] is None (or
the concrete representation used on workers) for clf_none; additionally compute
predictions from clf_default.predict(X_dask) and clf_explicit.predict(X_dask)
(same random_state) and assert they are different (or use an appropriate
array-assertion) to prove the explicit max_depth=16 path was applied on workers.

---

Duplicate comments:
In `@python/cuml/tests/dask/test_dask_random_forest.py`:
- Around line 360-387: Update both tests to validate numerical correctness by
performing a deterministic train/test split and comparing model performance to
scikit-learn's equivalent with max_depth=None: use train_test_split with a fixed
random_state to split X, y into train and test (do not train and predict on the
same partitions), prepare Dask partitions for the training set via
_prep_training_data (keep partitions_per_worker=1), instantiate
cuRFC_mg/cuRFR_mg with max_depth=None and fixed n_estimators, fit on the Dask
training data, compute predictions on the held-out test set (use .compute() and
convert to numpy), create a scikit-learn
RandomForestClassifier/RandomForestRegressor with max_depth=None and the same
random_state/n_estimators, fit on the same training arrays and compute baseline
metrics (accuracy for test_unlimited_max_depth_classifier, r2_score for
test_unlimited_max_depth_regressor), and assert the cuML metric is within a
small tolerance of scikit-learn's metric.

In `@python/cuml/tests/test_random_forest.py`:
- Around line 783-815: The tests test_unlimited_max_depth_classifier and
test_unlimited_max_depth_regressor are insufficiently validating max_depth=None;
replace the current checks with a deterministic train/test split (use
make_classification/make_regression to create X,y then train_test_split), fit
your estimator instances curfc and curfr on the train set, compute classifier
accuracy (accuracy_score) and regressor R² on the test set, then fit
sklearn.ensemble.RandomForestClassifier/RandomForestRegressor with identical
parameters including max_depth=None and compare metrics (e.g., assert the custom
classifier accuracy and regressor R² are close to sklearn's within a small
tolerance) to prove scikit-learn-compatible unlimited-depth behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 32eb8747-73f9-4060-b489-923a4169e4a4

📥 Commits

Reviewing files that changed from the base of the PR and between 7acd941 and 030dfe6.

📒 Files selected for processing (7)
  • python/cuml/cuml/dask/ensemble/randomforestclassifier.py
  • python/cuml/cuml/dask/ensemble/randomforestregressor.py
  • python/cuml/cuml/ensemble/randomforest_common.pyx
  • python/cuml/cuml/ensemble/randomforestclassifier.py
  • python/cuml/cuml/ensemble/randomforestregressor.py
  • python/cuml/tests/dask/test_dask_random_forest.py
  • python/cuml/tests/test_random_forest.py
✅ Files skipped from review due to trivial changes (3)
  • python/cuml/cuml/ensemble/randomforestregressor.py
  • python/cuml/cuml/dask/ensemble/randomforestregressor.py
  • python/cuml/cuml/dask/ensemble/randomforestclassifier.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/cuml/cuml/ensemble/randomforestclassifier.py
  • python/cuml/cuml/ensemble/randomforest_common.pyx

Comment thread python/cuml/tests/dask/test_dask_random_forest.py Outdated
@csadorf csadorf linked an issue Mar 23, 2026 that may be closed by this pull request
@csadorf
csadorf force-pushed the rf-support-unlimited-depth branch from 16c10c4 to d1d6b7d Compare March 23, 2026 16:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh (1)

81-87: Remove the remaining unused kernel parameters in the node-split path.

After dropping max_depth, min_samples_split and max_leaves are still threaded through but never used in nodeSplitKernel. Cleaning them out will simplify this API and reduce future call-site mistakes.

♻️ Suggested cleanup
-static __global__ void nodeSplitKernel(const IdxT min_samples_leaf,
-                                       const IdxT min_samples_split,
-                                       const IdxT max_leaves,
+static __global__ void nodeSplitKernel(const IdxT min_samples_leaf,
                                        const DataT min_impurity_decrease,
                                        const Dataset<DataT, LabelT, IdxT> dataset,
                                        const NodeWorkItem* work_items,
                                        const Split<DataT, IdxT>* splits)

-void launchNodeSplitKernel(const IdxT min_samples_leaf,
-                           const IdxT min_samples_split,
-                           const IdxT max_leaves,
+void launchNodeSplitKernel(const IdxT min_samples_leaf,
                            const DataT min_impurity_decrease,
                            const Dataset<DataT, LabelT, IdxT>& dataset,
                            const NodeWorkItem* work_items,
                            const size_t work_items_size,
                            const Split<DataT, IdxT>* splits,
                            cudaStream_t builder_stream)
{
  auto constexpr smem_size = 2 * sizeof(IdxT) * TPB;
  nodeSplitKernel<DataT, LabelT, IdxT, TPB>
-    <<<work_items_size, TPB, smem_size, builder_stream>>>(min_samples_leaf,
-                                                          min_samples_split,
-                                                          max_leaves,
-                                                          min_impurity_decrease,
+    <<<work_items_size, TPB, smem_size, builder_stream>>>(min_samples_leaf,
+                                                          min_impurity_decrease,
                                                           dataset,
                                                           work_items,
                                                           splits);
}

Also applies to: 100-118

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh`
around lines 81 - 87, The nodeSplitKernel signature still accepts unused
parameters (min_samples_split, max_leaves and min_depth was already removed) —
remove these unused kernel parameters from the nodeSplitKernel declaration (and
any overloads) and update all callers and kernel launches that pass
min_samples_split or max_leaves to instead pass only the required arguments
(e.g., min_samples_leaf, min_impurity_decrease, dataset, work_items, splits);
also adjust any declarations or typedefs referencing nodeSplitKernel so the
parameter list matches and rebuild to ensure all call-sites are fixed (search
for nodeSplitKernel and its kernel launches to locate every usage).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/cuml/tests/test_random_forest.py`:
- Around line 799-813: The test test_unlimited_max_depth_regressor only checks
prediction length; update it to validate numerical correctness by computing
predictions = reg.predict(X), asserting all values are finite (use np.isfinite),
and computing R² with sklearn.metrics.r2_score; build a shallow baseline
regressor (e.g., baseline = curfr(n_estimators=10, max_depth=3,
random_state=42)), fit it on X,y, get baseline_preds and assert r2_score(y,
predictions) > r2_score(y, baseline_preds). Keep the existing param/params
checks and use the same random_state/makeup as curfr/reg/reg2 to ensure
reproducibility.
- Around line 779-797: Add a scikit-learn baseline check to the classifier and
regressor unlimited-depth tests: in test_unlimited_max_depth_classifier (and the
analogous regressor test) after the existing param/get_params assertions, create
an sklearn.ensemble.RandomForestClassifier/RandomForestRegressor with the same
n_estimators, random_state and max_depth=None and fit/predict on the same X,y,
then assert that curfc (and the regressor's cuRF) predictions have comparable
numerical performance to sklearn (use accuracy_score for classifier, and
mean_squared_error or r2_score for regressor) when max_depth is None; keep the
existing -1 case for backward-compat coverage and place these comparisons right
after the existing shallow-vs-full assertions so the tests validate numerical
equivalence to sklearn for None semantics.

---

Nitpick comments:
In `@cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh`:
- Around line 81-87: The nodeSplitKernel signature still accepts unused
parameters (min_samples_split, max_leaves and min_depth was already removed) —
remove these unused kernel parameters from the nodeSplitKernel declaration (and
any overloads) and update all callers and kernel launches that pass
min_samples_split or max_leaves to instead pass only the required arguments
(e.g., min_samples_leaf, min_impurity_decrease, dataset, work_items, splits);
also adjust any declarations or typedefs referencing nodeSplitKernel so the
parameter list matches and rebuild to ensure all call-sites are fixed (search
for nodeSplitKernel and its kernel launches to locate every usage).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 65fdaadb-30c9-4c6a-85f1-71fee654e733

📥 Commits

Reviewing files that changed from the base of the PR and between 4739051 and d1d6b7d.

📒 Files selected for processing (11)
  • cpp/include/cuml/tree/decisiontree.hpp
  • cpp/src/decisiontree/batched-levelalgo/builder.cuh
  • cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh
  • cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh
  • python/cuml/cuml/dask/ensemble/randomforestclassifier.py
  • python/cuml/cuml/dask/ensemble/randomforestregressor.py
  • python/cuml/cuml/ensemble/randomforest_common.pyx
  • python/cuml/cuml/ensemble/randomforestclassifier.py
  • python/cuml/cuml/ensemble/randomforestregressor.py
  • python/cuml/tests/dask/test_dask_random_forest.py
  • python/cuml/tests/test_random_forest.py
✅ Files skipped from review due to trivial changes (6)
  • cpp/include/cuml/tree/decisiontree.hpp
  • python/cuml/cuml/dask/ensemble/randomforestregressor.py
  • python/cuml/cuml/ensemble/randomforestclassifier.py
  • python/cuml/tests/dask/test_dask_random_forest.py
  • python/cuml/cuml/dask/ensemble/randomforestclassifier.py
  • python/cuml/cuml/ensemble/randomforestregressor.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/cuml/cuml/ensemble/randomforest_common.pyx
  • cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh

Comment thread python/cuml/tests/test_random_forest.py Outdated
Comment thread python/cuml/tests/test_random_forest.py Outdated
@jcrist

jcrist commented Apr 1, 2026

Copy link
Copy Markdown
Member

/merge

@rapids-bot
rapids-bot Bot merged commit 54514a8 into NVIDIA:release/26.04 Apr 1, 2026
101 checks passed
@csadorf
csadorf deleted the rf-support-unlimited-depth branch April 1, 2026 15:31
@csadorf

csadorf commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai Create two issues for the follow-up work to deprecate the current default for max_depth in 26.06 and then switch to the new default in 26.08. Assign both issues to me.

@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll create both follow-up issues now.


🏁 Script executed:

#!/bin/bash

# Issue 1: Deprecate current default for max_depth in 26.06
gh issue create \
  --repo rapidsai/cuml \
  --title "Deprecate default max_depth=16 in RandomForest estimators (target: 26.06)" \
  --assignee csadorf \
  --body "## Summary

Add a deprecation warning for the current \`max_depth=16\` default in \`RandomForestClassifier\` and \`RandomForestRegressor\` (and their Dask variants), notifying users that the default will change to \`None\` (unlimited depth) in release 26.08.

## Background

PR `#7895` added support for \`max_depth=None\` (unlimited depth) in cuML random forest estimators to match the scikit-learn API. The current default of \`max_depth=16\` can lead to substantially lower accuracy than scikit-learn on large datasets (see issue `#6416`). The plan is to migrate to an unlimited default, giving users control over accuracy vs. compute cost trade-offs.

## Tasks

- [ ] Add a \`FutureWarning\` deprecation notice when \`max_depth\` is not explicitly set by the user (i.e. the default of \`16\` is used), informing users that the default will change to \`None\` in 26.08.
- [ ] Update documentation and release notes to reflect the upcoming default change.

## References

- PR `#7895` (adds \`max_depth=None\` support)
- Issue `#6416` (improve default parameters for RandomForestClassifier)
- Follow-up: switch default to \`None\` in 26.08 (see companion issue)

Requested by `@csadorf` in https://github.com/rapidsai/cuml/pull/7895"

echo "---"

# Issue 2: Switch default max_depth to None in 26.08
gh issue create \
  --repo rapidsai/cuml \
  --title "Switch default max_depth to None in RandomForest estimators (target: 26.08)" \
  --assignee csadorf \
  --body "## Summary

Change the default value of \`max_depth\` from \`16\` to \`None\` (unlimited depth) in \`RandomForestClassifier\` and \`RandomForestRegressor\` (and their Dask variants) in release 26.08.

## Background

PR `#7895` added support for \`max_depth=None\` (unlimited depth) in cuML random forest estimators to match the scikit-learn API. A deprecation warning is being introduced in 26.06 (see companion issue). This issue tracks the actual default switch in 26.08.

## Tasks

- [ ] Change the default value of \`max_depth\` to \`None\` in \`RandomForestClassifier\`, \`RandomForestRegressor\`, and their Dask variants.
- [ ] Remove the deprecation warning introduced in 26.06.
- [ ] Update documentation, docstrings, and release notes to reflect the new default.
- [ ] Verify that tests pass and accuracy on standard benchmarks (e.g. covtype) is on par with scikit-learn defaults.

## References

- PR `#7895` (adds \`max_depth=None\` support)
- Issue `#6416` (improve default parameters for RandomForestClassifier)
- Prerequisite: deprecation warning in 26.06 (see companion issue)

Requested by `@csadorf` in https://github.com/rapidsai/cuml/pull/7895"

Length of output: 93


@csadorf: Both follow-up issues have been created and assigned to you:

  1. #7945Deprecate default max_depth=16 in RandomForest estimators (target: 26.06)

    • Add a FutureWarning when the default max_depth=16 is used (not explicitly set), notifying users of the upcoming change.
  2. #7946Switch default max_depth to None in RandomForest estimators (target: 26.08)

    • Remove the deprecation warning and flip the default to None (unlimited depth).

Both issues reference this PR (#7895) and issue #6416 for context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CUDA/C++ Cython / Python Cython or Python issue improvement Improvement / enhancement to an existing function non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants