Skip to content

Add distributed RF C++ tree builder core - #8255

Closed
RAMitchell wants to merge 6 commits into
NVIDIA:mainfrom
RAMitchell:codex/distributed-rf-cpp-core
Closed

Add distributed RF C++ tree builder core#8255
RAMitchell wants to merge 6 commits into
NVIDIA:mainfrom
RAMitchell:codex/distributed-rf-cpp-core

Conversation

@RAMitchell

Copy link
Copy Markdown
Contributor

Summary

This PR implements the next C++ stage of distributed Random Forest training: workers now collaborate on split finding by all-reducing per-rank histograms, then applying the same globally selected split while partitioning only local row IDs.

This builds on the distributed quantile work in #8111 and #8190. It is a step toward #7969, and is related to the existing multi-GPU RF accuracy reports in #4429, #2437, and #4740.

Refs #7969
Refs #4429
Refs #2437
Refs #4740

Changes

  • Add distributed histogram aggregation in the batched-level RF tree builder.
  • Track both global and rank-local split counts:
    • global counts are used for tree metadata and split validity.
    • local counts are used for row partitioning on each rank.
  • Preserve the single-GPU path when RAFT comms are not initialized.
  • Allow distributed ranks with zero local rows.
  • Serialize tree construction in distributed mode so collectives are issued in a consistent order.
  • Add MG RF tests that validate:
    • globally consistent tree structure across ranks.
    • global node instance counts.
    • uneven and skewed data partitions.
    • classification and regression cases.
  • Fix MG histogram packing/unpacking issues found while running the tests.
  • Update feature importance normalization to handle infinite split metrics robustly.

Notes

This PR does not yet add the Python/Dask estimator interface wiring. That should be a follow-up stage after the C++ distributed builder path lands.

Testing

  • git diff --check
  • cmake --build cpp/build --target MG_RF_TEST MG_RF_QUANTILE_TEST -j8
  • mpiexec -np 2 ./cpp/build/tests/MG_RF_QUANTILE_TEST
  • mpiexec -np 2 ./cpp/build/tests/MG_RF_TEST
  • mpiexec -np 4 ./cpp/build/tests/MG_RF_QUANTILE_TEST
  • mpiexec -np 4 ./cpp/build/tests/MG_RF_TEST

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 adds the C++ core needed for distributed (multi-rank) RandomForest tree building in the batched-level decision tree trainer by all-reducing per-rank histograms to select globally consistent splits, while keeping rank-local row partitioning and preserving the single-GPU behavior when distributed comms aren’t initialized.

Changes:

  • Implement distributed histogram aggregation (all-reduce) for split finding and leaf histogram aggregation; track global vs rank-local counts for correct metadata vs local partitioning.
  • Update RF training to support ranks with zero local rows, and serialize distributed tree building to ensure consistent collective ordering.
  • Add Multi-GPU RF property tests and extend quantile MG test harness; improve feature-importance normalization robustness with infinite split metrics.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cpp/tests/sg/rf_test.cu Adds SG coverage to ensure empty global row training is rejected.
cpp/tests/mg/rf_test.cu New MG RF property tests for distributed consistency across ranks/partitions.
cpp/tests/mg/rf_quantile_test.cu Adds an MPI-aware main() to run MG quantile tests under MPI.
cpp/tests/CMakeLists.txt Registers the new MG RF test target.
cpp/src/randomforest/randomforest.cuh Enables distributed-mode behavior (allow empty local rows, force serial trees, ensure correct device in OMP threads).
cpp/src/randomforest/randomforest.cu Makes feature-importance normalization robust to infinite split contributions.
cpp/src/decisiontree/batched-levelalgo/split.cuh Extends split metadata to include global + local left counts using 64-bit counters.
cpp/src/decisiontree/batched-levelalgo/quantiles.cuh Allows n_rows==0 locally in distributed mode; guards null data pointer accordingly.
cpp/src/decisiontree/batched-levelalgo/objectives.cuh Promotes split-count math to 64-bit and adapts objectives to updated split representation.
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh Introduces split-histogram/evaluation kernel split and histogram pack/unpack helpers for all-reduce.
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh Implements the two-phase histogram build + split evaluation flow; updates node split/leaf logic for global+local counts.
cpp/src/decisiontree/batched-levelalgo/builder.cuh Adds distributed all-reduce for histograms and leaf histograms; wires global sampled-row counts into tree metadata.
cpp/src/decisiontree/batched-levelalgo/bins.cuh Switches histogram bin counts to 64-bit integer counters to avoid overflow.

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

Comment thread cpp/tests/mg/rf_test.cu
Comment on lines +168 to +183
for (size_t i = 0; i < rows.size(); ++i) {
int global_row = rows[i];
DataT signal = static_cast<DataT>((global_row % 97) - 48);
for (int col = 0; col < params.n_cols; ++col) {
DataT feature = signal * static_cast<DataT>(col + 1);
feature += static_cast<DataT>(((global_row + 13 * col + params.seed) % 11) - 5) /
static_cast<DataT>(10);
X[i * params.n_cols + col] = feature;
}
if constexpr (std::is_integral_v<LabelT>) {
y[i] = (signal >= DataT(0)) ? 1 : 0;
if (params.n_labels > 2 && global_row % 17 == 0) { y[i] = 2; }
} else {
y[i] = signal * DataT(0.5) + static_cast<DataT>((global_row % 7) - 3);
}
}

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.

Addressed in 3f06f4e: make_local_dataset now writes the training buffer in column-major layout with X[col * rows.size() + i], matching fit() / the batched-level tree builder. The prediction path remains row-major and no longer transposes before predict().

Comment thread cpp/tests/mg/rf_test.cu Outdated
Comment on lines +327 to +333
rmm::device_uvector<DataT> X(h_X.size(), handle.get_stream());
rmm::device_uvector<DataT> X_transpose(h_X.size(), handle.get_stream());
rmm::device_uvector<LabelT> predictions(params.n_rows, handle.get_stream());
raft::update_device(X.data(), h_X.data(), h_X.size(), handle.get_stream());
raft::linalg::transpose(
handle, X.data(), X_transpose.data(), params.n_rows, params.n_cols, handle.get_stream());
predict(handle, forest, X_transpose.data(), params.n_rows, params.n_cols, predictions.data());
@coderabbitai

coderabbitai Bot commented Jun 11, 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

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced distributed multi-GPU decision tree building with global histogram-based split/leaf computation for more consistent results across ranks.
  • Bug Fixes

    • Improved handling of empty local partitions during distributed training.
    • More robust random forest feature-importance calculations for non-finite floating-point values.
    • Improved quantile computation behavior when distributed ranks have zero rows.
  • Tests

    • Added/extended MPI-based multi-GPU RandomForest property tests covering multiple row partitioning strategies.
    • Added a test to confirm training with zero global rows is rejected.

Walkthrough

Refactors batched decision-tree and RandomForest training for distributed multi-GPU execution. Split<DataT> now tracks global and local left counts, split and leaf evaluation use histogram staging with distributed all-reduce, Builder adds distributed workspace and orchestration, runtime checks allow zero-row local partitions, and MPI-based validation tests are added.

Changes

Distributed Random Forest Training

Layer / File(s) Summary
Count and split contracts
cpp/src/decisiontree/batched-levelalgo/split.cuh, cpp/src/decisiontree/batched-levelalgo/objectives.cuh
Split drops the IdxT template parameter and stores global_nLeft and local_nLeft; reduction and best-split evaluation propagate both counts. Classification and regression objective gain helpers switch to CountT count parameters and return Split<DataT>.
Kernel API and two-stage staging
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh, cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh
WorkloadInfo becomes non-templated, split validity checks are simplified, and node split, split evaluation, and leaf prediction move to histogram/finalize kernels with packing helpers for structured bins.
Distributed builder orchestration
cpp/src/decisiontree/batched-levelalgo/builder.cuh
NodeQueue accepts global sampled rows and uses global/local counts in Push; Builder tracks distributed state, allocates packed histogram workspace, computes global sampled rows with all-reduce, and routes split/leaf execution through distributed histogram reduction helpers.
Runtime checks and feature-importance handling
cpp/src/decisiontree/batched-levelalgo/quantiles.cuh, cpp/src/randomforest/randomforest.cuh, cpp/src/randomforest/randomforest.cu
computeQuantiles allows zero-row distributed inputs. RandomForest::fit relaxes empty local-row checks in distributed mode and sets device/stream behavior accordingly. Feature importances accumulate in double with separate handling for positive-infinite contributions.
Distributed validation tests and MPI wiring
cpp/tests/CMakeLists.txt, cpp/tests/mg/rf_test.cu, cpp/tests/mg/rf_quantile_test.cu, cpp/tests/sg/rf_test.cu
Adds the MPI-enabled RF_TEST target, a multi-GPU RandomForest test executable with cross-rank hashing and invariant checks, an MPI main for the quantile test, and a single-GPU zero-global-row rejection test.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • rapidsai/cuml#8190: Both PRs modify cpp/src/decisiontree/batched-levelalgo/quantiles.cuh for distributed quantile behavior.
  • rapidsai/cuml#8247: Both PRs touch the batched-level decision-tree split/objective pipeline in objectives.cuh and related builder integration.
  • rapidsai/cuml#8257: Both PRs change the batched builder split execution path around launchNodeSplitKernel and WorkloadInfo.

