Skip to content

[BUG] cudf::pack/unpack (contiguous_split) silently corrupts dictionary columns: the keys child comes back with the parent's row count #23888

Description

@a-hirota

Describe the bug

cudf::pack followed by cudf::unpack on a table containing a DICTIONARY32 column does not raise an error, but the unpacked column's keys child has the parent's size (e.g. 5 keys become 100,000), so the column is invalid. contiguous_split has a CUDF_FAIL("Unsupported type") overload for dictionary32, but it is never selected.

Steps/Code to reproduce bug

#include <cudf/column/column_factories.hpp>
#include <cudf/contiguous_split.hpp>
#include <cudf/dictionary/dictionary_column_view.hpp>
#include <cudf/dictionary/encode.hpp>
#include <cudf/table/table_view.hpp>
#include <cuda_runtime.h>
#include <cstdio>
#include <vector>

int main() {
  int n = 100000;
  std::vector<int32_t> h(n);
  for (int i = 0; i < n; ++i) h[i] = i % 5;
  auto plain = cudf::make_numeric_column(cudf::data_type{cudf::type_id::INT32}, n);
  cudaMemcpy(plain->mutable_view().data<int32_t>(), h.data(), n * 4, cudaMemcpyHostToDevice);
  auto dict = cudf::dictionary::encode(*plain);  // INT32 indices, 5 keys
  std::printf("before: size=%d keys=%d\n", dict->size(), cudf::dictionary_column_view(*dict).keys_size());
  auto packed = cudf::pack(cudf::table_view{{*dict}});
  auto tv     = cudf::unpack(packed);
  std::printf("after : size=%d keys=%d\n", tv.column(0).size(), cudf::dictionary_column_view(tv.column(0)).keys_size());
}

Output:

before: size=100000 keys=5
after : size=100000 keys=100000

Expected behavior

Either a round trip that preserves the dictionary (keys child of size 5), or an explicit cudf::logic_error ("Unsupported type") as the code intends — not a silently malformed column.

Environment

  • libcudf 26.08.00 (aarch64, CUDA 13.3). Unchanged on main (d45003e0d0): cpp/src/copying/contiguous_split.cu lines 486-514.

Root cause (analysis)

buf_info_functor has an unconstrained primary template

template <typename T>
std::pair<src_buf_info*, size_type> operator()(column_view const& col, src_buf_info* current, int, int, int, rmm::cuda_stream_view)

and a constrained variadic overload meant for dictionaries

template <typename T, typename... Args>
std::pair<src_buf_info*, size_type> operator()(Args&&...) requires(std::is_same_v<T, cudf::dictionary32>)
{ CUDF_FAIL("Unsupported type"); }

For T = dictionary32 both are viable; partial ordering prefers the non-variadic primary template over the parameter pack, so the CUDF_FAIL overload is dead code and the dictionary parent is treated as a fixed-width 4-byte column. Its children are never visited by setup_source_buf_info (only string / list / struct recurse), while count_src_bufs and the packed metadata still count the two children, so unpack rebuilds them from mismatched metadata — hence the keys child with the parent's size.

Suggested fix

Make the dictionary overload actually win (e.g. constrain the primary template with requires(!cudf::is_dictionary<T>()), or give the dictionary overload the same non-variadic signature), so that dictionary input fails loudly; or implement proper support by recursing into the indices and keys children as the list/struct overloads do.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions