Skip to content

Select few large segments in segmented_top_k with cub::DeviceTopK ins… - #23602

Open
a-hirota wants to merge 1 commit into
NVIDIA:mainfrom
a-hirota:feat/segmented-topk-per-segment-fast-path
Open

Select few large segments in segmented_top_k with cub::DeviceTopK ins…#23602
a-hirota wants to merge 1 commit into
NVIDIA:mainfrom
a-hirota:feat/segmented-topk-per-segment-fast-path

Conversation

@a-hirota

@a-hirota a-hirota commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Description

Add a cub::DeviceTopK fast path for eligible inputs with at most 64 segments, an average
covered segment size of at least 16K rows, and k at most 1/8 of that average.

The fast path selects each segment independently, reuses temporary storage, and sorts only
the selected indices to preserve the existing per-segment ordering. Empty segments and
segments with at most k rows are handled without CUB.

Inputs with nulls, floating-point values, unsupported types, more than 64 segments,
an average covered segment size below 16K rows, or k greater than one eighth of that
average continue to use the existing sort-based implementation. Selection among equal
values at the k-th boundary remains unspecified.

This follows the pattern of the non-segmented top_k (#21582), which routes the same
eligible inputs to cub::DeviceTopK and falls back to the sort-based path for everything
else.

Performance

GB10, 16.8M rows, k=100:

Segments Rows per segment Before After
4 4.2M 106.92 ms 0.626 ms
16 1.0M 93.01 ms 0.919 ms
64 262K 84.14 ms 1.85 ms

Peak temporary memory decreased from 192 MiB to under 1 MiB. FLOAT32 remains on the
fallback path and showed no change.

The guard bounds come from crossover measurements (INT32, 16 segments unless noted; the
sort-based path is k-independent):

  • Average segment size: at 8K rows per segment the sort-based path wins (180 µs vs
    452 µs); at 16K the fast path wins 4x (465 µs vs 1.84 ms, also 3.4x at 64 segments).
    The segmented sort's small-segment kernels cut off between 8K and 16K.
  • k: at k = avg/8 the fast path wins at least 3.4x at every measured point (16K-128K
    rows per segment, 16 and 64 segments); at k = avg/2 the margin shrinks to 1.2-1.7x,
    since the post-selection sort approaches the full sort this path avoids.
  • Wider keys: with 8-byte keys (timestamp_s) the margins narrow but the fast path still
    wins at both bounds: 1.8x at 16K rows per segment and 1.5x at k = avg/8 (32K rows per
    segment). Below 16K it falls back, which errs toward the unchanged sort-based path.

A segmented_top_k_few_large benchmark is added (num_segments = {4, 16, 64}, segment =
{16K, 128K, 1M}, k = {100, 2048}) so the fast path's representative shapes and the guard
bounds stay measurable for future re-evaluation.

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@copy-pr-bot

copy-pr-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

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-actions github-actions Bot added the libcudf Affects libcudf (C++/CUDA) code. label Aug 9, 2026
@a-hirota
a-hirota force-pushed the feat/segmented-topk-per-segment-fast-path branch 2 times, most recently from f0baa97 to d814d8b Compare August 10, 2026 00:37
Add a cub::DeviceTopK fast path for eligible inputs with at most 64
segments, an average covered segment size of at least 16K rows, and k
at most 1/8 of that average.

The fast path selects each segment independently, reuses temporary
storage, and sorts only the selected indices to preserve the existing
per-segment ordering. Empty segments and segments with at most k rows
are handled without CUB.

Inputs with nulls, floating-point values, unsupported types, more than
64 segments, an average covered segment size below 16K rows, or k
greater than one eighth of that average continue to use the existing
sort-based implementation.
@a-hirota
a-hirota force-pushed the feat/segmented-topk-per-segment-fast-path branch from d814d8b to 0d73d76 Compare August 10, 2026 00:38
@a-hirota
a-hirota marked this pull request as ready for review August 10, 2026 00:49
@a-hirota
a-hirota requested a review from a team as a code owner August 10, 2026 00:49
@a-hirota
a-hirota requested review from bdice and simoneves August 10, 2026 00:49
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Performance

    • Improved segmented top-k performance for eligible fixed-width, non-floating-point data without nulls.
    • Added an optimized path for workloads with a small number of large segments and small top-k values.
    • Automatically retains the existing implementation for unsupported or less suitable workloads.
  • Bug Fixes

    • Improved handling of empty, undersized, and partially covered segments.
    • Added support validation across ascending and descending results, including chronological data.

Walkthrough

Changes

The segmented top-k implementation adds a CUB-based fast path for eligible large, fixed-width, non-floating-point inputs without nulls. Existing sort-based behavior remains the fallback. Benchmarks and tests cover large-segment configurations and boundary cases.

Segmented top-k optimization

Layer / File(s) Summary
Benchmark execution and coverage
cpp/benchmarks/sort/segmented_top_k.cpp
Shared benchmark setup is extracted into run_segmented_top_k. A benchmark for configurations with few large segments is added.
CUB fast path and fallback selection
cpp/src/sort/segmented_top_k.cu
Eligible inputs use per-segment CUB top-k selection with stable index reordering. Unsupported inputs use the existing sort-based implementation.
Large-partition validation
cpp/tests/sort/top_k_tests.cpp
Tests cover ordering, indices, empty and undersized segments, uncovered rows, and the 64/65 segment boundary.

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

Possibly related PRs

  • rapidsai/cudf#23105: Both changes modify segmented top-k implementation and tests, including uncovered-segment handling.

Suggested labels: 3 - Ready for Review, non-breaking

Suggested reviewers: bdice, davidwendt

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the segmented top-k change and the new CUB DeviceTopK path for few large segments.
Description check ✅ Passed The description directly explains the DeviceTopK fast path, eligibility rules, fallback behavior, benchmarks, and performance results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
cpp/src/sort/segmented_top_k.cu (3)

222-228: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Run clang-format on this call.

The continuation lines of stable_segmented_sort_by_key are not aligned consistently. The {topk_order} and {nulls} arguments use a different indentation than the other arguments.

As per coding guidelines: "Format C++ and CUDA code with clang-format."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/sort/segmented_top_k.cu` around lines 222 - 228, Run clang-format on
the stable_segmented_sort_by_key call in the segmented top-k implementation,
correcting the continuation-line indentation while preserving the call’s
arguments and behavior.

Source: Coding guidelines


170-172: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Document the non-deterministic tie behavior in the fast path.

The environment requests determinism::not_guaranteed. When several rows share the key value at the k-th boundary, cub::DeviceTopK can select a different subset on each run. The sort-based path selects the same subset on every run for the same input. The public API therefore becomes run-to-run non-deterministic for tied inputs on eligible columns only.

Add a note in the public segmented_top_k and segmented_top_k_order documentation, or use determinism::gpu_to_gpu if the cost is acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/sort/segmented_top_k.cu` around lines 170 - 172, Document in the
public segmented_top_k and segmented_top_k_order APIs that the fast path may
select different rows across runs when ties occur at the k-th boundary, while
the sort-based path remains deterministic. Alternatively, replace
determinism::not_guaranteed with determinism::gpu_to_gpu if its performance cost
is acceptable.

292-306: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Note the added device-to-host synchronization on the eligibility check.

cudf::detail::make_host_vector synchronizes the stream. Every null-free fixed-width column with at most 64 segments now pays this synchronization, including columns that later fall back to sort_based_segmented_top_k_order because of the average-size or k checks. The host offsets are required for the host-side segment loop, so the copy is unavoidable on the fast path. Consider ordering the cheap checks so that fewer fallback cases pay the synchronization, for example by checking col.size() / num_segments >= cub_min_avg_segment_size before the copy.

As per coding guidelines: "Avoid unnecessary host-device synchronization".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/sort/segmented_top_k.cu` around lines 292 - 306, Reorder the
fast-path eligibility checks in the segmented top-k dispatch so the cheap
average-size test using col.size() and num_segments runs before
make_host_vector. Only copy segment_offsets to h_offsets when the column passes
the segment-count, average-size, and k-fraction checks; preserve h_offsets for
dispatch_segmented_topk_fn and retain sort_based_segmented_top_k_order as the
fallback.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/sort/segmented_top_k.cu`:
- Around line 151-166: Validate the host offsets before entering the fast path
in segmented_top_k_order, ensuring they are nondecreasing and each value is
within the input column’s row count. For invalid offsets, route to
sort_based_segmented_top_k_order or reject via CUDF_EXPECTS; only compute
h_out_offsets and allocate indices after validation so negative or oversized
ranges cannot trigger invalid allocations or reads.
- Around line 136-140: Update is_fast_path to reject DECIMAL128 in addition to
null, floating-point, and non-fixed-width exclusions, ensuring DECIMAL128 uses
sort_based_segmented_top_k_order while DECIMAL32 and DECIMAL64 remain eligible
for DeviceTopK.

---

Nitpick comments:
In `@cpp/src/sort/segmented_top_k.cu`:
- Around line 222-228: Run clang-format on the stable_segmented_sort_by_key call
in the segmented top-k implementation, correcting the continuation-line
indentation while preserving the call’s arguments and behavior.
- Around line 170-172: Document in the public segmented_top_k and
segmented_top_k_order APIs that the fast path may select different rows across
runs when ties occur at the k-th boundary, while the sort-based path remains
deterministic. Alternatively, replace determinism::not_guaranteed with
determinism::gpu_to_gpu if its performance cost is acceptable.
- Around line 292-306: Reorder the fast-path eligibility checks in the segmented
top-k dispatch so the cheap average-size test using col.size() and num_segments
runs before make_host_vector. Only copy segment_offsets to h_offsets when the
column passes the segment-count, average-size, and k-fraction checks; preserve
h_offsets for dispatch_segmented_topk_fn and retain
sort_based_segmented_top_k_order as the fallback.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 3dca77ea-d205-4eea-ae9e-4efc267233cb

📥 Commits

Reviewing files that changed from the base of the PR and between 6a22d1d and 0d73d76.

📒 Files selected for processing (3)
  • cpp/benchmarks/sort/segmented_top_k.cpp
  • cpp/src/sort/segmented_top_k.cu
  • cpp/tests/sort/top_k_tests.cpp

Comment on lines +136 to +140
bool is_fast_path(column_view const& column)
{
return !column.has_nulls() && cudf::is_fixed_width(column.type()) &&
!cudf::is_floating_point(column.type()); // requires NaN-aware ordering
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the tests cover decimal or chrono types on the fast path.
rg -n 'TopKSegmented' -C5 cpp/tests/sort/top_k_tests.cpp | head -120

Repository: rapidsai/cudf

Length of output: 5381


🌐 Web query:

CUB DeviceTopK supported key types 128-bit integer __int128 radix

💡 Result:

As of August 10, 2026, cub::DeviceTopK supports both fundamental numeric types and custom user-defined types [1][2]. While the core implementation of DeviceTopK is radix-based [3], it does not have native, specialized support for 128-bit integers (e.g., int128) as a built-in fundamental type in the same manner as standard primitives like int or double [1][2]. However, you can process 128-bit integers by utilizing the support for custom types via a decomposer object [1][2]. To use 128-bit integers with DeviceTopK: 1. You must provide a decomposer object that allows the algorithm to extract the necessary bits or components for the radix-based partitioning [1][2]. 2. The interface requires that for custom types, you use the overload that accepts a decomposer, as the non-decomposer API is strictly limited to fundamental types [2]. The official documentation specifies that DeviceTopK can process built-in C++ numeric primitives, CUDA's 16-bit floating-point types (half and nv_bfloat16), and any user-defined types provided with a corresponding decomposer [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target implementation ---'
sed -n '1,240p' cpp/src/sort/segmented_top_k.cu
printf '%s\n' '--- CUB DeviceTopK call sites ---'
rg -n -C8 'DeviceTopK|cub_segmented_top_k_order|dispatch_storage_type|is_fast_path' cpp/src cpp/include cpp/tests | head -260
printf '%s\n' '--- decimal128 and test type coverage ---'
rg -n -C4 'DECIMAL128|decimal128|fixed_point|TopKTypes|using.*TypeParam|Types<' cpp/tests/sort cpp/src/sort cpp/include | head -300
printf '%s\n' '--- dependency/version evidence ---'
rg -n -C3 'CCCL|CUB|cub' cmake CMakeLists.txt cpp/include cpp/src | head -240

Repository: rapidsai/cudf

Length of output: 50369


🌐 Web query:

Official CCCL cub::DeviceTopK documentation: determine whether the non-decomposer overload accepts __int128_t or decimal128 keys, and identify the exact supported key-type rules and version.

💡 Result:

The CCCL cub::DeviceTopK non-decomposer overload does not officially support int128_t keys [1][2]. Supported Key-Type Rules: cub::DeviceTopK is implemented using radix-based logic, which relies on cub::Traits for bit-level interpretation [3]. The non-decomposer API is strictly limited to built-in C++ numeric primitive types (such as unsigned char, int, double, etc.) and specific CUDA floating-point types, namely half and nv_bfloat16 [1][2]. Types such as int128_t are not considered fundamental numeric primitives within this context and therefore do not function with the non-decomposer overload [3]. For types not supported by the non-decomposer API, developers must use the decomposer-based overload [1][4]. This version allows for custom types by utilizing a decomposer object to interpret the keys, though it explicitly prohibits the use of fundamental types that are already covered by the standard overload [4]. Version Context: Support for non-fundamental types via a decomposer was added to DeviceTopK in recent updates (e.g., via PR #8040, merged around March 2026) [3][4][5]. Despite this expansion, the non-decomposer path remains restricted to the aforementioned built-in types [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- dispatch and path selection ---'
sed -n '220,380p' cpp/src/sort/segmented_top_k.cu
printf '%s\n' '--- segmented top-k test type definitions and decimal coverage ---'
sed -n '1,150p' cpp/tests/sort/top_k_tests.cpp
rg -n -C5 'DECIMAL128|decimal128|fixed_point|TestTypes|ComparableTypes' cpp/tests/sort/top_k_tests.cpp cpp/tests/utilities/type_list.hpp cpp/tests | head -220
printf '%s\n' '--- exact type-dispatch mappings ---'
sed -n '160,230p' cpp/include/cudf/utilities/type_dispatcher.hpp
rg -n -C4 'device_storage_type_t|DECIMAL128' cpp/include/cudf | head -120

Repository: rapidsai/cudf

Length of output: 42800


🌐 Web query:

CCCL DeviceTopK MinPairs MaxPairs non-decomposer overload __int128_t compile error decimal128 exact documentation

💡 Result:

The issues and compilation errors you are observing are linked to architectural and structural limitations within the CUDA Core Compute Libraries (CCCL) regarding 128-bit types and the design of cub::DeviceTopK. Regarding int128_t compile errors: The int128_t and uint128_t types are often not supported in device code, leading to compilation errors such as "int128_t contains a 128-bit integer, which is not supported in device code" [1]. If you encounter this, ensure your environment supports it or explicitly disable it using the _LIBCUDACXX_HAS_NO_INT128 macro [1]. CCCL also provides a configuration macro, CCCL_DISABLE_INT128_SUPPORT, to disable this support if needed [2]. Note that hardware and compiler support for 128-bit types (including float128) varies significantly between target architectures (e.g., x86 vs. ARM) and compiler versions [3]. Regarding cub::DeviceTopK and non-decomposer overloads: The cub::DeviceTopK interface is designed primarily for fundamental types [4]. If you are attempting to use a custom decomposer with a fundamental type, you will trigger a static assertion error, as the library explicitly restricts custom decomposers to non-fundamental types [5]. The error message "Custom decomposers are not supported for fundamental types; use the non-decomposer API overload instead" appears because the library expects you to use the standard MinPairs or MaxPairs overloads for basic data types [5]. When working with custom, non-fundamental types, you must provide a decomposer to interpret the type, as the library's internal radix-based partitioning logic otherwise cannot handle them [5][4]. Regarding decimal128: There is no "decimal128 exact" support provided natively within the core CCCL / CUB library. While other systems (like ClickHouse or MongoDB) define their own Decimal128 types for specific purposes [6][7], CCCL does not provide an exact decimal128 arithmetic type. If you are seeing references to "decimal128" in conjunction with CCCL, it is likely related to external library interactions or confusion with the aforementioned 128-bit integer support issues, which are distinct from IEEE 754-2008 decimal floating-point arithmetic.

Citations:


Exclude DECIMAL128 from the DeviceTopK fast path.

dispatch_storage_type maps DECIMAL128 to __int128_t, but the non-decomposer cub::DeviceTopK::MinPairs and MaxPairs overloads do not support 128-bit keys. Add a DECIMAL128 guard to is_fast_path so these inputs use sort_based_segmented_top_k_order. DECIMAL32 and DECIMAL64 use supported 32-bit and 64-bit storage types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/sort/segmented_top_k.cu` around lines 136 - 140, Update is_fast_path
to reject DECIMAL128 in addition to null, floating-point, and non-fixed-width
exclusions, ensuring DECIMAL128 uses sort_based_segmented_top_k_order while
DECIMAL32 and DECIMAL64 remain eligible for DeviceTopK.

Comment on lines +151 to +166
auto const num_segments = static_cast<size_type>(h_offsets.size()) - 1;

auto h_out_offsets = std::vector<size_type>(num_segments + 1);
h_out_offsets[0] = 0;
for (size_type i = 0; i < num_segments; ++i) {
auto const size = h_offsets[i + 1] - h_offsets[i];
h_out_offsets[i + 1] = h_out_offsets[i] + cuda::std::min(size, k);
}

// Synchronous copy before any CUB work is queued: h_out_offsets is stack-local and an
// async copy would defer the read.
auto offsets = std::make_unique<column>(
cudf::detail::make_device_uvector(h_out_offsets, stream, mr), rmm::device_buffer{}, 0);

auto const temp_mr = cudf::get_current_device_resource_ref();
auto indices = rmm::device_uvector<size_type>(h_out_offsets[num_segments], stream, temp_mr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the fast path against non-monotonic or out-of-range offsets.

h_out_offsets accumulates min(size, k) for each segment. If h_offsets is not monotonically increasing, size becomes negative and the running total shrinks. The rmm::device_uvector size then becomes negative, and a negative value converted to std::size_t produces an enormous allocation request. The per-segment reads at Line 177 (in + begin) also read out of bounds when an offset exceeds col.size().

The sort-based path does not have the same failure mode, because resolve_segment_indices resolves segments on device against the actual row count. The fast path therefore changes the failure behavior for malformed offsets.

The host offsets are already available at Line 294. Add a cheap validation there and route invalid offsets to sort_based_segmented_top_k_order, or reject them with CUDF_EXPECTS.

🛡️ Proposed eligibility guard in `segmented_top_k_order`
     auto const avg_segment_size = (h_offsets.back() - h_offsets.front()) / num_segments;
-    if (avg_segment_size >= cub_min_avg_segment_size &&
-        k <= avg_segment_size / cub_max_k_fraction) {
+    auto const valid_offsets =
+      h_offsets.front() >= 0 && h_offsets.back() <= col.size() &&
+      std::is_sorted(h_offsets.begin(), h_offsets.end());
+    if (valid_offsets && avg_segment_size >= cub_min_avg_segment_size &&
+        k <= avg_segment_size / cub_max_k_fraction) {

As per coding guidelines: "Prevent invalid memory access, including out-of-bounds access" and "Validate inputs such as negative dimensions and null pointers".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/sort/segmented_top_k.cu` around lines 151 - 166, Validate the host
offsets before entering the fast path in segmented_top_k_order, ensuring they
are nondecreasing and each value is within the input column’s row count. For
invalid offsets, route to sort_based_segmented_top_k_order or reject via
CUDF_EXPECTS; only compute h_out_offsets and allocate indices after validation
so negative or oversized ranges cannot trigger invalid allocations or reads.

Source: Coding guidelines

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

Labels

libcudf Affects libcudf (C++/CUDA) code.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant