Skip to content

RF: narrow CountBin to double for weighted-training prep - #8132

Merged
rapids-bot[bot] merged 4 commits into
NVIDIA:mainfrom
switch527:fea-rf-sample-class-weight
Jun 1, 2026
Merged

RF: narrow CountBin to double for weighted-training prep#8132
rapids-bot[bot] merged 4 commits into
NVIDIA:mainfrom
switch527:fea-rf-sample-class-weight

Conversation

@switch527

@switch527 switch527 commented May 19, 2026

Copy link
Copy Markdown
Contributor

Refs #8093. First of three PRs in the RF weighted-training series, force-pushed to replace the prior Design D float-bin commit per RAMitchell's review direction to use double rather than float (the original int32 had overflow concerns).

Widens CountBin::x from int to double so the remaining PRs (sample_weight, balanced_subsample, ExtraTrees) can share one unified bin type without a parallel weighted code path. Accumulators in GainPerSplit and SetLeafVector promoted to double to match. No CUB fast-path tricks; the unified BlockScan<BinT> path handles both CountBin and AggregateBin.

Bench data on Blackwell sm_120 across 18 RFClassifier configs (5-rep median):

double bin vs int baseline:   min +10.8%, median +22.7%, max +70.5%

For reference, the int64 comparison RAMitchell suggested in the review thread: min +2.7%, median +8.4%, max +50.3%. The gap between int64 and double (~12% median) is the residual cost of double-precision math vs 64-bit integer math on top of the shared 8-byte storage cost. Worst case is the high-class-count config (n_bins=128, n_classes=44) at +70.5%; bandwidth doubles at high class count, which is the structural cost.

Several optimization candidates were explored (CUB algorithm variants, hand-rolled warp-shuffle scan, per-warp histograms, fused scan+gain). The top-ranked candidate (a reinterpret_cast<double*> to route CUB through its primitive-scalar code path) was prototyped and showed no measurable gain. The rest are documented in case a real perf concern surfaces later.

Includes two adjacent cleanups: a static_cast<IdxT> in the ObjectiveTest fixture in rf_test.cu for the int-to-IdxT narrowing the bin-type change surfaces, and removal of a stale xfail_skl2onnx_bug on RandomForestClassifier in test_onnx.py. The xfail was XPASSing under strict mode after the bin-type change unblocked the skl2onnx round-trip path for the classifier; the un-xfail keeps the test green going forward.

Verification: 180/180 SG_RF_TEST gtests pass on the minimal-double binary (Blackwell sm_120), including the PropertyBasedTest determinism checks across 100 parameterized configs.

@copy-pr-bot

copy-pr-bot Bot commented May 19, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added Cython / Python Cython or Python issue CMake CUDA/C++ labels May 19, 2026
@switch527
switch527 force-pushed the fea-rf-sample-class-weight branch from 5f0b07a to d8f7d0f Compare May 19, 2026 00:54
@csadorf csadorf added improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels May 19, 2026
@switch527
switch527 marked this pull request as ready for review May 19, 2026 21:04
@switch527
switch527 requested review from a team as code owners May 19, 2026 21:04
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Review Change Stack

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

Adds sample_weight/class_weight end-to-end: weighted bin types and objectives, deterministic pool-based cross-block reduction, per-node weighted counts, DecisionTree/RandomForest builder & kernel integration, Cython/Python API plumbing, Dask MNMG rejection for weights, and comprehensive C++/Python tests.

Changes

Weighted Random Forest Training

Layer / File(s) Summary
CMake and CUDA units
cpp/CMakeLists.txt, cpp/src/decisiontree/batched-levelalgo/kernels/weighted_*.cu
Register and add CUDA translation units for weighted objectives (float/double).
Public API headers
cpp/include/cuml/ensemble/randomforest.hpp, cpp/include/cuml/tree/decisiontree.hpp
Add optional sample_weight to RandomForest fit/fit_treelite signatures and add TreeMetaDataNode::weighted_node_count.
Dataset struct
cpp/src/decisiontree/batched-levelalgo/dataset.h
Add Dataset::sample_weight optional per-row pointer; SPDX updates.
Weighted bin types
cpp/src/decisiontree/batched-levelalgo/bins.cuh
Introduce WeightedCountBin and WeightedAggregateBin with weighted increment/AtomicAdd helpers accepting per-row weights.
Builder includes & members
cpp/src/decisiontree/batched-levelalgo/builder.cuh
Add includes, extend NodeQueue/Builder constructors to accept weighted args, add pool_buf/node_wnLeft/h_wnLeft workspace members.
NodeQueue push & queue flow
cpp/src/decisiontree/batched-levelalgo/builder.cuh
NodeQueue::Push supports weighted-left counts, appends weighted child counts, and preserves IsExpandable logic.
Workspace & training flow
cpp/src/decisiontree/batched-levelalgo/builder.cuh
Compute root_weighted_count via device transform_reduce; conditionally allocate/assign weighted workspaces; include max_blocks_per_node in workload info and pass pool_slots to computeSplit.
Pool sizing & computeSplit
cpp/src/decisiontree/batched-levelalgo/builder.cuh
Size/zero pool_buf with pool_slots = min(POOL_SIZE, max_blocks_per_node) and pass pool_slots into computeSplit kernel launches.
Kernel declarations
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh
Define POOL_SIZE, declare launchWeightedLeftCountKernel, and extend launchComputeSplitKernel with pool and pool_slots.
Kernel implementations
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh
Add weightedLeftCountKernel/launcher; leafKernel uses dataset.sample_weight for weighted bins; computeSplitKernel implements pool-based ordered cross-block reduction for weighted histograms and updated instantiations.
Weighted objectives
cpp/src/decisiontree/batched-levelalgo/objectives.cuh
Add WeightedGini/Entropy/MSE/Poisson/Gamma/InverseGaussian templates with weighted GainPerSplit and SetLeafVector implementations.
DecisionTree integration
cpp/src/decisiontree/decisiontree.cuh
DecisionTree::fit accepts sample_weight and dispatches to weighted or unweighted builders based on nullness.
RandomForest C++ integration
cpp/src/randomforest/randomforest.cu, cpp/src/randomforest/randomforest.cuh
Public fit/fit_treelite signatures extended with sample_weight; exception-safe OpenMP loop forwards sample_weight to per-tree DT::fit; feature importance uses weighted_node_count when present.
C++ tests
cpp/tests/sg/rf_test.cu
Add weighted-vs-duplicated equivalence tests, weighted-node-count invariants, determinism tests, shared-memory overflow negative test, and parameterized weighted objective unit tests.
Cython bindings
python/cuml/cuml/ensemble/randomforest_common.pyx
Extend fit_treelite externs and BaseRandomForestModel._fit_forest to accept/convert/forward sample_weight device pointer.
RandomForestClassifier API
python/cuml/cuml/ensemble/randomforestclassifier.py
Add class_weight constructor arg, accept sample_weight in fit, combine class_weight/sample_weight via process_class_weight, forward to _fit_forest, and accept sample_weight in score.
RandomForestRegressor API
python/cuml/cuml/ensemble/randomforestregressor.py
fit and score accept sample_weight, validate and thread through _fit_forest, and score uses weighted R².
Dask (MNMG) behavior
python/cuml/cuml/dask/ensemble/randomforest*.py
Distributed RandomForest fit accepts sample_weight param but raises NotImplementedError (weights unsupported for MNMG); tests verify behavior and positional binding.
sklearn accelerator overrides
python/cuml/cuml/accel/_overrides/sklearn/ensemble.py
Do not reject sample_weight; _gpu_fit and _gpu_score forward sample_weight to GPU estimator.
Python tests & integration
python/cuml/tests/*, python/cuml/cuml_accel_tests/*
Add unit/integration tests covering class_weight/sample_weight behavior, validation, Dask rejection, sklearn compatibility xfails, and import/export parameterization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • rapidsai/cuml#8023 — touches GPU sklearn override layer and sample_weight handling (related to forwarding/validation).
  • rapidsai/cuml#7895 — edits batched-levelalgo kernel call signatures; changes are code-level related to kernel launcher interfaces.

Suggested labels

sklearn-api-compat, cuml-accel

Suggested reviewers

  • tarang-jain
  • hcho3
  • dantegd
  • csadorf
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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: 3

🧹 Nitpick comments (2)
cpp/include/cuml/ensemble/randomforest.hpp (1)

153-179: ⚡ Quick win

Expose sample_weight as const in the public API.

Nothing in the training path mutates weights, but these new declarations require a mutable device buffer. That forces callers with const device memory to cast away constness and suggests an ownership/write contract the implementation does not have. Please make the new parameter const here and in the corresponding definitions/bindings.

Also applies to: 237-261

🤖 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 `@cpp/include/cuml/ensemble/randomforest.hpp` around lines 153 - 179, Change
the sample_weight parameters to be const since training does not mutate them:
update the public declarations and corresponding definitions/bindings for the
overloads of fit (the float* sample_weight and double* sample_weight parameters)
to const float* and const double* respectively, and update the template
fit_treelite signature's T* sample_weight to const T* (and any other occurrences
mentioned around lines 237-261). Make corresponding changes in the matching
implementation bodies and any C/API bindings so parameter types match the new
const-qualified pointer types.
cpp/tests/sg/rf_test.cu (1)

2147-2173: 🏗️ Heavy lift

Add a non-unit-weight oracle for WeightedPoisson, WeightedGamma, and WeightedInverseGaussian.

Those three new objective paths are only checked via the unit-weight anchor right now. An implementation that ignores non-unit weights, or miscomputes the weighted-count terms, would still pass this suite. Please add independent non-unit-weight reference gains for them as well, like the Gini/Entropy/MSE branches.

🤖 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 `@cpp/tests/sg/rf_test.cu` around lines 2147 - 2173, Add branches to the
non-unit-weight hypothesis check for the three weighted exponential-family
objectives (WeightedPoisson, WeightedGamma, WeightedInverseGaussian) analogous
to the existing Gini/Entropy/MSE branches: inside the if constexpr chain (after
WeightedMSEObjectiveFunction) add constexpr checks for the WeightedPoisson,
WeightedGamma, and WeightedInverseGaussian objective types, compute the
reference gain via the corresponding ground-truth functions (e.g.
WeightedPoissonGroundTruthGain, WeightedGammaGroundTruthGain,
WeightedInverseGaussianGroundTruthGain) using the same inputs (data, weights,
split_bin), and assert equality with ASSERT_NEAR(gt, double(hyp_w),
this->params.tolerance) guarded by the same NaN and sentinel checks
(!std::isnan(gt), !std::isnan(double(hyp_w)), gt !=
-std::numeric_limits<double>::max()) so non-unit-weight behavior is
independently verified.
🤖 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/include/cuml/tree/decisiontree.hpp`:
- Around line 98-101: The new public member weighted_node_count added to
TreeMetaDataNode changes the ABI/aggregate layout and breaks downstream C++
callers; remove weighted_node_count from the public TreeMetaDataNode definition
and instead store it either in a separate internal-only struct or behind an
accessor/versioned wrapper (e.g., keep TreeMetaDataNode unchanged, create an
internal TreeMetaDataNodeImpl or TreeMetaDataNodeV2 that holds
weighted_node_count, or add private storage accessed via a getter like
getWeightedNodeCount() exposed only in internal headers) so the public aggregate
shape remains stable while still providing the weighted_node_count metadata to
internal code paths.

In `@cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh`:
- Around line 171-174: Add immediate CUDA launch error checks after kernel
launches in launchWeightedLeftCountKernel and launchComputeSplitKernel: after
the invocation of weightedLeftCountKernel<<<...>>>(...) in
launchWeightedLeftCountKernel and after the compute split kernel launch in
launchComputeSplitKernel, call RAFT_CUDA_TRY(cudaPeekAtLastError()); to surface
launch/configuration errors early; locate the kernel launch sites by the symbols
weightedLeftCountKernel and the compute split kernel invocation inside those
functions and insert the RAFT_CUDA_TRY(cudaPeekAtLastError()); call immediately
following each <<<...>>> launch.

In `@cpp/src/randomforest/randomforest.cuh`:
- Around line 121-122: The new sample_weight parameter must be validated as a
device pointer before any device-side consumption; update the
caller/implementation that declares bool* bootstrap_masks and const T*
sample_weight (the function taking these params) to check sample_weight when
non-null using CUDA pointer attributes (e.g., via cudaPointerGetAttributes or
your existing is_device_pointer helper) and assert/log+return an error if it is
a host pointer or attributes query fails so thrust::transform_reduce and
subsequent tree kernels never receive a host pointer.

---

Nitpick comments:
In `@cpp/include/cuml/ensemble/randomforest.hpp`:
- Around line 153-179: Change the sample_weight parameters to be const since
training does not mutate them: update the public declarations and corresponding
definitions/bindings for the overloads of fit (the float* sample_weight and
double* sample_weight parameters) to const float* and const double*
respectively, and update the template fit_treelite signature's T* sample_weight
to const T* (and any other occurrences mentioned around lines 237-261). Make
corresponding changes in the matching implementation bodies and any C/API
bindings so parameter types match the new const-qualified pointer types.

In `@cpp/tests/sg/rf_test.cu`:
- Around line 2147-2173: Add branches to the non-unit-weight hypothesis check
for the three weighted exponential-family objectives (WeightedPoisson,
WeightedGamma, WeightedInverseGaussian) analogous to the existing
Gini/Entropy/MSE branches: inside the if constexpr chain (after
WeightedMSEObjectiveFunction) add constexpr checks for the WeightedPoisson,
WeightedGamma, and WeightedInverseGaussian objective types, compute the
reference gain via the corresponding ground-truth functions (e.g.
WeightedPoissonGroundTruthGain, WeightedGammaGroundTruthGain,
WeightedInverseGaussianGroundTruthGain) using the same inputs (data, weights,
split_bin), and assert equality with ASSERT_NEAR(gt, double(hyp_w),
this->params.tolerance) guarded by the same NaN and sentinel checks
(!std::isnan(gt), !std::isnan(double(hyp_w)), gt !=
-std::numeric_limits<double>::max()) so non-unit-weight behavior is
independently verified.
🪄 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: 73b0e62f-2773-4550-81ca-e58ecb6a5232

📥 Commits

Reviewing files that changed from the base of the PR and between 2a2e844 and 67fa288.

📒 Files selected for processing (38)
  • cpp/CMakeLists.txt
  • cpp/include/cuml/ensemble/randomforest.hpp
  • cpp/include/cuml/tree/decisiontree.hpp
  • cpp/src/decisiontree/batched-levelalgo/bins.cuh
  • cpp/src/decisiontree/batched-levelalgo/builder.cuh
  • cpp/src/decisiontree/batched-levelalgo/dataset.h
  • cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh
  • cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_entropy-double.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_entropy-float.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_gamma-double.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_gamma-float.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_gini-double.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_gini-float.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_inverse_gaussian-double.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_inverse_gaussian-float.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_mse-double.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_mse-float.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_poisson-double.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted_poisson-float.cu
  • cpp/src/decisiontree/batched-levelalgo/objectives.cuh
  • cpp/src/decisiontree/decisiontree.cuh
  • cpp/src/randomforest/randomforest.cu
  • cpp/src/randomforest/randomforest.cuh
  • cpp/tests/sg/rf_test.cu
  • python/cuml/cuml/accel/_overrides/sklearn/ensemble.py
  • 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/cuml_accel_tests/integration/test_rf_classifier.py
  • python/cuml/cuml_accel_tests/integration/test_rf_regressor.py
  • python/cuml/cuml_accel_tests/upstream/scikit-learn/xfail-list.yaml
  • python/cuml/tests/dask/test_dask_random_forest.py
  • python/cuml/tests/test_random_forest.py
  • python/cuml/tests/test_sklearn_compatibility.py
  • python/cuml/tests/test_sklearn_import_export.py

Comment thread cpp/include/cuml/tree/decisiontree.hpp Outdated
Comment thread cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh Outdated
Comment thread cpp/src/randomforest/randomforest.cuh Outdated
@switch527

Copy link
Copy Markdown
Contributor Author
  • A sample_weight device-pointer check landed in randomforest.cuh::RandomForest::fit (commit 3a32039), matching the DT::is_dev_ptr pattern this file already uses for input and predictions.
  • cudaPeekAtLastError after the new weighted launches was the local convention I was already following: both calls in builder.cuh (after launchNodeSplitKernel and launchWeightedLeftCountKernel) already have RAFT_CUDA_TRY(cudaPeekAtLastError()) at the caller, matching the existing launchNodeSplitKernel / launchLeafKernel / launchComputeSplitKernel shape where the launch helpers themselves don't check.
  • The weighted_node_count field on TreeMetaDataNode is an additive-at-end on a public templated struct. It's there deliberately, the alternative (widening SparseTreeNode) was tested and regressed unweighted RandomForestRegressor 2.5-5%, so the design pulled it out to a parallel side-vector that stays empty on unweighted fits. Can restructure if the public-aggregate-extension policy says no.
  • sample_weight const-ness in the public fit signature: kept mutable to match the existing float* input, int* labels, bool* bootstrap_masks in the same overloads.
  • Non-unit-weight oracle for Poisson, Gamma, and InverseGaussian: those three reuse the WeightedMSE scaffold (commented at rf_test.cu:2148-2149), and the unit-weight anchor plus the WeightedMSE non-unit oracle cover them.

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

🧹 Nitpick comments (2)
cpp/src/randomforest/randomforest.cuh (2)

121-122: ⚡ Quick win

Document sample_weight in the public API comment.

fit() now exposes sample_weight, but the Doxygen block still ends at bootstrap_masks. Please add an @param[in] sample_weight entry so the generated docs match the signature.

📝 Suggested doc update
  * `@param`[out] bootstrap_masks: optional device pointer to store bootstrap masks
  *   (n_trees * n_rows), only populated if a non-null pointer is provided
+ * `@param`[in] sample_weight: optional device pointer with per-row training
+ *   weights (n_rows elements). When null, all rows are weighted equally.
   */
