Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 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
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
60d17d3
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 29, 2026
d165781
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 29, 2026
b686565
change thrust::transform_iterator to cuda::transform_iterator
davidwendt Jul 29, 2026
cfd4dfa
Merge branch 'main' into dict-non-unique-keys
davidwendt Jul 30, 2026
9e952d6
add null gtests; fix is-valid; add dispatch-type parm
davidwendt Jul 30, 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
9 changes: 6 additions & 3 deletions cpp/doxygen/developer_guide/DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1578,7 +1578,7 @@ the null masks of both struct fields.
## Dictionary columns

Dictionaries provide an efficient way to represent low-cardinality data by storing a single copy
of each value. A dictionary comprises a column of distinct keys and a column containing an index into
of each value. A dictionary comprises a column of keys and a column containing an index into
Comment thread
davidwendt marked this conversation as resolved.
the keys column for each row of the parent column. The keys column may have any fixed-width data_type
or STRING data_type. The indices represent the corresponding positions of each
element's value in the keys. The indices child column can have any signed integer type
Expand All @@ -1589,8 +1589,11 @@ input column will produce equivalent dictionary columns but the keys may be in a
and therefore the indices will not match as well. Using `cudf::dictionary::decode()` on both dictionary
columns should produce the same result.

Although `cudf::make_dictionary_column()` expects distinct keys, the API does not enforce this constraint.
Using a dictionary column with non-distinct keys in libcudf APIs may result in undefined behavior.
The libcudf APIs also accept dictionary columns with non-unique keys.
However, output dictionary columns will generally contain unique keys in an unspecified order.
The exceptions are `cudf::make_dictionary_column()`, which accepts keys and indices without
changing them, and `cudf::dictionary::set_keys()`, which strictly honors the given keys
(both order and duplicates).
Comment thread
davidwendt marked this conversation as resolved.

## Nested column challenges

Expand Down
3 changes: 2 additions & 1 deletion cpp/include/cudf/dictionary/dictionary_column_view.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ namespace CUDF_EXPORT cudf {
* @brief A wrapper class for operations on a dictionary column.
*
* A dictionary column contains a set of keys and a column of indices.
* The keys are a sorted set of unique values for the column.
* The keys are not required to be unique.
*
* The indices represent the corresponding positions of each element's
* value in the keys.
*/
Expand Down
15 changes: 4 additions & 11 deletions cpp/include/cudf/dictionary/dictionary_factories.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ namespace CUDF_EXPORT cudf {
* @brief Construct a dictionary column by copying the provided `keys`
* and `indices`.
*
* It is expected that `keys_column.has_nulls() == false`.
* It is assumed the elements in `keys_column` are unique and
* are in a strict, total order. Meaning, `keys_column[i]` is ordered before
* `keys_column[i+1]` for all `i in [0,n-1)` where `n` is the number of keys.
* The keys_column must contain no nulls.
*
* The indices values must be in the range [0,keys_column.size()).
*
Expand All @@ -49,7 +46,7 @@ namespace CUDF_EXPORT cudf {
* @throw std::invalid_argument if keys_column contains nulls
* @throw std::invalid_argument if indices_column type is not a signed integer
*
* @param keys_column Column of unique, ordered values to use as the new dictionary column's keys.
* @param keys_column Column of non-null values to use as the new dictionary column's keys.
* @param indices_column Indices to use for the new 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.
Expand All @@ -66,16 +63,12 @@ std::unique_ptr<column> make_dictionary_column(
* and indices columns.
*
* The keys_column and indices columns must contain no nulls.
* It is assumed the elements in `keys_column` are unique and
* are in a strict, total order. Meaning, `keys_column[i]` is ordered before
* `keys_column[i+1]` for all `i in [0,n-1)` where `n` is the number of keys.
*
* The indices values must be in the range [0,keys_column.size()).
*
* @throw std::invalid_argument if keys_column or indices_column contains nulls
* @throw std::invalid_argument if indices_column type is not a signed integer type
*
* @param keys_column Column of unique, ordered values to use as the new dictionary column's keys.
* @param keys_column Column of non-null values to use as the new dictionary column's keys.
* @param indices_column Indices to use for the new dictionary column.
* @param null_mask Null mask for the output column.
* @param null_count Number of nulls for the output column.
Expand All @@ -101,7 +94,7 @@ std::unique_ptr<column> make_dictionary_column(std::unique_ptr<column> keys_colu
*
* @throw cudf::logic_error if keys_column contains nulls
*
* @param keys_column Column of unique, ordered values to use as the new dictionary column's keys.
* @param keys_column Column of non-null values to use as the new dictionary column's keys.
* @param indices_column Indices values and null-mask to use for the new 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.
Expand Down
3 changes: 3 additions & 0 deletions cpp/include/cudf/dictionary/update_keys.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ std::unique_ptr<column> remove_unused_keys(
* The indices are updated to reflect the position values of the new keys.
* Any indices pointing to removed keys sets those rows to null.
*
* Although duplicate keys are allowed, indices in the returned dictionary may
* only reference one of the duplicates.
Comment on lines +124 to +125

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 read this 3 times before I started to understand it.

What is guaranteed/preserved here? I think the number of keys in the output dictionary is keys.size(), even if keys contains duplicates. But because we have to reassign all the indices to match the new keys, we arbitrarily choose one of those duplicate values to provide the index for reassignment. Do we (or should we) provide any guarantee around which of the duplicates is used for the index? First/last/any?

It is hard to provide determinism and lossless conversion/transformation with the ambiguities introduced by duplicate keys.

Maybe there's a better way to say this, or maybe the current state is fine.

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.

We introduced non-determinism by no longer requiring sorted keys.
So determinism would be a big requirement and we would have reconsider all of the current dictionary implementation in libcudf again.

*
* @code{.pseudo}
* d1 = {keys=["a", "b", "c"], indices=[2, 0, 1, 2, 1]}
* d2 = set_keys(existing_dict, ["b","c","d"])
Expand Down
6 changes: 6 additions & 0 deletions cpp/include/cudf/reduction/detail/reduction_functions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,14 @@ std::unique_ptr<scalar> max(column_view const& col,
* If all elements in input column are null, output scalar is null.
*
* @param col input column to compute reduction
* @param dispatch_type The type to dispatch on. For dictionary columns this must be the keys type;
* for all other columns it must equal `col.type()`.
* @param stream CUDA stream used for device memory operations and kernel launches
* @param mr Device memory resource used to allocate the returned scalar's device memory
* @return Index of the minimum element as scalar of type `output_dtype`
*/
std::unique_ptr<scalar> argmin(column_view const& col,
data_type dispatch_type,
rmm::cuda_stream_view stream,
rmm::device_async_resource_ref mr);

Expand All @@ -121,11 +124,14 @@ std::unique_ptr<scalar> argmin(column_view const& col,
* If all elements in input column are null, output scalar is null.
*
* @param col input column to compute reduction
* @param dispatch_type The type to dispatch on. For dictionary columns this must be the keys type;
* for all other columns it must equal `col.type()`.
* @param stream CUDA stream used for device memory operations and kernel launches
* @param mr Device memory resource used to allocate the returned scalar's device memory
* @return Index of the maximum element as scalar of type `output_dtype`
*/
std::unique_ptr<scalar> argmax(column_view const& col,
data_type dispatch_type,
rmm::cuda_stream_view stream,
rmm::device_async_resource_ref mr);

Expand Down
8 changes: 2 additions & 6 deletions cpp/src/reductions/argmax.cu
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

#include "extrema_utils.cuh"

#include <cudf/dictionary/dictionary_column_view.hpp>
#include <cudf/utilities/traits.hpp>

namespace cudf::reduction::detail {

std::unique_ptr<scalar> argmax(column_view const& input,
data_type dispatch_type,
rmm::cuda_stream_view stream,
rmm::device_async_resource_ref mr)
{
auto const dispatch_type =
is_dictionary(input.type()) ? dictionary_column_view(input).indices().type() : input.type();
return type_dispatcher(
dispatch_type, simple::detail::arg_minmax_dispatcher<aggregation::ARGMAX>{}, input, stream, mr);
}
Expand Down
8 changes: 2 additions & 6 deletions cpp/src/reductions/argmin.cu
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

#include "extrema_utils.cuh"

#include <cudf/dictionary/dictionary_column_view.hpp>
#include <cudf/utilities/traits.hpp>

namespace cudf::reduction::detail {

std::unique_ptr<scalar> argmin(column_view const& input,
data_type dispatch_type,
rmm::cuda_stream_view stream,
rmm::device_async_resource_ref mr)
{
auto const dispatch_type =
is_dictionary(input.type()) ? dictionary_column_view(input).indices().type() : input.type();
return type_dispatcher(
dispatch_type, simple::detail::arg_minmax_dispatcher<aggregation::ARGMIN>{}, input, stream, mr);
}
Expand Down
32 changes: 25 additions & 7 deletions cpp/src/reductions/extrema_utils.cuh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand All @@ -8,6 +8,7 @@
#include "nested_types_extrema_utils.cuh"

#include <cudf/aggregation.hpp>
#include <cudf/dictionary/detail/iterator.cuh>
#include <cudf/dictionary/dictionary_column_view.hpp>
#include <cudf/scalar/scalar_factories.hpp>
#include <cudf/utilities/traits.hpp>
Expand Down Expand Up @@ -98,6 +99,9 @@ class arg_minmax_dispatcher {
}

// This function is used for types such as string, timestamp, fixed point, etc.
// `input` may be the dictionary column itself (rather than its keys or a decoded column):
// the lexicographic self_comparator compares `keys[indices[i]]` for dictionary columns
// directly, so no decoding is required and no assumption is made that the keys are sorted.
template <typename ElementType>
[[nodiscard]] size_type find_arg_minmax(column_view const& input,
rmm::cuda_stream_view stream) const
Expand All @@ -121,10 +125,26 @@ class arg_minmax_dispatcher {
rmm::cuda_stream_view stream) const
requires(cudf::is_numeric<ElementType>()) // integer + floating point numbers
{
using Op = std::conditional_t<K == aggregation::ARGMIN,
reduction::detail::op::min,
reduction::detail::op::max>;
// Dictionary keys are not guaranteed to be sorted, so the index of the min/max index does
// not identify the min/max key. Read `keys[indices[i]]` directly per row via a lazy
// iterator instead of decoding (and copying) the whole column.
if (is_dictionary(input.type())) {
auto const d_dict = column_device_view::create(input, stream);
if (input.has_nulls()) {
auto const transformer =
Op{}.template get_null_replacing_element_transformer<ElementType>();
auto const p =
cudf::dictionary::detail::make_dictionary_pair_iterator<ElementType>(*d_dict, true);
auto const it = thrust::make_transform_iterator(p, transformer);
return find_extremum_idx(it, input.size(), stream);
}
auto const it = cudf::dictionary::detail::make_dictionary_iterator<ElementType>(*d_dict);
return find_extremum_idx(it, input.size(), stream);
}
if (input.has_nulls()) {
using Op = std::conditional_t<K == aggregation::ARGMIN,
reduction::detail::op::min,
reduction::detail::op::max>;
auto const d_input = column_device_view::create(input, stream);
auto const transformer = Op{}.template get_null_replacing_element_transformer<ElementType>();
auto const it =
Expand All @@ -150,9 +170,7 @@ class arg_minmax_dispatcher {
rmm::device_async_resource_ref mr) const
requires(is_supported<ElementType>())
{
auto const& values =
is_dictionary(input.type()) ? dictionary_column_view(input).get_indices_annotated() : input;
auto const idx = find_arg_minmax<ElementType>(values, stream);
auto const idx = find_arg_minmax<ElementType>(input, stream);
return make_fixed_width_scalar<size_type>(idx, stream, mr);
}

Expand Down
Loading
Loading