Skip to content

[cub] Add static and runtime-sized shared-memory histogram privatization - #10556

Open
robobryce wants to merge 45 commits into
NVIDIA:mainfrom
robobryce:pr/histocache/smem-privatized
Open

[cub] Add static and runtime-sized shared-memory histogram privatization#10556
robobryce wants to merge 45 commits into
NVIDIA:mainfrom
robobryce:pr/histocache/smem-privatized

Conversation

@robobryce

@robobryce robobryce commented Jul 30, 2026

Copy link
Copy Markdown

Why

CUB's existing shared-memory privatized histogram path uses compile-time-sized storage and falls back to per-block global-memory privatization when the histogram exceeds that static capacity. On newer GPUs, many larger histograms still fit in opt-in shared memory.

This PR keeps the low-bin static kernel and adds a runtime-sized shared-memory kernel for that larger on-chip range. RANGE classification is split so the static kernel remains lean while the runtime-sized kernel can use interpolation and a per-thread bracket cache.

What changed

  • Add explicit static-SMEM, dynamic-SMEM, and GMEM privatization modes.
  • Allocate the dynamic private histogram from aligned extern __shared__ storage and include it in occupancy selection.
  • Keep local/private counters separate from output counters, widening contributions before the final atomic add.
  • Use SearchTransform for static-SMEM RANGE and for GMEM RANGE below the cached-search policy threshold.
  • Use CachedSearchTransform for runtime-sized SMEM RANGE and policy-selected block-private GMEM RANGE kernels.
  • Preserve legacy custom policy compatibility and the C Parallel runtime selector layout.
  • Add policy, dispatch-boundary, environment/API, custom-policy, and correctness coverage.

Dispatch overview