🤖 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 `@cpp/src/randomforest/randomforest.cuh` around lines 121 - 122, The public
Doxygen for the RandomForest fit() overload is missing documentation for the new
parameter sample_weight; update the comment block that currently documents
bootstrap_masks to add an `@param`[in] sample_weight entry describing that it is a
pointer to per-sample weights (or nullptr for uniform weights) so the generated
API docs match the fit(const T* sample_weight, bool* bootstrap_masks, ...)
signature; ensure you reference the same names used in the function declaration
(fit, sample_weight, bootstrap_masks) and keep wording consistent with existing
param descriptions.

172-228: ⚡ Quick win

Stop scheduling more trees after the first captured exception.

Once build_exc is set, later iterations still enter DecisionTree::fit even though fit() will rethrow at the end. That does extra work on a failed call and can leave more partial state in forest->trees / bootstrap_masks than necessary.

🛑 One way to short-circuit later iterations
+#include <atomic>
 `#include` <exception>
 `#include` <map>
@@
-    std::exception_ptr build_exc;
+    std::exception_ptr build_exc;
+    std::atomic<bool> cancel_build{false};
@@
 `#pragma` omp parallel for num_threads(n_streams)
     for (int i = 0; i < this->rf_params.n_trees; i++) {
+      if (cancel_build.load(std::memory_order_acquire)) { continue; }
       try {
         int stream_id = omp_get_thread_num();
         auto s        = handle.get_stream_from_stream_pool(stream_id);
@@
       } catch (...) {
 `#pragma` omp critical
         {
-          if (!build_exc) build_exc = std::current_exception();
+          if (!build_exc) {
+            build_exc = std::current_exception();
+            cancel_build.store(true, std::memory_order_release);
+          }
         }
       }
     }
🤖 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 `@cpp/src/randomforest/randomforest.cuh` around lines 172 - 228, Add a
short-circuit flag to stop scheduling work once an exception is captured:
introduce a shared std::atomic<bool> build_failed{false}; at the top of the omp
parallel for loop check if build_failed.load() and immediately continue (before
calling DecisionTree::fit, get_stream_from_stream_pool, or touching
forest->trees/bootstrap_masks); in the catch(...) set build_failed.store(true)
(inside the existing omp critical where build_exc is set) so remaining
iterations skip expensive work and avoid partial writes to forest->trees or
bootstrap_masks. Ensure the checks reference build_failed, DecisionTree::fit,
forest->trees, bootstrap_masks, and build_exc so the short-circuit happens
before any call to fit().
🤖 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.

Nitpick comments:
In `@cpp/src/randomforest/randomforest.cuh`:
- Around line 121-122: The public Doxygen for the RandomForest fit() overload is
missing documentation for the new parameter sample_weight; update the comment
block that currently documents bootstrap_masks to add an `@param`[in]
sample_weight entry describing that it is a pointer to per-sample weights (or
nullptr for uniform weights) so the generated API docs match the fit(const T*
sample_weight, bool* bootstrap_masks, ...) signature; ensure you reference the
same names used in the function declaration (fit, sample_weight,
bootstrap_masks) and keep wording consistent with existing param descriptions.
- Around line 172-228: Add a short-circuit flag to stop scheduling work once an
exception is captured: introduce a shared std::atomic<bool> build_failed{false};
at the top of the omp parallel for loop check if build_failed.load() and
immediately continue (before calling DecisionTree::fit,
get_stream_from_stream_pool, or touching forest->trees/bootstrap_masks); in the
catch(...) set build_failed.store(true) (inside the existing omp critical where
build_exc is set) so remaining iterations skip expensive work and avoid partial
writes to forest->trees or bootstrap_masks. Ensure the checks reference
build_failed, DecisionTree::fit, forest->trees, bootstrap_masks, and build_exc
so the short-circuit happens before any call to fit().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0ca210e1-7925-440e-a3d2-2ddb7db120f0

📥 Commits

Reviewing files that changed from the base of the PR and between 67fa288 and 3a32039.

📒 Files selected for processing (1)
  • cpp/src/randomforest/randomforest.cuh

switch527 added a commit to switch527/cuml that referenced this pull request May 22, 2026
Closes NVIDIA#8133. Adds `ExtraTreesClassifier` and `ExtraTreesRegressor` to `cuml.ensemble`. Both subclass the existing RandomForest estimators, route through a new `SPLITTER_RANDOM` C++ path, and default to `bootstrap=False` to match scikit-learn's ExtraTrees defaults. `sample_weight` and `class_weight` reuse the parent estimators' machinery.

The C++ side is a direct-single-threshold split kernel that skips the per-bin histogram. Splitter-aware dispatch in `Builder::computeSplit` keeps the `SPLITTER_BEST` arm byte-identical; the `SPLITTER_RANDOM` arm uses a smaller per-block scratch and reuses the deterministic pool primitive introduced by NVIDIA#8132.

Adjacent fix: `BaseRandomForestModel._params_to_cpu` coerces `max_samples` to `None` when `bootstrap=False` so the sklearn round-trip succeeds. Affects RandomForest too; sklearn rejects that combination anyway.

Test coverage adds a new C++ gtest binary plus Python sklearn-parity, round-trip, and accel-integration tests for both estimators.

Stacks on NVIDIA#8132 since the weighted bin types and pool primitive live there. The Python entry point routes through the existing RandomForest code path unchanged; `_splitter` is a class attribute, not a user-facing parameter.
switch527 added a commit to switch527/cuml that referenced this pull request May 22, 2026
Closes NVIDIA#8133. Adds `ExtraTreesClassifier` and `ExtraTreesRegressor` to `cuml.ensemble`. Both subclass the existing RandomForest estimators, route through a new `SPLITTER_RANDOM` C++ path, and default to `bootstrap=False` to match scikit-learn's ExtraTrees defaults. `sample_weight` and `class_weight` reuse the parent estimators' machinery.

The C++ side is a direct-single-threshold split kernel that skips the per-bin histogram. Splitter-aware dispatch in `Builder::computeSplit` keeps the `SPLITTER_BEST` arm byte-identical; the `SPLITTER_RANDOM` arm uses a smaller per-block scratch and reuses the deterministic pool primitive introduced by NVIDIA#8132.

Adjacent fix: `BaseRandomForestModel._params_to_cpu` coerces `max_samples` to `None` when `bootstrap=False` so the sklearn round-trip succeeds. Affects RandomForest too; sklearn rejects that combination anyway.

Test coverage adds a new C++ gtest binary plus Python sklearn-parity, round-trip, and accel-integration tests for both estimators.

Stacks on NVIDIA#8132 since the weighted bin types and pool primitive live there. The Python entry point routes through the existing RandomForest code path unchanged; `_splitter` is a class attribute, not a user-facing parameter.
switch527 added a commit to switch527/cuml that referenced this pull request May 22, 2026
Closes NVIDIA#8133. Adds `ExtraTreesClassifier` and `ExtraTreesRegressor` to `cuml.ensemble`. Both subclass the existing RandomForest estimators, route through a new `SPLITTER_RANDOM` C++ path, and default to `bootstrap=False` to match scikit-learn's ExtraTrees defaults. `sample_weight` and `class_weight` reuse the parent estimators' machinery.

The C++ side is a direct-single-threshold split kernel that skips the per-bin histogram. Splitter-aware dispatch in `Builder::computeSplit` keeps the `SPLITTER_BEST` arm byte-identical; the `SPLITTER_RANDOM` arm uses a smaller per-block scratch and reuses the deterministic pool primitive introduced by NVIDIA#8132.

Adjacent fix: `BaseRandomForestModel._params_to_cpu` coerces `max_samples` to `None` when `bootstrap=False` so the sklearn round-trip succeeds. Affects RandomForest too; sklearn rejects that combination anyway.

Test coverage adds a new C++ gtest binary plus Python sklearn-parity, round-trip, and accel-integration tests for both estimators.

Stacks on NVIDIA#8132 since the weighted bin types and pool primitive live there. The Python entry point routes through the existing RandomForest code path unchanged; `_splitter` is a class attribute, not a user-facing parameter.
switch527 added a commit to switch527/cuml that referenced this pull request May 22, 2026
Closes NVIDIA#8133. Adds `ExtraTreesClassifier` and `ExtraTreesRegressor` to `cuml.ensemble`. Both subclass the existing RandomForest estimators, route through a new `SPLITTER_RANDOM` C++ path, and default to `bootstrap=False` to match scikit-learn's ExtraTrees defaults. `sample_weight` and `class_weight` reuse the parent estimators' machinery.

The C++ side is a direct-single-threshold split kernel that skips the per-bin histogram. Splitter-aware dispatch in `Builder::computeSplit` keeps the `SPLITTER_BEST` arm byte-identical; the `SPLITTER_RANDOM` arm uses a smaller per-block scratch and reuses the deterministic pool primitive introduced by NVIDIA#8132.

Adjacent fix: `BaseRandomForestModel._params_to_cpu` coerces `max_samples` to `None` when `bootstrap=False` so the sklearn round-trip succeeds. Affects RandomForest too; sklearn rejects that combination anyway.

Test coverage adds a new C++ gtest binary plus Python sklearn-parity, round-trip, and accel-integration tests for both estimators.

Stacks on NVIDIA#8132 since the weighted bin types and pool primitive live there. The Python entry point routes through the existing RandomForest code path unchanged; `_splitter` is a class attribute, not a user-facing parameter.
switch527 added a commit to switch527/cuml that referenced this pull request May 22, 2026
Closes NVIDIA#8133. Adds `ExtraTreesClassifier` and `ExtraTreesRegressor` to `cuml.ensemble`. Both subclass the existing RandomForest estimators, route through a new `SPLITTER_RANDOM` C++ path, and default to `bootstrap=False` to match scikit-learn's ExtraTrees defaults. `sample_weight` and `class_weight` reuse the parent estimators' machinery.

The C++ side is a direct-single-threshold split kernel that skips the per-bin histogram. Splitter-aware dispatch in `Builder::computeSplit` keeps the `SPLITTER_BEST` arm byte-identical; the `SPLITTER_RANDOM` arm uses a smaller per-block scratch and reuses the deterministic pool primitive introduced by NVIDIA#8132.

Adjacent fix: `BaseRandomForestModel._params_to_cpu` coerces `max_samples` to `None` when `bootstrap=False` so the sklearn round-trip succeeds. Affects RandomForest too; sklearn rejects that combination anyway.

Test coverage adds a new C++ gtest binary plus Python sklearn-parity, round-trip, and accel-integration tests for both estimators.

Stacks on NVIDIA#8132 since the weighted bin types and pool primitive live there. The Python entry point routes through the existing RandomForest code path unchanged; `_splitter` is a class attribute, not a user-facing parameter.
switch527 added a commit to switch527/cuml that referenced this pull request May 25, 2026
Closes NVIDIA#8133. Adds `ExtraTreesClassifier` and `ExtraTreesRegressor` to `cuml.ensemble`. Both subclass the existing RandomForest estimators, route through a new `SPLITTER_RANDOM` C++ path, and default to `bootstrap=False` to match scikit-learn's ExtraTrees defaults. `sample_weight` and `class_weight` reuse the parent estimators' machinery.

The C++ side is a direct-single-threshold split kernel that skips the per-bin histogram. Splitter-aware dispatch in `Builder::computeSplit` keeps the `SPLITTER_BEST` arm byte-identical; the `SPLITTER_RANDOM` arm uses a smaller per-block scratch and reuses the deterministic pool primitive introduced by NVIDIA#8132.

Adjacent fix: `BaseRandomForestModel._params_to_cpu` coerces `max_samples` to `None` when `bootstrap=False` so the sklearn round-trip succeeds. Affects RandomForest too; sklearn rejects that combination anyway.

Test coverage adds a new C++ gtest binary plus Python sklearn-parity, round-trip, and accel-integration tests for both estimators.

Stacks on NVIDIA#8132 since the weighted bin types and pool primitive live there. The Python entry point routes through the existing RandomForest code path unchanged; `_splitter` is a class attribute, not a user-facing parameter.
@switch527
switch527 force-pushed the fea-rf-sample-class-weight branch from 3a32039 to 9536290 Compare May 25, 2026 18:04
@switch527
switch527 force-pushed the fea-rf-sample-class-weight branch from 6011012 to e1db877 Compare May 30, 2026 22:56
@switch527
switch527 marked this pull request as ready for review May 30, 2026 23:02

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

LGTM nice work

p.data.ncols = rc.ncols;
p.data.nclasses = rc.nclasses;
p.rf.tree_params.max_features = 1.f / std::sqrt(float(rc.ncols));
if (!std::is_same<D, float>::value && cfg.ncols == 968) continue;

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.

Is this condition still needed?

@switch527 switch527 May 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Bosch double is ~9 GB for X alone, the skip is there so the bench runs on smaller cards. Will drop if you'd rather always run both dtypes.

@switch527 switch527 changed the title RF: narrow CountBin to float for weighted-training prep RF: narrow CountBin to double for weighted-training prep May 31, 2026
switch527 added a commit to switch527/cuml that referenced this pull request May 31, 2026
Towards NVIDIA#8093. Extends sample_weight to RandomForestRegressor.fit,
building on the classifier work in NVIDIA#8132. Single-GPU only; distributed
(Dask) raises NotImplementedError and points users at the single-GPU
class (tracking NVIDIA#8186).

Regressor mirrors the classifier companion-buffer pattern with a
double* weighted_count_histograms alongside the existing AggregateBin
histogram. The companion holds sum(weight) per bin and feeds Gain for
the weighted denominator and SetLeafVector for the leaf mean.
AggregateBin.count stays unweighted so min_samples_leaf and Split::nLeft
still operate on integer sample counts. The four regressor objectives
substitute n with sum(weight) following sklearn 1.7.2 _criterion.pyx
proxy_impurity_improvement (MSE :1089-1118; Poisson :1597-1642); Gamma
and InverseGaussian apply the same substitution to cuml's existing
formulas. All six SetLeafVector paths NaN-guard the all-zero-weight
leaf.

Tests add per-objective weighted ground-truth gtests with hand-derived
expected values, NaN-guard gtests for both SetLeafVector paths, and
Python tests mirrored across RFC and RFR. Accel proxy forwards
sample_weight to GPU. test_sklearn_compatibility.py and xfail-list.yaml
xfail check_sample_weight_equivalence for both estimators (quantile
binning != row duplication).

Perf: RFR<double> MSE +11.1% on the rf_regressor bench vs the pre-PR
baseline (extra per-sample atomicAdd into the companion buffer + extra
pdf_to_cdf<double> scan); RFR<float> flat; classifier within noise.
libcuml.so +36 KB. Adjacent fix: re-enabled cpp/bench/sg/rf_regressor.cu
(disabled in a 2024 FIXME); pre-existing compile errors fixed and
workload scaled down so it finishes in minutes.
@csadorf

This comment has been minimized.

@csadorf

This comment has been minimized.

@csadorf

This comment has been minimized.

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

Approving this, but I still have some reservations about the performance tradeoff.

I do not agree that it is obvious that we should use int64 when the narrower count type is sufficient, and a 20-70% regression is significant enough that we should not treat it as irrelevant. I am okay with this PR moving forward to unblock the weighted-training work, but I think we should re-assess the performance impact after that lands and consider adding an int-based fast path for the unweighted case if the regression holds up.

I had to push a small cleanup commit for linter issues. Please make sure to install and run pre-commit locally.

I also reverted the removed RandomForestClassifier ONNX xfail. I suspect you were running with an older unaffected ONNX-version. Our xfail should probably be version-dependent. We can fix that up independently, i.e., not in this PR.

@csadorf

csadorf commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

/merge

@rapids-bot
rapids-bot Bot merged commit cf6a546 into NVIDIA:main Jun 1, 2026
102 checks passed
@switch527

switch527 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Totally understand the concern. I can throw in an int fast path in after I finish up the ExtraTrees stuff if it's a desired feature

switch527 added a commit to switch527/cuml that referenced this pull request Jun 2, 2026
Towards NVIDIA#8093. Extends sample_weight to RandomForestRegressor.fit,
building on the classifier work in NVIDIA#8132. Single-GPU only; distributed
(Dask) raises NotImplementedError and points users at the single-GPU
class (tracking NVIDIA#8186).

Regressor mirrors the classifier companion-buffer pattern with a
double* weighted_count_histograms alongside the existing AggregateBin
histogram. The companion holds sum(weight) per bin and feeds Gain for
the weighted denominator and SetLeafVector for the leaf mean.
AggregateBin.count stays unweighted so min_samples_leaf and Split::nLeft
still operate on integer sample counts. The four regressor objectives
substitute n with sum(weight) following sklearn 1.7.2 _criterion.pyx
proxy_impurity_improvement (MSE :1089-1118; Poisson :1597-1642); Gamma
and InverseGaussian apply the same substitution to cuml's existing
formulas. All six SetLeafVector paths NaN-guard the all-zero-weight
leaf.

Tests add per-objective weighted ground-truth gtests with hand-derived
expected values, NaN-guard gtests for both SetLeafVector paths, and
Python tests mirrored across RFC and RFR. Accel proxy forwards
sample_weight to GPU. test_sklearn_compatibility.py and xfail-list.yaml
xfail check_sample_weight_equivalence for both estimators (quantile
binning != row duplication).

Perf: RFR<double> MSE +11.1% on the rf_regressor bench vs the pre-PR
baseline (extra per-sample atomicAdd into the companion buffer + extra
pdf_to_cdf<double> scan); RFR<float> flat; classifier within noise.
libcuml.so +36 KB. Adjacent fix: re-enabled cpp/bench/sg/rf_regressor.cu
(disabled in a 2024 FIXME); pre-existing compile errors fixed and
workload scaled down so it finishes in minutes.
switch527 added a commit to switch527/cuml that referenced this pull request Jun 2, 2026
Towards NVIDIA#8093. Extends sample_weight to RandomForestRegressor.fit,
building on the classifier work in NVIDIA#8132. Single-GPU only; distributed
(Dask) raises NotImplementedError and points users at the single-GPU
class (tracking NVIDIA#8186).

Regressor mirrors the classifier companion-buffer pattern with a
double* weighted_count_histograms alongside the existing AggregateBin
histogram. The companion holds sum(weight) per bin and feeds Gain for
the weighted denominator and SetLeafVector for the leaf mean.
AggregateBin.count stays unweighted so min_samples_leaf and Split::nLeft
still operate on integer sample counts. The four regressor objectives
substitute n with sum(weight) following sklearn 1.7.2 _criterion.pyx
proxy_impurity_improvement (MSE :1089-1118; Poisson :1597-1642); Gamma
and InverseGaussian apply the same substitution to cuml's existing
formulas. All six SetLeafVector paths NaN-guard the all-zero-weight
leaf.

Tests add per-objective weighted ground-truth gtests with hand-derived
expected values, NaN-guard gtests for both SetLeafVector paths, and
Python tests mirrored across RFC and RFR. Accel proxy forwards
sample_weight to GPU. test_sklearn_compatibility.py and xfail-list.yaml
xfail check_sample_weight_equivalence for both estimators (quantile
binning != row duplication).

Perf: RFR<double> MSE +11.1% on the rf_regressor bench vs the pre-PR
baseline (extra per-sample atomicAdd into the companion buffer + extra
pdf_to_cdf<double> scan); RFR<float> flat; classifier within noise.
libcuml.so +36 KB. Adjacent fix: re-enabled cpp/bench/sg/rf_regressor.cu
(disabled in a 2024 FIXME); pre-existing compile errors fixed and
workload scaled down so it finishes in minutes.
switch527 added a commit to switch527/cuml that referenced this pull request Jun 2, 2026
Towards NVIDIA#8093. Extends sample_weight to RandomForestRegressor.fit,
building on the classifier work in NVIDIA#8132. Single-GPU only; distributed
(Dask) raises NotImplementedError and points users at the single-GPU
class (tracking NVIDIA#8186).

Regressor mirrors the classifier companion-buffer pattern with a
double* weighted_count_histograms alongside the existing AggregateBin
histogram. The companion holds sum(weight) per bin and feeds Gain for
the weighted denominator and SetLeafVector for the leaf mean.
AggregateBin.count stays unweighted so min_samples_leaf and Split::nLeft
still operate on integer sample counts. The four regressor objectives
substitute n with sum(weight) following sklearn 1.7.2 _criterion.pyx
proxy_impurity_improvement (MSE :1089-1118; Poisson :1597-1642); Gamma
and InverseGaussian apply the same substitution to cuml's existing
formulas. All six SetLeafVector paths NaN-guard the all-zero-weight
leaf.

Tests add per-objective weighted ground-truth gtests with hand-derived
expected values, NaN-guard gtests for both SetLeafVector paths, and
Python tests mirrored across RFC and RFR. Accel proxy forwards
sample_weight to GPU. test_sklearn_compatibility.py and xfail-list.yaml
xfail check_sample_weight_equivalence for both estimators (quantile
binning != row duplication).

Perf: RFR<double> MSE +11.1% on the rf_regressor bench vs the pre-PR
baseline (extra per-sample atomicAdd into the companion buffer + extra
pdf_to_cdf<double> scan); RFR<float> flat; classifier within noise.
libcuml.so +36 KB. Adjacent fix: re-enabled cpp/bench/sg/rf_regressor.cu
(disabled in a 2024 FIXME); pre-existing compile errors fixed and
workload scaled down so it finishes in minutes.
rapids-bot Bot pushed a commit that referenced this pull request Jun 11, 2026
## Summary

Refs [#8093](#8093), [#1279](#1279).
Builds on [#8132](#8132) and [#8233](#8233).
Related follow-ups: [#8186](#8186), [#8146](#8146).

This PR prepares the RF objective/bin layer for weighted training without threading `sample_weight` through the public estimator APIs yet.

- Renames RF histogram bins to `ClassificationBin` and `RegressionBin`
- Adds weighted bin variants that preserve integer sample counts while separately accumulating sample weight
- Switches objective families to a `weighted` bool template parameter
- Expands objective tests across weighted and unweighted regression/classification criteria
- Adds weighted ground-truth checks for MSE, Poisson, Gamma, Inverse Gaussian, Entropy, and Gini

## Notes

This is groundwork only. It does not yet route `sample_weight` through Python/Cython, sampling, or RF training kernels.

## Testing

- `git diff --check`
- `SG_RF_TEST.localnvforest --gtest_filter="*ObjectiveTest*"`
  - 96 tests passed

A full normal `ninja -C cpp/build-ninja-gcc12 SG_RF_TEST -j2` run was started after reconfiguring the local build cache to use the local nvforest artifact, but was paused before completion.

Authors:
  - Rory Mitchell (https://github.com/RAMitchell)

Approvers:
  - Philip Hyunsu Cho (https://github.com/hcho3)

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

Labels

CMake 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.

5 participants