Skip to content

Handle RF split histogram shared-memory pressure - #8323

Merged
rapids-bot[bot] merged 7 commits into
NVIDIA:mainfrom
RAMitchell:codex/bug-rf-shmem-histogram
Jul 8, 2026
Merged

Handle RF split histogram shared-memory pressure#8323
rapids-bot[bot] merged 7 commits into
NVIDIA:mainfrom
RAMitchell:codex/bug-rf-shmem-histogram

Conversation

@RAMitchell

Copy link
Copy Markdown
Contributor

Summary

Closes #8274.

This adds a runtime fallback for the RF/DT split histogram path when the per-block shared-memory histogram is too large or likely to reduce occupancy too much.

Instead of failing with the shared-memory limit for large n_classes * max_n_bins, computeSplitKernel now chooses between:

  • the existing shared-memory histogram path for small/default cases
  • a global-memory histogram path for large histograms

The fallback is selected with a runtime boolean and shared/global pointer switching in the same kernel, so we avoid a separate implementation of the split logic. The threshold is intentionally named/commented as a tunable performance heuristic; it currently switches away from shared memory when the dynamic histogram allocation exceeds 16 KiB.

Details

  • Adds use_global_memory_histogram to the split kernel launcher.
  • Keeps PDF-to-CDF conversion shared between shared/global histogram pointers.
  • Sizes global histogram workspace for all work items when the fallback is active.
  • Preserves the existing shared-memory multi-block unification path.
  • Adds a high-class-count RF classification test that exercises the fallback and verifies the fitted tree actually splits.

Benchmarks

Local RF split benchmarks on NVIDIA RTX PRO 6000 Blackwell, driver 580.159.03.

Shared-path overhead from adding the runtime selection was small: about 0.7-1.2% in the measured shared-memory case.

Forced global-memory histogram vs normal shared-memory path for workloads that fit in shared memory:

Problem Forced global vs normal
2 classes x 32 bins 57% slower
8 classes x 32 bins 14% slower
16 classes x 64 bins 19% faster
32 classes x 128 bins 67% faster
44 classes x 128 bins 71% faster

Regression/default-style case:

Problem Result
1 output x 128 bins forced global 26-29% slower
normal shared median ~73 ms
forced global median ~93-95 ms

This is why the fallback uses a 16 KiB tunable threshold rather than switching all shared-fit problems to global memory.

Testing

  • git diff --check
  • RF-only libcuml build
  • Added RfTests.HighClassCountSplitHistogramFallsBackToGlobalMemory

@copy-pr-bot

copy-pr-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@RAMitchell
RAMitchell requested a review from Copilot July 6, 2026 08:43
@RAMitchell RAMitchell added improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels Jul 6, 2026

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 a runtime fallback in the batched-level decision tree / random forest split-histogram computation to avoid shared-memory exhaustion (or severe occupancy loss) for large n_classes * max_n_bins classification workloads by switching to a global-memory histogram while keeping split scoring logic in a single kernel.

Changes:

  • Adds a use_global_memory_histogram runtime switch to computeSplitKernel / launchComputeSplitKernel, selecting shared-memory vs global-memory histogram storage.
  • Refactors histogram/CDF conversion to operate on either shared or global histogram pointers, and updates builder logic to size/clear histogram workspace appropriately for the selected path.
  • Adds an RF classification test targeting high class-count (n_classes=80, max_n_bins=256) to exercise the fallback and ensure the fitted tree actually splits.

Reviewed changes

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

Show a summary per file
File Description
cpp/tests/sg/rf_test.cu Adds a high-class-count RF test that exercises the global-memory histogram fallback and checks the tree splits.
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh Updates lower_bound to take a const pointer and extends launchComputeSplitKernel signature with the runtime fallback boolean.
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh Implements the shared/global histogram pointer switching inside computeSplitKernel and threads the new boolean through the kernel launcher.
cpp/src/decisiontree/batched-levelalgo/builder.cuh Adds the tunable heuristic threshold and selects shared vs global histogram mode at runtime, adjusting histogram workspace sizing accordingly.
cpp/src/decisiontree/batched-levelalgo/kernels/classification-float.cu Updates explicit template instantiation to include use_global_memory_histogram.
cpp/src/decisiontree/batched-levelalgo/kernels/classification-double.cu Updates explicit template instantiation to include use_global_memory_histogram.
cpp/src/decisiontree/batched-levelalgo/kernels/weighted-classification-float.cu Updates explicit template instantiation to include use_global_memory_histogram.
cpp/src/decisiontree/batched-levelalgo/kernels/weighted-classification-double.cu Updates explicit template instantiation to include use_global_memory_histogram.
cpp/src/decisiontree/batched-levelalgo/kernels/regression-float.cu Updates explicit template instantiation to include use_global_memory_histogram.
cpp/src/decisiontree/batched-levelalgo/kernels/regression-double.cu Updates explicit template instantiation to include use_global_memory_histogram.
cpp/src/decisiontree/batched-levelalgo/kernels/weighted-regression-float.cu Updates explicit template instantiation to include use_global_memory_histogram.
cpp/src/decisiontree/batched-levelalgo/kernels/weighted-regression-double.cu Updates explicit template instantiation to include use_global_memory_histogram.

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

@RAMitchell
RAMitchell marked this pull request as ready for review July 6, 2026 09:20
@RAMitchell
RAMitchell requested a review from a team as a code owner July 6, 2026 09:20
@RAMitchell
RAMitchell requested review from aamijar and csadorf July 6, 2026 09:20
@coderabbitai

coderabbitai Bot commented Jul 6, 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: 3cf74744-b2a5-4cce-b87a-ece85b706808

📥 Commits

Reviewing files that changed from the base of the PR and between ee8383d and 3e4c9ac.

📒 Files selected for processing (1)
  • cpp/tests/sg/rf_test.cu
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/tests/sg/rf_test.cu

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Decision tree training now automatically selects the most suitable histogram computation strategy (shared-memory “fast path” vs global-memory fallback) based on available GPU shared memory, improving performance and scalability for larger workloads.
  • Bug Fixes

    • Improved split construction reliability for datasets with very high class counts by ensuring an appropriate fallback when shared-memory limits are tight.
  • Tests

    • Added a new test covering high class-count training to confirm training completes and produces a valid tree.

Walkthrough

This PR adds a global-memory histogram fallback for decision-tree split computation when shared memory would be exceeded. It adds path-selection logic in builder.cuh, threads a new flag through kernel declarations and implementations, updates explicit instantiations, and adds a regression test.

Changes

Global-memory histogram fallback

Layer / File(s) Summary
Histogram size calculators and path decision in builder.cuh
cpp/src/decisiontree/batched-levelalgo/builder.cuh
Adds a tunable shared-memory threshold constant, replaces the single smem sizing function with separate dynamic/global-bookkeeping/static size calculators, and updates computeSplit to size histograms and select the histogram storage path used for kernel launch.
Kernel declaration signature updates
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels.cuh
Makes lower_bound's array parameter const and adds use_global_memory_histogram to launchComputeSplitKernel's declaration.
Kernel implementation: conditional histogram storage and CDF/split scoring
cpp/src/decisiontree/batched-levelalgo/kernels/builder_kernels_impl.cuh
Updates pdf_to_cdf, adds conditional shared/global histogram initialization and population, updates CDF and split gain evaluation to use the selected buffers, and threads the new flag through the launcher.
Explicit template instantiation updates and regression test
cpp/src/decisiontree/batched-levelalgo/kernels/classification-double.cu, classification-float.cu, regression-double.cu, regression-float.cu, weighted-classification-double.cu, weighted-classification-float.cu, weighted-regression-double.cu, weighted-regression-float.cu, cpp/tests/sg/rf_test.cu
Adds use_global_memory_histogram to all explicit template instantiations of launchComputeSplitKernel and adds a test with 80 classes and 256 bins verifying fit succeeds and produces a valid tree.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • rapidsai/cuml#8132: Both PRs modify the same split-building histogram and launcher path in builder.cuh and builder_kernels_impl.cuh.

