Skip to content

Commit bb3346e

Browse files
PointKernelabigalekim
authored andcommitted
Add a direct_inner_join API for pre-hashed distinct UINT32 keys (rapidsai#23147)
Closes rapidsai#23146 This PR adds a `direct_inner_join` free function to libcudf, the first step of the perfect hash join effort in rapidsai#23126. The keys are a single `UINT32` column per side, produced by a prior perfect hashing pass such as `cudf::key_remapping`, dictionary encoding, or dense integer primary keys. The right keys act as a perfect hash of the right rows: a lookup table of caller-specified `capacity` entries maps each key value to its row index and each left key probes that table directly, so the join performs no hashing or key comparison at all. - The caller controls the memory footprint via the explicit `capacity` argument. All key values must be in `[0, capacity)` and the right keys must be distinct; behavior is undefined otherwise. - Inner join only, as a free function: no table reuse across probes is needed, so there is no join object. - The build scatters right row indices into the lookup table with `cub::DeviceTransform::Fill` + `cub::DeviceFor::Bulk`; the probe is a single `cub`-based `copy_if` pass emitting the matched index pairs. - A new `JOIN_NVBENCH` benchmark compares `inner_join`, `distinct_hash_join`, and `direct_inner_join` on identical conforming input; results in the comment below. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - Tianyu Liu (https://github.com/kingcrimsontianyu) - Shruti Shivakumar (https://github.com/shrshi) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: rapidsai#23147
1 parent a02c53c commit bb3346e

8 files changed

Lines changed: 428 additions & 1 deletion

File tree

cpp/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,7 @@ add_library(
841841
src/jit/util.cpp
842842
src/join/conditional_join.cu
843843
src/join/cross_join.cu
844+
src/join/direct_join.cu
844845
src/join/distinct_hash_join.cu
845846
src/join/filter_join_indices/filter_join_indices.cu
846847
src/join/filter_join_indices/filter_join_indices_jit.cu

cpp/benchmarks/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ ConfigureNVBench(
150150
ConfigureNVBench(
151151
JOIN_NVBENCH
152152
join/conditional_join.cpp
153+
join/direct_join.cu
153154
join/distinct_join.cpp
154155
join/filter_join_indices.cpp
155156
join/filter_join_indices_jit.cu

cpp/benchmarks/join/direct_join.cu

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include "join_common.hpp"
7+
8+
#include <cudf/column/column_factories.hpp>
9+
#include <cudf/join/direct_join.hpp>
10+
#include <cudf/join/distinct_hash_join.hpp>
11+
#include <cudf/join/join.hpp>
12+
13+
#include <thrust/execution_policy.h>
14+
#include <thrust/functional.h>
15+
#include <thrust/random.h>
16+
#include <thrust/sequence.h>
17+
#include <thrust/shuffle.h>
18+
#include <thrust/tabulate.h>
19+
20+
// Apples-to-apples comparison of inner join implementations on input that satisfies
21+
// `direct_inner_join`'s preconditions: a single UINT32 key column per side, distinct right keys,
22+
// and all key values in [0, capacity) with capacity = right_size. The right keys are the shuffled
23+
// dense values [0, right_size), the key_remapping/dense-primary-key case, so every left key
24+
// matches and the input is identical for all algorithms.
25+
void nvbench_direct_inner_join(nvbench::state& state)
26+
{
27+
if (should_skip_large_sizes(state)) { return; }
28+
29+
auto const right_size = static_cast<cudf::size_type>(state.get_int64("right_size"));
30+
auto const left_size = static_cast<cudf::size_type>(state.get_int64("left_size"));
31+
auto const algorithm = state.get_string("algorithm");
32+
auto const capacity = static_cast<std::size_t>(right_size);
33+
34+
// Dense distinct right keys: a shuffled sequence of [0, capacity)
35+
auto right = cudf::make_numeric_column(
36+
cudf::data_type{cudf::type_id::UINT32}, right_size, cudf::mask_state::UNALLOCATED);
37+
thrust::sequence(thrust::device,
38+
right->mutable_view().begin<std::uint32_t>(),
39+
right->mutable_view().end<std::uint32_t>());
40+
thrust::shuffle(thrust::device,
41+
right->mutable_view().begin<std::uint32_t>(),
42+
right->mutable_view().end<std::uint32_t>(),
43+
thrust::default_random_engine{12345});
44+
45+
// Left keys cycle through [0, capacity), then shuffled
46+
auto left = cudf::make_numeric_column(
47+
cudf::data_type{cudf::type_id::UINT32}, left_size, cudf::mask_state::UNALLOCATED);
48+
thrust::tabulate(thrust::device,
49+
left->mutable_view().begin<std::uint32_t>(),
50+
left->mutable_view().end<std::uint32_t>(),
51+
thrust::placeholders::_1 % static_cast<std::uint32_t>(right_size));
52+
thrust::shuffle(thrust::device,
53+
left->mutable_view().begin<std::uint32_t>(),
54+
left->mutable_view().end<std::uint32_t>(),
55+
thrust::default_random_engine{67890});
56+
57+
auto const left_view = left->view();
58+
auto const right_view = right->view();
59+
auto const left_keys = cudf::table_view{{left_view}};
60+
auto const right_keys = cudf::table_view{{right_view}};
61+
62+
auto const input_bytes = estimate_size(left_keys) + estimate_size(right_keys);
63+
state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().value()));
64+
state.add_element_count(input_bytes, "input_bytes");
65+
state.add_global_memory_reads<nvbench::int8_t>(input_bytes);
66+
67+
if (algorithm == "hash") {
68+
state.exec(nvbench::exec_tag::sync, [&](nvbench::launch&) {
69+
auto result = cudf::inner_join(left_keys, right_keys, cudf::null_equality::UNEQUAL);
70+
});
71+
} else if (algorithm == "distinct_hash") {
72+
state.exec(nvbench::exec_tag::sync, [&](nvbench::launch&) {
73+
auto hj_obj = cudf::distinct_hash_join{right_keys, cudf::null_equality::UNEQUAL, 0.5};
74+
auto result = hj_obj.inner_join(left_keys);
75+
});
76+
} else if (algorithm == "direct") {
77+
state.exec(nvbench::exec_tag::sync, [&](nvbench::launch&) {
78+
auto result = cudf::direct_inner_join(left_view, right_view, capacity);
79+
});
80+
} else {
81+
state.skip("unknown algorithm");
82+
}
83+
}
84+
85+
NVBENCH_BENCH(nvbench_direct_inner_join)
86+
.set_name("direct_inner_join")
87+
.add_string_axis("algorithm", {"hash", "distinct_hash", "direct"})
88+
.add_int64_axis("left_size", JOIN_SIZE_RANGE)
89+
.add_int64_axis("right_size", JOIN_SIZE_RANGE)
90+
.add_int64_axis("skip_large_sizes", {1});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#pragma once
7+
8+
#include <cudf/column/column_view.hpp>
9+
#include <cudf/types.hpp>
10+
#include <cudf/utilities/default_stream.hpp>
11+
#include <cudf/utilities/export.hpp>
12+
#include <cudf/utilities/memory_resource.hpp>
13+
14+
#include <rmm/cuda_stream_view.hpp>
15+
#include <rmm/device_uvector.hpp>
16+
17+
#include <memory>
18+
#include <utility>
19+
20+
namespace CUDF_EXPORT cudf {
21+
22+
/**
23+
* @addtogroup column_join
24+
* @{
25+
* @file
26+
* @brief Direct join APIs for pre-hashed integer keys
27+
*/
28+
29+
/**
30+
* @brief Returns the row indices that can be used to construct the result of performing an inner
31+
* join between two key columns whose values directly determine the matched row index
32+
*
33+
* The right keys are treated as a perfect hash of the right rows: a lookup table of `capacity`
34+
* entries maps each key value to its row index, and each left key probes that table directly. No
35+
* hashing or key comparison is performed. Left keys that do not occur in the right keys produce no
36+
* output pair.
37+
*
38+
* @note Behavior is undefined if any key value is not less than `capacity`, or if the right keys
39+
* contain duplicates.
40+
*
41+
* @throw cudf::data_type_error if the key columns are not of type UINT32
42+
* @throw std::invalid_argument if the key columns contain nulls
43+
* @throw std::invalid_argument if `capacity` is less than the number of right keys
44+
*
45+
* @param left_keys The left key column containing pre-hashed keys in `[0, capacity)`, from which
46+
* the keys are probed
47+
* @param right_keys The right key column containing distinct pre-hashed keys in `[0, capacity)`
48+
* @param capacity The number of entries in the lookup table
49+
* @param stream CUDA stream used for device memory operations and kernel launches
50+
* @param mr Device memory resource used to allocate the returned indices' device memory
51+
*
52+
* @return A pair of vectors [`left_indices`, `right_indices`] that can be used to construct the
53+
* result of performing an inner join between two tables with `left_keys` and `right_keys` as the
54+
* join keys
55+
*/
56+
[[nodiscard]] std::pair<std::unique_ptr<rmm::device_uvector<size_type>>,
57+
std::unique_ptr<rmm::device_uvector<size_type>>>
58+
direct_inner_join(column_view const& left_keys,
59+
column_view const& right_keys,
60+
std::size_t capacity,
61+
rmm::cuda_stream_view stream = cudf::get_default_stream(),
62+
rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref());
63+
64+
/** @} */ // end of group
65+
66+
} // namespace CUDF_EXPORT cudf

cpp/src/join/direct_join.cu

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include <cudf/column/column_view.hpp>
7+
#include <cudf/detail/algorithms/copy_if.cuh>
8+
#include <cudf/detail/nvtx/ranges.hpp>
9+
#include <cudf/join/direct_join.hpp>
10+
#include <cudf/join/join.hpp>
11+
#include <cudf/types.hpp>
12+
#include <cudf/utilities/error.hpp>
13+
#include <cudf/utilities/memory_resource.hpp>
14+
15+
#include <rmm/cuda_stream_view.hpp>
16+
#include <rmm/device_uvector.hpp>
17+
18+
#include <cub/device/device_for.cuh>
19+
#include <cub/device/device_transform.cuh>
20+
#include <cuda/iterator>
21+
#include <cuda/std/iterator>
22+
23+
#include <cstdint>
24+
#include <memory>
25+
#include <utility>
26+
27+
namespace cudf {
28+
namespace detail {
29+
namespace {
30+
31+
// Scatters each right row index to the lookup slot addressed by its key value
32+
struct scatter_right_index {
33+
size_type* lookup;
34+
std::uint32_t const* right_keys;
35+
36+
__device__ void operator()(size_type right_idx) const
37+
{
38+
lookup[right_keys[right_idx]] = right_idx;
39+
}
40+
};
41+
42+
// Writes the (left, right) index pair of the `out_idx`-th match, given a matched left row index
43+
struct emit_match_pair {
44+
size_type* left_out;
45+
size_type* right_out;
46+
size_type const* lookup;
47+
std::uint32_t const* left_keys;
48+
49+
__device__ void operator()(size_type out_idx, size_type left_idx) const
50+
{
51+
left_out[out_idx] = left_idx;
52+
right_out[out_idx] = lookup[left_keys[left_idx]];
53+
}
54+
};
55+
56+
// Returns true if the left row's key hits a right row in the lookup table
57+
struct is_match {
58+
size_type const* lookup;
59+
std::uint32_t const* left_keys;
60+
61+
__device__ bool operator()(size_type left_idx) const
62+
{
63+
return lookup[left_keys[left_idx]] != JoinNoMatch;
64+
}
65+
};
66+
67+
} // namespace
68+
69+
std::pair<std::unique_ptr<rmm::device_uvector<size_type>>,
70+
std::unique_ptr<rmm::device_uvector<size_type>>>
71+
direct_inner_join(column_view const& left_keys,
72+
column_view const& right_keys,
73+
std::size_t capacity,
74+
rmm::cuda_stream_view stream,
75+
rmm::device_async_resource_ref mr)
76+
{
77+
CUDF_EXPECTS(
78+
left_keys.type().id() == type_id::UINT32 and right_keys.type().id() == type_id::UINT32,
79+
"direct_inner_join keys must be of type UINT32",
80+
cudf::data_type_error);
81+
CUDF_EXPECTS(not left_keys.has_nulls() and not right_keys.has_nulls(),
82+
"direct_inner_join keys must not contain nulls",
83+
std::invalid_argument);
84+
CUDF_EXPECTS(static_cast<std::size_t>(right_keys.size()) <= capacity,
85+
"capacity must be at least the number of right keys",
86+
std::invalid_argument);
87+
88+
if (left_keys.is_empty() or right_keys.is_empty()) {
89+
return std::pair(std::make_unique<rmm::device_uvector<size_type>>(0, stream, mr),
90+
std::make_unique<rmm::device_uvector<size_type>>(0, stream, mr));
91+
}
92+
93+
// Build: scatter each right row index to the slot addressed by its key value
94+
auto lookup =
95+
rmm::device_uvector<size_type>(capacity, stream, cudf::get_current_device_resource_ref());
96+
CUDF_CUDA_TRY(
97+
cub::DeviceTransform::Fill(lookup.begin(), lookup.size(), JoinNoMatch, stream.value()));
98+
CUDF_CUDA_TRY(
99+
cub::DeviceFor::Bulk(right_keys.size(),
100+
scatter_right_index{lookup.data(), right_keys.begin<std::uint32_t>()},
101+
stream.value()));
102+
103+
// Probe: a single pass emitting the (left index, matched right index) pairs
104+
auto left_indices =
105+
std::make_unique<rmm::device_uvector<size_type>>(left_keys.size(), stream, mr);
106+
auto right_indices =
107+
std::make_unique<rmm::device_uvector<size_type>>(left_keys.size(), stream, mr);
108+
109+
auto const d_left_keys = left_keys.begin<std::uint32_t>();
110+
auto const out_iter = cuda::tabulate_output_iterator{
111+
emit_match_pair{left_indices->data(), right_indices->data(), lookup.data(), d_left_keys}};
112+
113+
auto const out_end = cudf::detail::copy_if(cuda::counting_iterator<size_type>{0},
114+
cuda::counting_iterator<size_type>{left_keys.size()},
115+
out_iter,
116+
is_match{lookup.data(), d_left_keys},
117+
stream);
118+
119+
auto const num_matches = cuda::std::distance(out_iter, out_end);
120+
left_indices->resize(num_matches, stream);
121+
right_indices->resize(num_matches, stream);
122+
123+
return std::pair(std::move(left_indices), std::move(right_indices));
124+
}
125+
126+
} // namespace detail
127+
128+
std::pair<std::unique_ptr<rmm::device_uvector<size_type>>,
129+
std::unique_ptr<rmm::device_uvector<size_type>>>
130+
direct_inner_join(column_view const& left_keys,
131+
column_view const& right_keys,
132+
std::size_t capacity,
133+
rmm::cuda_stream_view stream,
134+
rmm::device_async_resource_ref mr)
135+
{
136+
CUDF_FUNC_RANGE();
137+
return detail::direct_inner_join(left_keys, right_keys, capacity, stream, mr);
138+
}
139+
140+
} // namespace cudf

cpp/tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ ConfigureTest(
176176
join/cross_join_tests.cpp
177177
join/semi_anti_join_tests.cpp
178178
join/mixed_join_tests.cu
179+
join/direct_join_tests.cpp
179180
join/distinct_join_tests.cpp
180181
join/key_remapping_tests.cpp
181182
GPUS 1

0 commit comments

Comments
 (0)