Suggested reviewers

  • jcrist
  • hcho3
  • viclafargue
  • dantegd
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.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 summarizes the main change: adding distributed Random Forest C++ tree builder core.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the distributed RF builder work.
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

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/src/decisiontree/batched-levelalgo/split.cuh (1)

1-4: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update copyright year to 2026.

The copyright header shows 2019-2022 but should reflect the current year 2019-2026.

Proposed fix
 /*
- * SPDX-FileCopyrightText: Copyright (c) 2019-2022, NVIDIA CORPORATION.
+ * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION.
  * SPDX-License-Identifier: Apache-2.0
  */
🤖 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/decisiontree/batched-levelalgo/split.cuh` around lines 1 - 4, Update
the file header comment block at the top of split.cuh by changing the copyright
year range from "2019-2022" to "2019-2026" so the SPDX header reads
"SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION." and keep
the SPDX-License-Identifier unchanged.

Source: Coding guidelines

🧹 Nitpick comments (2)
cpp/src/decisiontree/batched-levelalgo/objectives.cuh (1)

283-288: 💤 Low value

Consider adding division-by-zero guard in SetLeafVector.

If shist[i].count is zero, this produces Inf/NaN. While leaf nodes should always have samples (upstream logic should prevent this), a defensive epsilon or guard would improve robustness.

Optional defensive fix
   static DI void SetLeafVector(BinT const* shist, int nclasses, DataT* out)
   {
     for (int i = 0; i < nclasses; i++) {
-      out[i] = shist[i].label_sum / shist[i].count;
+      out[i] = shist[i].count > 0 ? shist[i].label_sum / shist[i].count : DataT(0);
     }
   }
🤖 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/decisiontree/batched-levelalgo/objectives.cuh` around lines 283 -
288, SetLeafVector can produce Inf/NaN when shist[i].count == 0; update the loop
in SetLeafVector to guard the division by checking shist[i].count for zero and
handle it defensively (e.g., if count == 0 set out[i] to 0 (or a safe default)
otherwise compute out[i] = shist[i].label_sum / shist[i].count); reference the
SetLeafVector function and the shist[i].count/shist[i].label_sum/out arrays when
making this change (alternatively you can use a small epsilon divisor instead of
the zero-check if preferred).
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh (1)

402-405: Remove unused histogramTransformBlocks helper

histogramTransformBlocks is only defined in cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh (around lines 402-405) and has no other references/call sites in the repository; remove it or inline the expression where needed.

🤖 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/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh` around
lines 402 - 405, Remove the unused helper histogramTransformBlocks: delete its
definition and, if you intended to use it elsewhere, replace any future call
sites with the equivalent expression std::max<std::size_t>(std::size_t{1},
raft::ceildiv<std::size_t>(len, 256)); otherwise simply remove the
histogramTransformBlocks function declaration/definition to eliminate dead code
(search for histogramTransformBlocks to ensure no references remain).
🤖 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/tests/mg/rf_test.cu`:
- Around line 244-252: The code incorrectly uses the global MPI rank and global
size for CUDA device selection: compute a node-local rank/size and use that to
validate and set the device instead of using the global rank/size. Replace the
comparison of n_gpus against size with a check against the per-node local_size,
and call cudaSetDevice(local_rank) (wrapped with RAFT_CUDA_TRY) where local_rank
is the process's rank on the local node; obtain local_rank/local_size by
creating a node-local communicator (e.g., MPI_Comm_split_type with
MPI_COMM_TYPE_SHARED) or by reading standard local-rank environment variables,
so that n_gpus, cudaGetDeviceCount, cudaSetDevice and RAFT_CUDA_TRY use
node-local indices rather than the global rank/size.

---

Outside diff comments:
In `@cpp/src/decisiontree/batched-levelalgo/split.cuh`:
- Around line 1-4: Update the file header comment block at the top of split.cuh
by changing the copyright year range from "2019-2022" to "2019-2026" so the SPDX
header reads "SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA
CORPORATION." and keep the SPDX-License-Identifier unchanged.

---

Nitpick comments:
In `@cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh`:
- Around line 402-405: Remove the unused helper histogramTransformBlocks: delete
its definition and, if you intended to use it elsewhere, replace any future call
sites with the equivalent expression std::max<std::size_t>(std::size_t{1},
raft::ceildiv<std::size_t>(len, 256)); otherwise simply remove the
histogramTransformBlocks function declaration/definition to eliminate dead code
(search for histogramTransformBlocks to ensure no references remain).

In `@cpp/src/decisiontree/batched-levelalgo/objectives.cuh`:
- Around line 283-288: SetLeafVector can produce Inf/NaN when shist[i].count ==
0; update the loop in SetLeafVector to guard the division by checking
shist[i].count for zero and handle it defensively (e.g., if count == 0 set
out[i] to 0 (or a safe default) otherwise compute out[i] = shist[i].label_sum /
shist[i].count); reference the SetLeafVector function and the
shist[i].count/shist[i].label_sum/out arrays when making this change
(alternatively you can use a small epsilon divisor instead of the zero-check if
preferred).
🪄 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: bfe64907-3651-42e0-a9cf-4350bd65ae81

📥 Commits

Reviewing files that changed from the base of the PR and between 9a1664a and 13d7a95.

📒 Files selected for processing (13)
  • cpp/src/decisiontree/batched-levelalgo/bins.cuh
  • 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/objectives.cuh
  • cpp/src/decisiontree/batched-levelalgo/quantiles.cuh
  • cpp/src/decisiontree/batched-levelalgo/split.cuh
  • cpp/src/randomforest/randomforest.cu
  • cpp/src/randomforest/randomforest.cuh
  • cpp/tests/CMakeLists.txt
  • cpp/tests/mg/rf_quantile_test.cu
  • cpp/tests/mg/rf_test.cu
  • cpp/tests/sg/rf_test.cu

Comment thread cpp/tests/mg/rf_test.cu
@RAMitchell RAMitchell added improvement Improvement / enhancement to an existing function non-breaking Non-breaking change algo: random-forest labels Jun 11, 2026
@csadorf
csadorf requested a review from chyunsu3 June 11, 2026 19:34
…f-cpp-core

# Conflicts:
#	cpp/src/decisiontree/batched-levelalgo/bins.cuh
#	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/objectives.cuh
@copy-pr-bot

copy-pr-bot Bot commented Jun 22, 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.

…f-cpp-core

# Conflicts:
#	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

@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

🤖 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_impl.cuh`:
- Around line 78-81: `nodeSplitLocalCountKernel` and the downstream index
calculation in `builder_kernels_impl.cuh` are using `split.local_nLeft` as if it
were always zero, but the count is only accumulated via `atomicAdd` into
`splits[nid].local_nLeft`. Reset `local_nLeft` before running the local count
kernel, or ensure the selected split copied into the build path is initialized
with `local_nLeft == 0`, so `rank`, `local_left_count`, and `out_idx` stay
correct for right-side rows. Apply the same fix consistently in the related code
paths referenced by `nodeSplitLocalCountKernel` and the split-building logic.

In `@cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh`:
- Around line 60-64: The split validation in SplitNotValid currently only checks
colid and a direct best_metric_val comparison, so NaN gains can still pass as
valid. Update SplitNotValid in builder_kernels.cuh and the matching builder path
check in builder.cuh to reject non-finite split gains (or use a NaN-safe
comparison) before a split is accepted or written into the tree. Keep the fix
centered on SplitNotValid and the builder validation logic so both paths behave
consistently.
🪄 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: 48c48b8d-ad11-447f-95b4-c1336b5a2bb3

📥 Commits

Reviewing files that changed from the base of the PR and between e409597 and 365871c.

📒 Files selected for processing (5)
  • 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/randomforest/randomforest.cu
  • cpp/tests/sg/rf_test.cu
🚧 Files skipped from review as they are similar to previous changes (2)
  • cpp/src/randomforest/randomforest.cu
  • cpp/src/decisiontree/batched-levelalgo/builder.cuh

Comment on lines +78 to +81
const auto rank = state.goes_left ? std::size_t(state.left_count - CountT{1})
: range_pos - std::size_t(state.left_count);
const auto local_left_count = std::size_t(split.local_nLeft);
const auto out_idx = range_start + (state.goes_left ? rank : local_left_count + rank);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

HIGH: Reset local_nLeft before accumulating local counts.

nodeSplitLocalCountKernel only atomicAdds into splits[nid].local_nLeft; if the selected global split carries a non-zero or stale local count, Lines 78-81 offset right-side rows incorrectly. Add a reset launch before the count kernel, or verify the copied split always has local_nLeft == 0.

Suggested fix
+template <typename DataT>
+static __global__ void resetNodeSplitLocalCountKernel(Split<DataT>* splits,
+                                                      const WorkloadInfo* workload_info)
+{
+  using CountT = typename Split<DataT>::CountT;
+  const auto nid = workload_info[blockIdx.x].nodeid;
+  splits[nid].local_nLeft = CountT{0};
+}
+
 template <typename DataT, typename LabelT, typename IdxT, int TPB>
 static __global__ void nodeSplitLocalCountKernel(const DataT min_impurity_decrease,
                                                  const Dataset<DataT, LabelT, IdxT> dataset,
@@
   using CountT = typename Split<DataT>::CountT;
+  resetNodeSplitLocalCountKernel<DataT><<<n_blocks_dimx, 1, 0, builder_stream>>>(
+    splits, workload_info);
   nodeSplitLocalCountKernel<DataT, LabelT, IdxT, TPB><<<n_blocks_dimx, TPB, 0, builder_stream>>>(
     min_impurity_decrease, dataset, work_items, splits, workload_info);

Also applies to: 87-108, 147-149

🤖 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/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh`
around lines 78 - 81, `nodeSplitLocalCountKernel` and the downstream index
calculation in `builder_kernels_impl.cuh` are using `split.local_nLeft` as if it
were always zero, but the count is only accumulated via `atomicAdd` into
`splits[nid].local_nLeft`. Reset `local_nLeft` before running the local count
kernel, or ensure the selected split copied into the build path is initialized
with `local_nLeft == 0`, so `rank`, `local_left_count`, and `out_idx` stay
correct for right-side rows. Apply the same fix consistently in the related code
paths referenced by `nodeSplitLocalCountKernel` and the split-building logic.

Comment on lines +60 to 64
template <typename SplitT, typename DataT>
HDI bool SplitNotValid(const SplitT& split, DataT min_impurity_decrease)
{
return split.best_metric_val <= min_impurity_decrease ||
SplitPartitionNotValid(split, min_samples_leaf, num_rows);
return split.colid == -1 || split.best_metric_val <= min_impurity_decrease;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== builder_kernels.cuh (around SplitNotValid) ==\n'
nl -ba cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh | sed -n '1,180p'

printf '\n== usages of SplitNotValid ==\n'
rg -n "SplitNotValid|best_metric_val" cpp/src/decisiontree -g '!**/build/**'

Repository: rapidsai/cuml

Length of output: 241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== builder_kernels.cuh (around SplitNotValid) ==\n'
nl -ba cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh | sed -n '1,180p'

printf '\n== usages of SplitNotValid ==\n'
rg -n "SplitNotValid|best_metric_val" cpp/src/decisiontree -g '!**/build/**'

Repository: rapidsai/cuml

Length of output: 241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== builder_kernels.cuh (around SplitNotValid) ==\n'
sed -n '1,180p' cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh | cat -n

printf '\n== usages of SplitNotValid ==\n'
rg -n "SplitNotValid|best_metric_val" cpp/src/decisiontree -g '!**/build/**'

Repository: rapidsai/cuml

Length of output: 10841


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== builder.cuh around line 100 ==\n'
sed -n '80,130p' cpp/src/decisiontree/batched-levelalgo/builder.cuh | cat -n

printf '\n== split.cuh around metric initialization/comparison ==\n'
sed -n '1,220p' cpp/src/decisiontree/batched-levelalgo/split.cuh | cat -n

Repository: rapidsai/cuml

Length of output: 10767


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== builder.cuh around line 100 ==\n'
sed -n '80,130p' cpp/src/decisiontree/batched-levelalgo/builder.cuh | cat -n

printf '\n== split.cuh around metric initialization/comparison ==\n'
sed -n '1,220p' cpp/src/decisiontree/batched-levelalgo/split.cuh | cat -n

Repository: rapidsai/cuml

Length of output: 10767


Handle NaN split gains in the builder path

cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh:61-63 treats NaN as valid, and cpp/src/decisiontree/batched-levelalgo/builder.cuh:21 has the same direct compare. That lets invalid gains slip through and be written into the tree; reject non-finite gains here or use a NaN-safe comparison in both places.

🤖 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/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh` around
lines 60 - 64, The split validation in SplitNotValid currently only checks colid
and a direct best_metric_val comparison, so NaN gains can still pass as valid.
Update SplitNotValid in builder_kernels.cuh and the matching builder path check
in builder.cuh to reject non-finite split gains (or use a NaN-safe comparison)
before a split is accepted or written into the tree. Keep the fix centered on
SplitNotValid and the builder validation logic so both paths behave
consistently.

Source: Coding guidelines

@RAMitchell RAMitchell closed this Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

algo: random-forest CMake CUDA/C++ 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.

4 participants