Suggested reviewers: hcho3, dantegd, csadorf

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a shared-memory pressure fallback for RF split histograms.
Description check ✅ Passed The description is directly related to the split-histogram fallback and test additions in this PR.
Linked Issues check ✅ Passed The PR implements the requested global-memory fallback, preserves the shared-memory path, and adds a test for high-class-count splits.
Out of Scope Changes check ✅ Passed No clearly unrelated changes are introduced beyond support updates needed for the histogram fallback.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

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

571-614: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Hoist the histogram-path decision out of the per-column-batch loop.

shouldUseGlobalMemoryHistogram (and its inputs computeSplitHistogramSmemSize()/computeSplitSmemSize(), plus a device-property query) don't depend on col, yet computeSplit is invoked once per iteration of the for (IdxT c = 0; c < dataset.n_sampled_cols; c += n_blks_for_cols) loop in computeBestSplits (Line 498). The decision and both size calculations are recomputed identically on every column batch of every node-queue round. Since this decision is invariant per Builder instance/params, consider computing it once (e.g., in the constructor or at the top of computeBestSplits) and threading the resulting use_global_memory_histogram/smem_size into computeSplit.

♻️ Sketch of hoisting the decision
-  void computeSplit(IdxT col, size_t n_blocks_dimx, size_t n_large_nodes, size_t n_work_items)
+  void computeSplit(IdxT col,
+                    size_t n_blocks_dimx,
+                    size_t n_large_nodes,
+                    size_t n_work_items,
+                    bool use_global_memory_histogram,
+                    size_t smem_size)
   {
     // if no instances to split, return
     if (n_blocks_dimx == 0) return;
     raft::common::nvtx::range fun_scope("Builder::computeSplit `@builder.cuh` [batched-levelalgo]");
     auto n_bins                             = params.max_n_bins;
     auto n_classes                          = dataset.num_outputs;
-    auto shared_histogram_dynamic_smem_size = computeSplitHistogramSmemSize();
-    auto shared_path_total_smem_size        = computeSplitSmemSize();
-    auto use_global_memory_histogram        = shouldUseGlobalMemoryHistogram(
-      shared_histogram_dynamic_smem_size, shared_path_total_smem_size);
-    auto smem_size = use_global_memory_histogram ? computeSplitGlobalHistogramSmemSize()
-                                                 : shared_histogram_dynamic_smem_size;
     // if columns left to be processed lesser than `n_blks_for_cols`, shrink the blocks along dimy

Compute use_global_memory_histogram/smem_size once in computeBestSplits and pass them through.

🤖 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/builder.cuh` around lines 571 - 614,
The histogram-path selection in computeSplit is being recomputed for every
column batch even though it is invariant for a given Builder configuration.
Hoist the calls to shouldUseGlobalMemoryHistogram,
computeSplitHistogramSmemSize, computeSplitSmemSize, and the related
device-property-dependent sizing out of computeSplit and into computeBestSplits
or the Builder setup path, then pass the resulting use_global_memory_histogram
and smem_size into computeSplit. Keep computeSplit focused on per-column work
and use the precomputed values when building the grid and launching
launchComputeSplitKernel.
🤖 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/decisiontree/batched-levelalgo/builder.cuh`:
- Around line 571-614: The histogram-path selection in computeSplit is being
recomputed for every column batch even though it is invariant for a given
Builder configuration. Hoist the calls to shouldUseGlobalMemoryHistogram,
computeSplitHistogramSmemSize, computeSplitSmemSize, and the related
device-property-dependent sizing out of computeSplit and into computeBestSplits
or the Builder setup path, then pass the resulting use_global_memory_histogram
and smem_size into computeSplit. Keep computeSplit focused on per-column work
and use the precomputed values when building the grid and launching
launchComputeSplitKernel.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7b0acd61-01de-4854-bd59-084c4f61b44b

📥 Commits

Reviewing files that changed from the base of the PR and between 857cc5a and 8f2b98a.

📒 Files selected for processing (12)
  • 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/kernels/classification-double.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/classification-float.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/regression-double.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/regression-float.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted-classification-double.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted-classification-float.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted-regression-double.cu
  • cpp/src/decisiontree/batched-levelalgo/kernels/weighted-regression-float.cu
  • cpp/tests/sg/rf_test.cu

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

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/builder.cuh (1)

589-590: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use ML::checked_mul<size_t> for the histogram length product.

len_histograms is a count-product fed directly into the cudaMemsetAsync size (and it bounds the buffer the kernel writes). The rest of this change (see computeSplitSharedMemoryConfig) already guards every count-product with ML::checked_mul; this site should follow the same invariant. The leading size_t(n_bins) widens the arithmetic but there is no explicit overflow guard.

As per path instructions: "Multiplications... of int... whose result is passed to... cudaMalloc*... or used as a size_t/int64_t parameter... Require ML::checked_mul<size_t>(...)... or equivalent widening + explicit guard at the call site."

🛡️ Proposed fix
-    size_t len_histograms     = size_t(n_bins) * n_classes * n_blocks_dimy * histogram_node_count;
-    RAFT_CUDA_TRY(cudaMemsetAsync(histograms, 0, sizeof(BinT) * len_histograms, builder_stream));
+    size_t len_histograms = ML::checked_mul<std::size_t>(
+      std::size_t(n_bins), n_classes, n_blocks_dimy, histogram_node_count);
+    RAFT_CUDA_TRY(cudaMemsetAsync(
+      histograms, 0, ML::checked_mul<std::size_t>(sizeof(BinT), len_histograms), builder_stream));
🤖 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/builder.cuh` around lines 589 - 590,
The histogram buffer length in the builder logic is computed with unchecked
multiplication before being passed to cudaMemsetAsync, so update the
len_histograms calculation in builder.cuh to use ML::checked_mul<size_t> for the
full n_bins * n_classes * n_blocks_dimy * histogram_node_count product. Keep the
existing zeroing call intact, but ensure the count-product is overflow-guarded
in the same style used by computeSplitSharedMemoryConfig and any other size
calculations in the builder path.

Source: Path instructions

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

Outside diff comments:
In `@cpp/src/decisiontree/batched-levelalgo/builder.cuh`:
- Around line 589-590: The histogram buffer length in the builder logic is
computed with unchecked multiplication before being passed to cudaMemsetAsync,
so update the len_histograms calculation in builder.cuh to use
ML::checked_mul<size_t> for the full n_bins * n_classes * n_blocks_dimy *
histogram_node_count product. Keep the existing zeroing call intact, but ensure
the count-product is overflow-guarded in the same style used by
computeSplitSharedMemoryConfig and any other size calculations in the builder
path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 331a8816-be91-42c8-9d81-bbe3e312b7c3

📥 Commits

Reviewing files that changed from the base of the PR and between 8f2b98a and 3bcf2b3.

📒 Files selected for processing (1)
  • cpp/src/decisiontree/batched-levelalgo/builder.cuh

@csadorf
csadorf requested a review from chyunsu3 July 7, 2026 14:07
@csadorf

csadorf commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@RAMitchell Please avoid merging main unless that is necessary to pass CI or if you had to push a change anyways. It is not necessary for a branch to be "current" to be merged and it creates a lot of CI churn.

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

LGTM!

Comment thread cpp/tests/sg/rf_test.cu
handle.sync_stream_pool();
}

TEST(RfTests, HighClassCountSplitHistogramFallsBackToGlobalMemory)

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.

Considering that this test does not explicitly test whether the GlobalMemory path was selected, we should maybe rename it.

Suggested change
TEST(RfTests, HighClassCountSplitHistogramFallsBackToGlobalMemory)
TEST(RfTests, HighClassCountSplitHistogram)

@RAMitchell

Copy link
Copy Markdown
Contributor Author

/merge

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

Labels

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.

RF classification split kernel can exceed shared memory for large n_classes * max_n_bins

5 participants