Skip to content

[libcudacxx] Add parallel cuda::std::minmax_element - #9805

Open
edenfunf wants to merge 5 commits into
NVIDIA:mainfrom
edenfunf:feat/cuda-std-minmax-element
Open

[libcudacxx] Add parallel cuda::std::minmax_element#9805
edenfunf wants to merge 5 commits into
NVIDIA:mainfrom
edenfunf:feat/cuda-std-minmax-element

Conversation

@edenfunf

@edenfunf edenfunf commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Description

Adds the ExecutionPolicy overload of cuda::std::minmax_element together with a CUDA backend, completing the min_element / max_element / minmax_element parallel algorithm family (the serial overload already existed).

Implementation. A single traversal of the range following the thrust::extrema approach: one cub::DeviceReduce::TransformReduce over a counting iterator, where each element enters the reduction as a {value, value, index, index} accumulator and a fused operator combines the min and max halves. Unlike thrust's operator, the max half prefers the larger index on ties, so a single pass directly yields the first minimum and last maximum required by the standard. The initial value is flagged with offset -1 and loses against every element, avoiding the need for an identity value of T (requires T to be default-constructible, same as ArgMin's empty-input path). For counts <= INT_MAX the accumulator carries 32 bit offsets, shrinking it by 8 bytes and selecting cub's 32 bit offset kernels. One allocation (scratch buffer plus result slot), one kernel launch, one synchronization.

Benchmarks. Adds bench/minmax_element/basic.cu mirroring the existing min_element benchmark, and the missing minmax_element entry in the thrust extrema benchmark for a same-harness comparison. On RTX 5070 at 2^28 elements: 1.39x/1.18x faster than thrust::minmax_element for 1/2 byte types, parity for 4/8 byte integer and F32 types, 0.76x for F64 (bound by cub::DeviceReduce default tuning versus thrust's own ReduceAgent; see PR discussion for details).

Tests. pstl_minmax_element.cu compares the parallel result positionally against the serial cuda::std::minmax_element reference computed on identical data. This stays correct under ties — e.g. __nv_bfloat16, where adjacent large values round to the same representation, so the last maximum is not at a fixed position — across all_types, all four policy configurations (default stream / provided stream / provided memory_resource / both), plus empty and single-element ranges.

Verified locally: 512 assertions / 8 test cases pass on RTX 5070 (sm_120, CUDA 13.3), plus a 2^31+ elements spot check exercising the 64 bit offset path; clang-format clean.

Related: #5160, #5592.

Checklist

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

Adds the ExecutionPolicy overload of cuda::std::minmax_element with a CUDA
backend, completing the min/max/minmax_element parallel algorithm family
(the serial overload already existed).

What:
- New __pstl_algorithm::__minmax_element dispatch tag.
- Serial routing header __pstl/minmax_element.h (mirrors __pstl/min_element.h),
  returning pair<Iter, Iter> and short-circuiting the empty range.
- CUDA backend __pstl/cuda/minmax_element.h.

Tie-breaking (why/how):
std::minmax_element returns the *first* minimum and the *last* maximum.
cub::DeviceReduce::ArgMin/ArgMax both keep the smaller offset on ties, i.e.
the first extremum. ArgMin over the forward range yields the first minimum
directly; the last maximum is obtained by running ArgMax over the reversed
range (the first maximum in reverse order is the last maximum in forward
order) and mapping the index back via count - 1 - rev.

The two reductions run sequentially on one stream and share a single
allocation (one scratch buffer sized to the larger requirement plus both
result slots) with a single synchronization. A single-pass fused ArgMinMax
would need a custom reduction operator (CUB has no ArgMinMax primitive); the
two-reduction form follows the existing multi-launch backends (sort.h,
inclusive_scan.h).

Tests (pstl_minmax_element.cu): the parallel result is compared positionally
against the serial cuda::std::minmax_element reference on identical data,
which stays correct under ties (e.g. bfloat16, where adjacent large values
round together) across all_types, all four policy configurations, plus empty
and single-element ranges.

Verified: 512 assertions / 8 test cases pass on RTX 5070 (sm_120, CUDA 13.3);
clang-format clean.
@edenfunf
edenfunf requested review from a team as code owners July 12, 2026 07:55
@edenfunf
edenfunf requested a review from wmaxey July 12, 2026 07:55
@copy-pr-bot

copy-pr-bot Bot commented Jul 12, 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 Jul 12, 2026
@cccl-authenticator-app cccl-authenticator-app Bot moved this from Todo to In Review in CCCL Jul 12, 2026
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

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: 12641df1-8b5c-44e5-8fe6-0bd0f2bf2f29

📥 Commits

Reviewing files that changed from the base of the PR and between 8b2bec2 and 3ba5ac1.

📒 Files selected for processing (1)
  • thrust/benchmarks/bench/extrema/basic.cu
🚧 Files skipped from review as they are similar to previous changes (1)
  • thrust/benchmarks/bench/extrema/basic.cu

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added parallel execution-policy support for cuda::std::minmax_element, including correct tie-breaking behavior.
    • Introduced an optimized CUDA backend for computing the first minimum and last maximum in a single reduction.
  • Documentation
    • Updated the “Parallel standard algorithms” list to include additional supported algorithms (max_element, merge, min_element, minmax_element).
  • Tests
    • Added PSTL coverage for cuda::std::minmax_element, including empty/single-element cases and tie scenarios across execution configurations.
  • Benchmarks
    • Added cuda::std::minmax_element and extended the thrust extrema benchmarks to measure minmax_element.

Walkthrough

Changes

Adds a PSTL execution-policy overload and CUDA backend for cuda::std::minmax_element, using one CUB transform reduction. Tests cover iterator, policy, stream, memory-resource, and tie-handling cases; benchmarks and documentation are added.

Suggested reviewers: gonidelis

Parallel minmax_element

Layer / File(s) Summary
PSTL dispatch contract
libcudacxx/include/cuda/std/__pstl/dispatch.h, libcudacxx/include/cuda/std/__pstl/minmax_element.h, libcudacxx/include/cuda/std/execution
Registers the minmax_element dispatch tag, adds the execution-policy overload, and includes it in the PSTL algorithm set.
CUDA reduction implementation
libcudacxx/include/cuda/std/__pstl/cuda/minmax_element.h
Computes the first minimum and last maximum with one CUB TransformReduce, maps synchronized indices back to iterators, and translates allocation failures.
Validation and supported algorithm documentation
libcudacxx/test/libcudacxx/std/algorithms/alg.sorting/alg.min.max/pstl_minmax_element.cu, docs/libcudacxx/standard_api/algorithms_library.rst
Tests empty, single-element, ordered, descending, tied, iterator, stream, and memory-resource cases, and updates the supported algorithm list.
Benchmark coverage
libcudacxx/benchmarks/bench/minmax_element/basic.cu, thrust/benchmarks/bench/extrema/basic.cu
Adds default and comparator-based benchmarks for libcudacxx and Thrust minmax_element implementations across power-of-two input sizes.

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

@Jacobfaib
Jacobfaib requested review from miscco and removed request for wmaxey July 14, 2026 19:22

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

I believe this is not the right approach because we need to traverse the range mutliple times.

The proper approach is to create the right operator like thrust::extrema does and traverse only once.

I was pondering extending reduce to add a MinMax operation, but that is not as trivial as I hoped

@github-project-automation github-project-automation Bot moved this from In Review to In Progress in CCCL Jul 24, 2026
Replace the two ArgMin/ArgMax reductions with one
cub::DeviceReduce::TransformReduce over a counting iterator: each
element enters the reduction as {value, value, index, index} and a
fused operator keeps the smaller offset on equivalent minima and the
larger offset on equivalent maxima, yielding the first minimum and
last maximum required by minmax_element in one pass. The initial
value is flagged with offset -1 so it loses against every element.
@edenfunf

Copy link
Copy Markdown
Contributor Author

@miscco Thanks for the review. Reworked to a single traversal following the thrust::extrema approach:

  • One cub::DeviceReduce::TransformReduce over a counting iterator, with a fused operator reducing {value, value, index, index} accumulators.
  • Unlike thrust's operator, the max half prefers the larger index on ties, so a single pass yields the first minimum and last maximum required by the standard.
  • The initial value is flagged with offset -1 and loses against every element, avoiding the need for an identity value of T.

Verified locally: 512 assertions / 8 test cases pass (RTX 5070, sm_120, CUDA 13.3).

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

This looks good, please also add a benchmark and compare it agianst thrust::minmax_element

edenfunf added 2 commits July 28, 2026 17:15
Dispatch on the element count: for counts <= INT_MAX the accumulator
carries 32 bit offsets, which shrinks it by 8 bytes and lets cub
select the 32 bit offset kernels. On RTX 5070 at 2^28 elements this
makes the reduction 1.4x/1.2x faster than thrust::minmax_element for
1/2 byte types and keeps parity for 4/8 byte types.
Benchmark the parallel cuda::std::minmax_element next to the existing
min_element/max_element benchmarks, and add the missing minmax_element
entry to the thrust extrema benchmark so both implementations can be
compared with the same harness.
@edenfunf
edenfunf requested a review from a team as a code owner July 28, 2026 09:17
@edenfunf
edenfunf requested a review from gonidelis July 28, 2026 09:17
@edenfunf

Copy link
Copy Markdown
Contributor Author

Added the benchmarks: bench/minmax_element/basic.cu for the new overload, plus the missing minmax_element entry in the thrust extrema benchmark so both run on the same harness.

RTX 5070 (sm_120, CUDA 13.3), 2^28 elements, interleaved runs:

T cuda::std thrust ratio
I8 875 us 1216 us 1.39x
I16 1056 us 1250 us 1.18x
I32 1976 us 1960 us 0.99x
I64 3834 us 3804 us 0.99x
F32 1974 us 1960 us 0.99x
F64 5170 us 3945 us 0.76x

While benchmarking I added a 32 bit offset path for counts <= INT_MAX (smaller accumulator, 32 bit offset kernels), which is where the I8/I16 wins come from.

The F64 gap (420 vs 545 GB/s) is not in the operator: thrust runs its own ReduceAgent with a no-init reduce, while this overload is bound to cub::DeviceReduce::TransformReduce and its default tuning. Happy to look into a cub-side ArgMinMax or tuning as a follow-up.

Re-verified: 512 assertions / 8 test cases, plus a 2^31+ elements spot check for the 64 bit offset path.

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

🧹 Nitpick comments (3)
libcudacxx/benchmarks/bench/minmax_element/basic.cu (2)

11-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

suggestion: Include <cstddef> directly. std::size_t is used on Lines 22 and 46, but this file currently relies on another header to provide its declaration. As per coding guidelines, include all headers needed by the symbols being used; do not rely on transitive includes.

Source: Coding guidelines


24-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

suggestion: Declare both in vectors const. Neither benchmark mutates its input, and the resulting const iterators still provide the required read-only random-access traversal. As per coding guidelines, all variables that are not modified must be declared const.

Also applies to: 48-48

Source: Coding guidelines

thrust/benchmarks/bench/extrema/basic.cu (1)

64-67: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

suggestion: Add a matching comparator-based Thrust benchmark if this file is intended to baseline both libcudacxx registrations. The libcudacxx benchmark covers default and explicit-comparator paths, while this registration exercises only the default overload. Based on the PR benchmark scope, preserve comparable default/comparator coverage.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f6fadfaf-7bbc-430d-aedd-7ff11aaa2176

📥 Commits

Reviewing files that changed from the base of the PR and between 513d17c and 8b2bec2.

📒 Files selected for processing (3)
  • libcudacxx/benchmarks/bench/minmax_element/basic.cu
  • libcudacxx/include/cuda/std/__pstl/cuda/minmax_element.h
  • thrust/benchmarks/bench/extrema/basic.cu
🚧 Files skipped from review as they are similar to previous changes (1)
  • libcudacxx/include/cuda/std/__pstl/cuda/minmax_element.h

Comment thread thrust/benchmarks/bench/extrema/basic.cu Outdated
thrust::minmax_element returns two iterators, so the extremum
benchmark helper takes the number of outputs and the minmax_element
entry records two offset writes.
@miscco

miscco commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@edenfunf sorry to delay this again, I am currently swamped with high priority work

@miscco

miscco commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

/ok to test 3ba5ac1

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

😬 CI Workflow Results

🟥 Finished in 10h 33m: Pass: 95%/177 | Total: 6d 00h | Max: 2h 46m | Hits: 49%/941594

See results here.

@bernhardmgruber

Copy link
Copy Markdown
Contributor

Possibly independent of this PR: we should definitely add cub::DeviceReduce::ArgMinMax. I created an issue for this a while back: #6124. We would also need this in Thrust to get rid of the manual kernels for the extrema functions. I did a similar implementation as this PR in #8292 but Thrust was faster. I believe we need to add the appropriate tuning for DeviceReduce for this operator.

This PR can proceed independently, but we should eventually expose cub::DeviceReduce::ArgMinMax, tune it, and then rebase PSTL (and Thrust) on top of the CUB implementation.

@bernhardmgruber

Copy link
Copy Markdown
Contributor

Here is a new CUB API: #10731 and here it shows that it beats Thrust by a lot: #8292. We should probably move to it for PSTL once merged. I think we should wait with this PR for that to happen. The streaming approach of the new CUB API seems also superior to a version based on TransformReduce.

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