flowchart TD
    A[Histogram request] --> B[Compute one channel's private-histogram footprint:<br/>bins per channel x sizeof local counter]
    B --> C{Does that byte count fit the selected policy's<br/>compile-time static-SMEM budget?}

    C -- Yes --> D[Static shared-memory mode<br/>compile-time 1,024-byte private array per active channel]
    D --> D1{Histogram kind}
    D1 -- EVEN --> D2[ScaleTransform]
    D1 -- RANGE --> D3[SearchTransform<br/>UpperBound binary search]

    C -- No --> E[Compute the complete runtime-sized allocation:<br/>bins per channel x active channels x sizeof local counter]
    E --> F{Does that byte count fit both the selected policy's<br/>dynamic-SMEM budget and this GPU/kernel's capacity?}

    F -- Yes --> G[Dynamic shared-memory mode<br/>allocate exactly the requested private counters]
    G --> G1{Histogram kind}
    G1 -- EVEN --> G2[ScaleTransform]
    G1 -- RANGE --> G3[CachedSearchTransform<br/>piecewise-linear interpolation<br/>plus per-thread MRU bracket]

    F -- No --> H[Existing global-memory privatization fallback]
    H --> H1{Histogram kind}
    H1 -- EVEN --> H2[ScaleTransform]
    H1 -- RANGE --> H3{Cached-search policy threshold met?}
    H3 -- No --> H4[SearchTransform<br/>UpperBound binary search]
    H3 -- Yes --> H5[CachedSearchTransform<br/>piecewise-linear interpolation<br/>plus per-thread MRU bracket]
Loading

The host selector reasons entirely in bytes. It first compares the per-channel private histogram against the 1,024-byte compile-time static allocation. This means 4-byte counters use static SMEM through 256 bins/channel, while 8-byte counters use it through 128 bins/channel. Counter width is not otherwise special-cased.

If the static allocation does not fit, dispatch computes the exact runtime-sized per-block requirement:

bins per channel × active channels × sizeof(local counter)

If that allocation fits both the selected policy budget and the instantiated kernel's dynamic-SMEM capacity on the current GPU, dispatch launches the runtime-sized kernel. Otherwise it uses the existing GMEM-private fallback. This keeps architecture tuning in the policy while allowing the same architecture policy to run correctly on products with different opt-in shared-memory capacities.

On SM100, the dynamic single-channel budget is 228,352 bytes after reserving 4,096 bytes for other static kernel state. Multi-channel RANGE uses a measured 8,192-byte budget per active channel, or 2,048 bins/channel with 32-bit local counters. Multi-channel EVEN uses a measured 32,768-byte budget per active channel, or 8,192 bins/channel with 32-bit local counters. SM120 has its own explicit 99 KiB policy budget: 99 KiB for single-channel, 64 KiB for two-channel EVEN, and 96 KiB for three- and four-channel EVEN. The runtime kernel-capacity check remains a defensive fallback for kernel-specific limits and future devices. The controlled same-revision experiment below is the evidence for selecting the dynamic-SMEM tier.

The compile-time static allocation is 1,024 bytes per active channel for every default policy. The earlier reviewed version raised most SM100 configurations to 2,048 bytes, which changed the generated kernel even for 8–256 runtime bins and incorrectly kept 512-bin, 32-bit-counter requests in the static tier. Restoring the established 1,024-byte allocation removes that mechanism. At 512 bins, every eligible 32-bit-counter configuration, including multi-channel EVEN, enters the runtime-sized tier.

Policy selection

flowchart TD
    A[Sample and counter types, channel shape, histogram kind] --> B[policy_selector_from_types]
    B --> C{Selected compute capability}
    C -->|SM100+| D[Construct complete SM100 HistogramPolicy]
    C -->|SM90-SM99| E[Construct complete SM90 HistogramPolicy]
    C -->|Pre-SM90| F[Construct complete fallback HistogramPolicy]
    D --> G{Privatization mode}
    E --> G
    F --> G
    G -->|GMEM| H[Use gmem HistogramPrivatizationPolicy]
    G -->|static SMEM| I[Use static_smem HistogramPrivatizationPolicy]
    G -->|dynamic SMEM| J[Use dynamic_smem HistogramPrivatizationPolicy]
    H --> K[Histogram sweep kernel and AgentHistogram]
    I --> K
    J --> K

    L[Deprecated custom PolicyHub] --> M[policy_selector_from_hub adapter]
    M --> G
Loading

Host dispatch, launch bounds, and AgentHistogram select the same member of the same constexpr HistogramPolicy; the compatibility adapter is used only for explicitly supplied deprecated custom policy hubs.

Device-side child launches are different because cudaFuncSetAttribute is host-only. Device dispatch queries both the architecture's default per-block shared-memory limit and the instantiated kernel's static shared-memory footprint, then uses dynamic SMEM only when the complete request fits the remaining capacity; otherwise it dispatches the existing GMEM specialization. For the default direct-load B200 kernel, the static footprint is zero and the 49,152-byte capacity corresponds to 6,144 bins/channel for two active channels, 4,096 for three, and 3,072 for four with 32-bit counters. Custom policies with nonzero static SMEM automatically receive a smaller dynamic limit. Host and graph launches query the instantiated kernel's actual opt-in capacity, clamp the policy limit to that capacity, and fall back to GMEM when the requested runtime allocation does not fit.

Performance

The final production .base sweep was run on a B200 with CUDA 13.3 and GCC 13.3. It compares PR commit 10722be5354fff4a075a846774bacb68ba71b1b0 with upstream-main commit 915456f262b57793fbf5d3ccb2655ce197d9e0b3; both binaries use their unforced production policy selectors.

The matrix contains 6,480 matched PR/main cells: all four single- and multi-channel EVEN/RANGE APIs, I32 and F64 samples, 1M/16M/64M/256M/1G/2B elements, 8–2,048 bins/channel, and all 15 input shapes from the autoresearch benchmark suite.

API sample PR / main geomean minimum cell cells below 0.95x
single-channel EVEN I32 1.383x 0.906x 5 / 810
single-channel EVEN F64 1.508x 0.934x 5 / 810
single-channel RANGE I32 1.371x 0.956x 0 / 810
single-channel RANGE F64 1.305x 0.865x 82 / 810
three-active-channel EVEN I32 1.259x 0.927x 12 / 810
three-active-channel EVEN F64 1.337x 0.850x 67 / 810
three-active-channel RANGE I32 1.185x 0.959x 0 / 810
three-active-channel RANGE F64 1.091x 0.855x 55 / 810
all cells 1.299x 0.850x 226 / 6,480

There are remaining regressions. They are concentrated in F64: the 8–256-bin static tier for single-channel RANGE and multi-channel EVEN, plus individual input shapes in the 512–2,048-bin dynamic tier for multi-channel RANGE. Sixty-eight cells are below 0.90x. I32 is substantially more robust: both RANGE APIs have no cell below 0.95x, while the EVEN APIs have 17 such cells in total.

The plots below show the geomean over all 15 input shapes at each element count and bin count. The complete output also contains one plot per input shape.

Comprehensive PR-versus-main graphs

Single-channel EVEN

Single-channel EVEN I32

Single-channel EVEN F64

Single-channel RANGE

Single-channel RANGE I32

Single-channel RANGE F64

Three-active-channel EVEN

Three-active-channel EVEN I32

Three-active-channel EVEN F64

Three-active-channel RANGE

Three-active-channel RANGE I32

Three-active-channel RANGE F64

CachedSearchTransform for block-private GMEM RANGE

A controlled same-revision experiment changes only the block-private GMEM RANGE decode operation from SearchTransform to CachedSearchTransform; launch policy, load policy, and storage mode are unchanged. It covers I32/F64, 16M/64M/256M elements, all 15 input shapes, and the bin counts that are in the GMEM tier.

API bins/channel cached / uncached geomean minimum cell cached wins
single-channel RANGE 65,536–262,144 2.235x 0.969x 268 / 270
three-active-channel RANGE 4,096–32,768 1.466x 0.794x 339 / 360

The implementation uses CachedSearchTransform throughout the single-channel GMEM tier for the measured I32/F64 configurations. Multi-channel I32 is robust at 1.596x geomean with a 0.952x minimum. Multi-channel F64 has regressions at 4,096 and 8,192 bins/channel, but is robust from 16,384 bins/channel upward: 89 of 90 cells win at 16,384 with a 0.973x minimum, and all 90 cells win at 32,768. The SM100 policy therefore switches multi-channel block-private GMEM RANGE to CachedSearchTransform at 16,384 bins/channel; lower bin counts retain SearchTransform. The crossover is stored in the complete histogram policy and is enabled only for the measured primitive 32/64-bit sample and 32-bit counter configurations.

Cached versus uncached block-private GMEM RANGE

A controlled same-revision B200 benchmark also compared dynamic SMEM directly with block-private GMEM while holding the launch and load policy constant. It covers 120 matched states across single- and three-active-channel EVEN, I32 and F64 samples, 16M–256M pixels, 512–8,192 bins/channel, and two input entropies.

API dynamic SMEM / block-private GMEM geomean minimum cell maximum cell
single-channel EVEN 2.78x 1.65x 5.18x
three-active-channel EVEN 2.33x 1.52x 3.68x

Every measured cell favored dynamic SMEM.

Final dynamic-SMEM versus block-private-GMEM comparison

The final unforced selector check used 16M I32 pixels, three active channels, uniform input, and ten samples per point:

bins/channel 512 1,024 2,048 4,096 8,192
GPU time 1.513 ms 1.511 ms 1.517 ms 1.533 ms 1.559 ms

The boundary check measured 1.560 ms at 8,192 bins and 3.869 ms at 8,193 bins, confirming that the production host selector uses dynamic SMEM through 8,192 bins/channel and GMEM above it.

Final production-selector boundary benchmark

Validation

The branch is based directly on upstream/main b7aaea69a2b07e50f09e67f2962da0243e0b7c5d; the current head is e59377ebe2.

Fresh CUDA 13.3/GCC 13.3 validation for the final selector passed all host, device-side, and graph variants of:

  • cub.test.device.histogram.lid_0, .lid_1, and .lid_2;
  • cub.test.device.histogram_env.lid_0, .lid_1, and .lid_2;
  • cub.test.device.histogram_env_api.lid_0.

The dynamic-SMEM correctness test covers 1,024, 4,096, and 8,192 bins for both single-channel and three-active-channel APIs. The environment suite exercises the exact three-active-channel device-launch boundary at 4,096 bins, while the 8,192-bin device case exercises the GMEM fallback. Compile-time policy tests cover the SM100 and SM120 single-channel, RANGE, and two-, three-, and four-active-channel EVEN budgets, plus the cached-GMEM enable/disable and 16,384-bin crossover boundaries for I32 and F64.

The C Parallel histogram suite passed on an RTX PRO 6000. The fresh SM100 CUB build compiled the normal, host-launcher, device-launcher, graph-launcher, environment API, and legacy custom-policy targets. Eight applicable histogram, environment, and environment-API tests passed on B200. Targeted pre-commit and git diff --check pass on the final head.

Scope

This PR contains the static and runtime-sized shared-memory privatized algorithms plus the RANGE classification and tuning required by those kernels. It excludes the high-bin shared-memory cache/direct-atomic algorithm and the separate benchmark infrastructure work.

@copy-pr-bot

copy-pr-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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.

Comment thread cub/cub/device/dispatch/dispatch_histogram.cuh Outdated
Comment thread cub/cub/device/dispatch/dispatch_histogram.cuh Outdated
@brycelelbach

Copy link
Copy Markdown
Contributor

kernel with a direct per-block merge into the output.

Is the direct per-block merge into the output new?

In the autoresearch, did we end up using a cooperative kernel for privatized smem? Did we validate that at some point?

@brycelelbach

brycelelbach commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

There is no global staging slab, follow-on combine kernel, cooperative launch, or cache algorithm in this PR.

You need to port all of the winning smem privatized optimizations from the raw autoresearch branch. Have you done that?

No half measures. Keep it minimal but complete.

@brycelelbach

Copy link
Copy Markdown
Contributor

Please include a perf chart in this PR using existing data (no new runs).

You can make this non-draft.

@robobryce
robobryce marked this pull request as ready for review July 30, 2026 13:58
@robobryce
robobryce requested a review from a team as a code owner July 30, 2026 13:58
@robobryce
robobryce requested a review from NaderAlAwar July 30, 2026 13:58
@cccl-authenticator-app cccl-authenticator-app Bot moved this from In Progress to In Review in CCCL Jul 30, 2026
@robobryce

robobryce commented Jul 30, 2026

Copy link
Copy Markdown
Author

I ported the remaining winning SMEM-privatized changes in 9dbe9c3 and expanded the PR description with the exact limits, the low-bin/wide-counter tuning, and reused B200 performance data (including a chart). I also marked the PR ready for review.

On the direct-merge question: StoreOutput itself is not new; the static SMEM kernel already merged per-block counters directly. Autoresearch did implement a global-staging + cooperative-combine path, but the final runtime-sized kernel switched to direct merge after it measured +2.8% EVEN, +17.4% RANGE, +13.6% multi-RANGE, and -0.9% multi-EVEN (noise) versus the preceding four-tier implementation. The staging/combine kernels were then deleted.

The focused histogram and environment targets compile successfully, and pre-commit passes. Runtime remains blocked on this host by cudaErrorUnsupportedPtxVersion before test assertions.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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

  • New Features / Performance Improvements

    • Added runtime-sized shared-memory optimization for supported histogram workloads and larger bin counts.
    • Improved range-based bin selection with cached brackets, interpolation, and robust fallback handling.
    • Added support for separate internal and output counter widths.
    • Expanded histogram tuning controls, including dynamic/static shared-memory settings and SM100 support.
  • Tests

    • Added coverage for dynamic shared-memory histograms, large bin counts, counter-width variants, signed-range boundaries, and tuning decisions.

Walkthrough

Changes

Histogram execution now supports policy-selected dynamic shared-memory privatization, separate local and output counter types, cached range decoding, and SM100 tuning. Tests cover dynamic storage, counter widths, boundary classification, and policy limits.

Dynamic histogram privatization

Layer / File(s) Summary
Shared-memory policy tuning
cub/cub/device/dispatch/tuning/tuning_histogram.cuh
HistogramPolicy defines dynamic-memory budgets, static-memory tiers, channel-specific bin limits, sample-size classifications, and SM100 selection rules.
Decode precomputation and bracket caching
cub/cub/device/dispatch/kernels/kernel_histogram.cuh
Range decoding adds binary-search and cached interpolation transforms. Scale and pass-through transforms implement the shared precomputation interface.
Dynamic histogram storage and sweep kernel
cub/cub/agent/agent_histogram.cuh, cub/cub/device/dispatch/kernels/kernel_histogram.cuh
AgentHistogram accepts separate local and output counter types and external dynamic shared-memory storage. Sweep kernels initialize and execute the dynamic path.
Dynamic dispatch and launch configuration
cub/cub/device/dispatch/dispatch_histogram.cuh
Range and even dispatch select eligible dynamic kernels, calculate shared-memory requirements, update occupancy, and preserve static paths for unsupported modes.
Dynamic path and policy tests
cub/test/catch2_test_device_histogram.cu, cub/test/catch2_test_device_histogram_env.cu
Tests cover dynamic shared-memory histograms, wide output counters, local-counter selection, signed range boundaries, expanded policy fields, and SM100 limits.

Suggested reviewers: naderalawar


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

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

Actionable comments posted: 6

🧹 Nitpick comments (5)
cub/test/catch2_test_device_histogram.cu (1)

576-583: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

suggestion: Make this test require the dynamic kernel.

This test does not constrain the architecture or verify the selected policy. Since cub/test/catch2_test_device_histogram_env.cu explicitly expects dynamic_smem_bytes == 0 on SM90, it can pass through the global-memory fallback without covering runtime-sized shared-memory privatization. Gate it to SM100/B200 or assert a nonzero dynamic-memory budget before calling test_even_and_range.

As per path instructions, this test should verify the new CUB dispatch path, not only end-to-end histogram results.

Source: Path instructions

cub/cub/device/dispatch/tuning/tuning_histogram.cuh (1)

376-400: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

suggestion: sample_size and sample_size_bytes are used interchangeably in the same branch.

Line 377 gates on sample_size, Lines 385-386 switch on sample_size_bytes. They coincide for policy_selector_from_types, but a caller that sets them independently gets an inconsistent tier. Pick one field for the whole block.

cub/cub/device/dispatch/kernels/kernel_histogram.cuh (2)

1118-1141: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

suggestion: make the CounterT alignment of the dynamic shared block explicit.

extern __shared__ unsigned char dynamic_smem[] reinterpreted as CounterT* relies on the dynamic-smem base alignment. Declaring the extern array as CounterT (or alignas(alignof(CounterT))) documents the requirement and survives a future 8-byte counter type.


989-999: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

suggestion: Hoist this launch-bounds expression into a constexpr helper for readability; the 0 minBlocksPerMultiprocessor form is valid here, so the nested conditional is the only real issue.

cub/cub/agent/agent_histogram.cuh (1)

709-715: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

suggestion: every thread redundantly writes the same pointer table into shared memory.

temp_storage.histograms[ch] = p is executed identically by all threads. It's correct (each thread reads back only values it wrote itself), but it burns NumActiveChannels shared stores per thread and shared space for data that is a pure function of dyn_smem_histogram_base and num_privatized_bins. Keeping the per-channel bases in registers (a small local CounterT* [NumActiveChannels] view) removes both.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 61cfd019-a863-4ce6-9092-ba7e9011edf7

📥 Commits

Reviewing files that changed from the base of the PR and between 4bdb281 and 9dbe9c3.

📒 Files selected for processing (6)
  • cub/cub/agent/agent_histogram.cuh
  • cub/cub/device/dispatch/dispatch_histogram.cuh
  • cub/cub/device/dispatch/kernels/kernel_histogram.cuh
  • cub/cub/device/dispatch/tuning/tuning_histogram.cuh
  • cub/test/catch2_test_device_histogram.cu
  • cub/test/catch2_test_device_histogram_env.cu

Comment thread cub/cub/device/dispatch/dispatch_histogram.cuh Outdated
Comment thread cub/cub/device/dispatch/dispatch_histogram.cuh Outdated
Comment thread cub/cub/device/dispatch/dispatch_histogram.cuh Outdated
Comment thread cub/cub/device/dispatch/kernels/kernel_histogram.cuh Outdated
Comment thread cub/cub/device/dispatch/kernels/kernel_histogram.cuh Outdated
@brycelelbach

Copy link
Copy Markdown
Contributor

You gave me a graph, which is nice, but that's not the one I had in mind. I want a graph of the speedup relative to baseline. Use the existing data.

Comment thread cub/cub/device/dispatch/kernels/kernel_histogram.cuh
Comment thread cub/cub/device/dispatch/kernels/kernel_histogram.cuh Outdated
Comment thread cub/cub/device/dispatch/kernels/kernel_histogram.cuh Outdated
Comment thread cub/cub/device/dispatch/kernels/kernel_histogram.cuh Outdated
Comment thread cub/cub/device/dispatch/dispatch_histogram.cuh Outdated
Comment thread cub/cub/device/dispatch/kernels/kernel_histogram.cuh
@brycelelbach

Copy link
Copy Markdown
Contributor

cudaErrorUnsupportedPtxVersion: the provided PTX was compiled with an unsupported toolchain

Whatever this is fix it you have root my dude.

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

Pull request overview

This PR extends CUB’s DeviceHistogram implementation to support a runtime-sized, shared-memory–privatized histogram path (backed by extern __shared__) for bin counts beyond the existing compile-time 256-bin SMEM tier, with SM100/B200-specific tuning for dynamic shared-memory budgeting and related policy selection updates.

Changes:

  • Add a new histogram sweep kernel variant that places privatized counters in dynamic shared memory and merges per-block counters directly into the final output.
  • Extend histogram tuning/policy plumbing to carry a per-architecture dynamic-SMEM budget (plus static-tier launch-shape overrides) and select the dynamic-SMEM path when within tuned limits.
  • Add/extend tests to cover policy properties and include correctness coverage for larger-bin configurations (including wide counters).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
cub/test/catch2_test_device_histogram.cu Adds a new test case intended to exercise correctness for larger bin counts (and wide counters) relevant to the dynamic-SMEM path.
cub/test/catch2_test_device_histogram_env.cu Updates policy serialization/property tests for new HistogramPolicy fields and adds checks for SM100 dynamic-SMEM budget propagation (including legacy selector path).
cub/cub/device/dispatch/tuning/tuning_histogram.cuh Extends HistogramPolicy with dynamic-SMEM and static-tier shape fields; adds SM100 dynamic-SMEM budget constant and tuning updates.
cub/cub/device/dispatch/kernels/kernel_histogram.cuh Introduces decode-op precompute and MRU bracket-cache support for RANGE classification; adds a dynamic-SMEM sweep kernel and adjusts static-tier launch bounds/policy usage.
cub/cub/device/dispatch/dispatch_histogram.cuh Adds a dynamic shared-memory kernel tag and selection logic; computes dynamic-SMEM launch bytes, sets max dynamic-SMEM attribute, and removes unused global privatization allocation when using dynamic SMEM.
cub/cub/agent/agent_histogram.cuh Adds UseDynamicSmemHistogram mode for AgentHistogram to accept privatized histogram storage from dynamic shared memory and enables bracket-cache-aware bin selection in that mode.

Comment thread cub/cub/device/dispatch/kernels/kernel_histogram.cuh Outdated
@brycelelbach

Copy link
Copy Markdown
Contributor

Why there is no cooperative combine kernel

You can drop the discussion of why there's no cooperative combine kernel.

@robobryce
robobryce force-pushed the pr/histocache/smem-privatized branch from 9dbe9c3 to f223e6c Compare July 30, 2026 15:19
@robobryce
robobryce force-pushed the pr/histocache/smem-privatized branch from 5ba1d53 to 47e8690 Compare August 30, 2026 19:57
@robobryce

Copy link
Copy Markdown
Author

CI root cause and fix:

The SM100 policy was tuned on B200 and can request more opt-in dynamic shared memory than an RTX PRO 6000 kernel can actually receive. Host dispatch previously applied the policy limit directly with cudaFuncSetAttribute; on the RTX PRO 6000 this returned cudaErrorInvalidValue before the existing GMEM fallback could run. The same failure surfaced in ordinary CUB histogram tests, the narrower-local-counter allocation test, and C Parallel initialization.

Host dispatch now queries the instantiated kernel's actual dynamic-SMEM capacity, requires the runtime allocation to fit both that hardware limit and the policy limit, and opts in to the smaller of the two limits. Requests that do not fit use the existing GMEM path. Device dispatch retains its existing capacity query. I also marked the device-only num_channels test constant [[maybe_unused]] to fix the MSVC warning-as-error failure.

Fresh validation passed the eight applicable SM100 CUB histogram/environment tests on B200 and cccl.c.parallel.test.histogram on an RTX PRO 6000.

@brycelelbach

Copy link
Copy Markdown
Contributor

/ok to test 47e8690

@brycelelbach

Copy link
Copy Markdown
Contributor

CI root cause and fix:

The SM100 policy was tuned on B200 and can request more opt-in dynamic shared memory than an RTX PRO 6000 kernel can actually receive. Host dispatch previously applied the policy limit directly with cudaFuncSetAttribute; on the RTX PRO 6000 this returned cudaErrorInvalidValue before the existing GMEM fallback could run. The same failure surfaced in ordinary CUB histogram tests, the narrower-local-counter allocation test, and C Parallel initialization.

Host dispatch now queries the instantiated kernel's actual dynamic-SMEM capacity, requires the runtime allocation to fit both that hardware limit and the policy limit, and opts in to the smaller of the two limits. Requests that do not fit use the existing GMEM path. Device dispatch retains its existing capacity query. I also marked the device-only num_channels test constant [[maybe_unused]] to fix the MSVC warning-as-error failure.

Fresh validation passed the eight applicable SM100 CUB histogram/environment tests on B200 and cccl.c.parallel.test.histogram on an RTX PRO 6000.

Why not just introduce a separate tuning policy for SM120 instead? You can have the fallback that checks smem capacity as a fallback as well.

@github-actions

Copy link
Copy Markdown
Contributor

🔬 CUB benchmark SASS comparison

⚠️ The SASS changed for 4 of 84 CUB benchmark target(s). A benchmark run may be necessary

How to request a benchmark run
Request a CUB benchmark run for this PR:

1. Replace the `benchmarks:` block of ci/bench.yaml with exactly this:

benchmarks:
  filters:
    cub:
      - '^cub\.bench\.histogram\.even\.base$'
      - '^cub\.bench\.histogram\.multi\.even\.base$'
      - '^cub\.bench\.histogram\.multi\.range\.base$'
      - '^cub\.bench\.histogram\.range\.base$'
  gpus:
    - "h100"   # pick the GPUs that this change can affect

2. Commit with `[bench-only]` at the end of the commit summary, so that
   the unrelated CI jobs are skipped. Then push.

ci/bench.yaml must match ci/bench.template.yaml before the PR can merge.
Reset it once the measurement is done.
Run Value
Baseline b7aaea69a2b07e50f09e67f2962da0243e0b7c5d
Tested HEAD
Architectures 75-real;80-real;90-real;100-real;110-real;120-real;120-virtual
Targets with a SASS change
Target Architectures with a SASS change
cub.bench.histogram.even.base sm_80, sm_110, sm_90, sm_75, sm_100, sm_120
cub.bench.histogram.multi.even.base sm_80, sm_110, sm_90, sm_75, sm_100, sm_120
cub.bench.histogram.multi.range.base sm_80, sm_110, sm_90, sm_75, sm_100, sm_120
cub.bench.histogram.range.base sm_80, sm_110, sm_90, sm_75, sm_100, sm_120

‼️ Summary of Differences ‼️

Showing 4/4 summaries.

cub.bench.histogram.even.base - sm_80

Showing 40/37572 diff lines, 36158 changes. - ⬇️ Full diff

--- base/cub.bench.histogram.even.base.sm_80
+++ test/cub.bench.histogram.even.base.sm_80
@@ -40249,7 +40249,916 @@
 STG.E [R2.64], RZ ;
 EXIT ;
 BRA <+0x0>;
-Function : void cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::DeviceHistogramSweepKernel<cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::policy_selector_from_types<double, int, (int)1, (int)1, (bool)1>, (int)0, (int)1, (int)1, double *, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::ScaleTransform, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::PassThruTransform, int>(T5, cuda::std::__4::array<int, T4>, cuda::std::__4::array<int, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T8, T4>, cuda::std::__4::array<T7, T4>, T9, T9, T9, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::GridQueue<int>)
+Function : void cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::DeviceHistogramSweepKernel<cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::policy_selector_from_types<double, int, (int)1, (int)1, (bool)1>, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::HistogramPrivatizedDynamicSmem, (int)1, (int)1, double *, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::ScaleTransform, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::PassThruTransform, int, int>(T5, cuda::std::__4::array<int, T4>, cuda::std::__4::array<int, T4>, cuda::std::__4::array<T10 *, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T8, T4>, cuda::std::__4::array<T7, T4>, T9, T9, T9, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::GridQueue<int>)
+IMAD.MOV.U32 R1, RZ, RZ, c[0x0][0x28] ;
+S2R R5, SR_TID.X ;
+BSSY B0, <+0x200> ;
+ISETP.GE.AND P0, PT, R5, c[0x0][0x16c], PT ;
+@P0 BRA <+0x1d0> ;
+IMAD.MOV.U32 R0, RZ, RZ, R5 ;
+BSSY B1, <+0x110> ;
+LOP3.LUT R2, RZ, R0, RZ, 0x33, !PT ;
+IADD3 R4, R2, c[0x0][0x16c], RZ ;
+ISETP.GE.U32.AND P1, PT, R4.reuse, 0x480, PT ;
+IMAD.WIDE.U32 R2, R4, -0x55555555, RZ ;
+LEA.HI R2, R3, 0x1, RZ, 0x18 ;
+LOP3.LUT P0, R2, R2, 0x3, RZ, 0xc0, !PT ;
+@!P0 BRA <+0x90> ;
+IMAD.SHL.U32 R3, R5, 0x4, RZ ;
+IMAD.MOV.U32 R0, RZ, RZ, R5 ;
+IADD3 R2, R2, -0x1, RZ ;
+STS [R3], RZ ;
+IADD3 R0, R0, 0x180, RZ ;
+ISETP.NE.AND P0, PT, R2, RZ, PT ;
+IADD3 R3, R3, 0x600, RZ ;
+@P0 BRA <-0x50> ;
+BSYNC B1 ;
+@!P1 BRA <+0xa0> ;
+LEA R2, R0, 0xc00, 0x2 ;
+IADD3 R0, R0, 0x600, RZ ;
+STS [R2+-0xc00], RZ ;
+ISETP.GE.AND P0, PT, R0, c[0x0][0x16c], PT ;
+STS [R2+-0x600], RZ ;
+STS [R2], RZ ;
+STS [R2+0x600], RZ ;
+IADD3 R2, R2, 0x1800, RZ ;
cub.bench.histogram.multi.even.base - sm_80

Showing 40/25062 diff lines, 23434 changes. - ⬇️ Full diff

--- base/cub.bench.histogram.multi.even.base.sm_80
+++ test/cub.bench.histogram.multi.even.base.sm_80
@@ -40303,7 +40303,720 @@
 STG.E [R2.64], RZ ;
 EXIT ;
 BRA <+0x0>;
-Function : void cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::DeviceHistogramSweepKernel<cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::policy_selector_from_types<double, int, (int)4, (int)3, (bool)1>, (int)0, (int)4, (int)3, double *, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::ScaleTransform, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::PassThruTransform, int>(T5, cuda::std::__4::array<int, T4>, cuda::std::__4::array<int, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T8, T4>, cuda::std::__4::array<T7, T4>, T9, T9, T9, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::GridQueue<int>)
+Function : void cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::DeviceHistogramSweepKernel<cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::policy_selector_from_types<double, int, (int)4, (int)3, (bool)1>, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::HistogramPrivatizedDynamicSmem, (int)4, (int)3, double *, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::ScaleTransform, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::PassThruTransform, int, int>(T5, cuda::std::__4::array<int, T4>, cuda::std::__4::array<int, T4>, cuda::std::__4::array<T10 *, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T8, T4>, cuda::std::__4::array<T7, T4>, T9, T9, T9, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::GridQueue<int>)
+IMAD.MOV.U32 R1, RZ, RZ, c[0x0][0x28] ;
+S2R R16, SR_TID.X ;
+BSSY B0, <+0x200> ;
+ISETP.GE.AND P0, PT, R16, c[0x0][0x174], PT ;
+@P0 BRA <+0x1d0> ;
+IADD3 R0, -R16, c[0x0][0x174], RZ ;
+BSSY B1, <+0x120> ;
+PLOP3.LUT P0, PT, PT, PT, PT, 0x80, 0x0 ;
+IMAD.MOV.U32 R2, RZ, RZ, R16 ;
+ISETP.GT.AND P1, PT, R0, 0x480, PT ;
+IMAD.SHL.U32 R0, R16, 0x4, RZ ;
+@!P1 BRA <+0xc0> ;
+IMAD.MOV.U32 R3, RZ, RZ, c[0x0][0x174] ;
+PLOP3.LUT P0, PT, PT, PT, PT, 0x8, 0x0 ;
+IADD3 R3, R3, -0x480, RZ ;
+IADD3 R2, R2, 0x600, RZ ;
+STS [R0], RZ ;
+ISETP.GE.AND P1, PT, R2, R3, PT ;
+STS [R0+0x600], RZ ;
+STS [R0+0xc00], RZ ;
+STS [R0+0x1200], RZ ;
+IADD3 R0, R0, 0x1800, RZ ;
+@!P1 BRA <-0x70> ;
+BSYNC B1 ;
+IADD3 R3, -R2, c[0x0][0x174], RZ ;
+ISETP.GT.AND P1, PT, R3, 0x180, PT ;
+@P1 PLOP3.LUT P0, PT, PT, PT, PT, 0x8, 0x0 ;
+@P1 STS [R0], RZ ;
+@P1 IADD3 R2, R2, 0x300, RZ ;
+@P1 STS [R0+0x600], RZ ;
+ISETP.LT.OR P0, PT, R2, c[0x0][0x174], P0 ;
+@P1 IADD3 R0, R0, 0xc00, RZ ;
cub.bench.histogram.multi.range.base - sm_80

Showing 40/113865 diff lines, 111168 changes. - ⬇️ Full diff

--- base/cub.bench.histogram.multi.range.base.sm_80
+++ test/cub.bench.histogram.multi.range.base.sm_80
@@ -40303,7 +40303,11712 @@
 STG.E [R2.64], RZ ;
 EXIT ;
 BRA <+0x0>;
-Function : void cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::DeviceHistogramSweepKernel<cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::policy_selector_from_types<double, int, (int)4, (int)3, (bool)0>, (int)0, (int)4, (int)3, double *, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::SearchTransform<const double *>, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::PassThruTransform, int>(T5, cuda::std::__4::array<int, T4>, cuda::std::__4::array<int, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T8, T4>, cuda::std::__4::array<T7, T4>, T9, T9, T9, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::GridQueue<int>)
+Function : void cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::DeviceHistogramSweepKernel<cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::policy_selector_from_types<double, int, (int)4, (int)3, (bool)0>, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::HistogramPrivatizedDynamicSmem, (int)4, (int)3, double *, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::CachedSearchTransform<const double *>, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::PassThruTransform, int, int>(T5, cuda::std::__4::array<int, T4>, cuda::std::__4::array<int, T4>, cuda::std::__4::array<T10 *, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T8, T4>, cuda::std::__4::array<T7, T4>, T9, T9, T9, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::GridQueue<int>)
+IMAD.MOV.U32 R1, RZ, RZ, c[0x0][0x28] ;
+ULDC UR7, c[0x0][0x1d8] ;
+IMAD.MOV.U32 R36, RZ, RZ, c[0x0][0x1d0] ;
+UIADD3 UR7, UR7, -0x1, URZ ;
+MOV R37, c[0x0][0x1d4] ;
+UMOV UR4, 0x8 ;
+ULDC.64 UR10, c[0x0][0x1d0] ;
+UIMAD.WIDE UR10, UR7, UR4, UR10 ;
+ULDC.64 UR26, c[0x0][0x118] ;
+LDG.E.64.CONSTANT R2, [R36.64] ;
+IMAD.U32 R4, RZ, RZ, UR10 ;
+IMAD.U32 R5, RZ, RZ, UR11 ;
+LDG.E.64.CONSTANT R4, [R4.64] ;
+MOV R12, c[0x0][0x2b0] ;
+IMAD.MOV.U32 R11, RZ, RZ, c[0x0][0x2b4] ;
+MOV R9, c[0x0][0x25c] ;
+IMAD.MOV.U32 R10, RZ, RZ, c[0x0][0x258] ;
+IMAD.MOV.U32 R8, RZ, RZ, c[0x0][0x200] ;
+IMAD.MOV.U32 R7, RZ, RZ, c[0x0][0x204] ;
+DSETP.GEU.AND P0, PT, R2, R4, PT ;
+@P0 BRA <+0x500> ;
+R2UR UR8, R2 ;
+I2F R18, UR7 ;
+R2UR UR9, R3 ;
+R2UR UR24, R4 ;
+R2UR UR25, R5 ;
+MOV R6, UR8 ;
+IMAD.U32 R7, RZ, RZ, UR9 ;
+DADD R6, -R6, UR24 ;
+F2F.F32.F64 R30, R6 ;
+MUFU.RCP R0, R30 ;
+FCHK P0, R18, R30 ;
cub.bench.histogram.range.base - sm_80

Showing 40/112070 diff lines, 111736 changes. - ⬇️ Full diff

--- base/cub.bench.histogram.range.base.sm_80
+++ test/cub.bench.histogram.range.base.sm_80
@@ -40249,7 +40249,14677 @@
 STG.E [R2.64], RZ ;
 EXIT ;
 BRA <+0x0>;
-Function : void cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::DeviceHistogramSweepKernel<cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::policy_selector_from_types<double, int, (int)1, (int)1, (bool)0>, (int)0, (int)1, (int)1, double *, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::SearchTransform<const double *>, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::PassThruTransform, int>(T5, cuda::std::__4::array<int, T4>, cuda::std::__4::array<int, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T8, T4>, cuda::std::__4::array<T7, T4>, T9, T9, T9, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::GridQueue<int>)
+Function : void cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::DeviceHistogramSweepKernel<cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::policy_selector_from_types<double, int, (int)1, (int)1, (bool)0>, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::HistogramPrivatizedDynamicSmem, (int)1, (int)1, double *, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::CachedSearchTransform<const double *>, cub::_V_300600_SM_750_800_900_1000_1100_1200::detail::histogram::Transforms<double, int, double>::PassThruTransform, int, int>(T5, cuda::std::__4::array<int, T4>, cuda::std::__4::array<int, T4>, cuda::std::__4::array<T10 *, T4>, cuda::std::__4::array<T6 *, T4>, cuda::std::__4::array<T8, T4>, cuda::std::__4::array<T7, T4>, T9, T9, T9, int, cub::_V_300600_SM_750_800_900_1000_1100_1200::GridQueue<int>)
+IMAD.MOV.U32 R1, RZ, RZ, c[0x0][0x28] ;
+IMAD.MOV.U32 R0, RZ, RZ, c[0x0][0x1a8] ;
+HFMA2.MMA R33, -RZ, RZ, 0, 4.76837158203125e-07 ;
+IMAD.MOV.U32 R34, RZ, RZ, c[0x0][0x1a0] ;
+ULDC.64 UR14, c[0x0][0x118] ;
+IMAD.MOV.U32 R35, RZ, RZ, c[0x0][0x1a4] ;
+IADD3 R0, R0, -0x1, RZ ;
+LDG.E.64.CONSTANT R2, [R34.64] ;
+IMAD.WIDE R32, R0, R33, c[0x0][0x1a0] ;
+LDG.E.64.CONSTANT R6, [R32.64] ;
+MOV R5, c[0x0][0x1d0] ;
+IMAD.MOV.U32 R4, RZ, RZ, c[0x0][0x1d4] ;
+DSETP.GEU.AND P0, PT, R2, R6, PT ;
+@P0 BRA <+0x4e0> ;
+R2UR UR16, R6 ;
+I2F R9, R0 ;
+R2UR UR17, R7 ;
+R2UR UR4, R2 ;
+R2UR UR5, R3 ;
+DADD R4, -R2, UR16 ;
+F2F.F32.F64 R13, R4 ;
+MUFU.RCP R8, R13 ;
+FCHK P0, R9, R13 ;
+FFMA R11, -R13, R8, 1 ;
+FFMA R8, R8, R11, R8 ;
+FFMA R10, R9, R8, RZ ;
+FFMA R11, -R13, R10, R9 ;
+FFMA R7, R8, R11, R10 ;
+@!P0 BRA <+0x30> ;
+MOV R6, 0x1f0 ;
+CALL.REL.NOINC <+0x1be20> ;
+SHF.R.S32.HI R9, RZ, 0x1, R0 ;

@robobryce

Copy link
Copy Markdown
Author

Agreed. I added an explicit SM120 dynamic-SMEM budget in e59377ebe2 instead of relying on the runtime fallback as the normal SM120 selection mechanism.

SM100 retains its 227 KiB policy budget. SM120 now uses its 99 KiB per-block budget: 99 KiB for single-channel, 64 KiB for two-channel EVEN, and 96 KiB for three- and four-channel EVEN. Multi-channel RANGE remains below the SM120 ceiling already. The runtime kernel-capacity check remains as the defensive fallback for kernel-specific limits and future devices.

I added compile-time policy checks for SM120 and reran all three histogram environment test binaries; all passed (785/40, 236/19, and 631/28 assertions/test cases).

@github-actions

Copy link
Copy Markdown
Contributor

🥳 CI Workflow Results

🟩 Finished in 1h 16m: Pass: 100%/288 | Total: 3d 05h | Max: 49m 29s | Hits: 85%/252120

See results here.

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

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants