Skip to content

Match sklearn random forest feature sampling - #8239

Merged
rapids-bot[bot] merged 7 commits into
NVIDIA:mainfrom
RAMitchell:codex/rf-repeated-feature-sampling
Jun 11, 2026
Merged

Match sklearn random forest feature sampling#8239
rapids-bot[bot] merged 7 commits into
NVIDIA:mainfrom
RAMitchell:codex/rf-repeated-feature-sampling

Conversation

@RAMitchell

Copy link
Copy Markdown
Contributor

Summary

Fix RandomForest feature sampling so tree growth does not stop just because the first sampled feature subset has no valid split.

Previously, cuML sampled max_features columns for a node, evaluated only that subset, and made the node a leaf if none of those columns produced a valid split. scikit-learn continues drawing candidate features until it finds a valid split or exhausts the feature set. This PR changes cuML to retry subsequent feature samples for the same node before giving up.

Changes

  • Generate a deterministic per-node shuffled feature order.
  • Evaluate sampled columns in slices of max_features.
  • Retry only nodes whose sampled columns produced no valid split.
  • Stop after all columns have been considered, so the loop is bounded.
  • Always populate column_samples, including the no-sampling case.
  • Add regression coverage for the one-informative-feature failure mode.

Accuracy Evidence

The new regression test constructs a dataset where only one feature can split the labels. With max_features=1, the old behavior could stop early and predict a single class. With retrying enabled, RF reaches the informative feature and recovers perfect accuracy on the fixture.

Additional checks:

  • Targeted RF tests: 6 passed
  • Retry fixture: accuracy = 1.0, predicted classes [0, 1]
  • Constant-feature edge case terminates correctly with accuracy = 0.5

Performance

Performance is neutral in a smoke benchmark:

Case upstream/main this branch
RFClassifier<float>/blobs/0/manual_time 2172 ms 2150 ms

Validation

  • Rebuilt libcuml
  • Ran targeted RF pytest coverage
  • Ran edge-case checks to verify no infinite retry loop

@RAMitchell
RAMitchell requested review from a team as code owners June 8, 2026 13:37
@RAMitchell RAMitchell added the bug Something isn't working label Jun 8, 2026
@RAMitchell
RAMitchell requested a review from jcrist June 8, 2026 13:37
@RAMitchell RAMitchell added the non-breaking Non-breaking change label Jun 8, 2026
@RAMitchell
RAMitchell requested review from Copilot, csadorf and jinsolp June 8, 2026 13:37
@github-actions github-actions Bot added Cython / Python Cython or Python issue CUDA/C++ labels Jun 8, 2026
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b38bc32a-9db2-4a14-a23a-5f4b91e6bc6e

📥 Commits

Reviewing files that changed from the base of the PR and between 2f7e5bb and f8e8b95.

📒 Files selected for processing (2)
  • cpp/src/decisiontree/batched-levelalgo/builder.cuh
  • python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml
✅ Files skipped from review due to trivial changes (1)
  • python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/src/decisiontree/batched-levelalgo/builder.cuh

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved feature-sampling and split-validation in the Random Forest builder to better handle cases with very few predictive features and to retry only when needed.
  • Tests

    • Added regression tests covering feature-sampling retries and impurity-threshold behavior.
    • Updated test expectations by removing specific permutation-importance xfails.

Walkthrough

Refactors feature sampling to a deterministic Thrust-based device implementation, renames sampled-column buffer to column_samples, simplifies split kernels to read from that buffer, adds hosts-side multi-round retry logic for invalid splits, introduces FNV1a hash helpers, and adds regression tests and xfail updates.

Changes

Feature Sampling and Split Computation Refactor

Layer / File(s) Summary
Hash utilities for deterministic sampling
cpp/src/decisiontree/batched-levelalgo/random_utils.cuh
Added FNV1a helpers (fnv1a32_combine, fnv1a32_hash) for deterministic per-work-item RNG seeds.
Buffer rename and workspace allocation
cpp/src/decisiontree/batched-levelalgo/builder.cuh
Renamed colidscolumn_samples, updated includes, workspace sizing, and workspace assignment.
Kernel headers and split validation
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh
Added Thrust/CUDA iterator and RNG includes, introduced SplitPartitionNotValid, and updated launchComputeSplitKernel declaration to use column_samples.
Device-side feature sampling
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh
Implemented sample_features with Thrust counting and shuffle iterators to produce per-work-item sampled feature indices; removed prior custom sampling kernels.
Unified compute split kernel
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh
Updated computeSplitKernel and launchComputeSplitKernel to accept and load from column_samples, removing special-case branching; updated explicit instantiation.
Iterative sampling and retry orchestration
cpp/src/decisiontree/batched-levelalgo/builder.cuh
Refactored doSplit to a host-driven multi-round loop that reduces sampling width per round, calls computeBestSplits, validates partitions, retries invalid nodes, restores state, and commits final SplitT; added computeBestSplits and sampleFeatures.
Regression tests and xfail updates
python/cuml/tests/test_random_forest.py, python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml
Added two pytest regression tests covering sampling retry behavior and min_impurity_decrease behavior across seeds; removed four resolved xfail entries.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes


Suggested reviewers

  • dantegd
  • viclafargue
  • KyleFromNVIDIA
🚥 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 accurately summarizes the main change: making cuML's random forest feature sampling behavior match scikit-learn by implementing retry logic for invalid splits.
Description check ✅ Passed The description is directly related to the changeset, explaining the motivation, implementation approach, testing, and performance validation for the feature sampling retry mechanism.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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

Copilot AI 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.

Pull request overview

This PR updates cuML’s RandomForest feature sampling to match scikit-learn’s behavior: when a node’s initially sampled max_features subset yields no valid split, training now retries with additional feature slices (up to exhausting all features) instead of prematurely turning the node into a leaf.

Changes:

  • Introduces deterministic per-node feature ordering and samples features in bounded “rounds” (slices of max_features) until a valid split is found or all features are tried.
  • Simplifies/standardizes seed hashing via new fnv1a32_hash(...) helpers and reuses it for quantile column seeding.
  • Adds a regression test covering the “single informative feature + max_features=1” failure mode.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
python/cuml/tests/test_random_forest.py Adds a regression test ensuring RF does not stop early when the first sampled feature(s) can’t split.
cpp/src/decisiontree/batched-levelalgo/random_utils.cuh Adds reusable FNV-1a hash combine helpers for deterministic multi-value seeding.
cpp/src/decisiontree/batched-levelalgo/quantiles.cuh Refactors per-column quantile sampling seed derivation to use the new hash helper.
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh Replaces prior feature sampling kernels with a deterministic per-node shuffled-feature sampler and updates kernel interface to consume column_samples.
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh Updates split computation to always index features via column_samples (including the no-subsampling case).
cpp/src/decisiontree/batched-levelalgo/builder.cuh Implements the retry loop over feature slices per node, compacts retry work, and applies the final per-node best split once.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@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

🧹 Nitpick comments (1)
python/cuml/tests/test_random_forest.py (1)

1035-1055: ⚡ Quick win

HIGH: Consider adding scikit-learn comparison to verify behavior parity.

The test validates that cuML's retry logic achieves perfect accuracy on this edge case (single informative feature with max_features=1), but doesn't confirm that this matches scikit-learn's behavior. Since the PR objective is to match sklearn's feature sampling, comparing against sklearn would provide stronger evidence of parity.

🔍 Suggested enhancement to add sklearn baseline
 def test_rf_feature_sampling_retries_until_valid_split():
+    """
+    Test that feature sampling retries until a valid split is found.
+    
+    When max_features=1 and only one feature is informative, the retry logic
+    should eventually sample that feature, matching sklearn's behavior.
+    """
     n_samples = 128
     n_features = 32
     X = np.zeros((n_samples, n_features), dtype=np.float32)
     y = np.zeros(n_samples, dtype=np.int32)
     y[n_samples // 2 :] = 1
     X[:, 0] = y
 
     for random_state in range(8):
         clf = curfc(
             n_estimators=1,
             bootstrap=False,
             max_depth=None,
             max_features=1,
             n_bins=4,
             n_streams=1,
             random_state=random_state,
         )
         clf.fit(X, y)
-        assert accuracy_score(y, clf.predict(X)) == 1.0
+        cuml_acc = accuracy_score(y, clf.predict(X))
+        assert cuml_acc == 1.0
+        
+        # Verify sklearn achieves the same result
+        sk_clf = skrfc(
+            n_estimators=1,
+            bootstrap=False,
+            max_depth=None,
+            max_features=1,
+            random_state=random_state,
+        )
+        sk_clf.fit(X, y)
+        sk_acc = accuracy_score(y, sk_clf.predict(X))
+        assert sk_acc == 1.0, "sklearn baseline should also achieve perfect accuracy"
+        assert cuml_acc == sk_acc, "cuML should match sklearn behavior"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuml/tests/test_random_forest.py` around lines 1035 - 1055, The test
test_rf_feature_sampling_retries_until_valid_split currently asserts perfect
accuracy from cuML (curfc) but lacks a scikit-learn baseline; add a comparison
using sklearn.ensemble.RandomForestClassifier with matching parameters
(n_estimators=1, bootstrap=False, max_depth=None, max_features=1, random_state
set in the loop) to fit the same X,y and assert that sklearn's accuracy_score(y,
skl_clf.predict(X)) equals cuML's accuracy (or both equal 1.0) for each
random_state, so the test verifies parity between curfc and sklearn behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh`:
- Around line 90-100: The branch that returns early when k == n writes the
identity 0..n-1 instead of the per-node permutation; move the per-node RNG and
shuffle construction (rng_seed = fnv1a32_hash(seed, treeid, nodeid) and
cuda::shuffle_iterator<IdxT> shuffled_features(...)) out of the else-path so
they are available for the k==n case, and replace the direct write of
column_index with column_samples[sample_idx] = shuffled_features[column_index]
(using work_items[node_idx].idx, seed, treeid, sample_offset, and column_index
as before) so every node uses the same node-specific permutation.

---

Nitpick comments:
In `@python/cuml/tests/test_random_forest.py`:
- Around line 1035-1055: The test
test_rf_feature_sampling_retries_until_valid_split currently asserts perfect
accuracy from cuML (curfc) but lacks a scikit-learn baseline; add a comparison
using sklearn.ensemble.RandomForestClassifier with matching parameters
(n_estimators=1, bootstrap=False, max_depth=None, max_features=1, random_state
set in the loop) to fit the same X,y and assert that sklearn's accuracy_score(y,
skl_clf.predict(X)) equals cuML's accuracy (or both equal 1.0) for each
random_state, so the test verifies parity between curfc and sklearn behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: de5d1b29-ea36-4d0c-82cb-f721d7935a57

📥 Commits

Reviewing files that changed from the base of the PR and between 9526712 and 29461c6.

📒 Files selected for processing (6)
  • 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
  • cpp/src/decisiontree/batched-levelalgo/quantiles.cuh
  • cpp/src/decisiontree/batched-levelalgo/random_utils.cuh
  • python/cuml/tests/test_random_forest.py

Comment thread cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh Outdated

@dantegd dantegd 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.

Just found one potential mismatch remaining with sklearn.

Additionally, the default min_impurity_decrease=0 regression looks good for the one-informative-feature case. It would be helpful to add a small regression for nonzero min_impurity_decrease too, since that is where retry semantics can diverge from sklearn.

Comment thread cpp/src/decisiontree/batched-levelalgo/builder.cuh Outdated
@RAMitchell

RAMitchell commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

@csadorf this decreases the mean gap from about 8.30 percentage points to 0.15 percentage points but there is still something statistically different. I will go a bit deeper in another PR.

Model Accuracy Gap vs sklearn
sklearn RF (max_features="sqrt") 0.955177 ± 0.000864 0.000000
cuML RF pre-PR (max_features="sqrt") 0.872157 ± 0.001997 -0.083019
cuML RF PR (max_features="sqrt") 0.953672 ± 0.000516 -0.001504

@csadorf csadorf 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 for turning this around so quickly after our offline conversation last week.

I ran some targeted RF benchmarks and was not able to observe a performance regression from the retry logic. The expected caveat is that pathological cases where many nodes need to walk through multiple max_features chunks will be slower, but that is expected here: the change intentionally trades extra split-search work for matching sklearn's behavior when the initially sampled features cannot produce a valid partition.

I also like the significant simplification of the sampling algorithm. The PR could potentially use some extended C++ test coverage, but I am happy to approve this already.

@csadorf
csadorf requested review from dantegd and removed request for jcrist and jinsolp June 9, 2026 19:57
@csadorf

csadorf commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

/merge

@rapids-bot
rapids-bot Bot merged commit 324e39f into NVIDIA:main Jun 11, 2026
102 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working CUDA/C++ Cython / Python Cython or Python issue non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants