Skip to content

[libcu++]: Address cuda::std::partition I8 performance regression - #12514

Open
MaybeWilli wants to merge 9 commits into
NVIDIA:mainfrom
MaybeWilli:I8-partition-performance
Open

MaybeWilli wants to merge 9 commits into
NVIDIA:mainfrom
MaybeWilli:I8-partition-performance

Conversation

@MaybeWilli

@MaybeWilli MaybeWilli commented Nov 6, 2026 •

Copy link
Copy Markdown

Description

closes

Fixes #8112

Add a dispatch to count size for int32 when the number of elements fits within (int32 / 2). Speeds up large arrays of int8 elements by around 5%, close to thrust's performance. Does not dispatch for int64 elements because a performance regression was measured for that case.

Benchmarks were done on RTX 3060.

With guarantee:
n=2^24 threshold=-128 reset=0 thrust_partition min=293.740 median=298.340 us type=I8
n=2^24 threshold=-128 reset=0 pstl_partition min=287.040 median=293.671 us type=I8 guarantee=1
n=2^24 threshold=-128 reset=0 thrust_partition min=458.820 median=467.310 us type=I16
n=2^24 threshold=-128 reset=0 pstl_partition min=443.750 median=452.831 us type=I16 guarantee=1
n=2^24 threshold=-128 reset=0 thrust_partition min=835.321 median=841.091 us type=I32
n=2^24 threshold=-128 reset=0 pstl_partition min=827.601 median=835.821 us type=I32 guarantee=1
n=2^24 threshold=-128 reset=0 thrust_partition min=1638.382 median=1645.371 us type=I64
n=2^24 threshold=-128 reset=0 pstl_partition min=1631.601 median=1639.571 us type=I64 guarantee=1

Without guarantee:
n=2^24 threshold=-128 reset=0 thrust_partition min=277.470 median=281.200 us type=I8
n=2^24 threshold=-128 reset=0 pstl_partition min=290.080 median=297.370 us type=I8 guarantee=0
n=2^24 threshold=-128 reset=0 thrust_partition min=454.730 median=463.260 us type=I16
n=2^24 threshold=-128 reset=0 pstl_partition min=450.720 median=461.190 us type=I16 guarantee=0
n=2^24 threshold=-128 reset=0 thrust_partition min=831.331 median=837.151 us type=I32
n=2^24 threshold=-128 reset=0 pstl_partition min=831.321 median=838.091 us type=I32 guarantee=0
n=2^24 threshold=-128 reset=0 thrust_partition min=1637.621 median=1642.771 us type=I64
n=2^24 threshold=-128 reset=0 pstl_partition min=1630.171 median=1636.321 us type=I64 guarantee=0

Benchmark script
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cuda/execution>
#include <cuda/memory_resource>
#include <cuda/std/execution>
#include <cuda/std/iterator>
#include <cuda/stream>
#include <map>
#include <string>
#include <thrust/device_vector.h>
#include <thrust/execution_policy.h>
#include <thrust/partition.h>
#include <vector>

void check(cudaError_t e) {
  if (e != cudaSuccess) {
    std::fprintf(stderr, "%s\n", cudaGetErrorString(e));
    std::exit(1);
  }
}
struct cache {
  using value_type = char;
  std::multimap<size_t, void *> free;
  std::vector<void *> all;
  char *allocate(std::ptrdiff_t bytes) {
    auto it = free.find(bytes);
    if (it != free.end()) {
      auto p = it->second;
      free.erase(it);
      return static_cast<char *>(p);
    }
    void *p;
    check(cudaMalloc(&p, bytes));
    all.push_back(p);
    return static_cast<char *>(p);
  }
  void deallocate(char *p, size_t bytes) { free.emplace(bytes, p); }
  void *allocate_sync(size_t b, size_t) { return allocate(b); }
  void deallocate_sync(void *p, size_t b, size_t) {
    deallocate(static_cast<char *>(p), b);
  }
  void *allocate(cuda::stream_ref, size_t b, size_t) { return allocate(b); }
  void deallocate(cuda::stream_ref, void *p, size_t b, size_t) {
    deallocate(static_cast<char *>(p), b);
  }
  friend bool operator==(const cache &a, const cache &b) { return &a == &b; }
  friend bool operator!=(const cache &a, const cache &b) { return !(a == b); }
  friend void get_property(const cache &,
                           cuda::mr::device_accessible) noexcept {}
  ~cache() {
    for (auto p : all)
      cudaFree(p);
  }
};

// Keep values in [-128, 127] for every element type. Threshold=128 selects all.
template <class T> struct pred {
  int threshold;
  __host__ __device__ bool operator()(T x) const { return x < threshold; }
};
template <class T> __global__ void initialize(T *data, size_t n) {
  for (size_t i = blockIdx.x * blockDim.x + threadIdx.x; i < n;
       i += size_t(gridDim.x) * blockDim.x)
    data[i] = static_cast<T>(int(i % 256) - 128);
}
template <class T> void validate(const T *data, size_t n, pred<T> predicate) {
  std::vector<T> host(std::min(n, size_t(1) << 20));
  size_t counts[256] = {};
  bool rejected = false;
  for (size_t offset = 0; offset < n; offset += host.size()) {
    size_t count = std::min(host.size(), n - offset);
    check(cudaMemcpy(host.data(), data + offset, count * sizeof(T),
                     cudaMemcpyDeviceToHost));
    for (size_t i = 0; i < count; ++i) {
      T value = host[i];
      if (value < T(-128) || value > T(127)) {
        std::fprintf(stderr, "invalid value\n");
        std::exit(1);
      }
      ++counts[int(value) + 128];
      if (!predicate(value))
        rejected = true;
      else if (rejected) {
        std::fprintf(stderr, "invalid partition\n");
        std::exit(1);
      }
    }
  }
  for (auto count : counts)
    if (count != n / 256) {
      std::fprintf(stderr, "invalid multiset\n");
      std::exit(1);
    }
}
constexpr int guarantee_power = 29;

template <class T> int run(int power, int threshold, int reset, bool use_guarantee) {
  const size_t n = size_t(1) << power;
  thrust::device_vector<T> source(n), thrust_input(n), pstl_input(n);
  auto src = thrust::raw_pointer_cast(source.data());
  initialize<<<512, 256>>>(src, n);
  check(cudaGetLastError());
  check(cudaDeviceSynchronize());
  check(cudaMemcpy(thrust::raw_pointer_cast(thrust_input.data()), src,
                   n * sizeof(T), cudaMemcpyDeviceToDevice));
  check(cudaMemcpy(thrust::raw_pointer_cast(pstl_input.data()), src,
                   n * sizeof(T), cudaMemcpyDeviceToDevice));
  cache alloc;
  auto tp = thrust::cuda::par(alloc);
  auto cp = cuda::execution::gpu.with(cuda::mr::get_memory_resource, alloc);
  auto bounded_cp = cp.with(cuda::execution::guarantee(
      cuda::execution::max_num_items<(size_t{1} << guarantee_power)>{}));
  pred<T> predicate{threshold};
  std::vector<double> times[2];
  // Separate, identically initialized inputs. Five warmups and 40 measured
  // calls.
  for (int round = 0; round < 45; ++round)
    for (int j = 0; j < 2; ++j) {
      int mode = (j + round) % 2;
      auto &input = mode == 0 ? thrust_input : pstl_input;
      if (reset)
        check(cudaMemcpy(thrust::raw_pointer_cast(input.data()), src,
                         n * sizeof(T), cudaMemcpyDeviceToDevice));
      auto start = std::chrono::steady_clock::now();
      auto end =
          mode == 0
              ? thrust::partition(tp, input.begin(), input.end(), predicate)
              : use_guarantee
                    ? cuda::std::partition(bounded_cp, input.begin(), input.end(), predicate)
                    : cuda::std::partition(cp, input.begin(), input.end(), predicate);
      auto finish = std::chrono::steady_clock::now();
      if (end - input.begin() != std::ptrdiff_t(n / 256 * (threshold + 128))) {
        std::fprintf(stderr, "wrong selected count\n");
        return 1;
      }
      if (round >= 5)
        times[mode].push_back(
            std::chrono::duration<double, std::micro>(finish - start).count());
    }
  validate(thrust::raw_pointer_cast(thrust_input.data()), n, predicate);
  validate(thrust::raw_pointer_cast(pstl_input.data()), n, predicate);
  const char *names[] = {"thrust_partition", "pstl_partition"};
  for (int mode = 0; mode < 2; ++mode) {
    std::sort(times[mode].begin(), times[mode].end());
    std::printf(
        "n=2^%d threshold=%d reset=%d %s min=%.3f median=%.3f us type=I%zu%s\n",
        power, threshold, reset, names[mode], times[mode].front(),
        times[mode][times[mode].size() / 2], sizeof(T) * 8,
        mode == 0 ? "" : (use_guarantee ? " guarantee=1" : " guarantee=0"));
  }
  return 0;
}

int main(int argc, char **argv) {
  int power = argc > 1 ? std::atoi(argv[1]) : 24;
  int threshold = argc > 2 ? std::atoi(argv[2]) : -128;
  int reset = argc > 3 ? std::atoi(argv[3]) : 0;
  const std::string type = argc > 4 ? argv[4] : "i8";
  const std::string guarantee = argc > 5 ? argv[5] : "0";
  int bits = 0;
  if (type == "i8" || type == "I8" || type == "8")
    bits = 8;
  if (type == "i16" || type == "I16" || type == "16")
    bits = 16;
  if (type == "i32" || type == "I32" || type == "32")
    bits = 32;
  if (type == "i64" || type == "I64" || type == "64")
    bits = 64;
  if (argc > 6 || power < 8 || power > 30 || threshold < -128 ||
      threshold > 128 || (reset != 0 && reset != 1) || bits == 0 ||
      (guarantee != "0" && guarantee != "1")) {
    std::fprintf(stderr,
                 "usage: %s [power=8..30] [threshold=-128..128] [reset=0|1] "
                 "[type=i8|i16|i32|i64] [guarantee=0|1]\n",
                 argv[0]);
    return 2;
  }
  const bool use_guarantee = guarantee == "1";
  if (use_guarantee && power > guarantee_power) {
    std::fprintf(stderr, "guarantee=1 requires power <= %d\n", guarantee_power);
    return 2;
  }
  switch (bits) {
  case 8:
    return run<std::int8_t>(power, threshold, reset, use_guarantee);
  case 16:
    return run<std::int16_t>(power, threshold, reset, use_guarantee);
  case 32:
    return run<std::int32_t>(power, threshold, reset, use_guarantee);
  case 64:
    return run<std::int64_t>(power, threshold, reset, use_guarantee);
  }
  return 2;
}

Checklist

  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@MaybeWilli
MaybeWilli requested a review from a team as a code owner November 6, 2026 19:14
@copy-pr-bot

copy-pr-bot Bot commented Nov 6, 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.

@github-project-automation github-project-automation Bot moved this to Todo in CCCL Nov 6, 2026
@cccl-authenticator-app cccl-authenticator-app Bot moved this from Todo to In Review in CCCL Nov 6, 2026
@coderabbitai

coderabbitai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

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

No actionable comments were generated in the recent review.

Summary

Summary by CodeRabbit

  • New Features
    • Added a CUDA execution guarantee for specifying the maximum number of items an algorithm will process. Algorithms can use this bound when handling item counts.
  • Bug Fixes
    • Improved range-size handling in CUDA partition operations, including support for counts beyond int limits while retaining existing handling for 64-bit integral values.
  • Tests
    • Added coverage for partition results around integer count boundaries across signed and unsigned integer types and floating-point values, including cases with different partition outcomes.

CUDA partition dispatch now passes an element count to __par_impl and selects int or size_t offsets using the value type and policy item-count bound. The change adds the max_num_items execution guarantee and tests partitioning around the int offset threshold.

Changes: CUDA partition dispatch

Layer / File(s) Summary
Maximum item-count guarantee
libcudacxx/include/cuda/__execution/max_num_items.h, libcudacxx/include/cuda/execution, libcudacxx/include/cuda/execution.max_num_items.h
max_num_items<_MaxItems> exposes the bound as an integral constant. The execution headers export the guarantee.
Count-based dispatch
libcudacxx/include/cuda/std/__pstl/cuda/partition.h
__par_impl accepts a count. The dispatcher checks the policy bound and selects int or size_t offsets based on the value type and bound.
Partition count boundary test
libcudacxx/test/libcudacxx/std/algorithms/alg.modifying/alg.partitions/pstl_partition.cu
Tests apply the item-count guarantee and cover counts immediately below, at, and above half of int’s maximum. They check the returned boundary and both resulting groups.

Assessment against linked issues

Objective Addressed Explanation
Investigate and avoid the reported performance regressions for large arrays [#8112] ❓ The dispatch logic changes, but the provided context has no benchmark results confirming whether the reported regressions are avoided.

Suggested reviewers: miscco

Change: Bug fix · Severity of issue fixed: Low

Merge Risk: Minimal · up to c305a

This change lets CUDA partition use 32-bit counting when the caller guarantees a small enough input, which targets the reported int8 slowdown. Policies without the guarantee keep the previous behavior. The earlier naming concern in the test is resolved, and no actionable merge risk remains.

Security architecture risk: Minimal · up to c305a

The optimization is opt-in: existing callers retain wide offsets, while calls satisfying the new size promise fit the narrower representation. No introduced security issue or materially worsened ownership or failure-containment behavior was established.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed behavior is scoped to partition invocations whose application-supplied policy carries the new guarantee. Those invocations continue to operate on the caller’s iterator range using the policy’s existing CUDA context, stream, and memory resource; the guarantee itself grants no additional authority.

Trust Boundaries and Controls

  • observed — The size promise is not an unconditional input-validation boundary: _CCCL_ASSERT can compile to a no-op when host assertions are disabled. Callers must establish the documented bound themselves. For valid calls, count is no greater than the declared bound, which is below INT_MAX/2 on the narrow path, so the int conversion is representable.
- [ ] Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

  • Source mode: Auto
  • Repository: NVIDIA/cccl
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: d4593c0
  • Layers: Repository YAML
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: a683ceeb-d28f-4153-bead-7fe3df4751b9
Commits

Reviewing files that changed from the base of the PR and between 73a7c52 and c305ae9.

Files selected for processing: 1

  • libcudacxx/test/libcudacxx/std/algorithms/alg.modifying/alg.partitions/pstl_partition.cu

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

@MaybeWilli MaybeWilli changed the title Address cuda::std::partition I8 performance regression [libcu++]: Address cuda::std::partition I8 performance regression Nov 6, 2026

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

Caution

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

⚠️ Outside diff range comments (1)

Major · important: Make the padded partition scan safe at the 32-bit limit. · partition.h:149-160

libcudacxx/include/cuda/std/__pstl/cuda/partition.h:149-160
Major · Functional Correctness · Heavy lift

important: Make the padded partition scan safe at the 32-bit limit.

On SM100, an INT_MAX-element int16_t range with 3,000 rejected elements overflows CUB’s signed scan with the new dispatch. This is undefined behavior and may corrupt the partition or returned iterator. Falling back to size_t avoids this example, but not all predicates: both dispatches use a 32-bit per-partition scan.

Nitpick comments: 2

libcudacxx/test/libcudacxx/std/algorithms/alg.modifying/alg.partitions/pstl_partition.cu: 2

130-153: Trivial · Maintainability & Code Quality · Heavy lift

suggestion: Test the count type at the dispatcher.

If the dispatcher passes size_t again, these result checks can still pass while the performance regression returns. Add a dispatcher-level test that records the type passed to __par_impl. Check INT_MAX - 1, INT_MAX, INT_MAX + 1, and the 64-bit integer exception. Test-only specializations can avoid large allocations.


130-153: Trivial · Functional Correctness · Quick win

suggestion: Test a mixed partition at the count boundary.

A large-count wrapper regression that returns the input length instead of the selected count would pass this fixture. Add a case with rejected values before selected values, then check the returned boundary and both output ranges. The CUB large-input test does not exercise the libcudacxx wrapper.

Review info

Run configuration

  • Source mode: Auto
  • Repository: NVIDIA/cccl
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: d4593c0
  • Layers: Repository YAML
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: 9e7ab2af-e4cd-48db-a3ef-1d30dba41c65
Commits

Reviewing files that changed from the base of the PR and between c566577 and 17de8d2.

Files selected for processing: 1

  • libcudacxx/test/libcudacxx/std/algorithms/alg.modifying/alg.partitions/pstl_partition.cu

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

@MaybeWilli
MaybeWilli marked this pull request as draft November 6, 2026 23:13
@cccl-authenticator-app cccl-authenticator-app Bot moved this from In Review to In Progress in CCCL Nov 6, 2026
@MaybeWilli
MaybeWilli marked this pull request as ready for review November 6, 2026 23:31
@cccl-authenticator-app cccl-authenticator-app Bot moved this from In Progress to In Review in CCCL Nov 6, 2026

@miscco miscco left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the PR.

Unfortunately, this is something we will need to put on hold for now, because it has nontrivial side effect.

Having a runtime dispatch for both kernels will instantiate both of those kernels, which unfortunately greatly increases binary size and compile time.

Thrust uses this approach but we have users that have a lot of issues with this.

I believe the proper approach is to handle this via the guarantees framework, where a user can give us the guarantee that an input sequence does not exceed a given length.

@gevtushenko for visibility because we discussed this at some point but it is not on the roadmap

@github-project-automation github-project-automation Bot moved this from In Review to In Progress in CCCL Nov 9, 2026
@MaybeWilli

Copy link
Copy Markdown
Author

@miscco Thanks for the explanation. For the guarantee framework, would adding a max_num_items execution::guarantee or execution::require be in the right direction?

auto policy = cuda::execution::gpu.with(
    cuda::execution::guarantee(cuda::execution::max_num_items<(1u << 24)>{}));

auto middle = cuda::std::partition(policy, first, last, pred);

Or should I leave the PR on hold or close it for now, since it isn't on the roadmap?

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

Caution

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

⚠️ Outside diff range comments (1)

Trivial · suggestion: Exercise narrow offsets near the cutoff. · pstl_partition.cu:154

libcudacxx/test/libcudacxx/std/algorithms/alg.modifying/alg.partitions/pstl_partition.cu:154
Trivial · Maintainability & Code Quality · Quick win

suggestion: Exercise narrow offsets near the cutoff.

This policy has no max_num_items guarantee. All three counts therefore use wide offsets. The guarantee-bearing section covers only 1000 items.

Add cases with compile-time bounds of cutoff - 1, cutoff, and cutoff + 1, with matching counts. This covers the narrow path near its limit and the transition to wide offsets. As per path instructions, “Cover relevant edge cases.” (raw.githubusercontent.com)

Source: Path instructions

Review info

Run configuration

  • Source mode: Auto
  • Repository: NVIDIA/cccl
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: d4593c0
  • Layers: Repository YAML
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: 60808eea-4685-4038-8af7-78eb2720a93e
Commits

Reviewing files that changed from the base of the PR and between 118e806 and 73a7c52.

Files selected for processing: 5

  • libcudacxx/include/cuda/__execution/max_num_items.h
  • libcudacxx/include/cuda/execution
  • libcudacxx/include/cuda/execution.max_num_items.h
  • libcudacxx/include/cuda/std/__pstl/cuda/partition.h
  • libcudacxx/test/libcudacxx/std/algorithms/alg.modifying/alg.partitions/pstl_partition.cu

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

This branch has not been deployed

No deployments
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.

[FEA]: Investigate performance regression with cuda::std::partition

2 participants