Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
adb5e88
Support non-unique keys for dictionary column types
davidwendt Jun 10, 2026
b681491
Merge branch 'main' into dict-non-unique-keys
davidwendt Jun 12, 2026
f525d92
Merge branch 'main' into dict-non-unique-keys
davidwendt Jun 22, 2026
3c7bb7e
Merge branch 'main' into dict-non-unique-keys
davidwendt Jun 24, 2026
20a011b
add more gtests
davidwendt Jun 24, 2026
14c1296
Merge branch 'main' into dict-non-unique-keys
davidwendt Jun 25, 2026
c5ab27b
Merge branch 'main' into dict-non-unique-keys
davidwendt Jun 26, 2026
9bce5c6
Merge branch 'main' into dict-non-unique-keys
davidwendt Jun 29, 2026
1217031
Merge branch 'main' into dict-non-unique-keys
davidwendt Jun 30, 2026
1c7f8cd
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 1, 2026
ed286ad
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 1, 2026
ff49295
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 1, 2026
b5c0f64
fix min/max logic for non-unique keys
davidwendt Jul 1, 2026
277840b
fix style violations
davidwendt Jul 1, 2026
2d4373d
use make_unique instead of new
davidwendt Jul 1, 2026
64d5acd
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 1, 2026
b9252c9
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 2, 2026
b79cbbe
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 2, 2026
08cc3cc
fix doxygen description for keys parameter
davidwendt Jul 2, 2026
9b1c2c2
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 6, 2026
010a95b
update developer guide
davidwendt Jul 6, 2026
df0882f
add more conditions to the gather test
davidwendt Jul 6, 2026
c379a9f
undistinct fix in developer guide
davidwendt Jul 6, 2026
275c043
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 7, 2026
6489638
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 8, 2026
549df2f
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 10, 2026
27c1f2a
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 10, 2026
9cb3d3b
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 13, 2026
c38de88
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 14, 2026
0d182cb
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 14, 2026
e71a5bb
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 14, 2026
e9d45e5
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 15, 2026
b83ece0
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 15, 2026
734b73b
Add cudf::dictionary::remove_duplicate_keys API
davidwendt Jul 15, 2026
b604531
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 15, 2026
f16be6d
fix minmax for decimal scale
davidwendt Jul 15, 2026
410e759
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 22, 2026
de5e51b
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 23, 2026
d5a7508
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 27, 2026
9d1a331
Merge branch 'dict-non-unique-keys' into dict-remove-dup-keys
davidwendt Jul 27, 2026
60d17d3
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 29, 2026
3a0f87d
Merge branch 'dict-non-unique-keys' into dict-remove-dup-keys
davidwendt Jul 29, 2026
b6c551c
fix merge conflicts
davidwendt Jul 31, 2026
c0c46b4
Merge branch 'main' into dict-remove-dup-keys
davidwendt Aug 3, 2026
a061146
add more gtests
davidwendt Aug 3, 2026
242b24c
Merge branch 'main' into dict-remove-dup-keys
davidwendt Aug 3, 2026
9e1a049
Merge branch 'main' into dict-remove-dup-keys
davidwendt Aug 5, 2026
87f9e6c
add no guarantee order to doxygen
davidwendt Aug 5, 2026
9ecf8e8
Merge branch 'main' into dict-remove-dup-keys
davidwendt Aug 5, 2026
16f008a
Merge branch 'main' into dict-remove-dup-keys
davidwendt Aug 6, 2026
662ec1b
Merge branch 'main' into dict-remove-dup-keys
davidwendt Aug 10, 2026
8fd8ff9
Merge branch 'main' into dict-remove-dup-keys
davidwendt Aug 12, 2026
65c4dca
Merge branch 'main' into dict-remove-dup-keys
davidwendt Aug 13, 2026
5019cad
Merge branch 'main' into dict-remove-dup-keys
davidwendt Aug 13, 2026
cc3ad60
change to use cuda::stream_ref
davidwendt Aug 13, 2026
3804e1c
Merge branch 'main' into dict-remove-dup-keys
davidwendt Aug 14, 2026
918003b
fix merge conflict
davidwendt Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cpp/include/cudf/dictionary/detail/update_keys.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ std::unique_ptr<column> set_keys(dictionary_column_view const& dictionary_column
cuda::stream_ref stream,
rmm::device_async_resource_ref mr);

/**
* @copydoc cudf::dictionary::remove_duplicate_keys(dictionary_column_view
* const&,rmm::device_async_resource_ref)
*/
std::unique_ptr<column> remove_duplicate_keys(dictionary_column_view const& dictionary_column,
cuda::stream_ref stream,
rmm::device_async_resource_ref mr);

/**
* @brief Remap the indices of a dictionary column to a new key set, returning
* only the remapped index column with its null mask.
Expand Down
26 changes: 26 additions & 0 deletions cpp/include/cudf/dictionary/update_keys.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,32 @@ std::unique_ptr<column> set_keys(
cuda::stream_ref stream = cudf::get_default_stream(),
rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref());

/**
* @brief Create a new dictionary column by removing any duplicate keys.
*
* Any indices pointing to a duplicate key are remapped to just one of the duplicates.
* There is no guarantee which of the duplicate keys will be kept in the output column.
*
* @code{.pseudo}
* d1 = {keys=["b", "a", "b", "c"], indices=[3, 0, 2, 1, 2]}
* d2 = remove_duplicate_keys(d1)
* d2 is now {keys=["b", "a", "c"], indices=[2, 0, 0, 1, 0]}
* @endcode
*
* The output column will have the same number of rows as the input column.
* Null entries from the input column are copied to the output column.
* No new null entries are created by this operation.
*
* @param dictionary_column Existing dictionary column.
* @param stream CUDA stream used for device memory operations and kernel launches.
* @param mr Device memory resource used to allocate the returned column's device memory.
* @return New dictionary column with unique keys.
*/
std::unique_ptr<column> remove_duplicate_keys(
dictionary_column_view const& dictionary_column,
cuda::stream_ref stream = cudf::get_default_stream(),
rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref());
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* @brief Create new dictionaries that have keys merged from the input dictionaries.
*
Expand Down
27 changes: 27 additions & 0 deletions cpp/src/dictionary/remove_keys.cu
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#include <cudf/detail/indexalator.cuh>
#include <cudf/detail/nvtx/ranges.hpp>
#include <cudf/detail/search.hpp>
#include <cudf/detail/stream_compaction.hpp>
#include <cudf/detail/valid_if.cuh>
#include <cudf/dictionary/detail/update_keys.hpp>
#include <cudf/dictionary/dictionary_column_view.hpp>
#include <cudf/dictionary/dictionary_factories.hpp>
#include <cudf/dictionary/update_keys.hpp>
Expand Down Expand Up @@ -194,6 +196,23 @@ std::unique_ptr<column> remove_unused_keys(dictionary_column_view const& diction
return remove_keys_fn(dictionary_column, key_matcher, stream, mr);
}

std::unique_ptr<column> remove_duplicate_keys(dictionary_column_view const& input,
cuda::stream_ref stream,
rmm::device_async_resource_ref mr)
{
if (input.size() == 0) { return std::make_unique<column>(input.parent(), stream, mr); }
// deduplicate keys
auto unique_keys = cudf::detail::stable_distinct(table_view{{input.keys()}},
Comment thread
davidwendt marked this conversation as resolved.
{0},
duplicate_keep_option::KEEP_FIRST,
null_equality::EQUAL,
nan_equality::UNEQUAL,
stream,
cudf::get_current_device_resource_ref());
// set_keys then remaps all indices into this compacted key set in a single pass
return detail::set_keys(input, unique_keys->get_column(0).view(), stream, mr);
Comment on lines +205 to +213

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That was basically my intuition from seeing the PR title 🙂 Isn’t this essentially distinct + set_keys? @davidwendt, I’m wondering if we really need a separate facade for just these two cudf API calls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm ok with removing it. I was asked if there was a remove_duplicate_keys API once non-unique keys landed. And did not know a distinct + set_keys would fit until working on this. I could certainly foresee a case where something more involved (or more specialized for performance?) would be required in the future as dictionary keeps evolving.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm inclined to document the current effort and explain that we don't expose this convenience wrapper because it's simply a lightweight layer over existing APIs. If this feature request gains traction again in the future, we can revisit it and implement an optimized solution in one go.

}
Comment thread
davidwendt marked this conversation as resolved.

} // namespace detail

// external APIs
Expand All @@ -215,5 +234,13 @@ std::unique_ptr<column> remove_unused_keys(dictionary_column_view const& diction
return detail::remove_unused_keys(dictionary_column, stream, mr);
}

std::unique_ptr<column> remove_duplicate_keys(dictionary_column_view const& dictionary_column,
cuda::stream_ref stream,
rmm::device_async_resource_ref mr)
{
CUDF_FUNC_RANGE();
return detail::remove_duplicate_keys(dictionary_column, stream, mr);
}

} // namespace dictionary
} // namespace cudf
99 changes: 99 additions & 0 deletions cpp/tests/dictionary/remove_keys_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <cudf/copying.hpp>
#include <cudf/dictionary/dictionary_column_view.hpp>
#include <cudf/dictionary/dictionary_factories.hpp>
#include <cudf/dictionary/encode.hpp>
#include <cudf/dictionary/update_keys.hpp>
#include <cudf/utilities/error.hpp>
Expand Down Expand Up @@ -163,3 +164,101 @@ TEST_F(DictionaryRemoveKeysTest, Errors)
cudf::test::fixed_width_column_wrapper<int64_t> null_keys{{1, 2, 3}, {true, false, true}};
EXPECT_THROW(cudf::dictionary::remove_keys(dictionary->view(), null_keys), cudf::logic_error);
}

TEST_F(DictionaryRemoveKeysTest, RemoveDuplicateKeysStrings)
{
// Build a dictionary with intentionally duplicate keys via set_keys.
// keys = ["aaa", "ccc", "eee", "ccc"] (position 1 and 3 are both "ccc")
// indices point into that key set
auto input =
cudf::test::dictionary_column_wrapper<std::string>({"eee", "aaa", "ccc", "ccc", "eee", "aaa"});
auto dup_keys = cudf::test::strings_column_wrapper{"aaa", "ccc", "eee", "ccc"};
auto const with_dups = cudf::dictionary::set_keys(input, dup_keys);
cudf::dictionary_column_view dv(with_dups->view());
EXPECT_EQ(dv.keys_size(), 4); // confirm we have duplicates

auto const result = cudf::dictionary::remove_duplicate_keys(dv);
cudf::dictionary_column_view rv(result->view());

EXPECT_EQ(rv.keys_size(), 3);
cudf::test::strings_column_wrapper expected_decoded{"eee", "aaa", "ccc", "ccc", "eee", "aaa"};
auto const decoded = cudf::dictionary::decode(rv);
CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(decoded->view(), expected_decoded);
}

TEST_F(DictionaryRemoveKeysTest, RemoveDuplicateKeysIntegers)
{
// keys = [10, 20, 10, 30] (10 appears at positions 0 and 2)
// All indices pointing to position 2 must be remapped to position 0 after dedup
cudf::test::fixed_width_column_wrapper<int32_t> keys_col{10, 20, 10, 30};
cudf::test::fixed_width_column_wrapper<int32_t> indices_col{2, 0, 3, 1, 2, 0};
auto with_dups = cudf::make_dictionary_column(
keys_col.release(), indices_col.release(), rmm::device_buffer{}, 0);
cudf::dictionary_column_view dv(with_dups->view());
EXPECT_EQ(dv.keys_size(), 4);

auto const result = cudf::dictionary::remove_duplicate_keys(dv);
cudf::dictionary_column_view rv(result->view());

EXPECT_EQ(rv.keys_size(), 3);
cudf::test::fixed_width_column_wrapper<int32_t> expected_decoded{10, 10, 30, 20, 10, 10};
auto const decoded = cudf::dictionary::decode(rv);
CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(decoded->view(), expected_decoded);
}

TEST_F(DictionaryRemoveKeysTest, RemoveDuplicateKeysNoDuplicates)
{
// When no duplicates exist the output should be equivalent to the input
auto input = cudf::test::dictionary_column_wrapper<std::string>({"b", "a", "c", "a", "b"});
auto const result = cudf::dictionary::remove_duplicate_keys(cudf::dictionary_column_view(input));
auto const decoded = cudf::dictionary::decode(cudf::dictionary_column_view(result->view()));
cudf::test::strings_column_wrapper expected{"b", "a", "c", "a", "b"};
CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(decoded->view(), expected);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

TEST_F(DictionaryRemoveKeysTest, RemoveDuplicateKeysEmpty)
{
auto input = cudf::test::dictionary_column_wrapper<int32_t>({});
auto const result = cudf::dictionary::remove_duplicate_keys(cudf::dictionary_column_view(input));
EXPECT_EQ(result->size(), 0);
EXPECT_EQ(cudf::dictionary_column_view(result->view()).keys_size(), 0);
}

TEST_F(DictionaryRemoveKeysTest, RemoveDuplicateKeysSliced)
{
// Build a dictionary with duplicate keys then slice it to exercise offset handling.
// keys = ["aaa", "ccc", "eee", "ccc"] — "ccc" appears at positions 1 and 3
auto input =
cudf::test::dictionary_column_wrapper<std::string>({"eee", "aaa", "ccc", "ccc", "eee", "aaa"});
auto dup_keys = cudf::test::strings_column_wrapper{"aaa", "ccc", "eee", "ccc"};
auto const with_dups = cudf::dictionary::set_keys(input, dup_keys);

// Slice to rows [1, 4) → logical values {"aaa", "ccc", "ccc"}
auto sliced = cudf::slice(with_dups->view(), {1, 4}).front();
auto const result = cudf::dictionary::remove_duplicate_keys(cudf::dictionary_column_view(sliced));
cudf::dictionary_column_view rv(result->view());

EXPECT_EQ(rv.keys_size(), 3);
cudf::test::strings_column_wrapper expected_decoded{"aaa", "ccc", "ccc"};
auto const decoded = cudf::dictionary::decode(rv);
CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(decoded->view(), expected_decoded);
}

TEST_F(DictionaryRemoveKeysTest, RemoveDuplicateKeysWithNullRows)
{
// Null rows in the parent must be preserved; no new nulls are introduced
auto input = cudf::test::dictionary_column_wrapper<std::string>(
{"eee", "aaa", "ccc", "ccc", "eee"}, {true, true, false, true, true});
auto dup_keys = cudf::test::strings_column_wrapper{"aaa", "ccc", "eee", "ccc"};
auto const with_dups = cudf::dictionary::set_keys(input, dup_keys);
cudf::dictionary_column_view dv(with_dups->view());

auto const result = cudf::dictionary::remove_duplicate_keys(dv);
cudf::dictionary_column_view rv(result->view());
EXPECT_EQ(rv.keys_size(), 3);

cudf::test::strings_column_wrapper expected_decoded({"eee", "aaa", "", "ccc", "eee"},
{true, true, false, true, true});
auto const decoded = cudf::dictionary::decode(rv);
CUDF_TEST_EXPECT_COLUMNS_EQUAL(decoded->view(), expected_decoded);
}
Comment thread
davidwendt marked this conversation as resolved.
13 changes: 12 additions & 1 deletion cpp/tests/streams/dictionary_test.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand All @@ -14,6 +14,8 @@
#include <cudf/dictionary/search.hpp>
#include <cudf/dictionary/update_keys.hpp>

#include <rmm/device_buffer.hpp>

class DictionaryTest : public cudf::test::BaseFixture {};

TEST_F(DictionaryTest, FactoryColumnViews)
Expand Down Expand Up @@ -98,6 +100,15 @@ TEST_F(DictionaryTest, RemoveUnsedKeys)
cudf::dictionary::remove_unused_keys(dict_col_view, cudf::test::get_default_stream());
}

TEST_F(DictionaryTest, RemoveDuplicateKeys)
{
cudf::test::fixed_width_column_wrapper<int32_t> keys_col{10, 20, 10, 30};
cudf::test::fixed_width_column_wrapper<int32_t> indices_col{0, 1, 2, 3, 0};
auto dict = cudf::make_dictionary_column(
keys_col.release(), indices_col.release(), rmm::device_buffer{}, 0);
cudf::dictionary::remove_duplicate_keys(dict->view(), cudf::test::get_default_stream());
}

TEST_F(DictionaryTest, SetKeys)
{
std::vector<int32_t> elements{0, 2, 2, 6, 6};
Expand Down
Loading