Skip to content

Implement parallel cuda::std::lexicographical_compare - #8735

Open
edenfunf wants to merge 8 commits into
NVIDIA:mainfrom
edenfunf:feat/cuda-std-lexicographical-compare
Open

edenfunf wants to merge 8 commits into
NVIDIA:mainfrom
edenfunf:feat/cuda-std-lexicographical-compare

Conversation

@edenfunf

@edenfunf edenfunf commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Resolves #7752. Implements the CUDA backend for cuda::std::lexicographical_compare.

Design

Following the suggestion on #7752: a cuda::zip_transform_iterator produces an
int8_t per pair (-1 LESS, +1 GREATER, 0 EQUIV), cub::DeviceFind::FindIf
finds the first non-zero index with early termination, then a 1-element
cub::DeviceReduce::TransformReduce (with cuda::std::identity{} + plus<int8_t>{}
over int8_t{0}) reads the state at that position back to host. Final answer is
state < 0.

Spec complexity bound (≤ 2 * min(N1, N2) comp calls) is met: Pass 1 short-circuits
at 2k worst-case for early-termination index k; Pass 2 adds 1; total ≤ 2 * min - 1.

I prototyped four value-recovery variants locally (1-element FindIf, 1-element
TransformReduce, custom 1-thread kernel, two-FindIf baseline). TransformReduce-identity
won consistently by 5–30% in p50 because cub's 1-element reduce path has lower launch
overhead than 1-element FindIf in this regime. Happy to switch shape if you'd prefer
something different.

Performance (RTX 5070 / sm_120, CUDA 12.9)

Realistic workload — two independent random sequences (≈always diverges at k=0):

N T this PR full-scan reduce baseline
16M int32 103 µs 274 µs
64M int32 100 µs 930 µs
64M int64 110 µs 1755 µs

Worst case for the early-termination design (late divergence and equal-all at
scale): within 5–10% of the no-early-termination baseline.

Tests

  • pstl_lexicographical_compare.cu (default less) — 480 assertions
  • pstl_lexicographical_compare_comp.cu (custom Compare) — 192 assertions

Both cover the all_types matrix × 4 SECTIONs (default stream / provided stream /
provided memory_resource / both), plus edge cases: empty vs empty, empty/non-empty
mixed, single-element ranges (equal/less/greater — exercises the n=1 path through
both passes), prefix in either direction, divergence at k=0/N/2/N-1,
random_access_iterator wrapper, and greater<>/less<> duality
(lex_cmp(s2, s1, greater) == lex_cmp(s1, s2, less)).

bench/lexicographical_compare/basic.cu mirrors is_heap_until/basic.cu's shape
with 2 * violation_point memory accounting and ViolationAt ∈ {1.0, 0.5, 0.01}.

@edenfunf
edenfunf requested review from a team as code owners April 29, 2026 17:45
@edenfunf
edenfunf requested a review from shwina April 29, 2026 17:45
@edenfunf
edenfunf requested a review from davebayer April 29, 2026 17:45
@github-project-automation github-project-automation Bot moved this to Todo in CCCL Apr 29, 2026
@copy-pr-bot

copy-pr-bot Bot commented Apr 29, 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.

@cccl-authenticator-app cccl-authenticator-app Bot moved this from Todo to In Review in CCCL Apr 29, 2026
Comment thread libcudacxx/include/cuda/std/__pstl/lexicographical_compare.h Outdated
@edenfunf
edenfunf force-pushed the feat/cuda-std-lexicographical-compare branch from cabb2b2 to 15c236d Compare May 3, 2026 09:47
@edenfunf

edenfunf commented May 3, 2026

Copy link
Copy Markdown
Contributor Author

Tested locally on RTX 5070 / sm_120, CUDA 12.9, with both variants
implemented identically except for the state type. Per-call wall-clock
p50 (5 warmup + 50 reps via cudaEvent pairs); ratio = int / int8_t
so values < 1.0 mean int wins.

N T workload int8_t p50 int p50 ratio
64M int32 diverge@k=10 101.3 µs 79.6 µs 0.785
16M int64 diverge@k=10 120.6 µs 78.3 µs 0.649
64M int64 diverge@k=10 144.6 µs 117.3 µs 0.811
16M int32 diverge@k=N/2 242.4 µs 225.0 µs 0.928
64M int32 rand-all-diff 77.4 µs 78.2 µs 1.010
64M int32 equal-all 916.7 µs 928.4 µs 1.013

Honest disclosure: small-N + equal-all (where Pass 2 is skipped because
k == n) shows int8_t slightly faster by ~10 µs absolute (e.g., N=1M
i32 equal-all: int8_t 44 µs vs int 55 µs). That regime is launch-bound
either way, and the gain on the hot path (early-divergence + large N,
where Pass 2's 1-element TransformReduce dominates) is the meaningful
trade.

Pushed the swap as 15c236d.

@edenfunf

edenfunf commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Hi @miscco — gentle ping when you have a moment, I think this is ready for another look.

The int8_t → int suggestion is done (15c236d), with the before/after numbers in the table above. I also re-checked locally on a 5070 (CUDA 12.9): both tests pass, plus a 50M cross-check against std::lexicographical_compare across int/int64/uint8/float/double and the edge cases, and memcheck came back clean.

Happy to make any changes — please let me know if anything's still blocking. Thanks!

@miscco

miscco commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

/ok to test 15c236d

@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 a lot for the work. I believe we want to coalesce the two allocations we currently perform into a single one

Comment thread libcudacxx/include/cuda/std/__pstl/lexicographical_compare.h Outdated
Comment thread libcudacxx/include/cuda/std/__pstl/lexicographical_compare.h Outdated
Comment thread libcudacxx/include/cuda/std/__pstl/lexicographical_compare.h Outdated
…cation

Address review feedback:
- Replace the -1/0/+1 magic numbers with an `__lex_ordering` enum class
  (`__less`/`__equal`/`__greater`).
- Coalesce the two temporary allocations into one. The frontend no longer
  composes the generic find_if + transform_reduce dispatches (each of which
  allocates its own scratch); instead a dedicated
  `__pstl_dispatch<__lexicographical_compare, __cuda>` queries both cub passes
  up front, allocates a single `__temporary_storage` sized to the larger pass,
  and reuses that scratch region for both FindIf and the 1-element
  TransformReduce read-back. Only the two 1-element result slots are extra.

Also harden the common-length computation with `common_type_t` so iterators
with differing difference_type compile.
@edenfunf

edenfunf commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@miscco thanks for the review! All three points are addressed in 8b90fc46:

  • magic numbers are now an __lex_ordering enum class
  • the CUDA path moved into a dedicated __pstl/cuda/lexicographical_compare.h backend that only allocates once — both passes share a single __temporary_storage, sized to the larger of the two

Re-ran everything locally (RTX 5070, CUDA 12.9): tests pass and memcheck is clean. Let me know if anything else needs changing.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

🥳 CI Workflow Results

🟩 Finished in 1h 43m: Pass: 100%/110 | Total: 2d 11h | Max: 1h 04m | Hits: 97%/317054

See results here.

Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h Outdated
Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h
Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h Outdated
Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h Outdated

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

Sorry approved accidentally

@github-project-automation github-project-automation Bot moved this from In Review to In Progress in CCCL Jun 1, 2026
edenfunf and others added 2 commits June 2, 2026 12:03
Co-authored-by: Michael Schellenberger Costa <miscco@nvidia.com>
- Add a non-const `operator()` overload to `__lex_state_fn` for comparators
  with a non-const call operator.
- Rename `__n1/__n2/__n` to `__count1/__count2/__count`.
- Drop the read-back `TransformReduce`: a single-thread result kernel now
  resolves the answer in place on device from the find index, so only one
  value is copied back (no offset host round-trip, no second reduce launch,
  one sync instead of two). Also fixes the missing
  `is_nothrow_move_constructible` include.
@edenfunf

edenfunf commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

No worries! Another look whenever you have time would be great. Thanks again!

Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h Outdated
Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h Outdated
Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h Outdated
Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h
Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h Outdated
Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h
Comment thread libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h Outdated
@bernhardmgruber

Copy link
Copy Markdown
Contributor

Looking at the implementation from a bird’s-eye view, I was thinking whether we could avoid calling an extra kernel if we would extend DeviceFind with some kind of finalizer action when the right position is found. It turns out that's not so easy, because DeviceFind uses atomics in SMEM and GMEM to perform (integral) reductions when it starts to find indices where the predicate is true, so we would need to replace this by some other form of reduction that reduces index and value, since the blocks in the find kernel don't actually know with index will be the global first one (this is resolved in L2).

I think the current approach with using DeviceFind to find the mismatch position and then loading the two values again for the comparison is a bit wasteful, but doing better is a LOT harder. I think this is fine. We can keep a tracking issue around to revisit this.

@miscco

miscco commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

yeah I believe we could do better if we extended FindIf but I believe that is out of scope of this PR

- Move the result kernel inside _CCCL_BEGIN_NAMESPACE_ARCH_DEPENDENT.
- Rename the kernel's __k to __offset.
- Simplify the kernel-arg casts to static_cast<void*> (drop the
  reinterpret_cast/const_cast pair; the launched locals are non-const).
- Construct an __ensure_current_context guard so the stream's device is
  current for the device work.
- Qualify ::cudaErrorMemoryAllocation and rethrow with a plain throw.
@edenfunf

edenfunf commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@bernhardmgruber @miscco agreed, extending FindIf is out of scope here. Happy to keep a tracking issue around to revisit avoiding the second load later.

@miscco

miscco commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Sorry I totally dropped the ball on this. Ping me if I did not respond on monday

@edenfunf

Copy link
Copy Markdown
Contributor Author

Sorry I totally dropped the ball on this. Ping me if I did not respond on monday

No worries at all! Hope you have a great weekend :>

@edenfunf

edenfunf commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@miscco When you have a chance, could you please take a look? Thanks!

@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

📝 Summary

Summary by CodeRabbit

  • New Features

    • Added parallel CUDA support for cuda::std::lexicographical_compare.
    • Added execution-policy overloads, including support for custom comparators, streams, and memory resources.
    • Added handling for empty ranges, prefixes, differing elements, and configurable iterator types.
  • Tests

    • Added comprehensive coverage for default and custom-comparator comparisons across supported execution configurations.
  • Benchmarks

    • Added benchmarks measuring comparison performance and early termination across input sizes.

Walkthrough

Added the CUDA PSTL execution-policy overload and backend for cuda::std::lexicographical_compare. Added coverage for range ordering, comparators, execution options, and early-termination benchmarks.

Changes

Parallel lexicographical comparison

Layer / File(s) Summary
PSTL API and dispatch wiring
libcudacxx/include/cuda/std/__pstl/lexicographical_compare.h, libcudacxx/include/cuda/std/__pstl/dispatch.h, libcudacxx/include/cuda/std/execution
Adds the execution-policy overload, dispatch identifier, comparator and iterator constraints, backend selection, and execution-header inclusion.
CUDA backend implementation
libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h
Classifies paired elements, finds the first non-equal pair with CUB, resolves mismatch or prefix ordering on the device, copies the result to the host, and handles CUDA errors.
Validation and benchmarking
libcudacxx/test/libcudacxx/std/algorithms/alg.sorting/alg.lex.comparison/*, libcudacxx/benchmarks/bench/lexicographical_compare/basic.cu
Tests empty, equal, prefix, divergent, reversed, wrapped-iterator, comparator, stream, and memory-resource cases. Benchmarks default and cuda::std::less<> comparisons with configurable violation positions.

Assessment against linked issues

Objective Addressed Explanation
Implement a CUDA backend for parallel cuda::std::lexicographical_compare [#7752]

Suggested reviewers: miscco

Priority: ➖ Normal

Change: Feature

Merge Risk: 🔵 Low · up to 0f034

The new benchmark underreports logical reads by one compared element pair. Correct its accounting before using it for performance comparisons; no implementation correctness issue was established.


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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: NVIDIA/cccl/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: bd6d63d9-e47a-4048-8d9b-35a9e44fe29d

📥 Commits

Reviewing files that changed from the base of the PR and between 2fcc3f0 and 0f03425.

📒 Files selected for processing (7)
  • libcudacxx/benchmarks/bench/lexicographical_compare/basic.cu
  • libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h
  • libcudacxx/include/cuda/std/__pstl/dispatch.h
  • libcudacxx/include/cuda/std/__pstl/lexicographical_compare.h
  • libcudacxx/include/cuda/std/execution
  • libcudacxx/test/libcudacxx/std/algorithms/alg.sorting/alg.lex.comparison/pstl_lexicographical_compare.cu
  • libcudacxx/test/libcudacxx/std/algorithms/alg.sorting/alg.lex.comparison/pstl_lexicographical_compare_comp.cu

Included review availability: Your plan provides up to 12 included reviews per hour; 9 remain after this review.

thrust::device_vector<T> dinput2(elements, thrust::no_init);
prepare_inputs(dinput1, dinput2, violation_point);

state.add_global_memory_reads<T>(2 * violation_point);

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.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,110p' libcudacxx/benchmarks/bench/lexicographical_compare/basic.cu
rg -n -C4 'add_global_memory_reads|violation_point|lexicographical_compare' libcudacxx/benchmarks libcudacxx/include/cuda/std/__pstl/cuda

Repository: NVIDIA/cccl

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '145,235p' libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h
printf '\\n-- CUB DeviceFind definitions and relevant FindIf code --\\n'
rg -n -l 'class DeviceFind|struct DeviceFind|DeviceFind::FindIf|FindIf\\(' --glob '*.{h,hpp,cu,cpp,cuh}' . | head -40
printf '\\n-- focused CUB references --\\n'
rg -n -C8 'DeviceFind|FindIf' libcudacxx/include third_party cub thrust --glob '*.{h,hpp,cu,cpp,cuh}' 2>/dev/null | head -240

Repository: NVIDIA/cccl

Length of output: 20911


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '68,170p' cub/cub/device/device_find.cuh
printf '\\n-- dispatch_find structure and kernels --\\n'
rg -n -C6 'dispatch|kernel|Scan|Flag|predicate|scan_op|num_items|FindIf' cub/cub/device/dispatch/dispatch_find.cuh | head -320
printf '\\n-- complete focused implementation slices --\\n'
sed -n '1,260p' cub/cub/device/dispatch/dispatch_find.cuh

Repository: NVIDIA/cccl

Length of output: 18263


🏁 Script executed:

#!/bin/bash
set -eu
rg -n -C10 'class AgentFind|struct AgentFind|Process\\(|Load|num_items|tile_size|found_pos' cub/cub/agent/agent_find.cuh | head -360

Repository: NVIDIA/cccl

Length of output: 287


🏁 Script executed:

#!/bin/bash
set -eu
rg -n -F 'Process(' cub/cub/agent/agent_find.cuh
rg -n -F 'num_items' cub/cub/agent/agent_find.cuh
wc -l cub/cub/agent/agent_find.cuh

Repository: NVIDIA/cccl

Length of output: 587


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '50,205p' cub/cub/agent/agent_find.cuh

Repository: NVIDIA/cccl

Length of output: 5494


🏁 Script executed:

#!/bin/bash
set -eu
nl -ba libcudacxx/benchmarks/bench/lexicographical_compare/basic.cu | sed -n '15,82p'
printf '\\n-- lexicographical predicate --\\n'
nl -ba libcudacxx/include/cuda/std/__pstl/cuda/lexicographical_compare.h | sed -n '80,145p'

Repository: NVIDIA/cccl

Length of output: 6166


important: Count the logical differing pair. violation_point is zero-based, so the logical comparison covers indices 0 through violation_point and accounts for 2 * (violation_point + 1) input elements. Update both add_global_memory_reads<T> calls and revise the comment at lines 22-23 to describe logical comparison work, not an upper bound on physical device reads. CUB DeviceFind::FindIf can read positions beyond the first differing pair because its parallel tiles process assigned elements concurrently.

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]: Implement CUDA backend for parallel cuda::std::lexicographical_compare

4 participants