From 2e13e5fad7dcd91bdc7d1a3641aaab4cbc57d16e Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Fri, 5 Aug 2022 15:49:19 -0500 Subject: [PATCH 01/58] Drop support for `skiprows` and `num_rows` in `cudf.read_parquet` (#11480) This PR removes support for `skiprows` & `num_rows` in parquet reader. A continuation of https://github.com/rapidsai/cudf/pull/11218 Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/11480 --- python/cudf/cudf/_lib/cpp/io/parquet.pxd | 6 - python/cudf/cudf/_lib/parquet.pyx | 12 +- python/cudf/cudf/io/parquet.py | 20 --- python/cudf/cudf/tests/test_parquet.py | 173 ++++------------------- python/cudf/cudf/utils/ioutils.py | 4 - 5 files changed, 32 insertions(+), 183 deletions(-) diff --git a/python/cudf/cudf/_lib/cpp/io/parquet.pxd b/python/cudf/cudf/_lib/cpp/io/parquet.pxd index d152503e82a..88850ff6687 100644 --- a/python/cudf/cudf/_lib/cpp/io/parquet.pxd +++ b/python/cudf/cudf/_lib/cpp/io/parquet.pxd @@ -20,8 +20,6 @@ cdef extern from "cudf/io/parquet.hpp" namespace "cudf::io" nogil: data_type get_timestamp_type() except + bool is_enabled_convert_strings_to_categories() except + bool is_enabled_use_pandas_metadata() except + - size_type get_skip_rows() except + - size_type get_num_rows() except + # setter @@ -29,8 +27,6 @@ cdef extern from "cudf/io/parquet.hpp" namespace "cudf::io" nogil: void set_row_groups(vector[vector[size_type]] row_grp) except + void enable_convert_strings_to_categories(bool val) except + void enable_use_pandas_metadata(bool val) except + - void set_skip_rows(size_type val) except + - void set_num_rows(size_type val) except + void set_timestamp_type(data_type type) except + @staticmethod @@ -55,8 +51,6 @@ cdef extern from "cudf/io/parquet.hpp" namespace "cudf::io" nogil: parquet_reader_options_builder& use_pandas_metadata( bool val ) except + - parquet_reader_options_builder& skip_rows(size_type val) except + - parquet_reader_options_builder& num_rows(size_type val) except + parquet_reader_options_builder& timestamp_type( data_type type ) except + diff --git a/python/cudf/cudf/_lib/parquet.pyx b/python/cudf/cudf/_lib/parquet.pyx index c25360b307d..1be3b953687 100644 --- a/python/cudf/cudf/_lib/parquet.pyx +++ b/python/cudf/cudf/_lib/parquet.pyx @@ -125,7 +125,7 @@ def _parse_metadata(meta): cpdef read_parquet(filepaths_or_buffers, columns=None, row_groups=None, - skiprows=None, num_rows=None, strings_to_categorical=False, + strings_to_categorical=False, use_pandas_metadata=True): """ Cython function to call into libcudf API, see `read_parquet`. @@ -151,8 +151,7 @@ cpdef read_parquet(filepaths_or_buffers, columns=None, row_groups=None, cdef bool cpp_strings_to_categorical = strings_to_categorical cdef bool cpp_use_pandas_metadata = use_pandas_metadata - cdef size_type cpp_skiprows = skiprows if skiprows is not None else 0 - cdef size_type cpp_num_rows = num_rows if num_rows is not None else -1 + cdef vector[vector[size_type]] cpp_row_groups cdef data_type cpp_timestamp_type = cudf_types.data_type( cudf_types.type_id.EMPTY @@ -168,8 +167,6 @@ cpdef read_parquet(filepaths_or_buffers, columns=None, row_groups=None, .row_groups(cpp_row_groups) .convert_strings_to_categories(cpp_strings_to_categorical) .use_pandas_metadata(cpp_use_pandas_metadata) - .skip_rows(cpp_skiprows) - .num_rows(cpp_num_rows) .timestamp_type(cpp_timestamp_type) .build() ) @@ -291,10 +288,7 @@ cpdef read_parquet(filepaths_or_buffers, columns=None, row_groups=None, step=range_index_meta['step'], name=range_index_meta['name'] ) - if skiprows is not None: - idx = idx[skiprows:] - if num_rows is not None: - idx = idx[:num_rows] + df._index = idx elif set(index_col).issubset(column_names): index_data = df[index_col] diff --git a/python/cudf/cudf/io/parquet.py b/python/cudf/cudf/io/parquet.py index 5a181dc076c..1812155d894 100644 --- a/python/cudf/cudf/io/parquet.py +++ b/python/cudf/cudf/io/parquet.py @@ -359,8 +359,6 @@ def read_parquet( columns=None, filters=None, row_groups=None, - skiprows=None, - num_rows=None, strings_to_categorical=False, use_pandas_metadata=True, use_python_file_object=True, @@ -371,18 +369,6 @@ def read_parquet( ): """{docstring}""" - if skiprows is not None: - warnings.warn( - "skiprows is deprecated and will be removed.", - FutureWarning, - ) - - if num_rows is not None: - warnings.warn( - "num_rows is deprecated and will be removed.", - FutureWarning, - ) - # Do not allow the user to set file-opening options # when `use_python_file_object=False` is specified if use_python_file_object is False: @@ -485,8 +471,6 @@ def read_parquet( *args, columns=columns, row_groups=row_groups, - skiprows=skiprows, - num_rows=num_rows, strings_to_categorical=strings_to_categorical, use_pandas_metadata=use_pandas_metadata, partition_keys=partition_keys, @@ -575,8 +559,6 @@ def _read_parquet( engine, columns=None, row_groups=None, - skiprows=None, - num_rows=None, strings_to_categorical=None, use_pandas_metadata=None, *args, @@ -589,8 +571,6 @@ def _read_parquet( filepaths_or_buffers, columns=columns, row_groups=row_groups, - skiprows=skiprows, - num_rows=num_rows, strings_to_categorical=strings_to_categorical, use_pandas_metadata=use_pandas_metadata, ) diff --git a/python/cudf/cudf/tests/test_parquet.py b/python/cudf/cudf/tests/test_parquet.py index 973f8c75553..326c117585b 100644 --- a/python/cudf/cudf/tests/test_parquet.py +++ b/python/cudf/cudf/tests/test_parquet.py @@ -618,30 +618,6 @@ def test_parquet_read_row_groups_non_contiguous(tmpdir, pdf, row_group_size): assert_eq(ref_df, gdf) -@pytest.mark.filterwarnings( - "ignore:skiprows is deprecated and will be removed." -) -@pytest.mark.filterwarnings( - "ignore:num_rows is deprecated and will be removed." -) -@pytest.mark.parametrize("row_group_size", [1, 4, 33]) -def test_parquet_read_rows(tmpdir, pdf, row_group_size): - if len(pdf) > 100: - pytest.skip("Skipping long setup test") - - fname = tmpdir.join("row_group.parquet") - pdf.to_parquet(fname, compression="None", row_group_size=row_group_size) - - total_rows, row_groups, col_names = cudf.io.read_parquet_metadata(fname) - - num_rows = total_rows // 4 - skiprows = (total_rows - num_rows) // 2 - gdf = cudf.read_parquet(fname, skiprows=skiprows, num_rows=num_rows) - - for row in range(num_rows): - assert gdf["col_int32"].iloc[row] == row + skiprows - - def test_parquet_reader_spark_timestamps(datadir): fname = datadir / "spark_timestamp.snappy.parquet" @@ -708,36 +684,6 @@ def test_parquet_reader_invalids(tmpdir): assert_eq(expect, got) -@pytest.mark.filterwarnings( - "ignore:skiprows is deprecated and will be removed." -) -@pytest.mark.filterwarnings( - "ignore:num_rows is deprecated and will be removed." -) -def test_parquet_chunked_skiprows(tmpdir): - processed = 0 - batch = 10000 - n = 100000 - out_df = cudf.DataFrame( - { - "y": np.arange(n), - "z": np.random.choice(range(1000000, 2000000), n, replace=False), - "s": np.random.choice(range(20), n, replace=True), - "a": np.round(np.random.uniform(1, 5000, n), 2), - } - ) - - fname = tmpdir.join("skiprows.parquet") - out_df.to_pandas().to_parquet(fname) - - for i in range(10): - chunk = cudf.read_parquet(fname, skiprows=processed, num_rows=batch) - expect = out_df[processed : processed + batch].reset_index(drop=True) - assert_eq(chunk.reset_index(drop=True), expect) - processed += batch - del chunk - - def test_parquet_reader_filenotfound(tmpdir): with pytest.raises(FileNotFoundError): cudf.read_parquet("TestMissingFile.parquet") @@ -987,20 +933,14 @@ def L(list_size, first_val): ] -def list_gen( - gen, skiprows, num_rows, lists_per_row, list_size, include_validity=False -): +def list_gen(gen, num_rows, lists_per_row, list_size, include_validity=False): """ Generate a list column based on input parameters. Args: gen: A callable which generates an individual leaf element based on an absolute index. - skiprows : Generate the column as if it had started at 'skiprows' - instead of 0. The intent here is to emulate the skiprows - parameter of the parquet reader. - num_rows : Number of rows to generate. Again, this is to emulate the - 'num_rows' parameter of the parquet reader. + num_rows : Number of rows to generate. lists_per_row : Number of lists to generate per row. list_size : Size of each generated list. include_validity : Whether or not to include nulls as part of the @@ -1028,16 +968,16 @@ def R(first_val, lists_per_row, list_size): return [ ( R( - lists_per_row * list_size * (i + skiprows), + lists_per_row * list_size * i, lists_per_row, list_size, ) - if (i + skiprows) % 2 == 0 + if i % 2 == 0 else None ) if include_validity else R( - lists_per_row * list_size * (i + skiprows), + lists_per_row * list_size * i, lists_per_row, list_size, ) @@ -1046,7 +986,7 @@ def R(first_val, lists_per_row, list_size): def test_parquet_reader_list_large(tmpdir): - expect = pd.DataFrame({"a": list_gen(int_gen, 0, 256, 80, 50)}) + expect = pd.DataFrame({"a": list_gen(int_gen, 256, 80, 50)}) fname = tmpdir.join("test_parquet_reader_list_large.parquet") expect.to_parquet(fname) assert os.path.exists(fname) @@ -1056,7 +996,7 @@ def test_parquet_reader_list_large(tmpdir): def test_parquet_reader_list_validity(tmpdir): expect = pd.DataFrame( - {"a": list_gen(int_gen, 0, 256, 80, 50, include_validity=True)} + {"a": list_gen(int_gen, 256, 80, 50, include_validity=True)} ) fname = tmpdir.join("test_parquet_reader_list_validity.parquet") expect.to_parquet(fname) @@ -1068,10 +1008,10 @@ def test_parquet_reader_list_validity(tmpdir): def test_parquet_reader_list_large_mixed(tmpdir): expect = pd.DataFrame( { - "a": list_gen(string_gen, 0, 128, 80, 50), - "b": list_gen(int_gen, 0, 128, 80, 50), - "c": list_gen(int_gen, 0, 128, 80, 50, include_validity=True), - "d": list_gen(string_gen, 0, 128, 80, 50, include_validity=True), + "a": list_gen(string_gen, 128, 80, 50), + "b": list_gen(int_gen, 128, 80, 50), + "c": list_gen(int_gen, 128, 80, 50, include_validity=True), + "d": list_gen(string_gen, 128, 80, 50, include_validity=True), } ) fname = tmpdir.join("test_parquet_reader_list_large_mixed.parquet") @@ -1119,7 +1059,7 @@ def test_parquet_reader_list_large_multi_rowgroup_nulls(tmpdir): row_group_size = 1000 expect = cudf.DataFrame( - {"a": list_gen(int_gen, 0, num_rows, 3, 2, include_validity=True)} + {"a": list_gen(int_gen, num_rows, 3, 2, include_validity=True)} ) # round trip the dataframe to/from parquet @@ -1132,61 +1072,6 @@ def test_parquet_reader_list_large_multi_rowgroup_nulls(tmpdir): assert_eq(expect, got) -@pytest.mark.filterwarnings( - "ignore:skiprows is deprecated and will be removed." -) -@pytest.mark.parametrize("skip", [0, 1, 5, 10]) -def test_parquet_reader_list_skiprows(skip, tmpdir): - num_rows = 10 - src = pd.DataFrame( - { - "a": list_gen(int_gen, 0, num_rows, 80, 50), - "b": list_gen(string_gen, 0, num_rows, 80, 50), - "c": list_gen(int_gen, 0, num_rows, 80, 50, include_validity=True), - } - ) - fname = tmpdir.join("test_parquet_reader_list_skiprows.parquet") - src.to_parquet(fname) - assert os.path.exists(fname) - - expect = src.iloc[skip:] - got = cudf.read_parquet(fname, skiprows=skip) - if expect.empty: - assert_eq(expect, got) - else: - assert pa.Table.from_pandas(expect).equals(got.to_arrow()) - - -@pytest.mark.filterwarnings( - "ignore:skiprows is deprecated and will be removed." -) -@pytest.mark.filterwarnings( - "ignore:num_rows is deprecated and will be removed." -) -@pytest.mark.parametrize("skip", [0, 1, 5, 10]) -def test_parquet_reader_list_num_rows(skip, tmpdir): - num_rows = 20 - src = pd.DataFrame( - { - "a": list_gen(int_gen, 0, num_rows, 80, 50), - "b": list_gen(string_gen, 0, num_rows, 80, 50), - "c": list_gen(int_gen, 0, num_rows, 80, 50, include_validity=True), - "d": list_gen( - string_gen, 0, num_rows, 80, 50, include_validity=True - ), - } - ) - fname = tmpdir.join("test_parquet_reader_list_num_rows.parquet") - src.to_parquet(fname) - assert os.path.exists(fname) - - # make sure to leave a few rows at the end that we don't read - rows_to_read = min(3, (num_rows - skip) - 5) - expect = src.iloc[skip:].head(rows_to_read) - got = cudf.read_parquet(fname, skiprows=skip, num_rows=rows_to_read) - assert pa.Table.from_pandas(expect).equals(got.to_arrow()) - - def struct_gen(gen, skip_rows, num_rows, include_validity=False): """ Generate a struct column based on input parameters. @@ -2069,7 +1954,7 @@ def test_parquet_writer_list_basic(tmpdir): def test_parquet_writer_list_large(tmpdir): - expect = pd.DataFrame({"a": list_gen(int_gen, 0, 256, 80, 50)}) + expect = pd.DataFrame({"a": list_gen(int_gen, 256, 80, 50)}) fname = tmpdir.join("test_parquet_writer_list_large.parquet") gdf = cudf.from_pandas(expect) @@ -2084,10 +1969,10 @@ def test_parquet_writer_list_large(tmpdir): def test_parquet_writer_list_large_mixed(tmpdir): expect = pd.DataFrame( { - "a": list_gen(string_gen, 0, 128, 80, 50), - "b": list_gen(int_gen, 0, 128, 80, 50), - "c": list_gen(int_gen, 0, 128, 80, 50, include_validity=True), - "d": list_gen(string_gen, 0, 128, 80, 50, include_validity=True), + "a": list_gen(string_gen, 128, 80, 50), + "b": list_gen(int_gen, 128, 80, 50), + "c": list_gen(int_gen, 128, 80, 50, include_validity=True), + "d": list_gen(string_gen, 128, 80, 50, include_validity=True), } ) fname = tmpdir.join("test_parquet_writer_list_large_mixed.parquet") @@ -2103,18 +1988,18 @@ def test_parquet_writer_list_large_mixed(tmpdir): def test_parquet_writer_list_chunked(tmpdir): table1 = cudf.DataFrame( { - "a": list_gen(string_gen, 0, 128, 80, 50), - "b": list_gen(int_gen, 0, 128, 80, 50), - "c": list_gen(int_gen, 0, 128, 80, 50, include_validity=True), - "d": list_gen(string_gen, 0, 128, 80, 50, include_validity=True), + "a": list_gen(string_gen, 128, 80, 50), + "b": list_gen(int_gen, 128, 80, 50), + "c": list_gen(int_gen, 128, 80, 50, include_validity=True), + "d": list_gen(string_gen, 128, 80, 50, include_validity=True), } ) table2 = cudf.DataFrame( { - "a": list_gen(string_gen, 0, 128, 80, 50), - "b": list_gen(int_gen, 0, 128, 80, 50), - "c": list_gen(int_gen, 0, 128, 80, 50, include_validity=True), - "d": list_gen(string_gen, 0, 128, 80, 50, include_validity=True), + "a": list_gen(string_gen, 128, 80, 50), + "b": list_gen(int_gen, 128, 80, 50), + "c": list_gen(int_gen, 128, 80, 50, include_validity=True), + "d": list_gen(string_gen, 128, 80, 50, include_validity=True), } ) fname = tmpdir.join("test_parquet_writer_list_chunked.parquet") @@ -2295,10 +2180,10 @@ def test_parquet_writer_statistics(tmpdir, pdf, add_nulls): def test_parquet_writer_list_statistics(tmpdir): df = pd.DataFrame( { - "a": list_gen(string_gen, 0, 128, 80, 50), - "b": list_gen(int_gen, 0, 128, 80, 50), - "c": list_gen(int_gen, 0, 128, 80, 50, include_validity=True), - "d": list_gen(string_gen, 0, 128, 80, 50, include_validity=True), + "a": list_gen(string_gen, 128, 80, 50), + "b": list_gen(int_gen, 128, 80, 50), + "c": list_gen(int_gen, 128, 80, 50, include_validity=True), + "d": list_gen(string_gen, 128, 80, 50, include_validity=True), } ) fname = tmpdir.join("test_parquet_writer_list_statistics.parquet") diff --git a/python/cudf/cudf/utils/ioutils.py b/python/cudf/cudf/utils/ioutils.py index d3c41de842a..f915da5fe69 100644 --- a/python/cudf/cudf/utils/ioutils.py +++ b/python/cudf/cudf/utils/ioutils.py @@ -150,10 +150,6 @@ If not None, specifies, for each input file, which row groups to read. If reading multiple inputs, a list of lists should be passed, one list for each input. -skiprows : int, default None - If not None, the number of rows to skip from the start of the file. -num_rows : int, default None - If not None, the total number of rows to read. strings_to_categorical : boolean, default False If True, return string columns as GDF_CATEGORY dtype; if False, return a as GDF_STRING dtype. From 56816563cd1a798b2a317eda9facb178b8bae833 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Fri, 5 Aug 2022 17:39:08 -0400 Subject: [PATCH 02/58] Refactor group_nunique.cu to use nullate::DYNAMIC for reduce-by-key functor (#11482) Refactored the `group_nunique.cu` source to use the `nullate::DYNAMIC` for the equal operator and the unique-iterator. This improves the compile time by almost 2x without much change to performance by reducing the number of calls to `thrust::reduce_by_key`. Found while investigating compile issues for #11437 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Mike Wilson (https://github.com/hyperbolic2346) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/11482 --- cpp/src/groupby/sort/group_nunique.cu | 93 ++++++++++++++------------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/cpp/src/groupby/sort/group_nunique.cu b/cpp/src/groupby/sort/group_nunique.cu index 478060cbd16..b719698b6b5 100644 --- a/cpp/src/groupby/sort/group_nunique.cu +++ b/cpp/src/groupby/sort/group_nunique.cu @@ -16,7 +16,6 @@ #include #include -#include #include #include #include @@ -24,7 +23,6 @@ #include #include -#include #include #include #include @@ -34,6 +32,41 @@ namespace cudf { namespace groupby { namespace detail { namespace { + +template +struct is_unique_iterator_fn { + Nullate nulls; + column_device_view const v; + element_equality_comparator equal; + null_policy null_handling; + size_type const* group_offsets; + size_type const* group_labels; + + is_unique_iterator_fn(Nullate nulls, + column_device_view const& v, + null_policy null_handling, + size_type const* group_offsets, + size_type const* group_labels) + : nulls{nulls}, + v{v}, + equal{nulls, v, v}, + null_handling{null_handling}, + group_offsets{group_offsets}, + group_labels{group_labels} + { + } + + __device__ size_type operator()(size_type i) + { + bool is_input_countable = + !nulls || (null_handling == null_policy::INCLUDE || v.is_valid_nocheck(i)); + bool is_unique = is_input_countable && + (group_offsets[group_labels[i]] == i || // first element or + (not equal.template operator()(i, i - 1))); // new unique value in sorted + return static_cast(is_unique); + } +}; + struct nunique_functor { template std::enable_if_t(), std::unique_ptr> operator()( @@ -50,49 +83,21 @@ struct nunique_functor { if (num_groups == 0) { return result; } - auto values_view = column_device_view::create(values, stream); - if (values.has_nulls()) { - auto equal = element_equality_comparator{nullate::YES{}, *values_view, *values_view}; - auto is_unique_iterator = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - [v = *values_view, - equal, - null_handling, - group_offsets = group_offsets.data(), - group_labels = group_labels.data()] __device__(auto i) -> size_type { - bool is_input_countable = - (null_handling == null_policy::INCLUDE || v.is_valid_nocheck(i)); - bool is_unique = is_input_countable && - (group_offsets[group_labels[i]] == i || // first element or - (not equal.operator()(i, i - 1))); // new unique value in sorted - return static_cast(is_unique); - }); + auto values_view = column_device_view::create(values, stream); + auto is_unique_iterator = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + is_unique_iterator_fn{nullate::DYNAMIC{values.has_nulls()}, + *values_view, + null_handling, + group_offsets.data(), + group_labels.data()}); + thrust::reduce_by_key(rmm::exec_policy(stream), + group_labels.begin(), + group_labels.end(), + is_unique_iterator, + thrust::make_discard_iterator(), + result->mutable_view().begin()); - thrust::reduce_by_key(rmm::exec_policy(stream), - group_labels.begin(), - group_labels.end(), - is_unique_iterator, - thrust::make_discard_iterator(), - result->mutable_view().begin()); - } else { - auto equal = element_equality_comparator{nullate::NO{}, *values_view, *values_view}; - auto is_unique_iterator = thrust::make_transform_iterator( - thrust::make_counting_iterator(0), - [v = *values_view, - equal, - group_offsets = group_offsets.data(), - group_labels = group_labels.data()] __device__(auto i) -> size_type { - bool is_unique = group_offsets[group_labels[i]] == i || // first element or - (not equal.operator()(i, i - 1)); // new unique value in sorted - return static_cast(is_unique); - }); - thrust::reduce_by_key(rmm::exec_policy(stream), - group_labels.begin(), - group_labels.end(), - is_unique_iterator, - thrust::make_discard_iterator(), - result->mutable_view().begin()); - } return result; } From 6fa49c71d04e8a3e77ab03f3b275af581995ebf7 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 5 Aug 2022 23:25:11 +0100 Subject: [PATCH 03/58] column: calculate null_count before release()ing the cudf::column (#11365) release() sets the null_count of a column to zero, so previously asking for the null_count provided an incorrect value. Fortunately this never exhibited in the final column, since Column.__init__ always ignores the provided null_count and computes it from the null_mask (if one is given). Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Ashwin Srinath (https://github.com/shwina) - Nghia Truong (https://github.com/ttnghia) - GALI PREM SAGAR (https://github.com/galipremsagar) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/11365 --- python/cudf/cudf/_lib/column.pyx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/python/cudf/cudf/_lib/column.pyx b/python/cudf/cudf/_lib/column.pyx index 9e129842724..8a9a79250b9 100644 --- a/python/cudf/cudf/_lib/column.pyx +++ b/python/cudf/cudf/_lib/column.pyx @@ -449,7 +449,7 @@ cdef class Column: size = c_col.get()[0].size() dtype = dtype_from_column_view(c_col.get()[0].view()) - has_nulls = c_col.get()[0].has_nulls() + null_count = c_col.get()[0].null_count() # After call to release(), c_col is unusable cdef column_contents contents = move(c_col.get()[0].release()) @@ -457,13 +457,11 @@ cdef class Column: data = DeviceBuffer.c_from_unique_ptr(move(contents.data)) data = Buffer(data) - if has_nulls: + if null_count > 0: mask = DeviceBuffer.c_from_unique_ptr(move(contents.null_mask)) mask = Buffer(mask) - null_count = c_col.get()[0].null_count() else: mask = None - null_count = 0 cdef vector[unique_ptr[column]] c_children = move(contents.children) children = () From d6951295e36959cb1710c67b719781755607ec61 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 5 Aug 2022 18:34:09 -0700 Subject: [PATCH 04/58] cuDF error handling document (#7917) This document aims to give instruction the following two things: - What to throw given invalid user inputs - How should cuDF handle exceptions from libcudf Authors: - Michael Wang (https://github.com/isVoid) Approvers: - Bradley Dice (https://github.com/bdice) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/7917 --- .../developer_guide/contributing_guide.md | 29 ++++++++++++++++++- docs/cudf/source/developer_guide/index.md | 1 + 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/cudf/source/developer_guide/contributing_guide.md b/docs/cudf/source/developer_guide/contributing_guide.md index 149028453b0..1126e5c110a 100644 --- a/docs/cudf/source/developer_guide/contributing_guide.md +++ b/docs/cudf/source/developer_guide/contributing_guide.md @@ -108,4 +108,31 @@ Any attempt to write pure Cython code for this purpose should be justified with ## Exception handling -This section is under development, see https://github.com/rapidsai/cudf/pull/7917. +In alignment with [maintaining compatibility with pandas](#pandas-compatibility), +any API that cuDF shares with pandas should throw all the same exceptions as the +corresponding pandas API given the same inputs. +However, it is not required to match the corresponding pandas API's exception message. + +When writing error messages, +sufficient information should be included to help users locate the source of the error, +such as including the expected argument type versus the actual argument provided. + +For parameters that are not yet supported, +raise `NotImplementedError`. +There is no need to mention when the argument will be supported in the future. + +### Handling libcudf Exceptions + +Currently libcudf raises `cudf::logic_error` and `cudf::cuda_error`. +These error types are mapped to `RuntimeError` in python. +Several APIs use the exception payload `what()` message to determine the exception type raised by libcudf. + +Determining error type based on exception payload is brittle since libcudf does not maintain API stability on exception messages. +This is a compromise due to libcudf only raising a limited number of error types. +Only adopt this strategy when necessary. + +The projected roadmap is to diversify the exception types raised by libcudf. +Standard C++ natively supports various [exception types](https://en.cppreference.com/w/cpp/error/exception), +which Cython maps to [these Python exception types](https://docs.cython.org/en/latest/src/userguide/wrapping_CPlusPlus.html#exceptions). +In the future, libcudf may employ custom C++ exception types. +If that occurs, this section will be updated to reflect how these may be mapped to desired Python exception types. diff --git a/docs/cudf/source/developer_guide/index.md b/docs/cudf/source/developer_guide/index.md index a9e1ac8cced..04f2bf6b8c0 100644 --- a/docs/cudf/source/developer_guide/index.md +++ b/docs/cudf/source/developer_guide/index.md @@ -21,6 +21,7 @@ Additionally, it includes longer sections on more specific topics like testing a :maxdepth: 2 library_design +contributing_guide documentation benchmarking options From e1a4e039819f2b7910beafac397433510a93196d Mon Sep 17 00:00:00 2001 From: Elias Stehle <3958403+elstehle@users.noreply.github.com> Date: Sat, 6 Aug 2022 12:21:30 +0200 Subject: [PATCH 05/58] Adds JSON tokenizer (#11264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR builds on the _Finite-State Transducer_ (_FST_) algorithm and the _Logical Stack_ to implement a tokenizer that demarcates sections from the JSON input and assigns a category to each such section. **This PR builds on:** ⛓️ https://github.com/rapidsai/cudf/pull/11242 ⛓️ https://github.com/rapidsai/cudf/pull/11078 Specifically, the tokenizer comprises the following processing steps: 1. FST to emit sequence of stack operations (i.e., emit push(LIST), push(STRUCT), pop(), read()). This FST does transduce each occurrence of an opening semantic bracket or brace to the respective push(LIST) and push(STRUCT) operation, respectively. Each semantic closing bracket or brace is transduced to a pop() operation. All other input is transduced to a read() operation. 2. The sequence of stack operations from (1) is fed into the logical stack that resolves what is on top of the stack before each operation from (1) (i.e., STRUCT, LIST). After this stage, for every input character we know what is on top of the stack: either a STRUCT or LIST or ROOT, if there is no symbol on top of the stack. 3. We use the top-of-stack information from (2) for a second FST. This part can be considered a full pushdown or DVPA (because now, we also have stack context). State transitions are caused by the combination of the input character + the top-of-stack for that character. The output of this stage is the token stream: ({beginning-of, end-of}x{struct, list}, field name, value, etc. Authors: - Elias Stehle (https://github.com/elstehle) - Karthikeyan (https://github.com/karthikeyann) Approvers: - Robert Maynard (https://github.com/robertmaynard) - Tobias Ribizel (https://github.com/upsj) - Karthikeyan (https://github.com/karthikeyann) - Yunsong Wang (https://github.com/PointKernel) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11264 --- cpp/CMakeLists.txt | 1 + cpp/src/io/fst/lookup_tables.cuh | 24 +- cpp/src/io/json/nested_json.hpp | 115 +++++ cpp/src/io/json/nested_json_gpu.cu | 801 +++++++++++++++++++++++++++++ cpp/tests/CMakeLists.txt | 1 + cpp/tests/io/fst/common.hpp | 55 +- cpp/tests/io/fst/fst_test.cu | 2 +- cpp/tests/io/nested_json_test.cu | 233 +++++++++ 8 files changed, 1199 insertions(+), 33 deletions(-) create mode 100644 cpp/src/io/json/nested_json.hpp create mode 100644 cpp/src/io/json/nested_json_gpu.cu create mode 100644 cpp/tests/io/nested_json_test.cu diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 90dc898c552..1838459b16d 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -328,6 +328,7 @@ add_library( src/io/csv/writer_impl.cu src/io/functions.cpp src/io/json/json_gpu.cu + src/io/json/nested_json_gpu.cu src/io/json/reader_impl.cu src/io/json/experimental/read_json.cpp src/io/orc/aggregate_orc_metadata.cpp diff --git a/cpp/src/io/fst/lookup_tables.cuh b/cpp/src/io/fst/lookup_tables.cuh index c5033868925..04a8418dcc2 100644 --- a/cpp/src/io/fst/lookup_tables.cuh +++ b/cpp/src/io/fst/lookup_tables.cuh @@ -142,7 +142,8 @@ class SingleSymbolSmemLUT { constexpr CUDF_HOST_DEVICE int32_t operator()(SymbolT const symbol) const { // Look up the symbol group for given symbol - return temp_storage.sym_to_sgid[min(symbol, num_valid_entries - 1)]; + return temp_storage + .sym_to_sgid[min(static_cast(symbol), num_valid_entries - 1U)]; } }; @@ -170,19 +171,21 @@ class TransitionTable { ItemT transitions[MAX_NUM_STATES * MAX_NUM_SYMBOLS]; }; - template ()})>> - static void InitDeviceTransitionTable(hostdevice_vector& transition_table_init, - std::vector> const& translation_table, - rmm::cuda_stream_view stream) + template + static void InitDeviceTransitionTable( + hostdevice_vector& transition_table_init, + std::array, MAX_NUM_STATES> const& translation_table, + rmm::cuda_stream_view stream) { // translation_table[state][symbol] -> new state for (std::size_t state = 0; state < translation_table.size(); ++state) { for (std::size_t symbol = 0; symbol < translation_table[state].size(); ++symbol) { CUDF_EXPECTS( - translation_table[state][symbol] <= std::numeric_limits::max(), + static_cast(translation_table[state][symbol]) <= + std::numeric_limits::max(), "Target state index value exceeds value representable by the transition table's type"); transition_table_init.host_ptr()->transitions[symbol * MAX_NUM_STATES + state] = - translation_table[state][symbol]; + static_cast(translation_table[state][symbol]); } } @@ -319,7 +322,8 @@ class TransducerLookupTable { */ static void InitDeviceTranslationTable( hostdevice_vector& translation_table_init, - std::vector>> const& translation_table, + std::array, MAX_NUM_SYMBOLS>, MAX_NUM_STATES> const& + translation_table, rmm::cuda_stream_view stream) { std::vector out_symbols; @@ -476,8 +480,8 @@ class Dfa { */ template Dfa(SymbolGroupIdItT const& symbol_vec, - std::vector> const& tt_vec, - std::vector>> const& out_tt_vec, + std::array, NUM_STATES> const& tt_vec, + std::array, NUM_SYMBOLS>, NUM_STATES> const& out_tt_vec, cudaStream_t stream) { constexpr std::size_t single_item = 1; diff --git a/cpp/src/io/json/nested_json.hpp b/cpp/src/io/json/nested_json.hpp new file mode 100644 index 00000000000..3f7d73fb931 --- /dev/null +++ b/cpp/src/io/json/nested_json.hpp @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +namespace cudf::io::json { + +/// Type used to represent the atomic symbol type used within the finite-state machine +using SymbolT = char; + +/// Type used to represent the stack alphabet (i.e.: empty-stack, struct, list) +using StackSymbolT = char; + +/// Type used to index into the symbols within the JSON input +using SymbolOffsetT = uint32_t; + +/// Type large enough to support indexing up to max nesting level (must be signed) +using StackLevelT = int8_t; + +/// Type used to represent a symbol group id of the input alphabet in the pushdown automaton +using PdaInputSymbolGroupIdT = char; + +/// Type used to represent a symbol group id of the stack alphabet in the pushdown automaton +using PdaStackSymbolGroupIdT = char; + +/// Type used to represent a (input-symbol, stack-symbol)-tuple in stack-symbol-major order +using PdaSymbolGroupIdT = char; + +/// Type being emitted by the pushdown automaton transducer +using PdaTokenT = char; + +/** + * @brief Tokens emitted while parsing a JSON input + */ +enum token_t : PdaTokenT { + /// Beginning-of-struct token (on encounter of semantic '{') + StructBegin, + /// End-of-struct token (on encounter of semantic '}') + StructEnd, + /// Beginning-of-list token (on encounter of semantic '[') + ListBegin, + /// End-of-list token (on encounter of semantic ']') + ListEnd, + /// Beginning-of-field-name token (on encounter of first quote) + FieldNameBegin, + /// End-of-field-name token (on encounter of a field name's second quote) + FieldNameEnd, + /// Beginning-of-string-value token (on encounter of the string's first quote) + StringBegin, + /// End-of-string token (on encounter of a string's second quote) + StringEnd, + /// Beginning-of-value token (first character of literal or numeric) + ValueBegin, + /// Post-value token (first character after a literal or numeric string) + ValueEnd, + /// Beginning-of-error token (on first encounter of a parsing error) + ErrorBegin, + /// Total number of tokens + NUM_TOKENS +}; + +namespace detail { +/** + * @brief Identifies the stack context for each character from a JSON input. Specifically, we + * identify brackets and braces outside of quoted fields (e.g., field names, strings). + * At this stage, we do not perform bracket matching, i.e., we do not verify whether a closing + * bracket would actually pop a the corresponding opening brace. + * + * @param[in] d_json_in The string of input characters + * @param[out] d_top_of_stack Will be populated with what-is-on-top-of-the-stack for any given input + * character of \p d_json_in, where a '{' represents that the corresponding input character is + * within the context of a struct, a '[' represents that it is within the context of an array, and a + * '_' symbol that it is at the root of the JSON. + * @param[in] stream The cuda stream to dispatch GPU kernels to + */ +void get_stack_context(device_span d_json_in, + SymbolT* d_top_of_stack, + rmm::cuda_stream_view stream); + +/** + * @brief Parses the given JSON string and emits a sequence of tokens that demarcate relevant + * sections from the input. + * + * @param[in] d_json_in The JSON input + * @param[out] d_tokens Device memory to which the parsed tokens are written + * @param[out] d_tokens_indices Device memory to which the indices are written, where each index + * represents the offset within \p d_json_in that cause the input being written + * @param[out] d_num_written_tokens The total number of tokens that were parsed + * @param[in] stream The CUDA stream to which kernels are dispatched + */ +void get_token_stream(device_span d_json_in, + PdaTokenT* d_tokens, + SymbolOffsetT* d_tokens_indices, + SymbolOffsetT* d_num_written_tokens, + rmm::cuda_stream_view stream); +} // namespace detail + +} // namespace cudf::io::json diff --git a/cpp/src/io/json/nested_json_gpu.cu b/cpp/src/io/json/nested_json_gpu.cu new file mode 100644 index 00000000000..b8e05054e11 --- /dev/null +++ b/cpp/src/io/json/nested_json_gpu.cu @@ -0,0 +1,801 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "nested_json.hpp" + +#include +#include +#include + +#include +#include + +#include + +namespace cudf::io::json { + +// JSON to stack operator DFA (Deterministic Finite Automata) +namespace to_stack_op { + +// Type used to represent the target state in the transition table +using StateT = char; + +/** + * @brief Definition of the DFA's states + */ +enum class dfa_states : StateT { + // The active state while outside of a string. When encountering an opening bracket or curly + // brace, we push it onto the stack. When encountering a closing bracket or brace, we pop from the + // stack. + TT_OOS = 0U, + + // The active state while within a string (e.g., field name or a string value). We do not push or + // pop from the stack while in this state. + TT_STR, + + // The active state after encountering an escape symbol (e.g., '\'), while in the TT_STR state. + TT_ESC, + + // Total number of states + TT_NUM_STATES +}; + +// Aliases for readability of the transition table +constexpr auto TT_OOS = dfa_states::TT_OOS; +constexpr auto TT_STR = dfa_states::TT_STR; +constexpr auto TT_ESC = dfa_states::TT_ESC; + +/** + * @brief Definition of the symbol groups + */ +enum class dfa_symbol_group_id : uint8_t { + OPENING_BRACE, ///< Opening brace SG: { + OPENING_BRACKET, ///< Opening bracket SG: [ + CLOSING_BRACE, ///< Closing brace SG: } + CLOSING_BRACKET, ///< Closing bracket SG: ] + QUOTE_CHAR, ///< Quote character SG: " + ESCAPE_CHAR, ///< Escape character SG: '\' + OTHER_SYMBOLS, ///< SG implicitly matching all other characters + NUM_SYMBOL_GROUPS ///< Total number of symbol groups +}; + +constexpr auto TT_NUM_STATES = static_cast(dfa_states::TT_NUM_STATES); +constexpr auto NUM_SYMBOL_GROUPS = static_cast(dfa_symbol_group_id::NUM_SYMBOL_GROUPS); + +// The i-th string representing all the characters of a symbol group +std::array const symbol_groups{ + {{"{"}, {"["}, {"}"}, {"]"}, {"\""}, {"\\"}}}; + +// Transition table +std::array, TT_NUM_STATES> const transition_table{ + {/* IN_STATE { [ } ] " \ OTHER */ + /* TT_OOS */ {{TT_OOS, TT_OOS, TT_OOS, TT_OOS, TT_STR, TT_OOS, TT_OOS}}, + /* TT_STR */ {{TT_STR, TT_STR, TT_STR, TT_STR, TT_OOS, TT_ESC, TT_STR}}, + /* TT_ESC */ {{TT_STR, TT_STR, TT_STR, TT_STR, TT_STR, TT_STR, TT_STR}}}}; + +// Translation table (i.e., for each transition, what are the symbols that we output) +std::array, NUM_SYMBOL_GROUPS>, TT_NUM_STATES> const translation_table{ + {/* IN_STATE { [ } ] " \ OTHER */ + /* TT_OOS */ {{{'{'}, {'['}, {'}'}, {']'}, {'x'}, {'x'}, {'x'}}}, + /* TT_STR */ {{{'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}}}, + /* TT_ESC */ {{{'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}}}}}; + +// The DFA's starting state +constexpr auto start_state = static_cast(TT_OOS); +} // namespace to_stack_op + +// JSON tokenizer pushdown automaton +namespace tokenizer_pda { + +// Type used to represent the target state in the transition table +using StateT = char; + +/** + * @brief Symbol groups for the input alphabet for the pushdown automaton + */ +enum class symbol_group_id : PdaSymbolGroupIdT { + /// Opening brace + OPENING_BRACE, + /// Opening bracket + OPENING_BRACKET, + /// Closing brace + CLOSING_BRACE, + /// Closing bracket + CLOSING_BRACKET, + /// Quote + QUOTE, + /// Escape + ESCAPE, + /// Comma + COMMA, + /// Colon + COLON, + /// Whitespace + WHITE_SPACE, + /// Other (any input symbol not assigned to one of the above symbol groups) + OTHER, + /// Total number of symbol groups amongst which to differentiate + NUM_PDA_INPUT_SGS +}; + +/** + * @brief Symbols in the stack alphabet + */ +enum class stack_symbol_group_id : PdaStackSymbolGroupIdT { + /// Symbol representing that we're at the JSON root (nesting level 0) + STACK_ROOT, + + /// Symbol representing that we're currently within a list object + STACK_LIST, + + /// Symbol representing that we're currently within a struct object + STACK_STRUCT, + + /// Total number of symbols in the stack alphabet + NUM_STACK_SGS +}; +constexpr auto NUM_PDA_INPUT_SGS = + static_cast(symbol_group_id::NUM_PDA_INPUT_SGS); +constexpr auto NUM_STACK_SGS = + static_cast(stack_symbol_group_id::NUM_STACK_SGS); + +/// Total number of symbol groups to differentiate amongst (stack alphabet * input alphabet) +constexpr PdaSymbolGroupIdT NUM_PDA_SGIDS = NUM_PDA_INPUT_SGS * NUM_STACK_SGS; + +/// Mapping a input symbol to the symbol group id +static __constant__ PdaSymbolGroupIdT tos_sg_to_pda_sgid[] = { + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::WHITE_SPACE), + static_cast(symbol_group_id::WHITE_SPACE), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::WHITE_SPACE), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::WHITE_SPACE), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::QUOTE), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::COMMA), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::COLON), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OPENING_BRACKET), + static_cast(symbol_group_id::ESCAPE), + static_cast(symbol_group_id::CLOSING_BRACKET), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::OPENING_BRACE), + static_cast(symbol_group_id::OTHER), + static_cast(symbol_group_id::CLOSING_BRACE), + static_cast(symbol_group_id::OTHER)}; + +/** + * @brief Maps a (top-of-stack symbol, input symbol)-pair to a symbol group id of the deterministic + * visibly pushdown automaton (DVPA) + */ +struct PdaSymbolToSymbolGroupId { + template + __device__ __forceinline__ PdaSymbolGroupIdT + operator()(thrust::tuple symbol_pair) + { + // The symbol read from the input + auto symbol = thrust::get<0>(symbol_pair); + + // The stack symbol (i.e., what is on top of the stack at the time the input symbol was read) + // I.e., whether we're reading in something within a struct, a list, or the JSON root + auto stack_symbol = thrust::get<1>(symbol_pair); + + // The stack symbol offset: '_' is the root group (0), '[' is the list group (1), '{' is the + // struct group (2) + int32_t stack_idx = static_cast( + (stack_symbol == '_') ? stack_symbol_group_id::STACK_ROOT + : ((stack_symbol == '[') ? stack_symbol_group_id::STACK_LIST + : stack_symbol_group_id::STACK_STRUCT)); + + // The relative symbol group id of the current input symbol + constexpr auto pda_sgid_lookup_size = + static_cast(sizeof(tos_sg_to_pda_sgid) / sizeof(tos_sg_to_pda_sgid[0])); + PdaSymbolGroupIdT symbol_gid = + tos_sg_to_pda_sgid[min(static_cast(symbol), pda_sgid_lookup_size - 1)]; + return stack_idx * static_cast(symbol_group_id::NUM_PDA_INPUT_SGS) + + symbol_gid; + } +}; + +// The states defined by the pushdown automaton +enum class pda_state_t : StateT { + // Beginning of value + PD_BOV, + // Beginning of array + PD_BOA, + // Literal or number + PD_LON, + // String + PD_STR, + // After escape char when within string + PD_SCE, + // After having parsed a value + PD_PVL, + // Before the next field name + PD_BFN, + // Field name + PD_FLN, + // After escape char when within field name + PD_FNE, + // After a field name inside a struct + PD_PFN, + // Error state (trap state) + PD_ERR, + // Total number of PDA states + PD_NUM_STATES +}; + +// Aliases for readability of the transition table +constexpr auto PD_BOV = pda_state_t::PD_BOV; +constexpr auto PD_BOA = pda_state_t::PD_BOA; +constexpr auto PD_LON = pda_state_t::PD_LON; +constexpr auto PD_STR = pda_state_t::PD_STR; +constexpr auto PD_SCE = pda_state_t::PD_SCE; +constexpr auto PD_PVL = pda_state_t::PD_PVL; +constexpr auto PD_BFN = pda_state_t::PD_BFN; +constexpr auto PD_FLN = pda_state_t::PD_FLN; +constexpr auto PD_FNE = pda_state_t::PD_FNE; +constexpr auto PD_PFN = pda_state_t::PD_PFN; +constexpr auto PD_ERR = pda_state_t::PD_ERR; + +constexpr auto PD_NUM_STATES = static_cast(pda_state_t::PD_NUM_STATES); + +// The starting state of the pushdown automaton +constexpr auto start_state = static_cast(pda_state_t::PD_BOV); + +// Identity symbol to symbol group lookup table +std::vector> const pda_sgids{ + {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, + {15}, {16}, {17}, {18}, {19}, {20}, {21}, {22}, {23}, {24}, {25}, {26}, {27}, {28}, {29}}; + +/** + * @brief Getting the transition table + */ +auto get_transition_table() +{ + std::array, PD_NUM_STATES> pda_tt; + // { [ } ] " \ , : space other + pda_tt[static_cast(pda_state_t::PD_BOV)] = { + PD_BOA, PD_BOA, PD_ERR, PD_ERR, PD_STR, PD_ERR, PD_ERR, PD_ERR, PD_BOV, PD_LON, + PD_BOA, PD_BOA, PD_ERR, PD_ERR, PD_STR, PD_ERR, PD_ERR, PD_ERR, PD_BOV, PD_LON, + PD_BOA, PD_BOA, PD_ERR, PD_ERR, PD_STR, PD_ERR, PD_ERR, PD_ERR, PD_BOV, PD_LON}; + pda_tt[static_cast(pda_state_t::PD_BOA)] = { + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_BOA, PD_BOA, PD_ERR, PD_PVL, PD_STR, PD_ERR, PD_ERR, PD_ERR, PD_BOA, PD_LON, + PD_ERR, PD_ERR, PD_PVL, PD_ERR, PD_FLN, PD_ERR, PD_ERR, PD_ERR, PD_BOA, PD_ERR}; + pda_tt[static_cast(pda_state_t::PD_LON)] = { + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_PVL, PD_LON, + PD_ERR, PD_ERR, PD_ERR, PD_PVL, PD_ERR, PD_ERR, PD_BOV, PD_ERR, PD_PVL, PD_LON, + PD_ERR, PD_ERR, PD_PVL, PD_ERR, PD_ERR, PD_ERR, PD_BFN, PD_ERR, PD_PVL, PD_LON}; + pda_tt[static_cast(pda_state_t::PD_STR)] = { + PD_STR, PD_STR, PD_STR, PD_STR, PD_PVL, PD_SCE, PD_STR, PD_STR, PD_STR, PD_STR, + PD_STR, PD_STR, PD_STR, PD_STR, PD_PVL, PD_SCE, PD_STR, PD_STR, PD_STR, PD_STR, + PD_STR, PD_STR, PD_STR, PD_STR, PD_PVL, PD_SCE, PD_STR, PD_STR, PD_STR, PD_STR}; + pda_tt[static_cast(pda_state_t::PD_SCE)] = { + PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, + PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, + PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR, PD_STR}; + pda_tt[static_cast(pda_state_t::PD_PVL)] = { + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_PVL, PD_ERR, + PD_ERR, PD_ERR, PD_ERR, PD_PVL, PD_ERR, PD_ERR, PD_BOV, PD_ERR, PD_PVL, PD_ERR, + PD_ERR, PD_ERR, PD_PVL, PD_ERR, PD_ERR, PD_ERR, PD_BFN, PD_ERR, PD_PVL, PD_ERR}; + pda_tt[static_cast(pda_state_t::PD_BFN)] = { + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_FLN, PD_ERR, PD_ERR, PD_ERR, PD_BFN, PD_ERR}; + pda_tt[static_cast(pda_state_t::PD_FLN)] = { + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_FLN, PD_FLN, PD_FLN, PD_FLN, PD_PFN, PD_FNE, PD_FLN, PD_FLN, PD_FLN, PD_FLN}; + pda_tt[static_cast(pda_state_t::PD_FNE)] = { + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_FLN, PD_FLN, PD_FLN, PD_FLN, PD_FLN, PD_FLN, PD_FLN, PD_FLN, PD_FLN, PD_FLN}; + pda_tt[static_cast(pda_state_t::PD_PFN)] = { + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_BOV, PD_PFN, PD_ERR}; + pda_tt[static_cast(pda_state_t::PD_ERR)] = { + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, + PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR, PD_ERR}; + return pda_tt; +} + +/** + * @brief Getting the translation table + */ +auto get_translation_table() +{ + std::array, NUM_PDA_SGIDS>, PD_NUM_STATES> pda_tlt; + pda_tlt[static_cast(pda_state_t::PD_BOV)] = {{{token_t::StructBegin}, + {token_t::ListBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::StringBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {token_t::ValueBegin}, + {token_t::StructBegin}, + {token_t::ListBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::StringBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {token_t::ValueBegin}, + {token_t::StructBegin}, + {token_t::ListBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::StringBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {token_t::ValueBegin}}}; + pda_tlt[static_cast(pda_state_t::PD_BOA)] = {{{token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::StructBegin}, + {token_t::ListBegin}, + {token_t::ErrorBegin}, + {token_t::ListEnd}, + {token_t::StringBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {token_t::ValueBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::StructEnd}, + {token_t::ErrorBegin}, + {token_t::FieldNameBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {token_t::ErrorBegin}}}; + pda_tlt[static_cast(pda_state_t::PD_LON)] = {{{token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ValueEnd}, + {}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ValueEnd, token_t::ListEnd}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ValueEnd}, + {token_t::ErrorBegin}, + {token_t::ValueEnd}, + {}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ValueEnd, token_t::StructEnd}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ValueEnd}, + {token_t::ErrorBegin}, + {token_t::ValueEnd}, + {}}}; + pda_tlt[static_cast(pda_state_t::PD_STR)] = { + {{}, {}, {}, {}, {token_t::StringEnd}, {}, {}, {}, {}, {}, + {}, {}, {}, {}, {token_t::StringEnd}, {}, {}, {}, {}, {}, + {}, {}, {}, {}, {token_t::StringEnd}, {}, {}, {}, {}, {}}}; + pda_tlt[static_cast(pda_state_t::PD_SCE)] = {{{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, + {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, + {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}}; + pda_tlt[static_cast(pda_state_t::PD_PVL)] = {{{token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ListEnd}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {token_t::ErrorBegin}, + {}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::StructEnd}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {token_t::ErrorBegin}, + {}, + {token_t::ErrorBegin}}}; + pda_tlt[static_cast(pda_state_t::PD_BFN)] = {{{token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::FieldNameBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {token_t::ErrorBegin}}}; + pda_tlt[static_cast(pda_state_t::PD_FLN)] = {{{token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {}, + {}, + {}, + {token_t::FieldNameEnd}, + {}, + {}, + {}, + {}, + {}}}; + pda_tlt[static_cast(pda_state_t::PD_FNE)] = {{{token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}}}; + pda_tlt[static_cast(pda_state_t::PD_PFN)] = {{{token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {token_t::ErrorBegin}, + {}, + {}, + {token_t::ErrorBegin}}}; + pda_tlt[static_cast(pda_state_t::PD_ERR)] = {{{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, + {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, + {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}}; + return pda_tlt; +} + +} // namespace tokenizer_pda + +/** + * @brief Function object used to filter for brackets and braces that represent push and pop + * operations + */ +struct JSONToStackOp { + template + constexpr CUDF_HOST_DEVICE fst::stack_op_type operator()(StackSymbolT const& stack_symbol) const + { + return (stack_symbol == '{' || stack_symbol == '[') ? fst::stack_op_type::PUSH + : (stack_symbol == '}' || stack_symbol == ']') ? fst::stack_op_type::POP + : fst::stack_op_type::READ; + } +}; + +namespace detail { + +void get_stack_context(device_span json_in, + SymbolT* d_top_of_stack, + rmm::cuda_stream_view stream) +{ + constexpr std::size_t single_item = 1; + + // Symbol representing the JSON-root (i.e., we're at nesting level '0') + constexpr StackSymbolT root_symbol = '_'; + // This can be any stack symbol from the stack alphabet that does not push onto stack + constexpr StackSymbolT read_symbol = 'x'; + + // Number of stack operations in the input (i.e., number of '{', '}', '[', ']' outside of quotes) + hostdevice_vector num_stack_ops(single_item, stream); + + // Sequence of stack symbols and their position in the original input (sparse representation) + rmm::device_uvector stack_ops{json_in.size(), stream}; + rmm::device_uvector stack_op_indices{json_in.size(), stream}; + + // Prepare finite-state transducer that only selects '{', '}', '[', ']' outside of quotes + using ToStackOpFstT = + cudf::io::fst::detail::Dfa( + to_stack_op::dfa_symbol_group_id::NUM_SYMBOL_GROUPS), + static_cast(to_stack_op::dfa_states::TT_NUM_STATES)>; + ToStackOpFstT json_to_stack_ops_fst{to_stack_op::symbol_groups, + to_stack_op::transition_table, + to_stack_op::translation_table, + stream}; + + // "Search" for relevant occurrence of brackets and braces that indicate the beginning/end + // of structs/lists + json_to_stack_ops_fst.Transduce(json_in.begin(), + static_cast(json_in.size()), + stack_ops.data(), + stack_op_indices.data(), + num_stack_ops.device_ptr(), + to_stack_op::start_state, + stream); + + // stack operations with indices are converted to top of the stack for each character in the input + fst::sparse_stack_op_to_top_of_stack( + stack_ops.data(), + device_span{stack_op_indices.data(), stack_op_indices.size()}, + JSONToStackOp{}, + d_top_of_stack, + root_symbol, + read_symbol, + json_in.size(), + stream); +} + +// TODO: return pair of device_uvector instead of passing pre-allocated pointers. +void get_token_stream(device_span json_in, + PdaTokenT* d_tokens, + SymbolOffsetT* d_tokens_indices, + SymbolOffsetT* d_num_written_tokens, + rmm::cuda_stream_view stream) +{ + // Memory holding the top-of-stack stack context for the input + rmm::device_uvector stack_op_indices{json_in.size(), stream}; + + // Identify what is the stack context for each input character (is it: JSON-root, struct, or list) + get_stack_context(json_in, stack_op_indices.data(), stream); + + // Prepare for PDA transducer pass, merging input symbols with stack symbols + rmm::device_uvector pda_sgids{json_in.size(), stream}; + auto zip_in = thrust::make_zip_iterator(json_in.data(), stack_op_indices.data()); + thrust::transform(rmm::exec_policy(stream), + zip_in, + zip_in + json_in.size(), + pda_sgids.data(), + tokenizer_pda::PdaSymbolToSymbolGroupId{}); + + // PDA transducer alias + using ToTokenStreamFstT = + cudf::io::fst::detail::Dfa( + tokenizer_pda::pda_state_t::PD_NUM_STATES)>; + + // Instantiating PDA transducer + ToTokenStreamFstT json_to_tokens_fst{tokenizer_pda::pda_sgids, + tokenizer_pda::get_transition_table(), + tokenizer_pda::get_translation_table(), + stream}; + + // Perform a PDA-transducer pass + json_to_tokens_fst.Transduce(pda_sgids.begin(), + static_cast(json_in.size()), + d_tokens, + d_tokens_indices, + d_num_written_tokens, + tokenizer_pda::start_state, + stream); +} + +} // namespace detail + +} // namespace cudf::io::json diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 4a3eb1b9aef..be610d33b1b 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -222,6 +222,7 @@ ConfigureTest(FILE_IO_TEST io/file_io_test.cpp) ConfigureTest(ORC_TEST io/orc_test.cpp) ConfigureTest(PARQUET_TEST io/parquet_test.cpp) ConfigureTest(JSON_TEST io/json_test.cpp) +ConfigureTest(NESTED_JSON_TEST io/nested_json_test.cu) ConfigureTest(ARROW_IO_SOURCE_TEST io/arrow_io_source_test.cpp) ConfigureTest(MULTIBYTE_SPLIT_TEST io/text/multibyte_split_test.cpp) ConfigureTest(LOGICAL_STACK_TEST io/fst/logical_stack_test.cu) diff --git a/cpp/tests/io/fst/common.hpp b/cpp/tests/io/fst/common.hpp index bf19a9e8f6a..ce09c810e88 100644 --- a/cpp/tests/io/fst/common.hpp +++ b/cpp/tests/io/fst/common.hpp @@ -16,6 +16,7 @@ #pragma once +#include #include #include @@ -24,7 +25,7 @@ namespace cudf::test::io::json { // TEST FST SPECIFICATIONS //------------------------------------------------------------------------------ // FST to check for brackets and braces outside of pairs of quotes -enum DFA_STATES : char { +enum class dfa_states : char { // The state being active while being outside of a string. When encountering an opening bracket or // curly brace, we push it onto the stack. When encountering a closing bracket or brace, we pop it // from the stack. @@ -39,36 +40,46 @@ enum DFA_STATES : char { TT_NUM_STATES }; -// Definition of the symbol groups -enum PDA_SG_ID { - OBC = 0U, ///< Opening brace SG: { - OBT, ///< Opening bracket SG: [ - CBC, ///< Closing brace SG: } - CBT, ///< Closing bracket SG: ] - QTE, ///< Quote character SG: " - ESC, ///< Escape character SG: '\' - OTR, ///< SG implicitly matching all other characters +/** + * @brief Definition of the symbol groups + */ +enum class dfa_symbol_group_id : uint32_t { + OPENING_BRACE, ///< Opening brace SG: { + OPENING_BRACKET, ///< Opening bracket SG: [ + CLOSING_BRACE, ///< Closing brace SG: } + CLOSING_BRACKET, ///< Closing bracket SG: ] + QUOTE_CHAR, ///< Quote character SG: " + ESCAPE_CHAR, ///< Escape character SG: '\' + OTHER_SYMBOLS, ///< SG implicitly matching all other characters NUM_SYMBOL_GROUPS ///< Total number of symbol groups }; +// Aliases for readability of the transition table +constexpr auto TT_OOS = dfa_states::TT_OOS; +constexpr auto TT_STR = dfa_states::TT_STR; +constexpr auto TT_ESC = dfa_states::TT_ESC; + +constexpr auto TT_NUM_STATES = static_cast(dfa_states::TT_NUM_STATES); +constexpr auto NUM_SYMBOL_GROUPS = static_cast(dfa_symbol_group_id::NUM_SYMBOL_GROUPS); + // Transition table -const std::vector> pda_state_tt = { - /* IN_STATE { [ } ] " \ OTHER */ - /* TT_OOS */ {TT_OOS, TT_OOS, TT_OOS, TT_OOS, TT_STR, TT_OOS, TT_OOS}, - /* TT_STR */ {TT_STR, TT_STR, TT_STR, TT_STR, TT_OOS, TT_ESC, TT_STR}, - /* TT_ESC */ {TT_STR, TT_STR, TT_STR, TT_STR, TT_STR, TT_STR, TT_STR}}; +std::array, TT_NUM_STATES> const pda_state_tt{ + {/* IN_STATE { [ } ] " \ OTHER */ + /* TT_OOS */ {{TT_OOS, TT_OOS, TT_OOS, TT_OOS, TT_STR, TT_OOS, TT_OOS}}, + /* TT_STR */ {{TT_STR, TT_STR, TT_STR, TT_STR, TT_OOS, TT_ESC, TT_STR}}, + /* TT_ESC */ {{TT_STR, TT_STR, TT_STR, TT_STR, TT_STR, TT_STR, TT_STR}}}}; // Translation table (i.e., for each transition, what are the symbols that we output) -const std::vector>> pda_out_tt = { - /* IN_STATE { [ } ] " \ OTHER */ - /* TT_OOS */ {{'{'}, {'['}, {'}'}, {']'}, {'x'}, {'x'}, {'x'}}, - /* TT_STR */ {{'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}}, - /* TT_ESC */ {{'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}}}; +std::array, NUM_SYMBOL_GROUPS>, TT_NUM_STATES> const pda_out_tt{ + {/* IN_STATE { [ } ] " \ OTHER */ + /* TT_OOS */ {{{'{'}, {'['}, {'}'}, {']'}, {'x'}, {'x'}, {'x'}}}, + /* TT_STR */ {{{'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}}}, + /* TT_ESC */ {{{'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}, {'x'}}}}}; // The i-th string representing all the characters of a symbol group -const std::vector pda_sgs = {"{", "[", "}", "]", "\"", "\\"}; +std::array const pda_sgs{"{", "[", "}", "]", "\"", "\\"}; // The DFA's starting state -constexpr DFA_STATES start_state = TT_OOS; +constexpr char start_state = static_cast(dfa_states::TT_OOS); } // namespace cudf::test::io::json diff --git a/cpp/tests/io/fst/fst_test.cu b/cpp/tests/io/fst/fst_test.cu index c472b6851b0..64ecf1f7329 100644 --- a/cpp/tests/io/fst/fst_test.cu +++ b/cpp/tests/io/fst/fst_test.cu @@ -109,7 +109,7 @@ static std::pair fst_baseline(InputItT begin, out_index_tape = std::fill_n(out_index_tape, out_size, in_offset); // Transition the state of the finite-state machine - state = transition_table[state][symbol_group]; + state = static_cast(transition_table[state][symbol_group]); // Continue with next symbol from input tape in_offset++; diff --git a/cpp/tests/io/nested_json_test.cu b/cpp/tests/io/nested_json_test.cu new file mode 100644 index 00000000000..0b7e2bb82f8 --- /dev/null +++ b/cpp/tests/io/nested_json_test.cu @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include + +#include +#include + +namespace cuio_json = cudf::io::json; + +// Base test fixture for tests +struct JsonTest : public cudf::test::BaseFixture { +}; + +TEST_F(JsonTest, StackContext) +{ + // Type used to represent the atomic symbol type used within the finite-state machine + using SymbolT = char; + using StackSymbolT = char; + + // Prepare cuda stream for data transfers & kernels + rmm::cuda_stream stream{}; + rmm::cuda_stream_view stream_view(stream); + + // Test input + std::string input = R"( [{)" + R"("category": "reference",)" + R"("index:": [4,12,42],)" + R"("author": "Nigel Rees",)" + R"("title": "[Sayings of the Century]",)" + R"("price": 8.95)" + R"(}, )" + R"({)" + R"("category": "reference",)" + R"("index": [4,{},null,{"a":[{ }, {}] } ],)" + R"("author": "Nigel Rees",)" + R"("title": "{}\\\"[], <=semantic-symbols-string\\\\",)" + R"("price": 8.95)" + R"(}] )"; + + // Prepare input & output buffers + rmm::device_uvector d_input(input.size(), stream_view); + hostdevice_vector stack_context(input.size(), stream_view); + + ASSERT_CUDA_SUCCEEDED(cudaMemcpyAsync(d_input.data(), + input.data(), + input.size() * sizeof(SymbolT), + cudaMemcpyHostToDevice, + stream.value())); + + // Run algorithm + cuio_json::detail::get_stack_context(d_input, stack_context.device_ptr(), stream_view); + + // Copy back the results + stack_context.device_to_host(stream_view); + + // Make sure we copied back the stack context + stream_view.synchronize(); + + std::vector golden_stack_context{ + '_', '_', '_', '[', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '[', '[', '[', '[', '[', '[', '[', '[', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '[', '[', '[', '[', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '[', '[', '[', '{', '[', '[', '[', '[', '[', '[', '[', '{', + '{', '{', '{', '{', '[', '{', '{', '[', '[', '[', '{', '[', '{', '{', '[', '[', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '[', '_'}; + + ASSERT_EQ(golden_stack_context.size(), stack_context.size()); + CUDF_TEST_EXPECT_VECTOR_EQUAL(golden_stack_context, stack_context, stack_context.size()); +} + +TEST_F(JsonTest, StackContextUtf8) +{ + // Type used to represent the atomic symbol type used within the finite-state machine + using SymbolT = char; + using StackSymbolT = char; + + // Prepare cuda stream for data transfers & kernels + rmm::cuda_stream stream{}; + rmm::cuda_stream_view stream_view(stream); + + // Test input + std::string input = R"([{"a":{"year":1882,"author": "Bharathi"}, {"a":"filip ʒakotɛ"}}])"; + + // Prepare input & output buffers + rmm::device_uvector d_input(input.size(), stream_view); + hostdevice_vector stack_context(input.size(), stream_view); + + ASSERT_CUDA_SUCCEEDED(cudaMemcpyAsync(d_input.data(), + input.data(), + input.size() * sizeof(SymbolT), + cudaMemcpyHostToDevice, + stream.value())); + + // Run algorithm + cuio_json::detail::get_stack_context(d_input, stack_context.device_ptr(), stream_view); + + // Copy back the results + stack_context.device_to_host(stream_view); + + // Make sure we copied back the stack context + stream_view.synchronize(); + + std::vector golden_stack_context{ + '_', '[', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '['}; + + ASSERT_EQ(golden_stack_context.size(), stack_context.size()); + CUDF_TEST_EXPECT_VECTOR_EQUAL(golden_stack_context, stack_context, stack_context.size()); +} + +TEST_F(JsonTest, TokenStream) +{ + using cuio_json::PdaTokenT; + using cuio_json::SymbolOffsetT; + using cuio_json::SymbolT; + + constexpr std::size_t single_item = 1; + + // Prepare cuda stream for data transfers & kernels + rmm::cuda_stream stream{}; + rmm::cuda_stream_view stream_view(stream); + + // Test input + std::string input = R"( [{)" + R"("category": "reference",)" + R"("index:": [4,12,42],)" + R"("author": "Nigel Rees",)" + R"("title": "[Sayings of the Century]",)" + R"("price": 8.95)" + R"(}, )" + R"({)" + R"("category": "reference",)" + R"("index": [4,{},null,{"a":[{ }, {}] } ],)" + R"("author": "Nigel Rees",)" + R"("title": "{}[], <=semantic-symbols-string",)" + R"("price": 8.95)" + R"(}] )"; + + // Prepare input & output buffers + rmm::device_uvector d_input(input.size(), stream_view); + + ASSERT_CUDA_SUCCEEDED(cudaMemcpyAsync(d_input.data(), + input.data(), + input.size() * sizeof(SymbolT), + cudaMemcpyHostToDevice, + stream.value())); + + hostdevice_vector tokens_gpu{input.size(), stream_view}; + hostdevice_vector token_indices_gpu{input.size(), stream_view}; + hostdevice_vector num_tokens_out{single_item, stream_view}; + + // Parse the JSON and get the token stream + cuio_json::detail::get_token_stream(d_input, + tokens_gpu.device_ptr(), + token_indices_gpu.device_ptr(), + num_tokens_out.device_ptr(), + stream_view); + + // Copy back the number of tokens that were written + num_tokens_out.device_to_host(stream_view); + tokens_gpu.device_to_host(stream_view); + token_indices_gpu.device_to_host(stream_view); + + // Make sure we copied back all relevant data + stream_view.synchronize(); + + // Golden token stream sample + using token_t = cuio_json::token_t; + std::vector> golden_token_stream = { + {2, token_t::ListBegin}, {3, token_t::StructBegin}, {4, token_t::FieldNameBegin}, + {13, token_t::FieldNameEnd}, {16, token_t::StringBegin}, {26, token_t::StringEnd}, + {28, token_t::FieldNameBegin}, {35, token_t::FieldNameEnd}, {38, token_t::ListBegin}, + {39, token_t::ValueBegin}, {40, token_t::ValueEnd}, {41, token_t::ValueBegin}, + {43, token_t::ValueEnd}, {44, token_t::ValueBegin}, {46, token_t::ValueEnd}, + {46, token_t::ListEnd}, {48, token_t::FieldNameBegin}, {55, token_t::FieldNameEnd}, + {58, token_t::StringBegin}, {69, token_t::StringEnd}, {71, token_t::FieldNameBegin}, + {77, token_t::FieldNameEnd}, {80, token_t::StringBegin}, {105, token_t::StringEnd}, + {107, token_t::FieldNameBegin}, {113, token_t::FieldNameEnd}, {116, token_t::ValueBegin}, + {120, token_t::ValueEnd}, {120, token_t::StructEnd}, {124, token_t::StructBegin}, + {125, token_t::FieldNameBegin}, {134, token_t::FieldNameEnd}, {137, token_t::StringBegin}, + {147, token_t::StringEnd}, {149, token_t::FieldNameBegin}, {155, token_t::FieldNameEnd}, + {158, token_t::ListBegin}, {159, token_t::ValueBegin}, {160, token_t::ValueEnd}, + {161, token_t::StructBegin}, {162, token_t::StructEnd}, {164, token_t::ValueBegin}, + {168, token_t::ValueEnd}, {169, token_t::StructBegin}, {170, token_t::FieldNameBegin}, + {172, token_t::FieldNameEnd}, {174, token_t::ListBegin}, {175, token_t::StructBegin}, + {177, token_t::StructEnd}, {180, token_t::StructBegin}, {181, token_t::StructEnd}, + {182, token_t::ListEnd}, {184, token_t::StructEnd}, {186, token_t::ListEnd}, + {188, token_t::FieldNameBegin}, {195, token_t::FieldNameEnd}, {198, token_t::StringBegin}, + {209, token_t::StringEnd}, {211, token_t::FieldNameBegin}, {217, token_t::FieldNameEnd}, + {220, token_t::StringBegin}, {252, token_t::StringEnd}, {254, token_t::FieldNameBegin}, + {260, token_t::FieldNameEnd}, {263, token_t::ValueBegin}, {267, token_t::ValueEnd}, + {267, token_t::StructEnd}, {268, token_t::ListEnd}}; + + // Verify the number of tokens matches + ASSERT_EQ(golden_token_stream.size(), num_tokens_out[0]); + + for (std::size_t i = 0; i < num_tokens_out[0]; i++) { + // Ensure the index the tokens are pointing to do match + EXPECT_EQ(golden_token_stream[i].first, token_indices_gpu[i]) << "Mismatch at #" << i; + + // Ensure the token category is correct + EXPECT_EQ(golden_token_stream[i].second, tokens_gpu[i]) << "Mismatch at #" << i; + } +} From 6b20f2a6f338685ca65572b4ac5b15046fdf1f49 Mon Sep 17 00:00:00 2001 From: Nghia Truong Date: Mon, 8 Aug 2022 06:18:54 -0700 Subject: [PATCH 06/58] Add groupby `nunique` aggregation benchmark (#11472) This adds a simple benchmark for groupby `nunique` aggregation. Authors: - Nghia Truong (https://github.com/ttnghia) Approvers: - Bradley Dice (https://github.com/bdice) - Tobias Ribizel (https://github.com/upsj) URL: https://github.com/rapidsai/cudf/pull/11472 --- cpp/benchmarks/CMakeLists.txt | 3 +- cpp/benchmarks/groupby/group_nunique.cpp | 85 ++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 cpp/benchmarks/groupby/group_nunique.cpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 43c5211e3e5..3c917e54c63 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -200,7 +200,8 @@ ConfigureBench( ) ConfigureNVBench( - GROUPBY_NVBENCH groupby/group_max.cpp groupby/group_rank.cpp groupby/group_struct_keys.cpp + GROUPBY_NVBENCH groupby/group_max.cpp groupby/group_nunique.cpp groupby/group_rank.cpp + groupby/group_struct_keys.cpp ) # ################################################################################################## diff --git a/cpp/benchmarks/groupby/group_nunique.cpp b/cpp/benchmarks/groupby/group_nunique.cpp new file mode 100644 index 00000000000..8a704e4d1d2 --- /dev/null +++ b/cpp/benchmarks/groupby/group_nunique.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include + +namespace { + +template +auto make_aggregation_request_vector(cudf::column_view const& values, Args&&... args) +{ + std::vector> aggregations; + (aggregations.emplace_back(std::forward(args)), ...); + + std::vector requests; + requests.emplace_back(cudf::groupby::aggregation_request{values, std::move(aggregations)}); + + return requests; +} + +} // namespace + +template +void bench_groupby_nunique(nvbench::state& state, nvbench::type_list) +{ + cudf::rmm_pool_raii pool_raii; + const auto size = static_cast(state.get_int64("num_rows")); + + auto const keys_table = [&] { + data_profile profile; + profile.set_null_frequency(std::nullopt); + profile.set_cardinality(0); + profile.set_distribution_params( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); + return create_random_table({cudf::type_to_id()}, row_count{size}, profile); + }(); + + auto const vals_table = [&] { + data_profile profile; + if (const auto null_freq = state.get_float64("null_frequency"); null_freq > 0) { + profile.set_null_frequency({null_freq}); + } else { + profile.set_null_frequency(std::nullopt); + } + profile.set_cardinality(0); + profile.set_distribution_params(cudf::type_to_id(), + distribution_id::UNIFORM, + static_cast(0), + static_cast(1000)); + return create_random_table({cudf::type_to_id()}, row_count{size}, profile); + }(); + + auto const& keys = keys_table->get_column(0); + auto const& vals = vals_table->get_column(0); + + auto gb_obj = cudf::groupby::groupby(cudf::table_view({keys, keys, keys})); + auto const requests = make_aggregation_request_vector( + vals, cudf::make_nunique_aggregation()); + + state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::default_stream_value.value())); + state.exec(nvbench::exec_tag::sync, + [&](nvbench::launch& launch) { auto const result = gb_obj.aggregate(requests); }); +} + +NVBENCH_BENCH_TYPES(bench_groupby_nunique, NVBENCH_TYPE_AXES(nvbench::type_list)) + .set_name("groupby_nunique") + .add_int64_power_of_two_axis("num_rows", {12, 16, 20, 24}) + .add_float64_axis("null_frequency", {0, 0.5}); From 099e83cb3440c3a7273fb8adc06e339065ee32d2 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Mon, 8 Aug 2022 13:49:49 -0500 Subject: [PATCH 07/58] Unpin `dask` and `distributed` for development (#11492) This PR unpins `dask` & `distributed` for `22.10` development. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Charles Blackmon-Luca (https://github.com/charlesbluca) - Ray Douglass (https://github.com/raydouglass) URL: https://github.com/rapidsai/cudf/pull/11492 --- ci/benchmark/build.sh | 6 +++--- ci/gpu/build.sh | 6 +++--- conda/environments/cudf_dev_cuda11.5.yml | 4 ++-- conda/recipes/custreamz/meta.yaml | 4 ++-- conda/recipes/dask-cudf/meta.yaml | 8 ++++---- python/dask_cudf/setup.py | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ci/benchmark/build.sh b/ci/benchmark/build.sh index 5593633640a..e9ea4630133 100755 --- a/ci/benchmark/build.sh +++ b/ci/benchmark/build.sh @@ -37,7 +37,7 @@ export GBENCH_BENCHMARKS_DIR="$WORKSPACE/cpp/build/gbenchmarks/" export LIBCUDF_KERNEL_CACHE_PATH="$HOME/.jitify-cache" # Dask & Distributed option to install main(nightly) or `conda-forge` packages. -export INSTALL_DASK_MAIN=0 +export INSTALL_DASK_MAIN=1 function remove_libcudf_kernel_cache_dir { EXITCODE=$? @@ -82,8 +82,8 @@ if [[ "${INSTALL_DASK_MAIN}" == 1 ]]; then gpuci_logger "gpuci_mamba_retry update dask" gpuci_mamba_retry update dask else - gpuci_logger "gpuci_mamba_retry install conda-forge::dask==2022.7.1 conda-forge::distributed==2022.7.1 conda-forge::dask-core==2022.7.1 --force-reinstall" - gpuci_mamba_retry install conda-forge::dask==2022.7.1 conda-forge::distributed==2022.7.1 conda-forge::dask-core==2022.7.1 --force-reinstall + gpuci_logger "gpuci_mamba_retry install conda-forge::dask>=2022.7.1 conda-forge::distributed>=2022.7.1 conda-forge::dask-core>=2022.7.1 --force-reinstall" + gpuci_mamba_retry install conda-forge::dask>=2022.7.1 conda-forge::distributed>=2022.7.1 conda-forge::dask-core>=2022.7.1 --force-reinstall fi # Install the master version of streamz diff --git a/ci/gpu/build.sh b/ci/gpu/build.sh index 8f215d1bb54..08a3b70fe42 100755 --- a/ci/gpu/build.sh +++ b/ci/gpu/build.sh @@ -32,7 +32,7 @@ export MINOR_VERSION=`echo $GIT_DESCRIBE_TAG | grep -o -E '([0-9]+\.[0-9]+)'` unset GIT_DESCRIBE_TAG # Dask & Distributed option to install main(nightly) or `conda-forge` packages. -export INSTALL_DASK_MAIN=0 +export INSTALL_DASK_MAIN=1 # ucx-py version export UCX_PY_VERSION='0.28.*' @@ -92,8 +92,8 @@ function install_dask { gpuci_mamba_retry update dask conda list else - gpuci_logger "gpuci_mamba_retry install conda-forge::dask==2022.7.1 conda-forge::distributed==2022.7.1 conda-forge::dask-core==2022.7.1 --force-reinstall" - gpuci_mamba_retry install conda-forge::dask==2022.7.1 conda-forge::distributed==2022.7.1 conda-forge::dask-core==2022.7.1 --force-reinstall + gpuci_logger "gpuci_mamba_retry install conda-forge::dask>=2022.7.1 conda-forge::distributed>=2022.7.1 conda-forge::dask-core>=2022.7.1 --force-reinstall" + gpuci_mamba_retry install conda-forge::dask>=2022.7.1 conda-forge::distributed>=2022.7.1 conda-forge::dask-core>=2022.7.1 --force-reinstall fi # Install the main version of streamz gpuci_logger "Install the main version of streamz" diff --git a/conda/environments/cudf_dev_cuda11.5.yml b/conda/environments/cudf_dev_cuda11.5.yml index 1e323182ffd..940d7452183 100644 --- a/conda/environments/cudf_dev_cuda11.5.yml +++ b/conda/environments/cudf_dev_cuda11.5.yml @@ -48,8 +48,8 @@ dependencies: - pydocstyle=6.1.1 - typing_extensions - pre-commit - - dask==2022.7.1 - - distributed==2022.7.1 + - dask>=2022.7.1 + - distributed>=2022.7.1 - streamz - arrow-cpp=8 - dlpack>=0.5,<0.6.0a0 diff --git a/conda/recipes/custreamz/meta.yaml b/conda/recipes/custreamz/meta.yaml index 118f084b436..36fbcb5197d 100644 --- a/conda/recipes/custreamz/meta.yaml +++ b/conda/recipes/custreamz/meta.yaml @@ -29,8 +29,8 @@ requirements: - python - streamz - cudf ={{ version }} - - dask==2022.7.1 - - distributed==2022.7.1 + - dask>=2022.7.1 + - distributed>=2022.7.1 - python-confluent-kafka >=1.7.0,<1.8.0a0 - cudf_kafka ={{ version }} diff --git a/conda/recipes/dask-cudf/meta.yaml b/conda/recipes/dask-cudf/meta.yaml index c9a179301b0..98b69ba9e5d 100644 --- a/conda/recipes/dask-cudf/meta.yaml +++ b/conda/recipes/dask-cudf/meta.yaml @@ -24,14 +24,14 @@ requirements: host: - python - cudf ={{ version }} - - dask==2022.7.1 - - distributed==2022.7.1 + - dask>=2022.7.1 + - distributed>=2022.7.1 - cudatoolkit ={{ cuda_version }} run: - python - cudf ={{ version }} - - dask==2022.7.1 - - distributed==2022.7.1 + - dask>=2022.7.1 + - distributed>=2022.7.1 - {{ pin_compatible('cudatoolkit', max_pin='x', min_pin='x') }} test: # [linux64] diff --git a/python/dask_cudf/setup.py b/python/dask_cudf/setup.py index 7d8a6d7c3a3..f86cee2454b 100644 --- a/python/dask_cudf/setup.py +++ b/python/dask_cudf/setup.py @@ -10,8 +10,8 @@ install_requires = [ "cudf", - "dask==2022.7.1", - "distributed==2022.7.1", + "dask>=2022.7.1", + "distributed>=2022.7.1", "fsspec>=0.6.0", "numpy", "pandas>=1.0,<1.5.0dev0", From 36b5b4619902a6756eeedfec034e4ef2bc3e567a Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Mon, 8 Aug 2022 14:48:58 -0700 Subject: [PATCH 08/58] Disable Arrow S3 support by default. (#11470) This PR is a breaking change that disables Arrow S3 support by default. Enabling this feature by default has caused build issues for many downstream consumers, all of whom (to my knowledge) manually disable support for this feature. Most commonly, that build error appears as `fatal error: aws/core/Aws.h: No such file or directory`. In my understanding, several downstream consumers of cudf no longer rely on Arrow S3 support from this library and instead get S3 access via fsspec. I am not aware of any users of libcudf who rely on this being enabled by default (or enabled at all). See related issues and discussions: #8617, #11333, #8867, https://github.com/rapidsai/cudf/pull/10644#discussion_r853270226, https://github.com/NVIDIA/spark-rapids/pull/2827. Build errors caused by this default behavior have also been reported internally. cc: @rjzamora @beckernick @jdye64 @randerzander @robertmaynard @jlowe @quasiben if you have comments following our previous discussion. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Nghia Truong (https://github.com/ttnghia) - GALI PREM SAGAR (https://github.com/galipremsagar) - Vyas Ramasubramani (https://github.com/vyasr) - AJ Schmidt (https://github.com/ajschmidt8) URL: https://github.com/rapidsai/cudf/pull/11470 --- conda/recipes/libcudf/build.sh | 2 +- cpp/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conda/recipes/libcudf/build.sh b/conda/recipes/libcudf/build.sh index c7b5d1bd7fd..7ac9e83f31c 100644 --- a/conda/recipes/libcudf/build.sh +++ b/conda/recipes/libcudf/build.sh @@ -2,4 +2,4 @@ # Copyright (c) 2018-2022, NVIDIA CORPORATION. export cudf_ROOT="$(realpath ./cpp/build)" -./build.sh -n -v libcudf libcudf_kafka benchmarks tests --build_metrics --incl_cache_stats --cmake-args=\"-DCMAKE_INSTALL_LIBDIR=lib\" +./build.sh -n -v libcudf libcudf_kafka benchmarks tests --build_metrics --incl_cache_stats --cmake-args=\"-DCMAKE_INSTALL_LIBDIR=lib -DCUDF_ENABLE_ARROW_S3=ON\" diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 1838459b16d..44aaac54adb 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -53,7 +53,7 @@ option(CUDF_USE_ARROW_STATIC "Build and statically link Arrow libraries" OFF) option(CUDF_ENABLE_ARROW_ORC "Build the Arrow ORC adapter" OFF) option(CUDF_ENABLE_ARROW_PYTHON "Find (or build) Arrow with Python support" OFF) option(CUDF_ENABLE_ARROW_PARQUET "Find (or build) Arrow with Parquet support" OFF) -option(CUDF_ENABLE_ARROW_S3 "Build/Enable AWS S3 Arrow filesystem support" ON) +option(CUDF_ENABLE_ARROW_S3 "Build/Enable AWS S3 Arrow filesystem support" OFF) option( CUDF_USE_PER_THREAD_DEFAULT_STREAM "Build cuDF with per-thread default stream, including passing the per-thread default From 0e29353b0aa1a2eaca8bb82c7cc50d7669f90d7f Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Mon, 8 Aug 2022 16:51:21 -0500 Subject: [PATCH 09/58] Fix a misalignment in `cudf.get_dummies` docstring (#11443) This PR fixes a minor misalignment in `get_dummies` docstring example. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Michael Wang (https://github.com/isVoid) URL: https://github.com/rapidsai/cudf/pull/11443 --- python/cudf/cudf/core/reshape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 3b462d7c6ac..ae46305d401 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -679,7 +679,7 @@ def get_dummies( 4 4 dtype: int64 >>> cudf.get_dummies(series, dummy_na=True) - null 1 2 4 + null 1 2 4 0 0 1 0 0 1 0 0 1 0 2 1 0 0 0 From 6221539b1dfde5f3f70743fbf8e0ba21aa05153e Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Mon, 8 Aug 2022 15:14:43 -0700 Subject: [PATCH 10/58] Remove unused is_struct trait. (#11450) This PR removes the unused `is_struct` trait. Users should instead check the column `data_type` id, like `col->type().id() == cudf::type_id::STRUCT`. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Karthikeyan (https://github.com/karthikeyann) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/11450 --- cpp/include/cudf/utilities/traits.hpp | 31 --------------------------- 1 file changed, 31 deletions(-) diff --git a/cpp/include/cudf/utilities/traits.hpp b/cpp/include/cudf/utilities/traits.hpp index 573d0c81380..c1065c2446f 100644 --- a/cpp/include/cudf/utilities/traits.hpp +++ b/cpp/include/cudf/utilities/traits.hpp @@ -736,37 +736,6 @@ constexpr inline bool is_nested(data_type type) return cudf::type_dispatcher(type, is_nested_impl{}); } -/** - * @brief Indicates whether `T` is a struct type. - * - * @param T The type to verify - * @return A boolean indicating if T is a struct type - */ -template -constexpr inline bool is_struct() -{ - return std::is_same_v; -} - -struct is_struct_impl { - template - constexpr bool operator()() - { - return is_struct(); - } -}; - -/** - * @brief Indicates whether `type` is a struct type. - * - * @param type The `data_type` to verify - * @return A boolean indicating if `type` is a struct type - */ -constexpr inline bool is_struct(data_type type) -{ - return cudf::type_dispatcher(type, is_struct_impl{}); -} - template struct is_bit_castable_to_impl { template ()>* = nullptr> From fea0bda247123ce3d96beba87e6ccb91576e86cc Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Tue, 9 Aug 2022 12:17:46 -0500 Subject: [PATCH 11/58] Move SparkMurmurHash3_32 functor. (#11489) This PR moves the `SparkMurmurHash3_32` functor from `hash_functions.cuh` to `spark_murmur_hash.cu`, the only place where it is used. **This is a pure move**, with one small exception to avoid compiler warnings about unused members of the hash functor template instantiations for nested types. I refactored the class template to disallow nested types for the hash functor and removed those specializations using `CUDF_UNREACHABLE`, rather than allowing type dispatching to create template instantiations that have no defined use. (Nested types are being handled by the custom device row hasher in `spark_murmur_hash.cu`, and require some state information that cannot be easily carried in the functor itself.) I am planning to do further refactoring later, but wanted to separate this "pure move" as much as possible. Part of #10081. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Nghia Truong (https://github.com/ttnghia) - Ryan Lee (https://github.com/rwlee) URL: https://github.com/rapidsai/cudf/pull/11489 --- .../cudf/detail/utilities/hash_functions.cuh | 217 ------------------ cpp/src/hash/spark_murmur_hash.cu | 212 ++++++++++++++++- 2 files changed, 208 insertions(+), 221 deletions(-) diff --git a/cpp/include/cudf/detail/utilities/hash_functions.cuh b/cpp/include/cudf/detail/utilities/hash_functions.cuh index 778f6e98801..743ae491def 100644 --- a/cpp/include/cudf/detail/utilities/hash_functions.cuh +++ b/cpp/include/cudf/detail/utilities/hash_functions.cuh @@ -346,223 +346,6 @@ hash_value_type __device__ inline MurmurHash3_32::operator()( CUDF_UNREACHABLE("Direct hashing of struct_view is not supported"); } -template -struct SparkMurmurHash3_32 { - using result_type = hash_value_type; - - constexpr SparkMurmurHash3_32() = default; - constexpr SparkMurmurHash3_32(uint32_t seed) : m_seed(seed) {} - - [[nodiscard]] __device__ inline uint32_t fmix32(uint32_t h) const - { - h ^= h >> 16; - h *= 0x85ebca6b; - h ^= h >> 13; - h *= 0xc2b2ae35; - h ^= h >> 16; - return h; - } - - [[nodiscard]] __device__ inline uint32_t getblock32(std::byte const* data, - cudf::size_type offset) const - { - // Read a 4-byte value from the data pointer as individual bytes for safe - // unaligned access (very likely for string types). - auto block = reinterpret_cast(data + offset); - return block[0] | (block[1] << 8) | (block[2] << 16) | (block[3] << 24); - } - - [[nodiscard]] result_type __device__ inline operator()(Key const& key) const - { - return compute(key); - } - - template - result_type __device__ inline compute(T const& key) const - { - return compute_bytes(reinterpret_cast(&key), sizeof(T)); - } - - result_type __device__ inline compute_remaining_bytes(std::byte const* data, - cudf::size_type len, - cudf::size_type tail_offset, - result_type h) const - { - // Process remaining bytes that do not fill a four-byte chunk using Spark's approach - // (does not conform to normal MurmurHash3). - for (auto i = tail_offset; i < len; i++) { - // We require a two-step cast to get the k1 value from the byte. First, - // we must cast to a signed int8_t. Then, the sign bit is preserved when - // casting to uint32_t under 2's complement. Java preserves the sign when - // casting byte-to-int, but C++ does not. - uint32_t k1 = static_cast(std::to_integer(data[i])); - k1 *= c1; - k1 = cudf::detail::rotate_bits_left(k1, rot_c1); - k1 *= c2; - h ^= k1; - h = cudf::detail::rotate_bits_left(h, rot_c2); - h = h * 5 + c3; - } - return h; - } - - result_type __device__ compute_bytes(std::byte const* data, cudf::size_type const len) const - { - constexpr cudf::size_type BLOCK_SIZE = 4; - cudf::size_type const nblocks = len / BLOCK_SIZE; - cudf::size_type const tail_offset = nblocks * BLOCK_SIZE; - result_type h = m_seed; - - // Process all four-byte chunks. - for (cudf::size_type i = 0; i < nblocks; i++) { - uint32_t k1 = getblock32(data, i * BLOCK_SIZE); - k1 *= c1; - k1 = cudf::detail::rotate_bits_left(k1, rot_c1); - k1 *= c2; - h ^= k1; - h = cudf::detail::rotate_bits_left(h, rot_c2); - h = h * 5 + c3; - } - - h = compute_remaining_bytes(data, len, tail_offset, h); - - // Finalize hash. - h ^= len; - h = fmix32(h); - return h; - } - - private: - uint32_t m_seed{cudf::DEFAULT_HASH_SEED}; - static constexpr uint32_t c1 = 0xcc9e2d51; - static constexpr uint32_t c2 = 0x1b873593; - static constexpr uint32_t c3 = 0xe6546b64; - static constexpr uint32_t rot_c1 = 15; - static constexpr uint32_t rot_c2 = 13; -}; - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()(bool const& key) const -{ - return compute(key); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()(int8_t const& key) const -{ - return compute(key); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()(uint8_t const& key) const -{ - return compute(key); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()(int16_t const& key) const -{ - return compute(key); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()( - uint16_t const& key) const -{ - return compute(key); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()(float const& key) const -{ - return compute(detail::normalize_nans(key)); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()(double const& key) const -{ - return compute(detail::normalize_nans(key)); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()( - cudf::string_view const& key) const -{ - auto const data = reinterpret_cast(key.data()); - auto const len = key.size_bytes(); - return compute_bytes(data, len); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()( - numeric::decimal32 const& key) const -{ - return compute(key.value()); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()( - numeric::decimal64 const& key) const -{ - return compute(key.value()); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()( - numeric::decimal128 const& key) const -{ - // Generates the Spark MurmurHash3 hash value, mimicking the conversion: - // java.math.BigDecimal.valueOf(unscaled_value, _scale).unscaledValue().toByteArray() - // https://github.com/apache/spark/blob/master/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/hash.scala#L381 - __int128_t const val = key.value(); - constexpr cudf::size_type key_size = sizeof(__int128_t); - std::byte const* data = reinterpret_cast(&val); - - // Small negative values start with 0xff..., small positive values start with 0x00... - bool const is_negative = val < 0; - std::byte const zero_value = is_negative ? std::byte{0xff} : std::byte{0x00}; - - // If the value can be represented with a shorter than 16-byte integer, the - // leading bytes of the little-endian value are truncated and are not hashed. - auto const reverse_begin = thrust::reverse_iterator(data + key_size); - auto const reverse_end = thrust::reverse_iterator(data); - auto const first_nonzero_byte = - thrust::find_if_not(thrust::seq, reverse_begin, reverse_end, [zero_value](std::byte const& v) { - return v == zero_value; - }).base(); - // Max handles special case of 0 and -1 which would shorten to 0 length otherwise - cudf::size_type length = - std::max(1, static_cast(thrust::distance(data, first_nonzero_byte))); - - // Preserve the 2's complement sign bit by adding a byte back on if necessary. - // e.g. 0x0000ff would shorten to 0x00ff. The 0x00 byte is retained to - // preserve the sign bit, rather than leaving an "f" at the front which would - // change the sign bit. However, 0x00007f would shorten to 0x7f. No extra byte - // is needed because the leftmost bit matches the sign bit. Similarly for - // negative values: 0xffff00 --> 0xff00 and 0xffff80 --> 0x80. - if ((length < key_size) && (is_negative ^ bool(data[length - 1] & std::byte{0x80}))) { ++length; } - - // Convert to big endian by reversing the range of nonzero bytes. Only those bytes are hashed. - __int128_t big_endian_value = 0; - auto big_endian_data = reinterpret_cast(&big_endian_value); - thrust::reverse_copy(thrust::seq, data, data + length, big_endian_data); - return compute_bytes(big_endian_data, length); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()( - cudf::list_view const& key) const -{ - CUDF_UNREACHABLE("List column hashing is not supported"); -} - -template <> -hash_value_type __device__ inline SparkMurmurHash3_32::operator()( - cudf::struct_view const& key) const -{ - CUDF_UNREACHABLE("Direct hashing of struct_view is not supported"); -} - /** * @brief This hash function simply returns the value that is asked to be hash * reinterpreted as the result_type of the functor. diff --git a/cpp/src/hash/spark_murmur_hash.cu b/cpp/src/hash/spark_murmur_hash.cu index a9bcf8b848d..fa441e0ec6f 100644 --- a/cpp/src/hash/spark_murmur_hash.cu +++ b/cpp/src/hash/spark_murmur_hash.cu @@ -31,12 +31,216 @@ namespace detail { namespace { -// TODO: Spark uses int32_t hash values, but libcudf defines hash_value_type as -// uint32_t elsewhere. I plan to move the SparkMurmurHash3_32 functor into this -// file (since it is only used here), and replace its use of hash_value_type -// with spark_hash_value_type. --bdice using spark_hash_value_type = int32_t; +template ())> +struct SparkMurmurHash3_32 { + using result_type = spark_hash_value_type; + + constexpr SparkMurmurHash3_32() = default; + constexpr SparkMurmurHash3_32(uint32_t seed) : m_seed(seed) {} + + [[nodiscard]] __device__ inline uint32_t fmix32(uint32_t h) const + { + h ^= h >> 16; + h *= 0x85ebca6b; + h ^= h >> 13; + h *= 0xc2b2ae35; + h ^= h >> 16; + return h; + } + + [[nodiscard]] __device__ inline uint32_t getblock32(std::byte const* data, + cudf::size_type offset) const + { + // Read a 4-byte value from the data pointer as individual bytes for safe + // unaligned access (very likely for string types). + auto block = reinterpret_cast(data + offset); + return block[0] | (block[1] << 8) | (block[2] << 16) | (block[3] << 24); + } + + [[nodiscard]] result_type __device__ inline operator()(Key const& key) const + { + return compute(key); + } + + template + result_type __device__ inline compute(T const& key) const + { + return compute_bytes(reinterpret_cast(&key), sizeof(T)); + } + + result_type __device__ inline compute_remaining_bytes(std::byte const* data, + cudf::size_type len, + cudf::size_type tail_offset, + result_type h) const + { + // Process remaining bytes that do not fill a four-byte chunk using Spark's approach + // (does not conform to normal MurmurHash3). + for (auto i = tail_offset; i < len; i++) { + // We require a two-step cast to get the k1 value from the byte. First, + // we must cast to a signed int8_t. Then, the sign bit is preserved when + // casting to uint32_t under 2's complement. Java preserves the sign when + // casting byte-to-int, but C++ does not. + uint32_t k1 = static_cast(std::to_integer(data[i])); + k1 *= c1; + k1 = cudf::detail::rotate_bits_left(k1, rot_c1); + k1 *= c2; + h ^= k1; + h = cudf::detail::rotate_bits_left(h, rot_c2); + h = h * 5 + c3; + } + return h; + } + + result_type __device__ compute_bytes(std::byte const* data, cudf::size_type const len) const + { + constexpr cudf::size_type BLOCK_SIZE = 4; + cudf::size_type const nblocks = len / BLOCK_SIZE; + cudf::size_type const tail_offset = nblocks * BLOCK_SIZE; + result_type h = m_seed; + + // Process all four-byte chunks. + for (cudf::size_type i = 0; i < nblocks; i++) { + uint32_t k1 = getblock32(data, i * BLOCK_SIZE); + k1 *= c1; + k1 = cudf::detail::rotate_bits_left(k1, rot_c1); + k1 *= c2; + h ^= k1; + h = cudf::detail::rotate_bits_left(h, rot_c2); + h = h * 5 + c3; + } + + h = compute_remaining_bytes(data, len, tail_offset, h); + + // Finalize hash. + h ^= len; + h = fmix32(h); + return h; + } + + private: + uint32_t m_seed{cudf::DEFAULT_HASH_SEED}; + static constexpr uint32_t c1 = 0xcc9e2d51; + static constexpr uint32_t c2 = 0x1b873593; + static constexpr uint32_t c3 = 0xe6546b64; + static constexpr uint32_t rot_c1 = 15; + static constexpr uint32_t rot_c2 = 13; +}; + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()(bool const& key) const +{ + return compute(key); +} + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()( + int8_t const& key) const +{ + return compute(key); +} + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()( + uint8_t const& key) const +{ + return compute(key); +} + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()( + int16_t const& key) const +{ + return compute(key); +} + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()( + uint16_t const& key) const +{ + return compute(key); +} + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()( + float const& key) const +{ + return compute(detail::normalize_nans(key)); +} + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()( + double const& key) const +{ + return compute(detail::normalize_nans(key)); +} + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()( + cudf::string_view const& key) const +{ + auto const data = reinterpret_cast(key.data()); + auto const len = key.size_bytes(); + return compute_bytes(data, len); +} + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()( + numeric::decimal32 const& key) const +{ + return compute(key.value()); +} + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()( + numeric::decimal64 const& key) const +{ + return compute(key.value()); +} + +template <> +spark_hash_value_type __device__ inline SparkMurmurHash3_32::operator()( + numeric::decimal128 const& key) const +{ + // Generates the Spark MurmurHash3 hash value, mimicking the conversion: + // java.math.BigDecimal.valueOf(unscaled_value, _scale).unscaledValue().toByteArray() + // https://github.com/apache/spark/blob/master/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/hash.scala#L381 + __int128_t const val = key.value(); + constexpr cudf::size_type key_size = sizeof(__int128_t); + std::byte const* data = reinterpret_cast(&val); + + // Small negative values start with 0xff..., small positive values start with 0x00... + bool const is_negative = val < 0; + std::byte const zero_value = is_negative ? std::byte{0xff} : std::byte{0x00}; + + // If the value can be represented with a shorter than 16-byte integer, the + // leading bytes of the little-endian value are truncated and are not hashed. + auto const reverse_begin = thrust::reverse_iterator(data + key_size); + auto const reverse_end = thrust::reverse_iterator(data); + auto const first_nonzero_byte = + thrust::find_if_not(thrust::seq, reverse_begin, reverse_end, [zero_value](std::byte const& v) { + return v == zero_value; + }).base(); + // Max handles special case of 0 and -1 which would shorten to 0 length otherwise + cudf::size_type length = + std::max(1, static_cast(thrust::distance(data, first_nonzero_byte))); + + // Preserve the 2's complement sign bit by adding a byte back on if necessary. + // e.g. 0x0000ff would shorten to 0x00ff. The 0x00 byte is retained to + // preserve the sign bit, rather than leaving an "f" at the front which would + // change the sign bit. However, 0x00007f would shorten to 0x7f. No extra byte + // is needed because the leftmost bit matches the sign bit. Similarly for + // negative values: 0xffff00 --> 0xff00 and 0xffff80 --> 0x80. + if ((length < key_size) && (is_negative ^ bool(data[length - 1] & std::byte{0x80}))) { ++length; } + + // Convert to big endian by reversing the range of nonzero bytes. Only those bytes are hashed. + __int128_t big_endian_value = 0; + auto big_endian_data = reinterpret_cast(&big_endian_value); + thrust::reverse_copy(thrust::seq, data, data + length, big_endian_data); + return compute_bytes(big_endian_data, length); +} + /** * @brief Computes the hash value of a row in the given table. * From dccb586111651b5e0fa1ad7845d73f593670fa07 Mon Sep 17 00:00:00 2001 From: Nghia Truong Date: Tue, 9 Aug 2022 12:44:17 -0700 Subject: [PATCH 12/58] Fix out-of-bound access in `cudf::detail::label_segments` (#11497) In `cudf::detail::label_segments`, when the input lists column has empty/nulls lists at the end of the column, its `offsets` column will contain out-of-bound indices. This leads to invalid memory access bug. Such bug is elusive and doesn't show up consistently. Test failures reported in https://github.com/NVIDIA/spark-rapids/issues/6249 are due to this. The existing unit tests already cover such corner case. Unfortunately, the bug didn't show up until being tested on some systems. Even that, it was very difficult to reproduce it. Closes https://github.com/rapidsai/cudf/issues/11495. Authors: - Nghia Truong (https://github.com/ttnghia) Approvers: - Tobias Ribizel (https://github.com/upsj) - Bradley Dice (https://github.com/bdice) - Jim Brennan (https://github.com/jbrennan333) - Alessandro Bellina (https://github.com/abellina) - Karthikeyan (https://github.com/karthikeyann) --- .../cudf/detail/labeling/label_segments.cuh | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/cpp/include/cudf/detail/labeling/label_segments.cuh b/cpp/include/cudf/detail/labeling/label_segments.cuh index e30f5b3ee91..5a901cc4e3f 100644 --- a/cpp/include/cudf/detail/labeling/label_segments.cuh +++ b/cpp/include/cudf/detail/labeling/label_segments.cuh @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -67,10 +68,12 @@ void label_segments(InputIterator offsets_begin, OutputIterator label_end, rmm::cuda_stream_view stream) { + auto const num_labels = thrust::distance(label_begin, label_end); + // If the output array is empty, that means we have all empty segments. // In such cases, we must terminate immediately. Otherwise, the `for_each` loop below may try to // access memory of the output array, resulting in "illegal memory access" error. - if (thrust::distance(label_begin, label_end) == 0) { return; } + if (num_labels == 0) { return; } // When the output array is not empty, always fill it with `0` value first. using OutputType = typename thrust::iterator_value::type; @@ -83,19 +86,24 @@ void label_segments(InputIterator offsets_begin, // very large segment. if (thrust::distance(offsets_begin, offsets_end) <= 2) { return; } - thrust::for_each(rmm::exec_policy(stream), - offsets_begin + 1, // exclude the first offset value - offsets_end - 1, // exclude the last offset value - [offsets = offsets_begin, output = label_begin] __device__(auto const idx) { - // Zero-normalized offsets. - auto const dst_idx = idx - (*offsets); - - // Scatter value `1` to the index at (idx - offsets[0]). - // In case we have repeated offsets (i.e., we have empty segments), this - // `atomicAdd` call will make sure the label values corresponding to these - // empty segments will be skipped in the output. - atomicAdd(&output[dst_idx], OutputType{1}); - }); + thrust::for_each( + rmm::exec_policy(stream), + offsets_begin + 1, // exclude the first offset value + offsets_end - 1, // exclude the last offset value + [num_labels = static_cast::type>(num_labels), + offsets = offsets_begin, + output = label_begin] __device__(auto const idx) { + // Zero-normalized offsets. + auto const dst_idx = idx - (*offsets); + + // Scatter value `1` to the index at (idx - offsets[0]). + // Note that we need to check for out of bound, since the offset values may be invalid due to + // empty segments at the end. + // In case we have repeated offsets (i.e., we have empty segments), this `atomicAdd` call will + // make sure the label values corresponding to these empty segments will be skipped in the + // output. + if (dst_idx < num_labels) { atomicAdd(&output[dst_idx], OutputType{1}); } + }); thrust::inclusive_scan(rmm::exec_policy(stream), label_begin, label_end, label_begin); } From 11d40a01064c9b4846e6db7d7b299350d0ba5074 Mon Sep 17 00:00:00 2001 From: Nghia Truong Date: Wed, 10 Aug 2022 06:26:46 -0700 Subject: [PATCH 13/58] Add reduction `distinct_count` benchmark (#11473) This adds a simple benchmark for reduction `distinct_count`. Authors: - Nghia Truong (https://github.com/ttnghia) Approvers: - Karthikeyan (https://github.com/karthikeyann) - Elias Stehle (https://github.com/elstehle) URL: https://github.com/rapidsai/cudf/pull/11473 --- cpp/benchmarks/CMakeLists.txt | 4 +- cpp/benchmarks/reduction/distinct_count.cpp | 63 +++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 cpp/benchmarks/reduction/distinct_count.cpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 3c917e54c63..4a4da2f28b9 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -182,7 +182,9 @@ ConfigureBench( REDUCTION_BENCH reduction/anyall.cpp reduction/dictionary.cpp reduction/minmax.cpp reduction/reduce.cpp reduction/scan.cpp ) -ConfigureNVBench(REDUCTION_NVBENCH reduction/segment_reduce.cu reduction/rank.cpp) +ConfigureNVBench( + REDUCTION_NVBENCH reduction/distinct_count.cpp reduction/rank.cpp reduction/segment_reduce.cu +) # ################################################################################################## # * reduction benchmark --------------------------------------------------------------------------- diff --git a/cpp/benchmarks/reduction/distinct_count.cpp b/cpp/benchmarks/reduction/distinct_count.cpp new file mode 100644 index 00000000000..6e582c501e2 --- /dev/null +++ b/cpp/benchmarks/reduction/distinct_count.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include + +template +static void bench_reduction_distinct_count(nvbench::state& state, nvbench::type_list) +{ + cudf::rmm_pool_raii pool_raii; + + auto const dtype = cudf::type_to_id(); + auto const size = static_cast(state.get_int64("num_rows")); + auto const null_frequency = state.get_float64("null_frequency"); + + data_profile profile; + profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, size / 100); + if (null_frequency > 0) { + profile.set_null_frequency({null_frequency}); + } else { + profile.set_null_frequency(std::nullopt); + } + + auto const data_table = create_random_table({dtype}, row_count{size}, profile); + auto const& data_column = data_table->get_column(0); + auto const input_table = cudf::table_view{{data_column, data_column, data_column}}; + + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { + rmm::cuda_stream_view stream{launch.get_stream()}; + cudf::detail::distinct_count(input_table, cudf::null_equality::EQUAL, stream); + }); +} + +using data_type = nvbench::type_list; + +NVBENCH_BENCH_TYPES(bench_reduction_distinct_count, NVBENCH_TYPE_AXES(data_type)) + .set_name("reduction_distinct_count") + .add_int64_axis("num_rows", + { + 10000, // 10k + 100000, // 100k + 1000000, // 1M + 10000000, // 10M + 100000000, // 100M + }) + .add_float64_axis("null_frequency", {0, 0.5}); From 80a2f2b0d9cb96434c9af79e0130812acf63ed7b Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Wed, 10 Aug 2022 14:19:23 -0500 Subject: [PATCH 14/58] Update parquet fuzz tests to drop support for `skiprows` & `num_rows` (#11505) In a previous PR https://github.com/rapidsai/cudf/pull/11480/, `skiprows` & `num_rows` were removed from `cudf.read_parquet`, this PR updates the corresponding parquet reader fuzz tests. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11505 --- python/cudf/cudf/_fuzz_testing/parquet.py | 4 ---- .../cudf/_fuzz_testing/tests/fuzz_test_parquet.py | 12 +----------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/python/cudf/cudf/_fuzz_testing/parquet.py b/python/cudf/cudf/_fuzz_testing/parquet.py index 31be9aa2a5e..2d934e4816d 100644 --- a/python/cudf/cudf/_fuzz_testing/parquet.py +++ b/python/cudf/cudf/_fuzz_testing/parquet.py @@ -102,10 +102,6 @@ def set_rand_params(self, params): params_dict[param] = list( np.unique(np.random.choice(self._df.columns, col_size)) ) - elif param in ("skiprows", "num_rows"): - params_dict[param] = np.random.choice( - [None, self._rand(len(self._df))] - ) else: params_dict[param] = np.random.choice(values) self._current_params["test_kwargs"] = self.process_kwargs(params_dict) diff --git a/python/cudf/cudf/_fuzz_testing/tests/fuzz_test_parquet.py b/python/cudf/cudf/_fuzz_testing/tests/fuzz_test_parquet.py index 5b5e7c5964d..3d070576a12 100644 --- a/python/cudf/cudf/_fuzz_testing/tests/fuzz_test_parquet.py +++ b/python/cudf/cudf/_fuzz_testing/tests/fuzz_test_parquet.py @@ -28,29 +28,19 @@ def parquet_reader_test(parquet_buffer): params={ "columns": ALL_POSSIBLE_VALUES, "use_pandas_metadata": [True, False], - "skiprows": ALL_POSSIBLE_VALUES, - "num_rows": ALL_POSSIBLE_VALUES, }, ) -def parquet_reader_columns( - parquet_buffer, columns, use_pandas_metadata, skiprows, num_rows -): +def parquet_reader_columns(parquet_buffer, columns, use_pandas_metadata): pdf = pd.read_parquet( parquet_buffer, columns=columns, use_pandas_metadata=use_pandas_metadata, ) - pdf = pdf.iloc[skiprows:] - if num_rows is not None: - pdf = pdf.head(num_rows) - gdf = cudf.read_parquet( parquet_buffer, columns=columns, use_pandas_metadata=use_pandas_metadata, - skiprows=skiprows, - num_rows=num_rows, ) compare_dataframe(gdf, pdf) From 9257549e563c211b301fe249c13db9783dba5610 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Wed, 10 Aug 2022 16:26:33 -0500 Subject: [PATCH 15/58] Upgrade to `arrow-9.x` (#11507) This PR upgrades `arrow` to `9.x` in `cudf`. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - AJ Schmidt (https://github.com/ajschmidt8) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11507 --- conda/environments/cudf_dev_cuda11.5.yml | 4 ++-- conda/recipes/cudf/meta.yaml | 2 +- conda/recipes/libcudf/conda_build_config.yaml | 2 +- cpp/cmake/thirdparty/get_arrow.cmake | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/conda/environments/cudf_dev_cuda11.5.yml b/conda/environments/cudf_dev_cuda11.5.yml index 940d7452183..bdc853f8a97 100644 --- a/conda/environments/cudf_dev_cuda11.5.yml +++ b/conda/environments/cudf_dev_cuda11.5.yml @@ -21,7 +21,7 @@ dependencies: - numba>=0.54 - numpy - pandas>=1.0,<1.5.0dev0 - - pyarrow=8 + - pyarrow=9 - fastavro>=0.22.9 - python-snappy>=0.6.0 - notebook>=0.5.0 @@ -51,7 +51,7 @@ dependencies: - dask>=2022.7.1 - distributed>=2022.7.1 - streamz - - arrow-cpp=8 + - arrow-cpp=9 - dlpack>=0.5,<0.6.0a0 - double-conversion - rapidjson diff --git a/conda/recipes/cudf/meta.yaml b/conda/recipes/cudf/meta.yaml index 60cd15c4ae5..6a7554b99aa 100644 --- a/conda/recipes/cudf/meta.yaml +++ b/conda/recipes/cudf/meta.yaml @@ -40,7 +40,7 @@ requirements: - setuptools - numba >=0.54 - dlpack>=0.5,<0.6.0a0 - - pyarrow =8 + - pyarrow =9 - libcudf ={{ version }} - rmm ={{ minor_version }} - cudatoolkit ={{ cuda_version }} diff --git a/conda/recipes/libcudf/conda_build_config.yaml b/conda/recipes/libcudf/conda_build_config.yaml index 3a403d592c4..4cf672997d3 100644 --- a/conda/recipes/libcudf/conda_build_config.yaml +++ b/conda/recipes/libcudf/conda_build_config.yaml @@ -17,7 +17,7 @@ gtest_version: - "=1.10.0" arrow_cpp_version: - - "=8" + - "=9" dlpack_version: - ">=0.5,<0.6.0a0" diff --git a/cpp/cmake/thirdparty/get_arrow.cmake b/cpp/cmake/thirdparty/get_arrow.cmake index e0f9a711776..82525dab6cd 100644 --- a/cpp/cmake/thirdparty/get_arrow.cmake +++ b/cpp/cmake/thirdparty/get_arrow.cmake @@ -275,7 +275,7 @@ endfunction() if(NOT DEFINED CUDF_VERSION_Arrow) set(CUDF_VERSION_Arrow - 8.0.0 + 9.0.0 CACHE STRING "The version of Arrow to find (or build)" ) endif() From 0df617810ce60724633cf9ef41160e6830c4ffbe Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 10 Aug 2022 21:25:06 -0500 Subject: [PATCH 16/58] Update to Thrust 1.17.0 (#11437) Updates the bundled version of Thrust to 1.17.0. I will run benchmarks and include results in a comment below. Depends on #11457. Supersedes #10489, #10577, #10586. Closes #10841. **This should be merged concurrently with https://github.com/rapidsai/rapids-cmake/pull/231.** Authors: - Bradley Dice (https://github.com/bdice) Approvers: - David Wendt (https://github.com/davidwendt) - Nghia Truong (https://github.com/ttnghia) - Robert Maynard (https://github.com/robertmaynard) URL: https://github.com/rapidsai/cudf/pull/11437 --- cpp/cmake/thirdparty/get_thrust.cmake | 2 +- cpp/cmake/thrust.patch | 141 ++++++++++++++++---------- 2 files changed, 88 insertions(+), 55 deletions(-) diff --git a/cpp/cmake/thirdparty/get_thrust.cmake b/cpp/cmake/thirdparty/get_thrust.cmake index 927186d3f49..cbdaf5520ff 100644 --- a/cpp/cmake/thirdparty/get_thrust.cmake +++ b/cpp/cmake/thirdparty/get_thrust.cmake @@ -80,6 +80,6 @@ function(find_and_configure_thrust VERSION) endif() endfunction() -set(CUDF_MIN_VERSION_Thrust 1.15.0) +set(CUDF_MIN_VERSION_Thrust 1.17.0) find_and_configure_thrust(${CUDF_MIN_VERSION_Thrust}) diff --git a/cpp/cmake/thrust.patch b/cpp/cmake/thrust.patch index 2f9201d8ab4..ae1962e4738 100644 --- a/cpp/cmake/thrust.patch +++ b/cpp/cmake/thrust.patch @@ -1,83 +1,116 @@ -diff --git a/thrust/system/cuda/detail/sort.h b/thrust/system/cuda/detail/sort.h -index 1ffeef0..5e80800 100644 ---- a/thrust/system/cuda/detail/sort.h -+++ b/thrust/system/cuda/detail/sort.h -@@ -108,7 +108,7 @@ namespace __merge_sort { - key_type key2 = keys_shared[keys2_beg]; - - +diff --git a/cub/block/block_merge_sort.cuh b/cub/block/block_merge_sort.cuh +index 4769df36..d86d6342 100644 +--- a/cub/block/block_merge_sort.cuh ++++ b/cub/block/block_merge_sort.cuh +@@ -91,7 +91,7 @@ __device__ __forceinline__ void SerialMerge(KeyT *keys_shared, + KeyT key1 = keys_shared[keys1_beg]; + KeyT key2 = keys_shared[keys2_beg]; + -#pragma unroll +#pragma unroll 1 - for (int ITEM = 0; ITEM < ITEMS_PER_THREAD; ++ITEM) - { - bool p = (keys2_beg < keys2_end) && -@@ -311,10 +311,10 @@ namespace __merge_sort { - void stable_odd_even_sort(key_type (&keys)[ITEMS_PER_THREAD], - item_type (&items)[ITEMS_PER_THREAD]) + for (int item = 0; item < ITEMS_PER_THREAD; ++item) + { + bool p = (keys2_beg < keys2_end) && +@@ -383,7 +383,7 @@ public: + // + KeyT max_key = oob_default; + +- #pragma unroll ++ #pragma unroll 1 + for (int item = 1; item < ITEMS_PER_THREAD; ++item) { --#pragma unroll -+#pragma unroll 1 - for (int i = 0; i < ITEMS_PER_THREAD; ++i) - { --#pragma unroll -+#pragma unroll 1 - for (int j = 1 & i; j < ITEMS_PER_THREAD - 1; j += 2) - { - if (compare_op(keys[j + 1], keys[j])) -@@ -350,7 +350,7 @@ namespace __merge_sort { - // each thread has sorted keys_loc - // merge sort keys_loc in shared memory - // --#pragma unroll -+#pragma unroll 1 - for (int coop = 2; coop <= BLOCK_THREADS; coop *= 2) - { - sync_threadblock(); -@@ -479,7 +479,7 @@ namespace __merge_sort { - // and fill the remainig keys with it - // - key_type max_key = keys_loc[0]; --#pragma unroll -+#pragma unroll 1 - for (int ITEM = 1; ITEM < ITEMS_PER_THREAD; ++ITEM) - { - if (ITEMS_PER_THREAD * tid + ITEM < num_remaining) -diff a/cub/device/dispatch/dispatch_radix_sort.cuh b/cub/device/dispatch/dispatch_radix_sort.cuh -index 41eb1d2..f2893b4 100644 + if (ITEMS_PER_THREAD * linear_tid + item < valid_items) +@@ -407,7 +407,7 @@ public: + // each thread has sorted keys + // merge sort keys in shared memory + // +- #pragma unroll ++ #pragma unroll 1 + for (int target_merged_threads_number = 2; + target_merged_threads_number <= NUM_THREADS; + target_merged_threads_number *= 2) +diff --git a/cub/device/dispatch/dispatch_radix_sort.cuh b/cub/device/dispatch/dispatch_radix_sort.cuh +index b188c75f..3f36656f 100644 --- a/cub/device/dispatch/dispatch_radix_sort.cuh +++ b/cub/device/dispatch/dispatch_radix_sort.cuh -@@ -723,7 +723,7 @@ struct DeviceRadixSortPolicy - - +@@ -736,7 +736,7 @@ struct DeviceRadixSortPolicy + + /// SM60 (GP100) - struct Policy600 : ChainedPolicy<600, Policy600, Policy500> + struct Policy600 : ChainedPolicy<600, Policy600, Policy600> { enum { PRIMARY_RADIX_BITS = (sizeof(KeyT) > 1) ? 7 : 5, // 6.9B 32b keys/s (Quadro P100) -diff a/cub/device/dispatch/dispatch_reduce.cuh b/cub/device/dispatch/dispatch_reduce.cuh -index f6aee45..dd64301 100644 +diff --git a/cub/device/dispatch/dispatch_reduce.cuh b/cub/device/dispatch/dispatch_reduce.cuh +index e0470ccb..6a0c2ed6 100644 --- a/cub/device/dispatch/dispatch_reduce.cuh +++ b/cub/device/dispatch/dispatch_reduce.cuh -@@ -284,7 +284,7 @@ struct DeviceReducePolicy +@@ -280,7 +280,7 @@ struct DeviceReducePolicy }; - + /// SM60 - struct Policy600 : ChainedPolicy<600, Policy600, Policy350> + struct Policy600 : ChainedPolicy<600, Policy600, Policy600> { // ReducePolicy (P100: 591 GB/s @ 64M 4B items; 583 GB/s @ 256M 1B items) typedef AgentReducePolicy< -diff a/cub/device/dispatch/dispatch_scan.cuh b/cub/device/dispatch/dispatch_scan.cuh -index c0c6d59..937ee31 100644 +diff --git a/cub/device/dispatch/dispatch_scan.cuh b/cub/device/dispatch/dispatch_scan.cuh +index c2d04588..ac2d10e0 100644 --- a/cub/device/dispatch/dispatch_scan.cuh +++ b/cub/device/dispatch/dispatch_scan.cuh -@@ -178,7 +178,7 @@ struct DeviceScanPolicy +@@ -177,7 +177,7 @@ struct DeviceScanPolicy }; - + /// SM600 - struct Policy600 : ChainedPolicy<600, Policy600, Policy520> + struct Policy600 : ChainedPolicy<600, Policy600, Policy600> { typedef AgentScanPolicy< 128, 15, ///< Threads per block, items per thread +diff --git a/cub/thread/thread_sort.cuh b/cub/thread/thread_sort.cuh +index 5d486789..b42fb5f0 100644 +--- a/cub/thread/thread_sort.cuh ++++ b/cub/thread/thread_sort.cuh +@@ -83,10 +83,10 @@ StableOddEvenSort(KeyT (&keys)[ITEMS_PER_THREAD], + { + constexpr bool KEYS_ONLY = std::is_same::value; + +- #pragma unroll ++ #pragma unroll 1 + for (int i = 0; i < ITEMS_PER_THREAD; ++i) + { +- #pragma unroll ++ #pragma unroll 1 + for (int j = 1 & i; j < ITEMS_PER_THREAD - 1; j += 2) + { + if (compare_op(keys[j + 1], keys[j])) +diff --git a/thrust/system/cuda/detail/dispatch.h b/thrust/system/cuda/detail/dispatch.h +index d0e3f94..76774b0 100644 +--- a/thrust/system/cuda/detail/dispatch.h ++++ b/thrust/system/cuda/detail/dispatch.h +@@ -32,9 +32,8 @@ + status = call arguments; \ + } \ + else { \ +- auto THRUST_PP_CAT2(count, _fixed) = static_cast(count); \ +- status = call arguments; \ +- } ++ throw std::runtime_error("THRUST_INDEX_TYPE_DISPATCH 64-bit count is unsupported in libcudf"); \ ++ } + + /** + * Dispatch between 32-bit and 64-bit index based versions of the same algorithm +@@ -52,10 +51,8 @@ + status = call arguments; \ + } \ + else { \ +- auto THRUST_PP_CAT2(count1, _fixed) = static_cast(count1); \ +- auto THRUST_PP_CAT2(count2, _fixed) = static_cast(count2); \ +- status = call arguments; \ +- } ++ throw std::runtime_error("THRUST_DOUBLE_INDEX_TYPE_DISPATCH 64-bit count is unsupported in libcudf"); \ ++ } + /** + * Dispatch between 32-bit and 64-bit index based versions of the same algorithm + * implementation. This version allows using different token sequences for callables From 5628f57765f2cd4ce84eeabbee07d27bcd4020cf Mon Sep 17 00:00:00 2001 From: Robert Maynard Date: Thu, 11 Aug 2022 08:54:44 -0400 Subject: [PATCH 17/58] copy_range ballot_syncs to have no execution dependency (#11508) We can simplify the logic around determining the warp_mask by having both queries issued without a dependency Authors: - Robert Maynard (https://github.com/robertmaynard) Approvers: - David Wendt (https://github.com/davidwendt) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/11508 --- cpp/include/cudf/detail/copy_range.cuh | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/cpp/include/cudf/detail/copy_range.cuh b/cpp/include/cudf/detail/copy_range.cuh index 9065ed83b32..fca6fd7547d 100644 --- a/cpp/include/cudf/detail/copy_range.cuh +++ b/cpp/include/cudf/detail/copy_range.cuh @@ -79,18 +79,15 @@ __global__ void copy_range_kernel(SourceValueIterator source_value_begin, if (in_range) target.element(index) = *(source_value_begin + source_idx); if (has_validity) { // update bitmask - int active_mask = __ballot_sync(0xFFFFFFFF, in_range); - - bool valid = in_range && *(source_validity_begin + source_idx); - int warp_mask = __ballot_sync(active_mask, valid); + const bool valid = in_range && *(source_validity_begin + source_idx); + const int active_mask = __ballot_sync(0xFFFFFFFF, in_range); + const int valid_mask = __ballot_sync(0xFFFFFFFF, valid); + const int warp_mask = active_mask & valid_mask; cudf::bitmask_type old_mask = target.get_mask_word(mask_idx); - if (lane_id == leader_lane) { - cudf::bitmask_type new_mask = (old_mask & ~active_mask) | (warp_mask & active_mask); + cudf::bitmask_type new_mask = (old_mask & ~active_mask) | warp_mask; target.set_mask_word(mask_idx, new_mask); - // null_diff = - // (warp_size - __popc(new_mask)) - (warp_size - __popc(old_mask)) warp_null_change += __popc(active_mask & old_mask) - __popc(active_mask & new_mask); } } From 95935db773c637d9a4d271bccb0c6bf8d60b109f Mon Sep 17 00:00:00 2001 From: "Mads R. B. Kristensen" Date: Thu, 11 Aug 2022 15:40:45 +0200 Subject: [PATCH 18/58] Refactor the `Buffer` class (#11447) This PR introduces factory functions to create `Buffer` instances, which makes it possible to change the returned buffer type based on a configuration option in a follow-up PR. Beside simplifying the code base a bit, this is motivated by the spilling work in https://github.com/rapidsai/cudf/pull/10746. We would like to introduce a new spillable Buffer class that requires minimal changes to the existing code and is only used when enabled explicitly. This way, we can introduce spilling in cuDF as an experimental feature with minimal risk to the existing code. @shwina and I discussed the possibility to let `Buffer.__new__` return different class type instances instead of using factory functions but we concluded that having `Buffer()` return anything other than an instance of `Buffer` is simply too surprising :) **Notice**, this is breaking because it removes unused methods such as `Buffer.copy()` and `Buffer.nbytes`. ~~However, we still support creating a buffer directly by calling `Buffer(obj)`. AFAIK, this is the only way `Buffer` is created outside of cuDF, which [a github search seems to confirm](https://github.com/search?l=&q=cudf.core.buffer+-repo%3Arapidsai%2Fcudf&type=code).~~ This PR doesn't change the signature of `Buffer.__init__()` anymore. Authors: - Mads R. B. Kristensen (https://github.com/madsbk) Approvers: - Ashwin Srinath (https://github.com/shwina) - Lawrence Mitchell (https://github.com/wence-) - Bradley Dice (https://github.com/bdice) - https://github.com/brandon-b-miller URL: https://github.com/rapidsai/cudf/pull/11447 --- python/cudf/cudf/_lib/column.pyi | 28 +- python/cudf/cudf/_lib/column.pyx | 71 ++-- python/cudf/cudf/_lib/concat.pyx | 8 +- python/cudf/cudf/_lib/copying.pyx | 6 +- python/cudf/cudf/_lib/null_mask.pyx | 6 +- python/cudf/cudf/_lib/transform.pyx | 12 +- python/cudf/cudf/_lib/utils.pyx | 4 +- python/cudf/cudf/core/abc.py | 12 +- python/cudf/cudf/core/buffer.py | 377 +++++++++++------- python/cudf/cudf/core/column/categorical.py | 18 +- python/cudf/cudf/core/column/column.py | 86 ++-- python/cudf/cudf/core/column/datetime.py | 14 +- python/cudf/cudf/core/column/decimal.py | 14 +- python/cudf/cudf/core/column/lists.py | 3 +- python/cudf/cudf/core/column/numerical.py | 18 +- python/cudf/cudf/core/column/string.py | 6 +- python/cudf/cudf/core/column/struct.py | 4 +- python/cudf/cudf/core/column/timedelta.py | 14 +- python/cudf/cudf/core/df_protocol.py | 34 +- python/cudf/cudf/core/dtypes.py | 4 +- python/cudf/cudf/core/index.py | 2 +- python/cudf/cudf/core/series.py | 4 +- python/cudf/cudf/tests/test_buffer.py | 96 +++-- python/cudf/cudf/tests/test_column.py | 2 +- .../cudf/tests/test_cuda_array_interface.py | 6 +- python/cudf/cudf/tests/test_df_protocol.py | 3 +- python/cudf/cudf/tests/test_pickling.py | 4 +- python/cudf/cudf/tests/test_serialize.py | 4 +- python/cudf/cudf/tests/test_testing.py | 2 +- python/cudf/cudf/utils/string.py | 13 + python/cudf/cudf/utils/utils.py | 6 +- 31 files changed, 517 insertions(+), 364 deletions(-) create mode 100644 python/cudf/cudf/utils/string.py diff --git a/python/cudf/cudf/_lib/column.pyi b/python/cudf/cudf/_lib/column.pyi index c38c560b982..fd9aab038d4 100644 --- a/python/cudf/cudf/_lib/column.pyi +++ b/python/cudf/cudf/_lib/column.pyi @@ -5,16 +5,16 @@ from __future__ import annotations from typing import Dict, Optional, Tuple, TypeVar from cudf._typing import Dtype, DtypeObj, ScalarLike -from cudf.core.buffer import Buffer +from cudf.core.buffer import DeviceBufferLike from cudf.core.column import ColumnBase T = TypeVar("T") class Column: - _data: Optional[Buffer] - _mask: Optional[Buffer] - _base_data: Optional[Buffer] - _base_mask: Optional[Buffer] + _data: Optional[DeviceBufferLike] + _mask: Optional[DeviceBufferLike] + _base_data: Optional[DeviceBufferLike] + _base_mask: Optional[DeviceBufferLike] _dtype: DtypeObj _size: int _offset: int @@ -25,10 +25,10 @@ class Column: def __init__( self, - data: Optional[Buffer], + data: Optional[DeviceBufferLike], size: int, dtype: Dtype, - mask: Optional[Buffer] = None, + mask: Optional[DeviceBufferLike] = None, offset: int = None, null_count: int = None, children: Tuple[ColumnBase, ...] = (), @@ -40,27 +40,27 @@ class Column: @property def size(self) -> int: ... @property - def base_data(self) -> Optional[Buffer]: ... + def base_data(self) -> Optional[DeviceBufferLike]: ... @property def base_data_ptr(self) -> int: ... @property - def data(self) -> Optional[Buffer]: ... + def data(self) -> Optional[DeviceBufferLike]: ... @property def data_ptr(self) -> int: ... - def set_base_data(self, value: Buffer) -> None: ... + def set_base_data(self, value: DeviceBufferLike) -> None: ... @property def nullable(self) -> bool: ... def has_nulls(self, include_nan: bool = False) -> bool: ... @property - def base_mask(self) -> Optional[Buffer]: ... + def base_mask(self) -> Optional[DeviceBufferLike]: ... @property def base_mask_ptr(self) -> int: ... @property - def mask(self) -> Optional[Buffer]: ... + def mask(self) -> Optional[DeviceBufferLike]: ... @property def mask_ptr(self) -> int: ... - def set_base_mask(self, value: Optional[Buffer]) -> None: ... - def set_mask(self: T, value: Optional[Buffer]) -> T: ... + def set_base_mask(self, value: Optional[DeviceBufferLike]) -> None: ... + def set_mask(self: T, value: Optional[DeviceBufferLike]) -> T: ... @property def null_count(self) -> int: ... @property diff --git a/python/cudf/cudf/_lib/column.pyx b/python/cudf/cudf/_lib/column.pyx index 8a9a79250b9..78125c027dd 100644 --- a/python/cudf/cudf/_lib/column.pyx +++ b/python/cudf/cudf/_lib/column.pyx @@ -9,7 +9,7 @@ import rmm import cudf import cudf._lib as libcudf from cudf.api.types import is_categorical_dtype, is_list_dtype, is_struct_dtype -from cudf.core.buffer import Buffer +from cudf.core.buffer import Buffer, DeviceBufferLike, as_device_buffer_like from cpython.buffer cimport PyObject_CheckBuffer from libc.stdint cimport uintptr_t @@ -56,9 +56,9 @@ cdef class Column: A Column stores columnar data in device memory. A Column may be composed of: - * A *data* Buffer + * A *data* DeviceBufferLike * One or more (optional) *children* Columns - * An (optional) *mask* Buffer representing the nullmask + * An (optional) *mask* DeviceBufferLike representing the nullmask The *dtype* indicates the Column's element type. """ @@ -110,18 +110,9 @@ cdef class Column: if self.base_data is None: return None if self._data is None: - itemsize = self.dtype.itemsize - size = self.size * itemsize - offset = self.offset * itemsize if self.size else 0 - if offset == 0 and self.base_data.size == size: - # `data` spans all of `base_data` - self._data = self.base_data - else: - self._data = Buffer.from_buffer( - buffer=self.base_data, - size=size, - offset=offset - ) + start = self.offset * self.dtype.itemsize + end = start + self.size * self.dtype.itemsize + self._data = self.base_data[start:end] return self._data @property @@ -132,9 +123,11 @@ cdef class Column: return self.data.ptr def set_base_data(self, value): - if value is not None and not isinstance(value, Buffer): - raise TypeError("Expected a Buffer or None for data, got " + - type(value).__name__) + if value is not None and not isinstance(value, DeviceBufferLike): + raise TypeError( + "Expected a DeviceBufferLike or None for data, " + f"got {type(value).__name__}" + ) self._data = None self._base_data = value @@ -179,17 +172,18 @@ cdef class Column: modify size or offset in any way, so the passed mask is expected to be compatible with the current offset. """ - if value is not None and not isinstance(value, Buffer): - raise TypeError("Expected a Buffer or None for mask, got " + - type(value).__name__) + if value is not None and not isinstance(value, DeviceBufferLike): + raise TypeError( + "Expected a DeviceBufferLike or None for mask, " + f"got {type(value).__name__}" + ) if value is not None: required_size = bitmask_allocation_size_bytes(self.base_size) if value.size < required_size: error_msg = ( - "The Buffer for mask is smaller than expected, got " + - str(value.size) + " bytes, expected " + - str(required_size) + " bytes." + "The DeviceBufferLike for mask is smaller than expected, " + f"got {value.size} bytes, expected {required_size} bytes." ) if self.offset > 0 or self.size < self.base_size: error_msg += ( @@ -233,31 +227,31 @@ cdef class Column: if isinstance(value, Column): value = value.data_array_view value = cp.asarray(value).view('|u1') - mask = Buffer(value) + mask = as_device_buffer_like(value) if mask.size < required_num_bytes: raise ValueError(error_msg.format(str(value.size))) if mask.size < mask_size: dbuf = rmm.DeviceBuffer(size=mask_size) dbuf.copy_from_device(value) - mask = Buffer(dbuf) + mask = as_device_buffer_like(dbuf) elif hasattr(value, "__array_interface__"): value = np.asarray(value).view("u1")[:mask_size] if value.size < required_num_bytes: raise ValueError(error_msg.format(str(value.size))) dbuf = rmm.DeviceBuffer(size=mask_size) dbuf.copy_from_host(value) - mask = Buffer(dbuf) + mask = as_device_buffer_like(dbuf) elif PyObject_CheckBuffer(value): value = np.asarray(value).view("u1")[:mask_size] if value.size < required_num_bytes: raise ValueError(error_msg.format(str(value.size))) dbuf = rmm.DeviceBuffer(size=mask_size) dbuf.copy_from_host(value) - mask = Buffer(dbuf) + mask = as_device_buffer_like(dbuf) else: raise TypeError( - "Expected a Buffer-like object or None for mask, got " - + type(value).__name__ + "Expected a DeviceBufferLike object or None for mask, " + f"got {type(value).__name__}" ) return cudf.core.column.build_column( @@ -455,11 +449,11 @@ cdef class Column: cdef column_contents contents = move(c_col.get()[0].release()) data = DeviceBuffer.c_from_unique_ptr(move(contents.data)) - data = Buffer(data) + data = as_device_buffer_like(data) if null_count > 0: mask = DeviceBuffer.c_from_unique_ptr(move(contents.null_mask)) - mask = Buffer(mask) + mask = as_device_buffer_like(mask) else: mask = None @@ -484,9 +478,10 @@ cdef class Column: Given a ``cudf::column_view``, constructs a ``cudf.Column`` from it, along with referencing an ``owner`` Python object that owns the memory lifetime. If ``owner`` is a ``cudf.Column``, we reach inside of it and - make the owner of each newly created ``Buffer`` the respective - ``Buffer`` from the ``owner`` ``cudf.Column``. If ``owner`` is - ``None``, we allocate new memory for the resulting ``cudf.Column``. + make the owner of each newly created ``DeviceBufferLike`` the + respective ``DeviceBufferLike`` from the ``owner`` ``cudf.Column``. + If ``owner`` is ``None``, we allocate new memory for the resulting + ``cudf.Column``. """ column_owner = isinstance(owner, Column) mask_owner = owner @@ -509,7 +504,7 @@ cdef class Column: if data_ptr: if data_owner is None: - data = Buffer( + data = as_device_buffer_like( rmm.DeviceBuffer(ptr=data_ptr, size=(size+offset) * dtype.itemsize) ) @@ -520,7 +515,7 @@ cdef class Column: owner=data_owner ) else: - data = Buffer( + data = as_device_buffer_like( rmm.DeviceBuffer(ptr=data_ptr, size=0) ) @@ -550,7 +545,7 @@ cdef class Column: # result: mask = None else: - mask = Buffer( + mask = as_device_buffer_like( rmm.DeviceBuffer( ptr=mask_ptr, size=bitmask_allocation_size_bytes(size+offset) diff --git a/python/cudf/cudf/_lib/concat.pyx b/python/cudf/cudf/_lib/concat.pyx index a7f8296bad5..ed858034032 100644 --- a/python/cudf/cudf/_lib/concat.pyx +++ b/python/cudf/cudf/_lib/concat.pyx @@ -1,4 +1,4 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2020-2022, NVIDIA CORPORATION. from libcpp cimport bool from libcpp.memory cimport make_unique, unique_ptr @@ -19,7 +19,7 @@ from cudf._lib.utils cimport ( table_view_from_table, ) -from cudf.core.buffer import Buffer +from cudf.core.buffer import as_device_buffer_like from rmm._lib.device_buffer cimport DeviceBuffer, device_buffer @@ -31,7 +31,9 @@ cpdef concat_masks(object columns): with nogil: c_result = move(libcudf_concatenate_masks(c_views)) c_unique_result = make_unique[device_buffer](move(c_result)) - return Buffer(DeviceBuffer.c_from_unique_ptr(move(c_unique_result))) + return as_device_buffer_like( + DeviceBuffer.c_from_unique_ptr(move(c_unique_result)) + ) cpdef concat_columns(object columns): diff --git a/python/cudf/cudf/_lib/copying.pyx b/python/cudf/cudf/_lib/copying.pyx index fcf70f2f69f..f1183e008f8 100644 --- a/python/cudf/cudf/_lib/copying.pyx +++ b/python/cudf/cudf/_lib/copying.pyx @@ -718,7 +718,11 @@ cdef class _CPackedColumns: header = {} frames = [] - gpu_data = Buffer(self.gpu_data_ptr, self.gpu_data_size, self) + gpu_data = Buffer( + data=self.gpu_data_ptr, + size=self.gpu_data_size, + owner=self + ) data_header, data_frames = gpu_data.serialize() header["data"] = data_header frames.extend(data_frames) diff --git a/python/cudf/cudf/_lib/null_mask.pyx b/python/cudf/cudf/_lib/null_mask.pyx index ce83a6f0f18..b0ee28baf29 100644 --- a/python/cudf/cudf/_lib/null_mask.pyx +++ b/python/cudf/cudf/_lib/null_mask.pyx @@ -17,7 +17,7 @@ from cudf._lib.cpp.null_mask cimport ( ) from cudf._lib.cpp.types cimport mask_state, size_type -from cudf.core.buffer import Buffer +from cudf.core.buffer import as_device_buffer_like class MaskState(Enum): @@ -47,7 +47,7 @@ def copy_bitmask(Column col): up_db = make_unique[device_buffer](move(db)) rmm_db = DeviceBuffer.c_from_unique_ptr(move(up_db)) - buf = Buffer(rmm_db) + buf = as_device_buffer_like(rmm_db) return buf @@ -93,5 +93,5 @@ def create_null_mask(size_type size, state=MaskState.UNINITIALIZED): up_db = make_unique[device_buffer](move(db)) rmm_db = DeviceBuffer.c_from_unique_ptr(move(up_db)) - buf = Buffer(rmm_db) + buf = as_device_buffer_like(rmm_db) return buf diff --git a/python/cudf/cudf/_lib/transform.pyx b/python/cudf/cudf/_lib/transform.pyx index 2d94ef2cedf..5fa45f68357 100644 --- a/python/cudf/cudf/_lib/transform.pyx +++ b/python/cudf/cudf/_lib/transform.pyx @@ -6,7 +6,7 @@ from numba.np import numpy_support import cudf from cudf._lib.types import SUPPORTED_NUMPY_TO_LIBCUDF_TYPES from cudf.core._internals.expressions import parse_expression -from cudf.core.buffer import Buffer +from cudf.core.buffer import as_device_buffer_like from cudf.utils import cudautils from cython.operator cimport dereference @@ -40,7 +40,7 @@ from cudf._lib.utils cimport ( def bools_to_mask(Column col): """ Given an int8 (boolean) column, compress the data from booleans to bits and - return a Buffer + return a DeviceBufferLike """ cdef column_view col_view = col.view() cdef pair[unique_ptr[device_buffer], size_type] cpp_out @@ -52,7 +52,7 @@ def bools_to_mask(Column col): up_db = move(cpp_out.first) rmm_db = DeviceBuffer.c_from_unique_ptr(move(up_db)) - buf = Buffer(rmm_db) + buf = as_device_buffer_like(rmm_db) return buf @@ -61,9 +61,9 @@ def mask_to_bools(object mask_buffer, size_type begin_bit, size_type end_bit): Given a mask buffer, returns a boolean column representng bit 0 -> False and 1 -> True within range of [begin_bit, end_bit), """ - if not isinstance(mask_buffer, cudf.core.buffer.Buffer): + if not isinstance(mask_buffer, cudf.core.buffer.DeviceBufferLike): raise TypeError("mask_buffer is not an instance of " - "cudf.core.buffer.Buffer") + "cudf.core.buffer.DeviceBufferLike") cdef bitmask_type* bit_mask = (mask_buffer.ptr) cdef unique_ptr[column] result @@ -88,7 +88,7 @@ def nans_to_nulls(Column input): return None buffer = DeviceBuffer.c_from_unique_ptr(move(c_buffer)) - buffer = Buffer(buffer) + buffer = as_device_buffer_like(buffer) return buffer diff --git a/python/cudf/cudf/_lib/utils.pyx b/python/cudf/cudf/_lib/utils.pyx index 643a1adca9f..e0bdc7d8f74 100644 --- a/python/cudf/cudf/_lib/utils.pyx +++ b/python/cudf/cudf/_lib/utils.pyx @@ -341,8 +341,8 @@ cdef data_from_table_view( along with referencing an ``owner`` Python object that owns the memory lifetime. If ``owner`` is a Frame we reach inside of it and reach inside of each ``cudf.Column`` to make the owner of each newly - created ``Buffer`` underneath the ``cudf.Column`` objects of the - created Frame the respective ``Buffer`` from the relevant + created ``DeviceBufferLike`` underneath the ``cudf.Column`` objects of the + created Frame the respective ``DeviceBufferLike`` from the relevant ``cudf.Column`` of the ``owner`` Frame """ cdef size_type column_idx = 0 diff --git a/python/cudf/cudf/core/abc.py b/python/cudf/cudf/core/abc.py index d3da544f8b5..dcbf96313a7 100644 --- a/python/cudf/cudf/core/abc.py +++ b/python/cudf/cudf/core/abc.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2021, NVIDIA CORPORATION. +# Copyright (c) 2020-2022, NVIDIA CORPORATION. """Common abstract base classes for cudf.""" import sys @@ -90,13 +90,15 @@ def device_serialize(self): header : dict The metadata required to reconstruct the object. frames : list - The Buffers or memoryviews that the object should contain. + The DeviceBufferLike or memoryview objects that the object + should contain. :meta private: """ header, frames = self.serialize() assert all( - (type(f) in [cudf.core.buffer.Buffer, memoryview]) for f in frames + isinstance(f, (cudf.core.buffer.DeviceBufferLike, memoryview)) + for f in frames ) header["type-serialized"] = pickle.dumps(type(self)) header["is-cuda"] = [ @@ -130,7 +132,7 @@ def device_deserialize(cls, header, frames): """ typ = pickle.loads(header["type-serialized"]) frames = [ - cudf.core.buffer.Buffer(f) if c else memoryview(f) + cudf.core.buffer.as_device_buffer_like(f) if c else memoryview(f) for c, f in zip(header["is-cuda"], frames) ] assert all( @@ -158,7 +160,7 @@ def host_serialize(self): header, frames = self.device_serialize() header["writeable"] = len(frames) * (None,) frames = [ - f.to_host_array().data if c else memoryview(f) + f.memoryview() if c else memoryview(f) for c, f in zip(header["is-cuda"], frames) ] return header, frames diff --git a/python/cudf/cudf/core/buffer.py b/python/cudf/cudf/core/buffer.py index 753be3b27e1..647e747e127 100644 --- a/python/cudf/cudf/core/buffer.py +++ b/python/cudf/cudf/core/buffer.py @@ -2,10 +2,19 @@ from __future__ import annotations -import functools -import operator +import math import pickle -from typing import Any, Dict, Tuple +from typing import ( + Any, + Dict, + List, + Mapping, + Protocol, + Sequence, + Tuple, + Union, + runtime_checkable, +) import numpy as np @@ -13,24 +22,129 @@ import cudf from cudf.core.abc import Serializable +from cudf.utils.string import format_bytes + +# Frame type for serialization and deserialization of `DeviceBufferLike` +Frame = Union[memoryview, "DeviceBufferLike"] + + +@runtime_checkable +class DeviceBufferLike(Protocol): + def __getitem__(self, key: slice) -> DeviceBufferLike: + """Create a new view of the buffer.""" + + @property + def size(self) -> int: + """Size of the buffer in bytes.""" + + @property + def nbytes(self) -> int: + """Size of the buffer in bytes.""" + + @property + def ptr(self) -> int: + """Device pointer to the start of the buffer.""" + + @property + def owner(self) -> Any: + """Object owning the memory of the buffer.""" + + @property + def __cuda_array_interface__(self) -> Mapping: + """Implementation of the CUDA Array Interface.""" + + def memoryview(self) -> memoryview: + """Read-only access to the buffer through host memory.""" + + def serialize(self) -> Tuple[dict, List[Frame]]: + """Serialize the buffer into header and frames. + + The frames can be a mixture of memoryview and device-buffer-like + objects. + + Returns + ------- + Tuple[Dict, List] + The first element of the returned tuple is a dict containing any + serializable metadata required to reconstruct the object. The + second element is a list containing the device buffers and + memoryviews of the object. + """ + + @classmethod + def deserialize( + cls, header: dict, frames: List[Frame] + ) -> DeviceBufferLike: + """Generate an buffer from a serialized representation. + + Parameters + ---------- + header : dict + The metadata required to reconstruct the object. + frames : list + The device-buffer-like and memoryview buffers that the object + should contain. + + Returns + ------- + DeviceBufferLike + A new object that implements DeviceBufferLike. + """ + + +def as_device_buffer_like(obj: Any) -> DeviceBufferLike: + """ + Factory function to wrap `obj` in a DeviceBufferLike object. + + If `obj` isn't device-buffer-like already, a new buffer that implements + DeviceBufferLike and points to the memory of `obj` is created. If `obj` + represents host memory, it is copied to a new `rmm.DeviceBuffer` device + allocation. Otherwise, the data of `obj` is **not** copied, instead the + new buffer keeps a reference to `obj` in order to retain the lifetime + of `obj`. + + Raises ValueError if the data of `obj` isn't C-contiguous. + + Parameters + ---------- + obj : buffer-like or array-like + An object that exposes either device or host memory through + `__array_interface__`, `__cuda_array_interface__`, or the + buffer protocol. If `obj` represents host memory, data will + be copied. + + Return + ------ + DeviceBufferLike + A device-buffer-like instance that represents the device memory + of `obj`. + """ + + if isinstance(obj, DeviceBufferLike): + return obj + return Buffer(obj) class Buffer(Serializable): """ - A Buffer represents a device memory allocation. + A Buffer represents device memory. + + Usually Buffers will be created using `as_device_buffer_like(obj)`, + which will make sure that `obj` is device-buffer-like and not a `Buffer` + necessarily. Parameters ---------- - data : Buffer, array_like, int - An array-like object or integer representing a - device or host pointer to pre-allocated memory. + data : int or buffer-like or array-like + An integer representing a pointer to device memory or a buffer-like + or array-like object. When not an integer, `size` and `owner` must + be None. size : int, optional - Size of memory allocation. Required if a pointer - is passed for `data`. + Size of device memory in bytes. Must be specified if `data` is an + integer. owner : object, optional - Python object to which the lifetime of the memory - allocation is tied. If provided, a reference to this - object is kept in this Buffer. + Python object to which the lifetime of the memory allocation is tied. + A reference to this object is kept in the returned Buffer. """ _ptr: int @@ -38,62 +152,61 @@ class Buffer(Serializable): _owner: object def __init__( - self, data: Any = None, size: int = None, owner: object = None + self, data: Union[int, Any], *, size: int = None, owner: object = None ): - if isinstance(data, Buffer): - self._ptr = data._ptr - self._size = data.size - self._owner = owner or data._owner - elif isinstance(data, rmm.DeviceBuffer): - self._ptr = data.ptr - self._size = data.size - self._owner = data - elif hasattr(data, "__array_interface__") or hasattr( - data, "__cuda_array_interface__" - ): - self._init_from_array_like(data, owner) - elif isinstance(data, memoryview): - self._init_from_array_like(np.asarray(data), owner) - elif isinstance(data, int): - if not isinstance(size, int): - raise TypeError("size must be integer") + if isinstance(data, int): + if size is None: + raise ValueError( + "size must be specified when `data` is an integer" + ) + if size < 0: + raise ValueError("size cannot be negative") self._ptr = data self._size = size self._owner = owner - elif data is None: - self._ptr = 0 - self._size = 0 - self._owner = None else: - try: - data = memoryview(data) - except TypeError: - raise TypeError("data must be Buffer, array-like or integer") - self._init_from_array_like(np.asarray(data), owner) + if size is not None or owner is not None: + raise ValueError( + "`size` and `owner` must be None when " + "`data` is a buffer-like object" + ) + + # `data` is a buffer-like object + buf: Any = data + if isinstance(buf, rmm.DeviceBuffer): + self._ptr = buf.ptr + self._size = buf.size + self._owner = buf + return + iface = getattr(buf, "__cuda_array_interface__", None) + if iface: + ptr, size = get_ptr_and_size(iface) + self._ptr = ptr + self._size = size + self._owner = buf + return + ptr, size = get_ptr_and_size(np.asarray(buf).__array_interface__) + buf = rmm.DeviceBuffer(ptr=ptr, size=size) + self._ptr = buf.ptr + self._size = buf.size + self._owner = buf + + def __getitem__(self, key: slice) -> Buffer: + if not isinstance(key, slice): + raise ValueError("index must be an slice") + start, stop, step = key.indices(self.size) + if step != 1: + raise ValueError("slice must be contiguous") + return self.__class__( + data=self.ptr + start, size=stop - start, owner=self.owner + ) - @classmethod - def from_buffer(cls, buffer: Buffer, size: int = None, offset: int = 0): - """ - Create a buffer from another buffer - - Parameters - ---------- - buffer : Buffer - The base buffer, which will also be set as the owner of - the memory allocation. - size : int, optional - Size of the memory allocation (default: `buffer.size`). - offset : int, optional - Start offset relative to `buffer.ptr`. - """ - - ret = cls() - ret._ptr = buffer._ptr + offset - ret._size = buffer.size if size is None else size - ret._owner = buffer - return ret + @property + def size(self) -> int: + return self._size - def __len__(self) -> int: + @property + def nbytes(self) -> int: return self._size @property @@ -101,12 +214,8 @@ def ptr(self) -> int: return self._ptr @property - def size(self) -> int: - return self._size - - @property - def nbytes(self) -> int: - return self._size + def owner(self) -> Any: + return self._owner @property def __cuda_array_interface__(self) -> dict: @@ -118,32 +227,10 @@ def __cuda_array_interface__(self) -> dict: "version": 0, } - def to_host_array(self): - data = np.empty((self.size,), "u1") - rmm._lib.device_buffer.copy_ptr_to_host(self.ptr, data) - return data - - def _init_from_array_like(self, data, owner): - - if hasattr(data, "__cuda_array_interface__"): - confirm_1d_contiguous(data.__cuda_array_interface__) - ptr, size = _buffer_data_from_array_interface( - data.__cuda_array_interface__ - ) - self._ptr = ptr - self._size = size - self._owner = owner or data - elif hasattr(data, "__array_interface__"): - confirm_1d_contiguous(data.__array_interface__) - ptr, size = _buffer_data_from_array_interface( - data.__array_interface__ - ) - dbuf = rmm.DeviceBuffer(ptr=ptr, size=size) - self._init_from_array_like(dbuf, owner) - else: - raise TypeError( - f"Cannot construct Buffer from {data.__class__.__name__}" - ) + def memoryview(self) -> memoryview: + host_buf = bytearray(self.size) + rmm._lib.device_buffer.copy_ptr_to_host(self.ptr, host_buf) + return memoryview(host_buf).toreadonly() def serialize(self) -> Tuple[dict, list]: header = {} # type: Dict[Any, Any] @@ -171,62 +258,68 @@ def deserialize(cls, header: dict, frames: list) -> Buffer: return buf - @classmethod - def empty(cls, size: int) -> Buffer: - return Buffer(rmm.DeviceBuffer(size=size)) + def __repr__(self) -> str: + return ( + f" Buffer: - """ - Create a new Buffer containing a copy of the data contained - in this Buffer. - """ - from rmm._lib.device_buffer import copy_device_to_ptr - out = Buffer.empty(size=self.size) - copy_device_to_ptr(self.ptr, out.ptr, self.size) - return out - - -def _buffer_data_from_array_interface(array_interface): - ptr = array_interface["data"][0] - if ptr is None: - ptr = 0 - itemsize = cudf.dtype(array_interface["typestr"]).itemsize - shape = ( - array_interface["shape"] if len(array_interface["shape"]) > 0 else (1,) - ) - size = functools.reduce(operator.mul, shape) - return ptr, size * itemsize +def is_c_contiguous( + shape: Sequence[int], strides: Sequence[int], itemsize: int +) -> bool: + """ + Determine if shape and strides are C-contiguous + Parameters + ---------- + shape : Sequence[int] + Number of elements in each dimension. + strides : Sequence[int] + The stride of each dimension in bytes. + itemsize : int + Size of an element in bytes. + + Return + ------ + bool + The boolean answer. + """ -def confirm_1d_contiguous(array_interface): - strides = array_interface["strides"] - shape = array_interface["shape"] - itemsize = cudf.dtype(array_interface["typestr"]).itemsize - typestr = array_interface["typestr"] - if typestr not in ("|i1", "|u1", "|b1"): - raise TypeError("Buffer data must be of uint8 type") - if not get_c_contiguity(shape, strides, itemsize): - raise ValueError("Buffer data must be 1D C-contiguous") + if any(dim == 0 for dim in shape): + return True + cumulative_stride = itemsize + for dim, stride in zip(reversed(shape), reversed(strides)): + if dim > 1 and stride != cumulative_stride: + return False + cumulative_stride *= dim + return True -def get_c_contiguity(shape, strides, itemsize): - """ - Determine if combination of array parameters represents a - c-contiguous array. +def get_ptr_and_size(array_interface: Mapping) -> Tuple[int, int]: """ - ndim = len(shape) - assert strides is None or ndim == len(strides) + Retrieve the pointer and size from an array interface. - if ndim == 0 or strides is None or (ndim == 1 and strides[0] == itemsize): - return True + Raises ValueError if array isn't C-contiguous. - # any dimension zero, trivial case - for dim in shape: - if dim == 0: - return True + Parameters + ---------- + array_interface : Mapping + The array interface metadata. + + Return + ------ + pointer : int + The pointer to device or host memory + size : int + The size in bytes + """ - for this_dim, this_stride in zip(shape, strides): - if this_stride != this_dim * itemsize: - return False - return True + shape = array_interface["shape"] or (1,) + strides = array_interface["strides"] + itemsize = cudf.dtype(array_interface["typestr"]).itemsize + if strides is None or is_c_contiguous(shape, strides, itemsize): + nelem = math.prod(shape) + ptr = array_interface["data"][0] or 0 + return ptr, nelem * itemsize + raise ValueError("Buffer data must be C-contiguous") diff --git a/python/cudf/cudf/core/column/categorical.py b/python/cudf/cudf/core/column/categorical.py index c04e2e45461..6762c0bc6c3 100644 --- a/python/cudf/cudf/core/column/categorical.py +++ b/python/cudf/cudf/core/column/categorical.py @@ -16,7 +16,7 @@ from cudf._lib.transform import bools_to_mask from cudf._typing import ColumnBinaryOperand, ColumnLike, Dtype, ScalarLike from cudf.api.types import is_categorical_dtype, is_interval_dtype -from cudf.core.buffer import Buffer +from cudf.core.buffer import DeviceBufferLike from cudf.core.column import column from cudf.core.column.methods import ColumnMethods from cudf.core.dtypes import CategoricalDtype @@ -610,7 +610,7 @@ class CategoricalColumn(column.ColumnBase): Parameters ---------- dtype : CategoricalDtype - mask : Buffer + mask : DeviceBufferLike The validity mask offset : int Data offset @@ -634,7 +634,7 @@ class CategoricalColumn(column.ColumnBase): def __init__( self, dtype: CategoricalDtype, - mask: Buffer = None, + mask: DeviceBufferLike = None, size: int = None, offset: int = 0, null_count: int = None, @@ -693,7 +693,7 @@ def _process_values_for_isin( rhs = cudf.core.column.as_column(values, dtype=self.dtype) return lhs, rhs - def set_base_mask(self, value: Optional[Buffer]): + def set_base_mask(self, value: Optional[DeviceBufferLike]): super().set_base_mask(value) self._codes = None @@ -705,16 +705,12 @@ def set_base_children(self, value: Tuple[ColumnBase, ...]): def children(self) -> Tuple[NumericalColumn]: if self._children is None: codes_column = self.base_children[0] - - buf = Buffer.from_buffer( - buffer=codes_column.base_data, - size=self.size * codes_column.dtype.itemsize, - offset=self.offset * codes_column.dtype.itemsize, - ) + start = self.offset * codes_column.dtype.itemsize + end = start + self.size * codes_column.dtype.itemsize codes_column = cast( cudf.core.column.NumericalColumn, column.build_column( - data=buf, + data=codes_column.base_data[start:end], dtype=codes_column.dtype, size=self.size, ), diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index 67d744e6690..2e75c6c2225 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -26,6 +26,8 @@ import pyarrow as pa from numba import cuda +import rmm + import cudf from cudf import _lib as libcudf from cudf._lib.column import Column @@ -61,7 +63,7 @@ is_struct_dtype, ) from cudf.core.abc import Serializable -from cudf.core.buffer import Buffer +from cudf.core.buffer import Buffer, DeviceBufferLike, as_device_buffer_like from cudf.core.dtypes import ( CategoricalDtype, IntervalDtype, @@ -351,7 +353,7 @@ def valid_count(self) -> int: return len(self) - self.null_count @property - def nullmask(self) -> Buffer: + def nullmask(self) -> DeviceBufferLike: """The gpu buffer for the null-mask""" if not self.nullable: raise ValueError("Column has no null mask") @@ -423,19 +425,9 @@ def view(self, dtype: Dtype) -> ColumnBase: # This assertion prevents mypy errors below. assert self.base_data is not None - # If the view spans all of `base_data`, we return `base_data`. - if ( - self.offset == 0 - and self.base_data.size == self.size * self.dtype.itemsize - ): - view_buf = self.base_data - else: - view_buf = Buffer.from_buffer( - buffer=self.base_data, - size=self.size * self.dtype.itemsize, - offset=self.offset * self.dtype.itemsize, - ) - return build_column(view_buf, dtype=dtype) + start = self.offset * self.dtype.itemsize + end = start + self.size * self.dtype.itemsize + return build_column(self.base_data[start:end], dtype=dtype) def element_indexing(self, index: int): """Default implementation for indexing to an element @@ -767,12 +759,12 @@ def _obtain_isin_result(self, rhs: ColumnBase) -> ColumnBase: res = res.drop_duplicates(subset="orig_order", ignore_index=True) return res._data["bool"].fillna(False) - def as_mask(self) -> Buffer: + def as_mask(self) -> DeviceBufferLike: """Convert booleans to bitmask Returns ------- - Buffer + DeviceBufferLike """ if self.has_nulls(): @@ -1277,7 +1269,11 @@ def column_empty( data = None children = ( build_column( - data=Buffer.empty(row_count * cudf.dtype("int32").itemsize), + data=as_device_buffer_like( + rmm.DeviceBuffer( + size=row_count * cudf.dtype("int32").itemsize + ) + ), dtype="int32", ), ) @@ -1286,12 +1282,18 @@ def column_empty( children = ( full(row_count + 1, 0, dtype="int32"), build_column( - data=Buffer.empty(row_count * cudf.dtype("int8").itemsize), + data=as_device_buffer_like( + rmm.DeviceBuffer( + size=row_count * cudf.dtype("int8").itemsize + ) + ), dtype="int8", ), ) else: - data = Buffer.empty(row_count * dtype.itemsize) + data = as_device_buffer_like( + rmm.DeviceBuffer(size=row_count * dtype.itemsize) + ) if masked: mask = create_null_mask(row_count, state=MaskState.ALL_NULL) @@ -1304,11 +1306,11 @@ def column_empty( def build_column( - data: Union[Buffer, None], + data: Union[DeviceBufferLike, None], dtype: Dtype, *, size: int = None, - mask: Buffer = None, + mask: DeviceBufferLike = None, offset: int = 0, null_count: int = None, children: Tuple[ColumnBase, ...] = (), @@ -1318,12 +1320,12 @@ def build_column( Parameters ---------- - data : Buffer + data : DeviceBufferLike The data buffer (can be None if constructing certain Column types like StringColumn, ListColumn, or CategoricalColumn) dtype The dtype associated with the Column to construct - mask : Buffer, optional + mask : DeviceBufferLike, optional The mask buffer size : int, optional offset : int, optional @@ -1468,7 +1470,7 @@ def build_column( def build_categorical_column( categories: ColumnBase, codes: ColumnBase, - mask: Buffer = None, + mask: DeviceBufferLike = None, size: int = None, offset: int = 0, null_count: int = None, @@ -1484,7 +1486,7 @@ def build_categorical_column( codes : Column Column of codes, the size of the resulting Column will be the size of `codes` - mask : Buffer + mask : DeviceBufferLike Null mask size : int, optional offset : int, optional @@ -1528,7 +1530,7 @@ def build_interval_column( Column of values representing the left of the interval right_col : Column Column of representing the right of the interval - mask : Buffer + mask : DeviceBufferLike Null mask size : int, optional offset : int, optional @@ -1559,7 +1561,7 @@ def build_interval_column( def build_list_column( indices: ColumnBase, elements: ColumnBase, - mask: Buffer = None, + mask: DeviceBufferLike = None, size: int = None, offset: int = 0, null_count: int = None, @@ -1573,7 +1575,7 @@ def build_list_column( Column of list indices elements : ColumnBase Column of list elements - mask: Buffer + mask: DeviceBufferLike Null mask size: int, optional offset: int, optional @@ -1597,7 +1599,7 @@ def build_struct_column( names: Sequence[str], children: Tuple[ColumnBase, ...], dtype: Optional[Dtype] = None, - mask: Buffer = None, + mask: DeviceBufferLike = None, size: int = None, offset: int = 0, null_count: int = None, @@ -1611,7 +1613,7 @@ def build_struct_column( Field names to map to children dtypes, must be strings. children : tuple - mask: Buffer + mask: DeviceBufferLike Null mask size: int, optional offset: int, optional @@ -1647,7 +1649,9 @@ def _make_copy_replacing_NaT_with_null(column): out_col = cudf._lib.replace.replace( column, build_column( - Buffer(np.array([na_value], dtype=column.dtype).view("|u1")), + as_device_buffer_like( + np.array([na_value], dtype=column.dtype).view("|u1") + ), dtype=column.dtype, ), null, @@ -1742,7 +1746,7 @@ def as_column( ): arbitrary = cupy.ascontiguousarray(arbitrary) - data = _data_from_cuda_array_interface_desc(arbitrary) + data = as_device_buffer_like(arbitrary) col = build_column(data, dtype=current_dtype, mask=mask) if dtype is not None: @@ -1890,7 +1894,7 @@ def as_column( if cast_dtype: arbitrary = arbitrary.astype(cudf.dtype("datetime64[s]")) - buffer = Buffer(arbitrary.view("|u1")) + buffer = as_device_buffer_like(arbitrary.view("|u1")) mask = None if nan_as_null is None or nan_as_null is True: data = build_column(buffer, dtype=arbitrary.dtype) @@ -1908,7 +1912,7 @@ def as_column( if cast_dtype: arbitrary = arbitrary.astype(cudf.dtype("timedelta64[s]")) - buffer = Buffer(arbitrary.view("|u1")) + buffer = as_device_buffer_like(arbitrary.view("|u1")) mask = None if nan_as_null is None or nan_as_null is True: data = build_column(buffer, dtype=arbitrary.dtype) @@ -2187,17 +2191,7 @@ def _construct_array( return arbitrary -def _data_from_cuda_array_interface_desc(obj) -> Buffer: - desc = obj.__cuda_array_interface__ - ptr = desc["data"][0] - nelem = desc["shape"][0] if len(desc["shape"]) > 0 else 1 - dtype = cudf.dtype(desc["typestr"]) - - data = Buffer(data=ptr, size=nelem * dtype.itemsize, owner=obj) - return data - - -def _mask_from_cuda_array_interface_desc(obj) -> Union[Buffer, None]: +def _mask_from_cuda_array_interface_desc(obj) -> Union[DeviceBufferLike, None]: desc = obj.__cuda_array_interface__ mask = desc.get("mask", None) diff --git a/python/cudf/cudf/core/column/datetime.py b/python/cudf/cudf/core/column/datetime.py index 375a19f5423..1419b14e8c6 100644 --- a/python/cudf/cudf/core/column/datetime.py +++ b/python/cudf/cudf/core/column/datetime.py @@ -23,7 +23,7 @@ ) from cudf.api.types import is_datetime64_dtype, is_scalar, is_timedelta64_dtype from cudf.core._compat import PANDAS_GE_120 -from cudf.core.buffer import Buffer +from cudf.core.buffer import DeviceBufferLike from cudf.core.column import ColumnBase, as_column, column, string from cudf.core.column.timedelta import _unit_to_nanoseconds_conversion from cudf.utils.utils import _fillna_natwise @@ -98,11 +98,11 @@ class DatetimeColumn(column.ColumnBase): Parameters ---------- - data : Buffer + data : DeviceBufferLike The datetime values dtype : np.dtype The data type - mask : Buffer; optional + mask : DeviceBufferLike; optional The validity mask """ @@ -121,9 +121,9 @@ class DatetimeColumn(column.ColumnBase): def __init__( self, - data: Buffer, + data: DeviceBufferLike, dtype: DtypeObj, - mask: Buffer = None, + mask: DeviceBufferLike = None, size: int = None, # TODO: make non-optional offset: int = 0, null_count: int = None, @@ -131,7 +131,9 @@ def __init__( dtype = cudf.dtype(dtype) if data.size % dtype.itemsize: - raise ValueError("Buffer size must be divisible by element size") + raise ValueError( + "DeviceBufferLike size must be divisible by element size" + ) if size is None: size = data.size // dtype.itemsize size = size - offset diff --git a/python/cudf/cudf/core/column/decimal.py b/python/cudf/cudf/core/column/decimal.py index 69009106d15..e03802e6d8c 100644 --- a/python/cudf/cudf/core/column/decimal.py +++ b/python/cudf/cudf/core/column/decimal.py @@ -16,7 +16,7 @@ ) from cudf._typing import ColumnBinaryOperand, Dtype from cudf.api.types import is_integer_dtype, is_scalar -from cudf.core.buffer import Buffer +from cudf.core.buffer import as_device_buffer_like from cudf.core.column import ColumnBase, as_column from cudf.core.dtypes import ( Decimal32Dtype, @@ -203,7 +203,7 @@ def from_arrow(cls, data: pa.Array): data_128 = cp.array(np.frombuffer(data.buffers()[1]).view("int32")) data_32 = data_128[::4].copy() return cls( - data=Buffer(data_32.view("uint8")), + data=as_device_buffer_like(data_32.view("uint8")), size=len(data), dtype=dtype, offset=data.offset, @@ -211,7 +211,7 @@ def from_arrow(cls, data: pa.Array): ) def to_arrow(self): - data_buf_32 = self.base_data.to_host_array().view("int32") + data_buf_32 = np.array(self.base_data.memoryview()).view("int32") data_buf_128 = np.empty(len(data_buf_32) * 4, dtype="int32") # use striding to set the first 32 bits of each 128-bit chunk: @@ -231,7 +231,7 @@ def to_arrow(self): mask_buf = ( self.base_mask if self.base_mask is None - else pa.py_buffer(self.base_mask.to_host_array()) + else pa.py_buffer(self.base_mask.memoryview()) ) return pa.Array.from_buffers( type=self.dtype.to_arrow(), @@ -290,7 +290,7 @@ def from_arrow(cls, data: pa.Array): data_128 = cp.array(np.frombuffer(data.buffers()[1]).view("int64")) data_64 = data_128[::2].copy() return cls( - data=Buffer(data_64.view("uint8")), + data=as_device_buffer_like(data_64.view("uint8")), size=len(data), dtype=dtype, offset=data.offset, @@ -298,7 +298,7 @@ def from_arrow(cls, data: pa.Array): ) def to_arrow(self): - data_buf_64 = self.base_data.to_host_array().view("int64") + data_buf_64 = np.array(self.base_data.memoryview()).view("int64") data_buf_128 = np.empty(len(data_buf_64) * 2, dtype="int64") # use striding to set the first 64 bits of each 128-bit chunk: @@ -312,7 +312,7 @@ def to_arrow(self): mask_buf = ( self.base_mask if self.base_mask is None - else pa.py_buffer(self.base_mask.to_host_array()) + else pa.py_buffer(self.base_mask.memoryview()) ) return pa.Array.from_buffers( type=self.dtype.to_arrow(), diff --git a/python/cudf/cudf/core/column/lists.py b/python/cudf/cudf/core/column/lists.py index 32a71a31b83..0d5b351f69e 100644 --- a/python/cudf/cudf/core/column/lists.py +++ b/python/cudf/cudf/core/column/lists.py @@ -147,8 +147,7 @@ def to_arrow(self): pa_type = pa.list_(elements.type) if self.nullable: - nbuf = self.mask.to_host_array().view("int8") - nbuf = pa.py_buffer(nbuf) + nbuf = pa.py_buffer(self.mask.memoryview()) buffers = (nbuf, offsets.buffers()[1]) else: buffers = offsets.buffers() diff --git a/python/cudf/cudf/core/column/numerical.py b/python/cudf/cudf/core/column/numerical.py index 0529c614393..4b74dde129c 100644 --- a/python/cudf/cudf/core/column/numerical.py +++ b/python/cudf/cudf/core/column/numerical.py @@ -35,7 +35,7 @@ is_integer_dtype, is_number, ) -from cudf.core.buffer import Buffer +from cudf.core.buffer import DeviceBufferLike, as_device_buffer_like from cudf.core.column import ( ColumnBase, as_column, @@ -65,10 +65,10 @@ class NumericalColumn(NumericalBaseColumn): Parameters ---------- - data : Buffer + data : DeviceBufferLike dtype : np.dtype - The dtype associated with the data Buffer - mask : Buffer, optional + The dtype associated with the data DeviceBufferLike + mask : DeviceBufferLike, optional """ _nan_count: Optional[int] @@ -76,9 +76,9 @@ class NumericalColumn(NumericalBaseColumn): def __init__( self, - data: Buffer, + data: DeviceBufferLike, dtype: DtypeObj, - mask: Buffer = None, + mask: DeviceBufferLike = None, size: int = None, # TODO: make this non-optional offset: int = 0, null_count: int = None, @@ -86,7 +86,9 @@ def __init__( dtype = cudf.dtype(dtype) if data.size % dtype.itemsize: - raise ValueError("Buffer size must be divisible by element size") + raise ValueError( + "DeviceBufferLike size must be divisible by element size" + ) if size is None: size = (data.size // dtype.itemsize) - offset self._nan_count = None @@ -266,7 +268,7 @@ def normalize_binop_value( else: ary = full(len(self), other, dtype=other_dtype) return column.build_column( - data=Buffer(ary), + data=as_device_buffer_like(ary), dtype=ary.dtype, mask=self.mask, ) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index d591008fa9a..726985fa091 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -32,7 +32,7 @@ is_scalar, is_string_dtype, ) -from cudf.core.buffer import Buffer +from cudf.core.buffer import DeviceBufferLike from cudf.core.column import column, datetime from cudf.core.column.methods import ColumnMethods from cudf.utils.docutils import copy_docstring @@ -5051,7 +5051,7 @@ class StringColumn(column.ColumnBase): Parameters ---------- - mask : Buffer + mask : DeviceBufferLike The validity mask offset : int Data offset @@ -5085,7 +5085,7 @@ class StringColumn(column.ColumnBase): def __init__( self, - mask: Buffer = None, + mask: DeviceBufferLike = None, size: int = None, # TODO: make non-optional offset: int = 0, null_count: int = None, diff --git a/python/cudf/cudf/core/column/struct.py b/python/cudf/cudf/core/column/struct.py index a9b6d4cad12..67ff3e48dbd 100644 --- a/python/cudf/cudf/core/column/struct.py +++ b/python/cudf/cudf/core/column/struct.py @@ -47,9 +47,7 @@ def to_arrow(self): ) if self.nullable: - nbuf = self.mask.to_host_array().view("int8") - nbuf = pa.py_buffer(nbuf) - buffers = (nbuf,) + buffers = (pa.py_buffer(self.mask.memoryview()),) else: buffers = (None,) diff --git a/python/cudf/cudf/core/column/timedelta.py b/python/cudf/cudf/core/column/timedelta.py index 3dc923e7ded..e6d688014fa 100644 --- a/python/cudf/cudf/core/column/timedelta.py +++ b/python/cudf/cudf/core/column/timedelta.py @@ -13,7 +13,7 @@ from cudf import _lib as libcudf from cudf._typing import ColumnBinaryOperand, DatetimeLikeScalar, Dtype from cudf.api.types import is_scalar, is_timedelta64_dtype -from cudf.core.buffer import Buffer +from cudf.core.buffer import DeviceBufferLike from cudf.core.column import ColumnBase, column, string from cudf.utils.dtypes import np_to_pa_dtype from cudf.utils.utils import _fillna_natwise @@ -40,13 +40,13 @@ class TimeDeltaColumn(ColumnBase): """ Parameters ---------- - data : Buffer + data : DeviceBufferLike The Timedelta values dtype : np.dtype The data type size : int Size of memory allocation. - mask : Buffer; optional + mask : DeviceBufferLike; optional The validity mask offset : int Data offset @@ -78,17 +78,19 @@ class TimeDeltaColumn(ColumnBase): def __init__( self, - data: Buffer, + data: DeviceBufferLike, dtype: Dtype, size: int = None, # TODO: make non-optional - mask: Buffer = None, + mask: DeviceBufferLike = None, offset: int = 0, null_count: int = None, ): dtype = cudf.dtype(dtype) if data.size % dtype.itemsize: - raise ValueError("Buffer size must be divisible by element size") + raise ValueError( + "DeviceBufferLike size must be divisible by element size" + ) if size is None: size = data.size // dtype.itemsize size = size - offset diff --git a/python/cudf/cudf/core/df_protocol.py b/python/cudf/cudf/core/df_protocol.py index f4ce658bff3..86b2d83ceec 100644 --- a/python/cudf/cudf/core/df_protocol.py +++ b/python/cudf/cudf/core/df_protocol.py @@ -18,7 +18,7 @@ from numba.cuda import as_cuda_array import cudf -from cudf.core.buffer import Buffer +from cudf.core.buffer import Buffer, DeviceBufferLike from cudf.core.column import as_column, build_categorical_column, build_column # Implementation of interchange protocol classes @@ -64,12 +64,12 @@ class _CuDFBuffer: def __init__( self, - buf: cudf.core.buffer.Buffer, + buf: DeviceBufferLike, dtype: np.dtype, allow_copy: bool = True, ) -> None: """ - Use cudf.core.buffer.Buffer object. + Use DeviceBufferLike object. """ # Store the cudf buffer where the data resides as a private # attribute, so we can use it to retrieve the public attributes @@ -80,9 +80,9 @@ def __init__( @property def bufsize(self) -> int: """ - Buffer size in bytes. + DeviceBufferLike size in bytes. """ - return self._buf.nbytes + return self._buf.size @property def ptr(self) -> int: @@ -622,11 +622,11 @@ def __dataframe__( Notes ----- -- Interpreting a raw pointer (as in ``Buffer.ptr``) is annoying and unsafe to - do in pure Python. It's more general but definitely less friendly than - having ``to_arrow`` and ``to_numpy`` methods. So for the buffers which lack - ``__dlpack__`` (e.g., because the column dtype isn't supported by DLPack), - this is worth looking at again. +- Interpreting a raw pointer (as in ``DeviceBufferLike.ptr``) is annoying and + unsafe to do in pure Python. It's more general but definitely less friendly + than having ``to_arrow`` and ``to_numpy`` methods. So for the buffers which + lack ``__dlpack__`` (e.g., because the column dtype isn't supported by + DLPack), this is worth looking at again. """ @@ -716,7 +716,7 @@ def _protocol_to_cudf_column_numeric( _dbuffer, _ddtype = buffers["data"] _check_buffer_is_on_gpu(_dbuffer) cudfcol_num = build_column( - Buffer(_dbuffer.ptr, _dbuffer.bufsize), + Buffer(data=_dbuffer.ptr, size=_dbuffer.bufsize, owner=None), protocol_dtype_to_cupy_dtype(_ddtype), ) return _set_missing_values(col, cudfcol_num), buffers @@ -746,7 +746,10 @@ def _set_missing_values( valid_mask = protocol_col.get_buffers()["validity"] if valid_mask is not None: bitmask = cp.asarray( - Buffer(valid_mask[0].ptr, valid_mask[0].bufsize), cp.bool8 + Buffer( + data=valid_mask[0].ptr, size=valid_mask[0].bufsize, owner=None + ), + cp.bool8, ) cudf_col[~bitmask] = None @@ -784,7 +787,8 @@ def _protocol_to_cudf_column_categorical( _check_buffer_is_on_gpu(codes_buffer) cdtype = protocol_dtype_to_cupy_dtype(codes_dtype) codes = build_column( - Buffer(codes_buffer.ptr, codes_buffer.bufsize), cdtype + Buffer(data=codes_buffer.ptr, size=codes_buffer.bufsize, owner=None), + cdtype, ) cudfcol = build_categorical_column( @@ -815,7 +819,7 @@ def _protocol_to_cudf_column_string( data_buffer, data_dtype = buffers["data"] _check_buffer_is_on_gpu(data_buffer) encoded_string = build_column( - Buffer(data_buffer.ptr, data_buffer.bufsize), + Buffer(data=data_buffer.ptr, size=data_buffer.bufsize, owner=None), protocol_dtype_to_cupy_dtype(data_dtype), ) @@ -825,7 +829,7 @@ def _protocol_to_cudf_column_string( offset_buffer, offset_dtype = buffers["offsets"] _check_buffer_is_on_gpu(offset_buffer) offsets = build_column( - Buffer(offset_buffer.ptr, offset_buffer.bufsize), + Buffer(data=offset_buffer.ptr, size=offset_buffer.bufsize, owner=None), protocol_dtype_to_cupy_dtype(offset_dtype), ) diff --git a/python/cudf/cudf/core/dtypes.py b/python/cudf/cudf/core/dtypes.py index 070837c127b..62717a3c5a8 100644 --- a/python/cudf/cudf/core/dtypes.py +++ b/python/cudf/cudf/core/dtypes.py @@ -20,7 +20,7 @@ from cudf._typing import Dtype from cudf.core._compat import PANDAS_GE_130 from cudf.core.abc import Serializable -from cudf.core.buffer import Buffer +from cudf.core.buffer import DeviceBufferLike def dtype(arbitrary): @@ -370,7 +370,7 @@ def serialize(self) -> Tuple[dict, list]: header: Dict[str, Any] = {} header["type-serialized"] = pickle.dumps(type(self)) - frames: List[Buffer] = [] + frames: List[DeviceBufferLike] = [] fields: Dict[str, Union[bytes, Tuple[Any, Tuple[int, int]]]] = {} diff --git a/python/cudf/cudf/core/index.py b/python/cudf/cudf/core/index.py index 9cbaa552e48..0fdcabc0e8b 100644 --- a/python/cudf/cudf/core/index.py +++ b/python/cudf/cudf/core/index.py @@ -2796,7 +2796,7 @@ def as_index(arbitrary, nan_as_null=None, **kwargs) -> BaseIndex: Currently supported inputs are: * ``Column`` - * ``Buffer`` + * ``DeviceBufferLike`` * ``Series`` * ``Index`` * numba device array diff --git a/python/cudf/cudf/core/series.py b/python/cudf/cudf/core/series.py index 7990abbb89a..ca7919b5c40 100644 --- a/python/cudf/cudf/core/series.py +++ b/python/cudf/cudf/core/series.py @@ -1830,8 +1830,8 @@ def data(self): 3 4 dtype: int64 >>> series.data - - >>> series.data.to_host_array() + + >>> np.array(series.data.memoryview()) array([1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0], dtype=uint8) """ # noqa: E501 diff --git a/python/cudf/cudf/tests/test_buffer.py b/python/cudf/cudf/tests/test_buffer.py index 4600d932c6f..16ba18581ed 100644 --- a/python/cudf/cudf/tests/test_buffer.py +++ b/python/cudf/cudf/tests/test_buffer.py @@ -1,8 +1,10 @@ +# Copyright (c) 2020-2022, NVIDIA CORPORATION. +from typing import Callable + import cupy as cp import pytest -from cupy.testing import assert_array_equal -from cudf.core.buffer import Buffer +from cudf.core.buffer import Buffer, DeviceBufferLike, as_device_buffer_like arr_len = 10 @@ -21,38 +23,82 @@ def test_buffer_from_cuda_iface_contiguous(data): data, expect_success = data if expect_success: - buf = Buffer(data=data.view("|u1"), size=data.size) # noqa: F841 + as_device_buffer_like(data.view("|u1")) else: with pytest.raises(ValueError): - buf = Buffer(data=data.view("|u1"), size=data.size) # noqa: F841 + as_device_buffer_like(data.view("|u1")) @pytest.mark.parametrize( "data", [ - (cp.zeros(arr_len)), - (cp.zeros((1, arr_len))), - (cp.zeros((1, arr_len, 1))), - (cp.zeros((arr_len, arr_len))), - (cp.zeros((arr_len, arr_len)).reshape(arr_len * arr_len)), + cp.arange(arr_len), + cp.arange(arr_len).reshape(1, arr_len), + cp.arange(arr_len).reshape(1, arr_len, 1), + cp.arange(arr_len**2).reshape(arr_len, arr_len), ], ) @pytest.mark.parametrize("dtype", ["uint8", "int8", "float32", "int32"]) def test_buffer_from_cuda_iface_dtype(data, dtype): data = data.astype(dtype) - if dtype not in ("uint8", "int8"): - with pytest.raises( - TypeError, match="Buffer data must be of uint8 type" - ): - buf = Buffer(data=data, size=data.size) # noqa: F841 - - -@pytest.mark.parametrize("size", [0, 1, 10, 100, 1000, 10_000]) -def test_buffer_copy(size): - data = cp.random.randint(low=0, high=100, size=size, dtype="u1") - buf = Buffer(data=data) - got = buf.copy() - assert got.size == buf.size - if size > 0: - assert got.ptr != buf.ptr - assert_array_equal(cp.asarray(buf), cp.asarray(got)) + buf = as_device_buffer_like(data) + ary = cp.array(buf).flatten().view("uint8") + assert (ary == buf).all() + + +@pytest.mark.parametrize("creator", [Buffer, as_device_buffer_like]) +def test_buffer_creation_from_any(creator: Callable[[object], Buffer]): + ary = cp.arange(arr_len) + b = creator(ary) + assert isinstance(b, DeviceBufferLike) + assert ary.__cuda_array_interface__["data"][0] == b.ptr + assert ary.nbytes == b.size + + with pytest.raises( + ValueError, match="size must be specified when `data` is an integer" + ): + Buffer(42) + + +@pytest.mark.parametrize( + "size,expect", [(10, "10B"), (2**10 + 500, "1.49KiB"), (2**20, "1MiB")] +) +def test_buffer_repr(size, expect): + ary = cp.arange(size, dtype="uint8") + buf = as_device_buffer_like(ary) + assert f"size={expect}" in repr(buf) + + +@pytest.mark.parametrize( + "idx", + [ + slice(0, 0), + slice(0, 1), + slice(-2, -1), + slice(0, arr_len), + slice(2, 3), + slice(2, -1), + ], +) +def test_buffer_slice(idx): + ary = cp.arange(arr_len, dtype="uint8") + buf = as_device_buffer_like(ary) + assert (ary[idx] == buf[idx]).all() + + +@pytest.mark.parametrize( + "idx, err_msg", + [ + (1, "index must be an slice"), + (slice(3, 2), "size cannot be negative"), + (slice(1, 2, 2), "slice must be contiguous"), + (slice(1, 2, -1), "slice must be contiguous"), + (slice(3, 2, -1), "slice must be contiguous"), + ], +) +def test_buffer_slice_fail(idx, err_msg): + ary = cp.arange(arr_len, dtype="uint8") + buf = as_device_buffer_like(ary) + + with pytest.raises(ValueError, match=err_msg): + buf[idx] diff --git a/python/cudf/cudf/tests/test_column.py b/python/cudf/cudf/tests/test_column.py index 2e5b121844a..4e2a26d31bd 100644 --- a/python/cudf/cudf/tests/test_column.py +++ b/python/cudf/cudf/tests/test_column.py @@ -406,7 +406,7 @@ def test_column_view_string_slice(slc): ) def test_as_column_buffer(data, expected): actual_column = cudf.core.column.as_column( - cudf.core.buffer.Buffer(data), dtype=data.dtype + cudf.core.buffer.as_device_buffer_like(data), dtype=data.dtype ) assert_eq(cudf.Series(actual_column), cudf.Series(expected)) diff --git a/python/cudf/cudf/tests/test_cuda_array_interface.py b/python/cudf/cudf/tests/test_cuda_array_interface.py index c4eacac2017..2a62a289747 100644 --- a/python/cudf/cudf/tests/test_cuda_array_interface.py +++ b/python/cudf/cudf/tests/test_cuda_array_interface.py @@ -179,9 +179,9 @@ def test_cuda_array_interface_pytorch(): got = cudf.Series(tensor) assert_eq(got, series) - from cudf.core.buffer import Buffer - - buffer = Buffer(cupy.ones(10, dtype=np.bool_)) + buffer = cudf.core.buffer.as_device_buffer_like( + cupy.ones(10, dtype=np.bool_) + ) tensor = torch.tensor(buffer) got = cudf.Series(tensor, dtype=np.bool_) diff --git a/python/cudf/cudf/tests/test_df_protocol.py b/python/cudf/cudf/tests/test_df_protocol.py index 21e18470b2f..c88b6ac9228 100644 --- a/python/cudf/cudf/tests/test_df_protocol.py +++ b/python/cudf/cudf/tests/test_df_protocol.py @@ -25,7 +25,8 @@ def assert_buffer_equal(buffer_and_dtype: Tuple[_CuDFBuffer, Any], cudfcol): device_id = cp.asarray(cudfcol.data).device.id assert buf.__dlpack_device__() == (2, device_id) col_from_buf = build_column( - Buffer(buf.ptr, buf.bufsize), protocol_dtype_to_cupy_dtype(dtype) + Buffer(data=buf.ptr, size=buf.bufsize, owner=None), + protocol_dtype_to_cupy_dtype(dtype), ) # check that non null values are the equals as nulls are represented # by sentinel values in the buffer. diff --git a/python/cudf/cudf/tests/test_pickling.py b/python/cudf/cudf/tests/test_pickling.py index 35ebd1b77c7..1427a214a72 100644 --- a/python/cudf/cudf/tests/test_pickling.py +++ b/python/cudf/cudf/tests/test_pickling.py @@ -7,7 +7,7 @@ import pytest from cudf import DataFrame, GenericIndex, RangeIndex, Series -from cudf.core.buffer import Buffer +from cudf.core.buffer import as_device_buffer_like from cudf.testing._utils import assert_eq if sys.version_info < (3, 8): @@ -97,7 +97,7 @@ def test_pickle_index(): def test_pickle_buffer(): arr = np.arange(10).view("|u1") - buf = Buffer(arr) + buf = as_device_buffer_like(arr) assert buf.size == arr.nbytes pickled = pickle.dumps(buf) unpacked = pickle.loads(pickled) diff --git a/python/cudf/cudf/tests/test_serialize.py b/python/cudf/cudf/tests/test_serialize.py index b7d679e95d5..61eee6bba43 100644 --- a/python/cudf/cudf/tests/test_serialize.py +++ b/python/cudf/cudf/tests/test_serialize.py @@ -356,8 +356,8 @@ def test_serialize_sliced_string(): # because both should be identical for i in range(3): assert_eq( - serialized_gd_series[1][i].to_host_array(), - serialized_sliced[1][i].to_host_array(), + serialized_gd_series[1][i].memoryview(), + serialized_sliced[1][i].memoryview(), ) recreated = cudf.Series.deserialize(*sliced.serialize()) diff --git a/python/cudf/cudf/tests/test_testing.py b/python/cudf/cudf/tests/test_testing.py index e5c78b6ea9a..60f01d567ef 100644 --- a/python/cudf/cudf/tests/test_testing.py +++ b/python/cudf/cudf/tests/test_testing.py @@ -429,7 +429,7 @@ def test_assert_column_memory_slice(arrow_arrays): def test_assert_column_memory_basic_same(arrow_arrays): data = cudf.core.column.ColumnBase.from_arrow(arrow_arrays) - buf = cudf.core.buffer.Buffer(data=data.base_data, owner=data) + buf = cudf.core.buffer.as_device_buffer_like(data.base_data) left = cudf.core.column.build_column(buf, dtype=np.int32) right = cudf.core.column.build_column(buf, dtype=np.int32) diff --git a/python/cudf/cudf/utils/string.py b/python/cudf/cudf/utils/string.py new file mode 100644 index 00000000000..9c02d1d6b34 --- /dev/null +++ b/python/cudf/cudf/utils/string.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. + + +def format_bytes(nbytes: int) -> str: + """Format `nbytes` to a human readable string""" + n = float(nbytes) + for unit in ["B", "KiB", "MiB", "GiB", "TiB"]: + if abs(n) < 1024: + if n.is_integer(): + return f"{int(n)}{unit}" + return f"{n:.2f}{unit}" + n /= 1024 + return f"{n:.2f} PiB" diff --git a/python/cudf/cudf/utils/utils.py b/python/cudf/cudf/utils/utils.py index 29a33556efc..0e080c6f078 100644 --- a/python/cudf/cudf/utils/utils.py +++ b/python/cudf/cudf/utils/utils.py @@ -14,7 +14,7 @@ import cudf from cudf.core import column -from cudf.core.buffer import Buffer +from cudf.core.buffer import as_device_buffer_like # The size of the mask in bytes mask_dtype = cudf.dtype(np.int32) @@ -277,8 +277,8 @@ def pa_mask_buffer_to_mask(mask_buf, size): if mask_buf.size < mask_size: dbuf = rmm.DeviceBuffer(size=mask_size) dbuf.copy_from_host(np.asarray(mask_buf).view("u1")) - return Buffer(dbuf) - return Buffer(mask_buf) + return as_device_buffer_like(dbuf) + return as_device_buffer_like(mask_buf) def _isnat(val): From de06ed92dd80a7a8191d8b19ea955cc9471f3bcf Mon Sep 17 00:00:00 2001 From: Nghia Truong Date: Thu, 11 Aug 2022 08:29:35 -0700 Subject: [PATCH 19/58] Fix cmake error after upgrading to Arrow 9 (#11513) After upgrading to Arrow 9 (https://github.com/rapidsai/cudf/pull/11507), some systems experience a cmake issue: ``` cudf/cpp/build/_deps/arrow-src/cpp/CMakeLists.txt:864: error: The dependency target "xsimd" of target "arrow_dependencies" does not exist. ``` This may be due to the configurations for Arrow is looking for a local installation of `xsimd`, which does not exist or the installation path is not provided. This PRs adds an option to cudf cmake, specifying that Arrow should handle `xsimd` by itself. Authors: - Nghia Truong (https://github.com/ttnghia) Approvers: - Bradley Dice (https://github.com/bdice) - Robert Maynard (https://github.com/robertmaynard) URL: https://github.com/rapidsai/cudf/pull/11513 --- cpp/cmake/thirdparty/get_arrow.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/cmake/thirdparty/get_arrow.cmake b/cpp/cmake/thirdparty/get_arrow.cmake index 82525dab6cd..10241b8eede 100644 --- a/cpp/cmake/thirdparty/get_arrow.cmake +++ b/cpp/cmake/thirdparty/get_arrow.cmake @@ -121,6 +121,7 @@ function(find_and_configure_arrow VERSION BUILD_STATIC ENABLE_S3 ENABLE_ORC ENAB "ARROW_GRPC_USE_SHARED ${ARROW_BUILD_SHARED}" "ARROW_PROTOBUF_USE_SHARED ${ARROW_BUILD_SHARED}" "ARROW_ZSTD_USE_SHARED ${ARROW_BUILD_SHARED}" + "xsimd_SOURCE AUTO" ) set(ARROW_FOUND TRUE) From 80bce299e016412c8e8c2dc1fc54bd1dcdb126ef Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 11 Aug 2022 13:05:09 -0500 Subject: [PATCH 20/58] Fix reverse binary operators acting on a host value and cudf.Scalar (#11512) This PR resolves #11225. It fixes binary operator dispatch for reverse ops like `__radd__` acting on host scalars and `cudf.Scalar` objects in expressions like `1 + cudf.Scalar(3)`, which previously threw an error. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - https://github.com/brandon-b-miller - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/11512 --- python/cudf/cudf/tests/test_binops.py | 18 +++++++++++++++--- python/cudf/cudf/utils/dtypes.py | 7 +++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/python/cudf/cudf/tests/test_binops.py b/python/cudf/cudf/tests/test_binops.py index e92e608cc67..c1a08e507b3 100644 --- a/python/cudf/cudf/tests/test_binops.py +++ b/python/cudf/cudf/tests/test_binops.py @@ -417,7 +417,7 @@ def test_series_cmpop_mixed_dtype(cmpop, lhs_dtype, rhs_dtype, obj_class): @pytest.mark.parametrize( "func, dtype", list(product(_reflected_ops, utils.NUMERIC_TYPES)) ) -def test_reflected_ops_scalar(func, dtype, obj_class): +def test_series_reflected_ops_scalar(func, dtype, obj_class): # create random series np.random.seed(12) random_series = utils.gen_rand(dtype, 100, low=10) @@ -442,6 +442,19 @@ def test_reflected_ops_scalar(func, dtype, obj_class): np.testing.assert_allclose(ps_result, gs_result.to_numpy()) +@pytest.mark.parametrize( + "func, dtype", list(product(_reflected_ops, utils.NUMERIC_TYPES)) +) +def test_cudf_scalar_reflected_ops_scalar(func, dtype): + value = 42 + scalar = cudf.Scalar(42) + + expected = func(value) + actual = func(scalar).value + + assert np.isclose(expected, actual) + + _cudf_scalar_reflected_ops = [ lambda x: cudf.Scalar(1) + x, lambda x: cudf.Scalar(2) * x, @@ -483,7 +496,7 @@ def test_reflected_ops_scalar(func, dtype, obj_class): ) ), ) -def test_reflected_ops_cudf_scalar(funcs, dtype, obj_class): +def test_series_reflected_ops_cudf_scalar(funcs, dtype, obj_class): cpu_func, gpu_func = funcs # create random series @@ -3052,7 +3065,6 @@ def test_binop_integer_power_int_series(): utils.assert_eq(expected, got) -@pytest.mark.xfail(reason="Reverse binops fail for scalar. See GH: #11225.") def test_binop_integer_power_int_scalar(): # GH: #10178 base = 3 diff --git a/python/cudf/cudf/utils/dtypes.py b/python/cudf/cudf/utils/dtypes.py index 9a13c46885a..29d2337e9d6 100644 --- a/python/cudf/cudf/utils/dtypes.py +++ b/python/cudf/cudf/utils/dtypes.py @@ -469,12 +469,19 @@ def get_allowed_combinations_for_operator(dtype_l, dtype_r, op): to_numpy_ops = { "__add__": _ADD_TYPES, + "__radd__": _ADD_TYPES, "__sub__": _SUB_TYPES, + "__rsub__": _SUB_TYPES, "__mul__": _MUL_TYPES, + "__rmul__": _MUL_TYPES, "__floordiv__": _FLOORDIV_TYPES, + "__rfloordiv__": _FLOORDIV_TYPES, "__truediv__": _TRUEDIV_TYPES, + "__rtruediv__": _TRUEDIV_TYPES, "__mod__": _MOD_TYPES, + "__rmod__": _MOD_TYPES, "__pow__": _POW_TYPES, + "__rpow__": _POW_TYPES, } allowed = to_numpy_ops.get(op, op) From e66ed15fbf974e84372ccbaf44c7b0e225eda020 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 11 Aug 2022 13:05:41 -0500 Subject: [PATCH 21/58] Remove deprecated expand parameter from str.findall. (#11030) This PR follows up on #10459, #10491 to remove the deprecated `expand` parameter and update the behavior of `str.findall` to always returns a list column of results. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/11030 --- python/cudf/cudf/core/column/string.py | 17 +++----------- python/cudf/cudf/tests/test_string.py | 31 +++----------------------- 2 files changed, 6 insertions(+), 42 deletions(-) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index 726985fa091..a69a422db7f 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -3485,9 +3485,7 @@ def count(self, pat: str, flags: int = 0) -> SeriesOrIndex: libstrings.count_re(self._column, pat, flags) ) - def findall( - self, pat: str, flags: int = 0, expand: bool = True - ) -> SeriesOrIndex: + def findall(self, pat: str, flags: int = 0) -> SeriesOrIndex: """ Find all occurrences of pattern or regular expression in the Series/Index. @@ -3559,17 +3557,8 @@ def findall( "unsupported value for `flags` parameter" ) - if expand: - warnings.warn( - "The expand parameter is deprecated and will be removed in a " - "future version. Set expand=False to match future behavior.", - FutureWarning, - ) - data, _ = libstrings.findall(self._column, pat, flags) - return self._return_or_inplace(data, expand=expand) - else: - data = libstrings.findall_record(self._column, pat, flags) - return self._return_or_inplace(data, expand=expand) + data = libstrings.findall_record(self._column, pat, flags) + return self._return_or_inplace(data) def isempty(self) -> SeriesOrIndex: """ diff --git a/python/cudf/cudf/tests/test_string.py b/python/cudf/cudf/tests/test_string.py index 47854368199..21267544cf2 100644 --- a/python/cudf/cudf/tests/test_string.py +++ b/python/cudf/cudf/tests/test_string.py @@ -1879,34 +1879,9 @@ def test_string_findall(pat, flags): ps = pd.Series(test_data) gs = cudf.Series(test_data) - # TODO: Update this test to remove "expand=False" when removing the expand - # parameter from Series.str.findall. - assert_eq( - ps.str.findall(pat, flags), gs.str.findall(pat, flags, expand=False) - ) - - -@pytest.mark.filterwarnings("ignore:The expand parameter is deprecated") -def test_string_findall_expand_True(): - # TODO: Remove this test when removing the expand parameter from - # Series.str.findall. - test_data = ["Lion", "Monkey", "Rabbit", "Don\nkey"] - ps = pd.Series(test_data) - gs = cudf.Series(test_data) - - assert_eq(ps.str.findall("Monkey")[1][0], gs.str.findall("Monkey")[0][1]) - assert_eq(ps.str.findall("on")[0][0], gs.str.findall("on")[0][0]) - assert_eq(ps.str.findall("on")[1][0], gs.str.findall("on")[0][1]) - assert_eq(ps.str.findall("b")[2][1], gs.str.findall("b")[1][2]) - assert_eq(ps.str.findall("on$")[0][0], gs.str.findall("on$")[0][0]) - assert_eq( - ps.str.findall("on$", re.MULTILINE)[3][0], - gs.str.findall("on$", re.MULTILINE)[0][3], - ) - assert_eq( - ps.str.findall("o.*k", re.DOTALL)[3][0], - gs.str.findall("o.*k", re.DOTALL)[0][3], - ) + expected = ps.str.findall(pat, flags) + actual = gs.str.findall(pat, flags) + assert_eq(expected, actual) def test_string_replace_multi(): From a67b718b220b7f722ff5f7a375618662419e5695 Mon Sep 17 00:00:00 2001 From: Srikar Vanavasam Date: Thu, 11 Aug 2022 11:31:10 -0700 Subject: [PATCH 22/58] Sanitize percentile_approx() output for empty input (#11498) Closes #10856 If all of the tdigest inputs to percentile_approx are empty, it will return a column containing null rows, as expected, but they will have unsanitary offsets. This PR checks if all the inputs are empty and returns an empty column as expected. Authors: - Srikar Vanavasam (https://github.com/SrikarVanavasam) Approvers: - Nghia Truong (https://github.com/ttnghia) - MithunR (https://github.com/mythrocks) - https://github.com/nvdbaranec URL: https://github.com/rapidsai/cudf/pull/11498 --- cpp/src/quantiles/tdigest/tdigest.cu | 9 +++++++-- cpp/tests/quantiles/percentile_approx_test.cu | 3 +-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cpp/src/quantiles/tdigest/tdigest.cu b/cpp/src/quantiles/tdigest/tdigest.cu index 3ebb89dfba4..afefba58801 100644 --- a/cpp/src/quantiles/tdigest/tdigest.cu +++ b/cpp/src/quantiles/tdigest/tdigest.cu @@ -352,13 +352,18 @@ std::unique_ptr percentile_approx(tdigest_column_view const& input, // output is a list column with each row containing percentiles.size() percentile values auto offsets = cudf::make_fixed_width_column( data_type{type_id::INT32}, input.size() + 1, mask_state::UNALLOCATED, stream, mr); - auto row_size_iter = thrust::make_constant_iterator(percentiles.size()); + auto const all_empty_rows = + thrust::count_if(rmm::exec_policy(stream), + input.size_begin(), + input.size_begin() + input.size(), + [] __device__(auto const x) { return x == 0; }) == input.size(); + auto row_size_iter = thrust::make_constant_iterator(all_empty_rows ? 0 : percentiles.size()); thrust::exclusive_scan(rmm::exec_policy(stream), row_size_iter, row_size_iter + input.size() + 1, offsets->mutable_view().begin()); - if (percentiles.size() == 0) { + if (percentiles.size() == 0 || all_empty_rows) { return cudf::make_lists_column( input.size(), std::move(offsets), diff --git a/cpp/tests/quantiles/percentile_approx_test.cu b/cpp/tests/quantiles/percentile_approx_test.cu index 08401b3c2f2..0ca63526c51 100644 --- a/cpp/tests/quantiles/percentile_approx_test.cu +++ b/cpp/tests/quantiles/percentile_approx_test.cu @@ -405,8 +405,7 @@ TEST_F(PercentileApproxTest, EmptyInput) 3, cudf::test::detail::make_null_mask(nulls.begin(), nulls.end())); - // TODO: change percentile_approx to produce sanitary list outputs for this case. - CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*result, *expected); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*result, *expected); } TEST_F(PercentileApproxTest, EmptyPercentiles) From 87a5e6abbcb6c62c3470e0be15042e05c414a8dc Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 11 Aug 2022 17:20:26 -0500 Subject: [PATCH 23/58] Fix Feather test warning. (#11511) This fixes a test warning from `test_feather.py`. ``` cudf/python/cudf/cudf/tests/test_feather.py:15: DeprecationWarning: distutils Version classes are deprecated. Use packaging.version instead. ``` The code used `distutils` to check the pandas version and import a special `feather` package if it was less than pandas 0.24. However, we no longer need to test against pandas versions that old so we can just remove this check entirely instead of updating it to use `packaging.version`. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/11511 --- python/cudf/cudf/tests/test_feather.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/python/cudf/cudf/tests/test_feather.py b/python/cudf/cudf/tests/test_feather.py index 6c83ee3c458..ebc7ea4efe2 100644 --- a/python/cudf/cudf/tests/test_feather.py +++ b/python/cudf/cudf/tests/test_feather.py @@ -1,7 +1,6 @@ -# Copyright (c) 2018, NVIDIA CORPORATION. +# Copyright (c) 2018-2022, NVIDIA CORPORATION. import os -from distutils.version import LooseVersion from string import ascii_letters import numpy as np @@ -12,15 +11,6 @@ import cudf from cudf.testing._utils import NUMERIC_TYPES, assert_eq -if LooseVersion(pd.__version__) < LooseVersion("0.24"): - try: - import feather # noqa F401 - except ImportError: - pytest.skip( - "Feather is not installed and is required for Pandas <" " 0.24", - allow_module_level=True, - ) - @pytest.fixture(params=[0, 1, 10, 100]) def pdf(request): From d39b957fea7a2d4b501a88ca0ee5c879c2f22067 Mon Sep 17 00:00:00 2001 From: nvdbaranec <56695930+nvdbaranec@users.noreply.github.com> Date: Thu, 11 Aug 2022 17:49:36 -0500 Subject: [PATCH 24/58] Remove support for skip_rows / num_rows options in the parquet reader. (#11503) Removes support for skip_rows / num_rows options in the parquet reader. Users retain control of what gets read via row groups. Did some before/after benchmarking. As expected, this doesn't change much except for a minor boost in list reading (due to simplification of the preprocessing step). Most of the ways the row bounds affected the code was in the page setup process (making it slippery to think through the logic) and didn't do much in the actual process of decoding. A selection of before/after benchmarks (all input files ~512 MB) ``` ParquetRead/integral_buffer_input/29/1000/32/0/1/manual_time Before: bytes_per_second=31.4564G/s After: bytes_per_second=31.58G/s ParquetRead/floats_buffer_input/31/1000/32/0/1/manual_time Before: bytes_per_second=49.2819G/s After: bytes_per_second=49.7408G/s ParquetRead/string_file_input/23/1000/32/0/0/manual_time Before: bytes_per_second=24.634G/s After: bytes_per_second=24.6563G/s ParquetRead/string_buffer_input/23/0/1/0/1/manual_time Before: bytes_per_second=5.03313G/s After: bytes_per_second=5.03535G/s ParquetRead/list_buffer_input/24/0/1/1/1/manual_time Before: bytes_per_second=1.11488G/s After: bytes_per_second=1.31447G/s ``` Authors: - https://github.com/nvdbaranec Approvers: - Mike Wilson (https://github.com/hyperbolic2346) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/11503 --- cpp/benchmarks/io/parquet/parquet_reader.cpp | 34 +- cpp/include/cudf/io/parquet.hpp | 74 ----- cpp/src/io/parquet/page_data.cu | 332 +++++-------------- cpp/src/io/parquet/parquet_gpu.hpp | 20 -- cpp/src/io/parquet/reader_impl.cu | 69 ++-- cpp/src/io/parquet/reader_impl.hpp | 20 +- cpp/tests/io/parquet_test.cpp | 207 ------------ 7 files changed, 110 insertions(+), 646 deletions(-) diff --git a/cpp/benchmarks/io/parquet/parquet_reader.cpp b/cpp/benchmarks/io/parquet/parquet_reader.cpp index c66f0af2b76..5f32ebf6672 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader.cpp @@ -71,7 +71,7 @@ void BM_parq_read_varying_input(benchmark::State& state) std::vector get_col_names(cudf::io::source_info const& source) { cudf_io::parquet_reader_options const read_options = - cudf_io::parquet_reader_options::builder(source).num_rows(1); + cudf_io::parquet_reader_options::builder(source); return cudf_io::read_parquet(read_options).metadata.column_names; } @@ -113,9 +113,8 @@ void BM_parq_read_varying_options(benchmark::State& state) .use_pandas_metadata(use_pandas_metadata) .timestamp_type(ts_type); - auto const num_row_groups = data_size / (128 << 20); - cudf::size_type const chunk_row_cnt = view.num_rows() / num_chunks; - auto mem_stats_logger = cudf::memory_stats_logger(); + auto const num_row_groups = data_size / (128 << 20); + auto mem_stats_logger = cudf::memory_stats_logger(); for (auto _ : state) { try_drop_l3_cache(); cuda_event_timer raii(state, true); // flush_l2_cache = true, stream = 0 @@ -133,11 +132,7 @@ void BM_parq_read_varying_options(benchmark::State& state) } read_options.set_row_groups({row_groups_to_read}); } break; - case row_selection::NROWS: - read_options.set_skip_rows(chunk * chunk_row_cnt); - read_options.set_num_rows(chunk_row_cnt); - if (is_last_chunk) read_options.set_num_rows(-1); - break; + case row_selection::NROWS: [[fallthrough]]; default: CUDF_FAIL("Unsupported row selection method"); } @@ -186,24 +181,3 @@ BENCHMARK_REGISTER_F(ParquetRead, column_selection) // row_selection::ROW_GROUPS disabled until we add an API to read metadata from a parquet file and // determine num row groups. https://github.com/rapidsai/cudf/pull/9963#issuecomment-1004832863 -BENCHMARK_DEFINE_F(ParquetRead, row_selection) -(::benchmark::State& state) { BM_parq_read_varying_options(state); } -BENCHMARK_REGISTER_F(ParquetRead, row_selection) - ->ArgsProduct({{int32_t(column_selection::ALL)}, - {int32_t(row_selection::NROWS)}, - {1, 4}, - {0b01}, // defaults - {int32_t(cudf::type_id::EMPTY)}}) - ->Unit(benchmark::kMillisecond) - ->UseManualTime(); - -BENCHMARK_DEFINE_F(ParquetRead, misc_options) -(::benchmark::State& state) { BM_parq_read_varying_options(state); } -BENCHMARK_REGISTER_F(ParquetRead, misc_options) - ->ArgsProduct({{int32_t(column_selection::ALL)}, - {int32_t(row_selection::NROWS)}, - {1}, - {0b01, 0b00, 0b11, 0b010}, - {int32_t(cudf::type_id::EMPTY), int32_t(cudf::type_id::TIMESTAMP_NANOSECONDS)}}) - ->Unit(benchmark::kMillisecond) - ->UseManualTime(); diff --git a/cpp/include/cudf/io/parquet.hpp b/cpp/include/cudf/io/parquet.hpp index 19156e01c1e..0673f73ef89 100644 --- a/cpp/include/cudf/io/parquet.hpp +++ b/cpp/include/cudf/io/parquet.hpp @@ -56,10 +56,6 @@ class parquet_reader_options { // List of individual row groups to read (ignored if empty) std::vector> _row_groups; - // Number of rows to skip from the start - size_type _skip_rows = 0; - // Number of rows to read; -1 is all - size_type _num_rows = -1; // Whether to store string data as categorical type bool _convert_strings_to_categories = false; @@ -133,20 +129,6 @@ class parquet_reader_options { return _convert_binary_to_strings; } - /** - * @brief Returns number of rows to skip from the start. - * - * @return Number of rows to skip from the start - */ - [[nodiscard]] size_type get_skip_rows() const { return _skip_rows; } - - /** - * @brief Returns number of rows to read. - * - * @return Number of rows to read - */ - [[nodiscard]] size_type get_num_rows() const { return _num_rows; } - /** * @brief Returns names of column to be read, if set. * @@ -182,10 +164,6 @@ class parquet_reader_options { */ void set_row_groups(std::vector> row_groups) { - if ((!row_groups.empty()) and ((_skip_rows != 0) or (_num_rows != -1))) { - CUDF_FAIL("row_groups can't be set along with skip_rows and num_rows"); - } - _row_groups = std::move(row_groups); } @@ -214,34 +192,6 @@ class parquet_reader_options { _convert_binary_to_strings = std::move(val); } - /** - * @brief Sets number of rows to skip. - * - * @param val Number of rows to skip from start - */ - void set_skip_rows(size_type val) - { - if ((val != 0) and (!_row_groups.empty())) { - CUDF_FAIL("skip_rows can't be set along with a non-empty row_groups"); - } - - _skip_rows = val; - } - - /** - * @brief Sets number of rows to read. - * - * @param val Number of rows to read after skip - */ - void set_num_rows(size_type val) - { - if ((val != -1) and (!_row_groups.empty())) { - CUDF_FAIL("num_rows can't be set along with a non-empty row_groups"); - } - - _num_rows = val; - } - /** * @brief Sets timestamp_type used to cast timestamp columns. * @@ -332,30 +282,6 @@ class parquet_reader_options_builder { return *this; } - /** - * @brief Sets number of rows to skip. - * - * @param val Number of rows to skip from start - * @return this for chaining - */ - parquet_reader_options_builder& skip_rows(size_type val) - { - options.set_skip_rows(val); - return *this; - } - - /** - * @brief Sets number of rows to read. - * - * @param val Number of rows to read after skip - * @return this for chaining - */ - parquet_reader_options_builder& num_rows(size_type val) - { - options.set_num_rows(val); - return *this; - } - /** * @brief timestamp_type used to cast timestamp columns. * diff --git a/cpp/src/io/parquet/page_data.cu b/cpp/src/io/parquet/page_data.cu index 318f7138517..3e71c3d1a07 100644 --- a/cpp/src/io/parquet/page_data.cu +++ b/cpp/src/io/parquet/page_data.cu @@ -54,15 +54,13 @@ struct page_state_s { const uint8_t* data_start; const uint8_t* data_end; const uint8_t* lvl_end; - const uint8_t* dict_base; // ptr to dictionary page data - int32_t dict_size; // size of dictionary data - int32_t first_row; // First row in page to output - int32_t num_rows; // Rows in page to decode (including rows to be skipped) - int32_t first_output_value; // First value in page to output - int32_t num_input_values; // total # of input/level values in the page - int32_t dtype_len; // Output data type length - int32_t dtype_len_in; // Can be larger than dtype_len if truncating 32-bit into 8-bit - int32_t dict_bits; // # of bits to store dictionary indices + const uint8_t* dict_base; // ptr to dictionary page data + int32_t dict_size; // size of dictionary data + int32_t num_rows; // Rows in page to decode + int32_t num_input_values; // total # of input/level values in the page + int32_t dtype_len; // Output data type length + int32_t dtype_len_in; // Can be larger than dtype_len if truncating 32-bit into 8-bit + int32_t dict_bits; // # of bits to store dictionary indices uint32_t dict_run; int32_t dict_val; uint32_t initial_rle_run[NUM_LEVEL_TYPES]; // [def,rep] @@ -88,7 +86,6 @@ struct page_state_s { uint32_t def[non_zero_buffer_size]; // circular buffer of definition level values const uint8_t* lvl_start[NUM_LEVEL_TYPES]; // [def,rep] int32_t lvl_count[NUM_LEVEL_TYPES]; // how many of each of the streams we've decoded - int32_t row_index_lower_bound; // lower bound of row indices we should process }; /** @@ -865,17 +862,14 @@ static __device__ void gpuOutputGeneric(volatile page_state_s* s, * @param[in, out] s The local page state to be filled in * @param[in] p The global page to be copied from * @param[in] chunks The global list of chunks - * @param[in] num_rows Maximum number of rows to read - * @param[in] min_row Crop all rows below min_row + * @param[in] num_rows Maximum number of rows to process */ static __device__ bool setupLocalPageInfo(page_state_s* const s, PageInfo const* p, device_span chunks, - size_t min_row, size_t num_rows) { - int t = threadIdx.x; - int chunk_idx; + int const t = threadIdx.x; // Fetch page info if (t == 0) s->page = *p; @@ -883,7 +877,7 @@ static __device__ bool setupLocalPageInfo(page_state_s* const s, if (s->page.flags & PAGEINFO_FLAGS_DICTIONARY) { return false; } // Fetch column chunk info - chunk_idx = s->page.chunk_idx; + int const chunk_idx = s->page.chunk_idx; if (t == 0) { s->col = chunks[chunk_idx]; } // zero nested value and valid counts @@ -904,19 +898,18 @@ static __device__ bool setupLocalPageInfo(page_state_s* const s, // our starting row (absolute index) is // col.start_row == absolute row index // page.chunk-row == relative row index within the chunk - size_t page_start_row = s->col.start_row + s->page.chunk_row; + size_t const page_start_row = s->col.start_row + s->page.chunk_row; // IMPORTANT : nested schemas can have 0 rows in a page but still have // values. The case is: // - On page N-1, the last row starts, with 2/6 values encoded // - On page N, the remaining 4/6 values are encoded, but there are no new rows. - // if (s->page.num_input_values > 0 && s->page.num_rows > 0) { if (s->page.num_input_values > 0) { - uint8_t* cur = s->page.page_data; - uint8_t* end = cur + s->page.uncompressed_page_size; + uint8_t const* cur = s->page.page_data; + uint8_t const* const end = cur + s->page.uncompressed_page_size; - uint32_t dtype_len_out = s->col.data_type >> 3; - s->ts_scale = 0; + uint32_t const dtype_len_out = s->col.data_type >> 3; + s->ts_scale = 0; // Validate data type auto const data_type = s->col.data_type & 7; switch (data_type) { @@ -965,17 +958,10 @@ static __device__ bool setupLocalPageInfo(page_state_s* const s, s->dtype_len = 8; // Convert to 64-bit timestamp } - // first row within the page to output - if (page_start_row >= min_row) { - s->first_row = 0; - } else { - s->first_row = (int32_t)min(min_row - page_start_row, (size_t)s->page.num_rows); - } // # of rows within the page to output s->num_rows = s->page.num_rows; - if ((page_start_row + s->first_row) + s->num_rows > min_row + num_rows) { - s->num_rows = - (int32_t)max((int64_t)(min_row + num_rows - (page_start_row + s->first_row)), INT64_C(0)); + if (page_start_row + s->num_rows > num_rows) { + s->num_rows = (int32_t)max((int64_t)(num_rows - page_start_row), INT64_C(0)); } // during the decoding step we need to offset the global output buffers @@ -984,25 +970,18 @@ static __device__ bool setupLocalPageInfo(page_state_s* const s, // - for flat schemas, we can do this directly by using row counts // - for nested schemas, these offsets are computed during the preprocess step if (s->col.column_data_base != nullptr) { - int max_depth = s->col.max_nesting_depth; + int const max_depth = s->col.max_nesting_depth; for (int idx = 0; idx < max_depth; idx++) { PageNestingInfo* pni = &s->page.nesting[idx]; - size_t output_offset; - // schemas without lists - if (s->col.max_level[level_type::REPETITION] == 0) { - output_offset = page_start_row >= min_row ? page_start_row - min_row : 0; - } - // for schemas with lists, we've already got the exactly value precomputed - else { - output_offset = pni->page_start_value; - } + size_t const output_offset = + s->col.max_level[level_type::REPETITION] == 0 ? page_start_row : pni->page_start_value; pni->data_out = static_cast(s->col.column_data_base[idx]); if (pni->data_out != nullptr) { // anything below max depth with a valid data pointer must be a list, so the // element size is the size of the offset type. - uint32_t len = idx < max_depth - 1 ? sizeof(cudf::size_type) : s->dtype_len; + uint32_t const len = idx < max_depth - 1 ? sizeof(cudf::size_type) : s->dtype_len; pni->data_out += (output_offset * len); } pni->valid_map = s->col.valid_map_base[idx]; @@ -1012,7 +991,6 @@ static __device__ bool setupLocalPageInfo(page_state_s* const s, } } } - s->first_output_value = 0; // Find the compressed size of repetition levels cur += InitLevelSection(s, cur, end, level_type::REPETITION); @@ -1065,53 +1043,9 @@ static __device__ bool setupLocalPageInfo(page_state_s* const s, s->dict_pos = 0; s->src_pos = 0; - // for flat hierarchies, we can't know how many leaf values to skip unless we do a full - // preprocess of the definition levels (since nulls will have no actual decodable value, there - // is no direct correlation between # of rows and # of decodable values). so we will start - // processing at the beginning of the value stream and disregard any indices that start - // before the first row. - if (s->col.max_level[level_type::REPETITION] == 0) { - s->page.skipped_values = 0; - s->page.skipped_leaf_values = 0; - s->input_value_count = 0; - s->input_row_count = 0; - - s->row_index_lower_bound = -1; - } - // for nested hierarchies, we have run a preprocess that lets us skip directly to the values - // we need to start decoding at - else { - // input_row_count translates to "how many rows we have processed so far", so since we are - // skipping directly to where we want to start decoding, set it to first_row - s->input_row_count = s->first_row; - - // return the lower bound to compare (page-relative) thread row index against. Explanation: - // In the case of nested schemas, rows can span page boundaries. That is to say, - // we can encounter the first value for row X on page M, but the last value for page M - // might not be the last value for row X. page M+1 (or further) may contain the last value. - // - // This means that the first values we encounter for a given page (M+1) may not belong to the - // row indicated by chunk_row, but to the row before it that spanned page boundaries. If that - // previous row is within the overall row bounds, include the values by allowing relative row - // index -1 - int const max_row = (min_row + num_rows) - 1; - if (min_row < page_start_row && max_row >= page_start_row - 1) { - s->row_index_lower_bound = -1; - } else { - s->row_index_lower_bound = s->first_row; - } - - // if we're in the decoding step, jump directly to the first - // value we care about - if (s->col.column_data_base != nullptr) { - s->input_value_count = s->page.skipped_values > -1 ? s->page.skipped_values : 0; - } else { - s->input_value_count = 0; - s->input_leaf_count = 0; - s->page.skipped_values = -1; - s->page.skipped_leaf_values = -1; - } - } + s->input_value_count = 0; + s->input_row_count = 0; + s->input_leaf_count = 0; __threadfence_block(); } @@ -1257,10 +1191,7 @@ static __device__ void gpuUpdateValidityOffsetsAndRowIndices(int32_t target_inpu input_row_count + ((__popc(warp_row_count_mask & ((1 << t) - 1)) + is_new_row) - 1); input_row_count += __popc(warp_row_count_mask); // is this thread within read row bounds? - int const in_row_bounds = thread_row_index >= s->row_index_lower_bound && - thread_row_index < (s->first_row + s->num_rows) - ? 1 - : 0; + int const in_row_bounds = thread_row_index < s->num_rows; // compute warp and thread value counts uint32_t const warp_count_mask = @@ -1335,9 +1266,7 @@ static __device__ void gpuUpdateValidityOffsetsAndRowIndices(int32_t target_inpu // the correct position to start reading. since we are about to write the validity vector here // we need to adjust our computed mask to take into account the write row bounds. int const in_write_row_bounds = - max_depth == 1 - ? thread_row_index >= s->first_row && thread_row_index < (s->first_row + s->num_rows) - : in_row_bounds; + max_depth == 1 ? thread_row_index < s->num_rows : in_row_bounds; int const first_thread_in_write_range = max_depth == 1 ? __ffs(ballot(in_write_row_bounds)) - 1 : 0; // # of bits to of the validity mask to write out @@ -1425,16 +1354,11 @@ __device__ void gpuDecodeLevels(page_state_s* s, int32_t target_leaf_count, int * @param[in] s The local page info * @param[in] target_input_value_count The # of repetition/definition levels to process up to * @param[in] t Thread index - * @param[in] bounds_set Whether or not s->row_index_lower_bound, s->first_row and s->num_rows - * have been computed for this page (they will only be set in the second/trim pass). */ -static __device__ void gpuUpdatePageSizes(page_state_s* s, - int32_t target_input_value_count, - int t, - bool bounds_set) +static __device__ void gpuUpdatePageSizes(page_state_s* s, int32_t target_input_value_count, int t) { // max nesting depth of the column - int max_depth = s->col.max_nesting_depth; + int const max_depth = s->col.max_nesting_depth; // bool has_repetition = s->col.max_level[level_type::REPETITION] > 0 ? true : false; // how many input level values we've processed in the page so far int input_value_count = s->input_value_count; @@ -1449,44 +1373,23 @@ static __device__ void gpuUpdatePageSizes(page_state_s* s, start_depth, end_depth, d, s, input_value_count, target_input_value_count, t); // count rows and leaf values - int is_new_row = start_depth == 0 ? 1 : 0; - uint32_t warp_row_count_mask = ballot(is_new_row); - int is_new_leaf = (d >= s->page.nesting[max_depth - 1].max_def_level) ? 1 : 0; - uint32_t warp_leaf_count_mask = ballot(is_new_leaf); - - // is this thread within row bounds? on the first pass we don't know the bounds, so we will be - // computing the full size of the column. on the second pass, we will know our actual row - // bounds, so the computation will cap sizes properly. - int in_row_bounds = 1; - if (bounds_set) { - // absolute row index - int32_t thread_row_index = - input_row_count + ((__popc(warp_row_count_mask & ((1 << t) - 1)) + is_new_row) - 1); - in_row_bounds = thread_row_index >= s->row_index_lower_bound && - thread_row_index < (s->first_row + s->num_rows) - ? 1 - : 0; - - uint32_t row_bounds_mask = ballot(in_row_bounds); - int first_thread_in_range = __ffs(row_bounds_mask) - 1; - - // if we've found the beginning of the first row, mark down the position - // in the def/repetition buffer (skipped_values) and the data buffer (skipped_leaf_values) - if (!t && first_thread_in_range >= 0 && s->page.skipped_values < 0) { - // how many values we've skipped in the rep/def levels - s->page.skipped_values = input_value_count + first_thread_in_range; - // how many values we've skipped in the actual data stream - s->page.skipped_leaf_values = - input_leaf_count + __popc(warp_leaf_count_mask & ((1 << first_thread_in_range) - 1)); - } - } + int const is_new_row = start_depth == 0 ? 1 : 0; + uint32_t const warp_row_count_mask = ballot(is_new_row); + int const is_new_leaf = (d >= s->page.nesting[max_depth - 1].max_def_level) ? 1 : 0; + uint32_t const warp_leaf_count_mask = ballot(is_new_leaf); + + // is this thread within row bounds? + int32_t const thread_row_index = + input_row_count + ((__popc(warp_row_count_mask & ((1 << t) - 1)) + is_new_row) - 1); + int const in_row_bounds = thread_row_index < s->num_rows; // increment counts across all nesting depths for (int s_idx = 0; s_idx < max_depth; s_idx++) { // if we are within the range of nesting levels we should be adding value indices for - int in_nesting_bounds = (s_idx >= start_depth && s_idx <= end_depth && in_row_bounds) ? 1 : 0; + int const in_nesting_bounds = + (s_idx >= start_depth && s_idx <= end_depth && in_row_bounds) ? 1 : 0; - uint32_t count_mask = ballot(in_nesting_bounds); + uint32_t const count_mask = ballot(in_nesting_bounds); if (!t) { s->page.nesting[s_idx].size += __popc(count_mask); } } @@ -1510,29 +1413,18 @@ static __device__ void gpuUpdatePageSizes(page_state_s* s, * * @param pages List of pages * @param chunks List of column chunks - * @param min_row Row index to start reading at - * @param num_rows Maximum number of rows to read. Pass as INT_MAX to guarantee reading all rows. - * @param trim_pass Whether or not this is the trim pass. We first have to compute - * the full size information of every page before we come through in a second (trim) pass - * to determine what subset of rows in this page we should be reading. */ __global__ void __launch_bounds__(block_size) - gpuComputePageSizes(PageInfo* pages, - device_span chunks, - size_t min_row, - size_t num_rows, - bool trim_pass) + gpuComputePageSizes(PageInfo* pages, device_span chunks) { __shared__ __align__(16) page_state_s state_g; page_state_s* const s = &state_g; - int page_idx = blockIdx.x; - int t = threadIdx.x; - PageInfo* pp = &pages[page_idx]; + int const page_idx = blockIdx.x; + int const t = threadIdx.x; + PageInfo* const pp = &pages[page_idx]; - if (!setupLocalPageInfo(s, pp, chunks, trim_pass ? min_row : 0, trim_pass ? num_rows : INT_MAX)) { - return; - } + if (!setupLocalPageInfo(s, pp, chunks, INT_MAX)) { return; } // zero sizes int d = 0; @@ -1541,21 +1433,12 @@ __global__ void __launch_bounds__(block_size) d += blockDim.x; } if (!t) { - s->page.skipped_values = -1; - s->page.skipped_leaf_values = -1; - s->input_row_count = 0; - s->input_value_count = 0; - - // if this isn't the trim pass, make sure we visit absolutely everything - if (!trim_pass) { - s->first_row = 0; - s->num_rows = INT_MAX; - s->row_index_lower_bound = -1; - } + s->input_row_count = 0; + s->input_value_count = 0; } __syncthreads(); - bool has_repetition = s->col.max_level[level_type::REPETITION] > 0; + bool const has_repetition = s->col.max_level[level_type::REPETITION] > 0; // optimization : it might be useful to have a version of gpuDecodeStream that could go wider than // 1 warp. Currently it only uses 1 warp so that it can overlap work with the value decoding step @@ -1574,22 +1457,18 @@ __global__ void __launch_bounds__(block_size) __syncwarp(); // we may have decoded different amounts from each stream, so only process what we've been - int actual_input_count = has_repetition ? min(s->lvl_count[level_type::REPETITION], - s->lvl_count[level_type::DEFINITION]) - : s->lvl_count[level_type::DEFINITION]; + int const actual_input_count = has_repetition ? min(s->lvl_count[level_type::REPETITION], + s->lvl_count[level_type::DEFINITION]) + : s->lvl_count[level_type::DEFINITION]; // process what we got back - gpuUpdatePageSizes(s, actual_input_count, t, trim_pass); + gpuUpdatePageSizes(s, actual_input_count, t); target_input_count = actual_input_count + batch_size; __syncwarp(); } } // update # rows in the actual page - if (!t) { - pp->num_rows = s->page.nesting[0].size; - pp->skipped_values = s->page.skipped_values; - pp->skipped_leaf_values = s->page.skipped_leaf_values; - } + if (!t) { pp->num_rows = s->page.nesting[0].size; } } /** @@ -1602,20 +1481,19 @@ __global__ void __launch_bounds__(block_size) * * @param pages List of pages * @param chunks List of column chunks - * @param min_row Row index to start reading at * @param num_rows Maximum number of rows to read */ -__global__ void __launch_bounds__(block_size) gpuDecodePageData( - PageInfo* pages, device_span chunks, size_t min_row, size_t num_rows) +__global__ void __launch_bounds__(block_size) + gpuDecodePageData(PageInfo* pages, device_span chunks, size_t num_rows) { __shared__ __align__(16) page_state_s state_g; page_state_s* const s = &state_g; - int page_idx = blockIdx.x; - int t = threadIdx.x; + int const page_idx = blockIdx.x; + int const t = threadIdx.x; int out_thread0; - if (!setupLocalPageInfo(s, &pages[page_idx], chunks, min_row, num_rows)) { return; } + if (!setupLocalPageInfo(s, &pages[page_idx], chunks, num_rows)) { return; } if (s->dict_base) { out_thread0 = (s->dict_bits > 0) ? 64 : 32; @@ -1624,8 +1502,6 @@ __global__ void __launch_bounds__(block_size) gpuDecodePageData( ((s->col.data_type & 7) == BOOLEAN || (s->col.data_type & 7) == BYTE_ARRAY) ? 64 : 32; } - // skipped_leaf_values will always be 0 for flat hierarchies. - uint32_t skipped_leaf_values = s->page.skipped_leaf_values; while (!s->error && (s->input_value_count < s->num_input_values || s->src_pos < s->nz_count)) { int target_pos; int src_pos = s->src_pos; @@ -1645,8 +1521,7 @@ __global__ void __launch_bounds__(block_size) gpuDecodePageData( // - produces non-NULL value indices in s->nz_idx for subsequent decoding gpuDecodeLevels(s, target_pos, t); } else if (t < out_thread0) { - // skipped_leaf_values will always be 0 for flat hierarchies. - uint32_t src_target_pos = target_pos + skipped_leaf_values; + uint32_t src_target_pos = target_pos; // WARP1: Decode dictionary indices, booleans or string positions if (s->dict_base) { @@ -1659,70 +1534,51 @@ __global__ void __launch_bounds__(block_size) gpuDecodePageData( if (t == 32) { *(volatile int32_t*)&s->dict_pos = src_target_pos; } } else { // WARP1..WARP3: Decode values - int dtype = s->col.data_type & 7; + int const dtype = s->col.data_type & 7; src_pos += t - out_thread0; // the position in the output column/buffer - int dst_pos = s->nz_idx[rolling_index(src_pos)]; - - // for the flat hierarchy case we will be reading from the beginning of the value stream, - // regardless of the value of first_row. so adjust our destination offset accordingly. - // example: - // - user has passed skip_rows = 2, so our first_row to output is 2 - // - the row values we get from nz_idx will be - // 0, 1, 2, 3, 4 .... - // - by shifting these values by first_row, the sequence becomes - // -1, -2, 0, 1, 2 ... - // - so we will end up ignoring the first two input rows, and input rows 2..n will - // get written to the output starting at position 0. - // - if (s->col.max_nesting_depth == 1) { dst_pos -= s->first_row; } + int const dst_pos = s->nz_idx[rolling_index(src_pos)]; // target_pos will always be properly bounded by num_rows, but dst_pos may be negative (values // before first_row) in the flat hierarchy case. if (src_pos < target_pos && dst_pos >= 0) { - // src_pos represents the logical row position we want to read from. But in the case of - // nested hierarchies, there is no 1:1 mapping of rows to values. So our true read position - // has to take into account the # of values we have to skip in the page to get to the - // desired logical row. For flat hierarchies, skipped_leaf_values will always be 0. - uint32_t val_src_pos = src_pos + skipped_leaf_values; - // nesting level that is storing actual leaf values - int leaf_level_index = s->col.max_nesting_depth - 1; + int const leaf_level_index = s->col.max_nesting_depth - 1; - uint32_t dtype_len = s->dtype_len; + uint32_t const dtype_len = s->dtype_len; void* dst = s->page.nesting[leaf_level_index].data_out + static_cast(dst_pos) * dtype_len; if (dtype == BYTE_ARRAY) { - gpuOutputString(s, val_src_pos, dst); + gpuOutputString(s, src_pos, dst); } else if (dtype == BOOLEAN) { - gpuOutputBoolean(s, val_src_pos, static_cast(dst)); + gpuOutputBoolean(s, src_pos, static_cast(dst)); } else if (s->col.converted_type == DECIMAL) { switch (dtype) { - case INT32: gpuOutputFast(s, val_src_pos, static_cast(dst)); break; - case INT64: gpuOutputFast(s, val_src_pos, static_cast(dst)); break; + case INT32: gpuOutputFast(s, src_pos, static_cast(dst)); break; + case INT64: gpuOutputFast(s, src_pos, static_cast(dst)); break; default: if (s->dtype_len_in <= sizeof(int32_t)) { - gpuOutputFixedLenByteArrayAsInt(s, val_src_pos, static_cast(dst)); + gpuOutputFixedLenByteArrayAsInt(s, src_pos, static_cast(dst)); } else if (s->dtype_len_in <= sizeof(int64_t)) { - gpuOutputFixedLenByteArrayAsInt(s, val_src_pos, static_cast(dst)); + gpuOutputFixedLenByteArrayAsInt(s, src_pos, static_cast(dst)); } else { - gpuOutputFixedLenByteArrayAsInt(s, val_src_pos, static_cast<__int128_t*>(dst)); + gpuOutputFixedLenByteArrayAsInt(s, src_pos, static_cast<__int128_t*>(dst)); } break; } } else if (dtype == INT96) { - gpuOutputInt96Timestamp(s, val_src_pos, static_cast(dst)); + gpuOutputInt96Timestamp(s, src_pos, static_cast(dst)); } else if (dtype_len == 8) { if (s->ts_scale) { - gpuOutputInt64Timestamp(s, val_src_pos, static_cast(dst)); + gpuOutputInt64Timestamp(s, src_pos, static_cast(dst)); } else { - gpuOutputFast(s, val_src_pos, static_cast(dst)); + gpuOutputFast(s, src_pos, static_cast(dst)); } } else if (dtype_len == 4) { - gpuOutputFast(s, val_src_pos, static_cast(dst)); + gpuOutputFast(s, src_pos, static_cast(dst)); } else { - gpuOutputGeneric(s, val_src_pos, static_cast(dst), dtype_len); + gpuOutputGeneric(s, src_pos, static_cast(dst), dtype_len); } } @@ -1793,8 +1649,6 @@ void PreprocessColumnData(hostdevice_vector& pages, std::vector& input_columns, std::vector& output_columns, size_t num_rows, - size_t min_row, - bool uses_custom_row_bounds, rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr) { @@ -1803,16 +1657,7 @@ void PreprocessColumnData(hostdevice_vector& pages, // computes: // PageNestingInfo::size for each level of nesting, for each page. - // This computes the size for the entire page, not taking row bounds into account. - // If uses_custom_row_bounds is set to true, we have to do a second pass later that "trims" - // the starting and ending read values to account for these bounds. - gpuComputePageSizes<<>>( - pages.device_ptr(), - chunks, - // if uses_custom_row_bounds is false, include all possible rows. - uses_custom_row_bounds ? min_row : 0, - uses_custom_row_bounds ? num_rows : INT_MAX, - !uses_custom_row_bounds); + gpuComputePageSizes<<>>(pages.device_ptr(), chunks); // computes: // PageInfo::chunk_row for all pages @@ -1826,16 +1671,6 @@ void PreprocessColumnData(hostdevice_vector& pages, page_input, chunk_row_output_iter{pages.device_ptr()}); - // computes: - // PageNestingInfo::size for each level of nesting, for each page, taking row bounds into account. - // PageInfo::skipped_values, which tells us where to start decoding in the input . - // It is only necessary to do this second pass if uses_custom_row_bounds is set (if the user has - // specified artifical bounds). - if (uses_custom_row_bounds) { - gpuComputePageSizes<<>>( - pages.device_ptr(), chunks, min_row, num_rows, true); - } - // ordering of pages is by input column schema, repeated across row groups. so // if we had 3 columns, each with 2 pages, and 1 row group, our schema values might look like // @@ -1900,13 +1735,11 @@ void PreprocessColumnData(hostdevice_vector& pages, // Handle a specific corner case. It is possible to construct a parquet file such that // a column within a row group contains more rows than the row group itself. This may be // invalid, but we have seen instances of this in the wild, including how they were created - // using the apache parquet tools. Normally, the trim pass would handle this case quietly, - // but if we are not running the trim pass (which is most of the time) we need to cap the - // number of rows we will allocate/read from the file with the amount specified in the - // associated row group. This only applies to columns that are not children of lists as - // those may have an arbitrary number of rows in them. - if (!uses_custom_row_bounds && - !(out_buf.user_data & PARQUET_COLUMN_BUFFER_FLAG_HAS_LIST_PARENT) && + // using the apache parquet tools. So we need to cap the number of rows we will + // allocate/read from the file with the amount specified in the associated row group. This + // only applies to columns that are not children of lists as those may have an arbitrary + // number of rows in them. + if (!(out_buf.user_data & PARQUET_COLUMN_BUFFER_FLAG_HAS_LIST_PARENT) && size > static_cast(num_rows)) { size = static_cast(num_rows); } @@ -1941,14 +1774,13 @@ void PreprocessColumnData(hostdevice_vector& pages, void __host__ DecodePageData(hostdevice_vector& pages, hostdevice_vector const& chunks, size_t num_rows, - size_t min_row, rmm::cuda_stream_view stream) { dim3 dim_block(block_size, 1); dim3 dim_grid(pages.size(), 1); // 1 threadblock per page gpuDecodePageData<<>>( - pages.device_ptr(), chunks, min_row, num_rows); + pages.device_ptr(), chunks, num_rows); } } // namespace gpu diff --git a/cpp/src/io/parquet/parquet_gpu.hpp b/cpp/src/io/parquet/parquet_gpu.hpp index 0faeba7987b..81f802ff9ca 100644 --- a/cpp/src/io/parquet/parquet_gpu.hpp +++ b/cpp/src/io/parquet/parquet_gpu.hpp @@ -135,19 +135,6 @@ struct PageInfo { Encoding definition_level_encoding; // Encoding used for definition levels (data page) Encoding repetition_level_encoding; // Encoding used for repetition levels (data page) - // for nested types, we run a preprocess step in order to determine output - // column sizes. Because of this, we can jump directly to the position in the - // input data to start decoding instead of reading all of the data and discarding - // rows we don't care about. - // - // NOTE: for flat hierarchies we do not do the preprocess step, so skipped_values and - // skipped_leaf_values will always be 0. - // - // # of values skipped in the repetition/definition level stream - int skipped_values; - // # of values skipped in the actual data stream. - int skipped_leaf_values; - // nesting information (input/output) for each page int num_nesting_levels; PageNestingInfo* nesting; @@ -429,9 +416,6 @@ void BuildStringDictionaryIndex(ColumnChunkDesc* chunks, * @param input_columns Input column information * @param output_columns Output column information * @param num_rows Maximum number of rows to read - * @param min_rows crop all rows below min_row - * @param uses_custom_row_bounds Whether or not num_rows and min_rows represents user-specific - * bounds * @param stream Cuda stream */ void PreprocessColumnData(hostdevice_vector& pages, @@ -439,8 +423,6 @@ void PreprocessColumnData(hostdevice_vector& pages, std::vector& input_columns, std::vector& output_columns, size_t num_rows, - size_t min_row, - bool uses_custom_row_bounds, rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr); @@ -453,13 +435,11 @@ void PreprocessColumnData(hostdevice_vector& pages, * @param[in,out] pages All pages to be decoded * @param[in] chunks All chunks to be decoded * @param[in] num_rows Total number of rows to read - * @param[in] min_row Minimum number of rows to read * @param[in] stream CUDA stream to use, default 0 */ void DecodePageData(hostdevice_vector& pages, hostdevice_vector const& chunks, size_t num_rows, - size_t min_row, rmm::cuda_stream_view stream); /** diff --git a/cpp/src/io/parquet/reader_impl.cu b/cpp/src/io/parquet/reader_impl.cu index 4685ed4bc52..4ee6a88d94a 100644 --- a/cpp/src/io/parquet/reader_impl.cu +++ b/cpp/src/io/parquet/reader_impl.cu @@ -540,15 +540,14 @@ class aggregate_reader_metadata { * @brief Filters and reduces down to a selection of row groups * * @param row_groups Lists of row groups to read, one per source - * @param row_start Starting row of the selection - * @param row_count Total number of rows selected * - * @return List of row group indexes and its starting row + * @return List of row group info structs and the total number of rows */ - [[nodiscard]] auto select_row_groups(std::vector> const& row_groups, - size_type& row_start, - size_type& row_count) const + [[nodiscard]] std::pair, size_type> select_row_groups( + std::vector> const& row_groups) const { + size_type row_count = 0; + if (!row_groups.empty()) { std::vector selection; CUDF_EXPECTS(row_groups.size() == per_file_metadata.size(), @@ -565,17 +564,12 @@ class aggregate_reader_metadata { row_count += get_row_group(rowgroup_idx, src_idx).num_rows; } } - return selection; + return {selection, row_count}; } - row_start = std::max(row_start, 0); - if (row_count < 0) { - row_count = static_cast( - std::min(get_num_rows(), std::numeric_limits::max())); - } - row_count = min(row_count, get_num_rows() - row_start); + row_count = static_cast( + std::min(get_num_rows(), std::numeric_limits::max())); CUDF_EXPECTS(row_count >= 0, "Invalid row count"); - CUDF_EXPECTS(row_start <= get_num_rows(), "Invalid row start"); std::vector selection; size_type count = 0; @@ -583,14 +577,12 @@ class aggregate_reader_metadata { for (size_t rg_idx = 0; rg_idx < per_file_metadata[src_idx].row_groups.size(); ++rg_idx) { auto const chunk_start_row = count; count += get_row_group(rg_idx, src_idx).num_rows; - if (count > row_start || count == 0) { - selection.emplace_back(rg_idx, chunk_start_row, src_idx); - } - if (count >= row_start + row_count) { break; } + selection.emplace_back(rg_idx, chunk_start_row, src_idx); + if (count >= row_count) { break; } } } - return selection; + return {selection, row_count}; } /** @@ -1350,9 +1342,7 @@ void reader::impl::allocate_nesting_info(hostdevice_vector */ void reader::impl::preprocess_columns(hostdevice_vector& chunks, hostdevice_vector& pages, - size_t min_row, - size_t total_rows, - bool uses_custom_row_bounds, + size_t num_rows, bool has_lists) { // TODO : we should be selectively preprocessing only columns that have @@ -1365,22 +1355,15 @@ void reader::impl::preprocess_columns(hostdevice_vector& c [&](std::vector& cols) { for (size_t idx = 0; idx < cols.size(); idx++) { auto& col = cols[idx]; - col.create(total_rows, _stream, _mr); + col.create(num_rows, _stream, _mr); create_columns(col.children); } }; create_columns(_output_columns); } else { // preprocess per-nesting level sizes by page - gpu::PreprocessColumnData(pages, - chunks, - _input_columns, - _output_columns, - total_rows, - min_row, - uses_custom_row_bounds, - _stream, - _mr); + gpu::PreprocessColumnData( + pages, chunks, _input_columns, _output_columns, num_rows, _stream, _mr); _stream.synchronize(); } } @@ -1391,7 +1374,6 @@ void reader::impl::preprocess_columns(hostdevice_vector& c void reader::impl::decode_page_data(hostdevice_vector& chunks, hostdevice_vector& pages, hostdevice_vector& page_nesting, - size_t min_row, size_t total_rows) { auto is_dict_chunk = [](const gpu::ColumnChunkDesc& chunk) { @@ -1513,7 +1495,7 @@ void reader::impl::decode_page_data(hostdevice_vector& chu gpu::BuildStringDictionaryIndex(chunks.device_ptr(), chunks.size(), _stream); } - gpu::DecodePageData(pages, chunks, total_rows, min_row, _stream); + gpu::DecodePageData(pages, chunks, total_rows, _stream); pages.device_to_host(_stream); page_nesting.device_to_host(_stream); _stream.synchronize(); @@ -1605,14 +1587,10 @@ reader::impl::impl(std::vector>&& sources, _timestamp_type.id()); } -table_with_metadata reader::impl::read(size_type skip_rows, - size_type num_rows, - bool uses_custom_row_bounds, - std::vector> const& row_group_list) +table_with_metadata reader::impl::read(std::vector> const& row_group_list) { // Select only row groups required - const auto selected_row_groups = - _metadata->select_row_groups(row_group_list, skip_rows, num_rows); + const auto [selected_row_groups, num_rows] = _metadata->select_row_groups(row_group_list); table_metadata out_metadata; @@ -1761,10 +1739,10 @@ table_with_metadata reader::impl::read(size_type skip_rows, // // - for nested schemas, output buffer offset values per-page, per nesting-level for the // purposes of decoding. - preprocess_columns(chunks, pages, skip_rows, num_rows, uses_custom_row_bounds, has_lists); + preprocess_columns(chunks, pages, num_rows, has_lists); // decoding of column data itself - decode_page_data(chunks, pages, page_nesting_info, skip_rows, num_rows); + decode_page_data(chunks, pages, page_nesting_info, num_rows); auto make_output_column = [&](column_buffer& buf, column_name_info* schema_info, int i) { auto col = make_column(buf, schema_info, _stream, _mr); @@ -1828,12 +1806,7 @@ reader::~reader() = default; // Forward to implementation table_with_metadata reader::read(parquet_reader_options const& options) { - // if the user has specified custom row bounds - bool const uses_custom_row_bounds = options.get_num_rows() >= 0 || options.get_skip_rows() != 0; - return _impl->read(options.get_skip_rows(), - options.get_num_rows(), - uses_custom_row_bounds, - options.get_row_groups()); + return _impl->read(options.get_row_groups()); } } // namespace parquet diff --git a/cpp/src/io/parquet/reader_impl.hpp b/cpp/src/io/parquet/reader_impl.hpp index 97582b8ebd7..99c1a231f62 100644 --- a/cpp/src/io/parquet/reader_impl.hpp +++ b/cpp/src/io/parquet/reader_impl.hpp @@ -69,18 +69,11 @@ class reader::impl { /** * @brief Read an entire set or a subset of data and returns a set of columns * - * @param skip_rows Number of rows to skip from the start - * @param num_rows Number of rows to read - * @param uses_custom_row_bounds Whether or not num_rows and min_rows represents user-specific - * bounds * @param row_group_indices Lists of row groups to read, one per source * * @return The set of columns along with metadata */ - table_with_metadata read(size_type skip_rows, - size_type num_rows, - bool uses_custom_row_bounds, - std::vector> const& row_group_indices); + table_with_metadata read(std::vector> const& row_group_indices); private: /** @@ -159,18 +152,13 @@ class reader::impl { * * @param chunks All chunks to be decoded * @param pages All pages to be decoded - * @param min_rows crop all rows below min_row - * @param total_rows Maximum number of rows to read - * @param uses_custom_row_bounds Whether or not num_rows and min_rows represents user-specific - * bounds + * @param num_rows The number of rows to be decoded * @param has_lists Whether or not this data contains lists and requires * a preprocess. */ void preprocess_columns(hostdevice_vector& chunks, hostdevice_vector& pages, - size_t min_row, - size_t total_rows, - bool uses_custom_row_bounds, + size_t num_rows, bool has_lists); /** @@ -179,13 +167,11 @@ class reader::impl { * @param chunks List of column chunk descriptors * @param pages List of page information * @param page_nesting Page nesting array - * @param min_row Minimum number of rows from start * @param total_rows Number of rows to output */ void decode_page_data(hostdevice_vector& chunks, hostdevice_vector& pages, hostdevice_vector& page_nesting, - size_t min_row, size_t total_rows); /** diff --git a/cpp/tests/io/parquet_test.cpp b/cpp/tests/io/parquet_test.cpp index 2f153c98b48..0350bfe2981 100644 --- a/cpp/tests/io/parquet_test.cpp +++ b/cpp/tests/io/parquet_test.cpp @@ -2505,213 +2505,6 @@ TEST_F(ParquetWriterStressTest, DeviceWriteLargeTableWithValids) CUDF_TEST_EXPECT_TABLES_EQUAL(custom_tbl.tbl->view(), expected->view()); } -TEST_F(ParquetReaderTest, UserBounds) -{ - // trying to read more rows than there are should result in - // receiving the properly capped # of rows - { - srand(31337); - auto expected = create_random_fixed_table(4, 4, false); - - auto filepath = temp_env->get_temp_filepath("TooManyRows.parquet"); - cudf_io::parquet_writer_options args = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, *expected); - cudf_io::write_parquet(args); - - // attempt to read more rows than there actually are - cudf_io::parquet_reader_options read_opts = - cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}).num_rows(16); - auto result = cudf_io::read_parquet(read_opts); - - // we should only get back 4 rows - EXPECT_EQ(result.tbl->view().column(0).size(), 4); - } - - // trying to read past the end of the # of actual rows should result - // in empty columns. - { - srand(31337); - auto expected = create_random_fixed_table(4, 4, false); - - auto filepath = temp_env->get_temp_filepath("PastBounds.parquet"); - cudf_io::parquet_writer_options args = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, *expected); - cudf_io::write_parquet(args); - - // attempt to read more rows than there actually are - cudf_io::parquet_reader_options read_opts = - cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}).skip_rows(4); - auto result = cudf_io::read_parquet(read_opts); - - // we should get empty columns back - EXPECT_EQ(result.tbl->view().num_columns(), 4); - EXPECT_EQ(result.tbl->view().column(0).size(), 0); - } - - // trying to read 0 rows should result in reading the whole file - // at the moment we get back 4. when that bug gets fixed, this - // test can be flipped. - { - srand(31337); - auto expected = create_random_fixed_table(4, 4, false); - - auto filepath = temp_env->get_temp_filepath("ZeroRows.parquet"); - cudf_io::parquet_writer_options args = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, *expected); - cudf_io::write_parquet(args); - - // attempt to read more rows than there actually are - cudf_io::parquet_reader_options read_opts = - cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}).num_rows(0); - auto result = cudf_io::read_parquet(read_opts); - - EXPECT_EQ(result.tbl->view().num_columns(), 4); - EXPECT_EQ(result.tbl->view().column(0).size(), 0); - } - - // trying to read 0 rows past the end of the # of actual rows should result - // in empty columns. - { - srand(31337); - auto expected = create_random_fixed_table(4, 4, false); - - auto filepath = temp_env->get_temp_filepath("ZeroRowsPastBounds.parquet"); - cudf_io::parquet_writer_options args = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, *expected); - cudf_io::write_parquet(args); - - // attempt to read more rows than there actually are - cudf_io::parquet_reader_options read_opts = - cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}) - .skip_rows(4) - .num_rows(0); - auto result = cudf_io::read_parquet(read_opts); - - // we should get empty columns back - EXPECT_EQ(result.tbl->view().num_columns(), 4); - EXPECT_EQ(result.tbl->view().column(0).size(), 0); - } -} - -TEST_F(ParquetReaderTest, UserBoundsWithNulls) -{ - // clang-format off - cudf::test::fixed_width_column_wrapper col{{1,1,1,1,1,1,1,1, 2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4, 5,5,5,5,5,5,5,5, 6,6,6,6,6,6,6,6, 7,7,7,7,7,7,7,7, 8,8,8,8,8,8,8,8} - ,{1,1,1,0,0,0,1,1, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,0,0, 1,0,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,0}}; - // clang-format on - cudf::table_view tbl({col}); - auto filepath = temp_env->get_temp_filepath("UserBoundsWithNulls.parquet"); - cudf_io::parquet_writer_options out_args = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, tbl); - cudf_io::write_parquet(out_args); - - // skip_rows / num_rows - // clang-format off - std::vector> params{ {-1, -1}, {1, 3}, {3, -1}, - {31, -1}, {32, -1}, {33, -1}, - {31, 5}, {32, 5}, {33, 5}, - {-1, 7}, {-1, 31}, {-1, 32}, {-1, 33}, - {62, -1}, {63, -1}, - {62, 2}, {63, 1}}; - // clang-format on - for (auto p : params) { - cudf_io::parquet_reader_options read_args = - cudf::io::parquet_reader_options::builder(cudf_io::source_info{filepath}); - if (p.first >= 0) { read_args.set_skip_rows(p.first); } - if (p.second >= 0) { read_args.set_num_rows(p.second); } - auto result = cudf_io::read_parquet(read_args); - - p.first = p.first < 0 ? 0 : p.first; - p.second = p.second < 0 ? static_cast(col).size() - p.first : p.second; - std::vector slice_indices{p.first, p.first + p.second}; - auto expected = cudf::slice(col, slice_indices); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(0), expected[0]); - } -} - -TEST_F(ParquetReaderTest, UserBoundsWithNullsLarge) -{ - constexpr int num_rows = 30 * 1000000; - - std::mt19937 gen(6747); - std::bernoulli_distribution bn(0.7f); - auto valids = - cudf::detail::make_counting_transform_iterator(0, [&](int index) { return bn(gen); }); - auto values = thrust::make_counting_iterator(0); - - cudf::test::fixed_width_column_wrapper col(values, values + num_rows, valids); - - // this file will have row groups of 1,000,000 each - cudf::table_view tbl({col}); - auto filepath = temp_env->get_temp_filepath("UserBoundsWithNullsLarge.parquet"); - cudf_io::parquet_writer_options out_args = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, tbl); - cudf_io::write_parquet(out_args); - - // skip_rows / num_rows - // clang-format off - std::vector> params{ {-1, -1}, {31, -1}, {32, -1}, {33, -1}, {1613470, -1}, {1999999, -1}, - {31, 1}, {32, 1}, {33, 1}, - // deliberately span some row group boundaries - {999000, 1001}, {999000, 2000}, {2999999, 2}, {13999997, -1}, - {16785678, 3}, {22996176, 31}, - {24001231, 17}, {29000001, 989999}, {29999999, 1} }; - // clang-format on - for (auto p : params) { - cudf_io::parquet_reader_options read_args = - cudf::io::parquet_reader_options::builder(cudf_io::source_info{filepath}); - if (p.first >= 0) { read_args.set_skip_rows(p.first); } - if (p.second >= 0) { read_args.set_num_rows(p.second); } - auto result = cudf_io::read_parquet(read_args); - - p.first = p.first < 0 ? 0 : p.first; - p.second = p.second < 0 ? static_cast(col).size() - p.first : p.second; - std::vector slice_indices{p.first, p.first + p.second}; - auto expected = cudf::slice(col, slice_indices); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(0), expected[0]); - } -} - -TEST_F(ParquetReaderTest, ListUserBoundsWithNullsLarge) -{ - constexpr int num_rows = 5 * 1000000; - auto colp = make_parquet_list_col(0, num_rows, 5, 8, true); - cudf::column_view col = *colp; - - // this file will have row groups of 1,000,000 each - cudf::table_view tbl({col}); - auto filepath = temp_env->get_temp_filepath("ListUserBoundsWithNullsLarge.parquet"); - cudf_io::parquet_writer_options out_args = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, tbl); - cudf_io::write_parquet(out_args); - - // skip_rows / num_rows - // clang-format off - std::vector> params{ {-1, -1}, {31, -1}, {32, -1}, {33, -1}, {161470, -1}, {4499997, -1}, - {31, 1}, {32, 1}, {33, 1}, - // deliberately span some row group boundaries - {999000, 1001}, {999000, 2000}, {2999999, 2}, - {1678567, 3}, {4299676, 31}, - {4001231, 17}, {1900000, 989999}, {4999999, 1} }; - // clang-format on - for (auto p : params) { - cudf_io::parquet_reader_options read_args = - cudf::io::parquet_reader_options::builder(cudf_io::source_info{filepath}); - if (p.first >= 0) { read_args.set_skip_rows(p.first); } - if (p.second >= 0) { read_args.set_num_rows(p.second); } - auto result = cudf_io::read_parquet(read_args); - - p.first = p.first < 0 ? 0 : p.first; - p.second = p.second < 0 ? static_cast(col).size() - p.first : p.second; - std::vector slice_indices{p.first, p.first + p.second}; - auto expected = cudf::slice(col, slice_indices); - - CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(0), expected[0]); - } -} - TEST_F(ParquetReaderTest, ReorderedColumns) { { From 42b3bb0a23bb1fd231114248374333a975a79b8d Mon Sep 17 00:00:00 2001 From: Shaswat Anand <33908100+shaswat-indian@users.noreply.github.com> Date: Thu, 11 Aug 2022 16:19:10 -0700 Subject: [PATCH 25/58] Added 'crosstab' and 'pivot_table' features (#11314) Resolves https://github.com/rapidsai/cudf/issues/5944, resolves https://github.com/rapidsai/cudf/issues/1214 This PR adds support for basic use cases of `crosstab` and `pivot_table` functions. Authors: - Shaswat Anand (https://github.com/shaswat-indian) Approvers: - Ashwin Srinath (https://github.com/shwina) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11314 --- python/cudf/benchmarks/API/bench_functions.py | 32 ++- python/cudf/cudf/__init__.py | 4 + python/cudf/cudf/core/reshape.py | 264 ++++++++++++++++++ python/cudf/cudf/tests/test_reshape.py | 92 ++++++ 4 files changed, 391 insertions(+), 1 deletion(-) diff --git a/python/cudf/benchmarks/API/bench_functions.py b/python/cudf/benchmarks/API/bench_functions.py index a166317a46b..ec4be221d9f 100644 --- a/python/cudf/benchmarks/API/bench_functions.py +++ b/python/cudf/benchmarks/API/bench_functions.py @@ -2,9 +2,11 @@ """Benchmarks of free functions that accept cudf objects.""" +import numpy as np import pytest import pytest_cases -from config import cudf, cupy +from config import NUM_ROWS, cudf, cupy +from utils import benchmark_with_object @pytest_cases.parametrize_with_cases("objs", prefix="concat") @@ -50,3 +52,31 @@ def bench_get_dummies_simple(benchmark, prefix): benchmark( cudf.get_dummies, df, columns=["col1", "col2", "col3"], prefix=prefix ) + + +@benchmark_with_object(cls="dataframe", dtype="int", cols=6) +def bench_pivot_table_simple(benchmark, dataframe): + values = ["d", "e"] + index = ["a", "b"] + columns = ["c"] + benchmark( + cudf.pivot_table, + data=dataframe, + values=values, + index=index, + columns=columns, + ) + + +@pytest_cases.parametrize("nr", NUM_ROWS) +def bench_crosstab_simple(benchmark, nr): + series_a = np.array(["foo", "bar"] * nr) + series_b = np.array(["one", "two"] * nr) + series_c = np.array(["dull", "shiny"] * nr) + np.random.shuffle(series_a) + np.random.shuffle(series_b) + np.random.shuffle(series_c) + series_a = cudf.Series(series_a) + series_b = cudf.Series(series_b) + series_c = cudf.Series(series_c) + benchmark(cudf.crosstab, index=series_a, columns=[series_b, series_c]) diff --git a/python/cudf/cudf/__init__.py b/python/cudf/cudf/__init__.py index fff311b251d..506bd9163bc 100644 --- a/python/cudf/cudf/__init__.py +++ b/python/cudf/cudf/__init__.py @@ -59,9 +59,11 @@ from cudf.core.groupby import Grouper from cudf.core.reshape import ( concat, + crosstab, get_dummies, melt, pivot, + pivot_table, unstack, ) from cudf.core.series import isclose @@ -148,6 +150,7 @@ "UInt8Index", "api", "concat", + "crosstab", "cut", "date_range", "describe_option", @@ -162,6 +165,7 @@ "melt", "merge", "pivot", + "pivot_table", "read_avro", "read_csv", "read_feather", diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index ae46305d401..98eaabcab70 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -1160,3 +1160,267 @@ def _length_check_params(obj, columns, name): f"length of the columns being " f"encoded ({len(columns)})." ) + + +def _get_pivot_names(arrs, names, prefix): + """ + Generates unique names for rows/columns + """ + if names is None: + names = [] + for i, arr in enumerate(arrs): + if isinstance(arr, cudf.Series) and arr.name is not None: + names.append(arr.name) + else: + names.append(f"{prefix}_{i}") + else: + if len(names) != len(arrs): + raise ValueError("arrays and names must have the same length") + if not isinstance(names, list): + names = list(names) + + return names + + +def crosstab( + index, + columns, + values=None, + rownames=None, + colnames=None, + aggfunc=None, + margins=False, + margins_name="All", + dropna=None, + normalize=False, +): + """ + Compute a simple cross tabulation of two (or more) factors. By default + computes a frequency table of the factors unless an array of values and an + aggregation function are passed. + + Parameters + ---------- + index : array-like, Series, or list of arrays/Series + Values to group by in the rows. + columns : array-like, Series, or list of arrays/Series + Values to group by in the columns. + values : array-like, optional + Array of values to aggregate according to the factors. + Requires `aggfunc` be specified. + rownames : list of str, default None + If passed, must match number of row arrays passed. + colnames : list of str, default None + If passed, must match number of column arrays passed. + aggfunc : function, optional + If specified, requires `values` be specified as well. + margins : Not supported + margins_name : Not supported + dropna : Not supported + normalize : Not supported + + Returns + ------- + DataFrame + Cross tabulation of the data. + + Examples + -------- + >>> a = cudf.Series(["foo", "foo", "foo", "foo", "bar", "bar", + ... "bar", "bar", "foo", "foo", "foo"], dtype=object) + >>> b = cudf.Series(["one", "one", "one", "two", "one", "one", + ... "one", "two", "two", "two", "one"], dtype=object) + >>> c = cudf.Series(["dull", "dull", "shiny", "dull", "dull", "shiny", + ... "shiny", "dull", "shiny", "shiny", "shiny"], + ... dtype=object) + >>> cudf.crosstab(a, [b, c], rownames=['a'], colnames=['b', 'c']) + b one two + c dull shiny dull shiny + a + bar 1 2 1 0 + foo 2 2 1 2 + """ + if normalize is not False: + raise NotImplementedError("normalize is not supported yet") + + if values is None and aggfunc is not None: + raise ValueError("aggfunc cannot be used without values.") + + if values is not None and aggfunc is None: + raise ValueError("values cannot be used without an aggfunc.") + + if not isinstance(index, (list, tuple)): + index = [index] + if not isinstance(columns, (list, tuple)): + columns = [columns] + + if not rownames: + rownames = _get_pivot_names(index, rownames, prefix="row") + if not colnames: + colnames = _get_pivot_names(columns, colnames, prefix="col") + + if len(index) != len(rownames): + raise ValueError("index and rownames must have same length") + if len(columns) != len(colnames): + raise ValueError("columns and colnames must have same length") + + if len(set(rownames)) != len(rownames): + raise ValueError("rownames must be unique") + if len(set(colnames)) != len(colnames): + raise ValueError("colnames must be unique") + + data = { + **dict(zip(rownames, map(as_column, index))), + **dict(zip(colnames, map(as_column, columns))), + } + + df = cudf.DataFrame._from_data(data) + + if values is None: + df["__dummy__"] = 0 + kwargs = {"aggfunc": "count", "fill_value": 0} + else: + df["__dummy__"] = values + kwargs = {"aggfunc": aggfunc} + + table = pivot_table( + data=df, + index=rownames, + columns=colnames, + values="__dummy__", + margins=margins, + margins_name=margins_name, + dropna=dropna, + **kwargs, + ) + + return table + + +def pivot_table( + data, + values=None, + index=None, + columns=None, + aggfunc="mean", + fill_value=None, + margins=False, + dropna=None, + margins_name="All", + observed=False, + sort=True, +): + """ + Create a spreadsheet-style pivot table as a DataFrame. + + Parameters + ---------- + data : DataFrame + values : column name or list of column names to aggregate, optional + index : list of column names + Values to group by in the rows. + columns : list of column names + Values to group by in the columns. + aggfunc : str or dict, default "mean" + If dict is passed, the key is column to aggregate + and value is function name. + fill_value : scalar, default None + Value to replace missing values with + (in the resulting pivot table, after aggregation). + margins : Not supported + dropna : Not supported + margins_name : Not supported + observed : Not supported + sort : Not supported + + Returns + ------- + DataFrame + An Excel style pivot table. + """ + if margins is not False: + raise NotImplementedError("margins is not supported yet") + + if margins_name != "All": + raise NotImplementedError("margins_name is not supported yet") + + if dropna is not None: + raise NotImplementedError("dropna is not supported yet") + + if observed is not False: + raise NotImplementedError("observed is not supported yet") + + if sort is not True: + raise NotImplementedError("sort is not supported yet") + + keys = index + columns + + values_passed = values is not None + if values_passed: + if pd.api.types.is_list_like(values): + values_multi = True + values = list(values) + else: + values_multi = False + values = [values] + + for i in values: + if i not in data: + raise KeyError(i) + + to_filter = [] + for x in keys + values: + if isinstance(x, cudf.Grouper): + x = x.key + try: + if x in data: + to_filter.append(x) + except TypeError: + pass + if len(to_filter) < len(data._column_names): + data = data[to_filter] + + else: + values = data.columns + for key in keys: + try: + values = values.drop(key) + except (TypeError, ValueError, KeyError): + pass + values = list(values) + + grouped = data.groupby(keys) + agged = grouped.agg(aggfunc) + + table = agged + + if table.index.nlevels > 1 and index: + # If index_names are integers, determine whether the integers refer + # to the level position or name. + index_names = agged.index.names[: len(index)] + to_unstack = [] + for i in range(len(index), len(keys)): + name = agged.index.names[i] + if name is None or name in index_names: + to_unstack.append(i) + else: + to_unstack.append(name) + table = agged.unstack(to_unstack) + + if fill_value is not None: + table = table.fillna(fill_value) + + # discard the top level + if values_passed and not values_multi and table._data.multiindex: + column_names = table._data.level_names[1:] + table_columns = tuple( + map(lambda column: column[1:], table._data.names) + ) + table.columns = cudf.MultiIndex.from_tuples( + tuples=table_columns, names=column_names + ) + + if len(index) == 0 and len(columns) > 0: + table = table.T + + return table diff --git a/python/cudf/cudf/tests/test_reshape.py b/python/cudf/cudf/tests/test_reshape.py index f0def3040d4..118cb4cf53c 100644 --- a/python/cudf/cudf/tests/test_reshape.py +++ b/python/cudf/cudf/tests/test_reshape.py @@ -529,3 +529,95 @@ def test_pivot_duplicate_error(): gdf.pivot(index="a", columns="b") with pytest.raises(ValueError): gdf.pivot(index="b", columns="a") + + +@pytest.mark.parametrize( + "data", + [ + { + "A": ["one", "one", "two", "three"] * 6, + "B": ["A", "B", "C"] * 8, + "C": ["foo", "foo", "foo", "bar", "bar", "bar"] * 4, + "D": np.random.randn(24), + "E": np.random.randn(24), + } + ], +) +@pytest.mark.parametrize( + "aggfunc", ["mean", "count", {"D": "sum", "E": "count"}] +) +@pytest.mark.parametrize("fill_value", [0]) +def test_pivot_table_simple(data, aggfunc, fill_value): + pdf = pd.DataFrame(data) + expected = pd.pivot_table( + pdf, + values=["D", "E"], + index=["A", "B"], + columns=["C"], + aggfunc=aggfunc, + fill_value=fill_value, + ) + cdf = cudf.DataFrame(data) + actual = cudf.pivot_table( + cdf, + values=["D", "E"], + index=["A", "B"], + columns=["C"], + aggfunc=aggfunc, + fill_value=fill_value, + ) + assert_eq(expected, actual, check_dtype=False) + + +def test_crosstab_simple(): + a = np.array( + [ + "foo", + "foo", + "foo", + "foo", + "bar", + "bar", + "bar", + "bar", + "foo", + "foo", + "foo", + ], + dtype=object, + ) + b = np.array( + [ + "one", + "one", + "one", + "two", + "one", + "one", + "one", + "two", + "two", + "two", + "one", + ], + dtype=object, + ) + c = np.array( + [ + "dull", + "dull", + "shiny", + "dull", + "dull", + "shiny", + "shiny", + "dull", + "shiny", + "shiny", + "shiny", + ], + dtype=object, + ) + expected = pd.crosstab(a, [b, c], rownames=["a"], colnames=["b", "c"]) + actual = cudf.crosstab(a, [b, c], rownames=["a"], colnames=["b", "c"]) + assert_eq(expected, actual, check_dtype=False) From 2be93fe7e7e5e8ecd6bec70792f7e6b1509b403c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Aug 2022 14:43:35 +0000 Subject: [PATCH 26/58] Bump hadoop-common from 3.2.3 to 3.2.4 in /java (#11516) Bumps hadoop-common from 3.2.3 to 3.2.4. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.hadoop:hadoop-common&package-manager=maven&previous-version=3.2.3&new-version=3.2.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) - `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language - `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language - `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language - `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/rapidsai/cudf/network/alerts).
Authors: - https://github.com/apps/dependabot Approvers: - Jason Lowe (https://github.com/jlowe) URL: https://github.com/rapidsai/cudf/pull/11516 --- java/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/pom.xml b/java/pom.xml index b9ecc2ecdfd..f2bb3def459 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -147,7 +147,7 @@ org.apache.hadoop hadoop-common - 3.2.3 + 3.2.4 test From 6035cc2f74f388fb4d5a8e470391906fccef2f4e Mon Sep 17 00:00:00 2001 From: Elias Stehle <3958403+elstehle@users.noreply.github.com> Date: Fri, 12 Aug 2022 21:21:07 +0200 Subject: [PATCH 27/58] Adds the end-to-end JSON parser implementation (#11388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR builds on the [JSON tokenizer](https://github.com/rapidsai/cudf/pull/11264) algorithm to implement an end-to-end JSON parser that parses to a `table_with_metadata`. **Chained PR depending on:** ⛓️ https://github.com/rapidsai/cudf/pull/11264 Authors: - Elias Stehle (https://github.com/elstehle) - Karthikeyan (https://github.com/karthikeyann) Approvers: - https://github.com/nvdbaranec - Bradley Dice (https://github.com/bdice) - Yunsong Wang (https://github.com/PointKernel) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/11388 --- .../cudf_test/io_metadata_utilities.hpp | 10 +- cpp/src/io/json/nested_json.hpp | 198 ++++++ cpp/src/io/json/nested_json_gpu.cu | 619 +++++++++++++++++- cpp/tests/CMakeLists.txt | 2 +- cpp/tests/io/metadata_utilities.cpp | 25 +- cpp/tests/io/nested_json_test.cpp | 506 ++++++++++++++ cpp/tests/io/nested_json_test.cu | 233 ------- 7 files changed, 1355 insertions(+), 238 deletions(-) create mode 100644 cpp/tests/io/nested_json_test.cpp delete mode 100644 cpp/tests/io/nested_json_test.cu diff --git a/cpp/include/cudf_test/io_metadata_utilities.hpp b/cpp/include/cudf_test/io_metadata_utilities.hpp index 6ca6eba6884..6fd1a52239c 100644 --- a/cpp/include/cudf_test/io_metadata_utilities.hpp +++ b/cpp/include/cudf_test/io_metadata_utilities.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * Copyright (c) 2021-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,4 +22,10 @@ namespace cudf::test { void expect_metadata_equal(cudf::io::table_input_metadata in_meta, cudf::io::table_metadata out_meta); -} +/** + * @brief Ensures that the metadata of two tables matches for the root columns as well as for all + * descendents (recursively) + */ +void expect_metadata_equal(cudf::io::table_metadata lhs_meta, cudf::io::table_metadata rhs_meta); + +} // namespace cudf::test diff --git a/cpp/src/io/json/nested_json.hpp b/cpp/src/io/json/nested_json.hpp index 3f7d73fb931..03acd393594 100644 --- a/cpp/src/io/json/nested_json.hpp +++ b/cpp/src/io/json/nested_json.hpp @@ -16,10 +16,16 @@ #pragma once +#include +#include +#include +#include #include #include +#include + namespace cudf::io::json { /// Type used to represent the atomic symbol type used within the finite-state machine @@ -46,6 +52,184 @@ using PdaSymbolGroupIdT = char; /// Type being emitted by the pushdown automaton transducer using PdaTokenT = char; +/// Type used to represent the class of a node (or a node "category") within the tree representation +using NodeT = char; + +/// Type used to index into the nodes within the tree of structs, lists, field names, and value +/// nodes +using NodeIndexT = uint32_t; + +/// Type large enough to represent tree depth from [0, max-tree-depth); may be an unsigned type +using TreeDepthT = StackLevelT; + +/** + * @brief Struct that encapsulate all information of a columnar tree representation. + */ +struct tree_meta_t { + std::vector node_categories; + std::vector parent_node_ids; + std::vector node_levels; + std::vector node_range_begin; + std::vector node_range_end; +}; + +constexpr NodeIndexT parent_node_sentinel = std::numeric_limits::max(); + +/** + * @brief Class of a node (or a node "category") within the tree representation + */ +enum node_t : NodeT { + /// A node representing a struct + NC_STRUCT, + /// A node representing a list + NC_LIST, + /// A node representing a field name + NC_FN, + /// A node representing a string value + NC_STR, + /// A node representing a numeric or literal value (e.g., true, false, null) + NC_VAL, + /// A node representing a parser error + NC_ERR, + /// Total number of node classes + NUM_NODE_CLASSES +}; + +/** + * @brief A column type + */ +enum class json_col_t : char { ListColumn, StructColumn, StringColumn, Unknown }; + +/** + * @brief Intermediate representation of data from a nested JSON input + */ +struct json_column { + // Type used to count number of rows + using row_offset_t = size_type; + + // The inferred type of this column (list, struct, or value/string column) + json_col_t type = json_col_t::Unknown; + + std::vector string_offsets; + std::vector string_lengths; + + // Row offsets + std::vector child_offsets; + + // Validity bitmap + std::vector validity; + row_offset_t valid_count = 0; + + // Map of child columns, if applicable. + // Following "items" as the default child column's name of a list column + // Using the struct's field names + std::map child_columns; + + // Counting the current number of items in this column + row_offset_t current_offset = 0; + + json_column() = default; + json_column(json_column&& other) = default; + json_column& operator=(json_column&&) = default; + json_column(const json_column&) = delete; + json_column& operator=(const json_column&) = delete; + + /** + * @brief Fills the rows up to the given \p up_to_row_offset with nulls. + * + * @param up_to_row_offset The row offset up to which to fill with nulls. + */ + void null_fill(row_offset_t up_to_row_offset) + { + // Fill all the rows up to up_to_row_offset with "empty"/null rows + validity.resize(word_index(up_to_row_offset) + 1); + std::fill_n(std::back_inserter(string_offsets), + up_to_row_offset - string_offsets.size(), + (string_offsets.size() > 0) ? string_offsets.back() : 0); + std::fill_n(std::back_inserter(string_lengths), up_to_row_offset - string_lengths.size(), 0); + std::fill_n(std::back_inserter(child_offsets), + up_to_row_offset + 1 - child_offsets.size(), + (child_offsets.size() > 0) ? child_offsets.back() : 0); + current_offset = up_to_row_offset; + } + + /** + * @brief Recursively iterates through the tree of columns making sure that all child columns of a + * struct column have the same row count, filling missing rows with nulls. + * + * @param min_row_count The minimum number of rows to be filled. + */ + void level_child_cols_recursively(row_offset_t min_row_count) + { + // Fill this columns with nulls up to the given row count + null_fill(min_row_count); + + // If this is a struct column, we need to level all its child columns + if (type == json_col_t::StructColumn) { + for (auto it = std::begin(child_columns); it != std::end(child_columns); it++) { + it->second.level_child_cols_recursively(min_row_count); + } + } + // If this is a list column, we need to make sure that its child column levels its children + else if (type == json_col_t::ListColumn) { + auto it = std::begin(child_columns); + // Make that child column fill its child columns up to its own row count + if (it != std::end(child_columns)) { + it->second.level_child_cols_recursively(it->second.current_offset); + } + } + } + + /** + * @brief Appends the row at the given index to the column, filling all rows between the column's + * current offset and the given \p row_index with null items. + * + * @param row_index The row index at which to insert the given row + * @param row_type The row's type + * @param string_offset The string offset within the original JSON input of this item + * @param string_end The one-past-the-last-char offset within the original JSON input of this item + * @param child_count In case of a list column, this row's number of children is used to compute + * the offsets + */ + void append_row(uint32_t row_index, + json_col_t const& row_type, + uint32_t string_offset, + uint32_t string_end, + uint32_t child_count) + { + // If, thus far, the column's type couldn't be inferred, we infer it to the given type + if (type == json_col_t::Unknown) { type = row_type; } + + // We shouldn't run into this, as we shouldn't be asked to append an "unknown" row type + // CUDF_EXPECTS(type != json_col_t::Unknown, "Encountered invalid JSON token sequence"); + + // Fill all the omitted rows with "empty"/null rows (if needed) + null_fill(row_index); + + // Table listing what we intend to use for a given column type and row type combination + // col type | row type => {valid, FAIL, null} + // ----------------------------------------------- + // List | List => valid + // List | Struct => FAIL + // List | String => null + // Struct | List => FAIL + // Struct | Struct => valid + // Struct | String => null + // String | List => null + // String | Struct => null + // String | String => valid + bool const is_valid = (type == row_type); + if (static_cast(validity.size()) < word_index(current_offset)) + validity.push_back({}); + set_bit_unsafe(&validity.back(), intra_word_index(current_offset)); + valid_count += (is_valid) ? 1U : 0U; + string_offsets.push_back(string_offset); + string_lengths.push_back(string_end - string_offset); + child_offsets.push_back((child_offsets.size() > 0) ? child_offsets.back() + child_count : 0); + current_offset++; + }; +}; + /** * @brief Tokens emitted while parsing a JSON input */ @@ -110,6 +294,20 @@ void get_token_stream(device_span d_json_in, SymbolOffsetT* d_tokens_indices, SymbolOffsetT* d_num_written_tokens, rmm::cuda_stream_view stream); + +/** + * @brief Parses the given JSON string and generates table from the given input. + * + * @param input The JSON input + * @param stream The CUDA stream to which kernels are dispatched + * @param mr Optional, resource with which to allocate. + * @return The data parsed from the given JSON input + */ +table_with_metadata parse_nested_json( + host_span input, + rmm::cuda_stream_view stream = cudf::default_stream_value, + rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + } // namespace detail } // namespace cudf::io::json diff --git a/cpp/src/io/json/nested_json_gpu.cu b/cpp/src/io/json/nested_json_gpu.cu index b8e05054e11..5e293f8a750 100644 --- a/cpp/src/io/json/nested_json_gpu.cu +++ b/cpp/src/io/json/nested_json_gpu.cu @@ -20,11 +20,50 @@ #include #include +#include +#include +#include +#include #include +#include +#include #include +#include #include +#include +#include + +#include + +// Debug print flag +#ifndef NJP_DEBUG_PRINT +//#define NJP_DEBUG_PRINT +#endif + +namespace { + +/** + * @brief While parsing the token stream, we use a stack of tree_nodes to maintain all the + * information about the data path that is relevant. + */ +struct tree_node { + // The column that this node is associated with + cudf::io::json::json_column* column; + + // The row offset that this node belongs to within the given column + uint32_t row_index; + + // Selected child column + // E.g., if this is a struct node, and we subsequently encountered the field name "a", then this + // point's to the struct's "a" child column + cudf::io::json::json_column* current_selected_col = nullptr; + + std::size_t num_children = 0; +}; +} // namespace + namespace cudf::io::json { // JSON to stack operator DFA (Deterministic Finite Automata) @@ -796,6 +835,584 @@ void get_token_stream(device_span json_in, stream); } -} // namespace detail +/** + * @brief Parses the given JSON string and generates a tree representation of the given input. + * + * @param[in,out] root_column The root column of the hierarchy of columns into which data is parsed + * @param[in,out] current_data_path The stack represents the path from the JSON root node to the + * first node encountered in \p input + * @param[in] input The JSON input in host memory + * @param[in] d_input The JSON input in device memory + * @param[in] stream The CUDA stream to which kernels are dispatched + * @return The columnar representation of the data from the given JSON input + */ +void make_json_column(json_column& root_column, + std::stack& current_data_path, + host_span input, + device_span d_input, + rmm::cuda_stream_view stream) +{ + // Default name for a list's child column + std::string const list_child_name = "element"; + + constexpr std::size_t single_item = 1; + hostdevice_vector tokens_gpu{input.size(), stream}; + hostdevice_vector token_indices_gpu{input.size(), stream}; + hostdevice_vector num_tokens_out{single_item, stream}; + + // Parse the JSON and get the token stream + get_token_stream(d_input, + tokens_gpu.device_ptr(), + token_indices_gpu.device_ptr(), + num_tokens_out.device_ptr(), + stream); + + // Copy the JSON tokens to the host + token_indices_gpu.device_to_host(stream); + tokens_gpu.device_to_host(stream); + num_tokens_out.device_to_host(stream); + + // Make sure tokens have been copied to the host + stream.synchronize(); + + // Whether this token is the valid token to begin the JSON document with + auto is_valid_root_token = [](PdaTokenT const token) { + switch (token) { + case token_t::StructBegin: + case token_t::ListBegin: + case token_t::StringBegin: + case token_t::ValueBegin: return true; + default: return false; + }; + }; + + // Returns the token's corresponding column type + auto token_to_column_type = [](PdaTokenT const token) { + switch (token) { + case token_t::StructBegin: return json_col_t::StructColumn; + case token_t::ListBegin: return json_col_t::ListColumn; + case token_t::StringBegin: return json_col_t::StringColumn; + case token_t::ValueBegin: return json_col_t::StringColumn; + default: return json_col_t::Unknown; + }; + }; + + // Whether this token is a beginning-of-list or beginning-of-struct token + auto is_nested_token = [](PdaTokenT const token) { + switch (token) { + case token_t::StructBegin: + case token_t::ListBegin: return true; + default: return false; + }; + }; + + // Skips the quote char if the token is a beginning-of-string or beginning-of-field-name token + auto get_token_index = [](PdaTokenT const token, SymbolOffsetT const token_index) { + constexpr SymbolOffsetT skip_quote_char = 1; + switch (token) { + case token_t::StringBegin: return token_index + skip_quote_char; + case token_t::FieldNameBegin: return token_index + skip_quote_char; + default: return token_index; + }; + }; + + // The end-of-* partner token for a given beginning-of-* token + auto end_of_partner = [](PdaTokenT const token) { + switch (token) { + case token_t::StringBegin: return token_t::StringEnd; + case token_t::ValueBegin: return token_t::ValueEnd; + case token_t::FieldNameBegin: return token_t::FieldNameEnd; + default: return token_t::ErrorBegin; + }; + }; + +#ifdef NJP_DEBUG_PRINT + auto column_type_string = [](json_col_t column_type) { + switch (column_type) { + case json_col_t::Unknown: return "Unknown"; + case json_col_t::ListColumn: return "List"; + case json_col_t::StructColumn: return "Struct"; + case json_col_t::StringColumn: return "String"; + default: return "Unknown"; + } + }; + + auto token_to_string = [](PdaTokenT token_type) { + switch (token_type) { + case token_t::StructBegin: return "StructBegin"; + case token_t::StructEnd: return "StructEnd"; + case token_t::ListBegin: return "ListBegin"; + case token_t::ListEnd: return "ListEnd"; + case token_t::FieldNameBegin: return "FieldNameBegin"; + case token_t::FieldNameEnd: return "FieldNameEnd"; + case token_t::StringBegin: return "StringBegin"; + case token_t::StringEnd: return "StringEnd"; + case token_t::ValueBegin: return "ValueBegin"; + case token_t::ValueEnd: return "ValueEnd"; + case token_t::ErrorBegin: return "ErrorBegin"; + default: return "Unknown"; + } + }; +#endif + + /** + * @brief Updates the given row in the given column with a new string_end and child_count. In + * particular, updating the child count is relevant for list columns. + */ + auto update_row = + [](json_column* column, uint32_t row_index, uint32_t string_end, uint32_t child_count) { +#ifdef NJP_DEBUG_PRINT + std::cout << " -> update_row()\n"; + std::cout << " ---> col@" << column << "\n"; + std::cout << " ---> row #" << row_index << "\n"; + std::cout << " ---> string_lengths = " << (string_end - column->string_offsets[row_index]) + << "\n"; + std::cout << " ---> child_offsets = " << (column->child_offsets[row_index + 1] + child_count) + << "\n"; +#endif + column->string_lengths[row_index] = column->child_offsets[row_index + 1] + child_count; + column->child_offsets[row_index + 1] = column->child_offsets[row_index + 1] + child_count; + }; + + /** + * @brief Gets the currently selected child column given a \p current_data_path. + * + * That is, if \p current_data_path top-of-stack is + * (a) a struct, the selected child column corresponds to the child column of the last field name + * node encountered. + * (b) a list, the selected child column corresponds to single child column of + * the list column. In this case, the child column may not exist yet. + */ + auto get_selected_column = [&list_child_name](std::stack& current_data_path) { + json_column* selected_col = current_data_path.top().current_selected_col; + + // If the node does not have a selected column yet + if (selected_col == nullptr) { + // We're looking at the child column of a list column + if (current_data_path.top().column->type == json_col_t::ListColumn) { + CUDF_EXPECTS(current_data_path.top().column->child_columns.size() <= 1, + "Encountered a list column with more than a single child column"); + // The child column has yet to be created + if (current_data_path.top().column->child_columns.size() == 0) { + current_data_path.top().column->child_columns.emplace(std::string{list_child_name}, + json_column{json_col_t::Unknown}); + } + current_data_path.top().current_selected_col = + ¤t_data_path.top().column->child_columns.begin()->second; + selected_col = current_data_path.top().current_selected_col; + } else { + CUDF_FAIL("Trying to retrieve child column without encountering a field name."); + } + } +#ifdef NJP_DEBUG_PRINT + std::cout << " -> get_selected_column()\n"; + std::cout << " ---> selected col@" << selected_col << "\n"; +#endif + return selected_col; + }; + + /** + * @brief Returns a pointer to the child column with the given \p field_name within the current + * struct column. + */ + auto select_column = [](std::stack& current_data_path, std::string const& field_name) { +#ifdef NJP_DEBUG_PRINT + std::cout << " -> select_column(" << field_name << ")\n"; +#endif + // The field name's parent struct node + auto& current_struct_node = current_data_path.top(); + + // Verify that the field name node is actually a child of a struct + CUDF_EXPECTS(current_data_path.top().column->type == json_col_t::StructColumn, + "Invalid JSON token sequence"); + + json_column* struct_col = current_struct_node.column; + auto const& child_col_it = struct_col->child_columns.find(field_name); + + // The field name's column exists already, select that as the struct node's currently selected + // child column + if (child_col_it != struct_col->child_columns.end()) { return &child_col_it->second; } + + // The field name's column does not exist yet, so we have to append the child column to the + // struct column + return &struct_col->child_columns.emplace(field_name, json_column{}).first->second; + }; + + /** + * @brief Gets the row offset at which to insert. I.e., for a child column of a list column, we + * just have to append the row to the end. Otherwise we have to propagate the row offset from the + * parent struct column. + */ + auto get_target_row_index = [](std::stack const& current_data_path, + json_column* target_column) { +#ifdef NJP_DEBUG_PRINT + std::cout << " -> target row: " + << ((current_data_path.top().column->type == json_col_t::ListColumn) + ? target_column->current_offset + : current_data_path.top().row_index) + << "\n"; +#endif + return (current_data_path.top().column->type == json_col_t::ListColumn) + ? target_column->current_offset + : current_data_path.top().row_index; + }; + + // The offset of the token currently being processed + std::size_t offset = 0; + + // Giving names to magic constants + constexpr uint32_t row_offset_zero = 0; + constexpr uint32_t zero_child_count = 0; + + //-------------------------------------------------------------------------------- + // INITIALIZE JSON ROOT NODE + //-------------------------------------------------------------------------------- + // The JSON root may only be a struct, list, string, or value node + CUDF_EXPECTS(num_tokens_out[0] > 0, "Empty JSON input not supported"); + CUDF_EXPECTS(is_valid_root_token(tokens_gpu[offset]), "Invalid beginning of JSON document"); + + // The JSON root is either a struct or list + if (is_nested_token(tokens_gpu[offset])) { + // Initialize the root column and append this row to it + root_column.append_row(row_offset_zero, + token_to_column_type(tokens_gpu[offset]), + get_token_index(tokens_gpu[offset], token_indices_gpu[offset]), + get_token_index(tokens_gpu[offset], token_indices_gpu[offset]), + 0); + + // Push the root node onto the stack for the data path + current_data_path.push({&root_column, row_offset_zero, nullptr, zero_child_count}); + + // Continue with the next token from the token stream + offset++; + } + // The JSON is a simple scalar value -> create simple table and return + else { + constexpr SymbolOffsetT max_tokens_for_scalar_value = 2; + CUDF_EXPECTS(num_tokens_out[0] <= max_tokens_for_scalar_value, + "Invalid JSON format. Expected just a scalar value."); + + // If this isn't the only token, verify the subsequent token is the correct end-of-* partner + if ((offset + 1) < num_tokens_out[0]) { + CUDF_EXPECTS(tokens_gpu[offset + 1] == end_of_partner(tokens_gpu[offset]), + "Invalid JSON token sequence"); + } + + // The offset to the first symbol from the JSON input associated with the current token + auto const& token_begin_offset = get_token_index(tokens_gpu[offset], token_indices_gpu[offset]); + + // The offset to one past the last symbol associated with the current token + // Literals without trailing space are missing the corresponding end-of-* counterpart. + auto const& token_end_offset = + (offset + 1 < num_tokens_out[0]) + ? get_token_index(tokens_gpu[offset + 1], token_indices_gpu[offset + 1]) + : input.size(); + + root_column.append_row(row_offset_zero, + json_col_t::StringColumn, + token_begin_offset, + token_end_offset, + zero_child_count); + return; + } + + while (offset < num_tokens_out[0]) { + // Verify there's at least the JSON root node left on the stack to which we can append data + CUDF_EXPECTS(current_data_path.size() > 0, "Invalid JSON structure"); + // Verify that the current node in the tree (which becomes this nodes parent) can have children + CUDF_EXPECTS(current_data_path.top().column->type == json_col_t::ListColumn or + current_data_path.top().column->type == json_col_t::StructColumn, + "Invalid JSON structure"); + + // The token we're currently parsing + auto const& token = tokens_gpu[offset]; + +#ifdef NJP_DEBUG_PRINT + std::cout << "[" << token_to_string(token) << "]\n"; +#endif + + // StructBegin token + if (token == token_t::StructBegin) { + // Get this node's column. That is, the parent node's selected column: + // (a) if parent is a list, then this will (create and) return the list's only child column + // (b) if parent is a struct, then this will return the column selected by the last field name + // encountered. + json_column* selected_col = get_selected_column(current_data_path); + + // Get the row offset at which to insert + auto const target_row_index = get_target_row_index(current_data_path, selected_col); + + // Increment parent's child count and insert this struct node into the data path + current_data_path.top().num_children++; + current_data_path.push({selected_col, target_row_index, nullptr, zero_child_count}); + + // Add this struct node to the current column + selected_col->append_row(target_row_index, + token_to_column_type(tokens_gpu[offset]), + get_token_index(tokens_gpu[offset], token_indices_gpu[offset]), + get_token_index(tokens_gpu[offset], token_indices_gpu[offset]), + zero_child_count); + } + + // StructEnd token + else if (token == token_t::StructEnd) { + // Verify that this node in fact a struct node (i.e., it was part of a struct column) + CUDF_EXPECTS(current_data_path.top().column->type == json_col_t::StructColumn, + "Broken invariant while parsing JSON"); + CUDF_EXPECTS(current_data_path.top().column != nullptr, + "Broken invariant while parsing JSON"); + + // Update row to account for string offset + update_row(current_data_path.top().column, + current_data_path.top().row_index, + get_token_index(tokens_gpu[offset], token_indices_gpu[offset]), + current_data_path.top().num_children); + + // Pop struct from the path stack + current_data_path.pop(); + } + + // ListBegin token + else if (token == token_t::ListBegin) { + // Get the selected column + json_column* selected_col = get_selected_column(current_data_path); + + // Get the row offset at which to insert + auto const target_row_index = get_target_row_index(current_data_path, selected_col); + + // Increment parent's child count and insert this struct node into the data path + current_data_path.top().num_children++; + current_data_path.push({selected_col, target_row_index, nullptr, zero_child_count}); + + // Add this struct node to the current column + selected_col->append_row(target_row_index, + token_to_column_type(tokens_gpu[offset]), + get_token_index(tokens_gpu[offset], token_indices_gpu[offset]), + get_token_index(tokens_gpu[offset], token_indices_gpu[offset]), + zero_child_count); + } + + // ListEnd token + else if (token == token_t::ListEnd) { + // Verify that this node in fact a list node (i.e., it was part of a list column) + CUDF_EXPECTS(current_data_path.top().column->type == json_col_t::ListColumn, + "Broken invariant while parsing JSON"); + CUDF_EXPECTS(current_data_path.top().column != nullptr, + "Broken invariant while parsing JSON"); + + // Update row to account for string offset + update_row(current_data_path.top().column, + current_data_path.top().row_index, + get_token_index(tokens_gpu[offset], token_indices_gpu[offset]), + current_data_path.top().num_children); + + // Pop list from the path stack + current_data_path.pop(); + } + + // Error token + else if (token == token_t::ErrorBegin) { +#ifdef NJP_DEBUG_PRINT + std::cout << "[ErrorBegin]\n"; +#endif + CUDF_FAIL("Parser encountered an invalid format."); + } + + // FieldName, String, or Value (begin, end)-pair + else if (token == token_t::FieldNameBegin or token == token_t::StringBegin or + token == token_t::ValueBegin) { + // Verify that this token has the right successor to build a correct (being, end) token pair + CUDF_EXPECTS((offset + 1) < num_tokens_out[0], "Invalid JSON token sequence"); + CUDF_EXPECTS(tokens_gpu[offset + 1] == end_of_partner(token), "Invalid JSON token sequence"); + + // The offset to the first symbol from the JSON input associated with the current token + auto const& token_begin_offset = + get_token_index(tokens_gpu[offset], token_indices_gpu[offset]); + + // The offset to one past the last symbol associated with the current token + auto const& token_end_offset = + get_token_index(tokens_gpu[offset + 1], token_indices_gpu[offset + 1]); + + // FieldNameBegin + // For the current struct node in the tree, select the child column corresponding to this + // field name + if (token == token_t::FieldNameBegin) { + std::string field_name{input.data() + token_begin_offset, + (token_end_offset - token_begin_offset)}; + current_data_path.top().current_selected_col = select_column(current_data_path, field_name); + } + // StringBegin + // ValueBegin + // As we currently parse to string columns there's no further differentiation + else if (token == token_t::StringBegin or token == token_t::ValueBegin) { + // Get the selected column + json_column* selected_col = get_selected_column(current_data_path); + + // Get the row offset at which to insert + auto const target_row_index = get_target_row_index(current_data_path, selected_col); + + current_data_path.top().num_children++; + + selected_col->append_row(target_row_index, + token_to_column_type(token), + token_begin_offset, + token_end_offset, + zero_child_count); + } else { + CUDF_FAIL("Unknown JSON token"); + } + + // As we've also consumed the end-of-* token, we advance the processed token offset by one + offset++; + } + + offset++; + } + + // Make sure all of a struct's child columns have the same length + root_column.level_child_cols_recursively(root_column.current_offset); +} + +std::pair, std::vector> json_column_to_cudf_column( + json_column const& json_col, + device_span d_input, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + auto make_validity = + [stream, mr](json_column const& json_col) -> std::pair { + if (json_col.current_offset == json_col.valid_count) { return {rmm::device_buffer{}, 0}; } + + return {rmm::device_buffer{json_col.validity.data(), + bitmask_allocation_size_bytes(json_col.current_offset), + stream, + mr}, + json_col.current_offset - json_col.valid_count}; + }; + + switch (json_col.type) { + case json_col_t::StringColumn: { + // move string_offsets to GPU and transform to string column + auto const col_size = json_col.string_offsets.size(); + using char_length_pair_t = thrust::pair; + CUDF_EXPECTS(json_col.string_offsets.size() == json_col.string_lengths.size(), + "string offset, string length mismatch"); + rmm::device_uvector d_string_data(col_size, stream); + rmm::device_uvector d_string_offsets = + cudf::detail::make_device_uvector_async(json_col.string_offsets, stream); + rmm::device_uvector d_string_lengths = + cudf::detail::make_device_uvector_async(json_col.string_lengths, stream); + auto offset_length_it = + thrust::make_zip_iterator(d_string_offsets.begin(), d_string_lengths.begin()); + thrust::transform(rmm::exec_policy(stream), + offset_length_it, + offset_length_it + col_size, + d_string_data.data(), + [data = d_input.data()] __device__(auto ip) { + return char_length_pair_t{data + thrust::get<0>(ip), thrust::get<1>(ip)}; + }); + auto str_col_ptr = make_strings_column(d_string_data, stream, mr); + auto [result_bitmask, null_count] = make_validity(json_col); + str_col_ptr->set_null_mask(result_bitmask, null_count); + return {std::move(str_col_ptr), {{"offsets"}, {"chars"}}}; + break; + } + case json_col_t::StructColumn: { + std::vector> child_columns; + std::vector column_names{}; + size_type num_rows{json_col.current_offset}; + // Create children columns + for (auto const& col : json_col.child_columns) { + column_names.emplace_back(col.first); + auto const& child_col = col.second; + auto [child_column, names] = json_column_to_cudf_column(child_col, d_input, stream, mr); + CUDF_EXPECTS(num_rows == child_column->size(), + "All children columns must have the same size"); + child_columns.push_back(std::move(child_column)); + column_names.back().children = names; + } + auto [result_bitmask, null_count] = make_validity(json_col); + return { + make_structs_column( + num_rows, std::move(child_columns), null_count, std::move(result_bitmask), stream, mr), + column_names}; + break; + } + case json_col_t::ListColumn: { + size_type num_rows = json_col.child_offsets.size(); + std::vector column_names{}; + column_names.emplace_back("offsets"); + column_names.emplace_back(json_col.child_columns.begin()->first); + + rmm::device_uvector d_offsets = + cudf::detail::make_device_uvector_async(json_col.child_offsets, stream, mr); + auto offsets_column = + std::make_unique(data_type{type_id::INT32}, num_rows, d_offsets.release()); + // Create children column + auto [child_column, names] = + json_column_to_cudf_column(json_col.child_columns.begin()->second, d_input, stream, mr); + column_names.back().children = names; + auto [result_bitmask, null_count] = make_validity(json_col); + return {make_lists_column(num_rows - 1, + std::move(offsets_column), + std::move(child_column), + null_count, + std::move(result_bitmask), + stream, + mr), + std::move(column_names)}; + break; + } + default: CUDF_FAIL("Unsupported column type, yet to be implemented"); break; + } + + return {}; +} + +table_with_metadata parse_nested_json(host_span input, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + // Allocate device memory for the JSON input & copy over to device + rmm::device_uvector d_input = cudf::detail::make_device_uvector_async(input, stream); + + // Get internal JSON column + json_column root_column{}; + std::stack data_path{}; + make_json_column(root_column, data_path, input, d_input, stream); + + // Verify that we were in fact given a list of structs (or in JSON speech: an array of objects) + auto constexpr single_child_col_count = 1; + CUDF_EXPECTS(root_column.type == json_col_t::ListColumn and + root_column.child_columns.size() == single_child_col_count and + root_column.child_columns.begin()->second.type == json_col_t::StructColumn, + "Currently the nested JSON parser only supports an array of (nested) objects"); + + // Slice off the root list column, which has only a single row that contains all the structs + auto const& root_struct_col = root_column.child_columns.begin()->second; + + // Initialize meta data to be populated while recursing through the tree of columns + std::vector> out_columns; + std::vector out_column_names; + + // Iterate over the struct's child columns and convert to cudf column + for (auto const& [col_name, json_col] : root_struct_col.child_columns) { + // Insert this columns name into the schema + out_column_names.emplace_back(col_name); + + // Get this JSON column's cudf column and schema info + auto [cudf_col, col_name_info] = json_column_to_cudf_column(json_col, d_input, stream, mr); + out_column_names.back().children = std::move(col_name_info); + out_columns.emplace_back(std::move(cudf_col)); + } + + return table_with_metadata{std::make_unique(std::move(out_columns)), + {{}, out_column_names}}; +} + +} // namespace detail } // namespace cudf::io::json + +// Debug print flag +#undef NJP_DEBUG_PRINT diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index be610d33b1b..8aba2a11d10 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -222,7 +222,7 @@ ConfigureTest(FILE_IO_TEST io/file_io_test.cpp) ConfigureTest(ORC_TEST io/orc_test.cpp) ConfigureTest(PARQUET_TEST io/parquet_test.cpp) ConfigureTest(JSON_TEST io/json_test.cpp) -ConfigureTest(NESTED_JSON_TEST io/nested_json_test.cu) +ConfigureTest(NESTED_JSON_TEST io/nested_json_test.cpp) ConfigureTest(ARROW_IO_SOURCE_TEST io/arrow_io_source_test.cpp) ConfigureTest(MULTIBYTE_SPLIT_TEST io/text/multibyte_split_test.cpp) ConfigureTest(LOGICAL_STACK_TEST io/fst/logical_stack_test.cu) diff --git a/cpp/tests/io/metadata_utilities.cpp b/cpp/tests/io/metadata_utilities.cpp index 39617c99690..84f04f67038 100644 --- a/cpp/tests/io/metadata_utilities.cpp +++ b/cpp/tests/io/metadata_utilities.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * Copyright (c) 2021-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,4 +39,27 @@ void expect_metadata_equal(cudf::io::table_input_metadata in_meta, } } +void expect_metadata_equal(cudf::io::table_metadata lhs_meta, cudf::io::table_metadata rhs_meta) +{ + std::function compare_names = + [&](cudf::io::column_name_info lhs, cudf::io::column_name_info rhs) { + // Ensure column names match + EXPECT_EQ(lhs.name, rhs.name); + + // Ensure number of child columns match + ASSERT_EQ(lhs.children.size(), rhs.children.size()); + for (size_t i = 0; i < lhs.children.size(); ++i) { + compare_names(lhs.children[i], rhs.children[i]); + } + }; + + // Ensure the number of columns at the root level matches + ASSERT_EQ(lhs_meta.schema_info.size(), rhs_meta.schema_info.size()); + + // Recurse for each column making sure their names and descendants match + for (size_t i = 0; i < rhs_meta.schema_info.size(); ++i) { + compare_names(lhs_meta.schema_info[i], rhs_meta.schema_info[i]); + } +} + } // namespace cudf::test diff --git a/cpp/tests/io/nested_json_test.cpp b/cpp/tests/io/nested_json_test.cpp new file mode 100644 index 00000000000..d426acf26f9 --- /dev/null +++ b/cpp/tests/io/nested_json_test.cpp @@ -0,0 +1,506 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace cuio_json = cudf::io::json; + +namespace { +// Forward declaration +void print_column(std::string const& input, + cuio_json::json_column const& column, + uint32_t indent = 0); + +/** + * @brief Helper to generate indentation + */ +std::string pad(uint32_t indent = 0) +{ + std::string pad{}; + if (indent > 0) pad.insert(pad.begin(), indent, ' '); + return pad; +} + +/** + * @brief Prints a string column. + */ +void print_json_string_col(std::string const& input, + cuio_json::json_column const& column, + uint32_t indent = 0) +{ + for (std::size_t i = 0; i < column.string_offsets.size(); i++) { + std::cout << pad(indent) << i << ": [" << (column.validity[i] ? "1" : "0") << "] '" + << input.substr(column.string_offsets[i], column.string_lengths[i]) << "'\n"; + } +} + +/** + * @brief Prints a list column. + */ +void print_json_list_col(std::string const& input, + cuio_json::json_column const& column, + uint32_t indent = 0) +{ + std::cout << pad(indent) << " [LIST]\n"; + std::cout << pad(indent) << " -> num. child-columns: " << column.child_columns.size() << "\n"; + std::cout << pad(indent) << " -> num. rows: " << column.current_offset << "\n"; + std::cout << pad(indent) << " -> num. valid: " << column.valid_count << "\n"; + std::cout << pad(indent) << " offsets[]: " + << "\n"; + for (std::size_t i = 0; i < column.child_offsets.size() - 1; i++) { + std::cout << pad(indent + 2) << i << ": [" << (column.validity[i] ? "1" : "0") << "] [" + << column.child_offsets[i] << ", " << column.child_offsets[i + 1] << ")\n"; + } + if (column.child_columns.size() > 0) { + std::cout << pad(indent) << column.child_columns.begin()->first << "[]: " + << "\n"; + print_column(input, column.child_columns.begin()->second, indent + 2); + } +} + +/** + * @brief Prints a struct column. + */ +void print_json_struct_col(std::string const& input, + cuio_json::json_column const& column, + uint32_t indent = 0) +{ + std::cout << pad(indent) << " [STRUCT]\n"; + std::cout << pad(indent) << " -> num. child-columns: " << column.child_columns.size() << "\n"; + std::cout << pad(indent) << " -> num. rows: " << column.current_offset << "\n"; + std::cout << pad(indent) << " -> num. valid: " << column.valid_count << "\n"; + std::cout << pad(indent) << " -> validity[]: " + << "\n"; + for (decltype(column.current_offset) i = 0; i < column.current_offset; i++) { + std::cout << pad(indent + 2) << i << ": [" << (column.validity[i] ? "1" : "0") << "]\n"; + } + auto it = std::begin(column.child_columns); + for (std::size_t i = 0; i < column.child_columns.size(); i++) { + std::cout << pad(indent + 2) << "child #" << i << " '" << it->first << "'[] \n"; + print_column(input, it->second, indent + 2); + it++; + } +} + +/** + * @brief Prints the column's data and recurses through and prints all the child columns. + */ +void print_column(std::string const& input, cuio_json::json_column const& column, uint32_t indent) +{ + switch (column.type) { + case cuio_json::json_col_t::StringColumn: print_json_string_col(input, column, indent); break; + case cuio_json::json_col_t::ListColumn: print_json_list_col(input, column, indent); break; + case cuio_json::json_col_t::StructColumn: print_json_struct_col(input, column, indent); break; + case cuio_json::json_col_t::Unknown: std::cout << pad(indent) << "[UNKNOWN]\n"; break; + default: break; + } +} +} // namespace + +// Base test fixture for tests +struct JsonTest : public cudf::test::BaseFixture { +}; + +TEST_F(JsonTest, StackContext) +{ + // Type used to represent the atomic symbol type used within the finite-state machine + using SymbolT = char; + using StackSymbolT = char; + + // Prepare cuda stream for data transfers & kernels + rmm::cuda_stream stream{}; + rmm::cuda_stream_view stream_view(stream); + + // Test input + std::string input = R"( [{)" + R"("category": "reference",)" + R"("index:": [4,12,42],)" + R"("author": "Nigel Rees",)" + R"("title": "[Sayings of the Century]",)" + R"("price": 8.95)" + R"(}, )" + R"({)" + R"("category": "reference",)" + R"("index": [4,{},null,{"a":[{ }, {}] } ],)" + R"("author": "Nigel Rees",)" + R"("title": "{}\\\"[], <=semantic-symbols-string\\\\",)" + R"("price": 8.95)" + R"(}] )"; + + // Prepare input & output buffers + rmm::device_uvector d_input(input.size(), stream_view); + hostdevice_vector stack_context(input.size(), stream_view); + + ASSERT_CUDA_SUCCEEDED(cudaMemcpyAsync(d_input.data(), + input.data(), + input.size() * sizeof(SymbolT), + cudaMemcpyHostToDevice, + stream.value())); + + // Run algorithm + cuio_json::detail::get_stack_context(d_input, stack_context.device_ptr(), stream_view); + + // Copy back the results + stack_context.device_to_host(stream_view); + + // Make sure we copied back the stack context + stream_view.synchronize(); + + std::vector golden_stack_context{ + '_', '_', '_', '[', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '[', '[', '[', '[', '[', '[', '[', '[', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '[', '[', '[', '[', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '[', '[', '[', '{', '[', '[', '[', '[', '[', '[', '[', '{', + '{', '{', '{', '{', '[', '{', '{', '[', '[', '[', '{', '[', '{', '{', '[', '[', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '[', '_'}; + + ASSERT_EQ(golden_stack_context.size(), stack_context.size()); + CUDF_TEST_EXPECT_VECTOR_EQUAL(golden_stack_context, stack_context, stack_context.size()); +} + +TEST_F(JsonTest, StackContextUtf8) +{ + // Type used to represent the atomic symbol type used within the finite-state machine + using SymbolT = char; + using StackSymbolT = char; + + // Prepare cuda stream for data transfers & kernels + rmm::cuda_stream stream{}; + rmm::cuda_stream_view stream_view(stream); + + // Test input + std::string input = R"([{"a":{"year":1882,"author": "Bharathi"}, {"a":"filip ʒakotɛ"}}])"; + + // Prepare input & output buffers + rmm::device_uvector d_input(input.size(), stream_view); + hostdevice_vector stack_context(input.size(), stream_view); + + ASSERT_CUDA_SUCCEEDED(cudaMemcpyAsync(d_input.data(), + input.data(), + input.size() * sizeof(SymbolT), + cudaMemcpyHostToDevice, + stream.value())); + + // Run algorithm + cuio_json::detail::get_stack_context(d_input, stack_context.device_ptr(), stream_view); + + // Copy back the results + stack_context.device_to_host(stream_view); + + // Make sure we copied back the stack context + stream_view.synchronize(); + + std::vector golden_stack_context{ + '_', '[', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', + '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '['}; + + ASSERT_EQ(golden_stack_context.size(), stack_context.size()); + CUDF_TEST_EXPECT_VECTOR_EQUAL(golden_stack_context, stack_context, stack_context.size()); +} + +TEST_F(JsonTest, TokenStream) +{ + using cuio_json::PdaTokenT; + using cuio_json::SymbolOffsetT; + using cuio_json::SymbolT; + + constexpr std::size_t single_item = 1; + + // Prepare cuda stream for data transfers & kernels + rmm::cuda_stream stream{}; + rmm::cuda_stream_view stream_view(stream); + + // Test input + std::string input = R"( [{)" + R"("category": "reference",)" + R"("index:": [4,12,42],)" + R"("author": "Nigel Rees",)" + R"("title": "[Sayings of the Century]",)" + R"("price": 8.95)" + R"(}, )" + R"({)" + R"("category": "reference",)" + R"("index": [4,{},null,{"a":[{ }, {}] } ],)" + R"("author": "Nigel Rees",)" + R"("title": "{}[], <=semantic-symbols-string",)" + R"("price": 8.95)" + R"(}] )"; + + // Prepare input & output buffers + rmm::device_uvector d_input(input.size(), stream_view); + + ASSERT_CUDA_SUCCEEDED(cudaMemcpyAsync(d_input.data(), + input.data(), + input.size() * sizeof(SymbolT), + cudaMemcpyHostToDevice, + stream.value())); + + hostdevice_vector tokens_gpu{input.size(), stream_view}; + hostdevice_vector token_indices_gpu{input.size(), stream_view}; + hostdevice_vector num_tokens_out{single_item, stream_view}; + + // Parse the JSON and get the token stream + cuio_json::detail::get_token_stream(d_input, + tokens_gpu.device_ptr(), + token_indices_gpu.device_ptr(), + num_tokens_out.device_ptr(), + stream_view); + + // Copy back the number of tokens that were written + num_tokens_out.device_to_host(stream_view); + tokens_gpu.device_to_host(stream_view); + token_indices_gpu.device_to_host(stream_view); + + // Make sure we copied back all relevant data + stream_view.synchronize(); + + // Golden token stream sample + using token_t = cuio_json::token_t; + std::vector> golden_token_stream = { + {2, token_t::ListBegin}, {3, token_t::StructBegin}, {4, token_t::FieldNameBegin}, + {13, token_t::FieldNameEnd}, {16, token_t::StringBegin}, {26, token_t::StringEnd}, + {28, token_t::FieldNameBegin}, {35, token_t::FieldNameEnd}, {38, token_t::ListBegin}, + {39, token_t::ValueBegin}, {40, token_t::ValueEnd}, {41, token_t::ValueBegin}, + {43, token_t::ValueEnd}, {44, token_t::ValueBegin}, {46, token_t::ValueEnd}, + {46, token_t::ListEnd}, {48, token_t::FieldNameBegin}, {55, token_t::FieldNameEnd}, + {58, token_t::StringBegin}, {69, token_t::StringEnd}, {71, token_t::FieldNameBegin}, + {77, token_t::FieldNameEnd}, {80, token_t::StringBegin}, {105, token_t::StringEnd}, + {107, token_t::FieldNameBegin}, {113, token_t::FieldNameEnd}, {116, token_t::ValueBegin}, + {120, token_t::ValueEnd}, {120, token_t::StructEnd}, {124, token_t::StructBegin}, + {125, token_t::FieldNameBegin}, {134, token_t::FieldNameEnd}, {137, token_t::StringBegin}, + {147, token_t::StringEnd}, {149, token_t::FieldNameBegin}, {155, token_t::FieldNameEnd}, + {158, token_t::ListBegin}, {159, token_t::ValueBegin}, {160, token_t::ValueEnd}, + {161, token_t::StructBegin}, {162, token_t::StructEnd}, {164, token_t::ValueBegin}, + {168, token_t::ValueEnd}, {169, token_t::StructBegin}, {170, token_t::FieldNameBegin}, + {172, token_t::FieldNameEnd}, {174, token_t::ListBegin}, {175, token_t::StructBegin}, + {177, token_t::StructEnd}, {180, token_t::StructBegin}, {181, token_t::StructEnd}, + {182, token_t::ListEnd}, {184, token_t::StructEnd}, {186, token_t::ListEnd}, + {188, token_t::FieldNameBegin}, {195, token_t::FieldNameEnd}, {198, token_t::StringBegin}, + {209, token_t::StringEnd}, {211, token_t::FieldNameBegin}, {217, token_t::FieldNameEnd}, + {220, token_t::StringBegin}, {252, token_t::StringEnd}, {254, token_t::FieldNameBegin}, + {260, token_t::FieldNameEnd}, {263, token_t::ValueBegin}, {267, token_t::ValueEnd}, + {267, token_t::StructEnd}, {268, token_t::ListEnd}}; + + // Verify the number of tokens matches + ASSERT_EQ(golden_token_stream.size(), num_tokens_out[0]); + + for (std::size_t i = 0; i < num_tokens_out[0]; i++) { + // Ensure the index the tokens are pointing to do match + EXPECT_EQ(golden_token_stream[i].first, token_indices_gpu[i]) << "Mismatch at #" << i; + + // Ensure the token category is correct + EXPECT_EQ(golden_token_stream[i].second, tokens_gpu[i]) << "Mismatch at #" << i; + } +} + +TEST_F(JsonTest, ExtractColumn) +{ + using cuio_json::SymbolT; + + // Prepare cuda stream for data transfers & kernels + rmm::cuda_stream stream{}; + rmm::cuda_stream_view stream_view(stream); + + std::string input = R"( [{"a":0.0, "b":1.0}, {"a":0.1, "b":1.1}, {"a":0.2, "b":1.2}] )"; + // Get the JSON's tree representation + auto const cudf_table = cuio_json::detail::parse_nested_json( + cudf::host_span{input.data(), input.size()}, stream_view); + + auto const expected_col_count = 2; + auto const first_column_index = 0; + auto const second_column_index = 1; + EXPECT_EQ(cudf_table.tbl->num_columns(), expected_col_count); + + auto expected_col1 = cudf::test::strings_column_wrapper({"0.0", "0.1", "0.2"}); + auto expected_col2 = cudf::test::strings_column_wrapper({"1.0", "1.1", "1.2"}); + cudf::column_view parsed_col1 = cudf_table.tbl->get_column(first_column_index); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_col1, parsed_col1); + cudf::column_view parsed_col2 = cudf_table.tbl->get_column(second_column_index); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_col2, parsed_col2); +} + +TEST_F(JsonTest, UTF_JSON) +{ + // Prepare cuda stream for data transfers & kernels + rmm::cuda_stream stream{}; + rmm::cuda_stream_view stream_view(stream); + + // Only ASCII string + std::string ascii_pass = R"([ + {"a":1,"b":2,"c":[3], "d": {}}, + {"a":1,"b":4.0,"c":[], "d": {"year":1882,"author": "Bharathi"}}, + {"a":1,"b":6.0,"c":[5, 7], "d": null}, + {"a":1,"b":8.0,"c":null, "d": {}}, + {"a":1,"b":null,"c":null}, + {"a":1,"b":Infinity,"c":[null], "d": {"year":-600,"author": "Kaniyan"}}])"; + + CUDF_EXPECT_NO_THROW(cuio_json::detail::parse_nested_json(ascii_pass, stream_view)); + + // utf-8 string that fails parsing. + std::string utf_failed = R"([ + {"a":1,"b":2,"c":[3], "d": {}}, + {"a":1,"b":4.0,"c":[], "d": {"year":1882,"author": "Bharathi"}}, + {"a":1,"b":6.0,"c":[5, 7], "d": null}, + {"a":1,"b":8.0,"c":null, "d": {}}, + {"a":1,"b":null,"c":null}, + {"a":1,"b":Infinity,"c":[null], "d": {"year":-600,"author": "filip ʒakotɛ"}}])"; + CUDF_EXPECT_NO_THROW(cuio_json::detail::parse_nested_json(utf_failed, stream_view)); + + // utf-8 string that passes parsing. + std::string utf_pass = R"([ + {"a":1,"b":2,"c":[3], "d": {}}, + {"a":1,"b":4.0,"c":[], "d": {"year":1882,"author": "Bharathi"}}, + {"a":1,"b":6.0,"c":[5, 7], "d": null}, + {"a":1,"b":8.0,"c":null, "d": {}}, + {"a":1,"b":null,"c":null}, + {"a":1,"b":Infinity,"c":[null], "d": {"year":-600,"author": "Kaniyan"}}, + {"a":1,"b":NaN,"c":[null, null], "d": {"year": 2, "author": "filip ʒakotɛ"}}])"; + CUDF_EXPECT_NO_THROW(cuio_json::detail::parse_nested_json(utf_pass, stream_view)); +} + +TEST_F(JsonTest, FromParquet) +{ + using cuio_json::SymbolT; + + std::string input = + R"([{"0":{},"1":[],"2":{}},{"1":[[""],[]],"2":{"2":""}},{"0":{"a":"1"},"2":{"0":"W&RR=+I","1":""}}])"; + + // Prepare cuda stream for data transfers & kernels + rmm::cuda_stream stream{}; + rmm::cuda_stream_view stream_view(stream); + + // Binary parquet data containing the same data as the data represented by the JSON string. + // We could add a dataset to include this file, but we don't want tests in cudf to have data. + const unsigned char parquet_data[] = { + 0x50, 0x41, 0x52, 0x31, 0x15, 0x00, 0x15, 0x18, 0x15, 0x18, 0x2C, 0x15, 0x06, 0x15, 0x00, 0x15, + 0x06, 0x15, 0x06, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x21, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x31, 0x15, 0x00, 0x15, 0x24, 0x15, 0x20, 0x2C, 0x15, 0x08, 0x15, 0x00, 0x15, 0x06, 0x15, 0x06, + 0x00, 0x00, 0x12, 0x18, 0x03, 0x00, 0x00, 0x00, 0x03, 0x10, 0x00, 0x05, 0x07, 0x04, 0x2D, 0x00, + 0x01, 0x01, 0x15, 0x00, 0x15, 0x22, 0x15, 0x22, 0x2C, 0x15, 0x06, 0x15, 0x00, 0x15, 0x06, 0x15, + 0x06, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x04, 0x07, 0x00, 0x00, 0x00, 0x57, 0x26, 0x52, + 0x52, 0x3D, 0x2B, 0x49, 0x15, 0x00, 0x15, 0x14, 0x15, 0x14, 0x2C, 0x15, 0x06, 0x15, 0x00, 0x15, + 0x06, 0x15, 0x06, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0x15, + 0x00, 0x15, 0x14, 0x15, 0x14, 0x2C, 0x15, 0x06, 0x15, 0x00, 0x15, 0x06, 0x15, 0x06, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x15, 0x02, 0x19, 0xCC, 0x48, 0x06, + 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x15, 0x06, 0x00, 0x35, 0x02, 0x18, 0x01, 0x30, 0x15, 0x02, + 0x00, 0x15, 0x0C, 0x25, 0x02, 0x18, 0x01, 0x61, 0x25, 0x00, 0x00, 0x35, 0x02, 0x18, 0x01, 0x31, + 0x15, 0x02, 0x15, 0x06, 0x00, 0x35, 0x04, 0x18, 0x04, 0x6C, 0x69, 0x73, 0x74, 0x15, 0x02, 0x00, + 0x35, 0x00, 0x18, 0x07, 0x65, 0x6C, 0x65, 0x6D, 0x65, 0x6E, 0x74, 0x15, 0x02, 0x15, 0x06, 0x00, + 0x35, 0x04, 0x18, 0x04, 0x6C, 0x69, 0x73, 0x74, 0x15, 0x02, 0x00, 0x15, 0x0C, 0x25, 0x00, 0x18, + 0x07, 0x65, 0x6C, 0x65, 0x6D, 0x65, 0x6E, 0x74, 0x25, 0x00, 0x00, 0x35, 0x00, 0x18, 0x01, 0x32, + 0x15, 0x06, 0x00, 0x15, 0x0C, 0x25, 0x02, 0x18, 0x01, 0x30, 0x25, 0x00, 0x00, 0x15, 0x0C, 0x25, + 0x02, 0x18, 0x01, 0x31, 0x25, 0x00, 0x00, 0x15, 0x0C, 0x25, 0x02, 0x18, 0x01, 0x32, 0x25, 0x00, + 0x00, 0x16, 0x06, 0x19, 0x1C, 0x19, 0x5C, 0x26, 0x00, 0x1C, 0x15, 0x0C, 0x19, 0x25, 0x00, 0x06, + 0x19, 0x28, 0x01, 0x30, 0x01, 0x61, 0x15, 0x00, 0x16, 0x06, 0x16, 0x3A, 0x16, 0x3A, 0x26, 0x08, + 0x3C, 0x36, 0x04, 0x28, 0x01, 0x31, 0x18, 0x01, 0x31, 0x00, 0x00, 0x00, 0x26, 0x00, 0x1C, 0x15, + 0x0C, 0x19, 0x25, 0x00, 0x06, 0x19, 0x58, 0x01, 0x31, 0x04, 0x6C, 0x69, 0x73, 0x74, 0x07, 0x65, + 0x6C, 0x65, 0x6D, 0x65, 0x6E, 0x74, 0x04, 0x6C, 0x69, 0x73, 0x74, 0x07, 0x65, 0x6C, 0x65, 0x6D, + 0x65, 0x6E, 0x74, 0x15, 0x02, 0x16, 0x08, 0x16, 0x46, 0x16, 0x42, 0x26, 0x42, 0x3C, 0x36, 0x00, + 0x28, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x1C, 0x15, 0x0C, 0x19, 0x25, 0x00, 0x06, + 0x19, 0x28, 0x01, 0x32, 0x01, 0x30, 0x15, 0x00, 0x16, 0x06, 0x16, 0x44, 0x16, 0x44, 0x26, 0x84, + 0x01, 0x3C, 0x36, 0x04, 0x28, 0x07, 0x57, 0x26, 0x52, 0x52, 0x3D, 0x2B, 0x49, 0x18, 0x07, 0x57, + 0x26, 0x52, 0x52, 0x3D, 0x2B, 0x49, 0x00, 0x00, 0x00, 0x26, 0x00, 0x1C, 0x15, 0x0C, 0x19, 0x25, + 0x00, 0x06, 0x19, 0x28, 0x01, 0x32, 0x01, 0x31, 0x15, 0x00, 0x16, 0x06, 0x16, 0x36, 0x16, 0x36, + 0x26, 0xC8, 0x01, 0x3C, 0x36, 0x04, 0x28, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x1C, + 0x15, 0x0C, 0x19, 0x25, 0x00, 0x06, 0x19, 0x28, 0x01, 0x32, 0x01, 0x32, 0x15, 0x00, 0x16, 0x06, + 0x16, 0x36, 0x16, 0x36, 0x26, 0xFE, 0x01, 0x3C, 0x36, 0x04, 0x28, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x00, 0x16, 0xAC, 0x02, 0x16, 0x06, 0x00, 0x19, 0x1C, 0x18, 0x06, 0x70, 0x61, 0x6E, 0x64, 0x61, + 0x73, 0x18, 0xFE, 0x04, 0x7B, 0x22, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x5F, 0x63, 0x6F, 0x6C, 0x75, + 0x6D, 0x6E, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x7B, 0x22, 0x6B, 0x69, 0x6E, 0x64, 0x22, 0x3A, 0x20, + 0x22, 0x72, 0x61, 0x6E, 0x67, 0x65, 0x22, 0x2C, 0x20, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, + 0x20, 0x6E, 0x75, 0x6C, 0x6C, 0x2C, 0x20, 0x22, 0x73, 0x74, 0x61, 0x72, 0x74, 0x22, 0x3A, 0x20, + 0x30, 0x2C, 0x20, 0x22, 0x73, 0x74, 0x6F, 0x70, 0x22, 0x3A, 0x20, 0x33, 0x2C, 0x20, 0x22, 0x73, + 0x74, 0x65, 0x70, 0x22, 0x3A, 0x20, 0x31, 0x7D, 0x5D, 0x2C, 0x20, 0x22, 0x63, 0x6F, 0x6C, 0x75, + 0x6D, 0x6E, 0x5F, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x65, 0x73, 0x22, 0x3A, 0x20, 0x5B, 0x7B, 0x22, + 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x6E, 0x75, 0x6C, 0x6C, 0x2C, 0x20, 0x22, 0x66, 0x69, + 0x65, 0x6C, 0x64, 0x5F, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x6E, 0x75, 0x6C, 0x6C, 0x2C, + 0x20, 0x22, 0x70, 0x61, 0x6E, 0x64, 0x61, 0x73, 0x5F, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, + 0x22, 0x75, 0x6E, 0x69, 0x63, 0x6F, 0x64, 0x65, 0x22, 0x2C, 0x20, 0x22, 0x6E, 0x75, 0x6D, 0x70, + 0x79, 0x5F, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, + 0x22, 0x2C, 0x20, 0x22, 0x6D, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3A, 0x20, 0x7B, + 0x22, 0x65, 0x6E, 0x63, 0x6F, 0x64, 0x69, 0x6E, 0x67, 0x22, 0x3A, 0x20, 0x22, 0x55, 0x54, 0x46, + 0x2D, 0x38, 0x22, 0x7D, 0x7D, 0x5D, 0x2C, 0x20, 0x22, 0x63, 0x6F, 0x6C, 0x75, 0x6D, 0x6E, 0x73, + 0x22, 0x3A, 0x20, 0x5B, 0x7B, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x30, 0x22, + 0x2C, 0x20, 0x22, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x5F, 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, + 0x22, 0x30, 0x22, 0x2C, 0x20, 0x22, 0x70, 0x61, 0x6E, 0x64, 0x61, 0x73, 0x5F, 0x74, 0x79, 0x70, + 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x20, 0x22, 0x6E, + 0x75, 0x6D, 0x70, 0x79, 0x5F, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, + 0x65, 0x63, 0x74, 0x22, 0x2C, 0x20, 0x22, 0x6D, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, + 0x3A, 0x20, 0x6E, 0x75, 0x6C, 0x6C, 0x7D, 0x2C, 0x20, 0x7B, 0x22, 0x6E, 0x61, 0x6D, 0x65, 0x22, + 0x3A, 0x20, 0x22, 0x31, 0x22, 0x2C, 0x20, 0x22, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x5F, 0x6E, 0x61, + 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x31, 0x22, 0x2C, 0x20, 0x22, 0x70, 0x61, 0x6E, 0x64, 0x61, + 0x73, 0x5F, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6C, 0x69, 0x73, 0x74, 0x5B, 0x6C, + 0x69, 0x73, 0x74, 0x5B, 0x75, 0x6E, 0x69, 0x63, 0x6F, 0x64, 0x65, 0x5D, 0x5D, 0x22, 0x2C, 0x20, + 0x22, 0x6E, 0x75, 0x6D, 0x70, 0x79, 0x5F, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, + 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x20, 0x22, 0x6D, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x22, 0x3A, 0x20, 0x6E, 0x75, 0x6C, 0x6C, 0x7D, 0x2C, 0x20, 0x7B, 0x22, 0x6E, 0x61, 0x6D, + 0x65, 0x22, 0x3A, 0x20, 0x22, 0x32, 0x22, 0x2C, 0x20, 0x22, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x5F, + 0x6E, 0x61, 0x6D, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x32, 0x22, 0x2C, 0x20, 0x22, 0x70, 0x61, 0x6E, + 0x64, 0x61, 0x73, 0x5F, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, + 0x63, 0x74, 0x22, 0x2C, 0x20, 0x22, 0x6E, 0x75, 0x6D, 0x70, 0x79, 0x5F, 0x74, 0x79, 0x70, 0x65, + 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x22, 0x2C, 0x20, 0x22, 0x6D, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3A, 0x20, 0x6E, 0x75, 0x6C, 0x6C, 0x7D, 0x5D, 0x2C, + 0x20, 0x22, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6F, 0x72, 0x22, 0x3A, 0x20, 0x7B, 0x22, 0x6C, 0x69, + 0x62, 0x72, 0x61, 0x72, 0x79, 0x22, 0x3A, 0x20, 0x22, 0x70, 0x79, 0x61, 0x72, 0x72, 0x6F, 0x77, + 0x22, 0x2C, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x38, + 0x2E, 0x30, 0x2E, 0x31, 0x22, 0x7D, 0x2C, 0x20, 0x22, 0x70, 0x61, 0x6E, 0x64, 0x61, 0x73, 0x5F, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x20, 0x22, 0x31, 0x2E, 0x34, 0x2E, 0x33, + 0x22, 0x7D, 0x00, 0x29, 0x5C, 0x1C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0B, 0x04, 0x00, 0x00, 0x50, 0x41, 0x52, 0x31}; + + // Read in the data via parquet reader + cudf::io::parquet_reader_options read_opts = cudf::io::parquet_reader_options::builder( + cudf::io::source_info{reinterpret_cast(parquet_data), sizeof(parquet_data)}); + auto result = cudf::io::read_parquet(read_opts); + + // Read in the data via the JSON parser + auto const cudf_table = cuio_json::detail::parse_nested_json( + cudf::host_span{input.data(), input.size()}, stream_view); + + // Verify that the data read via parquet matches the data read via JSON + CUDF_TEST_EXPECT_TABLES_EQUAL(cudf_table.tbl->view(), result.tbl->view()); + + // Verify that the schema read via parquet matches the schema read via JSON + cudf::test::expect_metadata_equal(cudf_table.metadata, result.metadata); +} diff --git a/cpp/tests/io/nested_json_test.cu b/cpp/tests/io/nested_json_test.cu deleted file mode 100644 index 0b7e2bb82f8..00000000000 --- a/cpp/tests/io/nested_json_test.cu +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (c) 2022, NVIDIA CORPORATION. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include - -#include -#include - -#include -#include - -namespace cuio_json = cudf::io::json; - -// Base test fixture for tests -struct JsonTest : public cudf::test::BaseFixture { -}; - -TEST_F(JsonTest, StackContext) -{ - // Type used to represent the atomic symbol type used within the finite-state machine - using SymbolT = char; - using StackSymbolT = char; - - // Prepare cuda stream for data transfers & kernels - rmm::cuda_stream stream{}; - rmm::cuda_stream_view stream_view(stream); - - // Test input - std::string input = R"( [{)" - R"("category": "reference",)" - R"("index:": [4,12,42],)" - R"("author": "Nigel Rees",)" - R"("title": "[Sayings of the Century]",)" - R"("price": 8.95)" - R"(}, )" - R"({)" - R"("category": "reference",)" - R"("index": [4,{},null,{"a":[{ }, {}] } ],)" - R"("author": "Nigel Rees",)" - R"("title": "{}\\\"[], <=semantic-symbols-string\\\\",)" - R"("price": 8.95)" - R"(}] )"; - - // Prepare input & output buffers - rmm::device_uvector d_input(input.size(), stream_view); - hostdevice_vector stack_context(input.size(), stream_view); - - ASSERT_CUDA_SUCCEEDED(cudaMemcpyAsync(d_input.data(), - input.data(), - input.size() * sizeof(SymbolT), - cudaMemcpyHostToDevice, - stream.value())); - - // Run algorithm - cuio_json::detail::get_stack_context(d_input, stack_context.device_ptr(), stream_view); - - // Copy back the results - stack_context.device_to_host(stream_view); - - // Make sure we copied back the stack context - stream_view.synchronize(); - - std::vector golden_stack_context{ - '_', '_', '_', '[', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '[', '[', '[', '[', '[', '[', '[', '[', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '[', '[', '[', '[', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '[', '[', '[', '{', '[', '[', '[', '[', '[', '[', '[', '{', - '{', '{', '{', '{', '[', '{', '{', '[', '[', '[', '{', '[', '{', '{', '[', '[', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '[', '_'}; - - ASSERT_EQ(golden_stack_context.size(), stack_context.size()); - CUDF_TEST_EXPECT_VECTOR_EQUAL(golden_stack_context, stack_context, stack_context.size()); -} - -TEST_F(JsonTest, StackContextUtf8) -{ - // Type used to represent the atomic symbol type used within the finite-state machine - using SymbolT = char; - using StackSymbolT = char; - - // Prepare cuda stream for data transfers & kernels - rmm::cuda_stream stream{}; - rmm::cuda_stream_view stream_view(stream); - - // Test input - std::string input = R"([{"a":{"year":1882,"author": "Bharathi"}, {"a":"filip ʒakotɛ"}}])"; - - // Prepare input & output buffers - rmm::device_uvector d_input(input.size(), stream_view); - hostdevice_vector stack_context(input.size(), stream_view); - - ASSERT_CUDA_SUCCEEDED(cudaMemcpyAsync(d_input.data(), - input.data(), - input.size() * sizeof(SymbolT), - cudaMemcpyHostToDevice, - stream.value())); - - // Run algorithm - cuio_json::detail::get_stack_context(d_input, stack_context.device_ptr(), stream_view); - - // Copy back the results - stack_context.device_to_host(stream_view); - - // Make sure we copied back the stack context - stream_view.synchronize(); - - std::vector golden_stack_context{ - '_', '[', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', - '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '{', '['}; - - ASSERT_EQ(golden_stack_context.size(), stack_context.size()); - CUDF_TEST_EXPECT_VECTOR_EQUAL(golden_stack_context, stack_context, stack_context.size()); -} - -TEST_F(JsonTest, TokenStream) -{ - using cuio_json::PdaTokenT; - using cuio_json::SymbolOffsetT; - using cuio_json::SymbolT; - - constexpr std::size_t single_item = 1; - - // Prepare cuda stream for data transfers & kernels - rmm::cuda_stream stream{}; - rmm::cuda_stream_view stream_view(stream); - - // Test input - std::string input = R"( [{)" - R"("category": "reference",)" - R"("index:": [4,12,42],)" - R"("author": "Nigel Rees",)" - R"("title": "[Sayings of the Century]",)" - R"("price": 8.95)" - R"(}, )" - R"({)" - R"("category": "reference",)" - R"("index": [4,{},null,{"a":[{ }, {}] } ],)" - R"("author": "Nigel Rees",)" - R"("title": "{}[], <=semantic-symbols-string",)" - R"("price": 8.95)" - R"(}] )"; - - // Prepare input & output buffers - rmm::device_uvector d_input(input.size(), stream_view); - - ASSERT_CUDA_SUCCEEDED(cudaMemcpyAsync(d_input.data(), - input.data(), - input.size() * sizeof(SymbolT), - cudaMemcpyHostToDevice, - stream.value())); - - hostdevice_vector tokens_gpu{input.size(), stream_view}; - hostdevice_vector token_indices_gpu{input.size(), stream_view}; - hostdevice_vector num_tokens_out{single_item, stream_view}; - - // Parse the JSON and get the token stream - cuio_json::detail::get_token_stream(d_input, - tokens_gpu.device_ptr(), - token_indices_gpu.device_ptr(), - num_tokens_out.device_ptr(), - stream_view); - - // Copy back the number of tokens that were written - num_tokens_out.device_to_host(stream_view); - tokens_gpu.device_to_host(stream_view); - token_indices_gpu.device_to_host(stream_view); - - // Make sure we copied back all relevant data - stream_view.synchronize(); - - // Golden token stream sample - using token_t = cuio_json::token_t; - std::vector> golden_token_stream = { - {2, token_t::ListBegin}, {3, token_t::StructBegin}, {4, token_t::FieldNameBegin}, - {13, token_t::FieldNameEnd}, {16, token_t::StringBegin}, {26, token_t::StringEnd}, - {28, token_t::FieldNameBegin}, {35, token_t::FieldNameEnd}, {38, token_t::ListBegin}, - {39, token_t::ValueBegin}, {40, token_t::ValueEnd}, {41, token_t::ValueBegin}, - {43, token_t::ValueEnd}, {44, token_t::ValueBegin}, {46, token_t::ValueEnd}, - {46, token_t::ListEnd}, {48, token_t::FieldNameBegin}, {55, token_t::FieldNameEnd}, - {58, token_t::StringBegin}, {69, token_t::StringEnd}, {71, token_t::FieldNameBegin}, - {77, token_t::FieldNameEnd}, {80, token_t::StringBegin}, {105, token_t::StringEnd}, - {107, token_t::FieldNameBegin}, {113, token_t::FieldNameEnd}, {116, token_t::ValueBegin}, - {120, token_t::ValueEnd}, {120, token_t::StructEnd}, {124, token_t::StructBegin}, - {125, token_t::FieldNameBegin}, {134, token_t::FieldNameEnd}, {137, token_t::StringBegin}, - {147, token_t::StringEnd}, {149, token_t::FieldNameBegin}, {155, token_t::FieldNameEnd}, - {158, token_t::ListBegin}, {159, token_t::ValueBegin}, {160, token_t::ValueEnd}, - {161, token_t::StructBegin}, {162, token_t::StructEnd}, {164, token_t::ValueBegin}, - {168, token_t::ValueEnd}, {169, token_t::StructBegin}, {170, token_t::FieldNameBegin}, - {172, token_t::FieldNameEnd}, {174, token_t::ListBegin}, {175, token_t::StructBegin}, - {177, token_t::StructEnd}, {180, token_t::StructBegin}, {181, token_t::StructEnd}, - {182, token_t::ListEnd}, {184, token_t::StructEnd}, {186, token_t::ListEnd}, - {188, token_t::FieldNameBegin}, {195, token_t::FieldNameEnd}, {198, token_t::StringBegin}, - {209, token_t::StringEnd}, {211, token_t::FieldNameBegin}, {217, token_t::FieldNameEnd}, - {220, token_t::StringBegin}, {252, token_t::StringEnd}, {254, token_t::FieldNameBegin}, - {260, token_t::FieldNameEnd}, {263, token_t::ValueBegin}, {267, token_t::ValueEnd}, - {267, token_t::StructEnd}, {268, token_t::ListEnd}}; - - // Verify the number of tokens matches - ASSERT_EQ(golden_token_stream.size(), num_tokens_out[0]); - - for (std::size_t i = 0; i < num_tokens_out[0]; i++) { - // Ensure the index the tokens are pointing to do match - EXPECT_EQ(golden_token_stream[i].first, token_indices_gpu[i]) << "Mismatch at #" << i; - - // Ensure the token category is correct - EXPECT_EQ(golden_token_stream[i].second, tokens_gpu[i]) << "Mismatch at #" << i; - } -} From 819dc2a9d7041cdf0c463875eb132faaf192a8bb Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Fri, 12 Aug 2022 18:47:14 -0400 Subject: [PATCH 28/58] Fix invalid results from conditional-left-anti-join in debug build (#11517) Fixes a bug found in `ConditionalLeftAntiJoinTest/*.TestCompareRandomToHashNulls` gtests in `conditional_join` kernel. Appears to be a race-condition that is fixed by calling `__syncwarp()` before the final `flust_output_cache()` call. The sync call is necessary to make sure shared data is synchronized otherwise garbage data is read from the shared data. This error only appears in a debug build of libcudf. The gtest uses random data so the error is somewhat intermittent. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Yunsong Wang (https://github.com/PointKernel) - Robert Maynard (https://github.com/robertmaynard) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11517 --- cpp/src/join/conditional_join_kernels.cuh | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/cpp/src/join/conditional_join_kernels.cuh b/cpp/src/join/conditional_join_kernels.cuh index 746377296b5..5231355cf30 100644 --- a/cpp/src/join/conditional_join_kernels.cuh +++ b/cpp/src/join/conditional_join_kernels.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * Copyright (c) 2021-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -206,9 +206,12 @@ __global__ void conditional_join(table_device_view left_table, } __syncwarp(activemask); + // flush output cache if next iteration does not fit - if (current_idx_shared[warp_id] + detail::warp_size >= output_cache_size) { - flush_output_cache(activemask, + auto const do_flush = current_idx_shared[warp_id] + detail::warp_size >= output_cache_size; + auto const flush_mask = __ballot_sync(activemask, do_flush); + if (do_flush) { + flush_output_cache(flush_mask, max_size, warp_id, lane_id, @@ -218,10 +221,10 @@ __global__ void conditional_join(table_device_view left_table, join_shared_r, join_output_l, join_output_r); - __syncwarp(activemask); + __syncwarp(flush_mask); if (0 == lane_id) { current_idx_shared[warp_id] = 0; } - __syncwarp(activemask); } + __syncwarp(activemask); } // Left, left anti, and full joins all require saving left columns that @@ -242,9 +245,13 @@ __global__ void conditional_join(table_device_view left_table, join_shared_r[warp_id]); } + __syncwarp(activemask); + // final flush of output cache - if (current_idx_shared[warp_id] > 0) { - flush_output_cache(activemask, + auto const do_flush = current_idx_shared[warp_id] > 0; + auto const flush_mask = __ballot_sync(activemask, do_flush); + if (do_flush) { + flush_output_cache(flush_mask, max_size, warp_id, lane_id, From 9c22da57ee9919d89c59f321226e4438322ea073 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Fri, 12 Aug 2022 16:38:22 -0700 Subject: [PATCH 29/58] Add fluent API builder to `data_profile` (#11479) Added a builder to enable complex initialization of `data_profile` objects. The builder slightly expands the API to make some common uses easier: - Setting distribution no longer requires passing a value range (default used to be passed in some benchmarks). - The special case where `set_null_frequency(nullopt)` is called to prevent the generator from materializing the null mask is now a more explicit call `no_validity()`. Updated the benchmarks to use the new builder. Setters are still used in places where `data_profile` object is modified and reused. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Karthikeyan (https://github.com/karthikeyann) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/11479 --- cpp/benchmarks/common/generate_input.cu | 26 +-- cpp/benchmarks/common/generate_input.hpp | 210 +++++++++++++++++- cpp/benchmarks/copying/contiguous_split.cu | 18 +- cpp/benchmarks/filling/repeat.cpp | 8 +- cpp/benchmarks/groupby/group_max.cpp | 10 +- cpp/benchmarks/groupby/group_no_requests.cpp | 10 +- cpp/benchmarks/groupby/group_nth.cpp | 5 +- cpp/benchmarks/groupby/group_nunique.cpp | 10 +- cpp/benchmarks/groupby/group_rank.cpp | 7 +- cpp/benchmarks/groupby/group_scan.cpp | 12 +- cpp/benchmarks/groupby/group_shift.cpp | 8 +- cpp/benchmarks/groupby/group_struct_keys.cpp | 6 +- .../groupby/group_struct_values.cpp | 8 +- cpp/benchmarks/groupby/group_sum.cpp | 12 +- cpp/benchmarks/io/orc/orc_reader.cpp | 9 +- cpp/benchmarks/io/orc/orc_writer.cpp | 9 +- cpp/benchmarks/io/parquet/parquet_reader.cpp | 9 +- cpp/benchmarks/io/parquet/parquet_writer.cpp | 9 +- cpp/benchmarks/io/text/multibyte_split.cpp | 9 +- cpp/benchmarks/quantiles/quantiles.cpp | 9 +- cpp/benchmarks/reduction/anyall.cpp | 9 +- cpp/benchmarks/reduction/dictionary.cpp | 12 +- cpp/benchmarks/reduction/distinct_count.cpp | 14 +- cpp/benchmarks/reduction/rank.cpp | 14 +- cpp/benchmarks/reduction/reduce.cpp | 4 +- cpp/benchmarks/reduction/segment_reduce.cu | 8 +- cpp/benchmarks/search/contains.cpp | 8 +- cpp/benchmarks/search/search.cpp | 6 +- cpp/benchmarks/sort/rank.cpp | 7 +- cpp/benchmarks/sort/sort.cpp | 14 +- .../stream_compaction/apply_boolean_mask.cpp | 10 +- cpp/benchmarks/stream_compaction/distinct.cpp | 28 +-- cpp/benchmarks/stream_compaction/unique.cpp | 6 +- cpp/benchmarks/string/combine.cpp | 3 +- cpp/benchmarks/string/contains.cpp | 8 +- cpp/benchmarks/string/copy.cu | 3 +- cpp/benchmarks/string/extract.cpp | 6 +- cpp/benchmarks/string/factory.cu | 3 +- cpp/benchmarks/string/filter.cpp | 3 +- cpp/benchmarks/string/find.cpp | 3 +- cpp/benchmarks/string/json.cu | 5 +- cpp/benchmarks/string/repeat_strings.cpp | 7 +- cpp/benchmarks/string/replace.cpp | 3 +- cpp/benchmarks/string/replace_re.cpp | 3 +- cpp/benchmarks/string/split.cpp | 3 +- cpp/benchmarks/string/substring.cpp | 3 +- cpp/benchmarks/string/translate.cpp | 3 +- cpp/benchmarks/text/ngrams.cpp | 7 +- cpp/benchmarks/text/normalize.cpp | 7 +- cpp/benchmarks/text/normalize_spaces.cpp | 7 +- cpp/benchmarks/text/tokenize.cpp | 7 +- 51 files changed, 362 insertions(+), 266 deletions(-) diff --git a/cpp/benchmarks/common/generate_input.cu b/cpp/benchmarks/common/generate_input.cu index d2683576f24..49831f680c7 100644 --- a/cpp/benchmarks/common/generate_input.cu +++ b/cpp/benchmarks/common/generate_input.cu @@ -394,8 +394,8 @@ std::unique_ptr create_random_column(data_profile const& profile, cudf::size_type num_rows) { // Bernoulli distribution - auto valid_dist = - random_value_fn(distribution_params{1. - profile.get_null_frequency().value_or(0)}); + auto valid_dist = random_value_fn( + distribution_params{1. - profile.get_null_probability().value_or(0)}); auto value_dist = random_value_fn{profile.get_distribution_params()}; // Distribution for picking elements from the array of samples @@ -434,7 +434,7 @@ std::unique_ptr create_random_column(data_profile const& profile, cudf::data_type{cudf::type_to_id()}, num_rows, data.release(), - profile.get_null_frequency().has_value() ? std::move(result_bitmask) : rmm::device_buffer{}); + profile.get_null_probability().has_value() ? std::move(result_bitmask) : rmm::device_buffer{}); } struct valid_or_zero { @@ -481,8 +481,8 @@ std::unique_ptr create_random_utf8_string_column(data_profile cons { auto len_dist = random_value_fn{profile.get_distribution_params().length_params}; - auto valid_dist = - random_value_fn(distribution_params{1. - profile.get_null_frequency().value_or(0)}); + auto valid_dist = random_value_fn( + distribution_params{1. - profile.get_null_probability().value_or(0)}); auto lengths = len_dist(engine, num_rows + 1); auto null_mask = valid_dist(engine, num_rows + 1); thrust::transform_if( @@ -512,7 +512,7 @@ std::unique_ptr create_random_utf8_string_column(data_profile cons num_rows, std::move(offsets), std::move(chars), - profile.get_null_frequency().has_value() ? std::move(result_bitmask) : rmm::device_buffer{}); + profile.get_null_probability().has_value() ? std::move(result_bitmask) : rmm::device_buffer{}); } /** @@ -609,8 +609,8 @@ std::unique_ptr create_random_column(data_profi cudf::data_type(type_id), create_rand_col_fn{}, profile, engine, num_rows); }); - auto valid_dist = - random_value_fn(distribution_params{1. - profile.get_null_frequency().value_or(0)}); + auto valid_dist = random_value_fn( + distribution_params{1. - profile.get_null_probability().value_or(0)}); // Generate the column bottom-up for (int lvl = dist_params.max_depth; lvl > 0; --lvl) { @@ -621,7 +621,7 @@ std::unique_ptr create_random_column(data_profi auto current_child = children.begin(); for (auto current_parent = parents.begin(); current_parent != parents.end(); ++current_parent) { auto [null_mask, null_count] = [&]() { - if (profile.get_null_frequency().has_value()) { + if (profile.get_null_probability().has_value()) { auto valids = valid_dist(engine, num_rows); return cudf::detail::valid_if(valids.begin(), valids.end(), thrust::identity{}); } @@ -683,8 +683,8 @@ std::unique_ptr create_random_column(data_profile cudf::data_type(dist_params.element_type), create_rand_col_fn{}, profile, engine, num_elements); auto len_dist = random_value_fn{profile.get_distribution_params().length_params}; - auto valid_dist = - random_value_fn(distribution_params{1. - profile.get_null_frequency().value_or(0)}); + auto valid_dist = random_value_fn( + distribution_params{1. - profile.get_null_probability().value_or(0)}); // Generate the list column bottom-up auto list_column = std::move(leaf_column); @@ -712,8 +712,8 @@ std::unique_ptr create_random_column(data_profile num_rows, std::move(offsets_column), std::move(current_child_column), - profile.get_null_frequency().has_value() ? null_count : 0, // cudf::UNKNOWN_NULL_COUNT, - profile.get_null_frequency().has_value() ? std::move(null_mask) : rmm::device_buffer{}); + profile.get_null_probability().has_value() ? null_count : 0, // cudf::UNKNOWN_NULL_COUNT, + profile.get_null_probability().has_value() ? std::move(null_mask) : rmm::device_buffer{}); } return list_column; // return the top-level column } diff --git a/cpp/benchmarks/common/generate_input.hpp b/cpp/benchmarks/common/generate_input.hpp index a5be50d3f96..12bb48a18c0 100644 --- a/cpp/benchmarks/common/generate_input.hpp +++ b/cpp/benchmarks/common/generate_input.hpp @@ -17,6 +17,7 @@ #pragma once #include +#include #include #include @@ -228,10 +229,10 @@ class data_profile { {cudf::type_id::INT32, cudf::type_id::FLOAT32, cudf::type_id::STRING}, 2}; std::map> decimal_params; - double bool_probability = 0.5; - std::optional null_frequency = 0.01; - cudf::size_type cardinality = 2000; - cudf::size_type avg_run_length = 4; + double bool_probability_true = 0.5; + std::optional null_probability = 0.01; + cudf::size_type cardinality = 2000; + cudf::size_type avg_run_length = 4; public: template >* = nullptr> distribution_params get_distribution_params() const { - return distribution_params{bool_probability}; + return distribution_params{bool_probability_true}; } template ()>* = nullptr> @@ -313,8 +314,8 @@ class data_profile { } } - auto get_bool_probability() const { return bool_probability; } - auto get_null_frequency() const { return null_frequency; }; + auto get_bool_probability_true() const { return bool_probability_true; } + auto get_null_probability() const { return null_probability; }; [[nodiscard]] auto get_cardinality() const { return cardinality; }; [[nodiscard]] auto get_avg_run_length() const { return avg_run_length; }; @@ -370,8 +371,17 @@ class data_profile { } } - void set_bool_probability(double p) { bool_probability = p; } - void set_null_frequency(std::optional f) { null_frequency = f; } + void set_bool_probability_true(double p) + { + CUDF_EXPECTS(p >= 0. and p <= 1., "probablity must be in range [0...1]"); + bool_probability_true = p; + } + void set_null_probability(std::optional p) + { + CUDF_EXPECTS(p.value_or(0.) >= 0. and p.value_or(0.) <= 1., + "probablity must be in range [0...1]"); + null_probability = p; + } void set_cardinality(cudf::size_type c) { cardinality = c; } void set_avg_run_length(cudf::size_type avg_rl) { avg_run_length = avg_rl; } @@ -389,14 +399,190 @@ class data_profile { struct_dist_desc.max_depth = max_depth; } - void set_struct_types(std::vector const& types) + void set_struct_types(cudf::host_span types) { CUDF_EXPECTS( std::none_of( - types.cbegin(), types.cend(), [](auto& type) { return type == cudf::type_id::STRUCT; }), + types.begin(), types.end(), [](auto& type) { return type == cudf::type_id::STRUCT; }), "Cannot include STRUCT as its own subtype"); - struct_dist_desc.leaf_types = types; + struct_dist_desc.leaf_types.assign(types.begin(), types.end()); + } +}; + +/** + * @brief Builder to construct data profiles for the random data generator. + * + * Setters can be chained to set multiple properties in a single expression. + * For example, `data_profile` initialization + * @code{.pseudo} + * data_profile profile; + * profile.set_null_probability(0.0); + * profile.set_cardinality(0); + * profile.set_distribution_params(cudf::type_id::INT32, distribution_id::UNIFORM, 0, 100); + * @endcode + * becomes + * @code{.pseudo} + * data_profile const profile = + * data_profile_builder().cardinality(0).null_probability(0.0).distribution( + * cudf::type_id::INT32, distribution_id::UNIFORM, 0, 100); + * @endcode + * The builder makes it easier to have immutable `data_profile` objects even with the complex + * initialization. The `profile` object in the example above is initialized from + * `data_profile_builder` using an implicit conversion operator. + * + * The builder API also includes a few additional convinience setters: + * Overload of `distribution` that only takes the distribution type (not the range). + * `no_validity`, which is a simpler equivalent of `null_probability(std::nullopr)`. + */ +class data_profile_builder { + data_profile profile; + + public: + /** + * @brief Sets random distribution type for a given set of data types. + * + * Only the distribution type is set; the distribution will use the default range. + * + * @param type_or_group Type or group ID, depending on whether the new distribution + * applies to a single type or a subset of types + * @param dist Random distribution type + * @tparam T Data type of the distribution range; does not need to match the data type + * @return this for chaining + */ + template + data_profile_builder& distribution(Type_enum type_or_group, distribution_id dist) + { + auto const range = default_range(); + profile.set_distribution_params(type_or_group, dist, range.first, range.second); + return *this; + } + + /** + * @brief Sets random distribution type and value range for a given set of data types. + * + * @tparam T Parameters that are forwarded to set_distribution_params + * @return this for chaining + */ + template + data_profile_builder& distribution(T&&... t) + { + profile.set_distribution_params(std::forward(t)...); + return *this; + } + + /** + * @brief Sets the probability that a randomly generated boolean element with be `true`. + * + * For example, passing `0.9` means that 90% of values in boolean columns with be `true`. + * + * @param p Probability of `true` values, in range [0..1] + * @return this for chaining + */ + data_profile_builder& bool_probability_true(double p) + { + profile.set_bool_probability_true(p); + return *this; + } + + /** + * @brief Sets the probability that a randomly generated element will be `null`. + * + * @param p Probability of `null` values, in range [0..1] + * @return this for chaining + */ + data_profile_builder& null_probability(std::optional p) + { + profile.set_null_probability(p); + return *this; } + + /** + * @brief Disables the creation of null mask in the output columns. + * + * @return this for chaining + */ + data_profile_builder& no_validity() + { + profile.set_null_probability(std::nullopt); + return *this; + } + + /** + * @brief Sets the maximum number of unique values in each output column. + * + * @param c Maximum number of unique values + * @return this for chaining + */ + data_profile_builder& cardinality(cudf::size_type c) + { + profile.set_cardinality(c); + return *this; + } + + /** + * @brief Sets the average length of sequences of equal elements in output columns. + * + * @param avg_rl Average sequence length (run-length) + * @return this for chaining + */ + data_profile_builder& avg_run_length(cudf::size_type avg_rl) + { + profile.set_avg_run_length(avg_rl); + return *this; + } + + /** + * @brief Sets the maximum nesting depth of generated list columns. + * + * @param max_depth maximum nesting depth + * @return this for chaining + */ + data_profile_builder& list_depth(cudf::size_type max_depth) + { + profile.set_list_depth(max_depth); + return *this; + } + + /** + * @brief Sets the data type of list elements. + * + * @param type data type ID + * @return this for chaining + */ + data_profile_builder& list_type(cudf::type_id type) + { + profile.set_list_type(type); + return *this; + } + + /** + * @brief Sets the maximum nesting depth of generated struct columns. + * + * @param max_depth maximum nesting depth + * @return this for chaining + */ + data_profile_builder& struct_depth(cudf::size_type max_depth) + { + profile.set_struct_depth(max_depth); + return *this; + } + + /** + * @brief Sets the data types of struct fields. + * + * @param types data type IDs + * @return this for chaining + */ + data_profile_builder& struct_types(cudf::host_span types) + { + profile.set_struct_types(types); + return *this; + } + + /** + * @brief move data_profile member once it's built. + */ + operator data_profile&&() { return std::move(profile); } }; /** diff --git a/cpp/benchmarks/copying/contiguous_split.cu b/cpp/benchmarks/copying/contiguous_split.cu index a61b18df8d1..6da28f6e3a5 100644 --- a/cpp/benchmarks/copying/contiguous_split.cu +++ b/cpp/benchmarks/copying/contiguous_split.cu @@ -77,16 +77,13 @@ void BM_contiguous_split(benchmark::State& state) int64_t const num_rows = total_desired_bytes / (num_cols * el_size); // generate input table - data_profile profile; - if (not include_validity) profile.set_null_frequency(std::nullopt); // <0 means, no null_mask - profile.set_cardinality(0); - auto range = default_range(); - profile.set_distribution_params( - cudf::type_id::INT32, distribution_id::UNIFORM, range.first, range.second); + auto builder = data_profile_builder().cardinality(0).distribution(cudf::type_id::INT32, + distribution_id::UNIFORM); + if (not include_validity) builder.no_validity(); auto src_cols = create_random_table(cycle_dtypes({cudf::type_id::INT32}, num_cols), row_count{static_cast(num_rows)}, - profile) + data_profile{builder}) ->release(); int64_t const total_bytes = @@ -115,13 +112,10 @@ void BM_contiguous_split_strings(benchmark::State& state) int64_t const num_rows = col_len_bytes / string_len; // generate input table - data_profile profile; - profile.set_null_frequency(std::nullopt); // <0 means, no null mask - profile.set_cardinality(0); - profile.set_distribution_params( + data_profile profile = data_profile_builder().no_validity().cardinality(0).distribution( cudf::type_id::INT32, distribution_id::UNIFORM, - 0, + 0ul, include_validity ? h_strings.size() * 2 : h_strings.size() - 1); // out of bounds nullified cudf::test::strings_column_wrapper one_col(h_strings.begin(), h_strings.end()); std::vector> src_cols(num_cols); diff --git a/cpp/benchmarks/filling/repeat.cpp b/cpp/benchmarks/filling/repeat.cpp index a73513e80af..e5d143d24ea 100644 --- a/cpp/benchmarks/filling/repeat.cpp +++ b/cpp/benchmarks/filling/repeat.cpp @@ -37,11 +37,9 @@ void BM_repeat(benchmark::State& state) auto input = cudf::table_view(*input_table); // repeat counts - using sizeT = cudf::size_type; - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params(cudf::type_to_id(), distribution_id::UNIFORM, 0, 3); + using sizeT = cudf::size_type; + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 3); auto repeat_table = create_random_table({cudf::type_to_id()}, row_count{n_rows}, profile); cudf::column_view repeat_count{repeat_table->get_column(0)}; diff --git a/cpp/benchmarks/groupby/group_max.cpp b/cpp/benchmarks/groupby/group_max.cpp index 21befecdf78..eb9c9859da9 100644 --- a/cpp/benchmarks/groupby/group_max.cpp +++ b/cpp/benchmarks/groupby/group_max.cpp @@ -29,7 +29,7 @@ void bench_groupby_max(nvbench::state& state, nvbench::type_list) auto const keys_table = [&] { data_profile profile; - profile.set_null_frequency(std::nullopt); + profile.set_null_probability(std::nullopt); profile.set_cardinality(0); profile.set_distribution_params( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); @@ -38,10 +38,10 @@ void bench_groupby_max(nvbench::state& state, nvbench::type_list) auto const vals_table = [&] { data_profile profile; - if (const auto null_freq = state.get_float64("null_frequency"); null_freq > 0) { - profile.set_null_frequency({null_freq}); + if (const auto null_freq = state.get_float64("null_probability"); null_freq > 0) { + profile.set_null_probability({null_freq}); } else { - profile.set_null_frequency(std::nullopt); + profile.set_null_probability(std::nullopt); } profile.set_cardinality(0); profile.set_distribution_params(cudf::type_to_id(), @@ -70,4 +70,4 @@ NVBENCH_BENCH_TYPES(bench_groupby_max, NVBENCH_TYPE_AXES(nvbench::type_list)) .set_name("groupby_max") .add_int64_power_of_two_axis("num_rows", {12, 18, 24}) - .add_float64_axis("null_frequency", {0, 0.1, 0.9}); + .add_float64_axis("null_probability", {0, 0.1, 0.9}); diff --git a/cpp/benchmarks/groupby/group_no_requests.cpp b/cpp/benchmarks/groupby/group_no_requests.cpp index 4639a1b8982..a819db5240d 100644 --- a/cpp/benchmarks/groupby/group_no_requests.cpp +++ b/cpp/benchmarks/groupby/group_no_requests.cpp @@ -31,10 +31,7 @@ void BM_basic_no_requests(benchmark::State& state) { const cudf::size_type column_size{(cudf::size_type)state.range(0)}; - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params( + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); @@ -65,10 +62,7 @@ void BM_pre_sorted_no_requests(benchmark::State& state) { const cudf::size_type column_size{(cudf::size_type)state.range(0)}; - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params( + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); diff --git a/cpp/benchmarks/groupby/group_nth.cpp b/cpp/benchmarks/groupby/group_nth.cpp index f574dd4f64a..ba16ae176e1 100644 --- a/cpp/benchmarks/groupby/group_nth.cpp +++ b/cpp/benchmarks/groupby/group_nth.cpp @@ -32,10 +32,7 @@ void BM_pre_sorted_nth(benchmark::State& state) // const cudf::size_type num_columns{(cudf::size_type)state.range(0)}; const cudf::size_type column_size{(cudf::size_type)state.range(0)}; - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params( + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); diff --git a/cpp/benchmarks/groupby/group_nunique.cpp b/cpp/benchmarks/groupby/group_nunique.cpp index 8a704e4d1d2..a8a5c69be48 100644 --- a/cpp/benchmarks/groupby/group_nunique.cpp +++ b/cpp/benchmarks/groupby/group_nunique.cpp @@ -45,7 +45,7 @@ void bench_groupby_nunique(nvbench::state& state, nvbench::type_list) auto const keys_table = [&] { data_profile profile; - profile.set_null_frequency(std::nullopt); + profile.set_null_probability(std::nullopt); profile.set_cardinality(0); profile.set_distribution_params( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); @@ -54,10 +54,10 @@ void bench_groupby_nunique(nvbench::state& state, nvbench::type_list) auto const vals_table = [&] { data_profile profile; - if (const auto null_freq = state.get_float64("null_frequency"); null_freq > 0) { - profile.set_null_frequency({null_freq}); + if (const auto null_freq = state.get_float64("null_probability"); null_freq > 0) { + profile.set_null_probability({null_freq}); } else { - profile.set_null_frequency(std::nullopt); + profile.set_null_probability(std::nullopt); } profile.set_cardinality(0); profile.set_distribution_params(cudf::type_to_id(), @@ -82,4 +82,4 @@ void bench_groupby_nunique(nvbench::state& state, nvbench::type_list) NVBENCH_BENCH_TYPES(bench_groupby_nunique, NVBENCH_TYPE_AXES(nvbench::type_list)) .set_name("groupby_nunique") .add_int64_power_of_two_axis("num_rows", {12, 16, 20, 24}) - .add_float64_axis("null_frequency", {0, 0.5}); + .add_float64_axis("null_probability", {0, 0.5}); diff --git a/cpp/benchmarks/groupby/group_rank.cpp b/cpp/benchmarks/groupby/group_rank.cpp index 1eeb15debe9..f573b63a75d 100644 --- a/cpp/benchmarks/groupby/group_rank.cpp +++ b/cpp/benchmarks/groupby/group_rank.cpp @@ -29,7 +29,6 @@ static void nvbench_groupby_rank(nvbench::state& state, nvbench::type_list>) { using namespace cudf; - using type = int64_t; constexpr auto dtype = type_to_id(); cudf::rmm_pool_raii pool_raii; @@ -37,10 +36,8 @@ static void nvbench_groupby_rank(nvbench::state& state, cudf::size_type const column_size = state.get_int64("data_size"); constexpr int num_groups = 100; - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, num_groups); + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( + dtype, distribution_id::UNIFORM, 0, num_groups); auto source_table = create_random_table({dtype, dtype}, row_count{column_size}, profile); diff --git a/cpp/benchmarks/groupby/group_scan.cpp b/cpp/benchmarks/groupby/group_scan.cpp index 7ccf082a3ba..e5d0b4b00a3 100644 --- a/cpp/benchmarks/groupby/group_scan.cpp +++ b/cpp/benchmarks/groupby/group_scan.cpp @@ -32,10 +32,7 @@ void BM_basic_sum_scan(benchmark::State& state) { const cudf::size_type column_size{(cudf::size_type)state.range(0)}; - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params( + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); @@ -72,14 +69,11 @@ void BM_pre_sorted_sum_scan(benchmark::State& state) { const cudf::size_type column_size{(cudf::size_type)state.range(0)}; - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params( + data_profile profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); - profile.set_null_frequency(0.1); + profile.set_null_probability(0.1); auto vals_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); diff --git a/cpp/benchmarks/groupby/group_shift.cpp b/cpp/benchmarks/groupby/group_shift.cpp index d9617deb269..ef3a5e94eee 100644 --- a/cpp/benchmarks/groupby/group_shift.cpp +++ b/cpp/benchmarks/groupby/group_shift.cpp @@ -32,11 +32,9 @@ void BM_group_shift(benchmark::State& state) const cudf::size_type column_size{(cudf::size_type)state.range(0)}; const int num_groups = 100; - data_profile profile; - profile.set_null_frequency(0.01); - profile.set_cardinality(0); - profile.set_distribution_params( - cudf::type_to_id(), distribution_id::UNIFORM, 0, num_groups); + data_profile const profile = + data_profile_builder().cardinality(0).null_probability(0.01).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, num_groups); auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); diff --git a/cpp/benchmarks/groupby/group_struct_keys.cpp b/cpp/benchmarks/groupby/group_struct_keys.cpp index 8e1cf59ee84..4e5df974134 100644 --- a/cpp/benchmarks/groupby/group_struct_keys.cpp +++ b/cpp/benchmarks/groupby/group_struct_keys.cpp @@ -69,11 +69,7 @@ void bench_groupby_struct_keys(nvbench::state& state) child_cols = std::vector>{}; child_cols.push_back(struct_col.release()); } - - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params( + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto const keys_table = cudf::table(std::move(child_cols)); diff --git a/cpp/benchmarks/groupby/group_struct_values.cpp b/cpp/benchmarks/groupby/group_struct_values.cpp index c5eceda2df2..a110efeacd5 100644 --- a/cpp/benchmarks/groupby/group_struct_values.cpp +++ b/cpp/benchmarks/groupby/group_struct_values.cpp @@ -29,10 +29,10 @@ static constexpr cudf::size_type max_str_length = 32; static auto create_data_table(cudf::size_type n_rows) { - data_profile table_profile; - table_profile.set_distribution_params(cudf::type_id::INT32, distribution_id::UNIFORM, 0, max_int); - table_profile.set_distribution_params( - cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); + data_profile const table_profile = + data_profile_builder() + .distribution(cudf::type_id::INT32, distribution_id::UNIFORM, 0, max_int) + .distribution(cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); // The first two struct members are int32 and string. // The first column is also used as keys in groupby. diff --git a/cpp/benchmarks/groupby/group_sum.cpp b/cpp/benchmarks/groupby/group_sum.cpp index 4dda47a7bc1..9baacec868c 100644 --- a/cpp/benchmarks/groupby/group_sum.cpp +++ b/cpp/benchmarks/groupby/group_sum.cpp @@ -31,10 +31,7 @@ void BM_basic_sum(benchmark::State& state) { const cudf::size_type column_size{(cudf::size_type)state.range(0)}; - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params( + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); @@ -72,14 +69,11 @@ void BM_pre_sorted_sum(benchmark::State& state) { const cudf::size_type column_size{(cudf::size_type)state.range(0)}; - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params( + data_profile profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); - profile.set_null_frequency(0.1); + profile.set_null_probability(0.1); auto vals_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); diff --git a/cpp/benchmarks/io/orc/orc_reader.cpp b/cpp/benchmarks/io/orc/orc_reader.cpp index 7d6eb432b5b..3ea1c09875e 100644 --- a/cpp/benchmarks/io/orc/orc_reader.cpp +++ b/cpp/benchmarks/io/orc/orc_reader.cpp @@ -40,11 +40,10 @@ void BM_orc_read_varying_input(benchmark::State& state) state.range(3) ? cudf_io::compression_type::SNAPPY : cudf_io::compression_type::NONE; auto const source_type = static_cast(state.range(4)); - data_profile table_data_profile; - table_data_profile.set_cardinality(cardinality); - table_data_profile.set_avg_run_length(run_length); - auto const tbl = create_random_table( - cycle_dtypes(data_types, num_cols), table_size_bytes{data_size}, table_data_profile); + auto const tbl = + create_random_table(cycle_dtypes(data_types, num_cols), + table_size_bytes{data_size}, + data_profile_builder().cardinality(cardinality).avg_run_length(run_length)); auto const view = tbl->view(); cuio_source_sink_pair source_sink(source_type); diff --git a/cpp/benchmarks/io/orc/orc_writer.cpp b/cpp/benchmarks/io/orc/orc_writer.cpp index 4e7781b402a..ce0c5b64b73 100644 --- a/cpp/benchmarks/io/orc/orc_writer.cpp +++ b/cpp/benchmarks/io/orc/orc_writer.cpp @@ -41,11 +41,10 @@ void BM_orc_write_varying_inout(benchmark::State& state) state.range(3) ? cudf_io::compression_type::SNAPPY : cudf_io::compression_type::NONE; auto const sink_type = static_cast(state.range(4)); - data_profile table_data_profile; - table_data_profile.set_cardinality(cardinality); - table_data_profile.set_avg_run_length(run_length); - auto const tbl = create_random_table( - cycle_dtypes(data_types, num_cols), table_size_bytes{data_size}, table_data_profile); + auto const tbl = + create_random_table(cycle_dtypes(data_types, num_cols), + table_size_bytes{data_size}, + data_profile_builder().cardinality(cardinality).avg_run_length(run_length)); auto const view = tbl->view(); cuio_source_sink_pair source_sink(sink_type); diff --git a/cpp/benchmarks/io/parquet/parquet_reader.cpp b/cpp/benchmarks/io/parquet/parquet_reader.cpp index 5f32ebf6672..c9723226476 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader.cpp @@ -40,11 +40,10 @@ void BM_parq_read_varying_input(benchmark::State& state) state.range(3) ? cudf_io::compression_type::SNAPPY : cudf_io::compression_type::NONE; auto const source_type = static_cast(state.range(4)); - data_profile table_data_profile; - table_data_profile.set_cardinality(cardinality); - table_data_profile.set_avg_run_length(run_length); - auto const tbl = create_random_table( - cycle_dtypes(data_types, num_cols), table_size_bytes{data_size}, table_data_profile); + auto const tbl = + create_random_table(cycle_dtypes(data_types, num_cols), + table_size_bytes{data_size}, + data_profile_builder().cardinality(cardinality).avg_run_length(run_length)); auto const view = tbl->view(); cuio_source_sink_pair source_sink(source_type); diff --git a/cpp/benchmarks/io/parquet/parquet_writer.cpp b/cpp/benchmarks/io/parquet/parquet_writer.cpp index 166f0a4aca9..9edfe8ce938 100644 --- a/cpp/benchmarks/io/parquet/parquet_writer.cpp +++ b/cpp/benchmarks/io/parquet/parquet_writer.cpp @@ -40,11 +40,10 @@ void BM_parq_write_varying_inout(benchmark::State& state) state.range(3) ? cudf_io::compression_type::SNAPPY : cudf_io::compression_type::NONE; auto const sink_type = static_cast(state.range(4)); - data_profile table_data_profile; - table_data_profile.set_cardinality(cardinality); - table_data_profile.set_avg_run_length(run_length); - auto const tbl = create_random_table( - cycle_dtypes(data_types, num_cols), table_size_bytes{data_size}, table_data_profile); + auto const tbl = + create_random_table(cycle_dtypes(data_types, num_cols), + table_size_bytes{data_size}, + data_profile_builder().cardinality(cardinality).avg_run_length(run_length)); auto const view = tbl->view(); cuio_source_sink_pair source_sink(sink_type); diff --git a/cpp/benchmarks/io/text/multibyte_split.cpp b/cpp/benchmarks/io/text/multibyte_split.cpp index df928c73dd1..abb3a1b1134 100644 --- a/cpp/benchmarks/io/text/multibyte_split.cpp +++ b/cpp/benchmarks/io/text/multibyte_split.cpp @@ -57,13 +57,8 @@ static cudf::string_scalar create_random_input(int32_t num_chars, auto const value_size_min = static_cast(value_size_avg * (1 - deviation)); auto const value_size_max = static_cast(value_size_avg * (1 + deviation)); - data_profile table_profile; - - table_profile.set_distribution_params( // - cudf::type_id::STRING, - distribution_id::NORMAL, - value_size_min, - value_size_max); + data_profile const table_profile = data_profile_builder().distribution( + cudf::type_id::STRING, distribution_id::NORMAL, value_size_min, value_size_max); auto const values_table = create_random_table( // {cudf::type_id::STRING}, diff --git a/cpp/benchmarks/quantiles/quantiles.cpp b/cpp/benchmarks/quantiles/quantiles.cpp index dc4298a856d..efe3d1ca6e5 100644 --- a/cpp/benchmarks/quantiles/quantiles.cpp +++ b/cpp/benchmarks/quantiles/quantiles.cpp @@ -36,11 +36,10 @@ static void BM_quantiles(benchmark::State& state, bool nulls) const cudf::size_type n_quantiles{(cudf::size_type)state.range(2)}; // Create columns with values in the range [0,100) - data_profile profile; - profile.set_null_frequency(nulls ? std::optional{0.01} - : std::nullopt); // 1% nulls or no null mask (<0) - profile.set_cardinality(0); - profile.set_distribution_params(cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); + data_profile profile = data_profile_builder().cardinality(0).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); + profile.set_null_probability(nulls ? std::optional{0.01} + : std::nullopt); // 1% nulls or no null mask (<0) auto input_table = create_random_table( cycle_dtypes({cudf::type_to_id()}, n_cols), row_count{n_rows}, profile); diff --git a/cpp/benchmarks/reduction/anyall.cpp b/cpp/benchmarks/reduction/anyall.cpp index 74304c77f32..f7cc3caca68 100644 --- a/cpp/benchmarks/reduction/anyall.cpp +++ b/cpp/benchmarks/reduction/anyall.cpp @@ -32,12 +32,9 @@ void BM_reduction_anyall(benchmark::State& state, std::unique_ptr const& agg) { const cudf::size_type column_size{static_cast(state.range(0))}; - auto const dtype = cudf::type_to_id(); - data_profile profile; - if (agg->kind == cudf::aggregation::ANY) - profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, 0); - else - profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, 100); + auto const dtype = cudf::type_to_id(); + data_profile const profile = data_profile_builder().distribution( + dtype, distribution_id::UNIFORM, 0, agg->kind == cudf::aggregation::ANY ? 0 : 100); auto const table = create_random_table({dtype}, row_count{column_size}, profile); table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); cudf::column_view values(table->view().column(0)); diff --git a/cpp/benchmarks/reduction/dictionary.cpp b/cpp/benchmarks/reduction/dictionary.cpp index cdb6e311302..d897cf52795 100644 --- a/cpp/benchmarks/reduction/dictionary.cpp +++ b/cpp/benchmarks/reduction/dictionary.cpp @@ -33,13 +33,11 @@ void BM_reduction_dictionary(benchmark::State& state, const cudf::size_type column_size{static_cast(state.range(0))}; // int column and encoded dictionary column - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params(cudf::type_to_id(), - distribution_id::UNIFORM, - (agg->kind == cudf::aggregation::ALL ? 1 : 0), - (agg->kind == cudf::aggregation::ANY ? 0 : 100)); + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( + cudf::type_to_id(), + distribution_id::UNIFORM, + (agg->kind == cudf::aggregation::ALL ? 1 : 0), + (agg->kind == cudf::aggregation::ANY ? 0 : 100)); auto int_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); auto number_col = cudf::cast(int_table->get_column(0), cudf::data_type{cudf::type_to_id()}); auto values = cudf::dictionary::encode(*number_col); diff --git a/cpp/benchmarks/reduction/distinct_count.cpp b/cpp/benchmarks/reduction/distinct_count.cpp index 6e582c501e2..c63f13875be 100644 --- a/cpp/benchmarks/reduction/distinct_count.cpp +++ b/cpp/benchmarks/reduction/distinct_count.cpp @@ -26,16 +26,16 @@ static void bench_reduction_distinct_count(nvbench::state& state, nvbench::type_ { cudf::rmm_pool_raii pool_raii; - auto const dtype = cudf::type_to_id(); - auto const size = static_cast(state.get_int64("num_rows")); - auto const null_frequency = state.get_float64("null_frequency"); + auto const dtype = cudf::type_to_id(); + auto const size = static_cast(state.get_int64("num_rows")); + auto const null_probability = state.get_float64("null_probability"); data_profile profile; profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, size / 100); - if (null_frequency > 0) { - profile.set_null_frequency({null_frequency}); + if (null_probability > 0) { + profile.set_null_probability({null_probability}); } else { - profile.set_null_frequency(std::nullopt); + profile.set_null_probability(std::nullopt); } auto const data_table = create_random_table({dtype}, row_count{size}, profile); @@ -60,4 +60,4 @@ NVBENCH_BENCH_TYPES(bench_reduction_distinct_count, NVBENCH_TYPE_AXES(data_type) 10000000, // 10M 100000000, // 100M }) - .add_float64_axis("null_frequency", {0, 0.5}); + .add_float64_axis("null_probability", {0, 0.5}); diff --git a/cpp/benchmarks/reduction/rank.cpp b/cpp/benchmarks/reduction/rank.cpp index 1be8998409b..c20f728e018 100644 --- a/cpp/benchmarks/reduction/rank.cpp +++ b/cpp/benchmarks/reduction/rank.cpp @@ -30,14 +30,14 @@ static void nvbench_reduction_scan(nvbench::state& state, nvbench::type_list(); - double const null_frequency = state.get_float64("null_frequency"); - size_t const size = state.get_int64("data_size"); + double const null_probability = state.get_float64("null_probability"); + size_t const size = state.get_int64("data_size"); - data_profile table_data_profile; - table_data_profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, 5); - table_data_profile.set_null_frequency(null_frequency); + data_profile const profile = data_profile_builder() + .null_probability(null_probability) + .distribution(dtype, distribution_id::UNIFORM, 0, 5); - auto const table = create_random_table({dtype}, table_size_bytes{size / 2}, table_data_profile); + auto const table = create_random_table({dtype}, table_size_bytes{size / 2}, profile); auto const new_tbl = cudf::repeat(table->view(), 2); cudf::column_view input(new_tbl->view().column(0)); @@ -53,7 +53,7 @@ using data_type = nvbench::type_list; NVBENCH_BENCH_TYPES(nvbench_reduction_scan, NVBENCH_TYPE_AXES(data_type)) .set_name("rank_scan") - .add_float64_axis("null_frequency", {0, 0.1, 0.5, 0.9}) + .add_float64_axis("null_probability", {0, 0.1, 0.5, 0.9}) .add_int64_axis("data_size", { 10000, // 10k diff --git a/cpp/benchmarks/reduction/reduce.cpp b/cpp/benchmarks/reduction/reduce.cpp index d24c9009ccf..3d4f2b8ff4e 100644 --- a/cpp/benchmarks/reduction/reduce.cpp +++ b/cpp/benchmarks/reduction/reduce.cpp @@ -33,8 +33,8 @@ void BM_reduction(benchmark::State& state, std::unique_ptr(); - data_profile profile; - profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, 100); + data_profile const profile = + data_profile_builder().distribution(dtype, distribution_id::UNIFORM, 0, 100); auto const table = create_random_table({dtype}, row_count{column_size}, profile); table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); cudf::column_view input_column(table->view().column(0)); diff --git a/cpp/benchmarks/reduction/segment_reduce.cu b/cpp/benchmarks/reduction/segment_reduce.cu index 08fc4622b43..edf9098bdad 100644 --- a/cpp/benchmarks/reduction/segment_reduce.cu +++ b/cpp/benchmarks/reduction/segment_reduce.cu @@ -68,11 +68,9 @@ std::pair, thrust::device_vector> make_test_d auto segment_length = column_size / num_segments; - auto const dtype = cudf::type_to_id(); - data_profile profile; - profile.set_null_frequency(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, 100); + auto const dtype = cudf::type_to_id(); + data_profile profile = data_profile_builder().cardinality(0).no_validity().distribution( + dtype, distribution_id::UNIFORM, 0, 100); auto input = create_random_table({dtype}, row_count{column_size}, profile); auto offset_it = diff --git a/cpp/benchmarks/search/contains.cpp b/cpp/benchmarks/search/contains.cpp index ac986e8c5fc..ca54b775ca7 100644 --- a/cpp/benchmarks/search/contains.cpp +++ b/cpp/benchmarks/search/contains.cpp @@ -29,11 +29,9 @@ std::unique_ptr create_table_data(cudf::size_type n_rows, cudf::size_type n_cols, bool has_nulls = false) { - data_profile profile; - profile.set_cardinality(0); - profile.set_null_frequency(has_nulls ? std::optional{0.1} : std::nullopt); - profile.set_distribution_params( - cudf::type_to_id(), distribution_id::UNIFORM, Type{0}, Type{1000}); + data_profile profile = data_profile_builder().cardinality(0).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 1000); + profile.set_null_probability(has_nulls ? std::optional{0.1} : std::nullopt); return create_random_table( cycle_dtypes({cudf::type_to_id()}, n_cols), row_count{n_rows}, profile); diff --git a/cpp/benchmarks/search/search.cpp b/cpp/benchmarks/search/search.cpp index 6bc509c8746..7d63df96a25 100644 --- a/cpp/benchmarks/search/search.cpp +++ b/cpp/benchmarks/search/search.cpp @@ -76,10 +76,8 @@ void BM_table(benchmark::State& state) auto const column_size{static_cast(state.range(1))}; auto const values_size = column_size; - data_profile profile; - profile.set_cardinality(0); - profile.set_null_frequency(0.1); - profile.set_distribution_params(cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); + data_profile profile = data_profile_builder().cardinality(0).null_probability(0.1).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto data_table = create_random_table( cycle_dtypes({cudf::type_to_id()}, num_columns), row_count{column_size}, profile); auto values_table = create_random_table( diff --git a/cpp/benchmarks/sort/rank.cpp b/cpp/benchmarks/sort/rank.cpp index 3ae27e65e98..5425c722cdf 100644 --- a/cpp/benchmarks/sort/rank.cpp +++ b/cpp/benchmarks/sort/rank.cpp @@ -31,10 +31,9 @@ static void BM_rank(benchmark::State& state, bool nulls) const cudf::size_type n_rows{(cudf::size_type)state.range(0)}; // Create columns with values in the range [0,100) - data_profile profile; - profile.set_null_frequency(nulls ? std::optional{0.01} : std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params(cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); + data_profile profile = data_profile_builder().cardinality(0).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); + profile.set_null_probability(nulls ? std::optional{0.01} : std::nullopt); auto keys_table = create_random_table({cudf::type_to_id()}, row_count{n_rows}, profile); cudf::column_view input{keys_table->get_column(0)}; diff --git a/cpp/benchmarks/sort/sort.cpp b/cpp/benchmarks/sort/sort.cpp index df047ea66df..13502ce0959 100644 --- a/cpp/benchmarks/sort/sort.cpp +++ b/cpp/benchmarks/sort/sort.cpp @@ -28,17 +28,17 @@ class Sort : public cudf::benchmark { template static void BM_sort(benchmark::State& state, bool nulls) { - using Type = int; + using Type = int; + auto const dtype = cudf::type_to_id(); const cudf::size_type n_rows{(cudf::size_type)state.range(0)}; const cudf::size_type n_cols{(cudf::size_type)state.range(1)}; // Create table with values in the range [0,100) - data_profile profile; - profile.set_null_frequency(nulls ? std::optional{0.01} : std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params(cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); - auto input_table = create_random_table( - cycle_dtypes({cudf::type_to_id()}, n_cols), row_count{n_rows}, profile); + data_profile const profile = data_profile_builder() + .cardinality(0) + .null_probability(nulls ? std::optional{0.01} : std::nullopt) + .distribution(dtype, distribution_id::UNIFORM, 0, 100); + auto input_table = create_random_table(cycle_dtypes({dtype}, n_cols), row_count{n_rows}, profile); cudf::table_view input{*input_table}; for (auto _ : state) { diff --git a/cpp/benchmarks/stream_compaction/apply_boolean_mask.cpp b/cpp/benchmarks/stream_compaction/apply_boolean_mask.cpp index f2adb18b2b3..8ed58b2afc7 100644 --- a/cpp/benchmarks/stream_compaction/apply_boolean_mask.cpp +++ b/cpp/benchmarks/stream_compaction/apply_boolean_mask.cpp @@ -76,16 +76,14 @@ void BM_apply_boolean_mask(benchmark::State& state, cudf::size_type num_columns) const cudf::size_type column_size{static_cast(state.range(0))}; const cudf::size_type percent_true{static_cast(state.range(1))}; - data_profile profile; - profile.set_null_frequency(0.0); // ==0 means, all valid - profile.set_cardinality(0); - profile.set_distribution_params(cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); + data_profile profile = data_profile_builder().cardinality(0).null_probability(0.0).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto source_table = create_random_table( cycle_dtypes({cudf::type_to_id()}, num_columns), row_count{column_size}, profile); - profile.set_bool_probability(percent_true / 100.0); - profile.set_null_frequency(std::nullopt); // <0 means, no null mask + profile.set_bool_probability_true(percent_true / 100.0); + profile.set_null_probability(std::nullopt); // no null mask auto mask_table = create_random_table({cudf::type_id::BOOL8}, row_count{column_size}, profile); cudf::column_view mask = mask_table->get_column(0); diff --git a/cpp/benchmarks/stream_compaction/distinct.cpp b/cpp/benchmarks/stream_compaction/distinct.cpp index 53a6a3613b9..7b11c303133 100644 --- a/cpp/benchmarks/stream_compaction/distinct.cpp +++ b/cpp/benchmarks/stream_compaction/distinct.cpp @@ -33,10 +33,8 @@ void nvbench_distinct(nvbench::state& state, nvbench::type_list) cudf::size_type const num_rows = state.get_int64("NumRows"); - data_profile profile; - profile.set_null_frequency(0.01); - profile.set_cardinality(0); - profile.set_distribution_params(cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); + data_profile profile = data_profile_builder().cardinality(0).null_probability(0.01).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto source_table = create_random_table(cycle_dtypes({cudf::type_to_id()}, 1), row_count{num_rows}, profile); @@ -67,27 +65,25 @@ void nvbench_distinct_list(nvbench::state& state, nvbench::type_list) { cudf::rmm_pool_raii pool_raii; - auto const size = state.get_int64("ColumnSize"); - auto const dtype = cudf::type_to_id(); - double const null_frequency = state.get_float64("null_frequency"); + auto const size = state.get_int64("ColumnSize"); + auto const dtype = cudf::type_to_id(); + double const null_probability = state.get_float64("null_probability"); - data_profile table_data_profile; + auto builder = data_profile_builder().null_probability(null_probability); if (dtype == cudf::type_id::LIST) { - table_data_profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, 4); - table_data_profile.set_distribution_params( - cudf::type_id::INT32, distribution_id::UNIFORM, 0, 4); - table_data_profile.set_list_depth(1); + builder.distribution(dtype, distribution_id::UNIFORM, 0, 4) + .distribution(cudf::type_id::INT32, distribution_id::UNIFORM, 0, 4) + .list_depth(1); } else { // We're comparing distinct() on a non-nested column to that on a list column with the same // number of distinct rows. The max list size is 4 and the number of distinct values in the // list's child is 5. So the number of distinct rows in the list = 1 + 5 + 5^2 + 5^3 + 5^4 = 781 // We want this column to also have 781 distinct values. - table_data_profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, 781); + builder.distribution(dtype, distribution_id::UNIFORM, 0, 781); } - table_data_profile.set_null_frequency(null_frequency); auto const table = create_random_table( - {dtype}, table_size_bytes{static_cast(size)}, table_data_profile, 0); + {dtype}, table_size_bytes{static_cast(size)}, data_profile{builder}, 0); state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { rmm::cuda_stream_view stream_view{launch.get_stream()}; @@ -104,5 +100,5 @@ NVBENCH_BENCH_TYPES(nvbench_distinct_list, NVBENCH_TYPE_AXES(nvbench::type_list)) .set_name("distinct_list") .set_type_axes_names({"Type"}) - .add_float64_axis("null_frequency", {0.0, 0.1}) + .add_float64_axis("null_probability", {0.0, 0.1}) .add_int64_axis("ColumnSize", {100'000'000}); diff --git a/cpp/benchmarks/stream_compaction/unique.cpp b/cpp/benchmarks/stream_compaction/unique.cpp index a1fc61eee5d..ef693dd74cb 100644 --- a/cpp/benchmarks/stream_compaction/unique.cpp +++ b/cpp/benchmarks/stream_compaction/unique.cpp @@ -54,10 +54,8 @@ void nvbench_unique(nvbench::state& state, nvbench::type_list(cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); + data_profile profile = data_profile_builder().cardinality(0).null_probability(0.01).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto source_table = create_random_table(cycle_dtypes({cudf::type_to_id()}, 1), row_count{num_rows}, profile); diff --git a/cpp/benchmarks/string/combine.cpp b/cpp/benchmarks/string/combine.cpp index 1396ea352ce..a8d0224916b 100644 --- a/cpp/benchmarks/string/combine.cpp +++ b/cpp/benchmarks/string/combine.cpp @@ -32,8 +32,7 @@ static void BM_combine(benchmark::State& state) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile table_profile; - table_profile.set_distribution_params( + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table( {cudf::type_id::STRING, cudf::type_id::STRING}, row_count{n_rows}, table_profile); diff --git a/cpp/benchmarks/string/contains.cpp b/cpp/benchmarks/string/contains.cpp index 1481fa72546..fd04d599e5e 100644 --- a/cpp/benchmarks/string/contains.cpp +++ b/cpp/benchmarks/string/contains.cpp @@ -50,11 +50,9 @@ std::unique_ptr build_input_column(cudf::size_type n_rows, int32_t auto matches = static_cast(n_rows * hit_rate) / 100; // Create a randomized gather-map to build a column out of the strings in data. - data_profile gather_profile; - gather_profile.set_distribution_params( - cudf::type_id::INT32, distribution_id::UNIFORM, 1, data_view.size() - 1); - gather_profile.set_null_frequency(0.0); // no nulls for gather-map - gather_profile.set_cardinality(0); + data_profile gather_profile = + data_profile_builder().cardinality(0).null_probability(0.0).distribution( + cudf::type_id::INT32, distribution_id::UNIFORM, 1, data_view.size() - 1); auto gather_table = create_random_table({cudf::type_id::INT32}, row_count{n_rows}, gather_profile); gather_table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); diff --git a/cpp/benchmarks/string/copy.cu b/cpp/benchmarks/string/copy.cu index 8bbaafa67af..318d2d524a3 100644 --- a/cpp/benchmarks/string/copy.cu +++ b/cpp/benchmarks/string/copy.cu @@ -39,8 +39,7 @@ static void BM_copy(benchmark::State& state, copy_type ct) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile table_profile; - table_profile.set_distribution_params( + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const source = diff --git a/cpp/benchmarks/string/extract.cpp b/cpp/benchmarks/string/extract.cpp index 4ff29285482..32f21d71030 100644 --- a/cpp/benchmarks/string/extract.cpp +++ b/cpp/benchmarks/string/extract.cpp @@ -53,10 +53,8 @@ static void BM_extract(benchmark::State& state, int groups) } cudf::test::strings_column_wrapper samples_column(samples.begin(), samples.end()); - data_profile profile; - profile.set_null_frequency(std::nullopt); // <0 means, all valid - profile.set_distribution_params( - cudf::type_to_id(), distribution_id::UNIFORM, 0, samples.size() - 1); + data_profile const profile = data_profile_builder().no_validity().distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0ul, samples.size() - 1); auto map_table = create_random_table({cudf::type_to_id()}, row_count{n_rows}, profile); auto input = cudf::gather(cudf::table_view{{samples_column}}, diff --git a/cpp/benchmarks/string/factory.cu b/cpp/benchmarks/string/factory.cu index 7e407ab2d91..52af92c033f 100644 --- a/cpp/benchmarks/string/factory.cu +++ b/cpp/benchmarks/string/factory.cu @@ -51,8 +51,7 @@ static void BM_factory(benchmark::State& state) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile table_profile; - table_profile.set_distribution_params( + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); auto d_column = cudf::column_device_view::create(table->view().column(0)); diff --git a/cpp/benchmarks/string/filter.cpp b/cpp/benchmarks/string/filter.cpp index 0bae967be6c..137c4126ddb 100644 --- a/cpp/benchmarks/string/filter.cpp +++ b/cpp/benchmarks/string/filter.cpp @@ -39,8 +39,7 @@ static void BM_filter_chars(benchmark::State& state, FilterAPI api) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile table_profile; - table_profile.set_distribution_params( + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); diff --git a/cpp/benchmarks/string/find.cpp b/cpp/benchmarks/string/find.cpp index 1068143b16a..f049de9a65d 100644 --- a/cpp/benchmarks/string/find.cpp +++ b/cpp/benchmarks/string/find.cpp @@ -37,8 +37,7 @@ static void BM_find_scalar(benchmark::State& state, FindAPI find_api) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile table_profile; - table_profile.set_distribution_params( + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); diff --git a/cpp/benchmarks/string/json.cu b/cpp/benchmarks/string/json.cu index 9b55375f191..5ee56c3cdae 100644 --- a/cpp/benchmarks/string/json.cu +++ b/cpp/benchmarks/string/json.cu @@ -161,10 +161,7 @@ struct json_benchmark_row_builder { auto build_json_string_column(int desired_bytes, int num_rows) { - data_profile profile; - profile.set_cardinality(0); - profile.set_null_frequency(std::nullopt); - profile.set_distribution_params( + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_id::FLOAT32, distribution_id::UNIFORM, 0.0, 1.0); auto float_2bool_columns = create_random_table({cudf::type_id::FLOAT32, cudf::type_id::BOOL8, cudf::type_id::BOOL8}, diff --git a/cpp/benchmarks/string/repeat_strings.cpp b/cpp/benchmarks/string/repeat_strings.cpp index 1b57630098a..db02fec13c2 100644 --- a/cpp/benchmarks/string/repeat_strings.cpp +++ b/cpp/benchmarks/string/repeat_strings.cpp @@ -35,17 +35,16 @@ static std::unique_ptr create_data_table(cudf::size_type n_cols, CUDF_EXPECTS(n_cols == 1 || n_cols == 2, "Invalid number of columns."); std::vector dtype_ids{cudf::type_id::STRING}; - data_profile table_profile; - table_profile.set_distribution_params( + auto builder = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); if (n_cols == 2) { dtype_ids.push_back(cudf::type_id::INT32); - table_profile.set_distribution_params( + builder.distribution( cudf::type_id::INT32, distribution_id::NORMAL, min_repeat_times, max_repeat_times); } - return create_random_table(dtype_ids, row_count{n_rows}, table_profile); + return create_random_table(dtype_ids, row_count{n_rows}, data_profile{builder}); } static void BM_repeat_strings_scalar_times(benchmark::State& state) diff --git a/cpp/benchmarks/string/replace.cpp b/cpp/benchmarks/string/replace.cpp index 34f86aa1849..d7a079201c0 100644 --- a/cpp/benchmarks/string/replace.cpp +++ b/cpp/benchmarks/string/replace.cpp @@ -38,8 +38,7 @@ static void BM_replace(benchmark::State& state, replace_type rt) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile table_profile; - table_profile.set_distribution_params( + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); diff --git a/cpp/benchmarks/string/replace_re.cpp b/cpp/benchmarks/string/replace_re.cpp index caa60cc980d..17b6f54f7bb 100644 --- a/cpp/benchmarks/string/replace_re.cpp +++ b/cpp/benchmarks/string/replace_re.cpp @@ -35,8 +35,7 @@ static void BM_replace(benchmark::State& state, replace_type rt) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile table_profile; - table_profile.set_distribution_params( + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); diff --git a/cpp/benchmarks/string/split.cpp b/cpp/benchmarks/string/split.cpp index 6ef2e5013f5..e26a853f22a 100644 --- a/cpp/benchmarks/string/split.cpp +++ b/cpp/benchmarks/string/split.cpp @@ -36,8 +36,7 @@ static void BM_split(benchmark::State& state, split_type rt) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile table_profile; - table_profile.set_distribution_params( + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); diff --git a/cpp/benchmarks/string/substring.cpp b/cpp/benchmarks/string/substring.cpp index a7e1da4845e..fce11aac251 100644 --- a/cpp/benchmarks/string/substring.cpp +++ b/cpp/benchmarks/string/substring.cpp @@ -40,8 +40,7 @@ static void BM_substring(benchmark::State& state, substring_type rt) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile table_profile; - table_profile.set_distribution_params( + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); diff --git a/cpp/benchmarks/string/translate.cpp b/cpp/benchmarks/string/translate.cpp index 87f5c3c7dbd..74e9b55ad84 100644 --- a/cpp/benchmarks/string/translate.cpp +++ b/cpp/benchmarks/string/translate.cpp @@ -39,8 +39,7 @@ static void BM_translate(benchmark::State& state, int entry_count) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile table_profile; - table_profile.set_distribution_params( + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); diff --git a/cpp/benchmarks/text/ngrams.cpp b/cpp/benchmarks/text/ngrams.cpp index b1e70517aea..38c597ece19 100644 --- a/cpp/benchmarks/text/ngrams.cpp +++ b/cpp/benchmarks/text/ngrams.cpp @@ -31,10 +31,9 @@ enum class ngrams_type { tokens, characters }; static void BM_ngrams(benchmark::State& state, ngrams_type nt) { - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); - data_profile table_profile; - table_profile.set_distribution_params( + auto const n_rows = static_cast(state.range(0)); + auto const max_str_length = static_cast(state.range(1)); + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); diff --git a/cpp/benchmarks/text/normalize.cpp b/cpp/benchmarks/text/normalize.cpp index 08a91db0e11..3925d8bebc2 100644 --- a/cpp/benchmarks/text/normalize.cpp +++ b/cpp/benchmarks/text/normalize.cpp @@ -29,10 +29,9 @@ class TextNormalize : public cudf::benchmark { static void BM_normalize(benchmark::State& state, bool to_lower) { - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); - data_profile table_profile; - table_profile.set_distribution_params( + auto const n_rows = static_cast(state.range(0)); + auto const max_str_length = static_cast(state.range(1)); + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); diff --git a/cpp/benchmarks/text/normalize_spaces.cpp b/cpp/benchmarks/text/normalize_spaces.cpp index bedb7ca5f83..2f5d6a4bcb1 100644 --- a/cpp/benchmarks/text/normalize_spaces.cpp +++ b/cpp/benchmarks/text/normalize_spaces.cpp @@ -30,10 +30,9 @@ class TextNormalize : public cudf::benchmark { static void BM_normalize(benchmark::State& state) { - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); - data_profile table_profile; - table_profile.set_distribution_params( + auto const n_rows = static_cast(state.range(0)); + auto const max_str_length = static_cast(state.range(1)); + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); diff --git a/cpp/benchmarks/text/tokenize.cpp b/cpp/benchmarks/text/tokenize.cpp index 8802efd79b2..01a971812a2 100644 --- a/cpp/benchmarks/text/tokenize.cpp +++ b/cpp/benchmarks/text/tokenize.cpp @@ -35,10 +35,9 @@ enum class tokenize_type { single, multi, count, count_multi, ngrams, characters static void BM_tokenize(benchmark::State& state, tokenize_type tt) { - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); - data_profile table_profile; - table_profile.set_distribution_params( + auto const n_rows = static_cast(state.range(0)); + auto const max_str_length = static_cast(state.range(1)); + data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); cudf::strings_column_view input(table->view().column(0)); From e5b92dfe73a37a6bcdefe382122d70c1c718fd35 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Mon, 15 Aug 2022 09:23:43 -0400 Subject: [PATCH 30/58] Add regex ASCII flag support for matching builtin character classes (#11404) Adds ASCII flag to the libcudf `regex_flags` for support with builtin character classes: `\w, \W, \s, \S, \d, \D`. Somewhat equivalent to https://docs.python.org/3/library/re.html#re.ASCII But strictly the flag modifies matching for these classes as follows: - `\w` = `[a-zA-Z_0-9]` (alphabetic characters plus underline) - `\W` = `[^\w]` (basically not `\w`) - `\s` = `[\t- ]` (tab through space in the [ASCII table](https://www.asciitable.com/)) - `\S` = `[^\s]` (basically not `\s`) - `\d` = `[0-9]` (digit characters) - `\D` = `[^\d]` (basically not `\d`) Additional gtests are included for this flag with these classes. This will be exposed through Python/Cython in a follow up PR. Closes #10894 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Robert Maynard (https://github.com/robertmaynard) URL: https://github.com/rapidsai/cudf/pull/11404 --- cpp/doxygen/regex.md | 12 +- cpp/include/cudf/strings/regex/flags.hpp | 20 ++- cpp/src/strings/regex/regcomp.cpp | 162 +++++++++++++++++------ cpp/tests/strings/contains_tests.cpp | 21 +++ 4 files changed, 165 insertions(+), 50 deletions(-) diff --git a/cpp/doxygen/regex.md b/cpp/doxygen/regex.md index 9f6a54e50b9..0f6762564aa 100644 --- a/cpp/doxygen/regex.md +++ b/cpp/doxygen/regex.md @@ -68,12 +68,12 @@ The details are based on features documented at https://www.regular-expressions. | Feature | Syntax | Description | Example | | ---------- | ------------- | ------------- | ------------- | -| Shorthand | `\d` | Adds all digits to the character class. Matches a single digit if used outside character classes. | `\d` matches a character that is a digit | -| Shorthand | `\w` | Adds all word characters to the character class. Matches a single word character if used outside character classes. | `\w` matches any single word character | -| Shorthand | `\s` | Adds all whitespace to the character class. Matches a single whitespace character if used outside character classes. | `\s` matches any single whitespace character | -| Shorthand | `\D` | Adds all non-digits to the character class. Matches a single character that is not a digit character if used outside character classes. | `[\D]` matches a single character that is not a digit character | -| Shorthand | `\W` | Adds all non-word characters to the character class. Matches a single character that is not a word character if used outside character classes. | [`\W`] matches a single character that is not a word character | -| Shorthand | `\S` | Adds all non-whitespace to the character class. Matches a single character that is not a whitespace character if used outside character classes. | `[\S]` matches a single character that is not a whitespace character | +| Shorthand | `\d` | Adds all digits to the character class. Matches a single digit if used outside character classes. The behavior can be controlled by [cudf::strings::regex_flags::ASCII](@ref cudf::strings::regex_flags) to include only `[0-9]` | `\d` matches a character that is a digit | +| Shorthand | `\w` | Adds all word characters to the character class. Matches a single word character if used outside character classes. The behavior can be controlled by [cudf::strings::regex_flags::ASCII](@ref cudf::strings::regex_flags) to include only `[0-9A-Za-z_]` | `\w` matches any single word character | +| Shorthand | `\s` | Adds all whitespace to the character class. Matches a single whitespace character if used outside character classes. The behavior can be controlled by [cudf::strings::regex_flags::ASCII](@ref cudf::strings::regex_flags) to include only `[\t- ]` | `\s` matches any single whitespace character | +| Shorthand | `\D` | Adds all non-digits to the character class. Matches a single character that is not a digit character if used outside character classes. The behavior can be controlled by [cudf::strings::regex_flags::ASCII](@ref cudf::strings::regex_flags) | `[\D]` matches a single character that is not a digit character | +| Shorthand | `\W` | Adds all non-word characters to the character class. Matches a single character that is not a word character if used outside character classes. The behavior can be controlled by [cudf::strings::regex_flags::ASCII](@ref cudf::strings::regex_flags) | [`\W`] matches a single character that is not a word character | +| Shorthand | `\S` | Adds all non-whitespace to the character class. Matches a single character that is not a whitespace character if used outside character classes. The behavior can be controlled by [cudf::strings::regex_flags::ASCII](@ref cudf::strings::regex_flags) | `[\S]` matches a single character that is not a whitespace character | ### Anchors diff --git a/cpp/include/cudf/strings/regex/flags.hpp b/cpp/include/cudf/strings/regex/flags.hpp index 637b3b0851b..3512d35959a 100644 --- a/cpp/include/cudf/strings/regex/flags.hpp +++ b/cpp/include/cudf/strings/regex/flags.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * Copyright (c) 2021-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,9 +33,10 @@ namespace strings { * and to match the Python flag values. */ enum regex_flags : uint32_t { - DEFAULT = 0, ///< default - MULTILINE = 8, ///< the '^' and '$' honor new-line characters - DOTALL = 16 ///< the '.' matching includes new-line characters + DEFAULT = 0, ///< default + MULTILINE = 8, ///< the '^' and '$' honor new-line characters + DOTALL = 16, ///< the '.' matching includes new-line characters + ASCII = 256 ///< use only ASCII when matching built-in character classes }; /** @@ -60,6 +61,17 @@ constexpr bool is_dotall(regex_flags const f) return (f & regex_flags::DOTALL) == regex_flags::DOTALL; } +/** + * @brief Returns true if the given flags contain ASCII. + * + * @param f Regex flags to check + * @return true if `f` includes ASCII + */ +constexpr bool is_ascii(regex_flags const f) +{ + return (f & regex_flags::ASCII) == regex_flags::ASCII; +} + /** @} */ // end of doxygen group } // namespace strings } // namespace cudf diff --git a/cpp/src/strings/regex/regcomp.cpp b/cpp/src/strings/regex/regcomp.cpp index bc6bdd9dc7b..1dc89433b82 100644 --- a/cpp/src/strings/regex/regcomp.cpp +++ b/cpp/src/strings/regex/regcomp.cpp @@ -145,6 +145,8 @@ int32_t const* reprog::starts_data() const { return _startinst_ids.data(); } int32_t reprog::starts_count() const { return static_cast(_startinst_ids.size()); } +static constexpr auto MAX_REGEX_CHAR = std::numeric_limits::max(); + /** * @brief Converts pattern into regex classes */ @@ -173,6 +175,7 @@ class regex_parser { char32_t const* const _pattern_begin; char32_t const* _expr_ptr; bool _lex_done{false}; + regex_flags const _flags; int32_t _id_cclass_w{-1}; // alphanumeric [a-zA-Z0-9_] int32_t _id_cclass_W{-1}; // not alphanumeric plus '\n' @@ -246,11 +249,49 @@ class regex_parser { return {false, c}; } + // for \d and \D + void add_ascii_digit_class(std::vector& ranges, bool negated = false) + { + if (!negated) { + ranges.push_back({'0', '9'}); + } else { + ranges.push_back({0, '0' - 1}); + ranges.push_back({'9' + 1, MAX_REGEX_CHAR}); + } + } + + // for \s and \S + void add_ascii_space_class(std::vector& ranges, bool negated = false) + { + if (!negated) { + ranges.push_back({'\t', ' '}); + } else { + ranges.push_back({0, '\t' - 1}); + ranges.push_back({' ' + 1, MAX_REGEX_CHAR}); + } + } + + // for \w and \W + void add_ascii_word_class(std::vector& ranges, bool negated = false) + { + add_ascii_digit_class(ranges, negated); + if (!negated) { + ranges.push_back({'a', 'z'}); + ranges.push_back({'A', 'Z'}); + ranges.push_back({'_', '_'}); + } else { + ranges.back().last = 'A' - 1; + ranges.push_back({'Z' + 1, 'a' - 1}); // {'_'-1, '_' + 1} + ranges.push_back({'z' + 1, MAX_REGEX_CHAR}); + } + } + int32_t build_cclass() { int32_t type = CCLASS; std::vector literals; int32_t builtins = 0; + std::vector ranges; auto [is_quoted, chr] = next_char(); // check for negation @@ -284,27 +325,30 @@ class regex_parser { break; } case 'w': - builtins |= cclass_w.builtins; - std::tie(is_quoted, chr) = next_char(); - continue; - case 's': - builtins |= cclass_s.builtins; - std::tie(is_quoted, chr) = next_char(); - continue; - case 'd': - builtins |= cclass_d.builtins; - std::tie(is_quoted, chr) = next_char(); - continue; case 'W': - builtins |= cclass_W.builtins; + if (is_ascii(_flags)) { + add_ascii_word_class(ranges, chr == 'W'); + } else { + builtins |= (chr == 'w' ? cclass_w.builtins : cclass_W.builtins); + } std::tie(is_quoted, chr) = next_char(); continue; + case 's': case 'S': - builtins |= cclass_S.builtins; + if (is_ascii(_flags)) { + add_ascii_space_class(ranges, chr == 'S'); + } else { + builtins |= (chr == 's' ? cclass_s.builtins : cclass_S.builtins); + } std::tie(is_quoted, chr) = next_char(); continue; + case 'd': case 'D': - builtins |= cclass_D.builtins; + if (is_ascii(_flags)) { + add_ascii_digit_class(ranges, chr == 'D'); + } else { + builtins |= (chr == 'd' ? cclass_d.builtins : cclass_D.builtins); + } std::tie(is_quoted, chr) = next_char(); continue; } @@ -323,11 +367,11 @@ class regex_parser { } // transform pairs of literals to ranges - std::vector ranges(literals.size() / 2); auto const counter = thrust::make_counting_iterator(0); - std::transform(counter, counter + ranges.size(), ranges.begin(), [&literals](auto idx) { - return reclass_range{literals[idx * 2], literals[idx * 2 + 1]}; - }); + std::transform( + counter, counter + (literals.size() / 2), std::back_inserter(ranges), [&literals](auto idx) { + return reclass_range{literals[idx * 2], literals[idx * 2 + 1]}; + }); // sort the ranges to help with detecting overlapping entries std::sort(ranges.begin(), ranges.end(), [](auto l, auto r) { return l.first == r.first ? l.last < r.last : l.first < r.first; @@ -372,41 +416,77 @@ class regex_parser { break; } case 'w': { - if (_id_cclass_w < 0) { _id_cclass_w = _prog.add_class(cclass_w); } - _cclass_id = _id_cclass_w; + if (is_ascii(_flags)) { + reclass cls; + add_ascii_word_class(cls.literals); + _cclass_id = _prog.add_class(cls); + } else { + if (_id_cclass_w < 0) { _id_cclass_w = _prog.add_class(cclass_w); } + _cclass_id = _id_cclass_w; + } return CCLASS; } case 'W': { - if (_id_cclass_W < 0) { - reclass cls = cclass_w; - cls.literals.push_back({'\n', '\n'}); - _id_cclass_W = _prog.add_class(cls); + if (is_ascii(_flags)) { + reclass cls; + add_ascii_word_class(cls.literals); + _cclass_id = _prog.add_class(cls); + } else { + if (_id_cclass_W < 0) { + reclass cls = cclass_w; + cls.literals.push_back({'\n', '\n'}); + _id_cclass_W = _prog.add_class(cls); + } + _cclass_id = _id_cclass_W; } - _cclass_id = _id_cclass_W; return NCCLASS; } case 's': { - if (_id_cclass_s < 0) { _id_cclass_s = _prog.add_class(cclass_s); } - _cclass_id = _id_cclass_s; + if (is_ascii(_flags)) { + reclass cls; + add_ascii_space_class(cls.literals); + _cclass_id = _prog.add_class(cls); + } else { + if (_id_cclass_s < 0) { _id_cclass_s = _prog.add_class(cclass_s); } + _cclass_id = _id_cclass_s; + } return CCLASS; } case 'S': { - if (_id_cclass_s < 0) { _id_cclass_s = _prog.add_class(cclass_s); } - _cclass_id = _id_cclass_s; - return NCCLASS; + if (is_ascii(_flags)) { + reclass cls; + add_ascii_space_class(cls.literals); + _cclass_id = _prog.add_class(cls); + } else { + if (_id_cclass_s < 0) { _id_cclass_s = _prog.add_class(cclass_s); } + _cclass_id = _id_cclass_s; + return NCCLASS; + } } case 'd': { - if (_id_cclass_d < 0) { _id_cclass_d = _prog.add_class(cclass_d); } - _cclass_id = _id_cclass_d; + if (is_ascii(_flags)) { + reclass cls; + add_ascii_digit_class(cls.literals); + _cclass_id = _prog.add_class(cls); + } else { + if (_id_cclass_d < 0) { _id_cclass_d = _prog.add_class(cclass_d); } + _cclass_id = _id_cclass_d; + } return CCLASS; } case 'D': { - if (_id_cclass_D < 0) { - reclass cls = cclass_d; - cls.literals.push_back({'\n', '\n'}); - _id_cclass_D = _prog.add_class(cls); + if (is_ascii(_flags)) { + reclass cls; + add_ascii_digit_class(cls.literals); + _cclass_id = _prog.add_class(cls); + } else { + if (_id_cclass_D < 0) { + reclass cls = cclass_d; + cls.literals.push_back({'\n', '\n'}); + _id_cclass_D = _prog.add_class(cls); + } + _cclass_id = _id_cclass_D; } - _cclass_id = _id_cclass_D; return NCCLASS; } case 'b': return BOW; @@ -660,9 +740,11 @@ class regex_parser { } public: - regex_parser(const char32_t* pattern, int32_t dot_type, reprog& prog) - : _prog(prog), _pattern_begin(pattern), _expr_ptr(pattern) + regex_parser(const char32_t* pattern, regex_flags const flags, reprog& prog) + : _prog(prog), _pattern_begin(pattern), _expr_ptr(pattern), _flags(flags) { + auto const dot_type = is_dotall(_flags) ? ANYNL : ANY; + int32_t type = 0; while ((type = lex(dot_type)) != END) { auto const item = [type, chr = _chr, cid = _cclass_id, n = _min_count, m = _max_count] { @@ -866,7 +948,7 @@ class regex_compiler { : _prog(prog), _last_was_and(false), _bracket_count(0), _flags(flags) { // Parse pattern into items - auto const items = regex_parser(pattern, is_dotall(flags) ? ANYNL : ANY, _prog).get_items(); + auto const items = regex_parser(pattern, _flags, _prog).get_items(); int cur_subid{}; int push_subid{}; diff --git a/cpp/tests/strings/contains_tests.cpp b/cpp/tests/strings/contains_tests.cpp index d725f3d5dd0..e97640986e9 100644 --- a/cpp/tests/strings/contains_tests.cpp +++ b/cpp/tests/strings/contains_tests.cpp @@ -521,6 +521,27 @@ TEST_F(StringsContainsTests, DotAll) CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected_count); } +TEST_F(StringsContainsTests, ASCII) +{ + auto input = cudf::test::strings_column_wrapper({"abc \t\f\r 12", "áé  ❽❽", "aZ ❽4", "XYZ 8"}); + auto view = cudf::strings_column_view(input); + + std::string patterns[] = {"\\w+[\\s]+\\d+", + "[^\\W]+\\s+[^\\D]+", + "[\\w]+[^\\S]+[\\d]+", + "[\\w]+\\s+[\\d]+", + "\\w+\\s+\\d+"}; + + for (auto ptn : patterns) { + auto results = cudf::strings::contains_re(view, ptn, cudf::strings::regex_flags::ASCII); + auto expected_contains = cudf::test::fixed_width_column_wrapper({1, 0, 0, 0}); + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected_contains); + results = cudf::strings::contains_re(view, ptn); + expected_contains = cudf::test::fixed_width_column_wrapper({1, 1, 1, 1}); + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected_contains); + } +} + TEST_F(StringsContainsTests, MediumRegex) { // This results in 95 regex instructions and falls in the 'medium' range. From a221d47f483a666581146b51709848db35069a8e Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Mon, 15 Aug 2022 08:47:17 -0500 Subject: [PATCH 31/58] Deprecate `skiprows` and `num_rows` in `read_orc` (#11522) Resolves the first step of #11519 by deprecating `skiprows` and `num_rows` in orc reader. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Bradley Dice (https://github.com/bdice) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/11522 --- python/cudf/cudf/io/orc.py | 12 ++++++++++++ python/cudf/cudf/utils/ioutils.py | 2 ++ 2 files changed, 14 insertions(+) diff --git a/python/cudf/cudf/io/orc.py b/python/cudf/cudf/io/orc.py index cd72a60b182..9b8e56f4f10 100644 --- a/python/cudf/cudf/io/orc.py +++ b/python/cudf/cudf/io/orc.py @@ -294,6 +294,18 @@ def read_orc( """{docstring}""" from cudf import DataFrame + if skiprows is not None: + warnings.warn( + "skiprows is deprecated and will be removed.", + FutureWarning, + ) + + if num_rows is not None: + warnings.warn( + "num_rows is deprecated and will be removed.", + FutureWarning, + ) + # Multiple sources are passed as a list. If a single source is passed, # wrap it in a list for unified processing downstream. if not is_list_like(filepath_or_buffer): diff --git a/python/cudf/cudf/utils/ioutils.py b/python/cudf/cudf/utils/ioutils.py index f915da5fe69..bb6716c1c4a 100644 --- a/python/cudf/cudf/utils/ioutils.py +++ b/python/cudf/cudf/utils/ioutils.py @@ -378,8 +378,10 @@ concatenated with index ignored. skiprows : int, default None If not None, the number of rows to skip from the start of the file. + This parameter is deprecated. num_rows : int, default None If not None, the total number of rows to read. + This parameter is deprecated. use_index : bool, default True If True, use row index if available for faster seeking. use_python_file_object : boolean, default True From dd0ff302c774d2d600ab7eedce98659e330c3d13 Mon Sep 17 00:00:00 2001 From: Mike Wilson Date: Mon, 15 Aug 2022 14:42:26 -0400 Subject: [PATCH 32/58] Fixing crash when writing binary nested data in parquet (#11526) This fixes the crash described in the bug related to writing nested data in parquet with the binary flag set to write binary data as byte_arrays. We were incorrectly selecting the top-most node instead of the list, which resulted in a crash down in the kernels when the data pointer was null for those upper list columns. closes #11506 Authors: - Mike Wilson (https://github.com/hyperbolic2346) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11526 --- cpp/src/io/utilities/column_utils.cuh | 16 ++++++++++------ cpp/tests/io/parquet_test.cpp | 3 +++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/cpp/src/io/utilities/column_utils.cuh b/cpp/src/io/utilities/column_utils.cuh index 1faab805811..ecb74173a46 100644 --- a/cpp/src/io/utilities/column_utils.cuh +++ b/cpp/src/io/utilities/column_utils.cuh @@ -67,13 +67,17 @@ rmm::device_uvector create_leaf_column_device_views( size_type index) mutable { col_desc[index].parent_column = parent_col_view.begin() + index; column_device_view col = parent_col_view.column(index); - if (col_desc[index].stats_dtype != dtype_byte_array) { - // traverse till leaf column - while (col.type().id() == type_id::LIST or col.type().id() == type_id::STRUCT) { - col = (col.type().id() == type_id::LIST) - ? col.child(lists_column_view::child_column_index) - : col.child(0); + // traverse till leaf column + while (cudf::is_nested(col.type())) { + auto const child = (col.type().id() == type_id::LIST) + ? col.child(lists_column_view::child_column_index) + : col.child(0); + // stop early if writing a byte array + if (col_desc[index].stats_dtype == dtype_byte_array && + (child.type().id() == type_id::INT8 || child.type().id() == type_id::UINT8)) { + break; } + col = child; } // Store leaf_column to device storage column_device_view* leaf_col_ptr = leaf_columns.begin() + index; diff --git a/cpp/tests/io/parquet_test.cpp b/cpp/tests/io/parquet_test.cpp index 0350bfe2981..774c58f1ecf 100644 --- a/cpp/tests/io/parquet_test.cpp +++ b/cpp/tests/io/parquet_test.cpp @@ -4250,6 +4250,9 @@ TEST_F(ParquetWriterTest, ByteArrayStats) read_footer(source, &fmd); + EXPECT_EQ(fmd.schema[1].type, cudf::io::parquet::Type::BYTE_ARRAY); + EXPECT_EQ(fmd.schema[2].type, cudf::io::parquet::Type::BYTE_ARRAY); + auto const stats0 = parse_statistics(fmd.row_groups[0].columns[0]); auto const stats1 = parse_statistics(fmd.row_groups[0].columns[1]); From 2e72db1589f671fdf0ea50c15f415523ebd3493c Mon Sep 17 00:00:00 2001 From: Robert Maynard Date: Mon, 15 Aug 2022 17:17:58 -0400 Subject: [PATCH 33/58] find_package(cudf) + arrow9 usable with cudf build directory (#11535) arrow 9's CMake code generates new imported interface targets which cudf needs to replicate so that consumers of cudf don't get errors abount `arrow::hadoop` or `arrow::flatbuffers`. Fixes #11521 Authors: - Robert Maynard (https://github.com/robertmaynard) Approvers: - Jason Lowe (https://github.com/jlowe) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11535 --- cpp/cmake/thirdparty/get_arrow.cmake | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cpp/cmake/thirdparty/get_arrow.cmake b/cpp/cmake/thirdparty/get_arrow.cmake index 10241b8eede..9fa5b9d1658 100644 --- a/cpp/cmake/thirdparty/get_arrow.cmake +++ b/cpp/cmake/thirdparty/get_arrow.cmake @@ -114,6 +114,7 @@ function(find_and_configure_arrow VERSION BUILD_STATIC ENABLE_S3 ENABLE_ORC ENAB "ARROW_SIMD_LEVEL ${ARROW_SIMD_LEVEL}" "ARROW_BUILD_STATIC ${ARROW_BUILD_STATIC}" "ARROW_BUILD_SHARED ${ARROW_BUILD_SHARED}" + "ARROW_POSITION_INDEPENDENT_CODE ON" "ARROW_DEPENDENCY_USE_SHARED ${ARROW_BUILD_SHARED}" "ARROW_BOOST_USE_SHARED ${ARROW_BUILD_SHARED}" "ARROW_BROTLI_USE_SHARED ${ARROW_BUILD_SHARED}" @@ -192,8 +193,38 @@ function(find_and_configure_arrow VERSION BUILD_STATIC ENABLE_S3 ENABLE_ORC ENAB if (TARGET cudf::arrow_static AND (NOT TARGET arrow_static)) add_library(arrow_static ALIAS cudf::arrow_static) endif() + if (NOT TARGET arrow::flatbuffers) + add_library(arrow::flatbuffers INTERFACE IMPORTED) + endif() + if (NOT TARGET arrow::hadoop) + add_library(arrow::hadoop INTERFACE IMPORTED) + endif() ]=] ) + if(ENABLE_PARQUET) + string( + APPEND + arrow_code_string + " + find_package(Boost) + if (NOT TARGET Boost::headers) + add_library(Boost::headers INTERFACE IMPORTED) + endif() + " + ) + endif() + if(NOT TARGET xsimd) + string( + APPEND + arrow_code_string + " + if(NOT TARGET xsimd) + add_library(xsimd INTERFACE IMPORTED) + target_include_directories(xsimd INTERFACE \"${Arrow_BINARY_DIR}/xsimd_ep/src/xsimd_ep-install/include\") + endif() + " + ) + endif() rapids_export( BUILD Arrow From c19c8c9406262f16addf2080096338e8fe4a2ba9 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Mon, 15 Aug 2022 14:50:18 -0700 Subject: [PATCH 34/58] Struct support for `NULL_EQUALS` binary operation (#11520) Adds support for Spark's null aware equality binop and expands/improves Java testing for struct binops. Properly tests null structs and full operator testing coverage. Utilizes existing Spark struct binop support with JNI changes to force the full null-aware comparison. Expands on #11153 Partial solution to #8964 -- `NULL_MAX` and `NULL_MIN` still outstanding. Authors: - Ryan Lee (https://github.com/rwlee) Approvers: - Tobias Ribizel (https://github.com/upsj) - Vukasin Milovanovic (https://github.com/vuule) - Jason Lowe (https://github.com/jlowe) URL: https://github.com/rapidsai/cudf/pull/11520 --- cpp/src/binaryop/compiled/binary_ops.cu | 1 + .../binaryop/compiled/struct_binary_ops.cuh | 3 +- java/src/main/native/src/ColumnViewJni.cpp | 23 +- java/src/main/native/src/ScalarJni.cpp | 12 +- .../java/ai/rapids/cudf/BinaryOpTest.java | 243 +++++++++++++----- 5 files changed, 212 insertions(+), 70 deletions(-) diff --git a/cpp/src/binaryop/compiled/binary_ops.cu b/cpp/src/binaryop/compiled/binary_ops.cu index d91b534dffb..56528ba8081 100644 --- a/cpp/src/binaryop/compiled/binary_ops.cu +++ b/cpp/src/binaryop/compiled/binary_ops.cu @@ -405,6 +405,7 @@ void apply_sorting_struct_binary_op(mutable_column_view& out, // Struct child column type and structure mismatches are caught within the two_table_comparator switch (op) { case binary_operator::EQUAL: [[fallthrough]]; + case binary_operator::NULL_EQUALS: [[fallthrough]]; case binary_operator::NOT_EQUAL: detail::apply_struct_equality_op( out, diff --git a/cpp/src/binaryop/compiled/struct_binary_ops.cuh b/cpp/src/binaryop/compiled/struct_binary_ops.cuh index 7cf19ef91e8..804b931fa5b 100644 --- a/cpp/src/binaryop/compiled/struct_binary_ops.cuh +++ b/cpp/src/binaryop/compiled/struct_binary_ops.cuh @@ -109,7 +109,8 @@ void apply_struct_equality_op(mutable_column_view& out, PhysicalEqualityComparator comparator = {}, rmm::cuda_stream_view stream = cudf::default_stream_value) { - CUDF_EXPECTS(op == binary_operator::EQUAL || op == binary_operator::NOT_EQUAL, + CUDF_EXPECTS(op == binary_operator::EQUAL || op == binary_operator::NOT_EQUAL || + op == binary_operator::NULL_EQUALS, "Unsupported operator for these types"); auto tlhs = table_view{{lhs}}; diff --git a/java/src/main/native/src/ColumnViewJni.cpp b/java/src/main/native/src/ColumnViewJni.cpp index f8f7c79ddf0..9687d333ea9 100644 --- a/java/src/main/native/src/ColumnViewJni.cpp +++ b/java/src/main/native/src/ColumnViewJni.cpp @@ -1318,8 +1318,15 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_binaryOpVV(JNIEnv *env, j cudf::binary_operator op = static_cast(int_op); if (lhs->type().id() == cudf::type_id::STRUCT) { - auto [new_mask, null_count] = cudf::bitmask_and(cudf::table_view{{*lhs, *rhs}}); - auto out = make_fixed_width_column(n_data_type, lhs->size(), std::move(new_mask), null_count); + auto out = make_fixed_width_column(n_data_type, lhs->size(), cudf::mask_state::UNALLOCATED); + + if (op == cudf::binary_operator::NULL_EQUALS) { + out->set_null_mask(rmm::device_buffer{}, 0); + } else { + auto [new_mask, null_count] = cudf::bitmask_and(cudf::table_view{{*lhs, *rhs}}); + out->set_null_mask(std::move(new_mask), null_count); + } + auto out_view = out->mutable_view(); cudf::binops::compiled::detail::apply_sorting_struct_binary_op(out_view, *lhs, *rhs, false, false, op); @@ -1357,9 +1364,15 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_binaryOpVS(JNIEnv *env, j cudf::binary_operator op = static_cast(int_op); if (lhs->type().id() == cudf::type_id::STRUCT) { - auto [new_mask, new_null_count] = cudf::binops::scalar_col_valid_mask_and(*lhs, *rhs); - auto out = - make_fixed_width_column(n_data_type, lhs->size(), std::move(new_mask), new_null_count); + auto out = make_fixed_width_column(n_data_type, lhs->size(), cudf::mask_state::UNALLOCATED); + + if (op == cudf::binary_operator::NULL_EQUALS) { + out->set_null_mask(rmm::device_buffer{}, 0); + } else { + auto [new_mask, new_null_count] = cudf::binops::scalar_col_valid_mask_and(*lhs, *rhs); + out->set_null_mask(std::move(new_mask), new_null_count); + } + auto rhsv = cudf::make_column_from_scalar(*rhs, 1); auto out_view = out->mutable_view(); cudf::binops::compiled::detail::apply_sorting_struct_binary_op(out_view, *lhs, rhsv->view(), diff --git a/java/src/main/native/src/ScalarJni.cpp b/java/src/main/native/src/ScalarJni.cpp index 9af3edd0356..b44d2604882 100644 --- a/java/src/main/native/src/ScalarJni.cpp +++ b/java/src/main/native/src/ScalarJni.cpp @@ -503,9 +503,15 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_Scalar_binaryOpSV(JNIEnv *env, jclas cudf::binary_operator op = static_cast(int_op); if (lhs->type().id() == cudf::type_id::STRUCT) { - auto [new_mask, new_null_count] = cudf::binops::scalar_col_valid_mask_and(*rhs, *lhs); - auto out = - make_fixed_width_column(n_data_type, rhs->size(), std::move(new_mask), new_null_count); + auto out = make_fixed_width_column(n_data_type, rhs->size(), cudf::mask_state::UNALLOCATED); + + if (op == cudf::binary_operator::NULL_EQUALS) { + out->set_null_mask(rmm::device_buffer{}, 0); + } else { + auto [new_mask, new_null_count] = cudf::binops::scalar_col_valid_mask_and(*rhs, *lhs); + out->set_null_mask(std::move(new_mask), new_null_count); + } + auto lhs_col = cudf::make_column_from_scalar(*lhs, 1); auto out_view = out->mutable_view(); cudf::binops::compiled::detail::apply_sorting_struct_binary_op(out_view, lhs_col->view(), diff --git a/java/src/test/java/ai/rapids/cudf/BinaryOpTest.java b/java/src/test/java/ai/rapids/cudf/BinaryOpTest.java index 35d1cb39324..28fcfb741e6 100644 --- a/java/src/test/java/ai/rapids/cudf/BinaryOpTest.java +++ b/java/src/test/java/ai/rapids/cudf/BinaryOpTest.java @@ -18,13 +18,19 @@ package ai.rapids.cudf; +import ai.rapids.cudf.HostColumnVector.BasicType; import ai.rapids.cudf.HostColumnVector.Builder; +import ai.rapids.cudf.HostColumnVector.DataType; +import ai.rapids.cudf.HostColumnVector.StructData; +import ai.rapids.cudf.HostColumnVector.StructType; + import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import java.util.Arrays; +import java.util.List; import java.util.stream.IntStream; import static ai.rapids.cudf.AssertUtils.assertColumnsAreEqual; @@ -57,6 +63,21 @@ public class BinaryOpTest extends CudfTestBase { private static final long[] DECIMAL64_1 = new long[]{10L, 23L, 12L, 24L, 123456789L}; private static final long[] DECIMAL64_2 = new long[]{33041L, 97290L, 36438L, 25379L, 48473L}; + private static final StructData INT_SD_1 = new StructData(1); + private static final StructData INT_SD_2 = new StructData(2); + private static final StructData INT_SD_3 = new StructData(3); + private static final StructData INT_SD_4 = new StructData(4); + private static final StructData INT_SD_5 = new StructData(5); + private static final StructData INT_SD_NULL = new StructData((List) null); + private static final StructData INT_SD_100 = new StructData(100); + + private static final StructData[] int_struct_data_1 = + new StructData[]{null, INT_SD_1, null, INT_SD_3, INT_SD_4, INT_SD_5, INT_SD_NULL, INT_SD_100}; + private static final StructData[] int_struct_data_2 = + new StructData[]{null, null, INT_SD_2, INT_SD_3, INT_SD_100, INT_SD_5, INT_SD_NULL, INT_SD_4}; + private static final DataType structType = + new StructType(true, new BasicType(true, DType.INT32)); + private static final BigInteger[] DECIMAL128_1 = new BigInteger[]{new BigInteger("1234567891234567"), new BigInteger("1234567891234567"), new BigInteger("1234567891234567"), new BigInteger("1234567891234567"), new BigInteger("1234567891234567")}; private static final BigInteger[] DECIMAL128_2 = new BigInteger[]{new BigInteger("234567891234567"), new BigInteger("234567891234567"), @@ -791,7 +812,11 @@ public void testPow() { @Test public void testEqual() { try (ColumnVector icv = ColumnVector.fromBoxedInts(INTS_1); + ColumnVector intscalar = ColumnVector.fromInts(4); + Scalar sscv = Scalar.structFromColumnViews(intscalar); ColumnVector dcv = ColumnVector.fromBoxedDoubles(DOUBLES_1); + ColumnVector structcv1 = ColumnVector.fromStructs(structType, int_struct_data_1); + ColumnVector structcv2 = ColumnVector.fromStructs(structType, int_struct_data_2); ColumnVector dec32cv_1 = ColumnVector.decimalFromInts(-dec32Scale_1, DECIMAL32_1); ColumnVector dec32cv_2 = ColumnVector.decimalFromInts(-dec32Scale_2, DECIMAL32_2)) { try (ColumnVector answer = icv.equalTo(dcv); @@ -837,6 +862,28 @@ public void testEqual() { (b, l, r, i) -> b.append(Short.toUnsignedInt(l.getShort(i)) == r.getInt(i)))) { assertColumnsAreEqual(expected, answer, "uint16 == uint32"); } + + try (ColumnVector answersv = sscv.equalTo(structcv1); + ColumnVector expectedsv = forEachS(DType.BOOL8, 4, structcv1, + (b, l, r, i) -> b.append(r.isNull(i) ? false : + l == r.getStruct(i).dataRecord.get(0)))) { + assertColumnsAreEqual(expectedsv, answersv, "scalar struct int32 == struct int32"); + } + + try (ColumnVector answervs = structcv1.equalTo(sscv); + ColumnVector expectedvs = forEachS(DType.BOOL8, structcv1, 4, + (b, l, r, i) -> b.append(l.isNull(i) ? false : + r == l.getStruct(i).dataRecord.get(0)))) { + assertColumnsAreEqual(expectedvs, answervs, "struct int32 == scalar struct int32"); + } + + try (ColumnVector answervv = structcv1.equalTo(structcv2); + ColumnVector expectedvv = forEach(DType.BOOL8, structcv1, structcv2, + (b, l, r, i) -> b.append(l.isNull(i) || r.isNull(i) || + l.getStruct(i).dataRecord.get(0) == null || r.getStruct(i).dataRecord.get(0) == null ? + false : l.getStruct(i).dataRecord.get(0) == r.getStruct(i).dataRecord.get(0)))) { + assertColumnsAreEqual(expectedvv, answervv, "struct int32 == struct int32"); + } } } @@ -884,16 +931,16 @@ public void testStringEqualScalarNotPresent() { @Test public void testNotEqual() { - try (ColumnVector icv1 = ColumnVector.fromBoxedInts(INTS_1); - ColumnVector icv2 = ColumnVector.fromBoxedInts(INTS_2); - ColumnVector structcv1 = ColumnVector.makeStruct(icv1); - ColumnVector structcv2 = ColumnVector.makeStruct(icv2); + try (ColumnVector icv = ColumnVector.fromBoxedInts(INTS_1); ColumnVector intscalar = ColumnVector.fromInts(4); + Scalar sscv = Scalar.structFromColumnViews(intscalar); ColumnVector dcv = ColumnVector.fromBoxedDoubles(DOUBLES_1); + ColumnVector structcv1 = ColumnVector.fromStructs(structType, int_struct_data_1); + ColumnVector structcv2 = ColumnVector.fromStructs(structType, int_struct_data_2); ColumnVector dec32cv_1 = ColumnVector.decimalFromInts(-dec32Scale_1, DECIMAL32_1); ColumnVector dec32cv_2 = ColumnVector.decimalFromInts(-dec32Scale_2, DECIMAL32_2)) { - try (ColumnVector answer = icv1.notEqualTo(dcv); - ColumnVector expected = forEach(DType.BOOL8, icv1, dcv, + try (ColumnVector answer = icv.notEqualTo(dcv); + ColumnVector expected = forEach(DType.BOOL8, icv, dcv, (b, l, r, i) -> b.append(l.getInt(i) != r.getDouble(i)))) { assertColumnsAreEqual(expected, answer, "int32 != double"); } @@ -913,36 +960,35 @@ public void testNotEqual() { } try (Scalar s = Scalar.fromFloat(1.0f); - ColumnVector answer = icv1.notEqualTo(s); - ColumnVector expected = forEachS(DType.BOOL8, icv1, 1.0f, + ColumnVector answer = icv.notEqualTo(s); + ColumnVector expected = forEachS(DType.BOOL8, icv, 1.0f, (b, l, r, i) -> b.append(l.getInt(i) != r))) { assertColumnsAreEqual(expected, answer, "int64 != scalar float"); } try (Scalar s = Scalar.fromShort((short) 100); - ColumnVector answer = s.notEqualTo(icv1); - ColumnVector expected = forEachS(DType.BOOL8, (short) 100, icv1, + ColumnVector answer = s.notEqualTo(icv); + ColumnVector expected = forEachS(DType.BOOL8, (short) 100, icv, (b, l, r, i) -> b.append(l != r.getInt(i)))) { assertColumnsAreEqual(expected, answer, "scalar short != int32"); } - try (Scalar s = Scalar.structFromColumnViews(intscalar); - ColumnVector answersv = s.notEqualTo(structcv1); - ColumnVector expectedsv = forEachS(DType.BOOL8, 4, icv1, - (b, l, r, i) -> b.append(r.isNull(i) ? true : l != r.getInt(i)), true)) { + try (ColumnVector answersv = sscv.notEqualTo(structcv1); + ColumnVector expectedsv = forEachS(DType.BOOL8, 4, structcv1, + (b, l, r, i) -> b.append(r.isNull(i) ? true : l != r.getStruct(i).dataRecord.get(0)))) { assertColumnsAreEqual(expectedsv, answersv, "scalar struct int32 != struct int32"); } - try (Scalar s = Scalar.structFromColumnViews(intscalar); - ColumnVector answervs = structcv1.notEqualTo(s); - ColumnVector expectedvs = forEachS(DType.BOOL8, icv1, 4, - (b, l, r, i) -> b.append(l.isNull(i) ? true : l.getInt(i) != r), true)) { + try (ColumnVector answervs = structcv1.notEqualTo(sscv); + ColumnVector expectedvs = forEachS(DType.BOOL8, structcv1, 4, + (b, l, r, i) -> b.append(l.isNull(i) ? true : l.getStruct(i).dataRecord.get(0) != r))) { assertColumnsAreEqual(expectedvs, answervs, "struct int32 != scalar struct int32"); } try (ColumnVector answervv = structcv1.notEqualTo(structcv2); - ColumnVector expectedvv = forEach(DType.BOOL8, icv1, icv2, - (b, l, r, i) -> b.append(l.isNull(i) ? !r.isNull(i) : r.isNull(i) || l.getInt(i) != r.getInt(i)), true)) { + ColumnVector expectedvv = forEach(DType.BOOL8, structcv1, structcv2, + (b, l, r, i) -> b.append(l.isNull(i) ? !r.isNull(i) : + r.isNull(i) || l.getStruct(i).dataRecord.get(0) != r.getStruct(i).dataRecord.get(0)))) { assertColumnsAreEqual(expectedvv, answervv, "struct int32 != struct int32"); } } @@ -993,7 +1039,11 @@ public void testStringNotEqualScalarNotPresent() { @Test public void testLessThan() { try (ColumnVector icv = ColumnVector.fromBoxedInts(INTS_1); + ColumnVector intscalar = ColumnVector.fromInts(4); + Scalar sscv = Scalar.structFromColumnViews(intscalar); ColumnVector dcv = ColumnVector.fromBoxedDoubles(DOUBLES_1); + ColumnVector structcv1 = ColumnVector.fromStructs(structType, int_struct_data_1); + ColumnVector structcv2 = ColumnVector.fromStructs(structType, int_struct_data_2); ColumnVector dec32cv_1 = ColumnVector.decimalFromInts(-dec32Scale_1, DECIMAL32_1); ColumnVector dec32cv_2 = ColumnVector.decimalFromInts(-dec32Scale_2, DECIMAL32_2)) { try (ColumnVector answer = icv.lessThan(dcv); @@ -1021,6 +1071,27 @@ public void testLessThan() { (b, l, r, i) -> b.append(l < r.getInt(i)))) { assertColumnsAreEqual(expected, answer, "scalar short < int32"); } + + try (ColumnVector answersv = sscv.lessThan(structcv1); + ColumnVector expectedsv = forEachS(DType.BOOL8, 4, structcv1, + (b, l, r, i) -> b.append(r.isNull(i) ? false : + l < (Integer) r.getStruct(i).dataRecord.get(0)))) { + assertColumnsAreEqual(expectedsv, answersv, "scalar struct int32 < struct int32"); + } + + try (ColumnVector answervs = structcv1.lessThan(sscv); + ColumnVector expectedvs = forEachS(DType.BOOL8, structcv1, 4, + (b, l, r, i) -> b.append(l.isNull(i) ? true : + (Integer) l.getStruct(i).dataRecord.get(0) < r))) { + assertColumnsAreEqual(expectedvs, answervs, "struct int32 < scalar struct int32"); + } + + try (ColumnVector answervv = structcv1.lessThan(structcv2); + ColumnVector expectedvv = forEach(DType.BOOL8, structcv1, structcv2, + (b, l, r, i) -> b.append(l.isNull(i) ? true : r.isNull(i) || + (Integer)l.getStruct(i).dataRecord.get(0) < (Integer)r.getStruct(i).dataRecord.get(0)))) { + assertColumnsAreEqual(expectedvv, answervv, "struct int32 < struct int32"); + } } } @@ -1075,16 +1146,16 @@ public void testStringLessThanScalarNotPresent() { @Test public void testGreaterThan() { - try (ColumnVector icv1 = ColumnVector.fromBoxedInts(INTS_1); - ColumnVector icv2 = ColumnVector.fromBoxedInts(INTS_2); - ColumnVector structcv1 = ColumnVector.makeStruct(icv1); - ColumnVector structcv2 = ColumnVector.makeStruct(icv2); + try (ColumnVector icv = ColumnVector.fromBoxedInts(INTS_1); ColumnVector intscalar = ColumnVector.fromInts(4); + Scalar sscv = Scalar.structFromColumnViews(intscalar); ColumnVector dcv = ColumnVector.fromBoxedDoubles(DOUBLES_1); + ColumnVector structcv1 = ColumnVector.fromStructs(structType, int_struct_data_1); + ColumnVector structcv2 = ColumnVector.fromStructs(structType, int_struct_data_2); ColumnVector dec32cv1 = ColumnVector.fromDecimals(BIGDECIMAL32_1); ColumnVector dec32cv2 = ColumnVector.fromDecimals(BIGDECIMAL32_2)) { - try (ColumnVector answer = icv1.greaterThan(dcv); - ColumnVector expected = forEach(DType.BOOL8, icv1, dcv, + try (ColumnVector answer = icv.greaterThan(dcv); + ColumnVector expected = forEach(DType.BOOL8, icv, dcv, (b, l, r, i) -> b.append(l.getInt(i) > r.getDouble(i)))) { assertColumnsAreEqual(expected, answer, "int32 > double"); } @@ -1096,36 +1167,37 @@ public void testGreaterThan() { } try (Scalar s = Scalar.fromFloat(1.0f); - ColumnVector answer = icv1.greaterThan(s); - ColumnVector expected = forEachS(DType.BOOL8, icv1, 1.0f, + ColumnVector answer = icv.greaterThan(s); + ColumnVector expected = forEachS(DType.BOOL8, icv, 1.0f, (b, l, r, i) -> b.append(l.getInt(i) > r))) { assertColumnsAreEqual(expected, answer, "int64 > scalar float"); } try (Scalar s = Scalar.fromShort((short) 100); - ColumnVector answer = s.greaterThan(icv1); - ColumnVector expected = forEachS(DType.BOOL8, (short) 100, icv1, + ColumnVector answer = s.greaterThan(icv); + ColumnVector expected = forEachS(DType.BOOL8, (short) 100, icv, (b, l, r, i) -> b.append(l > r.getInt(i)))) { assertColumnsAreEqual(expected, answer, "scalar short > int32"); } - try (Scalar s = Scalar.structFromColumnViews(intscalar); - ColumnVector answersv = s.greaterThan(structcv1); - ColumnVector expectedsv = forEachS(DType.BOOL8, 4, icv1, - (b, l, r, i) -> b.append(r.isNull(i) ? true : l > r.getInt(i)), true)) { + try (ColumnVector answersv = sscv.greaterThan(structcv1); + ColumnVector expectedsv = forEachS(DType.BOOL8, 4, structcv1, + (b, l, r, i) -> b.append(r.isNull(i) ? true : + l > (Integer) r.getStruct(i).dataRecord.get(0)))) { assertColumnsAreEqual(expectedsv, answersv, "scalar struct int32 > struct int32"); } - try (Scalar s = Scalar.structFromColumnViews(intscalar); - ColumnVector answervs = structcv1.greaterThan(s); - ColumnVector expectedvs = forEachS(DType.BOOL8, icv1, 4, - (b, l, r, i) -> b.append(l.isNull(i) ? false : l.getInt(i) > r), true)) { + try (ColumnVector answervs = structcv1.greaterThan(sscv); + ColumnVector expectedvs = forEachS(DType.BOOL8, structcv1, 4, + (b, l, r, i) -> b.append(l.isNull(i) ? false : + (Integer) l.getStruct(i).dataRecord.get(0) > r))) { assertColumnsAreEqual(expectedvs, answervs, "struct int32 > scalar struct int32"); } try (ColumnVector answervv = structcv1.greaterThan(structcv2); - ColumnVector expectedvv = forEach(DType.BOOL8, icv1, icv2, - (b, l, r, i) -> b.append(l.isNull(i) ? false : r.isNull(i) || l.getInt(i) > r.getInt(i)), true)) { + ColumnVector expectedvv = forEach(DType.BOOL8, structcv1, structcv2, + (b, l, r, i) -> b.append(l.isNull(i) ? false : r.isNull(i) || + (Integer)l.getStruct(i).dataRecord.get(0) > (Integer)r.getStruct(i).dataRecord.get(0)))) { assertColumnsAreEqual(expectedvv, answervv, "struct int32 > struct int32"); } } @@ -1181,29 +1253,29 @@ public void testStringGreaterThanScalarNotPresent() { @Test public void testLessOrEqualTo() { - try (ColumnVector icv1 = ColumnVector.fromBoxedInts(INTS_1); - ColumnVector icv2 = ColumnVector.fromBoxedInts(INTS_2); - ColumnVector structcv1 = ColumnVector.makeStruct(icv1); - ColumnVector structcv2 = ColumnVector.makeStruct(icv2); + try (ColumnVector icv = ColumnVector.fromBoxedInts(INTS_1); ColumnVector intscalar = ColumnVector.fromInts(4); + Scalar sscv = Scalar.structFromColumnViews(intscalar); ColumnVector dcv = ColumnVector.fromBoxedDoubles(DOUBLES_1); + ColumnVector structcv1 = ColumnVector.fromStructs(structType, int_struct_data_1); + ColumnVector structcv2 = ColumnVector.fromStructs(structType, int_struct_data_2); ColumnVector dec32cv = ColumnVector.decimalFromInts(-dec32Scale_2, DECIMAL32_2)) { - try (ColumnVector answer = icv1.lessOrEqualTo(dcv); - ColumnVector expected = forEach(DType.BOOL8, icv1, dcv, + try (ColumnVector answer = icv.lessOrEqualTo(dcv); + ColumnVector expected = forEach(DType.BOOL8, icv, dcv, (b, l, r, i) -> b.append(l.getInt(i) <= r.getDouble(i)))) { assertColumnsAreEqual(expected, answer, "int32 <= double"); } try (Scalar s = Scalar.fromFloat(1.0f); - ColumnVector answer = icv1.lessOrEqualTo(s); - ColumnVector expected = forEachS(DType.BOOL8, icv1, 1.0f, + ColumnVector answer = icv.lessOrEqualTo(s); + ColumnVector expected = forEachS(DType.BOOL8, icv, 1.0f, (b, l, r, i) -> b.append(l.getInt(i) <= r))) { assertColumnsAreEqual(expected, answer, "int64 <= scalar float"); } try (Scalar s = Scalar.fromShort((short) 100); - ColumnVector answer = s.lessOrEqualTo(icv1); - ColumnVector expected = forEachS(DType.BOOL8, (short) 100, icv1, + ColumnVector answer = s.lessOrEqualTo(icv); + ColumnVector expected = forEachS(DType.BOOL8, (short) 100, icv, (b, l, r, i) -> b.append(l <= r.getInt(i)))) { assertColumnsAreEqual(expected, answer, "scalar short <= int32"); } @@ -1216,23 +1288,24 @@ public void testLessOrEqualTo() { } } - try (Scalar s = Scalar.structFromColumnViews(intscalar); - ColumnVector answersv = s.lessOrEqualTo(structcv1); - ColumnVector expectedsv = forEachS(DType.BOOL8, 4, icv1, - (b, l, r, i) -> b.append(r.isNull(i) ? false : l <= r.getInt(i)), true)) { + try (ColumnVector answersv = sscv.lessOrEqualTo(structcv1); + ColumnVector expectedsv = forEachS(DType.BOOL8, 4, structcv1, + (b, l, r, i) -> b.append(r.isNull(i) ? false : + l <= (Integer) r.getStruct(i).dataRecord.get(0)))) { assertColumnsAreEqual(expectedsv, answersv, "scalar struct int32 <= struct int32"); } - try (Scalar s = Scalar.structFromColumnViews(intscalar); - ColumnVector answervs = structcv1.lessOrEqualTo(s); - ColumnVector expectedvs = forEachS(DType.BOOL8, icv1, 4, - (b, l, r, i) -> b.append(l.isNull(i) ? true : l.getInt(i) <= r), true)) { + try (ColumnVector answervs = structcv1.lessOrEqualTo(sscv); + ColumnVector expectedvs = forEachS(DType.BOOL8, structcv1, 4, + (b, l, r, i) -> b.append(l.isNull(i) ? true : + (Integer) l.getStruct(i).dataRecord.get(0) <= r))) { assertColumnsAreEqual(expectedvs, answervs, "struct int32 <= scalar struct int32"); } try (ColumnVector answervv = structcv1.lessOrEqualTo(structcv2); - ColumnVector expectedvv = forEach(DType.BOOL8, icv1, icv2, - (b, l, r, i) -> b.append(l.isNull(i) ? true : !r.isNull(i) && l.getInt(i) <= r.getInt(i)), true)) { + ColumnVector expectedvv = forEach(DType.BOOL8, structcv1, structcv2, + (b, l, r, i) -> b.append(l.isNull(i) ? true : !r.isNull(i) && + (Integer)l.getStruct(i).dataRecord.get(0) <= (Integer)r.getStruct(i).dataRecord.get(0)))) { assertColumnsAreEqual(expectedvv, answervv, "struct int32 <= struct int32"); } } @@ -1289,7 +1362,11 @@ public void testStringLessOrEqualToScalarNotPresent() { @Test public void testGreaterOrEqualTo() { try (ColumnVector icv = ColumnVector.fromBoxedInts(INTS_1); + ColumnVector intscalar = ColumnVector.fromInts(4); + Scalar sscv = Scalar.structFromColumnViews(intscalar); ColumnVector dcv = ColumnVector.fromBoxedDoubles(DOUBLES_1); + ColumnVector structcv1 = ColumnVector.fromStructs(structType, int_struct_data_1); + ColumnVector structcv2 = ColumnVector.fromStructs(structType, int_struct_data_2); ColumnVector dec32cv = ColumnVector.decimalFromInts(-dec32Scale_2, DECIMAL32_2)) { try (ColumnVector answer = icv.greaterOrEqualTo(dcv); ColumnVector expected = forEach(DType.BOOL8, icv, dcv, @@ -1318,6 +1395,25 @@ public void testGreaterOrEqualTo() { assertColumnsAreEqual(expected, answer, "dec32 >= scalar dec32"); } } + + try (ColumnVector answersv = sscv.greaterOrEqualTo(structcv1); + ColumnVector expectedsv = forEachS(DType.BOOL8, 4, structcv1, + (b, l, r, i) -> b.append(r.isNull(i) ? true : l >= (Integer) r.getStruct(i).dataRecord.get(0)))) { + assertColumnsAreEqual(expectedsv, answersv, "scalar struct int32 >= struct int32"); + } + + try (ColumnVector answervs = structcv1.greaterOrEqualTo(sscv); + ColumnVector expectedvs = forEachS(DType.BOOL8, structcv1, 4, + (b, l, r, i) -> b.append(l.isNull(i) ? false : (Integer) l.getStruct(i).dataRecord.get(0) >= r))) { + assertColumnsAreEqual(expectedvs, answervs, "struct int32 >= scalar struct int32"); + } + + try (ColumnVector answervv = structcv1.greaterOrEqualTo(structcv2); + ColumnVector expectedvv = forEach(DType.BOOL8, structcv1, structcv2, + (b, l, r, i) -> b.append(l.isNull(i) ? false : !r.isNull(i) && + (Integer)l.getStruct(i).dataRecord.get(0) >= (Integer)r.getStruct(i).dataRecord.get(0)))) { + assertColumnsAreEqual(expectedvv, answervv, "struct int32 >= struct int32"); + } } } @@ -1694,7 +1790,11 @@ public void testArctan2() { @Test public void testEqualNullAware() { try (ColumnVector icv = ColumnVector.fromBoxedInts(INTS_1); - ColumnVector dcv = ColumnVector.fromBoxedDoubles(DOUBLES_1)) { + ColumnVector intscalar = ColumnVector.fromInts(4); + Scalar sscv = Scalar.structFromColumnViews(intscalar); + ColumnVector dcv = ColumnVector.fromBoxedDoubles(DOUBLES_1); + ColumnVector structcv1 = ColumnVector.fromStructs(structType, int_struct_data_1); + ColumnVector structcv2 = ColumnVector.fromStructs(structType, int_struct_data_2)) { try (ColumnVector answer = icv.equalToNullAware(dcv); ColumnVector expected = ColumnVector.fromBoxedBooleans(true, false, false, false, false, false, false)) { @@ -1714,6 +1814,27 @@ public void testEqualNullAware() { false, true)) { assertColumnsAreEqual(expected, answer, "scalar short <=> int32"); } + + try (ColumnVector answersv = sscv.equalToNullAware(structcv1); + ColumnVector expectedsv = forEachS(DType.BOOL8, 4, structcv1, + (b, l, r, i) -> b.append(r.isNull(i) ? false : + l == r.getStruct(i).dataRecord.get(0)), true)) { + assertColumnsAreEqual(expectedsv, answersv, "scalar struct int32 <=> struct int32"); + } + + try (ColumnVector answervs = structcv1.equalToNullAware(sscv); + ColumnVector expectedvs = forEachS(DType.BOOL8, structcv1, 4, + (b, l, r, i) -> b.append(l.isNull(i) ? false : + l.getStruct(i).dataRecord.get(0) == r), true)) { + assertColumnsAreEqual(expectedvs, answervs, "struct int32 <=> scalar struct int32"); + } + + try (ColumnVector answervv = structcv1.equalToNullAware(structcv2); + ColumnVector expectedvv = forEach(DType.BOOL8, structcv1, structcv2, + (b, l, r, i) -> b.append(l.isNull(i) || r.isNull(i) ? l.isNull(i) && r.isNull(i) : + l.getStruct(i).dataRecord.get(0) == r.getStruct(i).dataRecord.get(0)), true)) { + assertColumnsAreEqual(expectedvv, answervv, "struct int32 <=> struct int32"); + } } } From 5e3107333a98f2ce9d561e35920654ee8644a2d5 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 16 Aug 2022 08:11:38 -0400 Subject: [PATCH 35/58] Refactor pad_side and strip_type enums into side_type enum (#11438) Refactors the `cudf::strings::pad_side` and `cudf::strings::strip_type` to a single enum `cudf::strings::side_type`. These have the same values as used by `cudf::strings::pad` and `cudf::strings::strip` Moving these into a single header helps with reusing them in the `strings_udf` work. Updates to gtests and cython code layers are also included. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) - Jason Lowe (https://github.com/jlowe) - AJ Schmidt (https://github.com/ajschmidt8) - Robert Maynard (https://github.com/robertmaynard) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11438 --- conda/recipes/libcudf/meta.yaml | 1 + cpp/include/cudf/strings/padding.hpp | 37 +++++++------------ cpp/include/cudf/strings/side_type.hpp | 37 +++++++++++++++++++ cpp/include/cudf/strings/strip.hpp | 24 ++++-------- cpp/src/strings/padding.cu | 18 ++++----- cpp/src/strings/strip.cu | 28 +++++++------- cpp/tests/strings/pad_tests.cpp | 12 +++--- cpp/tests/strings/strip_tests.cpp | 8 ++-- java/src/main/native/src/ColumnViewJni.cpp | 4 +- python/cudf/cudf/_lib/cpp/strings/padding.pxd | 11 ++---- .../cudf/cudf/_lib/cpp/strings/side_type.pxd | 12 ++++++ python/cudf/cudf/_lib/cpp/strings/strip.pxd | 9 ++--- python/cudf/cudf/_lib/strings/__init__.py | 9 ++++- python/cudf/cudf/_lib/strings/padding.pxd | 4 -- python/cudf/cudf/_lib/strings/padding.pyx | 34 ++++++++--------- python/cudf/cudf/_lib/strings/strip.pyx | 14 +++---- python/cudf/cudf/core/column/string.py | 2 +- 17 files changed, 143 insertions(+), 121 deletions(-) create mode 100644 cpp/include/cudf/strings/side_type.hpp create mode 100644 python/cudf/cudf/_lib/cpp/strings/side_type.pxd delete mode 100644 python/cudf/cudf/_lib/strings/padding.pxd diff --git a/conda/recipes/libcudf/meta.yaml b/conda/recipes/libcudf/meta.yaml index 8128ace0b78..a417b407044 100644 --- a/conda/recipes/libcudf/meta.yaml +++ b/conda/recipes/libcudf/meta.yaml @@ -235,6 +235,7 @@ outputs: - test -f $PREFIX/include/cudf/strings/repeat_strings.hpp - test -f $PREFIX/include/cudf/strings/replace.hpp - test -f $PREFIX/include/cudf/strings/replace_re.hpp + - test -f $PREFIX/include/cudf/strings/side_type.hpp - test -f $PREFIX/include/cudf/strings/split/partition.hpp - test -f $PREFIX/include/cudf/strings/split/split.hpp - test -f $PREFIX/include/cudf/strings/split/split_re.hpp diff --git a/cpp/include/cudf/strings/padding.hpp b/cpp/include/cudf/strings/padding.hpp index 2cbf549a485..da4bb066c3e 100644 --- a/cpp/include/cudf/strings/padding.hpp +++ b/cpp/include/cudf/strings/padding.hpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -29,23 +30,13 @@ namespace strings { * @file */ -/** - * @brief Pad types for the pad method specify where the pad - * character should be placed. - */ -enum class pad_side { - LEFT, ///< Add padding to the left. - RIGHT, ///< Add padding to the right. - BOTH ///< Add padding equally to the right and left. -}; - /** * @brief Add padding to each string using a provided character. * - * If the string is already width or more characters, no padding is performed. - * No strings are truncated. + * If the string is already `width` or more characters, no padding is performed. + * Also, no strings are truncated. * - * Null string entries result in null entries in the output column. + * Null string entries result in corresponding null entries in the output column. * * @code{.pseudo} * Example: @@ -54,19 +45,19 @@ enum class pad_side { * r is now ['aa ','bbb ','cccc','ddddd'] * @endcode * - * @param strings Strings instance for this operation. - * @param width The minimum number of characters for each string. - * @param side Where to place the padding characters. - * Default is pad right (left justify). - * @param fill_char Single UTF-8 character to use for padding. - * Default is the space character. - * @param mr Device memory resource used to allocate the returned column's device memory. - * @return New column with padded strings. + * @param input Strings instance for this operation + * @param width The minimum number of characters for each string + * @param side Where to place the padding characters; + * Default is pad right (left justify) + * @param fill_char Single UTF-8 character to use for padding; + * Default is the space character + * @param mr Device memory resource used to allocate the returned column's device memory + * @return New column with padded strings */ std::unique_ptr pad( - strings_column_view const& strings, + strings_column_view const& input, size_type width, - pad_side side = cudf::strings::pad_side::RIGHT, + side_type side = side_type::RIGHT, std::string_view fill_char = " ", rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); diff --git a/cpp/include/cudf/strings/side_type.hpp b/cpp/include/cudf/strings/side_type.hpp new file mode 100644 index 00000000000..5905e087deb --- /dev/null +++ b/cpp/include/cudf/strings/side_type.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +namespace cudf { +namespace strings { +/** + * @addtogroup strings_modify + * @{ + * @file + */ + +/** + * @brief Direction identifier for cudf::strings::strip and cudf::strings::pad functions. + */ +enum class side_type { + LEFT, ///< strip/pad characters from the beginning of the string + RIGHT, ///< strip/pad characters from the end of the string + BOTH ///< strip/pad characters from the beginning and end of the string +}; + +/** @} */ // end of doxygen group +} // namespace strings +} // namespace cudf diff --git a/cpp/include/cudf/strings/strip.hpp b/cpp/include/cudf/strings/strip.hpp index 9a2f64efc74..adf3b291144 100644 --- a/cpp/include/cudf/strings/strip.hpp +++ b/cpp/include/cudf/strings/strip.hpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -29,15 +30,6 @@ namespace strings { * @file */ -/** - * @brief Direction identifier for strip() function. - */ -enum class strip_type { - LEFT, ///< strip characters from the beginning of the string - RIGHT, ///< strip characters from the end of the string - BOTH ///< strip characters from the beginning and end of the string -}; - /** * @brief Removes the specified characters from the beginning or end * (or both) of each string. @@ -60,17 +52,17 @@ enum class strip_type { * * @throw cudf::logic_error if `to_strip` is invalid. * - * @param strings Strings column for this operation. - * @param stype Indicates characters are to be stripped from the beginning, end, or both of each - * string. Default is both. - * @param to_strip UTF-8 encoded characters to strip from each string. - * Default is empty string which indicates strip whitespace characters. + * @param input Strings column for this operation + * @param side Indicates characters are to be stripped from the beginning, end, or both of each + * string; Default is both + * @param to_strip UTF-8 encoded characters to strip from each string; + * Default is empty string which indicates strip whitespace characters * @param mr Device memory resource used to allocate the returned column's device memory. * @return New strings column. */ std::unique_ptr strip( - strings_column_view const& strings, - strip_type stype = strip_type::BOTH, + strings_column_view const& input, + side_type side = side_type::BOTH, string_scalar const& to_strip = string_scalar(""), rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); diff --git a/cpp/src/strings/padding.cu b/cpp/src/strings/padding.cu index f3ce169483b..7d3215078aa 100644 --- a/cpp/src/strings/padding.cu +++ b/cpp/src/strings/padding.cu @@ -60,7 +60,7 @@ struct compute_pad_output_length_fn { std::unique_ptr pad( strings_column_view const& strings, size_type width, - pad_side side = pad_side::RIGHT, + side_type side = side_type::RIGHT, std::string_view fill_char = " ", rmm::cuda_stream_view stream = cudf::default_stream_value, rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()) @@ -90,7 +90,7 @@ std::unique_ptr pad( auto chars_column = strings::detail::create_chars_child_column(bytes, stream, mr); auto d_chars = chars_column->mutable_view().data(); - if (side == pad_side::LEFT) { + if (side == side_type::LEFT) { thrust::for_each_n( rmm::exec_policy(stream), thrust::make_counting_iterator(0), @@ -104,7 +104,7 @@ std::unique_ptr pad( ptr += from_char_utf8(d_fill_char, ptr); copy_string(ptr, d_str); }); - } else if (side == pad_side::RIGHT) { + } else if (side == side_type::RIGHT) { thrust::for_each_n( rmm::exec_policy(stream), thrust::make_counting_iterator(0), @@ -118,7 +118,7 @@ std::unique_ptr pad( while (length++ < width) ptr += from_char_utf8(d_fill_char, ptr); }); - } else if (side == pad_side::BOTH) { + } else if (side == side_type::BOTH) { thrust::for_each_n( rmm::exec_policy(stream), thrust::make_counting_iterator(0), @@ -204,22 +204,22 @@ std::unique_ptr zfill( // Public APIs -std::unique_ptr pad(strings_column_view const& strings, +std::unique_ptr pad(strings_column_view const& input, size_type width, - pad_side side, + side_type side, std::string_view fill_char, rmm::mr::device_memory_resource* mr) { CUDF_FUNC_RANGE(); - return detail::pad(strings, width, side, fill_char, cudf::default_stream_value, mr); + return detail::pad(input, width, side, fill_char, cudf::default_stream_value, mr); } -std::unique_ptr zfill(strings_column_view const& strings, +std::unique_ptr zfill(strings_column_view const& input, size_type width, rmm::mr::device_memory_resource* mr) { CUDF_FUNC_RANGE(); - return detail::zfill(strings, width, cudf::default_stream_value, mr); + return detail::zfill(input, width, cudf::default_stream_value, mr); } } // namespace strings diff --git a/cpp/src/strings/strip.cu b/cpp/src/strings/strip.cu index 53b75d821fc..8f9794f6679 100644 --- a/cpp/src/strings/strip.cu +++ b/cpp/src/strings/strip.cu @@ -48,7 +48,7 @@ namespace { */ struct strip_fn { column_device_view const d_strings; - strip_type const stype; // right, left, or both + side_type const side; // right, left, or both string_view const d_to_strip; int32_t* d_offsets{}; char* d_chars{}; @@ -70,14 +70,14 @@ struct strip_fn { }; size_type const left_offset = [&] { - if (stype != strip_type::LEFT && stype != strip_type::BOTH) return 0; + if (side != side_type::LEFT && side != side_type::BOTH) return 0; auto const itr = thrust::find_if_not(thrust::seq, d_str.begin(), d_str.end(), is_strip_character); return itr != d_str.end() ? itr.byte_offset() : d_str.size_bytes(); }(); size_type right_offset = d_str.size_bytes(); - if (stype == strip_type::RIGHT || stype == strip_type::BOTH) { + if (side == side_type::RIGHT || side == side_type::BOTH) { auto const length = d_str.length(); auto itr = d_str.end(); for (size_type n = 0; n < length; ++n) { @@ -97,41 +97,41 @@ struct strip_fn { } // namespace std::unique_ptr strip( - strings_column_view const& strings, - strip_type stype = strip_type::BOTH, + strings_column_view const& input, + side_type side = side_type::BOTH, string_scalar const& to_strip = string_scalar(""), rmm::cuda_stream_view stream = cudf::default_stream_value, rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()) { - if (strings.is_empty()) return make_empty_column(type_id::STRING); + if (input.is_empty()) return make_empty_column(type_id::STRING); CUDF_EXPECTS(to_strip.is_valid(stream), "Parameter to_strip must be valid"); string_view const d_to_strip(to_strip.data(), to_strip.size()); - auto const d_column = column_device_view::create(strings.parent(), stream); + auto const d_column = column_device_view::create(input.parent(), stream); // this utility calls the strip_fn to build the offsets and chars columns auto children = cudf::strings::detail::make_strings_children( - strip_fn{*d_column, stype, d_to_strip}, strings.size(), stream, mr); + strip_fn{*d_column, side, d_to_strip}, input.size(), stream, mr); - return make_strings_column(strings.size(), + return make_strings_column(input.size(), std::move(children.first), std::move(children.second), - strings.null_count(), - cudf::detail::copy_bitmask(strings.parent(), stream, mr)); + input.null_count(), + cudf::detail::copy_bitmask(input.parent(), stream, mr)); } } // namespace detail // external APIs -std::unique_ptr strip(strings_column_view const& strings, - strip_type stype, +std::unique_ptr strip(strings_column_view const& input, + side_type side, string_scalar const& to_strip, rmm::mr::device_memory_resource* mr) { CUDF_FUNC_RANGE(); - return detail::strip(strings, stype, to_strip, cudf::default_stream_value, mr); + return detail::strip(input, side, to_strip, cudf::default_stream_value, mr); } } // namespace strings diff --git a/cpp/tests/strings/pad_tests.cpp b/cpp/tests/strings/pad_tests.cpp index 4ec4690cf00..c3ec2fa34f8 100644 --- a/cpp/tests/strings/pad_tests.cpp +++ b/cpp/tests/strings/pad_tests.cpp @@ -45,7 +45,7 @@ TEST_F(StringsPadTest, Padding) auto strings_view = cudf::strings_column_view(strings); { - auto results = cudf::strings::pad(strings_view, width, cudf::strings::pad_side::RIGHT, phil); + auto results = cudf::strings::pad(strings_view, width, cudf::strings::side_type::RIGHT, phil); std::vector h_expected{ "eee ddd", "bb cc+", nullptr, "++++++", "aa++++", "bbb+++", "ééé+++", "o+++++"}; @@ -56,7 +56,7 @@ TEST_F(StringsPadTest, Padding) CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); } { - auto results = cudf::strings::pad(strings_view, width, cudf::strings::pad_side::LEFT, phil); + auto results = cudf::strings::pad(strings_view, width, cudf::strings::side_type::LEFT, phil); std::vector h_expected{ "eee ddd", "+bb cc", nullptr, "++++++", "++++aa", "+++bbb", "+++ééé", "+++++o"}; @@ -67,7 +67,7 @@ TEST_F(StringsPadTest, Padding) CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); } { - auto results = cudf::strings::pad(strings_view, width, cudf::strings::pad_side::BOTH, phil); + auto results = cudf::strings::pad(strings_view, width, cudf::strings::side_type::BOTH, phil); std::vector h_expected{ "eee ddd", "bb cc+", nullptr, "++++++", "++aa++", "+bbb++", "+ééé++", "++o+++"}; @@ -86,12 +86,12 @@ TEST_F(StringsPadTest, PaddingBoth) auto strings_view = cudf::strings_column_view(strings); { // even width left justify - auto results = cudf::strings::pad(strings_view, 6, cudf::strings::pad_side::BOTH, phil); + auto results = cudf::strings::pad(strings_view, 6, cudf::strings::side_type::BOTH, phil); cudf::test::strings_column_wrapper expected({"koala+", "+foxx+", "+fox++", "chameleon"}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); } { // odd width right justify - auto results = cudf::strings::pad(strings_view, 7, cudf::strings::pad_side::BOTH, phil); + auto results = cudf::strings::pad(strings_view, 7, cudf::strings::side_type::BOTH, phil); cudf::test::strings_column_wrapper expected({"+koala+", "++foxx+", "++fox++", "chameleon"}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); } @@ -115,7 +115,7 @@ TEST_P(PadParameters, Padding) cudf::test::strings_column_wrapper strings(h_strings.begin(), h_strings.end()); cudf::size_type width = GetParam(); auto strings_view = cudf::strings_column_view(strings); - auto results = cudf::strings::pad(strings_view, width, cudf::strings::pad_side::RIGHT); + auto results = cudf::strings::pad(strings_view, width, cudf::strings::side_type::RIGHT); std::vector h_expected; for (auto itr = h_strings.begin(); itr != h_strings.end(); ++itr) { diff --git a/cpp/tests/strings/strip_tests.cpp b/cpp/tests/strings/strip_tests.cpp index 4c1d3b67600..6916b990762 100644 --- a/cpp/tests/strings/strip_tests.cpp +++ b/cpp/tests/strings/strip_tests.cpp @@ -41,7 +41,7 @@ TEST_F(StringsStripTest, StripLeft) thrust::make_transform_iterator(h_strings.begin(), [](auto str) { return str != nullptr; })); auto strings_view = cudf::strings_column_view(strings); - auto results = cudf::strings::strip(strings_view, cudf::strings::strip_type::LEFT); + auto results = cudf::strings::strip(strings_view, cudf::strings::side_type::LEFT); cudf::test::strings_column_wrapper expected( h_expected.begin(), @@ -62,7 +62,7 @@ TEST_F(StringsStripTest, StripRight) auto strings_view = cudf::strings_column_view(strings); auto results = - cudf::strings::strip(strings_view, cudf::strings::strip_type::RIGHT, cudf::string_scalar(" a")); + cudf::strings::strip(strings_view, cudf::strings::side_type::RIGHT, cudf::string_scalar(" a")); cudf::test::strings_column_wrapper expected( h_expected.begin(), @@ -83,7 +83,7 @@ TEST_F(StringsStripTest, StripBoth) auto strings_view = cudf::strings_column_view(strings); auto results = - cudf::strings::strip(strings_view, cudf::strings::strip_type::BOTH, cudf::string_scalar(" é")); + cudf::strings::strip(strings_view, cudf::strings::side_type::BOTH, cudf::string_scalar(" é")); cudf::test::strings_column_wrapper expected( h_expected.begin(), @@ -108,6 +108,6 @@ TEST_F(StringsStripTest, InvalidParameter) cudf::test::strings_column_wrapper strings(h_strings.begin(), h_strings.end()); auto strings_view = cudf::strings_column_view(strings); EXPECT_THROW(cudf::strings::strip( - strings_view, cudf::strings::strip_type::BOTH, cudf::string_scalar("", false)), + strings_view, cudf::strings::side_type::BOTH, cudf::string_scalar("", false)), cudf::logic_error); } diff --git a/java/src/main/native/src/ColumnViewJni.cpp b/java/src/main/native/src/ColumnViewJni.cpp index 9687d333ea9..9134879b372 100644 --- a/java/src/main/native/src/ColumnViewJni.cpp +++ b/java/src/main/native/src/ColumnViewJni.cpp @@ -1592,7 +1592,7 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_pad(JNIEnv *env, jclass, cudf::column_view *cv = reinterpret_cast(column_view); cudf::strings_column_view scv(*cv); cudf::size_type width = reinterpret_cast(j_width); - cudf::strings::pad_side side = static_cast(j_side); + cudf::strings::side_type side = static_cast(j_side); cudf::jni::native_jstring ss_fill(env, fill_char); return release_as_jlong(cudf::strings::pad(scv, width, side, ss_fill.get())); } @@ -1609,7 +1609,7 @@ JNIEXPORT jlong JNICALL Java_ai_rapids_cudf_ColumnView_stringStrip(JNIEnv *env, cudf::jni::auto_set_device(env); cudf::column_view *cv = reinterpret_cast(column_view); cudf::strings_column_view scv(*cv); - cudf::strings::strip_type s_striptype = static_cast(strip_type); + cudf::strings::side_type s_striptype = static_cast(strip_type); cudf::string_scalar *ss_tostrip = reinterpret_cast(to_strip); return release_as_jlong(cudf::strings::strip(scv, s_striptype, *ss_tostrip)); } diff --git a/python/cudf/cudf/_lib/cpp/strings/padding.pxd b/python/cudf/cudf/_lib/cpp/strings/padding.pxd index 2077e687be3..c3906a5b4c6 100644 --- a/python/cudf/cudf/_lib/cpp/strings/padding.pxd +++ b/python/cudf/cudf/_lib/cpp/strings/padding.pxd @@ -1,4 +1,4 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2020-2022, NVIDIA CORPORATION. from libc.stdint cimport int32_t from libcpp.memory cimport unique_ptr from libcpp.string cimport string @@ -6,23 +6,18 @@ from libcpp.string cimport string from cudf._lib.cpp.column.column cimport column from cudf._lib.cpp.column.column_view cimport column_view from cudf._lib.cpp.scalar.scalar cimport string_scalar +from cudf._lib.cpp.strings.side_type cimport side_type from cudf._lib.cpp.types cimport size_type cdef extern from "cudf/strings/padding.hpp" namespace "cudf::strings" nogil: - ctypedef enum pad_side: - LEFT 'cudf::strings::pad_side::LEFT' - RIGHT 'cudf::strings::pad_side::RIGHT' - BOTH 'cudf::strings::pad_side::BOTH' cdef unique_ptr[column] pad( column_view source_strings, size_type width, - pad_side side, + side_type side, string fill_char) except + cdef unique_ptr[column] zfill( column_view source_strings, size_type width) except + - -ctypedef int32_t underlying_type_t_pad_side diff --git a/python/cudf/cudf/_lib/cpp/strings/side_type.pxd b/python/cudf/cudf/_lib/cpp/strings/side_type.pxd new file mode 100644 index 00000000000..3a89299f11a --- /dev/null +++ b/python/cudf/cudf/_lib/cpp/strings/side_type.pxd @@ -0,0 +1,12 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. +from libc.stdint cimport int32_t + + +cdef extern from "cudf/strings/side_type.hpp" namespace "cudf::strings" nogil: + + ctypedef enum side_type: + LEFT 'cudf::strings::side_type::LEFT' + RIGHT 'cudf::strings::side_type::RIGHT' + BOTH 'cudf::strings::side_type::BOTH' + +ctypedef int32_t underlying_type_t_side_type diff --git a/python/cudf/cudf/_lib/cpp/strings/strip.pxd b/python/cudf/cudf/_lib/cpp/strings/strip.pxd index 82a84fd2d14..3a86f80328f 100644 --- a/python/cudf/cudf/_lib/cpp/strings/strip.pxd +++ b/python/cudf/cudf/_lib/cpp/strings/strip.pxd @@ -1,19 +1,16 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2020-2022, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr from cudf._lib.cpp.column.column cimport column from cudf._lib.cpp.column.column_view cimport column_view from cudf._lib.cpp.scalar.scalar cimport string_scalar +from cudf._lib.cpp.strings.side_type cimport side_type cdef extern from "cudf/strings/strip.hpp" namespace "cudf::strings" nogil: - ctypedef enum strip_type: - LEFT 'cudf::strings::strip_type::LEFT' - RIGHT 'cudf::strings::strip_type::RIGHT' - BOTH 'cudf::strings::strip_type::BOTH' cdef unique_ptr[column] strip( column_view source_strings, - strip_type stype, + side_type stype, string_scalar to_strip) except + diff --git a/python/cudf/cudf/_lib/strings/__init__.py b/python/cudf/cudf/_lib/strings/__init__.py index 7e1c88c9258..4fa86e2b30d 100644 --- a/python/cudf/cudf/_lib/strings/__init__.py +++ b/python/cudf/cudf/_lib/strings/__init__.py @@ -63,7 +63,14 @@ ) from cudf._lib.strings.findall import findall, findall_record from cudf._lib.strings.json import get_json_object, GetJsonObjectOptions -from cudf._lib.strings.padding import PadSide, center, ljust, pad, rjust, zfill +from cudf._lib.strings.padding import ( + SideType, + center, + ljust, + pad, + rjust, + zfill, +) from cudf._lib.strings.repeat import repeat_scalar, repeat_sequence from cudf._lib.strings.replace import ( insert, diff --git a/python/cudf/cudf/_lib/strings/padding.pxd b/python/cudf/cudf/_lib/strings/padding.pxd deleted file mode 100644 index 4b5984c61b6..00000000000 --- a/python/cudf/cudf/_lib/strings/padding.pxd +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. -from libc.stdint cimport int32_t - -ctypedef int32_t underlying_type_t_pad_side diff --git a/python/cudf/cudf/_lib/strings/padding.pyx b/python/cudf/cudf/_lib/strings/padding.pyx index c7b97977d60..9377870c1c1 100644 --- a/python/cudf/cudf/_lib/strings/padding.pyx +++ b/python/cudf/cudf/_lib/strings/padding.pyx @@ -1,4 +1,4 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2020-2022, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr from libcpp.utility cimport move @@ -14,24 +14,23 @@ from enum import IntEnum from libcpp.string cimport string from cudf._lib.cpp.column.column cimport column -from cudf._lib.cpp.strings.padding cimport ( - pad as cpp_pad, - pad_side as pad_side, - zfill as cpp_zfill, +from cudf._lib.cpp.strings.padding cimport pad as cpp_pad, zfill as cpp_zfill +from cudf._lib.cpp.strings.side_type cimport ( + side_type, + underlying_type_t_side_type, ) -from cudf._lib.strings.padding cimport underlying_type_t_pad_side -class PadSide(IntEnum): - LEFT = pad_side.LEFT - RIGHT = pad_side.RIGHT - BOTH = pad_side.BOTH +class SideType(IntEnum): + LEFT = side_type.LEFT + RIGHT = side_type.RIGHT + BOTH = side_type.BOTH def pad(Column source_strings, size_type width, fill_char, - side=PadSide.LEFT): + side=SideType.LEFT): """ Returns a Column by padding strings in `source_strings` up to the given `width`. Direction of padding is to be specified by `side`. @@ -43,8 +42,8 @@ def pad(Column source_strings, cdef string f_char = str(fill_char).encode() - cdef pad_side pad_direction = ( - side + cdef side_type pad_direction = ( + side ) with nogil: @@ -87,14 +86,13 @@ def center(Column source_strings, cdef unique_ptr[column] c_result cdef column_view source_view = source_strings.view() - cdef pad_side pad_direction cdef string f_char = str(fill_char).encode() with nogil: c_result = move(cpp_pad( source_view, width, - pad_side.BOTH, + side_type.BOTH, f_char )) @@ -111,14 +109,13 @@ def ljust(Column source_strings, cdef unique_ptr[column] c_result cdef column_view source_view = source_strings.view() - cdef pad_side pad_direction cdef string f_char = str(fill_char).encode() with nogil: c_result = move(cpp_pad( source_view, width, - pad_side.RIGHT, + side_type.RIGHT, f_char )) @@ -135,14 +132,13 @@ def rjust(Column source_strings, cdef unique_ptr[column] c_result cdef column_view source_view = source_strings.view() - cdef pad_side pad_direction cdef string f_char = str(fill_char).encode() with nogil: c_result = move(cpp_pad( source_view, width, - pad_side.LEFT, + side_type.LEFT, f_char )) diff --git a/python/cudf/cudf/_lib/strings/strip.pyx b/python/cudf/cudf/_lib/strings/strip.pyx index d3430a53cc6..93dfbcedb83 100644 --- a/python/cudf/cudf/_lib/strings/strip.pyx +++ b/python/cudf/cudf/_lib/strings/strip.pyx @@ -1,4 +1,4 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2020-2022, NVIDIA CORPORATION. from libcpp.memory cimport unique_ptr from libcpp.string cimport string @@ -8,10 +8,8 @@ from cudf._lib.column cimport Column from cudf._lib.cpp.column.column cimport column from cudf._lib.cpp.column.column_view cimport column_view from cudf._lib.cpp.scalar.scalar cimport string_scalar -from cudf._lib.cpp.strings.strip cimport ( - strip as cpp_strip, - strip_type as strip_type, -) +from cudf._lib.cpp.strings.side_type cimport side_type +from cudf._lib.cpp.strings.strip cimport strip as cpp_strip from cudf._lib.cpp.types cimport size_type from cudf._lib.scalar cimport DeviceScalar @@ -36,7 +34,7 @@ def strip(Column source_strings, with nogil: c_result = move(cpp_strip( source_view, - strip_type.BOTH, + side_type.BOTH, scalar_str[0] )) @@ -63,7 +61,7 @@ def lstrip(Column source_strings, with nogil: c_result = move(cpp_strip( source_view, - strip_type.LEFT, + side_type.LEFT, scalar_str[0] )) @@ -90,7 +88,7 @@ def rstrip(Column source_strings, with nogil: c_result = move(cpp_strip( source_view, - strip_type.RIGHT, + side_type.RIGHT, scalar_str[0] )) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index a69a422db7f..005545d662a 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -2897,7 +2897,7 @@ def pad( raise TypeError(msg) try: - side = libstrings.PadSide[side.upper()] + side = libstrings.SideType[side.upper()] except KeyError: raise ValueError( "side has to be either one of {‘left’, ‘right’, ‘both’}" From 8e207217a8a7570e412e12d5f1aeecb5d065faa6 Mon Sep 17 00:00:00 2001 From: Robert Maynard Date: Tue, 16 Aug 2022 08:48:38 -0400 Subject: [PATCH 36/58] Use rapids-cmake 22.10 best practice for RAPIDS.cmake location (#11493) Removes possibility of another projects `RAPIDS.cmake` being used, and removes need to always download a version. Authors: - Robert Maynard (https://github.com/robertmaynard) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11493 --- fetch_rapids.cmake | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fetch_rapids.cmake b/fetch_rapids.cmake index 354acc074ae..9e2917ffc07 100644 --- a/fetch_rapids.cmake +++ b/fetch_rapids.cmake @@ -11,7 +11,9 @@ # or implied. See the License for the specific language governing permissions and limitations under # the License. # ============================================================================= -file(DOWNLOAD https://raw.githubusercontent.com/rapidsai/rapids-cmake/branch-22.10/RAPIDS.cmake - ${CMAKE_BINARY_DIR}/RAPIDS.cmake -) -include(${CMAKE_BINARY_DIR}/RAPIDS.cmake) +if(NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/CUDF_RAPIDS.cmake) + file(DOWNLOAD https://raw.githubusercontent.com/rapidsai/rapids-cmake/branch-22.10/RAPIDS.cmake + ${CMAKE_CURRENT_BINARY_DIR}/CUDF_RAPIDS.cmake + ) +endif() +include(${CMAKE_CURRENT_BINARY_DIR}/CUDF_RAPIDS.cmake) From 0c4b319855a477c940dc83a31e0e3eab0f0ba0f4 Mon Sep 17 00:00:00 2001 From: Ed Seidl Date: Tue, 16 Aug 2022 07:43:43 -0700 Subject: [PATCH 37/58] Control Parquet page size through Python API (#11454) Closes #10988 Exposes page_size_rows and page_size_bytes properties of the Parquet writer. Authors: - Ed Seidl (https://github.com/etseidl) Approvers: - Bradley Dice (https://github.com/bdice) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/11454 --- python/cudf/cudf/_lib/cpp/io/parquet.pxd | 20 +++++++++++++++ python/cudf/cudf/_lib/parquet.pyx | 22 ++++++++++++++++- python/cudf/cudf/io/parquet.py | 10 ++++++++ python/cudf/cudf/tests/test_parquet.py | 31 ++++++++++++++++++++++++ python/cudf/cudf/utils/ioutils.py | 6 +++++ 5 files changed, 88 insertions(+), 1 deletion(-) diff --git a/python/cudf/cudf/_lib/cpp/io/parquet.pxd b/python/cudf/cudf/_lib/cpp/io/parquet.pxd index 88850ff6687..f388fff3beb 100644 --- a/python/cudf/cudf/_lib/cpp/io/parquet.pxd +++ b/python/cudf/cudf/_lib/cpp/io/parquet.pxd @@ -69,6 +69,8 @@ cdef extern from "cudf/io/parquet.hpp" namespace "cudf::io" nogil: string get_column_chunks_file_paths() except+ size_t get_row_group_size_bytes() except+ size_type get_row_group_size_rows() except+ + size_t get_max_page_size_bytes() except+ + size_type get_max_page_size_rows() except+ void set_partitions( vector[cudf_io_types.partition_info] partitions @@ -90,6 +92,8 @@ cdef extern from "cudf/io/parquet.hpp" namespace "cudf::io" nogil: ) except + void set_row_group_size_bytes(size_t val) except+ void set_row_group_size_rows(size_type val) except+ + void set_max_page_size_bytes(size_t val) except+ + void set_max_page_size_rows(size_type val) except+ @staticmethod parquet_writer_options_builder builder( @@ -131,6 +135,12 @@ cdef extern from "cudf/io/parquet.hpp" namespace "cudf::io" nogil: parquet_writer_options_builder& row_group_size_rows( size_type val ) except+ + parquet_writer_options_builder& max_page_size_bytes( + size_t val + ) except+ + parquet_writer_options_builder& max_page_size_rows( + size_type val + ) except+ parquet_writer_options build() except + @@ -147,6 +157,8 @@ cdef extern from "cudf/io/parquet.hpp" namespace "cudf::io" nogil: ) except+ size_t get_row_group_size_bytes() except+ size_type get_row_group_size_rows() except+ + size_t get_max_page_size_bytes() except+ + size_type get_max_page_size_rows() except+ void set_metadata( cudf_io_types.table_input_metadata *m @@ -162,6 +174,8 @@ cdef extern from "cudf/io/parquet.hpp" namespace "cudf::io" nogil: ) except + void set_row_group_size_bytes(size_t val) except+ void set_row_group_size_rows(size_type val) except+ + void set_max_page_size_bytes(size_t val) except+ + void set_max_page_size_rows(size_type val) except+ @staticmethod chunked_parquet_writer_options_builder builder( @@ -191,6 +205,12 @@ cdef extern from "cudf/io/parquet.hpp" namespace "cudf::io" nogil: chunked_parquet_writer_options_builder& row_group_size_rows( size_type val ) except+ + chunked_parquet_writer_options_builder& max_page_size_bytes( + size_t val + ) except+ + chunked_parquet_writer_options_builder& max_page_size_rows( + size_type val + ) except+ chunked_parquet_writer_options build() except + diff --git a/python/cudf/cudf/_lib/parquet.pyx b/python/cudf/cudf/_lib/parquet.pyx index 1be3b953687..c87f58bb16c 100644 --- a/python/cudf/cudf/_lib/parquet.pyx +++ b/python/cudf/cudf/_lib/parquet.pyx @@ -321,6 +321,8 @@ cpdef write_parquet( object int96_timestamps=False, object row_group_size_bytes=None, object row_group_size_rows=None, + object max_page_size_bytes=None, + object max_page_size_rows=None, object partitions_info=None): """ Cython function to call into libcudf API, see `write_parquet`. @@ -419,6 +421,10 @@ cpdef write_parquet( args.set_row_group_size_bytes(row_group_size_bytes) if row_group_size_rows is not None: args.set_row_group_size_rows(row_group_size_rows) + if max_page_size_bytes is not None: + args.set_max_page_size_bytes(max_page_size_bytes) + if max_page_size_rows is not None: + args.set_max_page_size_rows(max_page_size_rows) with nogil: out_metadata_c = move(parquet_writer(args)) @@ -456,6 +462,12 @@ cdef class ParquetWriter: row_group_size_rows: int, default 1000000 Maximum number of rows of each stripe of the output. By default, 1000000 (10^6 rows) will be used. + max_page_size_bytes: int, default 524288 + Maximum uncompressed size of each page of the output. + By default, 524288 (512KB) will be used. + max_page_size_rows: int, default 20000 + Maximum number of rows of each page of the output. + By default, 20000 will be used. See Also -------- @@ -471,11 +483,15 @@ cdef class ParquetWriter: cdef object index cdef size_t row_group_size_bytes cdef size_type row_group_size_rows + cdef size_t max_page_size_bytes + cdef size_type max_page_size_rows def __cinit__(self, object filepath_or_buffer, object index=None, object compression=None, str statistics="ROWGROUP", int row_group_size_bytes=134217728, - int row_group_size_rows=1000000): + int row_group_size_rows=1000000, + int max_page_size_bytes=524288, + int max_page_size_rows=20000): filepaths_or_buffers = ( list(filepath_or_buffer) if is_list_like(filepath_or_buffer) @@ -488,6 +504,8 @@ cdef class ParquetWriter: self.initialized = False self.row_group_size_bytes = row_group_size_bytes self.row_group_size_rows = row_group_size_rows + self.max_page_size_bytes = max_page_size_bytes + self.max_page_size_rows = max_page_size_rows def write_table(self, table, object partitions_info=None): """ Writes a single table to the file """ @@ -602,6 +620,8 @@ cdef class ParquetWriter: .stats_level(self.stat_freq) .row_group_size_bytes(self.row_group_size_bytes) .row_group_size_rows(self.row_group_size_rows) + .max_page_size_bytes(self.max_page_size_bytes) + .max_page_size_rows(self.max_page_size_rows) .build() ) self.writer.reset(new cpp_parquet_chunked_writer(args)) diff --git a/python/cudf/cudf/io/parquet.py b/python/cudf/cudf/io/parquet.py index 1812155d894..4e2d04cc5b0 100644 --- a/python/cudf/cudf/io/parquet.py +++ b/python/cudf/cudf/io/parquet.py @@ -56,6 +56,8 @@ def _write_parquet( int96_timestamps=False, row_group_size_bytes=None, row_group_size_rows=None, + max_page_size_bytes=None, + max_page_size_rows=None, partitions_info=None, **kwargs, ): @@ -82,6 +84,8 @@ def _write_parquet( "int96_timestamps": int96_timestamps, "row_group_size_bytes": row_group_size_bytes, "row_group_size_rows": row_group_size_rows, + "max_page_size_bytes": max_page_size_bytes, + "max_page_size_rows": max_page_size_rows, "partitions_info": partitions_info, } if all(ioutils.is_fsspec_open_file(buf) for buf in paths_or_bufs): @@ -598,6 +602,8 @@ def to_parquet( int96_timestamps=False, row_group_size_bytes=None, row_group_size_rows=None, + max_page_size_bytes=None, + max_page_size_rows=None, *args, **kwargs, ): @@ -627,6 +633,8 @@ def to_parquet( "int96_timestamps": int96_timestamps, "row_group_size_bytes": row_group_size_bytes, "row_group_size_rows": row_group_size_rows, + "max_page_size_bytes": max_page_size_bytes, + "max_page_size_rows": max_page_size_rows, } ) return write_to_dataset( @@ -656,6 +664,8 @@ def to_parquet( int96_timestamps=int96_timestamps, row_group_size_bytes=row_group_size_bytes, row_group_size_rows=row_group_size_rows, + max_page_size_bytes=max_page_size_bytes, + max_page_size_rows=max_page_size_rows, **kwargs, ) diff --git a/python/cudf/cudf/tests/test_parquet.py b/python/cudf/cudf/tests/test_parquet.py index 326c117585b..44b13823346 100644 --- a/python/cudf/cudf/tests/test_parquet.py +++ b/python/cudf/cudf/tests/test_parquet.py @@ -1558,6 +1558,37 @@ def test_parquet_writer_row_group_size(tmpdir, row_group_size_kwargs): assert_eq(cudf.read_parquet(fname), gdf) +@pytest.mark.parametrize( + "max_page_size_kwargs", + [ + {"max_page_size_bytes": 4 * 1024}, + {"max_page_size_rows": 5000}, + ], +) +def test_parquet_writer_max_page_size(tmpdir, max_page_size_kwargs): + # Check that max_page_size options are exposed in Python + # Since we don't have access to page metadata, instead check that + # file written with more pages will be slightly larger + + size = 20000 + gdf = cudf.DataFrame({"a": range(size), "b": [1] * size}) + + fname = tmpdir.join("gdf.parquet") + with ParquetWriter(fname, **max_page_size_kwargs) as writer: + writer.write_table(gdf) + s1 = os.path.getsize(fname) + + assert_eq(cudf.read_parquet(fname), gdf) + + fname = tmpdir.join("gdf0.parquet") + with ParquetWriter(fname) as writer: + writer.write_table(gdf) + s2 = os.path.getsize(fname) + + assert_eq(cudf.read_parquet(fname), gdf) + assert s1 > s2 + + @pytest.mark.parametrize("filename", ["myfile.parquet", None]) @pytest.mark.parametrize("cols", [["b"], ["c", "b"]]) def test_parquet_partitioned(tmpdir_factory, cols, filename): diff --git a/python/cudf/cudf/utils/ioutils.py b/python/cudf/cudf/utils/ioutils.py index bb6716c1c4a..3e7fb4c4f02 100644 --- a/python/cudf/cudf/utils/ioutils.py +++ b/python/cudf/cudf/utils/ioutils.py @@ -247,6 +247,12 @@ row_group_size_rows: integer or None, default None Maximum number of rows of each stripe of the output. If None, 1000000 will be used. +max_page_size_bytes: integer or None, default None + Maximum uncompressed size of each page of the output. + If None, 524288 (512KB) will be used. +max_page_size_rows: integer or None, default None + Maximum number of rows of each page of the output. + If None, 20000 will be used. **kwargs To request metadata binary blob when using with ``partition_cols``, Pass ``return_metadata=True`` instead of specifying ``metadata_file_path`` From 63a47d91fc558a5b0b89f3ce9b0a61ffb3ed84f5 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Tue, 16 Aug 2022 11:55:44 -0700 Subject: [PATCH 38/58] Add `create_random_column` function to the data generator (#11490) Adds an API to create a single random column, so users don't need to create a table even when a single column is required. The interface is the same as `create_random_table`, except that it only takes a single data type. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Karthikeyan (https://github.com/karthikeyann) - Nghia Truong (https://github.com/ttnghia) - Mike Wilson (https://github.com/hyperbolic2346) URL: https://github.com/rapidsai/cudf/pull/11490 --- cpp/benchmarks/common/generate_input.cu | 14 ++++++-- cpp/benchmarks/common/generate_input.hpp | 27 +++++++++++---- cpp/benchmarks/copying/contiguous_split.cu | 6 ++-- cpp/benchmarks/filling/repeat.cpp | 7 ++-- cpp/benchmarks/groupby/group_max.cpp | 33 +++++++------------ cpp/benchmarks/groupby/group_nth.cpp | 6 ++-- cpp/benchmarks/groupby/group_nunique.cpp | 31 +++++++---------- cpp/benchmarks/groupby/group_scan.cpp | 20 ++++------- cpp/benchmarks/groupby/group_struct_keys.cpp | 5 ++- cpp/benchmarks/groupby/group_sum.cpp | 20 ++++------- cpp/benchmarks/io/text/multibyte_split.cpp | 10 +++--- cpp/benchmarks/reduction/anyall.cpp | 8 ++--- cpp/benchmarks/reduction/dictionary.cpp | 4 +-- cpp/benchmarks/reduction/distinct_count.cpp | 4 +-- cpp/benchmarks/reduction/minmax.cpp | 7 ++-- cpp/benchmarks/reduction/reduce.cpp | 10 +++--- cpp/benchmarks/reduction/scan.cpp | 9 +++-- cpp/benchmarks/reduction/segment_reduce.cu | 4 +-- cpp/benchmarks/replace/clamp.cpp | 9 +++-- cpp/benchmarks/replace/nans.cpp | 7 ++-- cpp/benchmarks/search/contains.cpp | 13 ++------ cpp/benchmarks/sort/rank.cpp | 5 ++- .../stream_compaction/apply_boolean_mask.cpp | 5 ++- cpp/benchmarks/stream_compaction/distinct.cpp | 5 ++- cpp/benchmarks/stream_compaction/unique.cpp | 5 ++- cpp/benchmarks/string/case.cpp | 4 +-- cpp/benchmarks/string/convert_datetime.cpp | 4 +-- cpp/benchmarks/string/convert_fixed_point.cpp | 6 ++-- cpp/benchmarks/string/convert_numerics.cpp | 4 +-- cpp/benchmarks/string/extract.cpp | 14 ++++---- cpp/benchmarks/string/factory.cu | 8 ++--- cpp/benchmarks/string/filter.cpp | 6 ++-- cpp/benchmarks/string/find.cpp | 6 ++-- cpp/benchmarks/string/replace.cpp | 6 ++-- cpp/benchmarks/string/replace_re.cpp | 6 ++-- cpp/benchmarks/string/split.cpp | 6 ++-- cpp/benchmarks/string/substring.cpp | 6 ++-- cpp/benchmarks/string/translate.cpp | 6 ++-- cpp/benchmarks/text/ngrams.cpp | 10 +++--- cpp/benchmarks/text/normalize.cpp | 10 +++--- cpp/benchmarks/text/normalize_spaces.cpp | 10 +++--- cpp/benchmarks/text/tokenize.cpp | 10 +++--- cpp/docs/BENCHMARKING.md | 3 +- 43 files changed, 178 insertions(+), 221 deletions(-) diff --git a/cpp/benchmarks/common/generate_input.cu b/cpp/benchmarks/common/generate_input.cu index 49831f680c7..890a78bb9bf 100644 --- a/cpp/benchmarks/common/generate_input.cu +++ b/cpp/benchmarks/common/generate_input.cu @@ -785,13 +785,21 @@ std::unique_ptr create_random_table(std::vector cons columns_vector output_columns; std::transform( dtype_ids.begin(), dtype_ids.end(), std::back_inserter(output_columns), [&](auto tid) mutable { - auto engine = deterministic_engine(seed_dist(seed_engine)); - return cudf::type_dispatcher( - cudf::data_type(tid), create_rand_col_fn{}, profile, engine, num_rows.count); + return create_random_column(tid, num_rows, profile, seed_dist(seed_engine)); }); return std::make_unique(std::move(output_columns)); } +std::unique_ptr create_random_column(cudf::type_id dtype_id, + row_count num_rows, + data_profile const& profile, + unsigned seed) +{ + auto engine = deterministic_engine(seed); + return cudf::type_dispatcher( + cudf::data_type(dtype_id), create_rand_col_fn{}, profile, engine, num_rows.count); +} + std::unique_ptr create_sequence_table(std::vector const& dtype_ids, row_count num_rows, std::optional null_probability, diff --git a/cpp/benchmarks/common/generate_input.hpp b/cpp/benchmarks/common/generate_input.hpp index 12bb48a18c0..9451e4d9499 100644 --- a/cpp/benchmarks/common/generate_input.hpp +++ b/cpp/benchmarks/common/generate_input.hpp @@ -606,8 +606,8 @@ struct row_count { * @param dtype_ids Vector of requested column types * @param table_bytes Target size of the output table, in bytes. Some type may not produce columns * of exact size - * @param data_params optional, set of data parameters describing the data profile for each type - * @param seed optional, seed for the pseudo-random engine + * @param data_params Optional, set of data parameters describing the data profile for each type + * @param seed Optional, seed for the pseudo-random engine */ std::unique_ptr create_random_table(std::vector const& dtype_ids, table_size_bytes table_bytes, @@ -619,23 +619,36 @@ std::unique_ptr create_random_table(std::vector cons * * @param dtype_ids Vector of requested column types * @param num_rows Number of rows in the output table - * @param data_params optional, set of data parameters describing the data profile for each type - * @param seed optional, seed for the pseudo-random engine + * @param data_params Optional, set of data parameters describing the data profile for each type + * @param seed Optional, seed for the pseudo-random engine */ std::unique_ptr create_random_table(std::vector const& dtype_ids, row_count num_rows, data_profile const& data_params = data_profile{}, unsigned seed = 1); +/** + * @brief Deterministically generates a column filled with data with the given parameters. + * + * @param dtype_id Requested column type + * @param num_rows Number of rows in the output column + * @param data_params Optional, set of data parameters describing the data profile + * @param seed Optional, seed for the pseudo-random engine + */ +std::unique_ptr create_random_column(cudf::type_id dtype_id, + row_count num_rows, + data_profile const& data_params = data_profile{}, + unsigned seed = 1); + /** * @brief Generate sequence columns starting with value 0 in first row and increasing by 1 in * subsequent rows. * * @param dtype_ids Vector of requested column types * @param num_rows Number of rows in the output table - * @param null_probability optional, probability of a null value + * @param null_probability Optional, probability of a null value * no value implies no null mask, =0 implies all valids, >=1 implies all nulls - * @param seed optional, seed for the pseudo-random engine + * @param seed Optional, seed for the pseudo-random engine * @return A table with the sequence columns. */ std::unique_ptr create_sequence_table( @@ -660,7 +673,7 @@ std::vector cycle_dtypes(std::vector const& dtype_ * @param size number of rows * @param null_probability probability of a null value * no value implies no null mask, =0 implies all valids, >=1 implies all nulls - * @param seed optional, seed for the pseudo-random engine + * @param seed Optional, seed for the pseudo-random engine * @return null mask device buffer with random null mask data and null count */ std::pair create_random_null_mask( diff --git a/cpp/benchmarks/copying/contiguous_split.cu b/cpp/benchmarks/copying/contiguous_split.cu index 6da28f6e3a5..45f04c38923 100644 --- a/cpp/benchmarks/copying/contiguous_split.cu +++ b/cpp/benchmarks/copying/contiguous_split.cu @@ -120,10 +120,10 @@ void BM_contiguous_split_strings(benchmark::State& state) cudf::test::strings_column_wrapper one_col(h_strings.begin(), h_strings.end()); std::vector> src_cols(num_cols); for (int64_t idx = 0; idx < num_cols; idx++) { - auto random_indices = create_random_table( - {cudf::type_id::INT32}, row_count{static_cast(num_rows)}, profile); + auto random_indices = create_random_column( + cudf::type_id::INT32, row_count{static_cast(num_rows)}, profile); auto str_table = cudf::gather(cudf::table_view{{one_col}}, - random_indices->get_column(0), + *random_indices, (include_validity ? cudf::out_of_bounds_policy::NULLIFY : cudf::out_of_bounds_policy::DONT_CHECK)); src_cols[idx] = std::move(str_table->release()[0]); diff --git a/cpp/benchmarks/filling/repeat.cpp b/cpp/benchmarks/filling/repeat.cpp index e5d143d24ea..179cdba718c 100644 --- a/cpp/benchmarks/filling/repeat.cpp +++ b/cpp/benchmarks/filling/repeat.cpp @@ -40,15 +40,14 @@ void BM_repeat(benchmark::State& state) using sizeT = cudf::size_type; data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 3); - auto repeat_table = create_random_table({cudf::type_to_id()}, row_count{n_rows}, profile); - cudf::column_view repeat_count{repeat_table->get_column(0)}; + auto repeat_count = create_random_column(cudf::type_to_id(), row_count{n_rows}, profile); // warm up - auto output = cudf::repeat(input, repeat_count); + auto output = cudf::repeat(input, *repeat_count); for (auto _ : state) { cuda_event_timer raii(state, true); // flush_l2_cache = true, stream = 0 - cudf::repeat(input, repeat_count); + cudf::repeat(input, *repeat_count); } auto data_bytes = diff --git a/cpp/benchmarks/groupby/group_max.cpp b/cpp/benchmarks/groupby/group_max.cpp index eb9c9859da9..8454d1afee6 100644 --- a/cpp/benchmarks/groupby/group_max.cpp +++ b/cpp/benchmarks/groupby/group_max.cpp @@ -27,38 +27,29 @@ void bench_groupby_max(nvbench::state& state, nvbench::type_list) cudf::rmm_pool_raii pool_raii; const auto size = static_cast(state.get_int64("num_rows")); - auto const keys_table = [&] { - data_profile profile; - profile.set_null_probability(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params( + auto const keys = [&] { + data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); - return create_random_table({cudf::type_to_id()}, row_count{size}, profile); + return create_random_column(cudf::type_to_id(), row_count{size}, profile); }(); - auto const vals_table = [&] { - data_profile profile; + auto const vals = [&] { + auto builder = data_profile_builder().cardinality(0).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 1000); if (const auto null_freq = state.get_float64("null_probability"); null_freq > 0) { - profile.set_null_probability({null_freq}); + builder.null_probability(null_freq); } else { - profile.set_null_probability(std::nullopt); + builder.no_validity(); } - profile.set_cardinality(0); - profile.set_distribution_params(cudf::type_to_id(), - distribution_id::UNIFORM, - static_cast(0), - static_cast(1000)); - return create_random_table({cudf::type_to_id()}, row_count{size}, profile); + return create_random_column(cudf::type_to_id(), row_count{size}, data_profile{builder}); }(); - auto const& keys = keys_table->get_column(0); - auto const& vals = vals_table->get_column(0); - - auto gb_obj = cudf::groupby::groupby(cudf::table_view({keys, keys, keys})); + auto keys_view = keys->view(); + auto gb_obj = cudf::groupby::groupby(cudf::table_view({keys_view, keys_view, keys_view})); std::vector requests; requests.emplace_back(cudf::groupby::aggregation_request()); - requests[0].values = vals; + requests[0].values = vals->view(); requests[0].aggregations.push_back(cudf::make_max_aggregation()); state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::default_stream_value.value())); diff --git a/cpp/benchmarks/groupby/group_nth.cpp b/cpp/benchmarks/groupby/group_nth.cpp index ba16ae176e1..af2b9adc6b0 100644 --- a/cpp/benchmarks/groupby/group_nth.cpp +++ b/cpp/benchmarks/groupby/group_nth.cpp @@ -36,10 +36,8 @@ void BM_pre_sorted_nth(benchmark::State& state) cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); - auto vals_table = - create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); + auto vals = create_random_column(cudf::type_to_id(), row_count{column_size}, profile); - cudf::column_view vals(vals_table->get_column(0)); auto sort_order = cudf::sorted_order(*keys_table); auto sorted_keys = cudf::gather(*keys_table, *sort_order); // No need to sort values using sort_order because they were generated randomly @@ -48,7 +46,7 @@ void BM_pre_sorted_nth(benchmark::State& state) std::vector requests; requests.emplace_back(cudf::groupby::aggregation_request()); - requests[0].values = vals; + requests[0].values = vals->view(); requests[0].aggregations.push_back( cudf::make_nth_element_aggregation(-1)); diff --git a/cpp/benchmarks/groupby/group_nunique.cpp b/cpp/benchmarks/groupby/group_nunique.cpp index a8a5c69be48..1f95b5d5899 100644 --- a/cpp/benchmarks/groupby/group_nunique.cpp +++ b/cpp/benchmarks/groupby/group_nunique.cpp @@ -43,36 +43,27 @@ void bench_groupby_nunique(nvbench::state& state, nvbench::type_list) cudf::rmm_pool_raii pool_raii; const auto size = static_cast(state.get_int64("num_rows")); - auto const keys_table = [&] { - data_profile profile; - profile.set_null_probability(std::nullopt); - profile.set_cardinality(0); - profile.set_distribution_params( + auto const keys = [&] { + data_profile profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); - return create_random_table({cudf::type_to_id()}, row_count{size}, profile); + return create_random_column(cudf::type_to_id(), row_count{size}, profile); }(); - auto const vals_table = [&] { - data_profile profile; + auto const vals = [&] { + data_profile profile = data_profile_builder().cardinality(0).distribution( + cudf::type_to_id(), distribution_id::UNIFORM, 0, 1000); if (const auto null_freq = state.get_float64("null_probability"); null_freq > 0) { - profile.set_null_probability({null_freq}); + profile.set_null_probability(null_freq); } else { profile.set_null_probability(std::nullopt); } - profile.set_cardinality(0); - profile.set_distribution_params(cudf::type_to_id(), - distribution_id::UNIFORM, - static_cast(0), - static_cast(1000)); - return create_random_table({cudf::type_to_id()}, row_count{size}, profile); + return create_random_column(cudf::type_to_id(), row_count{size}, profile); }(); - auto const& keys = keys_table->get_column(0); - auto const& vals = vals_table->get_column(0); - - auto gb_obj = cudf::groupby::groupby(cudf::table_view({keys, keys, keys})); + auto gb_obj = + cudf::groupby::groupby(cudf::table_view({keys->view(), keys->view(), keys->view()})); auto const requests = make_aggregation_request_vector( - vals, cudf::make_nunique_aggregation()); + *vals, cudf::make_nunique_aggregation()); state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::default_stream_value.value())); state.exec(nvbench::exec_tag::sync, diff --git a/cpp/benchmarks/groupby/group_scan.cpp b/cpp/benchmarks/groupby/group_scan.cpp index e5d0b4b00a3..345519f881e 100644 --- a/cpp/benchmarks/groupby/group_scan.cpp +++ b/cpp/benchmarks/groupby/group_scan.cpp @@ -34,19 +34,14 @@ void BM_basic_sum_scan(benchmark::State& state) data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); - auto keys_table = - create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); - auto vals_table = - create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); + auto keys = create_random_column(cudf::type_to_id(), row_count{column_size}, profile); + auto vals = create_random_column(cudf::type_to_id(), row_count{column_size}, profile); - cudf::column_view keys(keys_table->get_column(0)); - cudf::column_view vals(vals_table->get_column(0)); - - cudf::groupby::groupby gb_obj(cudf::table_view({keys, keys, keys})); + cudf::groupby::groupby gb_obj(cudf::table_view({keys->view(), keys->view(), keys->view()})); std::vector requests; requests.emplace_back(cudf::groupby::scan_request()); - requests[0].values = vals; + requests[0].values = vals->view(); requests[0].aggregations.push_back(cudf::make_sum_aggregation()); for (auto _ : state) { @@ -74,10 +69,7 @@ void BM_pre_sorted_sum_scan(benchmark::State& state) auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); profile.set_null_probability(0.1); - auto vals_table = - create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); - - cudf::column_view vals(vals_table->get_column(0)); + auto vals = create_random_column(cudf::type_to_id(), row_count{column_size}, profile); auto sort_order = cudf::sorted_order(*keys_table); auto sorted_keys = cudf::gather(*keys_table, *sort_order); @@ -87,7 +79,7 @@ void BM_pre_sorted_sum_scan(benchmark::State& state) std::vector requests; requests.emplace_back(cudf::groupby::scan_request()); - requests[0].values = vals; + requests[0].values = vals->view(); requests[0].aggregations.push_back(cudf::make_sum_aggregation()); for (auto _ : state) { diff --git a/cpp/benchmarks/groupby/group_struct_keys.cpp b/cpp/benchmarks/groupby/group_struct_keys.cpp index 4e5df974134..227a4d5259a 100644 --- a/cpp/benchmarks/groupby/group_struct_keys.cpp +++ b/cpp/benchmarks/groupby/group_struct_keys.cpp @@ -73,14 +73,13 @@ void bench_groupby_struct_keys(nvbench::state& state) cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); auto const keys_table = cudf::table(std::move(child_cols)); - auto const vals_table = - create_random_table({cudf::type_to_id()}, row_count{n_rows}, profile); + auto const vals = create_random_column(cudf::type_to_id(), row_count{n_rows}, profile); cudf::groupby::groupby gb_obj(keys_table.view()); std::vector requests; requests.emplace_back(cudf::groupby::aggregation_request()); - requests[0].values = vals_table->get_column(0).view(); + requests[0].values = vals->view(); requests[0].aggregations.push_back(cudf::make_min_aggregation()); // Set up nvbench default stream diff --git a/cpp/benchmarks/groupby/group_sum.cpp b/cpp/benchmarks/groupby/group_sum.cpp index 9baacec868c..4d56af0bca1 100644 --- a/cpp/benchmarks/groupby/group_sum.cpp +++ b/cpp/benchmarks/groupby/group_sum.cpp @@ -33,19 +33,14 @@ void BM_basic_sum(benchmark::State& state) data_profile const profile = data_profile_builder().cardinality(0).no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); - auto keys_table = - create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); - auto vals_table = - create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); + auto keys = create_random_column(cudf::type_to_id(), row_count{column_size}, profile); + auto vals = create_random_column(cudf::type_to_id(), row_count{column_size}, profile); - cudf::column_view keys(keys_table->get_column(0)); - cudf::column_view vals(vals_table->get_column(0)); - - cudf::groupby::groupby gb_obj(cudf::table_view({keys, keys, keys})); + cudf::groupby::groupby gb_obj(cudf::table_view({keys->view(), keys->view(), keys->view()})); std::vector requests; requests.emplace_back(cudf::groupby::aggregation_request()); - requests[0].values = vals; + requests[0].values = vals->view(); requests[0].aggregations.push_back(cudf::make_sum_aggregation()); for (auto _ : state) { @@ -74,10 +69,7 @@ void BM_pre_sorted_sum(benchmark::State& state) auto keys_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); profile.set_null_probability(0.1); - auto vals_table = - create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); - - cudf::column_view vals(vals_table->get_column(0)); + auto vals = create_random_column(cudf::type_to_id(), row_count{column_size}, profile); auto sort_order = cudf::sorted_order(*keys_table); auto sorted_keys = cudf::gather(*keys_table, *sort_order); @@ -87,7 +79,7 @@ void BM_pre_sorted_sum(benchmark::State& state) std::vector requests; requests.emplace_back(cudf::groupby::aggregation_request()); - requests[0].values = vals; + requests[0].values = vals->view(); requests[0].aggregations.push_back(cudf::make_sum_aggregation()); for (auto _ : state) { diff --git a/cpp/benchmarks/io/text/multibyte_split.cpp b/cpp/benchmarks/io/text/multibyte_split.cpp index abb3a1b1134..f6e69452456 100644 --- a/cpp/benchmarks/io/text/multibyte_split.cpp +++ b/cpp/benchmarks/io/text/multibyte_split.cpp @@ -60,15 +60,13 @@ static cudf::string_scalar create_random_input(int32_t num_chars, data_profile const table_profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, value_size_min, value_size_max); - auto const values_table = create_random_table( // - {cudf::type_id::STRING}, - row_count{num_rows}, - table_profile); + auto const values = + create_random_column(cudf::type_id::STRING, row_count{num_rows}, table_profile); auto delim_scalar = cudf::make_string_scalar(delim); auto delims_column = cudf::make_column_from_scalar(*delim_scalar, num_rows); - auto input_table = cudf::table_view({values_table->get_column(0).view(), delims_column->view()}); - auto input_column = cudf::strings::concatenate(input_table); + auto input_table = cudf::table_view({values->view(), delims_column->view()}); + auto input_column = cudf::strings::concatenate(input_table); // extract the chars from the returned strings column. auto input_column_contents = input_column->release(); diff --git a/cpp/benchmarks/reduction/anyall.cpp b/cpp/benchmarks/reduction/anyall.cpp index f7cc3caca68..80a85b0f217 100644 --- a/cpp/benchmarks/reduction/anyall.cpp +++ b/cpp/benchmarks/reduction/anyall.cpp @@ -33,17 +33,15 @@ void BM_reduction_anyall(benchmark::State& state, { const cudf::size_type column_size{static_cast(state.range(0))}; auto const dtype = cudf::type_to_id(); - data_profile const profile = data_profile_builder().distribution( + data_profile const profile = data_profile_builder().no_validity().distribution( dtype, distribution_id::UNIFORM, 0, agg->kind == cudf::aggregation::ANY ? 0 : 100); - auto const table = create_random_table({dtype}, row_count{column_size}, profile); - table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); - cudf::column_view values(table->view().column(0)); + auto const values = create_random_column(dtype, row_count{column_size}, profile); cudf::data_type output_dtype{cudf::type_id::BOOL8}; for (auto _ : state) { cuda_event_timer timer(state, true); - auto result = cudf::reduce(values, agg, output_dtype); + auto result = cudf::reduce(*values, agg, output_dtype); } } diff --git a/cpp/benchmarks/reduction/dictionary.cpp b/cpp/benchmarks/reduction/dictionary.cpp index d897cf52795..219564d6b5c 100644 --- a/cpp/benchmarks/reduction/dictionary.cpp +++ b/cpp/benchmarks/reduction/dictionary.cpp @@ -38,8 +38,8 @@ void BM_reduction_dictionary(benchmark::State& state, distribution_id::UNIFORM, (agg->kind == cudf::aggregation::ALL ? 1 : 0), (agg->kind == cudf::aggregation::ANY ? 0 : 100)); - auto int_table = create_random_table({cudf::type_to_id()}, row_count{column_size}, profile); - auto number_col = cudf::cast(int_table->get_column(0), cudf::data_type{cudf::type_to_id()}); + auto int_column = create_random_column(cudf::type_to_id(), row_count{column_size}, profile); + auto number_col = cudf::cast(*int_column, cudf::data_type{cudf::type_to_id()}); auto values = cudf::dictionary::encode(*number_col); cudf::data_type output_dtype = [&] { diff --git a/cpp/benchmarks/reduction/distinct_count.cpp b/cpp/benchmarks/reduction/distinct_count.cpp index c63f13875be..489d7935809 100644 --- a/cpp/benchmarks/reduction/distinct_count.cpp +++ b/cpp/benchmarks/reduction/distinct_count.cpp @@ -30,8 +30,8 @@ static void bench_reduction_distinct_count(nvbench::state& state, nvbench::type_ auto const size = static_cast(state.get_int64("num_rows")); auto const null_probability = state.get_float64("null_probability"); - data_profile profile; - profile.set_distribution_params(dtype, distribution_id::UNIFORM, 0, size / 100); + data_profile profile = + data_profile_builder().distribution(dtype, distribution_id::UNIFORM, 0, size / 100); if (null_probability > 0) { profile.set_null_probability({null_probability}); } else { diff --git a/cpp/benchmarks/reduction/minmax.cpp b/cpp/benchmarks/reduction/minmax.cpp index 71a92e3498f..fa3b07b603e 100644 --- a/cpp/benchmarks/reduction/minmax.cpp +++ b/cpp/benchmarks/reduction/minmax.cpp @@ -30,13 +30,12 @@ void BM_reduction(benchmark::State& state) { const cudf::size_type column_size{(cudf::size_type)state.range(0)}; auto const dtype = cudf::type_to_id(); - auto const table = create_random_table({dtype}, row_count{column_size}); - table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); - cudf::column_view input_column(table->view().column(0)); + auto const input_column = + create_random_column(dtype, row_count{column_size}, data_profile_builder().no_validity()); for (auto _ : state) { cuda_event_timer timer(state, true); - auto result = cudf::minmax(input_column); + auto result = cudf::minmax(*input_column); } } diff --git a/cpp/benchmarks/reduction/reduce.cpp b/cpp/benchmarks/reduction/reduce.cpp index 3d4f2b8ff4e..4e354352c11 100644 --- a/cpp/benchmarks/reduction/reduce.cpp +++ b/cpp/benchmarks/reduction/reduce.cpp @@ -34,20 +34,18 @@ void BM_reduction(benchmark::State& state, std::unique_ptr(); data_profile const profile = - data_profile_builder().distribution(dtype, distribution_id::UNIFORM, 0, 100); - auto const table = create_random_table({dtype}, row_count{column_size}, profile); - table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); - cudf::column_view input_column(table->view().column(0)); + data_profile_builder().no_validity().distribution(dtype, distribution_id::UNIFORM, 0, 100); + auto const input_column = create_random_column(dtype, row_count{column_size}, profile); cudf::data_type output_dtype = (agg->kind == cudf::aggregation::MEAN || agg->kind == cudf::aggregation::VARIANCE || agg->kind == cudf::aggregation::STD) ? cudf::data_type{cudf::type_id::FLOAT64} - : input_column.type(); + : input_column->type(); for (auto _ : state) { cuda_event_timer timer(state, true); - auto result = cudf::reduce(input_column, agg, output_dtype); + auto result = cudf::reduce(*input_column, agg, output_dtype); } } diff --git a/cpp/benchmarks/reduction/scan.cpp b/cpp/benchmarks/reduction/scan.cpp index 8c434465795..354333ea411 100644 --- a/cpp/benchmarks/reduction/scan.cpp +++ b/cpp/benchmarks/reduction/scan.cpp @@ -31,15 +31,14 @@ template static void BM_reduction_scan(benchmark::State& state, bool include_nulls) { cudf::size_type const n_rows{(cudf::size_type)state.range(0)}; - auto const dtype = cudf::type_to_id(); - auto const table = create_random_table({dtype}, row_count{n_rows}); - if (!include_nulls) table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); - cudf::column_view input(table->view().column(0)); + auto const dtype = cudf::type_to_id(); + auto const column = create_random_column(dtype, row_count{n_rows}); + if (!include_nulls) column->set_null_mask(rmm::device_buffer{}, 0); for (auto _ : state) { cuda_event_timer timer(state, true); auto result = cudf::scan( - input, cudf::make_min_aggregation(), cudf::scan_type::INCLUSIVE); + *column, cudf::make_min_aggregation(), cudf::scan_type::INCLUSIVE); } } diff --git a/cpp/benchmarks/reduction/segment_reduce.cu b/cpp/benchmarks/reduction/segment_reduce.cu index edf9098bdad..dfd013fb4da 100644 --- a/cpp/benchmarks/reduction/segment_reduce.cu +++ b/cpp/benchmarks/reduction/segment_reduce.cu @@ -71,7 +71,7 @@ std::pair, thrust::device_vector> make_test_d auto const dtype = cudf::type_to_id(); data_profile profile = data_profile_builder().cardinality(0).no_validity().distribution( dtype, distribution_id::UNIFORM, 0, 100); - auto input = create_random_table({dtype}, row_count{column_size}, profile); + auto input = create_random_column(dtype, row_count{column_size}, profile); auto offset_it = detail::make_counting_transform_iterator(0, [column_size, segment_length] __device__(auto i) { @@ -80,7 +80,7 @@ std::pair, thrust::device_vector> make_test_d thrust::device_vector d_offsets(offset_it, offset_it + num_segments + 1); - return std::pair(std::move((input->release())[0]), d_offsets); + return std::pair(std::move(input), d_offsets); } template diff --git a/cpp/benchmarks/replace/clamp.cpp b/cpp/benchmarks/replace/clamp.cpp index e9a259d0c7b..ccd6b7ad9a1 100644 --- a/cpp/benchmarks/replace/clamp.cpp +++ b/cpp/benchmarks/replace/clamp.cpp @@ -33,11 +33,10 @@ static void BM_clamp(benchmark::State& state, bool include_nulls) { cudf::size_type const n_rows{(cudf::size_type)state.range(0)}; auto const dtype = cudf::type_to_id(); - auto const table = create_random_table({dtype}, row_count{n_rows}); - if (!include_nulls) { table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); } - cudf::column_view input(table->view().column(0)); + auto const input = create_random_column(dtype, row_count{n_rows}); + if (!include_nulls) input->set_null_mask(rmm::device_buffer{}, 0); - auto [low_scalar, high_scalar] = cudf::minmax(input); + auto [low_scalar, high_scalar] = cudf::minmax(*input); // set the clamps 2 in from the min and max { @@ -53,7 +52,7 @@ static void BM_clamp(benchmark::State& state, bool include_nulls) for (auto _ : state) { cuda_event_timer timer(state, true); - auto result = cudf::clamp(input, *low_scalar, *high_scalar); + auto result = cudf::clamp(*input, *low_scalar, *high_scalar); } } diff --git a/cpp/benchmarks/replace/nans.cpp b/cpp/benchmarks/replace/nans.cpp index 28ca798ebf0..0b63c8f3097 100644 --- a/cpp/benchmarks/replace/nans.cpp +++ b/cpp/benchmarks/replace/nans.cpp @@ -33,15 +33,14 @@ static void BM_replace_nans(benchmark::State& state, bool include_nulls) { cudf::size_type const n_rows{(cudf::size_type)state.range(0)}; auto const dtype = cudf::type_to_id(); - auto const table = create_random_table({dtype}, row_count{n_rows}); - if (!include_nulls) { table->get_column(0).set_null_mask(rmm::device_buffer{}, 0); } - cudf::column_view input(table->view().column(0)); + auto const input = create_random_column(dtype, row_count{n_rows}); + if (!include_nulls) input->set_null_mask(rmm::device_buffer{}, 0); auto zero = cudf::make_fixed_width_scalar(0); for (auto _ : state) { cuda_event_timer timer(state, true); - auto result = cudf::replace_nans(input, *zero); + auto result = cudf::replace_nans(*input, *zero); } } diff --git a/cpp/benchmarks/search/contains.cpp b/cpp/benchmarks/search/contains.cpp index ca54b775ca7..8daa975d4ed 100644 --- a/cpp/benchmarks/search/contains.cpp +++ b/cpp/benchmarks/search/contains.cpp @@ -25,22 +25,13 @@ namespace { template -std::unique_ptr create_table_data(cudf::size_type n_rows, - cudf::size_type n_cols, - bool has_nulls = false) +std::unique_ptr create_column_data(cudf::size_type n_rows, bool has_nulls = false) { data_profile profile = data_profile_builder().cardinality(0).distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 1000); profile.set_null_probability(has_nulls ? std::optional{0.1} : std::nullopt); - return create_random_table( - cycle_dtypes({cudf::type_to_id()}, n_cols), row_count{n_rows}, profile); -} - -template -std::unique_ptr create_column_data(cudf::size_type n_rows, bool has_nulls = false) -{ - return std::move(create_table_data(n_rows, 1, has_nulls)->release().front()); + return create_random_column(cudf::type_to_id(), row_count{n_rows}, profile); } } // namespace diff --git a/cpp/benchmarks/sort/rank.cpp b/cpp/benchmarks/sort/rank.cpp index 5425c722cdf..66277443800 100644 --- a/cpp/benchmarks/sort/rank.cpp +++ b/cpp/benchmarks/sort/rank.cpp @@ -34,13 +34,12 @@ static void BM_rank(benchmark::State& state, bool nulls) data_profile profile = data_profile_builder().cardinality(0).distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); profile.set_null_probability(nulls ? std::optional{0.01} : std::nullopt); - auto keys_table = create_random_table({cudf::type_to_id()}, row_count{n_rows}, profile); - cudf::column_view input{keys_table->get_column(0)}; + auto keys = create_random_column(cudf::type_to_id(), row_count{n_rows}, profile); for (auto _ : state) { cuda_event_timer raii(state, true, cudf::default_stream_value); - auto result = cudf::rank(input, + auto result = cudf::rank(keys->view(), cudf::rank_method::FIRST, cudf::order::ASCENDING, nulls ? cudf::null_policy::INCLUDE : cudf::null_policy::EXCLUDE, diff --git a/cpp/benchmarks/stream_compaction/apply_boolean_mask.cpp b/cpp/benchmarks/stream_compaction/apply_boolean_mask.cpp index 8ed58b2afc7..6663ccb7fd8 100644 --- a/cpp/benchmarks/stream_compaction/apply_boolean_mask.cpp +++ b/cpp/benchmarks/stream_compaction/apply_boolean_mask.cpp @@ -84,12 +84,11 @@ void BM_apply_boolean_mask(benchmark::State& state, cudf::size_type num_columns) profile.set_bool_probability_true(percent_true / 100.0); profile.set_null_probability(std::nullopt); // no null mask - auto mask_table = create_random_table({cudf::type_id::BOOL8}, row_count{column_size}, profile); - cudf::column_view mask = mask_table->get_column(0); + auto mask = create_random_column(cudf::type_id::BOOL8, row_count{column_size}, profile); for (auto _ : state) { cuda_event_timer raii(state, true); - auto result = cudf::apply_boolean_mask(*source_table, mask); + auto result = cudf::apply_boolean_mask(*source_table, mask->view()); } calculate_bandwidth(state, num_columns); diff --git a/cpp/benchmarks/stream_compaction/distinct.cpp b/cpp/benchmarks/stream_compaction/distinct.cpp index 7b11c303133..ad837bc4caa 100644 --- a/cpp/benchmarks/stream_compaction/distinct.cpp +++ b/cpp/benchmarks/stream_compaction/distinct.cpp @@ -36,10 +36,9 @@ void nvbench_distinct(nvbench::state& state, nvbench::type_list) data_profile profile = data_profile_builder().cardinality(0).null_probability(0.01).distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0, 100); - auto source_table = - create_random_table(cycle_dtypes({cudf::type_to_id()}, 1), row_count{num_rows}, profile); + auto source_column = create_random_column(cudf::type_to_id(), row_count{num_rows}, profile); - auto input_column = cudf::column_view(source_table->get_column(0)); + auto input_column = source_column->view(); auto input_table = cudf::table_view({input_column, input_column, input_column, input_column}); state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { diff --git a/cpp/benchmarks/stream_compaction/unique.cpp b/cpp/benchmarks/stream_compaction/unique.cpp index ef693dd74cb..6b586581408 100644 --- a/cpp/benchmarks/stream_compaction/unique.cpp +++ b/cpp/benchmarks/stream_compaction/unique.cpp @@ -57,10 +57,9 @@ void nvbench_unique(nvbench::state& state, nvbench::type_list(), distribution_id::UNIFORM, 0, 100); - auto source_table = - create_random_table(cycle_dtypes({cudf::type_to_id()}, 1), row_count{num_rows}, profile); + auto source_column = create_random_column(cudf::type_to_id(), row_count{num_rows}, profile); - auto input_column = cudf::column_view(source_table->get_column(0)); + auto input_column = source_column->view(); auto input_table = cudf::table_view({input_column, input_column, input_column, input_column}); state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { diff --git a/cpp/benchmarks/string/case.cpp b/cpp/benchmarks/string/case.cpp index 35ed825f769..1c43fa0f077 100644 --- a/cpp/benchmarks/string/case.cpp +++ b/cpp/benchmarks/string/case.cpp @@ -28,8 +28,8 @@ class StringCase : public cudf::benchmark { static void BM_case(benchmark::State& state) { cudf::size_type const n_rows{(cudf::size_type)state.range(0)}; - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}); + cudf::strings_column_view input(column->view()); for (auto _ : state) { cuda_event_timer raii(state, true, cudf::default_stream_value); diff --git a/cpp/benchmarks/string/convert_datetime.cpp b/cpp/benchmarks/string/convert_datetime.cpp index 488ce95d397..9f327a21aa8 100644 --- a/cpp/benchmarks/string/convert_datetime.cpp +++ b/cpp/benchmarks/string/convert_datetime.cpp @@ -34,8 +34,8 @@ void BM_convert_datetime(benchmark::State& state, direction dir) auto const n_rows = static_cast(state.range(0)); auto const data_type = cudf::data_type(cudf::type_to_id()); - auto const table = create_random_table({data_type.id()}, row_count{n_rows}); - cudf::column_view input(table->view().column(0)); + auto const column = create_random_column(data_type.id(), row_count{n_rows}); + cudf::column_view input(column->view()); auto source = dir == direction::to ? cudf::strings::from_timestamps(input, "%Y-%m-%d %H:%M:%S") : make_empty_column(cudf::data_type{cudf::type_id::STRING}); diff --git a/cpp/benchmarks/string/convert_fixed_point.cpp b/cpp/benchmarks/string/convert_fixed_point.cpp index 88657c409cd..7e91edbfcc4 100644 --- a/cpp/benchmarks/string/convert_fixed_point.cpp +++ b/cpp/benchmarks/string/convert_fixed_point.cpp @@ -26,9 +26,9 @@ namespace { std::unique_ptr get_strings_column(cudf::size_type rows) { - std::unique_ptr result = - create_random_table({cudf::type_id::FLOAT32}, row_count{static_cast(rows)}); - return cudf::strings::from_floats(result->release().front()->view()); + auto result = + create_random_column(cudf::type_id::FLOAT32, row_count{static_cast(rows)}); + return cudf::strings::from_floats(result->view()); } } // anonymous namespace diff --git a/cpp/benchmarks/string/convert_numerics.cpp b/cpp/benchmarks/string/convert_numerics.cpp index 3025c32b888..466117918d9 100644 --- a/cpp/benchmarks/string/convert_numerics.cpp +++ b/cpp/benchmarks/string/convert_numerics.cpp @@ -27,9 +27,7 @@ namespace { template std::unique_ptr get_numerics_column(cudf::size_type rows) { - std::unique_ptr result = - create_random_table({cudf::type_to_id()}, row_count{rows}); - return std::move(result->release().front()); + return create_random_column(cudf::type_to_id(), row_count{rows}); } template diff --git a/cpp/benchmarks/string/extract.cpp b/cpp/benchmarks/string/extract.cpp index 32f21d71030..4e9ac2f5395 100644 --- a/cpp/benchmarks/string/extract.cpp +++ b/cpp/benchmarks/string/extract.cpp @@ -55,19 +55,17 @@ static void BM_extract(benchmark::State& state, int groups) cudf::test::strings_column_wrapper samples_column(samples.begin(), samples.end()); data_profile const profile = data_profile_builder().no_validity().distribution( cudf::type_to_id(), distribution_id::UNIFORM, 0ul, samples.size() - 1); - auto map_table = - create_random_table({cudf::type_to_id()}, row_count{n_rows}, profile); - auto input = cudf::gather(cudf::table_view{{samples_column}}, - map_table->get_column(0).view(), - cudf::out_of_bounds_policy::DONT_CHECK); - cudf::strings_column_view view(input->get_column(0).view()); + auto map = create_random_column(cudf::type_to_id(), row_count{n_rows}, profile); + auto input = cudf::gather( + cudf::table_view{{samples_column}}, map->view(), cudf::out_of_bounds_policy::DONT_CHECK); + cudf::strings_column_view strings_view(input->get_column(0).view()); for (auto _ : state) { cuda_event_timer raii(state, true); - auto results = cudf::strings::extract(view, pattern); + auto results = cudf::strings::extract(strings_view, pattern); } - state.SetBytesProcessed(state.iterations() * view.chars_size()); + state.SetBytesProcessed(state.iterations() * strings_view.chars_size()); } static void generate_bench_args(benchmark::internal::Benchmark* b) diff --git a/cpp/benchmarks/string/factory.cu b/cpp/benchmarks/string/factory.cu index 52af92c033f..0e937b91e98 100644 --- a/cpp/benchmarks/string/factory.cu +++ b/cpp/benchmarks/string/factory.cu @@ -51,10 +51,10 @@ static void BM_factory(benchmark::State& state) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile const table_profile = data_profile_builder().distribution( + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - auto d_column = cudf::column_device_view::create(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + auto d_column = cudf::column_device_view::create(column->view()); rmm::device_uvector pairs(d_column->size(), cudf::default_stream_value); thrust::transform(thrust::device, d_column->pair_begin(), @@ -67,7 +67,7 @@ static void BM_factory(benchmark::State& state) cudf::make_strings_column(pairs); } - cudf::strings_column_view input(table->view().column(0)); + cudf::strings_column_view input(column->view()); state.SetBytesProcessed(state.iterations() * input.chars_size()); } diff --git a/cpp/benchmarks/string/filter.cpp b/cpp/benchmarks/string/filter.cpp index 137c4126ddb..4001fef5da6 100644 --- a/cpp/benchmarks/string/filter.cpp +++ b/cpp/benchmarks/string/filter.cpp @@ -39,10 +39,10 @@ static void BM_filter_chars(benchmark::State& state, FilterAPI api) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile const table_profile = data_profile_builder().distribution( + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); auto const types = cudf::strings::string_character_types::SPACE; std::vector> filter_table{ diff --git a/cpp/benchmarks/string/find.cpp b/cpp/benchmarks/string/find.cpp index f049de9a65d..62c76d18e1a 100644 --- a/cpp/benchmarks/string/find.cpp +++ b/cpp/benchmarks/string/find.cpp @@ -37,10 +37,10 @@ static void BM_find_scalar(benchmark::State& state, FindAPI find_api) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile const table_profile = data_profile_builder().distribution( + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); cudf::string_scalar target("+"); cudf::test::strings_column_wrapper targets({"+", "-"}); diff --git a/cpp/benchmarks/string/replace.cpp b/cpp/benchmarks/string/replace.cpp index d7a079201c0..e25bf679dbc 100644 --- a/cpp/benchmarks/string/replace.cpp +++ b/cpp/benchmarks/string/replace.cpp @@ -38,10 +38,10 @@ static void BM_replace(benchmark::State& state, replace_type rt) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile const table_profile = data_profile_builder().distribution( + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); cudf::string_scalar target("+"); cudf::string_scalar repl(""); cudf::test::strings_column_wrapper targets({"+", "-"}); diff --git a/cpp/benchmarks/string/replace_re.cpp b/cpp/benchmarks/string/replace_re.cpp index 17b6f54f7bb..f8b03daa338 100644 --- a/cpp/benchmarks/string/replace_re.cpp +++ b/cpp/benchmarks/string/replace_re.cpp @@ -35,10 +35,10 @@ static void BM_replace(benchmark::State& state, replace_type rt) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile const table_profile = data_profile_builder().distribution( + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); cudf::test::strings_column_wrapper repls({"#", ""}); for (auto _ : state) { diff --git a/cpp/benchmarks/string/split.cpp b/cpp/benchmarks/string/split.cpp index e26a853f22a..3a7a96b025d 100644 --- a/cpp/benchmarks/string/split.cpp +++ b/cpp/benchmarks/string/split.cpp @@ -36,10 +36,10 @@ static void BM_split(benchmark::State& state, split_type rt) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile const table_profile = data_profile_builder().distribution( + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); cudf::string_scalar target("+"); for (auto _ : state) { diff --git a/cpp/benchmarks/string/substring.cpp b/cpp/benchmarks/string/substring.cpp index fce11aac251..7ae5ad6f581 100644 --- a/cpp/benchmarks/string/substring.cpp +++ b/cpp/benchmarks/string/substring.cpp @@ -40,10 +40,10 @@ static void BM_substring(benchmark::State& state, substring_type rt) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile const table_profile = data_profile_builder().distribution( + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); auto starts_itr = thrust::constant_iterator(1); auto stops_itr = thrust::constant_iterator(max_str_length / 2); cudf::test::fixed_width_column_wrapper starts(starts_itr, starts_itr + n_rows); diff --git a/cpp/benchmarks/string/translate.cpp b/cpp/benchmarks/string/translate.cpp index 74e9b55ad84..359a3756ef2 100644 --- a/cpp/benchmarks/string/translate.cpp +++ b/cpp/benchmarks/string/translate.cpp @@ -39,10 +39,10 @@ static void BM_translate(benchmark::State& state, int entry_count) { cudf::size_type const n_rows{static_cast(state.range(0))}; cudf::size_type const max_str_length{static_cast(state.range(1))}; - data_profile const table_profile = data_profile_builder().distribution( + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); std::vector entries(entry_count); std::transform(thrust::counting_iterator(0), diff --git a/cpp/benchmarks/text/ngrams.cpp b/cpp/benchmarks/text/ngrams.cpp index 38c597ece19..5556e71c31b 100644 --- a/cpp/benchmarks/text/ngrams.cpp +++ b/cpp/benchmarks/text/ngrams.cpp @@ -31,12 +31,12 @@ enum class ngrams_type { tokens, characters }; static void BM_ngrams(benchmark::State& state, ngrams_type nt) { - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); - data_profile const table_profile = data_profile_builder().distribution( + auto const n_rows = static_cast(state.range(0)); + auto const max_str_length = static_cast(state.range(1)); + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); for (auto _ : state) { cuda_event_timer raii(state, true); diff --git a/cpp/benchmarks/text/normalize.cpp b/cpp/benchmarks/text/normalize.cpp index 3925d8bebc2..e5a0a1a95f4 100644 --- a/cpp/benchmarks/text/normalize.cpp +++ b/cpp/benchmarks/text/normalize.cpp @@ -29,12 +29,12 @@ class TextNormalize : public cudf::benchmark { static void BM_normalize(benchmark::State& state, bool to_lower) { - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); - data_profile const table_profile = data_profile_builder().distribution( + auto const n_rows = static_cast(state.range(0)); + auto const max_str_length = static_cast(state.range(1)); + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); for (auto _ : state) { cuda_event_timer raii(state, true, cudf::default_stream_value); diff --git a/cpp/benchmarks/text/normalize_spaces.cpp b/cpp/benchmarks/text/normalize_spaces.cpp index 2f5d6a4bcb1..414cd119575 100644 --- a/cpp/benchmarks/text/normalize_spaces.cpp +++ b/cpp/benchmarks/text/normalize_spaces.cpp @@ -30,12 +30,12 @@ class TextNormalize : public cudf::benchmark { static void BM_normalize(benchmark::State& state) { - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); - data_profile const table_profile = data_profile_builder().distribution( + auto const n_rows = static_cast(state.range(0)); + auto const max_str_length = static_cast(state.range(1)); + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); for (auto _ : state) { cuda_event_timer raii(state, true, cudf::default_stream_value); diff --git a/cpp/benchmarks/text/tokenize.cpp b/cpp/benchmarks/text/tokenize.cpp index 01a971812a2..4d8df6ae37c 100644 --- a/cpp/benchmarks/text/tokenize.cpp +++ b/cpp/benchmarks/text/tokenize.cpp @@ -35,12 +35,12 @@ enum class tokenize_type { single, multi, count, count_multi, ngrams, characters static void BM_tokenize(benchmark::State& state, tokenize_type tt) { - auto const n_rows = static_cast(state.range(0)); - auto const max_str_length = static_cast(state.range(1)); - data_profile const table_profile = data_profile_builder().distribution( + auto const n_rows = static_cast(state.range(0)); + auto const max_str_length = static_cast(state.range(1)); + data_profile const profile = data_profile_builder().distribution( cudf::type_id::STRING, distribution_id::NORMAL, 0, max_str_length); - auto const table = create_random_table({cudf::type_id::STRING}, row_count{n_rows}, table_profile); - cudf::strings_column_view input(table->view().column(0)); + auto const column = create_random_column(cudf::type_id::STRING, row_count{n_rows}, profile); + cudf::strings_column_view input(column->view()); cudf::test::strings_column_wrapper delimiters({" ", "+", "-"}); for (auto _ : state) { diff --git a/cpp/docs/BENCHMARKING.md b/cpp/docs/BENCHMARKING.md index 270e7a87e85..98e2f428cc3 100644 --- a/cpp/docs/BENCHMARKING.md +++ b/cpp/docs/BENCHMARKING.md @@ -39,7 +39,8 @@ performance in repeated iterations. For generating benchmark input data, helper functions are available at [cpp/benchmarks/common/generate_input.hpp](/cpp/benchmarks/common/generate_input.hpp). The input data generation happens on device, in contrast to any `column_wrapper` where data generation happens on the host. * `create_sequence_table` can generate sequence columns starting with value 0 in first row and increasing by 1 in subsequent rows. -* `create_random_table` can generate a table filled with random data. The random data parameters are configurable. +* `create_random_column` can generate a column filled with random data. The random data parameters are configurable. +* `create_random_table` can generate a table of columns filled with random data. The random data parameters are configurable. ## What should we benchmark? From 4178a514c4688612dfb9556f80053f2791e4f350 Mon Sep 17 00:00:00 2001 From: Mike Wilson Date: Tue, 16 Aug 2022 14:57:51 -0400 Subject: [PATCH 39/58] Adding optional parquet reader schema (#11524) Adding a schema for reading parquet files. This is useful for things like binary data reading where the default behavior of cudf is to read it as a string column, but users wish to read it as a list column instead. Using a schema allows for nested data types to be expressed completely. Authors: - Mike Wilson (https://github.com/hyperbolic2346) Approvers: - MithunR (https://github.com/mythrocks) - Vukasin Milovanovic (https://github.com/vuule) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/11524 --- cpp/include/cudf/io/parquet.hpp | 32 +- cpp/include/cudf/io/types.hpp | 91 +++++ cpp/src/io/avro/reader_impl.cu | 2 +- cpp/src/io/csv/reader_impl.cu | 2 +- cpp/src/io/json/reader_impl.cu | 2 +- cpp/src/io/orc/reader_impl.cu | 2 +- cpp/src/io/parquet/reader_impl.cu | 27 +- cpp/src/io/parquet/reader_impl.hpp | 16 +- cpp/src/io/utilities/column_buffer.cpp | 67 ++- cpp/src/io/utilities/column_buffer.hpp | 1 + cpp/tests/io/parquet_test.cpp | 386 ++++++++---------- java/src/main/native/src/TableJni.cpp | 3 +- .../test/java/ai/rapids/cudf/TableTest.java | 4 +- 13 files changed, 343 insertions(+), 292 deletions(-) diff --git a/cpp/include/cudf/io/parquet.hpp b/cpp/include/cudf/io/parquet.hpp index 0673f73ef89..8ed90f0ab31 100644 --- a/cpp/include/cudf/io/parquet.hpp +++ b/cpp/include/cudf/io/parquet.hpp @@ -63,8 +63,8 @@ class parquet_reader_options { bool _use_pandas_metadata = true; // Cast timestamp columns to a specific type data_type _timestamp_type{type_id::EMPTY}; - // Whether to store binary data as a string column - std::optional> _convert_binary_to_strings{std::nullopt}; + + std::optional> _reader_column_schema; /** * @brief Constructor from source info. @@ -117,16 +117,13 @@ class parquet_reader_options { [[nodiscard]] bool is_enabled_use_pandas_metadata() const { return _use_pandas_metadata; } /** - * @brief Returns optional vector of true/false values depending on whether binary data should be - * converted to strings or not. + * @brief Returns optional tree of metadata. * - * @return vector with ith value `true` if binary data should be converted to strings for the ith - * column. Will return std::nullopt if the user did not set this option, which defaults to all - * binary data being converted to strings. + * @return vector of reader_column_schema objects. */ - [[nodiscard]] std::optional> get_convert_binary_to_strings() const + [[nodiscard]] std::optional> get_column_schema() const { - return _convert_binary_to_strings; + return _reader_column_schema; } /** @@ -182,14 +179,14 @@ class parquet_reader_options { void enable_use_pandas_metadata(bool val) { _use_pandas_metadata = val; } /** - * @brief Sets to enable/disable conversion of binary to strings per column. + * @brief Sets reader column schema. * - * @param val Vector of boolean values to enable/disable conversion of binary to string columns. + * @param val Tree of schema nodes to enable/disable conversion of binary to string columns. * Note default is to convert to string columns. */ - void set_convert_binary_to_strings(std::vector val) + void set_column_schema(std::vector val) { - _convert_binary_to_strings = std::move(val); + _reader_column_schema = std::move(val); } /** @@ -270,15 +267,14 @@ class parquet_reader_options_builder { } /** - * @brief Sets enable/disable conversion of binary to strings per column. + * @brief Sets reader metadata. * - * @param val Vector of boolean values to enable/disable conversion of binary to string columns. - * Note default is to convert to string columns. + * @param val Tree of metadata information. * @return this for chaining */ - parquet_reader_options_builder& convert_binary_to_strings(std::vector val) + parquet_reader_options_builder& set_column_schema(std::vector val) { - options._convert_binary_to_strings = std::move(val); + options._reader_column_schema = std::move(val); return *this; } diff --git a/cpp/include/cudf/io/types.hpp b/cpp/include/cudf/io/types.hpp index c31176ab51c..e9cc7ea99f7 100644 --- a/cpp/include/cudf/io/types.hpp +++ b/cpp/include/cudf/io/types.hpp @@ -22,6 +22,7 @@ #pragma once #include +#include #include #include @@ -645,5 +646,95 @@ struct partition_info { } }; +/** + * @brief schema element for reader + * + */ +class reader_column_schema { + // Whether to read binary data as a string column + bool _convert_binary_to_strings{true}; + + std::vector children; + + public: + reader_column_schema() = default; + + /** + * @brief Construct a new reader column schema object + * + * @param number_of_children number of child schema objects to default construct + */ + reader_column_schema(size_type number_of_children) { children.resize(number_of_children); } + + /** + * @brief Construct a new reader column schema object with a span defining the children + * + * @param child_span span of child schema objects + */ + reader_column_schema(host_span const& child_span) + { + children.assign(child_span.begin(), child_span.end()); + } + + /** + * @brief Add the children metadata of this column + * + * @param child The children metadata of this column to add + * @return this for chaining + */ + reader_column_schema& add_child(reader_column_schema const& child) + { + children.push_back(child); + return *this; + } + + /** + * @brief Get reference to a child of this column + * + * @param i Index of the child to get + * @return this for chaining + */ + [[nodiscard]] reader_column_schema& child(size_type i) { return children[i]; } + + /** + * @brief Get const reference to a child of this column + * + * @param i Index of the child to get + * @return this for chaining + */ + [[nodiscard]] reader_column_schema const& child(size_type i) const { return children[i]; } + + /** + * @brief Specifies whether this column should be written as binary or string data + * Only valid for the following column types: + * string, list + * + * @param convert_to_string True = convert binary to strings False = return binary + * @return this for chaining + */ + reader_column_schema& set_convert_binary_to_strings(bool convert_to_string) + { + _convert_binary_to_strings = convert_to_string; + return *this; + } + + /** + * @brief Get whether to encode this column as binary or string data + * + * @return Boolean indicating whether to encode this column as binary data + */ + [[nodiscard]] bool is_enabled_convert_binary_to_strings() const + { + return _convert_binary_to_strings; + } + + /** + * @brief Get the number of child objects + * + * @return number of children + */ + [[nodiscard]] size_t get_num_children() const { return children.size(); } +}; + } // namespace io } // namespace cudf diff --git a/cpp/src/io/avro/reader_impl.cu b/cpp/src/io/avro/reader_impl.cu index f96a6daa376..e5b73dc9360 100644 --- a/cpp/src/io/avro/reader_impl.cu +++ b/cpp/src/io/avro/reader_impl.cu @@ -558,7 +558,7 @@ table_with_metadata read_avro(std::unique_ptr&& source, mr); for (size_t i = 0; i < column_types.size(); ++i) { - out_columns.emplace_back(make_column(out_buffers[i], nullptr, stream, mr)); + out_columns.emplace_back(make_column(out_buffers[i], nullptr, std::nullopt, stream, mr)); } } else { // Create empty columns diff --git a/cpp/src/io/csv/reader_impl.cu b/cpp/src/io/csv/reader_impl.cu index ba1237696f5..d669dea3115 100644 --- a/cpp/src/io/csv/reader_impl.cu +++ b/cpp/src/io/csv/reader_impl.cu @@ -846,7 +846,7 @@ table_with_metadata read_csv(cudf::io::datasource* source, out_columns.emplace_back( cudf::strings::replace(col->view(), dblquotechar, quotechar, -1, mr)); } else { - out_columns.emplace_back(make_column(out_buffers[i], nullptr, stream, mr)); + out_columns.emplace_back(make_column(out_buffers[i], nullptr, std::nullopt, stream, mr)); } } } else { diff --git a/cpp/src/io/json/reader_impl.cu b/cpp/src/io/json/reader_impl.cu index 6b12b462dd9..8e10dc2c9b4 100644 --- a/cpp/src/io/json/reader_impl.cu +++ b/cpp/src/io/json/reader_impl.cu @@ -540,7 +540,7 @@ table_with_metadata convert_data_to_table(parse_options_view const& parse_opts, for (size_t i = 0; i < num_columns; ++i) { out_buffers[i].null_count() = num_records - h_valid_counts[i]; - auto out_column = make_column(out_buffers[i], nullptr, stream, mr); + auto out_column = make_column(out_buffers[i], nullptr, std::nullopt, stream, mr); if (out_column->type().id() == type_id::STRING) { // Need to remove escape character in case of '\"' and '\\' out_columns.emplace_back(cudf::strings::detail::replace( diff --git a/cpp/src/io/orc/reader_impl.cu b/cpp/src/io/orc/reader_impl.cu index 4da9c224ab6..5df9b0dad7a 100644 --- a/cpp/src/io/orc/reader_impl.cu +++ b/cpp/src/io/orc/reader_impl.cu @@ -879,7 +879,7 @@ void reader::impl::create_columns(std::vector>&& col_ [&](auto const col_meta) { schema_info.emplace_back(""); auto col_buffer = assemble_buffer(col_meta.id, col_buffers, 0, stream); - return make_column(col_buffer, &schema_info.back(), stream, _mr); + return make_column(col_buffer, &schema_info.back(), std::nullopt, stream, _mr); }); } diff --git a/cpp/src/io/parquet/reader_impl.cu b/cpp/src/io/parquet/reader_impl.cu index 4ee6a88d94a..d926bd10807 100644 --- a/cpp/src/io/parquet/reader_impl.cu +++ b/cpp/src/io/parquet/reader_impl.cu @@ -1577,7 +1577,7 @@ reader::impl::impl(std::vector>&& sources, _strings_to_categorical = options.is_enabled_convert_strings_to_categories(); // Binary columns can be read as binary or strings - _force_binary_columns_as_strings = options.get_convert_binary_to_strings(); + _reader_column_schema = options.get_column_schema(); // Select only columns required by the options std::tie(_input_columns, _output_columns, _output_column_schemas) = @@ -1744,28 +1744,15 @@ table_with_metadata reader::impl::read(std::vector> const // decoding of column data itself decode_page_data(chunks, pages, page_nesting_info, num_rows); - auto make_output_column = [&](column_buffer& buf, column_name_info* schema_info, int i) { - auto col = make_column(buf, schema_info, _stream, _mr); - if (should_write_byte_array(i)) { - auto const& schema = _metadata->get_schema(_output_column_schemas[i]); - if (schema.converted_type == parquet::UNKNOWN) { - auto const num_rows = col->size(); - auto data = col->release(); - return make_lists_column( - num_rows, - std::move(data.children[strings_column_view::offsets_column_index]), - std::move(data.children[strings_column_view::chars_column_index]), - UNKNOWN_NULL_COUNT, - std::move(*data.null_mask)); - } - } - return col; - }; - // create the final output cudf columns for (size_t i = 0; i < _output_columns.size(); ++i) { column_name_info& col_name = out_metadata.schema_info.emplace_back(""); - out_columns.emplace_back(make_output_column(_output_columns[i], &col_name, i)); + auto const metadata = + _reader_column_schema.has_value() + ? std::make_optional((*_reader_column_schema)[i]) + : std::nullopt; + out_columns.emplace_back( + make_column(_output_columns[i], &col_name, metadata, _stream, _mr)); } } } diff --git a/cpp/src/io/parquet/reader_impl.hpp b/cpp/src/io/parquet/reader_impl.hpp index 99c1a231f62..b46fe042a13 100644 --- a/cpp/src/io/parquet/reader_impl.hpp +++ b/cpp/src/io/parquet/reader_impl.hpp @@ -174,20 +174,6 @@ class reader::impl { hostdevice_vector& page_nesting, size_t total_rows); - /** - * @brief Indicates if a column should be written as a byte array - * - * @param col column to check - * @return true if the column should be written as a byte array - * @return false if the column should be written as normal for that type - */ - bool should_write_byte_array(int col) - { - return _output_columns[col].type.id() == type_id::STRING && - _force_binary_columns_as_strings.has_value() && - !_force_binary_columns_as_strings.value()[col]; - } - private: rmm::cuda_stream_view _stream; rmm::mr::device_memory_resource* _mr = nullptr; @@ -203,7 +189,7 @@ class reader::impl { std::vector _output_column_schemas; bool _strings_to_categorical = false; - std::optional> _force_binary_columns_as_strings; + std::optional> _reader_column_schema; data_type _timestamp_type{type_id::EMPTY}; }; diff --git a/cpp/src/io/utilities/column_buffer.cpp b/cpp/src/io/utilities/column_buffer.cpp index d328a831708..e2d209a7c0a 100644 --- a/cpp/src/io/utilities/column_buffer.cpp +++ b/cpp/src/io/utilities/column_buffer.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, NVIDIA CORPORATION. + * Copyright (c) 2021-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ #include "column_buffer.hpp" #include +#include namespace cudf { namespace io { @@ -58,6 +59,7 @@ void column_buffer::create(size_type _size, */ std::unique_ptr make_column(column_buffer& buffer, column_name_info* schema_info, + std::optional const& schema, rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr) { @@ -65,11 +67,31 @@ std::unique_ptr make_column(column_buffer& buffer, switch (buffer.type.id()) { case type_id::STRING: - if (schema_info != nullptr) { - schema_info->children.push_back(column_name_info{"offsets"}); - schema_info->children.push_back(column_name_info{"chars"}); + if (schema.value_or(reader_column_schema{}).is_enabled_convert_binary_to_strings()) { + if (schema_info != nullptr) { + schema_info->children.push_back(column_name_info{"offsets"}); + schema_info->children.push_back(column_name_info{"chars"}); + } + + return make_strings_column(*buffer._strings, stream, mr); + } else { + // convert to binary + auto const string_col = make_strings_column(*buffer._strings, stream, mr); + auto const num_rows = string_col->size(); + auto col_contest = string_col->release(); + + if (schema_info != nullptr) { + schema_info->children.push_back(column_name_info{"offsets"}); + schema_info->children.push_back(column_name_info{"binary"}); + } + + return make_lists_column( + num_rows, + std::move(col_contest.children[strings_column_view::offsets_column_index]), + std::move(col_contest.children[strings_column_view::chars_column_index]), + UNKNOWN_NULL_COUNT, + std::move(*col_contest.null_mask)); } - return make_strings_column(*buffer._strings, stream, mr); case type_id::LIST: { // make offsets column @@ -83,9 +105,15 @@ std::unique_ptr make_column(column_buffer& buffer, child_info = &schema_info->children.back(); } + CUDF_EXPECTS(not schema.has_value() or schema->get_num_children() > 0, + "Invalid schema provided for read, expected child data for list!"); + auto const child_schema = schema.has_value() + ? std::make_optional(schema->child(0)) + : std::nullopt; + // make child column CUDF_EXPECTS(buffer.children.size() > 0, "Encountered malformed column_buffer"); - auto child = make_column(buffer.children[0], child_info, stream, mr); + auto child = make_column(buffer.children[0], child_info, child_schema, stream, mr); // make the final list column (note : size is the # of offsets, so our actual # of rows is 1 // less) @@ -101,17 +129,22 @@ std::unique_ptr make_column(column_buffer& buffer, case type_id::STRUCT: { std::vector> output_children; output_children.reserve(buffer.children.size()); - std::transform(buffer.children.begin(), - buffer.children.end(), - std::back_inserter(output_children), - [&](column_buffer& col) { - column_name_info* child_info = nullptr; - if (schema_info != nullptr) { - schema_info->children.push_back(column_name_info{""}); - child_info = &schema_info->children.back(); - } - return make_column(col, child_info, stream, mr); - }); + for (size_t i = 0; i < buffer.children.size(); ++i) { + column_name_info* child_info = nullptr; + if (schema_info != nullptr) { + schema_info->children.push_back(column_name_info{""}); + child_info = &schema_info->children.back(); + } + + CUDF_EXPECTS(not schema.has_value() or schema->get_num_children() > i, + "Invalid schema provided for read, expected more child data for struct!"); + auto const child_schema = schema.has_value() + ? std::make_optional(schema->child(i)) + : std::nullopt; + + output_children.emplace_back( + make_column(buffer.children[i], child_info, child_schema, stream, mr)); + } return make_structs_column(buffer.size, std::move(output_children), diff --git a/cpp/src/io/utilities/column_buffer.hpp b/cpp/src/io/utilities/column_buffer.hpp index fd510466477..8ae3d39a3ba 100644 --- a/cpp/src/io/utilities/column_buffer.hpp +++ b/cpp/src/io/utilities/column_buffer.hpp @@ -135,6 +135,7 @@ struct column_buffer { */ std::unique_ptr make_column(column_buffer& buffer, column_name_info* schema_info, + std::optional const& schema, rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr); diff --git a/cpp/tests/io/parquet_test.cpp b/cpp/tests/io/parquet_test.cpp index 774c58f1ecf..77a03cd5502 100644 --- a/cpp/tests/io/parquet_test.cpp +++ b/cpp/tests/io/parquet_test.cpp @@ -396,21 +396,19 @@ TYPED_TEST(ParquetWriterNumericTypeTest, SingleColumn) constexpr auto num_rows = 800; column_wrapper col(sequence, sequence + num_rows, validity); - std::vector> cols; - cols.push_back(col.release()); - auto expected = std::make_unique
(std::move(cols)); - EXPECT_EQ(1, expected->num_columns()); + auto expected = table_view{{col}}; + EXPECT_EQ(1, expected.num_columns()); auto filepath = temp_env->get_temp_filepath("SingleColumn.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected->view()); + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected); cudf_io::write_parquet(out_opts); cudf_io::parquet_reader_options in_opts = cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}); auto result = cudf_io::read_parquet(in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); } TYPED_TEST(ParquetWriterNumericTypeTest, SingleColumnWithNulls) @@ -422,21 +420,19 @@ TYPED_TEST(ParquetWriterNumericTypeTest, SingleColumnWithNulls) constexpr auto num_rows = 100; column_wrapper col(sequence, sequence + num_rows, validity); - std::vector> cols; - cols.push_back(col.release()); - auto expected = std::make_unique
(std::move(cols)); - EXPECT_EQ(1, expected->num_columns()); + auto expected = table_view{{col}}; + EXPECT_EQ(1, expected.num_columns()); auto filepath = temp_env->get_temp_filepath("SingleColumnWithNulls.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected->view()); + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected); cudf_io::write_parquet(out_opts); cudf_io::parquet_reader_options in_opts = cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}); auto result = cudf_io::read_parquet(in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); } TYPED_TEST(ParquetWriterChronoTypeTest, Chronos) @@ -449,14 +445,12 @@ TYPED_TEST(ParquetWriterChronoTypeTest, Chronos) column_wrapper col( sequence, sequence + num_rows, validity); - std::vector> cols; - cols.push_back(col.release()); - auto expected = std::make_unique
(std::move(cols)); - EXPECT_EQ(1, expected->num_columns()); + auto expected = table_view{{col}}; + EXPECT_EQ(1, expected.num_columns()); auto filepath = temp_env->get_temp_filepath("Chronos.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected->view()); + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected); cudf_io::write_parquet(out_opts); cudf_io::parquet_reader_options in_opts = @@ -464,7 +458,7 @@ TYPED_TEST(ParquetWriterChronoTypeTest, Chronos) .timestamp_type(this->type()); auto result = cudf_io::read_parquet(in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); } TYPED_TEST(ParquetWriterChronoTypeTest, ChronosWithNulls) @@ -478,14 +472,12 @@ TYPED_TEST(ParquetWriterChronoTypeTest, ChronosWithNulls) column_wrapper col( sequence, sequence + num_rows, validity); - std::vector> cols; - cols.push_back(col.release()); - auto expected = std::make_unique
(std::move(cols)); - EXPECT_EQ(1, expected->num_columns()); + auto expected = table_view{{col}}; + EXPECT_EQ(1, expected.num_columns()); auto filepath = temp_env->get_temp_filepath("ChronosWithNulls.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected->view()); + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected); cudf_io::write_parquet(out_opts); cudf_io::parquet_reader_options in_opts = @@ -493,7 +485,7 @@ TYPED_TEST(ParquetWriterChronoTypeTest, ChronosWithNulls) .timestamp_type(this->type()); auto result = cudf_io::read_parquet(in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); } TYPED_TEST(ParquetWriterTimestampTypeTest, TimestampOverflow) @@ -555,19 +547,9 @@ TEST_F(ParquetWriterTest, MultiColumn) column_wrapper col7{col7_data, col7_data + num_rows, validity}; column_wrapper col8{col8_data, col8_data + num_rows, validity}; - std::vector> cols; - // cols.push_back(col0.release()); - cols.push_back(col1.release()); - cols.push_back(col2.release()); - cols.push_back(col3.release()); - cols.push_back(col4.release()); - cols.push_back(col5.release()); - cols.push_back(col6.release()); - cols.push_back(col7.release()); - cols.push_back(col8.release()); - auto expected = std::make_unique
(std::move(cols)); - - cudf_io::table_input_metadata expected_metadata(*expected); + auto expected = table_view{{col1, col2, col3, col4, col5, col6, col7, col8}}; + + cudf_io::table_input_metadata expected_metadata(expected); // expected_metadata.column_metadata[0].set_name( "bools"); expected_metadata.column_metadata[0].set_name("int8s"); expected_metadata.column_metadata[1].set_name("int16s"); @@ -580,7 +562,7 @@ TEST_F(ParquetWriterTest, MultiColumn) auto filepath = temp_env->get_temp_filepath("MultiColumn.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected->view()) + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected) .metadata(&expected_metadata); cudf_io::write_parquet(out_opts); @@ -588,7 +570,7 @@ TEST_F(ParquetWriterTest, MultiColumn) cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}); auto result = cudf_io::read_parquet(in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); cudf::test::expect_metadata_equal(expected_metadata, result.metadata); } @@ -636,19 +618,10 @@ TEST_F(ParquetWriterTest, MultiColumnWithNulls) column_wrapper col6{col6_data, col6_data + num_rows, col6_mask}; column_wrapper col7{col7_data, col7_data + num_rows, col7_mask}; - std::vector> cols; - // cols.push_back(col0.release()); - cols.push_back(col1.release()); - cols.push_back(col2.release()); - cols.push_back(col3.release()); - cols.push_back(col4.release()); - cols.push_back(col5.release()); - cols.push_back(col6.release()); - cols.push_back(col7.release()); - auto expected = std::make_unique
(std::move(cols)); - EXPECT_EQ(7, expected->num_columns()); - - cudf_io::table_input_metadata expected_metadata(*expected); + auto expected = table_view{{/*col0, */ col1, col2, col3, col4, col5, col6, col7}}; + EXPECT_EQ(7, expected.num_columns()); + + cudf_io::table_input_metadata expected_metadata(expected); // expected_metadata.column_names.emplace_back("bools"); expected_metadata.column_metadata[0].set_name("int8s"); expected_metadata.column_metadata[1].set_name("int16s"); @@ -660,7 +633,7 @@ TEST_F(ParquetWriterTest, MultiColumnWithNulls) auto filepath = temp_env->get_temp_filepath("MultiColumnWithNulls.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected->view()) + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected) .metadata(&expected_metadata); cudf_io::write_parquet(out_opts); @@ -669,7 +642,7 @@ TEST_F(ParquetWriterTest, MultiColumnWithNulls) cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}); auto result = cudf_io::read_parquet(in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); // TODO: Need to be able to return metadata in tree form from reader so they can be compared. // Unfortunately the closest thing to a hierarchical schema is column_name_info which does not // have any tests for it c++ or python. @@ -690,21 +663,17 @@ TEST_F(ParquetWriterTest, Strings) column_wrapper col1{strings.begin(), strings.end()}; column_wrapper col2{seq_col2.begin(), seq_col2.end(), validity}; - std::vector> cols; - cols.push_back(col0.release()); - cols.push_back(col1.release()); - cols.push_back(col2.release()); - auto expected = std::make_unique
(std::move(cols)); - EXPECT_EQ(3, expected->num_columns()); + auto expected = table_view{{col0, col1, col2}}; + EXPECT_EQ(3, expected.num_columns()); - cudf_io::table_input_metadata expected_metadata(*expected); + cudf_io::table_input_metadata expected_metadata(expected); expected_metadata.column_metadata[0].set_name("col_other"); expected_metadata.column_metadata[1].set_name("col_string"); expected_metadata.column_metadata[2].set_name("col_another"); auto filepath = temp_env->get_temp_filepath("Strings.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected->view()) + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected) .metadata(&expected_metadata); cudf_io::write_parquet(out_opts); @@ -712,7 +681,7 @@ TEST_F(ParquetWriterTest, Strings) cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}); auto result = cudf_io::read_parquet(in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); cudf::test::expect_metadata_equal(expected_metadata, result.metadata); } @@ -744,16 +713,10 @@ TEST_F(ParquetWriterTest, StringsAsBinary) {'F', 'r', 'i', 'd', 'a', 'y'}, {'F', 'u', 'n', 'd', 'a', 'y'}}; - std::vector> cols; - cols.push_back(col0.release()); - cols.push_back(col1.release()); - cols.push_back(col2.release()); - cols.push_back(col3.release()); - cols.push_back(col4.release()); - auto write_tbl = std::make_unique
(std::move(cols)); - EXPECT_EQ(5, write_tbl->num_columns()); + auto write_tbl = table_view{{col0, col1, col2, col3, col4}}; + EXPECT_EQ(5, write_tbl.num_columns()); - cudf_io::table_input_metadata expected_metadata(*write_tbl); + cudf_io::table_input_metadata expected_metadata(write_tbl); expected_metadata.column_metadata[0].set_name("col_single").set_output_as_binary(true); expected_metadata.column_metadata[1].set_name("col_string").set_output_as_binary(true); expected_metadata.column_metadata[2].set_name("col_another").set_output_as_binary(true); @@ -762,22 +725,22 @@ TEST_F(ParquetWriterTest, StringsAsBinary) auto filepath = temp_env->get_temp_filepath("BinaryStrings.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, write_tbl->view()) + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, write_tbl) .metadata(&expected_metadata); cudf_io::write_parquet(out_opts); cudf_io::parquet_reader_options in_opts = cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}) - .convert_binary_to_strings({false, false, false, false, false, false, false, false, false}); - auto result = cudf_io::read_parquet(in_opts); + .set_column_schema( + {cudf_io::reader_column_schema().set_convert_binary_to_strings(false), + cudf_io::reader_column_schema().set_convert_binary_to_strings(false), + cudf_io::reader_column_schema().set_convert_binary_to_strings(false), + cudf_io::reader_column_schema().add_child(cudf_io::reader_column_schema()), + cudf_io::reader_column_schema().add_child(cudf_io::reader_column_schema())}); + auto result = cudf_io::read_parquet(in_opts); + auto expected = table_view{{col3, col4, col3, col3, col4}}; - auto original_cols = write_tbl->release(); - original_cols[0] = std::make_unique(original_cols[3]->view()); - original_cols[2] = std::make_unique(original_cols[3]->view()); - original_cols[1] = std::make_unique(original_cols[4]->view()); - auto expected = cudf::table(std::move(original_cols)); - - CUDF_TEST_EXPECT_TABLES_EQUAL(expected.view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); cudf::test::expect_metadata_equal(expected_metadata, result.metadata); } @@ -997,29 +960,23 @@ TEST_F(ParquetWriterTest, MultiIndex) { constexpr auto num_rows = 100; - auto col1_data = random_values(num_rows); - auto col2_data = random_values(num_rows); - auto col3_data = random_values(num_rows); - auto col4_data = random_values(num_rows); - auto col5_data = random_values(num_rows); + auto col0_data = random_values(num_rows); + auto col1_data = random_values(num_rows); + auto col2_data = random_values(num_rows); + auto col3_data = random_values(num_rows); + auto col4_data = random_values(num_rows); auto validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); - column_wrapper col1{col1_data.begin(), col1_data.end(), validity}; - column_wrapper col2{col2_data.begin(), col2_data.end(), validity}; - column_wrapper col3{col3_data.begin(), col3_data.end(), validity}; - column_wrapper col4{col4_data.begin(), col4_data.end(), validity}; - column_wrapper col5{col5_data.begin(), col5_data.end(), validity}; + column_wrapper col0{col0_data.begin(), col0_data.end(), validity}; + column_wrapper col1{col1_data.begin(), col1_data.end(), validity}; + column_wrapper col2{col2_data.begin(), col2_data.end(), validity}; + column_wrapper col3{col3_data.begin(), col3_data.end(), validity}; + column_wrapper col4{col4_data.begin(), col4_data.end(), validity}; - std::vector> cols; - cols.push_back(col1.release()); - cols.push_back(col2.release()); - cols.push_back(col3.release()); - cols.push_back(col4.release()); - cols.push_back(col5.release()); - auto expected = std::make_unique
(std::move(cols)); - EXPECT_EQ(5, expected->num_columns()); + auto expected = table_view{{col0, col1, col2, col3, col4}}; + EXPECT_EQ(5, expected.num_columns()); - cudf_io::table_input_metadata expected_metadata(*expected); + cudf_io::table_input_metadata expected_metadata(expected); expected_metadata.column_metadata[0].set_name("int8s"); expected_metadata.column_metadata[1].set_name("int16s"); expected_metadata.column_metadata[2].set_name("int32s"); @@ -1028,7 +985,7 @@ TEST_F(ParquetWriterTest, MultiIndex) auto filepath = temp_env->get_temp_filepath("MultiIndex.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected->view()) + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected) .metadata(&expected_metadata) .key_value_metadata( {{{"pandas", "\"index_columns\": [\"int8s\", \"int16s\"], \"column1\": [\"int32s\"]"}}}); @@ -1040,7 +997,7 @@ TEST_F(ParquetWriterTest, MultiIndex) .columns({"int32s", "floats", "doubles"}); auto result = cudf_io::read_parquet(in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); cudf::test::expect_metadata_equal(expected_metadata, result.metadata); } @@ -1052,24 +1009,22 @@ TEST_F(ParquetWriterTest, HostBuffer) cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); column_wrapper col{seq_col.begin(), seq_col.end(), validity}; - std::vector> cols; - cols.push_back(col.release()); - const auto expected = std::make_unique
(std::move(cols)); - EXPECT_EQ(1, expected->num_columns()); + const auto expected = table_view{{col}}; + EXPECT_EQ(1, expected.num_columns()); - cudf_io::table_input_metadata expected_metadata(*expected); + cudf_io::table_input_metadata expected_metadata(expected); expected_metadata.column_metadata[0].set_name("col_other"); std::vector out_buffer; cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info(&out_buffer), expected->view()) + cudf_io::parquet_writer_options::builder(cudf_io::sink_info(&out_buffer), expected) .metadata(&expected_metadata); cudf_io::write_parquet(out_opts); cudf_io::parquet_reader_options in_opts = cudf_io::parquet_reader_options::builder( cudf_io::source_info(out_buffer.data(), out_buffer.size())); const auto result = cudf_io::read_parquet(in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); cudf::test::expect_metadata_equal(expected_metadata, result.metadata); } @@ -3303,14 +3258,12 @@ TEST_F(ParquetWriterTest, CheckPageRows) constexpr auto num_rows = 2 * page_rows; column_wrapper col(sequence, sequence + num_rows, validity); - std::vector> cols; - cols.push_back(col.release()); - auto expected = std::make_unique
(std::move(cols)); - EXPECT_EQ(1, expected->num_columns()); + auto expected = table_view{{col}}; + EXPECT_EQ(1, expected.num_columns()); auto const filepath = temp_env->get_temp_filepath("CheckPageRows.parquet"); const cudf::io::parquet_writer_options out_opts = - cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected->view()) + cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected) .max_page_size_rows(page_rows); cudf::io::write_parquet(out_opts); @@ -3346,13 +3299,11 @@ TEST_F(ParquetWriterTest, Decimal128Stats) column_wrapper col0{{numeric::decimal128(val0, numeric::scale_type{0}), numeric::decimal128(val1, numeric::scale_type{0})}}; - std::vector> cols; - cols.push_back(col0.release()); - auto expected = std::make_unique
(std::move(cols)); + auto expected = table_view{{col0}}; auto const filepath = temp_env->get_temp_filepath("Decimal128Stats.parquet"); const cudf::io::parquet_writer_options out_opts = - cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected->view()); + cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected); cudf::io::write_parquet(out_opts); auto const source = cudf::io::datasource::create(filepath); @@ -3575,19 +3526,15 @@ TYPED_TEST(ParquetWriterComparableTypeTest, ThreeColumnSorted) { using T = TypeParam; - auto col1 = testdata::ascending(); - auto col2 = testdata::descending(); - auto col3 = testdata::unordered(); + auto col0 = testdata::ascending(); + auto col1 = testdata::descending(); + auto col2 = testdata::unordered(); - std::vector> cols; - cols.push_back(col1.release()); - cols.push_back(col2.release()); - cols.push_back(col3.release()); - auto const expected = std::make_unique
(std::move(cols)); + auto const expected = table_view{{col0, col1, col2}}; auto const filepath = temp_env->get_temp_filepath("ThreeColumnSorted.parquet"); const cudf::io::parquet_writer_options out_opts = - cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected->view()) + cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected) .max_page_size_rows(page_size_for_ordered_tests) .stats_level(cudf::io::statistics_freq::STATISTICS_COLUMN); cudf::io::write_parquet(out_opts); @@ -3599,7 +3546,7 @@ TYPED_TEST(ParquetWriterComparableTypeTest, ThreeColumnSorted) CUDF_EXPECTS(fmd.row_groups.size() > 0, "No row groups found"); auto const& columns = fmd.row_groups[0].columns; - CUDF_EXPECTS(columns.size() == static_cast(expected->num_columns()), + CUDF_EXPECTS(columns.size() == static_cast(expected.num_columns()), "Invalid number of columns"); // now check that the boundary order for chunk 1 is ascending, @@ -3710,20 +3657,11 @@ TEST_F(ParquetWriterTest, CheckColumnOffsetIndex) }); auto col7 = cudf::test::strings_column_wrapper(str2_elements, str2_elements + num_rows); - std::vector> cols; - cols.push_back(col0.release()); - cols.push_back(col1.release()); - cols.push_back(col2.release()); - cols.push_back(col3.release()); - cols.push_back(col4.release()); - cols.push_back(col5.release()); - cols.push_back(col6.release()); - cols.push_back(col7.release()); - auto const expected = std::make_unique
(std::move(cols)); + auto const expected = table_view{{col0, col1, col2, col3, col4, col5, col6, col7}}; auto const filepath = temp_env->get_temp_filepath("CheckColumnOffsetIndex.parquet"); const cudf::io::parquet_writer_options out_opts = - cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected->view()) + cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected) .stats_level(cudf::io::statistics_freq::STATISTICS_COLUMN) .max_page_size_rows(20000); cudf::io::write_parquet(out_opts); @@ -3816,20 +3754,11 @@ TEST_F(ParquetWriterTest, CheckColumnOffsetIndexNulls) }); auto col7 = cudf::test::strings_column_wrapper(str2_elements, str2_elements + num_rows, valids); - std::vector> cols; - cols.push_back(col0.release()); - cols.push_back(col1.release()); - cols.push_back(col2.release()); - cols.push_back(col3.release()); - cols.push_back(col4.release()); - cols.push_back(col5.release()); - cols.push_back(col6.release()); - cols.push_back(col7.release()); - auto expected = std::make_unique
(std::move(cols)); + auto expected = table_view{{col0, col1, col2, col3, col4, col5, col6, col7}}; auto const filepath = temp_env->get_temp_filepath("CheckColumnOffsetIndexNulls.parquet"); const cudf::io::parquet_writer_options out_opts = - cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected->view()) + cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected) .stats_level(cudf::io::statistics_freq::STATISTICS_COLUMN) .max_page_size_rows(20000); cudf::io::write_parquet(out_opts); @@ -3910,16 +3839,11 @@ TEST_F(ParquetWriterTest, CheckColumnOffsetIndexNullColumn) }); auto col3 = cudf::test::strings_column_wrapper(str2_elements, str2_elements + num_rows); - std::vector> cols; - cols.push_back(col0.release()); - cols.push_back(col1.release()); - cols.push_back(col2.release()); - cols.push_back(col3.release()); - auto expected = std::make_unique
(std::move(cols)); + auto expected = table_view{{col0, col1, col2, col3}}; auto const filepath = temp_env->get_temp_filepath("CheckColumnOffsetIndexNullColumn.parquet"); const cudf::io::parquet_writer_options out_opts = - cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected->view()) + cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected) .stats_level(cudf::io::statistics_freq::STATISTICS_COLUMN) .max_page_size_rows(20000); cudf::io::write_parquet(out_opts); @@ -4097,63 +4021,52 @@ TEST_F(ParquetReaderTest, BinaryAsStrings) {'F', 'r', 'i', 'd', 'a', 'y'}, {'F', 'u', 'n', 'd', 'a', 'y'}}; - std::vector> cols; - cols.push_back(std::make_unique(static_cast(int_col))); - cols.push_back(std::make_unique(static_cast(string_col))); - cols.push_back(std::make_unique(static_cast(float_col))); - cols.push_back(std::make_unique(static_cast(string_col))); - cols.push_back(std::make_unique(static_cast(list_int_col))); - auto ouput = std::make_unique
(std::move(cols)); - EXPECT_EQ(5, ouput->num_columns()); - cudf_io::table_input_metadata ouput_metadata(*ouput); - ouput_metadata.column_metadata[0].set_name("col_other"); - ouput_metadata.column_metadata[1].set_name("col_string"); - ouput_metadata.column_metadata[2].set_name("col_float"); - ouput_metadata.column_metadata[3].set_name("col_string2").set_output_as_binary(true); - ouput_metadata.column_metadata[4].set_name("col_binary").set_output_as_binary(true); + auto output = table_view{{int_col, string_col, float_col, string_col, list_int_col}}; + EXPECT_EQ(5, output.num_columns()); + cudf_io::table_input_metadata output_metadata(output); + output_metadata.column_metadata[0].set_name("col_other"); + output_metadata.column_metadata[1].set_name("col_string"); + output_metadata.column_metadata[2].set_name("col_float"); + output_metadata.column_metadata[3].set_name("col_string2").set_output_as_binary(true); + output_metadata.column_metadata[4].set_name("col_binary").set_output_as_binary(true); auto filepath = temp_env->get_temp_filepath("BinaryReadStrings.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, ouput->view()) - .metadata(&ouput_metadata); + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, output) + .metadata(&output_metadata); cudf_io::write_parquet(out_opts); - cols.clear(); - cols.push_back(std::make_unique(static_cast(int_col))); - cols.push_back(std::make_unique(static_cast(string_col))); - cols.push_back(std::make_unique(static_cast(float_col))); - cols.push_back(std::make_unique(static_cast(string_col))); - cols.push_back(std::make_unique(static_cast(string_col))); - auto expected_string = std::make_unique
(std::move(cols)); - EXPECT_EQ(5, expected_string->num_columns()); - cols.clear(); - cols.push_back(std::make_unique(static_cast(int_col))); - cols.push_back(std::make_unique(static_cast(string_col))); - cols.push_back(std::make_unique(static_cast(float_col))); - cols.push_back(std::make_unique(static_cast(list_int_col))); - cols.push_back(std::make_unique(static_cast(list_int_col))); - auto expected_mixed = std::make_unique
(std::move(cols)); - EXPECT_EQ(5, expected_mixed->num_columns()); + auto expected_string = table_view{{int_col, string_col, float_col, string_col, string_col}}; + EXPECT_EQ(5, expected_string.num_columns()); + + auto expected_mixed = table_view{{int_col, string_col, float_col, list_int_col, list_int_col}}; + EXPECT_EQ(5, expected_mixed.num_columns()); cudf_io::parquet_reader_options in_opts = cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}) - .convert_binary_to_strings({true, true, true, true, true}); + .set_column_schema({{}, {}, {}, {}, {}}); auto result = cudf_io::read_parquet(in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected_string->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected_string, result.tbl->view()); cudf_io::parquet_reader_options default_in_opts = cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}); result = cudf_io::read_parquet(default_in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected_string->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected_string, result.tbl->view()); + + std::vector md{ + {}, + {}, + {}, + cudf_io::reader_column_schema().set_convert_binary_to_strings(false), + cudf_io::reader_column_schema().set_convert_binary_to_strings(false)}; cudf_io::parquet_reader_options mixed_in_opts = - cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}) - .convert_binary_to_strings({true, true, true, false, false}); + cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}).set_column_schema(md); result = cudf_io::read_parquet(mixed_in_opts); - CUDF_TEST_EXPECT_TABLES_EQUAL(expected_mixed->view(), result.tbl->view()); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected_mixed, result.tbl->view()); } TEST_F(ParquetReaderTest, NestedByteArray) @@ -4191,20 +4104,31 @@ TEST_F(ParquetReaderTest, NestedByteArray) auto const expected = table_view{{int_col, float_col, list_list_int_col}}; EXPECT_EQ(3, expected.num_columns()); - cudf_io::table_input_metadata ouput_metadata(expected); - ouput_metadata.column_metadata[0].set_name("col_other"); - ouput_metadata.column_metadata[1].set_name("col_float"); - ouput_metadata.column_metadata[2].set_name("col_binary").set_output_as_binary(true); + cudf_io::table_input_metadata output_metadata(expected); + output_metadata.column_metadata[0].set_name("col_other"); + output_metadata.column_metadata[1].set_name("col_float"); + output_metadata.column_metadata[2].set_name("col_binary").child(1).set_output_as_binary(true); auto filepath = temp_env->get_temp_filepath("NestedByteArray.parquet"); cudf_io::parquet_writer_options out_opts = cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected) - .metadata(&ouput_metadata); + .metadata(&output_metadata); cudf_io::write_parquet(out_opts); + auto source = cudf_io::datasource::create(filepath); + cudf_io::parquet::FileMetaData fmd; + + read_footer(source, &fmd); + EXPECT_EQ(fmd.schema[5].type, cudf::io::parquet::Type::BYTE_ARRAY); + + std::vector md{ + {}, + {}, + cudf_io::reader_column_schema().add_child( + cudf_io::reader_column_schema().set_convert_binary_to_strings(false))}; + cudf_io::parquet_reader_options in_opts = - cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}) - .convert_binary_to_strings({true, true, true, true, true, true, true, true}); + cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}).set_column_schema(md); auto result = cudf_io::read_parquet(in_opts); CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); @@ -4225,24 +4149,21 @@ TEST_F(ParquetWriterTest, ByteArrayStats) cudf::test::lists_column_wrapper list_int_col1{ {0xfe, 0xfe, 0xfe}, {0xfe, 0xfe, 0xfe}, {0xfe, 0xfe, 0xfe}}; - std::vector> cols; - cols.push_back(list_int_col0.release()); - cols.push_back(list_int_col1.release()); - auto expected = std::make_unique
(std::move(cols)); - EXPECT_EQ(2, expected->num_columns()); - cudf_io::table_input_metadata ouput_metadata(*expected); - ouput_metadata.column_metadata[0].set_name("col_binary0").set_output_as_binary(true); - ouput_metadata.column_metadata[1].set_name("col_binary1").set_output_as_binary(true); + auto expected = table_view{{list_int_col0, list_int_col1}}; + EXPECT_EQ(2, expected.num_columns()); + cudf_io::table_input_metadata output_metadata(expected); + output_metadata.column_metadata[0].set_name("col_binary0").set_output_as_binary(true); + output_metadata.column_metadata[1].set_name("col_binary1").set_output_as_binary(true); auto filepath = temp_env->get_temp_filepath("ByteArrayStats.parquet"); cudf_io::parquet_writer_options out_opts = - cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected->view()) - .metadata(&ouput_metadata); + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected) + .metadata(&output_metadata); cudf_io::write_parquet(out_opts); cudf_io::parquet_reader_options in_opts = cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}) - .convert_binary_to_strings({true, true}); + .set_column_schema({{}, {}}); auto result = cudf_io::read_parquet(in_opts); auto source = cudf_io::datasource::create(filepath); @@ -4262,4 +4183,41 @@ TEST_F(ParquetWriterTest, ByteArrayStats) EXPECT_EQ(expected_col1_max, stats1.max_value); } +TEST_F(ParquetReaderTest, StructByteArray) +{ + constexpr auto num_rows = 100; + + auto seq_col0 = random_values(num_rows); + auto const validity = cudf::test::iterators::no_nulls(); + + column_wrapper int_col{seq_col0.begin(), seq_col0.end(), validity}; + cudf::test::lists_column_wrapper list_of_int{{seq_col0.begin(), seq_col0.begin() + 50}, + {seq_col0.begin() + 50, seq_col0.end()}}; + auto struct_col = cudf::test::structs_column_wrapper{{list_of_int}, validity}; + + auto const expected = table_view{{struct_col}}; + EXPECT_EQ(1, expected.num_columns()); + cudf_io::table_input_metadata output_metadata(expected); + output_metadata.column_metadata[0] + .set_name("struct_binary") + .child(0) + .set_name("a") + .set_output_as_binary(true); + + auto filepath = temp_env->get_temp_filepath("StructByteArray.parquet"); + cudf_io::parquet_writer_options out_opts = + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected) + .metadata(&output_metadata); + cudf_io::write_parquet(out_opts); + + std::vector md{cudf_io::reader_column_schema().add_child( + cudf_io::reader_column_schema().set_convert_binary_to_strings(false))}; + + cudf_io::parquet_reader_options in_opts = + cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}).set_column_schema(md); + auto result = cudf_io::read_parquet(in_opts); + + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); +} + CUDF_TEST_PROGRAM_MAIN() diff --git a/java/src/main/native/src/TableJni.cpp b/java/src/main/native/src/TableJni.cpp index 44c08aec110..4bc0a2c3b76 100644 --- a/java/src/main/native/src/TableJni.cpp +++ b/java/src/main/native/src/TableJni.cpp @@ -1533,8 +1533,7 @@ JNIEXPORT jlongArray JNICALL Java_ai_rapids_cudf_Table_readParquet( auto builder = cudf::io::parquet_reader_options::builder(source); if (n_filter_col_names.size() > 0) { - builder = builder.columns(n_filter_col_names.as_cpp_vector()) - .convert_binary_to_strings(n_col_binary_read.to_vector()); + builder = builder.columns(n_filter_col_names.as_cpp_vector()); } cudf::io::parquet_reader_options opts = diff --git a/java/src/test/java/ai/rapids/cudf/TableTest.java b/java/src/test/java/ai/rapids/cudf/TableTest.java index c7e6fecea26..7d915cffcfe 100644 --- a/java/src/test/java/ai/rapids/cudf/TableTest.java +++ b/java/src/test/java/ai/rapids/cudf/TableTest.java @@ -574,9 +574,9 @@ void testReadParquetBinary() { .includeColumn("value2", false) .build(); try (Table table = Table.readParquet(opts, TEST_PARQUET_FILE_BINARY)) { - assertTableTypes(new DType[]{DType.LIST, DType.STRING}, table); + assertTableTypes(new DType[]{DType.STRING, DType.STRING}, table); ColumnView columnView = table.getColumn(0); - assertEquals(DType.INT8, columnView.getChildColumnView(0).getType()); + assertEquals(DType.STRING, columnView.getType()); } } From 46c5e90bf1565ac4df87949a806597160ffbc22f Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Tue, 16 Aug 2022 14:43:17 -0500 Subject: [PATCH 40/58] Add hexadecimal value separators (#11527) This PR replaces #11509, and adds hexadecimal value separators `'` (supported in C++14 and newer) every 4 characters from the least significant (right) side. For example, values like `0xffffffff` should be written as `0xffff'ffff`. In many cases, I added an unsigned suffix `u` as well, if I could identify the value as needing to be unsigned while refactoring. I also added a note to the Developer Guide referencing [C++ Core Guidelines NL.11](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#nl11-make-literals-readable), which supports the general arguments for readability that @jrhemstad made in the PR conversation on #11509. I did not add separators to "magic values" used in some of the hashing code, because they're copy-pasted directly from a reference and it should be possible to search and find the original value. Happy to make those changes if reviewers think they're needed. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Jason Lowe (https://github.com/jlowe) - Mark Harris (https://github.com/harrism) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/11527 --- cpp/docs/DEVELOPER_GUIDE.md | 8 +++ cpp/include/cudf/detail/copy_if.cuh | 4 +- cpp/include/cudf/detail/copy_if_else.cuh | 2 +- cpp/include/cudf/detail/copy_range.cuh | 4 +- cpp/include/cudf/detail/hashing.hpp | 4 +- .../cudf/detail/utilities/device_atomics.cuh | 8 +-- .../cudf/detail/utilities/hash_functions.cuh | 12 ++-- .../cudf/detail/utilities/int_fastdiv.h | 24 ++++---- cpp/include/cudf/detail/valid_if.cuh | 4 +- cpp/include/cudf/strings/detail/utf8.hpp | 56 +++++++++---------- cpp/src/bitmask/null_mask.cu | 2 +- cpp/src/copying/concatenate.cu | 4 +- cpp/src/io/avro/avro.hpp | 2 +- cpp/src/io/comp/cpu_unbz2.cpp | 8 +-- cpp/src/io/comp/debrotli.cu | 18 +++--- cpp/src/io/comp/gpuinflate.cu | 2 +- cpp/src/io/comp/snap.cu | 4 +- cpp/src/io/comp/uncomp.cpp | 18 +++--- cpp/src/io/comp/unsnap.cu | 6 +- cpp/src/io/csv/csv_gpu.cu | 2 +- cpp/src/io/orc/dict_enc.cu | 10 ++-- cpp/src/io/orc/stripe_data.cu | 16 +++--- cpp/src/io/orc/stripe_enc.cu | 4 +- cpp/src/io/parquet/page_enc.cu | 6 +- cpp/src/io/parquet/parquet.hpp | 2 +- cpp/src/join/conditional_join_kernels.cuh | 2 +- cpp/src/merge/merge.cu | 2 +- cpp/src/replace/nulls.cu | 4 +- cpp/src/replace/replace.cu | 4 +- cpp/src/rolling/detail/rolling.cuh | 2 +- cpp/src/rolling/jit/kernel.cu | 2 +- cpp/src/strings/capitalize.cu | 2 +- cpp/src/strings/case.cu | 2 +- cpp/src/strings/char_types/char_types.cu | 4 +- cpp/src/strings/convert/convert_hex.cu | 6 +- cpp/src/strings/copying/concatenate.cu | 2 +- cpp/src/strings/json/json_path.cu | 2 +- cpp/src/strings/regex/regex.cuh | 2 +- cpp/src/strings/regex/regex.inl | 2 +- cpp/src/text/normalize.cu | 17 +++--- .../text/subword/detail/codepoint_metadata.ah | 4 +- cpp/src/text/subword/detail/cp_data.h | 4 +- .../device_atomics/device_atomics_test.cu | 4 +- cpp/tests/hashing/hash_test.cpp | 8 +-- cpp/tests/io/parquet_test.cpp | 2 +- java/src/main/native/src/row_conversion.cu | 6 +- 46 files changed, 162 insertions(+), 151 deletions(-) diff --git a/cpp/docs/DEVELOPER_GUIDE.md b/cpp/docs/DEVELOPER_GUIDE.md index 4cdb9411a90..33dd341a7e8 100644 --- a/cpp/docs/DEVELOPER_GUIDE.md +++ b/cpp/docs/DEVELOPER_GUIDE.md @@ -127,6 +127,14 @@ and we try to follow his rules: "No raw loops. No raw pointers. No raw synchroni does use raw synchronization primitives. So we should revisit Parent's third rule and improve here. +Additional style guidelines for libcudf code include: + + * [NL.11: Make Literals + Readable](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#nl11-make-literals-readable): + Decimal values should use integer separators every thousands place, like + `1'234'567`. Hexadecimal values should use separators every 4 characters, + like `0x0123'ABCD`. + Documentation is discussed in the [Documentation Guide](DOCUMENTATION.md). ### Includes diff --git a/cpp/include/cudf/detail/copy_if.cuh b/cpp/include/cudf/detail/copy_if.cuh index 34fc2661418..99d9f5181c7 100644 --- a/cpp/include/cudf/detail/copy_if.cuh +++ b/cpp/include/cudf/detail/copy_if.cuh @@ -172,7 +172,7 @@ __launch_bounds__(block_size) __global__ int valid_index = (block_offset / cudf::detail::warp_size) + wid; // compute the valid mask for this warp - uint32_t valid_warp = __ballot_sync(0xffffffff, temp_valids[threadIdx.x]); + uint32_t valid_warp = __ballot_sync(0xffff'ffffu, temp_valids[threadIdx.x]); // Note the atomicOr's below assume that output_valid has been set to // all zero before the kernel @@ -187,7 +187,7 @@ __launch_bounds__(block_size) __global__ // if the block is full and not aligned then we have one more warp to cover if ((wid == 0) && (last_warp == num_warps)) { - uint32_t valid_warp = __ballot_sync(0xffffffff, temp_valids[block_size + threadIdx.x]); + uint32_t valid_warp = __ballot_sync(0xffff'ffffu, temp_valids[block_size + threadIdx.x]); if (lane == 0 && valid_warp != 0) { tmp_warp_valid_counts += __popc(valid_warp); atomicOr(&output_valid[valid_index + num_warps], valid_warp); diff --git a/cpp/include/cudf/detail/copy_if_else.cuh b/cpp/include/cudf/detail/copy_if_else.cuh index b2710223693..e93a30bc43b 100644 --- a/cpp/include/cudf/detail/copy_if_else.cuh +++ b/cpp/include/cudf/detail/copy_if_else.cuh @@ -74,7 +74,7 @@ __launch_bounds__(block_size) __global__ // update validity if (has_nulls) { // the final validity mask for this warp - int warp_mask = __ballot_sync(0xFFFF'FFFF, opt_value.has_value()); + int warp_mask = __ballot_sync(0xFFFF'FFFFu, opt_value.has_value()); // only one guy in the warp needs to update the mask and count if (lane_id == 0) { out.set_mask_word(warp_cur, warp_mask); diff --git a/cpp/include/cudf/detail/copy_range.cuh b/cpp/include/cudf/detail/copy_range.cuh index fca6fd7547d..aaba729f2f2 100644 --- a/cpp/include/cudf/detail/copy_range.cuh +++ b/cpp/include/cudf/detail/copy_range.cuh @@ -80,8 +80,8 @@ __global__ void copy_range_kernel(SourceValueIterator source_value_begin, if (has_validity) { // update bitmask const bool valid = in_range && *(source_validity_begin + source_idx); - const int active_mask = __ballot_sync(0xFFFFFFFF, in_range); - const int valid_mask = __ballot_sync(0xFFFFFFFF, valid); + const int active_mask = __ballot_sync(0xFFFF'FFFFu, in_range); + const int valid_mask = __ballot_sync(0xFFFF'FFFFu, valid); const int warp_mask = active_mask & valid_mask; cudf::bitmask_type old_mask = target.get_mask_word(mask_idx); diff --git a/cpp/include/cudf/detail/hashing.hpp b/cpp/include/cudf/detail/hashing.hpp index 5e4dba412ff..66cbf24e607 100644 --- a/cpp/include/cudf/detail/hashing.hpp +++ b/cpp/include/cudf/detail/hashing.hpp @@ -73,7 +73,7 @@ std::unique_ptr md5_hash( */ constexpr uint32_t hash_combine(uint32_t lhs, uint32_t rhs) { - return lhs ^ (rhs + 0x9e3779b9 + (lhs << 6) + (lhs >> 2)); + return lhs ^ (rhs + 0x9e37'79b9 + (lhs << 6) + (lhs >> 2)); } /* Copyright 2005-2014 Daniel James. @@ -94,7 +94,7 @@ constexpr uint32_t hash_combine(uint32_t lhs, uint32_t rhs) */ constexpr std::size_t hash_combine(std::size_t lhs, std::size_t rhs) { - return lhs ^ (rhs + 0x9e3779b97f4a7c15 + (lhs << 6) + (lhs >> 2)); + return lhs ^ (rhs + 0x9e37'79b9'7f4a'7c15 + (lhs << 6) + (lhs >> 2)); } } // namespace detail diff --git a/cpp/include/cudf/detail/utilities/device_atomics.cuh b/cpp/include/cudf/detail/utilities/device_atomics.cuh index 0521418d2d3..a3c7dab475a 100644 --- a/cpp/include/cudf/detail/utilities/device_atomics.cuh +++ b/cpp/include/cudf/detail/utilities/device_atomics.cuh @@ -72,7 +72,7 @@ struct genericAtomicOperationImpl { assumed = old; T target_value = T((old >> shift) & 0xff); uint8_t updating_value = type_reinterpret(op(target_value, update_value)); - T_int new_value = (old & ~(0x000000ff << shift)) | (T_int(updating_value) << shift); + T_int new_value = (old & ~(0x0000'00ff << shift)) | (T_int(updating_value) << shift); old = atomicCAS(address_uint32, assumed, new_value); } while (assumed != old); @@ -98,7 +98,7 @@ struct genericAtomicOperationImpl { T const target_value = (is_32_align) ? T(old & 0xffff) : T(old >> 16); uint16_t updating_value = type_reinterpret(op(target_value, update_value)); - T_int const new_value = (is_32_align) ? (old & 0xffff0000) | updating_value + T_int const new_value = (is_32_align) ? (old & 0xffff'0000) | updating_value : (old & 0xffff) | (T_int(updating_value) << 16); old = atomicCAS(address_uint32, assumed, new_value); } while (assumed != old); @@ -338,7 +338,7 @@ struct typesAtomicCASImpl { // the `target_value` in `old` can be different with `compare` if (target_value != compare) break; - T_int new_value = (old & ~(0x000000ff << shift)) | (T_int(u_val) << shift); + T_int new_value = (old & ~(0x0000'00ff << shift)) | (T_int(u_val) << shift); old = atomicCAS(address_uint32, assumed, new_value); } while (assumed != old); @@ -367,7 +367,7 @@ struct typesAtomicCASImpl { if (target_value != compare) break; T_int new_value = - (is_32_align) ? (old & 0xffff0000) | u_val : (old & 0xffff) | (T_int(u_val) << 16); + (is_32_align) ? (old & 0xffff'0000) | u_val : (old & 0xffff) | (T_int(u_val) << 16); old = atomicCAS(address_uint32, assumed, new_value); } while (assumed != old); diff --git a/cpp/include/cudf/detail/utilities/hash_functions.cuh b/cpp/include/cudf/detail/utilities/hash_functions.cuh index 743ae491def..ca9c16043a3 100644 --- a/cpp/include/cudf/detail/utilities/hash_functions.cuh +++ b/cpp/include/cudf/detail/utilities/hash_functions.cuh @@ -169,16 +169,16 @@ auto __device__ inline get_element_pointer_and_size(string_view const& element) */ void __device__ inline uint32ToLowercaseHexString(uint32_t num, char* destination) { - // Transform 0xABCD1234 => 0x0000ABCD00001234 => 0x0B0A0D0C02010403 + // Transform 0xABCD'1234 => 0x0000'ABCD'0000'1234 => 0x0B0A'0D0C'0201'0403 uint64_t x = num; - x = ((x & 0xFFFF0000) << 16) | ((x & 0xFFFF)); - x = ((x & 0xF0000000F) << 8) | ((x & 0xF0000000F0) >> 4) | ((x & 0xF0000000F00) << 16) | - ((x & 0xF0000000F000) << 4); + x = ((x & 0xFFFF'0000u) << 16) | ((x & 0xFFFF)); + x = ((x & 0x000F'0000'000Fu) << 8) | ((x & 0x00F0'0000'00F0u) >> 4) | + ((x & 0x0F00'0000'0F00u) << 16) | ((x & 0xF000'0000'F000) << 4); // Calculate a mask of ascii value offsets for bytes that contain alphabetical hex digits - uint64_t offsets = (((x + 0x0606060606060606) >> 4) & 0x0101010101010101) * 0x27; + uint64_t offsets = (((x + 0x0606'0606'0606'0606) >> 4) & 0x0101'0101'0101'0101) * 0x27; - x |= 0x3030303030303030; + x |= 0x3030'3030'3030'3030; x += offsets; std::memcpy(destination, reinterpret_cast(&x), 8); } diff --git a/cpp/include/cudf/detail/utilities/int_fastdiv.h b/cpp/include/cudf/detail/utilities/int_fastdiv.h index 292b502cc78..b56fe0e88c1 100644 --- a/cpp/include/cudf/detail/utilities/int_fastdiv.h +++ b/cpp/include/cudf/detail/utilities/int_fastdiv.h @@ -1,17 +1,19 @@ /* - * Copyright 2014 Maxim Milakov + * Copyright (c) 2019-2022, NVIDIA CORPORATION. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Copyright 2014 Maxim Milakov * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -56,7 +58,7 @@ class int_fastdiv { int p; unsigned int ad, anc, delta, q1, r1, q2, r2, t; - const unsigned two31 = 0x80000000; + const unsigned two31 = 0x8000'0000u; ad = (d == 0) ? 1 : abs(d); t = two31 + ((unsigned int)d >> 31); anc = t - 1 - t % ad; diff --git a/cpp/include/cudf/detail/valid_if.cuh b/cpp/include/cudf/detail/valid_if.cuh index 01da5ea32ff..0fe7edad21d 100644 --- a/cpp/include/cudf/detail/valid_if.cuh +++ b/cpp/include/cudf/detail/valid_if.cuh @@ -53,7 +53,7 @@ __global__ void valid_if_kernel( thread_index_type const stride = blockDim.x * gridDim.x; size_type warp_valid_count{0}; - auto active_mask = __ballot_sync(0xFFFF'FFFF, i < size); + auto active_mask = __ballot_sync(0xFFFF'FFFFu, i < size); while (i < size) { bitmask_type ballot = __ballot_sync(active_mask, p(*(begin + i))); if (lane_id == leader_lane) { @@ -173,7 +173,7 @@ __global__ void valid_if_n_kernel(InputIterator1 begin1, auto const arg_1 = *(begin1 + mask_idx); auto const arg_2 = *(begin2 + thread_idx); auto const bit_is_valid = thread_active && p(arg_1, arg_2); - auto const warp_validity = __ballot_sync(0xffffffff, bit_is_valid); + auto const warp_validity = __ballot_sync(0xffff'ffffu, bit_is_valid); auto const mask_idx = word_index(thread_idx); if (thread_active && threadIdx.x % warp_size == 0) { mask[mask_idx] = warp_validity; } diff --git a/cpp/include/cudf/strings/detail/utf8.hpp b/cpp/include/cudf/strings/detail/utf8.hpp index 44e114865d9..c8d7ec4ae43 100644 --- a/cpp/include/cudf/strings/detail/utf8.hpp +++ b/cpp/include/cudf/strings/detail/utf8.hpp @@ -49,9 +49,9 @@ constexpr bool is_begin_utf8_char(uint8_t byte) */ constexpr size_type bytes_in_char_utf8(char_utf8 character) { - return 1 + static_cast((character & unsigned{0x0000FF00}) > 0) + - static_cast((character & unsigned{0x00FF0000}) > 0) + - static_cast((character & unsigned{0xFF000000}) > 0); + return 1 + static_cast((character & 0x0000'FF00u) > 0) + + static_cast((character & 0x00FF'0000u) > 0) + + static_cast((character & 0xFF00'0000u) > 0); } /** @@ -126,23 +126,23 @@ constexpr inline size_type from_char_utf8(char_utf8 character, char* str) constexpr uint32_t utf8_to_codepoint(cudf::char_utf8 utf8_char) { uint32_t unchr = 0; - if (utf8_char < 0x00000080) // single-byte pass thru + if (utf8_char < 0x0000'0080) // single-byte pass thru unchr = utf8_char; - else if (utf8_char < 0x0000E000) // two bytes + else if (utf8_char < 0x0000'E000) // two bytes { unchr = (utf8_char & 0x1F00) >> 2; // shift and unchr |= (utf8_char & 0x003F); // unmask - } else if (utf8_char < 0x00F00000) // three bytes + } else if (utf8_char < 0x00F0'0000) // three bytes { - unchr = (utf8_char & 0x0F0000) >> 4; // get upper 4 bits - unchr |= (utf8_char & 0x003F00) >> 2; // shift and - unchr |= (utf8_char & 0x00003F); // unmask - } else if (utf8_char <= (unsigned)0xF8000000) // four bytes + unchr = (utf8_char & 0x0F'0000) >> 4; // get upper 4 bits + unchr |= (utf8_char & 0x00'3F00) >> 2; // shift and + unchr |= (utf8_char & 0x00'003F); // unmask + } else if (utf8_char <= 0xF800'0000u) // four bytes { - unchr = (utf8_char & 0x03000000) >> 6; // upper 3 bits - unchr |= (utf8_char & 0x003F0000) >> 4; // next 6 bits - unchr |= (utf8_char & 0x00003F00) >> 2; // next 6 bits - unchr |= (utf8_char & 0x0000003F); // unmask + unchr = (utf8_char & 0x0300'0000) >> 6; // upper 3 bits + unchr |= (utf8_char & 0x003F'0000) >> 4; // next 6 bits + unchr |= (utf8_char & 0x0000'3F00) >> 2; // next 6 bits + unchr |= (utf8_char & 0x0000'003F); // unmask } return unchr; } @@ -156,26 +156,26 @@ constexpr uint32_t utf8_to_codepoint(cudf::char_utf8 utf8_char) constexpr cudf::char_utf8 codepoint_to_utf8(uint32_t unchr) { cudf::char_utf8 utf8 = 0; - if (unchr < 0x00000080) // single byte utf8 + if (unchr < 0x0000'0080) // single byte utf8 utf8 = unchr; - else if (unchr < 0x00000800) // double byte utf8 + else if (unchr < 0x0000'0800) // double byte utf8 { utf8 = (unchr << 2) & 0x1F00; // shift bits for utf8 |= (unchr & 0x3F); // utf8 encoding - utf8 |= 0x0000C080; - } else if (unchr < 0x00010000) // triple byte utf8 + utf8 |= 0x0000'C080; + } else if (unchr < 0x0001'0000) // triple byte utf8 { - utf8 = (unchr << 4) & 0x0F0000; // upper 4 bits - utf8 |= (unchr << 2) & 0x003F00; // next 6 bits - utf8 |= (unchr & 0x3F); // last 6 bits - utf8 |= 0x00E08080; - } else if (unchr < 0x00110000) // quadruple byte utf8 + utf8 = (unchr << 4) & 0x0F'0000; // upper 4 bits + utf8 |= (unchr << 2) & 0x00'3F00; // next 6 bits + utf8 |= (unchr & 0x3F); // last 6 bits + utf8 |= 0x00E0'8080; + } else if (unchr < 0x0011'0000) // quadruple byte utf8 { - utf8 = (unchr << 6) & 0x07000000; // upper 3 bits - utf8 |= (unchr << 4) & 0x003F0000; // next 6 bits - utf8 |= (unchr << 2) & 0x00003F00; // next 6 bits - utf8 |= (unchr & 0x3F); // last 6 bits - utf8 |= (unsigned)0xF0808080; + utf8 = (unchr << 6) & 0x0700'0000; // upper 3 bits + utf8 |= (unchr << 4) & 0x003F'0000; // next 6 bits + utf8 |= (unchr << 2) & 0x0000'3F00; // next 6 bits + utf8 |= (unchr & 0x3F); // last 6 bits + utf8 |= 0xF080'8080u; } return utf8; } diff --git a/cpp/src/bitmask/null_mask.cu b/cpp/src/bitmask/null_mask.cu index b444dd70542..4c9151533c2 100644 --- a/cpp/src/bitmask/null_mask.cu +++ b/cpp/src/bitmask/null_mask.cu @@ -107,7 +107,7 @@ __global__ void set_null_mask_kernel(bitmask_type* __restrict__ destination, { auto x = destination + word_index(begin_bit); const auto last_word = word_index(end_bit) - word_index(begin_bit); - bitmask_type fill_value = (valid == true) ? 0xffffffff : 0x00; + bitmask_type fill_value = (valid == true) ? 0xffff'ffff : 0; for (size_type destination_word_index = threadIdx.x + blockIdx.x * blockDim.x; destination_word_index < number_of_mask_words; diff --git a/cpp/src/copying/concatenate.cu b/cpp/src/copying/concatenate.cu index a75cd31f2c7..e0c764654c8 100644 --- a/cpp/src/copying/concatenate.cu +++ b/cpp/src/copying/concatenate.cu @@ -113,7 +113,7 @@ __global__ void concatenate_masks_kernel(column_device_view const* views, { size_type mask_index = threadIdx.x + blockIdx.x * blockDim.x; - auto active_mask = __ballot_sync(0xFFFF'FFFF, mask_index < number_of_mask_bits); + auto active_mask = __ballot_sync(0xFFFF'FFFFu, mask_index < number_of_mask_bits); while (mask_index < number_of_mask_bits) { size_type const source_view_index = @@ -177,7 +177,7 @@ __global__ void fused_concatenate_kernel(column_device_view const* input_views, size_type warp_valid_count = 0; unsigned active_mask; - if (Nullable) { active_mask = __ballot_sync(0xFFFF'FFFF, output_index < output_size); } + if (Nullable) { active_mask = __ballot_sync(0xFFFF'FFFFu, output_index < output_size); } while (output_index < output_size) { // Lookup input index by searching for output index in offsets // thrust::prev isn't in CUDA 10.0, so subtracting 1 here instead diff --git a/cpp/src/io/avro/avro.hpp b/cpp/src/io/avro/avro.hpp index 8b7a414917d..1ca50f04d18 100644 --- a/cpp/src/io/avro/avro.hpp +++ b/cpp/src/io/avro/avro.hpp @@ -119,7 +119,7 @@ class container { T get_encoded(); public: - bool parse(file_metadata* md, size_t max_num_rows = 0x7fffffff, size_t first_row = 0); + bool parse(file_metadata* md, size_t max_num_rows = 0x7fff'ffff, size_t first_row = 0); protected: const uint8_t* m_base; diff --git a/cpp/src/io/comp/cpu_unbz2.cpp b/cpp/src/io/comp/cpu_unbz2.cpp index f1cfa5fb1d4..aa3df527c21 100644 --- a/cpp/src/io/comp/cpu_unbz2.cpp +++ b/cpp/src/io/comp/cpu_unbz2.cpp @@ -212,7 +212,7 @@ int32_t bz2_decompress_block(unbz_state_s* s) // Start-of-block signature sig0 = getbits(s, 24); sig1 = getbits(s, 24); - if (sig0 != 0x314159 || sig1 != 0x265359) { return BZ_DATA_ERROR; } + if (sig0 != 0x31'4159 || sig1 != 0x26'5359) { return BZ_DATA_ERROR; } s->currBlockNo++; @@ -246,7 +246,7 @@ int32_t bz2_decompress_block(unbz_state_s* s) if (nGroups < 2 || nGroups > 6 || nSelectors < 1 || nSelectors > BZ_MAX_SELECTORS) return BZ_DATA_ERROR; - pos = 0x76543210; + pos = 0x7654'3210; for (i = 0; i < nSelectors; i++) { uint32_t selectorMtf = 0, mask, tmp; for (int32_t v = next32bits(s); v < 0; v <<= 1) { @@ -465,13 +465,13 @@ int32_t bz2_decompress_block(unbz_state_s* s) uint32_t save_bitpos = s->bitpos; sig0 = getbits(s, 24); sig1 = getbits(s, 24); - if (sig0 == 0x314159 && sig1 == 0x265359) { + if (sig0 == 0x31'4159 && sig1 == 0x26'5359) { // Start of another block: restore bitstream location s->cur = save_cur; s->bitbuf = save_bitbuf; s->bitpos = save_bitpos; return BZ_OK; - } else if (sig0 == 0x177245 && sig1 == 0x385090) { + } else if (sig0 == 0x17'7245 && sig1 == 0x38'5090) { // End-of-stream signature return BZ_STREAM_END; } else { diff --git a/cpp/src/io/comp/debrotli.cu b/cpp/src/io/comp/debrotli.cu index 427b575133e..07dc2cc9870 100644 --- a/cpp/src/io/comp/debrotli.cu +++ b/cpp/src/io/comp/debrotli.cu @@ -68,7 +68,7 @@ namespace io { constexpr uint32_t huffman_lookup_table_width = 8; constexpr int8_t brotli_code_length_codes = 18; constexpr uint32_t brotli_num_distance_short_codes = 16; -constexpr uint32_t brotli_max_allowed_distance = 0x7FFFFFFC; +constexpr uint32_t brotli_max_allowed_distance = 0x7FFF'FFFC; constexpr int block_size = 256; template @@ -1300,12 +1300,12 @@ static __device__ void InverseMoveToFrontTransform(debrotli_state_s* s, uint8_t* uint32_t upper_bound = s->mtf_upper_bound; uint32_t* mtf = &s->mtf[1]; // Make mtf[-1] addressable. auto* mtf_u8 = reinterpret_cast(mtf); - uint32_t pattern = 0x03020100; // Little-endian + uint32_t pattern = 0x0302'0100; // Little-endian // Initialize list using 4 consequent values pattern. mtf[0] = pattern; do { - pattern += 0x04040404; // Advance all 4 values by 4. + pattern += 0x0404'0404; // Advance all 4 values by 4. mtf[i] = pattern; i++; } while (i <= upper_bound); @@ -1744,10 +1744,10 @@ static __device__ void ProcessCommands(debrotli_state_s* s, const brotli_diction int dist = distance_code << 1; // kDistanceShortCodeIndexOffset has 2-bit values from LSB: 3, 2, 1, 0, 3, 3, 3, 3, // 3, 3, 2, 2, 2, 2, 2, 2 - const uint32_t kDistanceShortCodeIndexOffset = 0xAAAFFF1B; + const uint32_t kDistanceShortCodeIndexOffset = 0xAAAF'FF1B; // kDistanceShortCodeValueOffset has 2-bit values from LSB: -0, 0,-0, 0,-1, 1,-2, // 2,-3, 3,-1, 1,-2, 2,-3, 3 - const uint32_t kDistanceShortCodeValueOffset = 0xFA5FA500; + const uint32_t kDistanceShortCodeValueOffset = 0xFA5F'A500; int v = (dist_rb_idx + (int)(kDistanceShortCodeIndexOffset >> dist)) & 0x3; distance_code = s->dist_rb[v]; v = (int)(kDistanceShortCodeValueOffset >> dist) & 0x3; @@ -1758,7 +1758,7 @@ static __device__ void ProcessCommands(debrotli_state_s* s, const brotli_diction if (distance_code <= 0) { // A huge distance will cause a failure later on. This is a little faster than // failing here. - distance_code = 0x7FFFFFFF; + distance_code = 0x7FFF'FFFF; } } } @@ -1796,7 +1796,7 @@ static __device__ void ProcessCommands(debrotli_state_s* s, const brotli_diction // Apply copy of LZ77 back-reference, or static dictionary reference if the distance is // larger than the max LZ77 distance if (distance_code > max_distance) { - // The maximum allowed distance is brotli_max_allowed_distance = 0x7FFFFFFC. + // The maximum allowed distance is brotli_max_allowed_distance = 0x7FFF'FFFC. // With this choice, no signed overflow can occur after decoding // a special distance code (e.g., after adding 3 to the last distance). if (distance_code > brotli_max_allowed_distance || @@ -2092,7 +2092,7 @@ void gpu_debrotli(device_span const> inputs, CUDF_EXPECTS(scratch_size >= sizeof(brotli_dictionary_s), "Insufficient scratch space for debrotli"); - scratch_size = min(scratch_size, (size_t)0xffffffffu); + scratch_size = min(scratch_size, static_cast(0xffff'ffffu)); fb_heap_size = (uint32_t)((scratch_size - sizeof(brotli_dictionary_s)) & ~0xf); CUDF_CUDA_TRY(cudaMemsetAsync(scratch_u8, 0, 2 * sizeof(uint32_t), stream.value())); @@ -2114,7 +2114,7 @@ void gpu_debrotli(device_span const> inputs, &dump[0], scratch_u8 + cur, 2 * sizeof(uint32_t), cudaMemcpyDeviceToHost, stream.value())); stream.synchronize(); printf("@%d: next = %d, size = %d\n", cur, dump[0], dump[1]); - cur = (dump[0] > cur) ? dump[0] : 0xffffffffu; + cur = (dump[0] > cur) ? dump[0] : 0xffff'ffffu; } #endif } diff --git a/cpp/src/io/comp/gpuinflate.cu b/cpp/src/io/comp/gpuinflate.cu index c2d89604340..16f4ea84f7f 100644 --- a/cpp/src/io/comp/gpuinflate.cu +++ b/cpp/src/io/comp/gpuinflate.cu @@ -978,7 +978,7 @@ __device__ int parse_gzip_header(const uint8_t* src, size_t src_size) if (src_size >= 18) { uint32_t sig = (src[0] << 16) | (src[1] << 8) | src[2]; - if (sig == 0x1f8b08) // 24-bit GZIP inflate signature {0x1f, 0x8b, 0x08} + if (sig == 0x1f'8b08) // 24-bit GZIP inflate signature {0x1f, 0x8b, 0x08} { uint8_t flags = src[3]; hdr_len = 10; diff --git a/cpp/src/io/comp/snap.cu b/cpp/src/io/comp/snap.cu index e51dadac16b..820a7f937d7 100644 --- a/cpp/src/io/comp/snap.cu +++ b/cpp/src/io/comp/snap.cu @@ -91,7 +91,7 @@ static __device__ uint8_t* StoreLiterals( dst[2] = len_minus1 >> 8; } dst += 3; - } else if (len_minus1 <= 0xffffff) { + } else if (len_minus1 <= 0xff'ffff) { if (!t && dst + 3 < end) { dst[0] = 62 << 2; dst[1] = len_minus1; @@ -205,7 +205,7 @@ static __device__ uint32_t FindFourByteMatch(snap_state_s* s, offset = pos + local_match_lane; } else { offset = (pos & ~0xffff) | s->hash_map[hash]; - if (offset >= pos) { offset = (offset >= 0x10000) ? offset - 0x10000 : pos; } + if (offset >= pos) { offset = (offset >= 0x1'0000) ? offset - 0x1'0000 : pos; } match = (offset < pos && offset + max_copy_distance >= pos + t && fetch4(src + offset) == data32); } diff --git a/cpp/src/io/comp/uncomp.cpp b/cpp/src/io/comp/uncomp.cpp index 58dba71db00..6f33c9f1de9 100644 --- a/cpp/src/io/comp/uncomp.cpp +++ b/cpp/src/io/comp/uncomp.cpp @@ -48,7 +48,7 @@ struct gz_file_header_s { struct zip_eocd_s // end of central directory { - uint32_t sig; // 0x06054b50 + uint32_t sig; // 0x0605'4b50 uint16_t disk_id; // number of this disk uint16_t start_disk; // number of the disk with the start of the central directory uint16_t num_entries; // number of entries in the central dir on this disk @@ -60,7 +60,7 @@ struct zip_eocd_s // end of central directory struct zip64_eocdl // end of central dir locator { - uint32_t sig; // 0x07064b50 + uint32_t sig; // 0x0706'4b50 uint32_t disk_start; // number of the disk with the start of the zip64 end of central directory uint64_t eocdr_ofs; // relative offset of the zip64 end of central directory record uint32_t num_disks; // total number of disks @@ -68,7 +68,7 @@ struct zip64_eocdl // end of central dir locator struct zip_cdfh_s // central directory file header { - uint32_t sig; // 0x02014b50 + uint32_t sig; // 0x0201'4b50 uint16_t ver; // version made by uint16_t min_ver; // version needed to extract uint16_t gp_flags; // general purpose bit flag @@ -88,7 +88,7 @@ struct zip_cdfh_s // central directory file header }; struct zip_lfh_s { - uint32_t sig; // 0x04034b50 + uint32_t sig; // 0x0403'4b50 uint16_t ver; // version needed to extract uint16_t gp_flags; // general purpose bit flag uint16_t comp_method; // compression method @@ -200,7 +200,7 @@ bool OpenZipArchive(zip_archive_s* dst, const uint8_t* raw, size_t len) i + sizeof(zip_eocd_s) + 2 + 0xffff >= len && i >= 0; i--) { const auto* eocd = reinterpret_cast(raw + i); - if (eocd->sig == 0x06054b50 && + if (eocd->sig == 0x0605'4b50 && eocd->disk_id == eocd->start_disk // multi-file archives not supported && eocd->num_entries == eocd->total_entries && eocd->cdir_size >= sizeof(zip_cdfh_s) * eocd->num_entries && eocd->cdir_offset < len && @@ -209,10 +209,10 @@ bool OpenZipArchive(zip_archive_s* dst, const uint8_t* raw, size_t len) dst->eocd = eocd; if (i >= static_cast(sizeof(zip64_eocdl))) { const auto* eocdl = reinterpret_cast(raw + i - sizeof(zip64_eocdl)); - if (eocdl->sig == 0x07064b50) { dst->eocdl = eocdl; } + if (eocdl->sig == 0x0706'4b50) { dst->eocdl = eocdl; } } // Start of central directory - if (cdfh->sig == 0x02014b50) { dst->cdfh = cdfh; } + if (cdfh->sig == 0x0201'4b50) { dst->cdfh = cdfh; } } } } @@ -308,7 +308,7 @@ std::vector decompress(compression_type compression, host_span( reinterpret_cast(za.cdfh) + cdfh_ofs); int cdfh_len = sizeof(zip_cdfh_s) + cdfh->fname_len + cdfh->extra_len + cdfh->comment_len; - if (cdfh_ofs + cdfh_len > za.eocd->cdir_size || cdfh->sig != 0x02014b50) { + if (cdfh_ofs + cdfh_len > za.eocd->cdir_size || cdfh->sig != 0x0201'4b50) { // Bad cdir break; } @@ -316,7 +316,7 @@ std::vector decompress(compression_type compression, host_spancomp_method == 8 && cdfh->comp_size > 0 && cdfh->uncomp_size > 0) { size_t lfh_ofs = cdfh->hdr_ofs; const zip_lfh_s* lfh = reinterpret_cast(raw + lfh_ofs); - if (lfh_ofs + sizeof(zip_lfh_s) <= src.size() && lfh->sig == 0x04034b50 && + if (lfh_ofs + sizeof(zip_lfh_s) <= src.size() && lfh->sig == 0x0403'4b50 && lfh_ofs + sizeof(zip_lfh_s) + lfh->fname_len + lfh->extra_len <= src.size()) { if (lfh->comp_method == 8 && lfh->comp_size > 0 && lfh->uncomp_size > 0) { size_t file_start = lfh_ofs + sizeof(zip_lfh_s) + lfh->fname_len + lfh->extra_len; diff --git a/cpp/src/io/comp/unsnap.cu b/cpp/src/io/comp/unsnap.cu index 14d53259eb4..98011a57ea8 100644 --- a/cpp/src/io/comp/unsnap.cu +++ b/cpp/src/io/comp/unsnap.cu @@ -34,7 +34,7 @@ void __device__ busy_wait(size_t cycles) clock_t start = clock(); for (;;) { clock_t const now = clock(); - clock_t const elapsed = now > start ? now - start : now + (0xffffffff - start); + clock_t const elapsed = now > start ? now - start : now + (0xffff'ffff - start); if (elapsed >= cycles) return; } } @@ -361,8 +361,8 @@ __device__ void snappy_decode_symbols(unsnap_state_s* s, uint32_t t) v1 = ballot((clen >> 1) & 1); len3_mask = shuffle((t == 0) ? get_len5_mask(v0, v1) : 0); mask_t = (1 << (2 * t)) - 1; - cur_t = cur + 2 * t + 2 * __popc((len3_mask & 0xaaaaaaaa) & mask_t) + - __popc((len3_mask & 0x55555555) & mask_t); + cur_t = cur + 2 * t + 2 * __popc((len3_mask & 0xaaaa'aaaa) & mask_t) + + __popc((len3_mask & 0x5555'5555) & mask_t); b0 = byte_access(s, cur_t); is_long_sym = ((b0 & 3) ? ((b0 & 3) == 3) : (b0 > 3 * 4)) || (cur_t >= cur + 32) || (batch_len + t >= batch_size); diff --git a/cpp/src/io/csv/csv_gpu.cu b/cpp/src/io/csv/csv_gpu.cu index 55169e335cc..eb0ebd6a28d 100644 --- a/cpp/src/io/csv/csv_gpu.cu +++ b/cpp/src/io/csv/csv_gpu.cu @@ -895,7 +895,7 @@ __global__ void __launch_bounds__(rowofs_block_dim) // Eliminate rows that start before byte_range_start if (start_offset + block_pos < byte_range_start) { uint32_t dist_minus1 = min(byte_range_start - (start_offset + block_pos) - 1, UINT64_C(31)); - uint32_t mask = 0xfffffffe << dist_minus1; + uint32_t mask = 0xffff'fffe << dist_minus1; ctx_map.x &= mask; ctx_map.y &= mask; ctx_map.z &= mask; diff --git a/cpp/src/io/orc/dict_enc.cu b/cpp/src/io/orc/dict_enc.cu index 73e59b31747..0b5de26adfc 100644 --- a/cpp/src/io/orc/dict_enc.cu +++ b/cpp/src/io/orc/dict_enc.cu @@ -78,7 +78,7 @@ static __device__ void LoadNonNullIndices(volatile dictinit_state_s* s, uint32_t is_valid, nz_pos; if (t < block_size / 32) { if (!valid_map) { - s->scratch_red[t] = 0xffffffffu; + s->scratch_red[t] = 0xffff'ffffu; } else { uint32_t const row = s->chunk.start_row + i + t * 32; auto const chunk_end = s->chunk.start_row + s->chunk.num_rows; @@ -198,13 +198,13 @@ __global__ void __launch_bounds__(block_size, 2) uint32_t sum23 = count23 + (count23 << 16); uint32_t sum45 = count45 + (count45 << 16); uint32_t sum67 = count67 + (count67 << 16); - sum23 += (sum01 >> 16) * 0x10001; - sum45 += (sum23 >> 16) * 0x10001; - sum67 += (sum45 >> 16) * 0x10001; + sum23 += (sum01 >> 16) * 0x1'0001; + sum45 += (sum23 >> 16) * 0x1'0001; + sum67 += (sum45 >> 16) * 0x1'0001; uint32_t sum_w = sum67 >> 16; block_scan(temp_storage.scan_storage).InclusiveSum(sum_w, sum_w); __syncthreads(); - sum_w = (sum_w - (sum67 >> 16)) * 0x10001; + sum_w = (sum_w - (sum67 >> 16)) * 0x1'0001; s->map.u32[t * 4 + 0] = sum_w + sum01 - count01; s->map.u32[t * 4 + 1] = sum_w + sum23 - count23; s->map.u32[t * 4 + 2] = sum_w + sum45 - count45; diff --git a/cpp/src/io/orc/stripe_data.cu b/cpp/src/io/orc/stripe_data.cu index 040f75a8616..a4cd5de8ec8 100644 --- a/cpp/src/io/orc/stripe_data.cu +++ b/cpp/src/io/orc/stripe_data.cu @@ -360,19 +360,19 @@ inline __device__ uint32_t varint_length(volatile orc_bytestream_s* bs, int pos) { if (bytestream_readbyte(bs, pos) > 0x7f) { uint32_t next32 = bytestream_readu32(bs, pos + 1); - uint32_t zbit = __ffs((~next32) & 0x80808080); + uint32_t zbit = __ffs((~next32) & 0x8080'8080); if (sizeof(T) <= 4 || zbit) { return 1 + (zbit >> 3); // up to 5x7 bits } else { next32 = bytestream_readu32(bs, pos + 5); - zbit = __ffs((~next32) & 0x80808080); + zbit = __ffs((~next32) & 0x8080'8080); if (zbit) { return 5 + (zbit >> 3); // up to 9x7 bits } else if ((sizeof(T) <= 8) || (bytestream_readbyte(bs, pos + 9) <= 0x7f)) { return 10; // up to 70 bits } else { uint64_t next64 = bytestream_readu64(bs, pos + 10); - zbit = __ffsll((~next64) & 0x8080808080808080ull); + zbit = __ffsll((~next64) & 0x8080'8080'8080'8080ull); if (zbit) { return 10 + (zbit >> 3); // Up to 18x7 bits (126) } else { @@ -405,10 +405,10 @@ inline __device__ int decode_base128_varint(volatile orc_bytestream_s* bs, int p v = (v & 0x3fff) | (b << 14); if (b > 0x7f) { b = bytestream_readbyte(bs, pos++); - v = (v & 0x1fffff) | (b << 21); + v = (v & 0x1f'ffff) | (b << 21); if (b > 0x7f) { b = bytestream_readbyte(bs, pos++); - v = (v & 0x0fffffff) | (b << 28); + v = (v & 0x0fff'ffff) | (b << 28); if constexpr (sizeof(T) > 4) { uint32_t lo = v; uint64_t hi; @@ -421,10 +421,10 @@ inline __device__ int decode_base128_varint(volatile orc_bytestream_s* bs, int p v = (v & 0x3ff) | (b << 10); if (b > 0x7f) { b = bytestream_readbyte(bs, pos++); - v = (v & 0x1ffff) | (b << 17); + v = (v & 0x1'ffff) | (b << 17); if (b > 0x7f) { b = bytestream_readbyte(bs, pos++); - v = (v & 0xffffff) | (b << 24); + v = (v & 0xff'ffff) | (b << 24); if (b > 0x7f) { pos++; // last bit is redundant (extra byte implies bit63 is 1) } @@ -1208,7 +1208,7 @@ __global__ void __launch_bounds__(block_size) // Need to arrange the bytes to apply mask properly. uint32_t bits = (i + 32 <= skippedrows) ? s->vals.u32[i >> 5] : (__byte_perm(s->vals.u32[i >> 5], 0, 0x0123) & - (0xffffffffu << (0x20 - skippedrows + i))); + (0xffff'ffffu << (0x20 - skippedrows + i))); skip_count += __popc(bits); } skip_count = warp_reduce(temp_storage.wr_storage[t / 32]).Sum(skip_count); diff --git a/cpp/src/io/orc/stripe_enc.cu b/cpp/src/io/orc/stripe_enc.cu index 3e337c432d0..5e9a6f8df6b 100644 --- a/cpp/src/io/orc/stripe_enc.cu +++ b/cpp/src/io/orc/stripe_enc.cu @@ -838,8 +838,8 @@ __global__ void __launch_bounds__(block_size) case STRING: if (s->chunk.encoding_kind == DICTIONARY_V2) { uint32_t dict_idx = s->chunk.dict_index[row]; - if (dict_idx > 0x7fffffffu) { - dict_idx = s->chunk.dict_index[dict_idx & 0x7fffffffu]; + if (dict_idx > 0x7fff'ffffu) { + dict_idx = s->chunk.dict_index[dict_idx & 0x7fff'ffffu]; } s->vals.u32[nz_idx] = dict_idx; } else { diff --git a/cpp/src/io/parquet/page_enc.cu b/cpp/src/io/parquet/page_enc.cu index 5f106e3b6a2..8181c76c065 100644 --- a/cpp/src/io/parquet/page_enc.cu +++ b/cpp/src/io/parquet/page_enc.cu @@ -443,8 +443,8 @@ __global__ void __launch_bounds__(128) *[nbits-1] */ static __device__ __constant__ uint32_t kRleRunMask[24] = { - 0x00ffffff, 0x0fff, 0x00ff, 0x3f, 0x0f, 0x0f, 0x7, 0x7, 0x3, 0x3, 0x3, 0x3, - 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}; + 0x00ff'ffff, 0x0fff, 0x00ff, 0x3f, 0x0f, 0x0f, 0x7, 0x7, 0x3, 0x3, 0x3, 0x3, + 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}; /** * @brief Variable-length encode an integer @@ -570,7 +570,7 @@ static __device__ void RleEncode( if (!(t & 0x1f)) { s->rpt_map[t >> 5] = mask; } __syncthreads(); if (t < 32) { - uint32_t c32 = ballot(t >= 4 || s->rpt_map[t] != 0xffffffffu); + uint32_t c32 = ballot(t >= 4 || s->rpt_map[t] != 0xffff'ffffu); if (!t) { uint32_t last_idx = __ffs(c32) - 1; s->rle_rpt_count = diff --git a/cpp/src/io/parquet/parquet.hpp b/cpp/src/io/parquet/parquet.hpp index a03fdf27953..046ae38020c 100644 --- a/cpp/src/io/parquet/parquet.hpp +++ b/cpp/src/io/parquet/parquet.hpp @@ -363,7 +363,7 @@ struct ColumnIndex { }; // bit space we are reserving in column_buffer::user_data -constexpr uint32_t PARQUET_COLUMN_BUFFER_SCHEMA_MASK = (0xffffff); +constexpr uint32_t PARQUET_COLUMN_BUFFER_SCHEMA_MASK = (0xff'ffffu); constexpr uint32_t PARQUET_COLUMN_BUFFER_FLAG_LIST_TERMINATED = (1 << 24); // if this column has a list parent anywhere above it in the hierarchy constexpr uint32_t PARQUET_COLUMN_BUFFER_FLAG_HAS_LIST_PARENT = (1 << 25); diff --git a/cpp/src/join/conditional_join_kernels.cuh b/cpp/src/join/conditional_join_kernels.cuh index 5231355cf30..30650f6769f 100644 --- a/cpp/src/join/conditional_join_kernels.cuh +++ b/cpp/src/join/conditional_join_kernels.cuh @@ -171,7 +171,7 @@ __global__ void conditional_join(table_device_view left_table, cudf::size_type outer_row_index = threadIdx.x + blockIdx.x * block_size; - unsigned int const activemask = __ballot_sync(0xffffffff, outer_row_index < outer_num_rows); + unsigned int const activemask = __ballot_sync(0xffff'ffffu, outer_row_index < outer_num_rows); auto evaluator = cudf::ast::detail::expression_evaluator( left_table, right_table, device_expression_data); diff --git a/cpp/src/merge/merge.cu b/cpp/src/merge/merge.cu index e4985b34c13..91018d3f006 100644 --- a/cpp/src/merge/merge.cu +++ b/cpp/src/merge/merge.cu @@ -80,7 +80,7 @@ __global__ void materialize_merged_bitmask_kernel( { size_type destination_row = threadIdx.x + blockIdx.x * blockDim.x; - auto active_threads = __ballot_sync(0xffffffff, destination_row < num_destination_rows); + auto active_threads = __ballot_sync(0xffff'ffffu, destination_row < num_destination_rows); while (destination_row < num_destination_rows) { auto const [src_side, src_row] = merged_indices[destination_row]; diff --git a/cpp/src/replace/nulls.cu b/cpp/src/replace/nulls.cu index 8ce7948eb24..2a4a3fe6d1f 100644 --- a/cpp/src/replace/nulls.cu +++ b/cpp/src/replace/nulls.cu @@ -69,7 +69,7 @@ __global__ void replace_nulls_strings(cudf::column_device_view input, cudf::thread_index_type i = blockIdx.x * blockDim.x + threadIdx.x; cudf::thread_index_type const stride = blockDim.x * gridDim.x; - uint32_t active_mask = 0xffffffff; + uint32_t active_mask = 0xffff'ffff; active_mask = __ballot_sync(active_mask, i < nrows); auto const lane_id{threadIdx.x % cudf::detail::warp_size}; uint32_t valid_sum{0}; @@ -122,7 +122,7 @@ __global__ void replace_nulls(cudf::column_device_view input, cudf::thread_index_type i = blockIdx.x * blockDim.x + threadIdx.x; cudf::thread_index_type const stride = blockDim.x * gridDim.x; - uint32_t active_mask = 0xffffffff; + uint32_t active_mask = 0xffff'ffff; active_mask = __ballot_sync(active_mask, i < nrows); auto const lane_id{threadIdx.x % cudf::detail::warp_size}; uint32_t valid_sum{0}; diff --git a/cpp/src/replace/replace.cu b/cpp/src/replace/replace.cu index 2bf16b4e240..2499d862ba5 100644 --- a/cpp/src/replace/replace.cu +++ b/cpp/src/replace/replace.cu @@ -130,7 +130,7 @@ __global__ void replace_strings_first_pass(cudf::column_device_view input, { cudf::size_type nrows = input.size(); cudf::size_type i = blockIdx.x * blockDim.x + threadIdx.x; - uint32_t active_mask = 0xffffffff; + uint32_t active_mask = 0xffff'ffffu; active_mask = __ballot_sync(active_mask, i < nrows); auto const lane_id{threadIdx.x % cudf::detail::warp_size}; uint32_t valid_sum{0}; @@ -251,7 +251,7 @@ __global__ void replace_kernel(cudf::column_device_view input, cudf::size_type i = blockIdx.x * blockDim.x + threadIdx.x; - uint32_t active_mask = 0xffffffff; + uint32_t active_mask = 0xffff'ffffu; active_mask = __ballot_sync(active_mask, i < nrows); auto const lane_id{threadIdx.x % cudf::detail::warp_size}; uint32_t valid_sum{0}; diff --git a/cpp/src/rolling/detail/rolling.cuh b/cpp/src/rolling/detail/rolling.cuh index 933d0410df5..e20ac4467aa 100644 --- a/cpp/src/rolling/detail/rolling.cuh +++ b/cpp/src/rolling/detail/rolling.cuh @@ -1032,7 +1032,7 @@ __launch_bounds__(block_size) __global__ size_type warp_valid_count{0}; - auto active_threads = __ballot_sync(0xffffffff, i < input.size()); + auto active_threads = __ballot_sync(0xffff'ffffu, i < input.size()); while (i < input.size()) { // to prevent overflow issues when computing bounds use int64_t int64_t const preceding_window = preceding_window_begin[i]; diff --git a/cpp/src/rolling/jit/kernel.cu b/cpp/src/rolling/jit/kernel.cu index 9c09e92197c..ecdbbb6a0f2 100644 --- a/cpp/src/rolling/jit/kernel.cu +++ b/cpp/src/rolling/jit/kernel.cu @@ -56,7 +56,7 @@ __global__ void gpu_rolling_new(cudf::size_type nrows, cudf::size_type warp_valid_count{0}; - auto active_threads = __ballot_sync(0xffffffff, i < nrows); + auto active_threads = __ballot_sync(0xffff'ffffu, i < nrows); while (i < nrows) { // declare this as volatile to avoid some compiler optimizations that lead to incorrect results // for CUDA 10.0 and below (fixed in CUDA 10.1) diff --git a/cpp/src/strings/capitalize.cu b/cpp/src/strings/capitalize.cu index 085c45c7897..dbe0d277033 100644 --- a/cpp/src/strings/capitalize.cu +++ b/cpp/src/strings/capitalize.cu @@ -45,7 +45,7 @@ using char_info = thrust::pair; __device__ char_info get_char_info(character_flags_table_type const* d_flags, char_utf8 chr) { auto const code_point = detail::utf8_to_codepoint(chr); - auto const flag = code_point <= 0x00FFFF ? d_flags[code_point] : character_flags_table_type{0}; + auto const flag = code_point <= 0x00'FFFF ? d_flags[code_point] : character_flags_table_type{0}; return char_info{code_point, flag}; } diff --git a/cpp/src/strings/case.cu b/cpp/src/strings/case.cu index b647ad90fea..cabb1241f1b 100644 --- a/cpp/src/strings/case.cu +++ b/cpp/src/strings/case.cu @@ -85,7 +85,7 @@ struct upper_lower_fn { for (auto itr = d_str.begin(); itr != d_str.end(); ++itr) { uint32_t code_point = detail::utf8_to_codepoint(*itr); - detail::character_flags_table_type flag = code_point <= 0x00FFFF ? d_flags[code_point] : 0; + detail::character_flags_table_type flag = code_point <= 0x00'FFFF ? d_flags[code_point] : 0; // we apply special mapping in two cases: // - uncased characters with the special mapping flag, always diff --git a/cpp/src/strings/char_types/char_types.cu b/cpp/src/strings/char_types/char_types.cu index 3713a8fa432..2feeb009ffc 100644 --- a/cpp/src/strings/char_types/char_types.cu +++ b/cpp/src/strings/char_types/char_types.cu @@ -74,7 +74,7 @@ std::unique_ptr all_characters_of_type( for (auto itr = d_str.begin(); check && (itr != d_str.end()); ++itr) { auto code_point = detail::utf8_to_codepoint(*itr); // lookup flags in table by code-point - auto flag = code_point <= 0x00FFFF ? d_flags[code_point] : 0; + auto flag = code_point <= 0x00'FFFF ? d_flags[code_point] : 0; if ((verify_types & flag) || // should flag be verified (flag == 0 && verify_types == ALL_TYPES)) // special edge case { @@ -115,7 +115,7 @@ struct filter_chars_fn { __device__ bool replace_char(char_utf8 ch) { auto const code_point = detail::utf8_to_codepoint(ch); - auto const flag = code_point <= 0x00FFFF ? d_flags[code_point] : 0; + auto const flag = code_point <= 0x00'FFFF ? d_flags[code_point] : 0; if (flag == 0) // all types pass unless specifically identified return (types_to_remove == ALL_TYPES); if (types_to_keep == ALL_TYPES) // filter case diff --git a/cpp/src/strings/convert/convert_hex.cu b/cpp/src/strings/convert/convert_hex.cu index d7fe94e1873..c327f7da00e 100644 --- a/cpp/src/strings/convert/convert_hex.cu +++ b/cpp/src/strings/convert/convert_hex.cu @@ -151,8 +151,10 @@ struct integer_to_hex_fn { return; } - auto const value = d_column.element(idx); // ex. 123456 - auto value_bytes = reinterpret_cast(&value); // 0x40E20100 + // Reinterpret an integer value as a little-endian byte sequence. + // For example, 123456 becomes 0x40E2'0100 + auto const value = d_column.element(idx); + auto value_bytes = reinterpret_cast(&value); // compute the number of output bytes int bytes = sizeof(IntegerType); diff --git a/cpp/src/strings/copying/concatenate.cu b/cpp/src/strings/copying/concatenate.cu index a9bc8c673de..627e689d4d9 100644 --- a/cpp/src/strings/copying/concatenate.cu +++ b/cpp/src/strings/copying/concatenate.cu @@ -124,7 +124,7 @@ __global__ void fused_concatenate_string_offset_kernel(column_device_view const* size_type warp_valid_count = 0; unsigned active_mask; - if (Nullable) { active_mask = __ballot_sync(0xFFFF'FFFF, output_index < output_size); } + if (Nullable) { active_mask = __ballot_sync(0xFFFF'FFFFu, output_index < output_size); } while (output_index < output_size) { // Lookup input index by searching for output index in offsets // thrust::prev isn't in CUDA 10.0, so subtracting 1 here instead diff --git a/cpp/src/strings/json/json_path.cu b/cpp/src/strings/json/json_path.cu index 1783a51ef0f..f58fac3eb65 100644 --- a/cpp/src/strings/json/json_path.cu +++ b/cpp/src/strings/json/json_path.cu @@ -912,7 +912,7 @@ __launch_bounds__(block_size) __global__ if (out_valid_count.has_value()) { *(out_valid_count.value()) = 0; } size_type warp_valid_count{0}; - auto active_threads = __ballot_sync(0xffffffff, tid < col.size()); + auto active_threads = __ballot_sync(0xffff'ffffu, tid < col.size()); while (tid < col.size()) { bool is_valid = false; string_view const str = col.element(tid); diff --git a/cpp/src/strings/regex/regex.cuh b/cpp/src/strings/regex/regex.cuh index e899c84a48d..11cc1a493a0 100644 --- a/cpp/src/strings/regex/regex.cuh +++ b/cpp/src/strings/regex/regex.cuh @@ -40,7 +40,7 @@ using match_pair = thrust::pair; using match_result = thrust::optional; constexpr int32_t MAX_SHARED_MEM = 2048; ///< Memory size for storing prog instruction data -constexpr std::size_t MAX_WORKING_MEM = 0x01FFFFFFFF; ///< Memory size for state data +constexpr std::size_t MAX_WORKING_MEM = 0x01'FFFF'FFFF; ///< Memory size for state data constexpr int32_t MINIMUM_THREADS = 256; // Minimum threads for computing working memory /** diff --git a/cpp/src/strings/regex/regex.inl b/cpp/src/strings/regex/regex.inl index f2da405eeec..89dad84e45c 100644 --- a/cpp/src/strings/regex/regex.inl +++ b/cpp/src/strings/regex/regex.inl @@ -148,7 +148,7 @@ __device__ __forceinline__ bool reclass_device::is_match(char32_t const ch, if (!builtins) return false; uint32_t codept = utf8_to_codepoint(ch); - if (codept > 0x00FFFF) return false; + if (codept > 0x00'FFFF) return false; int8_t fl = codepoint_flags[codept]; if ((builtins & CCLASS_W) && ((ch == '_') || IS_ALPHANUM(fl))) // \w return true; diff --git a/cpp/src/text/normalize.cu b/cpp/src/text/normalize.cu index 49c4d094e3b..48921ac6520 100644 --- a/cpp/src/text/normalize.cu +++ b/cpp/src/text/normalize.cu @@ -97,7 +97,7 @@ struct normalize_spaces_fn { // code-point to multi-byte range limits constexpr uint32_t UTF8_1BYTE = 0x0080; constexpr uint32_t UTF8_2BYTE = 0x0800; -constexpr uint32_t UTF8_3BYTE = 0x010000; +constexpr uint32_t UTF8_3BYTE = 0x01'0000; /** * @brief Convert code-point arrays into UTF-8 bytes for each string. @@ -148,20 +148,19 @@ struct codepoint_to_utf8_fn { *out_ptr++ = static_cast(code_point); else if (code_point < UTF8_2BYTE) { // create two-byte UTF-8 // b00001xxx:byyyyyyyy => b110xxxyy:b10yyyyyy - *out_ptr++ = static_cast((((code_point << 2) & 0x001F00) | 0x00C000) >> 8); + *out_ptr++ = static_cast((((code_point << 2) & 0x00'1F00) | 0x00'C000) >> 8); *out_ptr++ = static_cast((code_point & 0x3F) | 0x0080); } else if (code_point < UTF8_3BYTE) { // create three-byte UTF-8 // bxxxxxxxx:byyyyyyyy => b1110xxxx:b10xxxxyy:b10yyyyyy - *out_ptr++ = static_cast((((code_point << 4) & 0x0F0000) | 0x00E00000) >> 16); - *out_ptr++ = static_cast((((code_point << 2) & 0x003F00) | 0x008000) >> 8); + *out_ptr++ = static_cast((((code_point << 4) & 0x0F'0000) | 0x00E0'0000) >> 16); + *out_ptr++ = static_cast((((code_point << 2) & 0x00'3F00) | 0x00'8000) >> 8); *out_ptr++ = static_cast((code_point & 0x3F) | 0x0080); } else { // create four-byte UTF-8 - // maximum code-point value is 0x00110000 + // maximum code-point value is 0x0011'0000 // b000xxxxx:byyyyyyyy:bzzzzzzzz => b11110xxx:b10xxyyyy:b10yyyyzz:b10zzzzzz - *out_ptr++ = - static_cast((((code_point << 6) & 0x07000000) | unsigned{0xF0000000}) >> 24); - *out_ptr++ = static_cast((((code_point << 4) & 0x003F0000) | 0x00800000) >> 16); - *out_ptr++ = static_cast((((code_point << 2) & 0x003F00) | 0x008000) >> 8); + *out_ptr++ = static_cast((((code_point << 6) & 0x0700'0000u) | 0xF000'0000u) >> 24); + *out_ptr++ = static_cast((((code_point << 4) & 0x003F'0000u) | 0x0080'0000u) >> 16); + *out_ptr++ = static_cast((((code_point << 2) & 0x00'3F00u) | 0x00'8000u) >> 8); *out_ptr++ = static_cast((code_point & 0x3F) | 0x0080); } } diff --git a/cpp/src/text/subword/detail/codepoint_metadata.ah b/cpp/src/text/subword/detail/codepoint_metadata.ah index bfff2aee91f..bc56d6c4ba5 100644 --- a/cpp/src/text/subword/detail/codepoint_metadata.ah +++ b/cpp/src/text/subword/detail/codepoint_metadata.ah @@ -22,9 +22,9 @@ // it is broken into pieces since only 10% of the values are unique // some magic numbers -constexpr uint32_t codepoint_metadata_size = 1114112; // 0x110000 +constexpr uint32_t codepoint_metadata_size = 1114112; // 0x11'0000 constexpr uint32_t aux_codepoint_data_size = 119233; -constexpr uint32_t codepoint_metadata_default_value = 83886080; // 0x05000000 +constexpr uint32_t codepoint_metadata_default_value = 83886080; // 0x0500'0000 constexpr uint32_t aux_codepoint_default_value = 0; constexpr uint32_t cp_section1_end = 195104; constexpr uint32_t cp_section2_begin = 917505; diff --git a/cpp/src/text/subword/detail/cp_data.h b/cpp/src/text/subword/detail/cp_data.h index 14a89d2936e..189ef14b809 100644 --- a/cpp/src/text/subword/detail/cp_data.h +++ b/cpp/src/text/subword/detail/cp_data.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2022, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ #include -constexpr uint32_t NEW_CP_MASK = 0x1fffff; +constexpr uint32_t NEW_CP_MASK = 0x1f'ffffu; constexpr uint32_t MULTICHAR_SHIFT = 23; constexpr uint32_t MULTICHAR_MASK = 1; diff --git a/cpp/tests/device_atomics/device_atomics_test.cu b/cpp/tests/device_atomics/device_atomics_test.cu index 363eece012a..d9788388fb1 100644 --- a/cpp/tests/device_atomics/device_atomics_test.cu +++ b/cpp/tests/device_atomics/device_atomics_test.cu @@ -331,12 +331,12 @@ TYPED_TEST(AtomicsBitwiseOpTest, atomicBitwiseOps) { { // test for AND, XOR std::vector input_array( - {0xfcfcfcfcfcfcfc7f, 0x7f7f7f7f7f7ffc, 0xfffddffddffddfdf, 0x7f7f7f7f7f7ffc}); + {0xfcfc'fcfc'fcfc'fc7f, 0x7f'7f7f'7f7f'7ffc, 0xfffd'dffd'dffd'dfdf, 0x7f'7f7f'7f7f'7ffc}); this->atomic_test(input_array); } { // test for OR, XOR std::vector input_array( - {0x01, 0xfc02, 0x1dff03, 0x1100a0b0801d0003, 0x8000000000000000, 0x1dff03}); + {0x01, 0xfc02, 0x1d'ff03, 0x1100'a0b0'801d'0003, 0x8000'0000'0000'0000, 0x1d'ff03}); this->atomic_test(input_array); } } diff --git a/cpp/tests/hashing/hash_test.cpp b/cpp/tests/hashing/hash_test.cpp index 6148ea85b0f..baa7ba07ee4 100644 --- a/cpp/tests/hashing/hash_test.cpp +++ b/cpp/tests/hashing/hash_test.cpp @@ -540,12 +540,12 @@ TEST_F(SparkMurmurHash3Test, MultiValueWithSeeds) using long_limits = std::numeric_limits; using float_limits = std::numeric_limits; using int_limits = std::numeric_limits; - fixed_width_column_wrapper a_col{0, 100, -100, 0x12345678, -0x76543210}; + fixed_width_column_wrapper a_col{0, 100, -100, 0x1234'5678, -0x7654'3210}; strings_column_wrapper b_col{"a", "bc", "def", "ghij", "klmno"}; fixed_width_column_wrapper x_col{ 0.f, 100.f, -100.f, float_limits::infinity(), -float_limits::infinity()}; fixed_width_column_wrapper y_col{ - 0L, 100L, -100L, 0x123456789abcdefL, -0x123456789abcdefL}; + 0L, 100L, -100L, 0x0123'4567'89ab'cdefL, -0x0123'4567'89ab'cdefL}; structs_column_wrapper c_col{{x_col, y_col}}; structs_column_wrapper const structs_col{{a_col, b_col, c_col}}; @@ -578,8 +578,8 @@ TEST_F(SparkMurmurHash3Test, MultiValueWithSeeds) {static_cast<__int128>(0), static_cast<__int128>(100), static_cast<__int128>(-1), - (static_cast<__int128>(0xFFFFFFFFFCC4D1C3u) << 64 | 0x602F7FC318000001u), - (static_cast<__int128>(0x0785EE10D5DA46D9u) << 64 | 0x00F4369FFFFFFFFFu)}, + (static_cast<__int128>(0xFFFF'FFFF'FCC4'D1C3u) << 64 | 0x602F'7FC3'1800'0001u), + (static_cast<__int128>(0x0785'EE10'D5DA'46D9u) << 64 | 0x00F4'369F'FFFF'FFFFu)}, numeric::scale_type{-11}); constexpr auto hasher = cudf::hash_id::HASH_SPARK_MURMUR3; diff --git a/cpp/tests/io/parquet_test.cpp b/cpp/tests/io/parquet_test.cpp index 77a03cd5502..8dbd1dd0c0e 100644 --- a/cpp/tests/io/parquet_test.cpp +++ b/cpp/tests/io/parquet_test.cpp @@ -3294,7 +3294,7 @@ TEST_F(ParquetWriterTest, Decimal128Stats) std::vector expected_max{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6}; - __int128_t val0 = 0xa1b2c3d4e5f6ULL; + __int128_t val0 = 0xa1b2'c3d4'e5f6ULL; __int128_t val1 = val0 << 80; column_wrapper col0{{numeric::decimal128(val0, numeric::scale_type{0}), numeric::decimal128(val1, numeric::scale_type{0})}}; diff --git a/java/src/main/native/src/row_conversion.cu b/java/src/main/native/src/row_conversion.cu index f5a4d8638b8..84e6f85ca3d 100644 --- a/java/src/main/native/src/row_conversion.cu +++ b/java/src/main/native/src/row_conversion.cu @@ -360,7 +360,7 @@ copy_from_rows_fixed_width_optimized(const size_type num_rows, const size_type n // But we might not use all of the threads if the number of rows does not go // evenly into the thread count. We don't want those threads to exit yet // because we may need them to copy data in for the next row group. - uint32_t active_mask = __ballot_sync(0xffffffff, row_index < num_rows); + uint32_t active_mask = __ballot_sync(0xffff'ffffu, row_index < num_rows); if (row_index < num_rows) { auto const col_index_start = threadIdx.y; auto const col_index_stride = blockDim.y; @@ -753,7 +753,7 @@ copy_validity_to_rows(const size_type num_rows, const size_type num_columns, auto const absolute_col = relative_col + tile.start_col; auto const absolute_row = relative_row + tile.start_row; auto const participating = absolute_col < num_columns && absolute_row < num_rows; - auto const participation_mask = __ballot_sync(0xFFFFFFFF, participating); + auto const participation_mask = __ballot_sync(0xFFFF'FFFFu, participating); if (participating) { auto my_data = input_nm[absolute_col] != nullptr ? @@ -1072,7 +1072,7 @@ copy_validity_from_rows(const size_type num_rows, const size_type num_columns, auto const row_batch_start = tile.batch_number == 0 ? 0 : batch_row_boundaries[tile.batch_number]; - auto const participation_mask = __ballot_sync(0xFFFFFFFF, absolute_row < num_rows); + auto const participation_mask = __ballot_sync(0xFFFF'FFFFu, absolute_row < num_rows); if (absolute_row < num_rows) { auto const my_byte = input_data[row_offsets(absolute_row, row_batch_start) + validity_offset + From 65a782112f4b76941483adf17f9a30a6824f6164 Mon Sep 17 00:00:00 2001 From: Mike Wilson Date: Tue, 16 Aug 2022 17:40:29 -0400 Subject: [PATCH 41/58] Removing unnecessary asserts in parquet tests (#11544) As noticed in review of #11524 there are unnecessary asserts in the parquet tests. This removes those. closes #11541 Authors: - Mike Wilson (https://github.com/hyperbolic2346) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/11544 --- cpp/tests/io/parquet_test.cpp | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/cpp/tests/io/parquet_test.cpp b/cpp/tests/io/parquet_test.cpp index 8dbd1dd0c0e..c218c4088bb 100644 --- a/cpp/tests/io/parquet_test.cpp +++ b/cpp/tests/io/parquet_test.cpp @@ -397,7 +397,6 @@ TYPED_TEST(ParquetWriterNumericTypeTest, SingleColumn) column_wrapper col(sequence, sequence + num_rows, validity); auto expected = table_view{{col}}; - EXPECT_EQ(1, expected.num_columns()); auto filepath = temp_env->get_temp_filepath("SingleColumn.parquet"); cudf_io::parquet_writer_options out_opts = @@ -421,7 +420,6 @@ TYPED_TEST(ParquetWriterNumericTypeTest, SingleColumnWithNulls) column_wrapper col(sequence, sequence + num_rows, validity); auto expected = table_view{{col}}; - EXPECT_EQ(1, expected.num_columns()); auto filepath = temp_env->get_temp_filepath("SingleColumnWithNulls.parquet"); cudf_io::parquet_writer_options out_opts = @@ -446,7 +444,6 @@ TYPED_TEST(ParquetWriterChronoTypeTest, Chronos) sequence, sequence + num_rows, validity); auto expected = table_view{{col}}; - EXPECT_EQ(1, expected.num_columns()); auto filepath = temp_env->get_temp_filepath("Chronos.parquet"); cudf_io::parquet_writer_options out_opts = @@ -473,7 +470,6 @@ TYPED_TEST(ParquetWriterChronoTypeTest, ChronosWithNulls) sequence, sequence + num_rows, validity); auto expected = table_view{{col}}; - EXPECT_EQ(1, expected.num_columns()); auto filepath = temp_env->get_temp_filepath("ChronosWithNulls.parquet"); cudf_io::parquet_writer_options out_opts = @@ -619,7 +615,6 @@ TEST_F(ParquetWriterTest, MultiColumnWithNulls) column_wrapper col7{col7_data, col7_data + num_rows, col7_mask}; auto expected = table_view{{/*col0, */ col1, col2, col3, col4, col5, col6, col7}}; - EXPECT_EQ(7, expected.num_columns()); cudf_io::table_input_metadata expected_metadata(expected); // expected_metadata.column_names.emplace_back("bools"); @@ -664,7 +659,6 @@ TEST_F(ParquetWriterTest, Strings) column_wrapper col2{seq_col2.begin(), seq_col2.end(), validity}; auto expected = table_view{{col0, col1, col2}}; - EXPECT_EQ(3, expected.num_columns()); cudf_io::table_input_metadata expected_metadata(expected); expected_metadata.column_metadata[0].set_name("col_other"); @@ -714,7 +708,6 @@ TEST_F(ParquetWriterTest, StringsAsBinary) {'F', 'u', 'n', 'd', 'a', 'y'}}; auto write_tbl = table_view{{col0, col1, col2, col3, col4}}; - EXPECT_EQ(5, write_tbl.num_columns()); cudf_io::table_input_metadata expected_metadata(write_tbl); expected_metadata.column_metadata[0].set_name("col_single").set_output_as_binary(true); @@ -974,7 +967,6 @@ TEST_F(ParquetWriterTest, MultiIndex) column_wrapper col4{col4_data.begin(), col4_data.end(), validity}; auto expected = table_view{{col0, col1, col2, col3, col4}}; - EXPECT_EQ(5, expected.num_columns()); cudf_io::table_input_metadata expected_metadata(expected); expected_metadata.column_metadata[0].set_name("int8s"); @@ -1010,7 +1002,6 @@ TEST_F(ParquetWriterTest, HostBuffer) column_wrapper col{seq_col.begin(), seq_col.end(), validity}; const auto expected = table_view{{col}}; - EXPECT_EQ(1, expected.num_columns()); cudf_io::table_input_metadata expected_metadata(expected); expected_metadata.column_metadata[0].set_name("col_other"); @@ -3259,7 +3250,6 @@ TEST_F(ParquetWriterTest, CheckPageRows) column_wrapper col(sequence, sequence + num_rows, validity); auto expected = table_view{{col}}; - EXPECT_EQ(1, expected.num_columns()); auto const filepath = temp_env->get_temp_filepath("CheckPageRows.parquet"); const cudf::io::parquet_writer_options out_opts = @@ -4022,7 +4012,6 @@ TEST_F(ParquetReaderTest, BinaryAsStrings) {'F', 'u', 'n', 'd', 'a', 'y'}}; auto output = table_view{{int_col, string_col, float_col, string_col, list_int_col}}; - EXPECT_EQ(5, output.num_columns()); cudf_io::table_input_metadata output_metadata(output); output_metadata.column_metadata[0].set_name("col_other"); output_metadata.column_metadata[1].set_name("col_string"); @@ -4037,10 +4026,7 @@ TEST_F(ParquetReaderTest, BinaryAsStrings) cudf_io::write_parquet(out_opts); auto expected_string = table_view{{int_col, string_col, float_col, string_col, string_col}}; - EXPECT_EQ(5, expected_string.num_columns()); - - auto expected_mixed = table_view{{int_col, string_col, float_col, list_int_col, list_int_col}}; - EXPECT_EQ(5, expected_mixed.num_columns()); + auto expected_mixed = table_view{{int_col, string_col, float_col, list_int_col, list_int_col}}; cudf_io::parquet_reader_options in_opts = cudf_io::parquet_reader_options::builder(cudf_io::source_info{filepath}) @@ -4103,7 +4089,6 @@ TEST_F(ParquetReaderTest, NestedByteArray) {{'M', 'o', 'n', 'd', 'a', 'y'}, {'F', 'r', 'i', 'd', 'a', 'y'}}}; auto const expected = table_view{{int_col, float_col, list_list_int_col}}; - EXPECT_EQ(3, expected.num_columns()); cudf_io::table_input_metadata output_metadata(expected); output_metadata.column_metadata[0].set_name("col_other"); output_metadata.column_metadata[1].set_name("col_float"); @@ -4150,7 +4135,6 @@ TEST_F(ParquetWriterTest, ByteArrayStats) {0xfe, 0xfe, 0xfe}, {0xfe, 0xfe, 0xfe}, {0xfe, 0xfe, 0xfe}}; auto expected = table_view{{list_int_col0, list_int_col1}}; - EXPECT_EQ(2, expected.num_columns()); cudf_io::table_input_metadata output_metadata(expected); output_metadata.column_metadata[0].set_name("col_binary0").set_output_as_binary(true); output_metadata.column_metadata[1].set_name("col_binary1").set_output_as_binary(true); From abd4302d6d57c080323068f7b58f26b2d76e26fb Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Wed, 17 Aug 2022 09:39:25 -0700 Subject: [PATCH 42/58] Use the new JSON parser when the experimental reader is selected (#11364) Core changes: - Implement the data ingest for the experimental JSON reader. - Call the new JSON parser when the option/flag to use the experimental implementation is set. - Modify C++ and Python tests so they don't expect an exception and check the output instead. Additional fix: - Return the vector of root columns' names from the JSON reader (along with the nested column info) to conform to the current Cython implementation. Info in these structures is redundant and can be removed in the future. Marked as breaking only because the experimental path does not throw any more. No changes in behavior when the experimental option is not selected. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Jason Lowe (https://github.com/jlowe) - Bradley Dice (https://github.com/bdice) - Elias Stehle (https://github.com/elstehle) - Matthew Roeschke (https://github.com/mroeschke) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/11364 --- cpp/include/cudf/io/types.hpp | 1 + cpp/src/io/json/experimental/read_json.cpp | 34 +++++++++- cpp/src/io/json/reader_impl.cu | 19 ++++-- cpp/tests/io/json_test.cpp | 75 +++++++++++++--------- java/src/main/native/src/TableJni.cpp | 8 +-- python/cudf/cudf/_lib/json.pyx | 2 +- python/cudf/cudf/tests/test_json.py | 15 +++++ 7 files changed, 112 insertions(+), 42 deletions(-) diff --git a/cpp/include/cudf/io/types.hpp b/cpp/include/cudf/io/types.hpp index e9cc7ea99f7..3cba2b428ca 100644 --- a/cpp/include/cudf/io/types.hpp +++ b/cpp/include/cudf/io/types.hpp @@ -21,6 +21,7 @@ #pragma once +#include #include #include diff --git a/cpp/src/io/json/experimental/read_json.cpp b/cpp/src/io/json/experimental/read_json.cpp index 146eaf203e4..e070aacaca2 100644 --- a/cpp/src/io/json/experimental/read_json.cpp +++ b/cpp/src/io/json/experimental/read_json.cpp @@ -16,16 +16,48 @@ #include "read_json.hpp" +#include +#include + #include +#include + namespace cudf::io::detail::json::experimental { +std::vector ingest_raw_input(host_span> sources, + compression_type compression) +{ + auto const total_source_size = + std::accumulate(sources.begin(), sources.end(), 0ul, [](size_t sum, auto& source) { + return sum + source->size(); + }); + auto buffer = std::vector(total_source_size); + + size_t bytes_read = 0; + for (const auto& source : sources) { + bytes_read += source->host_read(0, source->size(), buffer.data() + bytes_read); + } + + return (compression == compression_type::NONE) ? buffer : decompress(compression, buffer); +} + table_with_metadata read_json(host_span> sources, json_reader_options const& reader_opts, rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr) { - CUDF_FAIL("Not implemented"); + auto const dtypes_empty = + std::visit([](const auto& dtypes) { return dtypes.empty(); }, reader_opts.get_dtypes()); + CUDF_EXPECTS(dtypes_empty, "user specified dtypes are not yet supported"); + CUDF_EXPECTS(not reader_opts.is_enabled_lines(), "JSON Lines format is not yet supported"); + CUDF_EXPECTS(reader_opts.get_byte_range_offset() == 0 and reader_opts.get_byte_range_size() == 0, + "specifying a byte range is not yet supported"); + + auto const buffer = ingest_raw_input(sources, reader_opts.get_compression()); + auto data = host_span(reinterpret_cast(buffer.data()), buffer.size()); + + return cudf::io::json::detail::parse_nested_json(data, stream, mr); } } // namespace cudf::io::detail::json::experimental diff --git a/cpp/src/io/json/reader_impl.cu b/cpp/src/io/json/reader_impl.cu index 8e10dc2c9b4..da6e7621449 100644 --- a/cpp/src/io/json/reader_impl.cu +++ b/cpp/src/io/json/reader_impl.cu @@ -480,7 +480,7 @@ std::vector get_data_types(json_reader_options const& reader_opts, table_with_metadata convert_data_to_table(parse_options_view const& parse_opts, std::vector const& dtypes, - std::vector const& column_names, + std::vector&& column_names, col_map_type* column_map, device_span rec_starts, device_span data, @@ -552,8 +552,8 @@ table_with_metadata convert_data_to_table(parse_options_view const& parse_opts, std::vector column_infos; column_infos.reserve(column_names.size()); - std::transform(column_names.cbegin(), - column_names.cend(), + std::transform(std::make_move_iterator(column_names.begin()), + std::make_move_iterator(column_names.end()), std::back_inserter(column_infos), [](auto const& col_name) { return column_name_info{col_name}; }); @@ -563,8 +563,7 @@ table_with_metadata convert_data_to_table(parse_options_view const& parse_opts, CUDF_EXPECTS(!out_columns.empty(), "No columns created from json input"); - return table_with_metadata{std::make_unique
(std::move(out_columns)), - {column_names, column_infos}}; + return table_with_metadata{std::make_unique
(std::move(out_columns)), {{}, column_infos}}; } /** @@ -636,8 +635,14 @@ table_with_metadata read_json(std::vector>& sources, CUDF_EXPECTS(not dtypes.empty(), "Error in data type detection.\n"); - return convert_data_to_table( - parse_opts.view(), dtypes, column_names, column_map.get(), rec_starts, d_data, stream, mr); + return convert_data_to_table(parse_opts.view(), + dtypes, + std::move(column_names), + column_map.get(), + rec_starts, + d_data, + stream, + mr); } } // namespace json diff --git a/cpp/tests/io/json_test.cpp b/cpp/tests/io/json_test.cpp index c8aefece94f..67f0542ace2 100644 --- a/cpp/tests/io/json_test.cpp +++ b/cpp/tests/io/json_test.cpp @@ -171,8 +171,8 @@ TEST_F(JsonReaderTest, BasicJsonLines) EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::INT32); EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::FLOAT64); - EXPECT_EQ(result.metadata.column_names[0], "0"); - EXPECT_EQ(result.metadata.column_names[1], "1"); + EXPECT_EQ(result.metadata.schema_info[0].name, "0"); + EXPECT_EQ(result.metadata.schema_info[1].name, "1"); auto validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); @@ -228,9 +228,9 @@ TEST_F(JsonReaderTest, JsonLinesStrings) EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::FLOAT64); EXPECT_EQ(result.tbl->get_column(2).type().id(), cudf::type_id::STRING); - EXPECT_EQ(result.metadata.column_names[0], "0"); - EXPECT_EQ(result.metadata.column_names[1], "1"); - EXPECT_EQ(result.metadata.column_names[2], "2"); + EXPECT_EQ(result.metadata.schema_info[0].name, "0"); + EXPECT_EQ(result.metadata.schema_info[1].name, "1"); + EXPECT_EQ(result.metadata.schema_info[2].name, "2"); auto validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); @@ -414,9 +414,9 @@ TEST_F(JsonReaderTest, JsonLinesDtypeInference) EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::FLOAT64); EXPECT_EQ(result.tbl->get_column(2).type().id(), cudf::type_id::STRING); - EXPECT_EQ(std::string(result.metadata.column_names[0]), "0"); - EXPECT_EQ(std::string(result.metadata.column_names[1]), "1"); - EXPECT_EQ(std::string(result.metadata.column_names[2]), "2"); + EXPECT_EQ(result.metadata.schema_info[0].name, "0"); + EXPECT_EQ(result.metadata.schema_info[1].name, "1"); + EXPECT_EQ(result.metadata.schema_info[2].name, "2"); auto validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); @@ -444,8 +444,8 @@ TEST_F(JsonReaderTest, JsonLinesFileInput) EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::INT64); EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::FLOAT64); - EXPECT_EQ(std::string(result.metadata.column_names[0]), "0"); - EXPECT_EQ(std::string(result.metadata.column_names[1]), "1"); + EXPECT_EQ(result.metadata.schema_info[0].name, "0"); + EXPECT_EQ(result.metadata.schema_info[1].name, "1"); auto validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); @@ -472,7 +472,7 @@ TEST_F(JsonReaderTest, JsonLinesByteRange) EXPECT_EQ(result.tbl->num_rows(), 3); EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::INT64); - EXPECT_EQ(std::string(result.metadata.column_names[0]), "0"); + EXPECT_EQ(result.metadata.schema_info[0].name, "0"); auto validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); @@ -496,9 +496,9 @@ TEST_F(JsonReaderTest, JsonLinesObjects) EXPECT_EQ(result.tbl->num_rows(), 1); EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::INT64); - EXPECT_EQ(std::string(result.metadata.column_names[0]), "co\\\"l1"); + EXPECT_EQ(result.metadata.schema_info[0].name, "co\\\"l1"); EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::FLOAT64); - EXPECT_EQ(std::string(result.metadata.column_names[1]), "col2"); + EXPECT_EQ(result.metadata.schema_info[1].name, "col2"); auto validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); @@ -522,9 +522,9 @@ TEST_F(JsonReaderTest, JsonLinesObjectsStrings) EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::FLOAT64); EXPECT_EQ(result.tbl->get_column(2).type().id(), cudf::type_id::STRING); - EXPECT_EQ(std::string(result.metadata.column_names[0]), "col1"); - EXPECT_EQ(std::string(result.metadata.column_names[1]), "col2"); - EXPECT_EQ(std::string(result.metadata.column_names[2]), "col3"); + EXPECT_EQ(result.metadata.schema_info[0].name, "col1"); + EXPECT_EQ(result.metadata.schema_info[1].name, "col2"); + EXPECT_EQ(result.metadata.schema_info[2].name, "col3"); auto validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); @@ -563,9 +563,9 @@ TEST_F(JsonReaderTest, JsonLinesObjectsMissingData) EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::STRING); EXPECT_EQ(result.tbl->get_column(2).type().id(), cudf::type_id::FLOAT64); - EXPECT_EQ(std::string(result.metadata.column_names[0]), "col2"); - EXPECT_EQ(std::string(result.metadata.column_names[1]), "col3"); - EXPECT_EQ(std::string(result.metadata.column_names[2]), "col1"); + EXPECT_EQ(result.metadata.schema_info[0].name, "col2"); + EXPECT_EQ(result.metadata.schema_info[1].name, "col3"); + EXPECT_EQ(result.metadata.schema_info[2].name, "col1"); auto col1_validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return i != 0; }); @@ -598,9 +598,9 @@ TEST_F(JsonReaderTest, JsonLinesObjectsOutOfOrder) EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::INT64); EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::FLOAT64); - EXPECT_EQ(std::string(result.metadata.column_names[0]), "col1"); - EXPECT_EQ(std::string(result.metadata.column_names[1]), "col2"); - EXPECT_EQ(std::string(result.metadata.column_names[2]), "col3"); + EXPECT_EQ(result.metadata.schema_info[0].name, "col1"); + EXPECT_EQ(result.metadata.schema_info[1].name, "col2"); + EXPECT_EQ(result.metadata.schema_info[2].name, "col3"); auto validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); @@ -881,8 +881,8 @@ TEST_F(JsonReaderTest, JsonLinesMultipleFileInputs) EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::INT64); EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::FLOAT64); - EXPECT_EQ(std::string(result.metadata.column_names[0]), "0"); - EXPECT_EQ(std::string(result.metadata.column_names[1]), "1"); + EXPECT_EQ(result.metadata.schema_info[0].name, "0"); + EXPECT_EQ(result.metadata.schema_info[1].name, "1"); auto validity = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return true; }); @@ -915,13 +915,30 @@ TEST_F(JsonReaderTest, BadDtypeParams) EXPECT_THROW(cudf_io::read_json(options_map), cudf::logic_error); } -TEST_F(JsonReaderTest, ExperimentalParam) +TEST_F(JsonReaderTest, JsonExperimentalBasic) { - cudf_io::json_reader_options const options = - cudf_io::json_reader_options::builder(cudf_io::source_info{nullptr, 0}).experimental(true); + std::string const fname = temp_env->get_temp_dir() + "JsonExperimentalBasic.json"; + std::ofstream outfile(fname, std::ofstream::out); + outfile << R"([{"a":"11", "b":"1.1"},{"a":"22", "b":"2.2"}])"; + outfile.close(); + + cudf_io::json_reader_options options = + cudf_io::json_reader_options::builder(cudf_io::source_info{fname}).experimental(true); + auto result = cudf_io::read_json(options); + + EXPECT_EQ(result.tbl->num_columns(), 2); + EXPECT_EQ(result.tbl->num_rows(), 2); - // should throw for now - EXPECT_THROW(cudf_io::read_json(options), cudf::logic_error); + EXPECT_EQ(result.tbl->get_column(0).type().id(), cudf::type_id::STRING); + EXPECT_EQ(result.tbl->get_column(1).type().id(), cudf::type_id::STRING); + + EXPECT_EQ(result.metadata.schema_info[0].name, "a"); + EXPECT_EQ(result.metadata.schema_info[1].name, "b"); + + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(0), + cudf::test::strings_column_wrapper({"11", "22"})); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(result.tbl->get_column(1), + cudf::test::strings_column_wrapper({"1.1", "2.2"})); } CUDF_TEST_PROGRAM_MAIN() diff --git a/java/src/main/native/src/TableJni.cpp b/java/src/main/native/src/TableJni.cpp index 4bc0a2c3b76..52da3b96491 100644 --- a/java/src/main/native/src/TableJni.cpp +++ b/java/src/main/native/src/TableJni.cpp @@ -1459,7 +1459,7 @@ JNIEXPORT jlongArray JNICALL Java_ai_rapids_cudf_Table_readJSON( cudf::io::table_with_metadata result = cudf::io::read_json(opts.build()); // there is no need to re-order columns when inferring schema - if (result.metadata.column_names.empty() || n_col_names.size() <= 0) { + if (result.metadata.schema_info.empty() || n_col_names.size() <= 0) { return convert_table_for_return(env, result.tbl); } else { // json reader will not return the correct column order, @@ -1467,10 +1467,10 @@ JNIEXPORT jlongArray JNICALL Java_ai_rapids_cudf_Table_readJSON( // turn name and its index in table into map std::map m; - std::transform(result.metadata.column_names.begin(), result.metadata.column_names.end(), + std::transform(result.metadata.schema_info.cbegin(), result.metadata.schema_info.cend(), thrust::make_counting_iterator(0), std::inserter(m, m.end()), - [](auto const &column_name, auto const &index) { - return std::make_pair(column_name, index); + [](auto const &column_info, auto const &index) { + return std::make_pair(column_info.name, index); }); auto col_names_vec = n_col_names.as_cpp_vector(); diff --git a/python/cudf/cudf/_lib/json.pyx b/python/cudf/cudf/_lib/json.pyx index 0ee6062e7f2..376850b7b1b 100644 --- a/python/cudf/cudf/_lib/json.pyx +++ b/python/cudf/cudf/_lib/json.pyx @@ -113,7 +113,7 @@ cpdef read_json(object filepaths_or_buffers, with nogil: c_result = move(libcudf_read_json(opts)) - meta_names = [name.decode() for name in c_result.metadata.column_names] + meta_names = [info.name.decode() for info in c_result.metadata.schema_info] df = cudf.DataFrame._from_data(*data_from_unique_ptr( move(c_result.tbl), column_names=meta_names diff --git a/python/cudf/cudf/tests/test_json.py b/python/cudf/cudf/tests/test_json.py index 800ed68e8a4..368015cf563 100644 --- a/python/cudf/cudf/tests/test_json.py +++ b/python/cudf/cudf/tests/test_json.py @@ -579,3 +579,18 @@ def test_json_experimental(): # should raise an exception, for now with pytest.raises(RuntimeError): cudf.read_json("", engine="cudf_experimental") + + +def test_json_nested_basic(tmpdir): + fname = tmpdir.mkdir("gdf_json").join("tmp_json_nested_basic") + data = { + "c1": [{"f1": "sf11", "f2": "sf21"}, {"f1": "sf12", "f2": "sf22"}], + "c2": [["l11", "l21"], ["l12", "l22"]], + } + pdf = pd.DataFrame(data) + pdf.to_json(fname, orient="records") + + df = cudf.read_json(fname, engine="cudf_experimental", orient="records") + pdf = pd.read_json(fname, orient="records") + + assert_eq(pdf, df) From e6191dafa3f8ad9c79ecff37d093744fc9d16c35 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 17 Aug 2022 15:18:39 -0500 Subject: [PATCH 43/58] Reuse MurmurHash3_32 in Parquet page data. (#11528) This removes an implementation of a hashing function identical to the `MurmurHash3_32` used elsewhere in libcudf. This removes the re-implementation and instead uses the common hashing function code. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Nghia Truong (https://github.com/ttnghia) - Mike Wilson (https://github.com/hyperbolic2346) URL: https://github.com/rapidsai/cudf/pull/11528 --- cpp/src/io/parquet/page_data.cu | 65 ++++----------------------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/cpp/src/io/parquet/page_data.cu b/cpp/src/io/parquet/page_data.cu index 3e71c3d1a07..424882f45bf 100644 --- a/cpp/src/io/parquet/page_data.cu +++ b/cpp/src/io/parquet/page_data.cu @@ -19,6 +19,8 @@ #include #include +#include +#include #include #include @@ -88,62 +90,6 @@ struct page_state_s { int32_t lvl_count[NUM_LEVEL_TYPES]; // how many of each of the streams we've decoded }; -/** - * @brief Computes a 32-bit hash when given a byte stream and range. - * - * MurmurHash3_32 implementation from - * https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp - * - * MurmurHash3 was written by Austin Appleby, and is placed in the public - * domain. The author hereby disclaims copyright to this source code. - * - * @param[in] key The input data to hash - * @param[in] len The length of the input data - * @param[in] seed An initialization value - * - * @return The hash value - */ -__device__ uint32_t device_str2hash32(const char* key, size_t len, uint32_t seed = 33) -{ - const auto* p = reinterpret_cast(key); - uint32_t h1 = seed, k1; - const uint32_t c1 = 0xcc9e2d51; - const uint32_t c2 = 0x1b873593; - int l = len; - // body - while (l >= 4) { - k1 = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); - k1 *= c1; - k1 = rotl32(k1, 15); - k1 *= c2; - h1 ^= k1; - h1 = rotl32(h1, 13); - h1 = h1 * 5 + 0xe6546b64; - p += 4; - l -= 4; - } - // tail - k1 = 0; - switch (l) { - case 3: k1 ^= p[2] << 16; - case 2: k1 ^= p[1] << 8; - case 1: - k1 ^= p[0]; - k1 *= c1; - k1 = rotl32(k1, 15); - k1 *= c2; - h1 ^= k1; - } - // finalization - h1 ^= len; - h1 ^= h1 >> 16; - h1 *= 0x85ebca6b; - h1 ^= h1 >> 13; - h1 *= 0xc2b2ae35; - h1 ^= h1 >> 16; - return h1; -} - /** * @brief Read a 32-bit varint integer * @@ -538,8 +484,11 @@ inline __device__ void gpuOutputString(volatile page_state_s* s, int src_pos, vo } } if (s->dtype_len == 4) { - // Output hash - *static_cast(dstv) = device_str2hash32(ptr, len); + // Output hash. This hash value is used if the option to convert strings to + // categoricals is enabled. The seed value is chosen arbitrarily. + uint32_t constexpr hash_seed = 33; + cudf::string_view const sv{ptr, static_cast(len)}; + *static_cast(dstv) = cudf::detail::MurmurHash3_32{hash_seed}(sv); } else { // Output string descriptor auto* dst = static_cast(dstv); From 89fa003312fd3448fb7721893a268fc9736245f0 Mon Sep 17 00:00:00 2001 From: "Robert (Bobby) Evans" Date: Wed, 17 Aug 2022 16:10:11 -0500 Subject: [PATCH 44/58] JNI support for writing binary columns in parquet (#11556) Adds in java APIs to support writing binary columns in parquet. Authors: - Robert (Bobby) Evans (https://github.com/revans2) Approvers: - Jason Lowe (https://github.com/jlowe) URL: https://github.com/rapidsai/cudf/pull/11556 --- .../ai/rapids/cudf/ColumnWriterOptions.java | 48 +++++++++++++++++++ .../CompressionMetadataWriterOptions.java | 5 ++ java/src/main/java/ai/rapids/cudf/Table.java | 8 ++++ java/src/main/native/src/TableJni.cpp | 14 ++---- .../test/java/ai/rapids/cudf/TableTest.java | 47 ++++++++++++++++++ 5 files changed, 112 insertions(+), 10 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/ColumnWriterOptions.java b/java/src/main/java/ai/rapids/cudf/ColumnWriterOptions.java index f3fb7de6abe..2177f58c9de 100644 --- a/java/src/main/java/ai/rapids/cudf/ColumnWriterOptions.java +++ b/java/src/main/java/ai/rapids/cudf/ColumnWriterOptions.java @@ -32,6 +32,7 @@ public class ColumnWriterOptions { private int precision; private boolean isNullable; private boolean isMap = false; + private boolean isBinary = false; private String columnName; // only for Parquet private boolean hasParquetFieldId; @@ -143,6 +144,26 @@ protected ColumnWriterOptions withTimestamp(String name, boolean isInt96, return new ColumnWriterOptions(name, isInt96, UNKNOWN_PRECISION, isNullable, parquetFieldId); } + protected ColumnWriterOptions withBinary(String name, boolean isNullable) { + ColumnWriterOptions opt = listBuilder(name, isNullable) + // The name here does not matter. It will not be included in the final file + // This is just to get the metadata to line up properly for the C++ APIs + .withColumns(false, "BINARY_DATA") + .build(); + opt.isBinary = true; + return opt; + } + + protected ColumnWriterOptions withBinary(String name, boolean isNullable, int parquetFieldId) { + ColumnWriterOptions opt = listBuilder(name, isNullable) + // The name here does not matter. It will not be included in the final file + // This is just to get the metadata to line up properly for the C++ APIs + .withColumn(false, "BINARY_DATA", parquetFieldId) + .build(); + opt.isBinary = true; + return opt; + } + /** * Set the list column meta. * Lists should have only one child in ColumnVector, but the metadata expects a @@ -258,6 +279,24 @@ public T withDecimalColumn(String name, int precision) { return (T) this; } + /** + * Set a binary child meta data + * @return this for chaining. + */ + public T withBinaryColumn(String name, boolean nullable, int parquetFieldId) { + children.add(withBinary(name, nullable, parquetFieldId)); + return (T) this; + } + + /** + * Set a binary child meta data + * @return this for chaining. + */ + public T withBinaryColumn(String name, boolean nullable) { + children.add(withBinary(name, nullable)); + return (T) this; + } + /** * Set a timestamp child meta data * @return this for chaining. @@ -412,6 +451,15 @@ boolean[] getFlatIsMap() { } } + boolean[] getFlatIsBinary() { + boolean[] ret = {isBinary}; + if (childColumnOptions.length > 0) { + return getFlatBooleans(ret, (opt) -> opt.getFlatIsBinary()); + } else { + return ret; + } + } + int[] getFlatNumChildren() { int[] ret = {childColumnOptions.length}; if (childColumnOptions.length > 0) { diff --git a/java/src/main/java/ai/rapids/cudf/CompressionMetadataWriterOptions.java b/java/src/main/java/ai/rapids/cudf/CompressionMetadataWriterOptions.java index 3a3b7d721b7..27eb1be565a 100644 --- a/java/src/main/java/ai/rapids/cudf/CompressionMetadataWriterOptions.java +++ b/java/src/main/java/ai/rapids/cudf/CompressionMetadataWriterOptions.java @@ -66,6 +66,11 @@ boolean[] getFlatIsMap() { return super.getFlatBooleans(new boolean[]{}, (opt) -> opt.getFlatIsMap()); } + @Override + boolean[] getFlatIsBinary() { + return super.getFlatBooleans(new boolean[]{}, (opt) -> opt.getFlatIsBinary()); + } + @Override String[] getFlatColumnNames() { return super.getFlatColumnNames(new String[]{}); diff --git a/java/src/main/java/ai/rapids/cudf/Table.java b/java/src/main/java/ai/rapids/cudf/Table.java index e5194b8b7eb..c4a94809269 100644 --- a/java/src/main/java/ai/rapids/cudf/Table.java +++ b/java/src/main/java/ai/rapids/cudf/Table.java @@ -280,6 +280,7 @@ private static native long[] readAvro(String[] filterColumnNames, String filePat * @param precisions precision list containing all the precisions of the decimal types in * the columns * @param isMapValues true if a column is a map + * @param isBinaryValues true if a column is a binary * @param filename local output path * @return a handle that is used in later calls to writeParquetChunk and writeParquetEnd. */ @@ -294,6 +295,7 @@ private static native long writeParquetFileBegin(String[] columnNames, boolean[] isInt96, int[] precisions, boolean[] isMapValues, + boolean[] isBinaryValues, boolean[] hasParquetFieldIds, int[] parquetFieldIds, String filename) throws CudfException; @@ -312,6 +314,7 @@ private static native long writeParquetFileBegin(String[] columnNames, * @param precisions precision list containing all the precisions of the decimal types in * the columns * @param isMapValues true if a column is a map + * @param isBinaryValues true if a column is a binary * @param consumer consumer of host buffers produced. * @return a handle that is used in later calls to writeParquetChunk and writeParquetEnd. */ @@ -326,6 +329,7 @@ private static native long writeParquetBufferBegin(String[] columnNames, boolean[] isInt96, int[] precisions, boolean[] isMapValues, + boolean[] isBinaryValues, boolean[] hasParquetFieldIds, int[] parquetFieldIds, HostBufferConsumer consumer) throws CudfException; @@ -1213,6 +1217,7 @@ private ParquetTableWriter(ParquetWriterOptions options, File outputFile) { boolean[] columnNullabilities = options.getFlatIsNullable(); boolean[] timeInt96Values = options.getFlatIsTimeTypeInt96(); boolean[] isMapValues = options.getFlatIsMap(); + boolean[] isBinaryValues = options.getFlatIsBinary(); int[] precisions = options.getFlatPrecision(); boolean[] hasParquetFieldIds = options.getFlatHasParquetFieldId(); int[] parquetFieldIds = options.getFlatParquetFieldId(); @@ -1230,6 +1235,7 @@ private ParquetTableWriter(ParquetWriterOptions options, File outputFile) { timeInt96Values, precisions, isMapValues, + isBinaryValues, hasParquetFieldIds, parquetFieldIds, outputFile.getAbsolutePath()); @@ -1240,6 +1246,7 @@ private ParquetTableWriter(ParquetWriterOptions options, HostBufferConsumer cons boolean[] columnNullabilities = options.getFlatIsNullable(); boolean[] timeInt96Values = options.getFlatIsTimeTypeInt96(); boolean[] isMapValues = options.getFlatIsMap(); + boolean[] isBinaryValues = options.getFlatIsBinary(); int[] precisions = options.getFlatPrecision(); boolean[] hasParquetFieldIds = options.getFlatHasParquetFieldId(); int[] parquetFieldIds = options.getFlatParquetFieldId(); @@ -1257,6 +1264,7 @@ private ParquetTableWriter(ParquetWriterOptions options, HostBufferConsumer cons timeInt96Values, precisions, isMapValues, + isBinaryValues, hasParquetFieldIds, parquetFieldIds, consumer); diff --git a/java/src/main/native/src/TableJni.cpp b/java/src/main/native/src/TableJni.cpp index 52da3b96491..c1841afb419 100644 --- a/java/src/main/native/src/TableJni.cpp +++ b/java/src/main/native/src/TableJni.cpp @@ -1587,8 +1587,8 @@ JNIEXPORT long JNICALL Java_ai_rapids_cudf_Table_writeParquetBufferBegin( JNIEnv *env, jclass, jobjectArray j_col_names, jint j_num_children, jintArray j_children, jbooleanArray j_col_nullability, jobjectArray j_metadata_keys, jobjectArray j_metadata_values, jint j_compression, jint j_stats_freq, jbooleanArray j_isInt96, jintArray j_precisions, - jbooleanArray j_is_map, jbooleanArray j_hasParquetFieldIds, jintArray j_parquetFieldIds, - jobject consumer) { + jbooleanArray j_is_map, jbooleanArray j_is_binary, jbooleanArray j_hasParquetFieldIds, + jintArray j_parquetFieldIds, jobject consumer) { JNI_NULL_CHECK(env, j_col_names, "null columns", 0); JNI_NULL_CHECK(env, j_col_nullability, "null nullability", 0); JNI_NULL_CHECK(env, j_metadata_keys, "null metadata keys", 0); @@ -1598,9 +1598,6 @@ JNIEXPORT long JNICALL Java_ai_rapids_cudf_Table_writeParquetBufferBegin( std::unique_ptr data_sink( new cudf::jni::jni_writer_data_sink(env, consumer)); - // temp stub - jbooleanArray j_is_binary = NULL; - using namespace cudf::io; using namespace cudf::jni; sink_info sink{data_sink.get()}; @@ -1636,8 +1633,8 @@ JNIEXPORT long JNICALL Java_ai_rapids_cudf_Table_writeParquetFileBegin( JNIEnv *env, jclass, jobjectArray j_col_names, jint j_num_children, jintArray j_children, jbooleanArray j_col_nullability, jobjectArray j_metadata_keys, jobjectArray j_metadata_values, jint j_compression, jint j_stats_freq, jbooleanArray j_isInt96, jintArray j_precisions, - jbooleanArray j_is_map, jbooleanArray j_hasParquetFieldIds, jintArray j_parquetFieldIds, - jstring j_output_path) { + jbooleanArray j_is_map, jbooleanArray j_is_binary, jbooleanArray j_hasParquetFieldIds, + jintArray j_parquetFieldIds, jstring j_output_path) { JNI_NULL_CHECK(env, j_col_names, "null columns", 0); JNI_NULL_CHECK(env, j_col_nullability, "null nullability", 0); JNI_NULL_CHECK(env, j_metadata_keys, "null metadata keys", 0); @@ -1646,9 +1643,6 @@ JNIEXPORT long JNICALL Java_ai_rapids_cudf_Table_writeParquetFileBegin( try { cudf::jni::native_jstring output_path(env, j_output_path); - // temp stub - jbooleanArray j_is_binary = NULL; - using namespace cudf::io; using namespace cudf::jni; table_input_metadata metadata; diff --git a/java/src/test/java/ai/rapids/cudf/TableTest.java b/java/src/test/java/ai/rapids/cudf/TableTest.java index 7d915cffcfe..9b1f7a2012f 100644 --- a/java/src/test/java/ai/rapids/cudf/TableTest.java +++ b/java/src/test/java/ai/rapids/cudf/TableTest.java @@ -30,6 +30,7 @@ import ai.rapids.cudf.ast.ColumnReference; import ai.rapids.cudf.ast.CompiledExpression; import ai.rapids.cudf.ast.TableReference; +import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.hadoop.conf.Configuration; @@ -580,6 +581,52 @@ void testReadParquetBinary() { } } + List asList(String str) { + byte[] bytes = str.getBytes(Charsets.UTF_8); + List ret = new ArrayList<>(bytes.length); + for(int i = 0; i < bytes.length; i++) { + ret.add(bytes[i]); + } + return ret; + } + + @Test + void testParquetWriteToBufferChunkedBinary() { + // We create a String table and a Binary table with the same data in them to + // avoid trying to read the binary data back in the same way. At least until the + // API for that is stable + String string1 = "ABC"; + String string2 = "DEF"; + List bin1 = asList(string1); + List bin2 = asList(string2); + + try (Table binTable = new Table.TestBuilder() + .column(new ListType(true, new BasicType(false, DType.INT8)), + bin1, bin2) + .build(); + Table stringTable = new Table.TestBuilder() + .column(string1, string2) + .build(); + MyBufferConsumer consumer = new MyBufferConsumer()) { + ParquetWriterOptions options = ParquetWriterOptions.builder() + .withBinaryColumn("_c0", true) + .build(); + + try (TableWriter writer = Table.writeParquetChunked(options, consumer)) { + writer.write(binTable); + writer.write(binTable); + writer.write(binTable); + } + ParquetOptions opts = ParquetOptions.builder() + .includeColumn("_c0") + .build(); + try (Table table1 = Table.readParquet(opts, consumer.buffer, 0, consumer.offset); + Table concat = Table.concatenate(stringTable, stringTable, stringTable)) { + assertTablesAreEqual(concat, table1); + } + } + } + @Test void testReadParquetBuffer() throws IOException { ParquetOptions opts = ParquetOptions.builder() From 127d57485445001e5d0c9fdc20fd8e380d9a410d Mon Sep 17 00:00:00 2001 From: Nghia Truong Date: Wed, 17 Aug 2022 14:43:16 -0700 Subject: [PATCH 45/58] Fully support nested types in `cudf::contains` (#10656) This extends the `cudf::contains` API to support nested types (lists + structs) with arbitrarily nested levels. As such, `cudf::contains` will work with literally any type of input data. In addition, this fixes null handling of `cudf::contains` with structs column + struct scalar input when the structs column contains null rows at the top level while the scalar key is valid but all nulls at children levels. Closes: https://github.com/rapidsai/cudf/issues/8965 Depends on: * https://github.com/rapidsai/cudf/pull/10730 * https://github.com/rapidsai/cudf/pull/10883 * https://github.com/rapidsai/cudf/pull/10802 * https://github.com/rapidsai/cudf/pull/10997 * https://github.com/NVIDIA/cuCollections/issues/172 * https://github.com/NVIDIA/cuCollections/issues/173 * https://github.com/rapidsai/cudf/issues/11037 * https://github.com/rapidsai/cudf/pull/11356 Authors: - Nghia Truong (https://github.com/ttnghia) - Devavret Makkar (https://github.com/devavret) - Bradley Dice (https://github.com/bdice) - Karthikeyan (https://github.com/karthikeyann) Approvers: - AJ Schmidt (https://github.com/ajschmidt8) - Bradley Dice (https://github.com/bdice) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/10656 --- cpp/CMakeLists.txt | 2 +- cpp/include/cudf/detail/search.hpp | 16 - cpp/src/search/contains_column.cu | 202 +++------- cpp/src/search/contains_nested.cu | 66 ---- cpp/src/search/contains_scalar.cu | 176 +++++++++ cpp/tests/CMakeLists.txt | 4 +- cpp/tests/search/search_list_test.cpp | 349 ++++++++++++++++++ cpp/tests/search/search_struct_test.cpp | 472 ++++++++++++++++-------- 8 files changed, 894 insertions(+), 393 deletions(-) delete mode 100644 cpp/src/search/contains_nested.cu create mode 100644 cpp/src/search/contains_scalar.cu create mode 100644 cpp/tests/search/search_list_test.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 44aaac54adb..bb8620cd99c 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -449,8 +449,8 @@ add_library( src/scalar/scalar.cpp src/scalar/scalar_factories.cpp src/search/contains_column.cu + src/search/contains_scalar.cu src/search/contains_table.cu - src/search/contains_nested.cu src/search/search_ordered.cu src/sort/is_sorted.cu src/sort/rank.cu diff --git a/cpp/include/cudf/detail/search.hpp b/cpp/include/cudf/detail/search.hpp index a9764235c90..56d41fd635c 100644 --- a/cpp/include/cudf/detail/search.hpp +++ b/cpp/include/cudf/detail/search.hpp @@ -97,20 +97,4 @@ rmm::device_uvector contains( rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); -/** - * @brief Check if the (unique) row of the `needle` column is contained in the `haystack` column. - * - * If the input `needle` column has more than one row, only the first row will be considered. - * - * This function is designed for nested types only. It can also work with non-nested types - * but with lower performance due to the complexity of the implementation. - * - * @param haystack The column containing search space. - * @param needle A scalar value to check for existence in the search space. - * @return true if the given `needle` value exists in the `haystack` column. - */ -bool contains_nested_element(column_view const& haystack, - column_view const& needle, - rmm::cuda_stream_view stream); - } // namespace cudf::detail diff --git a/cpp/src/search/contains_column.cu b/cpp/src/search/contains_column.cu index 5d068e72584..51d265263fb 100644 --- a/cpp/src/search/contains_column.cu +++ b/cpp/src/search/contains_column.cu @@ -17,22 +17,18 @@ #include #include +#include #include #include #include #include -#include -#include -#include -#include +#include #include #include #include -#include #include -#include #include #include @@ -41,96 +37,25 @@ namespace detail { namespace { -/** - * @brief Get the underlying value of a scalar through a scalar device view. - * - * @tparam Type The scalar's value type - * @tparam ScalarDView Type of the input scalar device view - * @param d_scalar The input scalar device view - */ -template -__device__ auto inline get_scalar_value(ScalarDView d_scalar) -{ - if constexpr (cudf::is_fixed_point()) { - return d_scalar.rep(); - } else { - return d_scalar.value(); - } -} - -struct contains_scalar_dispatch { - template - bool operator()(column_view const& haystack, - scalar const& needle, - rmm::cuda_stream_view stream) const - { - CUDF_EXPECTS(haystack.type() == needle.type(), "scalar and column types must match"); - - using DType = device_storage_type_t; - auto const d_haystack = column_device_view::create(haystack, stream); - auto const d_needle = - get_scalar_device_view(static_cast&>(const_cast(needle))); - - if (haystack.has_nulls()) { - auto const begin = d_haystack->pair_begin(); - auto const end = d_haystack->pair_end(); - - return thrust::count_if( - rmm::exec_policy(stream), begin, end, [d_needle] __device__(auto const val_pair) { - auto const needle_pair = thrust::make_pair(get_scalar_value(d_needle), true); - return val_pair == needle_pair; - }) > 0; - } else { - auto const begin = d_haystack->begin(); - auto const end = d_haystack->end(); - - return thrust::count_if( - rmm::exec_policy(stream), begin, end, [d_needle] __device__(auto const val) { - return val == get_scalar_value(d_needle); - }) > 0; +struct contains_column_dispatch { + template + struct contains_fn { + bool __device__ operator()(size_type const idx) const + { + if (needles_have_nulls && needles.is_null_nocheck(idx)) { + // Exit early. The value doesn't matter, and will be masked as a null element. + return true; + } + + return haystack.contains(needles.template element(idx)); } - } -}; -template <> -bool contains_scalar_dispatch::operator()(column_view const&, - scalar const&, - rmm::cuda_stream_view) const -{ - CUDF_FAIL("list_view type not supported yet"); -} + Haystack const haystack; + column_device_view const needles; + bool const needles_have_nulls; + }; -template <> -bool contains_scalar_dispatch::operator()(column_view const& haystack, - scalar const& needle, - rmm::cuda_stream_view stream) const -{ - CUDF_EXPECTS(haystack.type() == needle.type(), "scalar and column types must match"); - // Haystack and needle structure compatibility will be checked by the table comparator - // constructor during call to `contains_nested_element`. - - auto const needle_as_col = make_column_from_scalar(needle, 1, stream); - return contains_nested_element(haystack, needle_as_col->view(), stream); -} - -template <> -bool contains_scalar_dispatch::operator()(column_view const& haystack, - scalar const& needle, - rmm::cuda_stream_view stream) const -{ - auto const dict_col = cudf::dictionary_column_view(haystack); - // first, find the needle in the dictionary's key set - auto const index = cudf::dictionary::detail::get_index(dict_col, needle, stream); - // if found, check the index is actually in the indices column - return index->is_valid(stream) && cudf::type_dispatcher(dict_col.indices().type(), - contains_scalar_dispatch{}, - dict_col.indices(), - *index, - stream); -} - -struct multi_contains_dispatch { - template + template ())> std::unique_ptr operator()(column_view const& haystack, column_view const& needles, rmm::cuda_stream_view stream, @@ -138,7 +63,7 @@ struct multi_contains_dispatch { { auto result = make_numeric_column(data_type{type_to_id()}, needles.size(), - copy_bitmask(needles), + copy_bitmask(needles, stream, mr), needles.null_count(), stream, mr); @@ -151,57 +76,38 @@ struct multi_contains_dispatch { return result; } - auto const haystack_set = cudf::detail::unordered_multiset::create(haystack, stream); + auto const haystack_set = cudf::detail::unordered_multiset::create(haystack, stream); + auto const haystack_set_dv = haystack_set.to_device(); auto const needles_cdv_ptr = column_device_view::create(needles, stream); - auto const needles_it = thrust::make_counting_iterator(0); - - if (needles.has_nulls()) { - thrust::transform(rmm::exec_policy(stream), - needles_it, - needles_it + needles.size(), - out_begin, - [haystack = haystack_set.to_device(), - needles = *needles_cdv_ptr] __device__(size_type const idx) { - return needles.is_null_nocheck(idx) || - haystack.contains(needles.template element(idx)); - }); - } else { - thrust::transform(rmm::exec_policy(stream), - needles_it, - needles_it + needles.size(), - out_begin, - [haystack = haystack_set.to_device(), - needles = *needles_cdv_ptr] __device__(size_type const index) { - return haystack.contains(needles.template element(index)); - }); - } + thrust::transform(rmm::exec_policy(stream), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(needles.size()), + out_begin, + contains_fn{ + haystack_set_dv, *needles_cdv_ptr, needles.has_nulls()}); return result; } -}; -template <> -std::unique_ptr multi_contains_dispatch::operator()( - column_view const&, - column_view const&, - rmm::cuda_stream_view, - rmm::mr::device_memory_resource*) const -{ - CUDF_FAIL("list_view type not supported"); -} - -template <> -std::unique_ptr multi_contains_dispatch::operator()( - column_view const&, - column_view const&, - rmm::cuda_stream_view, - rmm::mr::device_memory_resource*) const -{ - CUDF_FAIL("struct_view type not supported"); -} + template ())> + std::unique_ptr operator()(column_view const& haystack, + column_view const& needles, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) const + { + auto result_v = detail::contains(table_view{{haystack}}, + table_view{{needles}}, + null_equality::EQUAL, + nan_equality::ALL_EQUAL, + stream, + mr); + return std::make_unique( + std::move(result_v), copy_bitmask(needles, stream, mr), needles.null_count()); + } +}; template <> -std::unique_ptr multi_contains_dispatch::operator()( +std::unique_ptr contains_column_dispatch::operator()( column_view const& haystack_in, column_view const& needles_in, rmm::cuda_stream_view stream, @@ -219,22 +125,14 @@ std::unique_ptr multi_contains_dispatch::operator()( column_view const haystack_indices = haystack_view.get_indices_annotated(); column_view const needles_indices = needles_view.get_indices_annotated(); return cudf::type_dispatcher(haystack_indices.type(), - multi_contains_dispatch{}, + contains_column_dispatch{}, haystack_indices, needles_indices, stream, mr); } -} // namespace -bool contains(column_view const& haystack, scalar const& needle, rmm::cuda_stream_view stream) -{ - if (haystack.is_empty()) { return false; } - if (not needle.is_valid(stream)) { return haystack.has_nulls(); } - - return cudf::type_dispatcher( - haystack.type(), contains_scalar_dispatch{}, haystack, needle, stream); -} +} // namespace std::unique_ptr contains(column_view const& haystack, column_view const& needles, @@ -244,17 +142,11 @@ std::unique_ptr contains(column_view const& haystack, CUDF_EXPECTS(haystack.type() == needles.type(), "DTYPE mismatch"); return cudf::type_dispatcher( - haystack.type(), multi_contains_dispatch{}, haystack, needles, stream, mr); + haystack.type(), contains_column_dispatch{}, haystack, needles, stream, mr); } } // namespace detail -bool contains(column_view const& haystack, scalar const& needle) -{ - CUDF_FUNC_RANGE(); - return detail::contains(haystack, needle, cudf::default_stream_value); -} - std::unique_ptr contains(column_view const& haystack, column_view const& needles, rmm::mr::device_memory_resource* mr) diff --git a/cpp/src/search/contains_nested.cu b/cpp/src/search/contains_nested.cu deleted file mode 100644 index 6767b27a918..00000000000 --- a/cpp/src/search/contains_nested.cu +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2022, NVIDIA CORPORATION. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include - -#include -#include - -#include - -namespace cudf::detail { - -bool contains_nested_element(column_view const& haystack, - column_view const& needle, - rmm::cuda_stream_view stream) -{ - CUDF_EXPECTS(needle.size() > 0, "Input needle column should have at least ONE row."); - - auto const haystack_tv = table_view{{haystack}}; - auto const needle_tv = table_view{{needle}}; - auto const has_nulls = has_nested_nulls(haystack_tv) || has_nested_nulls(needle_tv); - - auto const comparator = - cudf::experimental::row::equality::two_table_comparator(haystack_tv, needle_tv, stream); - auto const d_comp = comparator.equal_to(nullate::DYNAMIC{has_nulls}); - - auto const begin = cudf::experimental::row::lhs_iterator(0); - auto const end = begin + haystack.size(); - using cudf::experimental::row::rhs_index_type; - - if (haystack.has_nulls()) { - auto const haystack_cdv_ptr = column_device_view::create(haystack, stream); - auto const haystack_valid_it = cudf::detail::make_validity_iterator(*haystack_cdv_ptr); - - return thrust::count_if(rmm::exec_policy(stream), - begin, - end, - [d_comp, haystack_valid_it] __device__(auto const idx) { - if (!haystack_valid_it[static_cast(idx)]) { return false; } - return d_comp( - idx, rhs_index_type{0}); // compare haystack[idx] == needle[0]. - }) > 0; - } - - return thrust::count_if( - rmm::exec_policy(stream), begin, end, [d_comp] __device__(auto const idx) { - return d_comp(idx, rhs_index_type{0}); // compare haystack[idx] == needle[0]. - }) > 0; -} - -} // namespace cudf::detail diff --git a/cpp/src/search/contains_scalar.cu b/cpp/src/search/contains_scalar.cu new file mode 100644 index 00000000000..e64cca44507 --- /dev/null +++ b/cpp/src/search/contains_scalar.cu @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2019-2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace cudf { +namespace detail { + +namespace { + +/** + * @brief Get the underlying value of a scalar through a scalar device view. + * + * @tparam Element The scalar's value type + * @tparam ScalarDView Type of the input scalar device view + * @param d_scalar The input scalar device view + */ +template +__device__ auto inline get_scalar_value(ScalarDView d_scalar) +{ + if constexpr (cudf::is_fixed_point()) { + return d_scalar.rep(); + } else { + return d_scalar.value(); + } +} + +struct contains_scalar_dispatch { + // SFINAE with conditional return type because we need to support device lambda in this function. + // This is required due to a limitation of nvcc. + template + std::enable_if_t(), bool> operator()(column_view const& haystack, + scalar const& needle, + rmm::cuda_stream_view stream) const + { + CUDF_EXPECTS(haystack.type() == needle.type(), "Scalar and column types must match"); + // Don't need to check for needle validity. If it is invalid, it should be handled by the caller + // before dispatching to this function. + + using DType = device_storage_type_t; + auto const d_haystack = column_device_view::create(haystack, stream); + auto const d_needle = get_scalar_device_view( + static_cast&>(const_cast(needle))); + + if (haystack.has_nulls()) { + auto const begin = d_haystack->pair_begin(); + auto const end = d_haystack->pair_end(); + + return thrust::count_if( + rmm::exec_policy(stream), begin, end, [d_needle] __device__(auto const val_pair) { + auto const needle_pair = + thrust::make_pair(get_scalar_value(d_needle), true); + return val_pair == needle_pair; + }) > 0; + } else { + auto const begin = d_haystack->begin(); + auto const end = d_haystack->end(); + + return thrust::count_if( + rmm::exec_policy(stream), begin, end, [d_needle] __device__(auto const val) { + return val == get_scalar_value(d_needle); + }) > 0; + } + } + + template + std::enable_if_t(), bool> operator()(column_view const& haystack, + scalar const& needle, + rmm::cuda_stream_view stream) const + { + CUDF_EXPECTS(haystack.type() == needle.type(), "Scalar and column types must match"); + // Don't need to check for needle validity. If it is invalid, it should be handled by the caller + // before dispatching to this function. + // In addition, haystack and needle structure compatibility will be checked later on by + // constructor of the table comparator. + + auto const haystack_tv = table_view{{haystack}}; + auto const needle_as_col = make_column_from_scalar(needle, 1, stream); + auto const needle_tv = table_view{{needle_as_col->view()}}; + auto const has_nulls = has_nested_nulls(haystack_tv) || has_nested_nulls(needle_tv); + + auto const comparator = + cudf::experimental::row::equality::two_table_comparator(haystack_tv, needle_tv, stream); + auto const d_comp = comparator.equal_to(nullate::DYNAMIC{has_nulls}); + + auto const begin = cudf::experimental::row::lhs_iterator(0); + auto const end = begin + haystack.size(); + using cudf::experimental::row::rhs_index_type; + + if (haystack.has_nulls()) { + auto const haystack_cdv_ptr = column_device_view::create(haystack, stream); + auto const haystack_valid_it = cudf::detail::make_validity_iterator(*haystack_cdv_ptr); + + return thrust::count_if(rmm::exec_policy(stream), + begin, + end, + [d_comp, haystack_valid_it] __device__(auto const idx) { + if (!haystack_valid_it[static_cast(idx)]) { + return false; + } + return d_comp( + idx, rhs_index_type{0}); // compare haystack[idx] == needle[0]. + }) > 0; + } + + return thrust::count_if( + rmm::exec_policy(stream), begin, end, [d_comp] __device__(auto const idx) { + return d_comp(idx, rhs_index_type{0}); // compare haystack[idx] == needle[0]. + }) > 0; + } +}; + +template <> +bool contains_scalar_dispatch::operator()(column_view const& haystack, + scalar const& needle, + rmm::cuda_stream_view stream) const +{ + auto const dict_col = cudf::dictionary_column_view(haystack); + // first, find the needle in the dictionary's key set + auto const index = cudf::dictionary::detail::get_index(dict_col, needle, stream); + // if found, check the index is actually in the indices column + return index->is_valid(stream) && cudf::type_dispatcher(dict_col.indices().type(), + contains_scalar_dispatch{}, + dict_col.indices(), + *index, + stream); +} + +} // namespace + +bool contains(column_view const& haystack, scalar const& needle, rmm::cuda_stream_view stream) +{ + if (haystack.is_empty()) { return false; } + if (not needle.is_valid(stream)) { return haystack.has_nulls(); } + + return cudf::type_dispatcher( + haystack.type(), contains_scalar_dispatch{}, haystack, needle, stream); +} + +} // namespace detail + +bool contains(column_view const& haystack, scalar const& needle) +{ + CUDF_FUNC_RANGE(); + return detail::contains(haystack, needle, cudf::default_stream_value); +} + +} // namespace cudf diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 8aba2a11d10..1964db53659 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -358,8 +358,8 @@ ConfigureTest( # ################################################################################################## # * search test ----------------------------------------------------------------------------------- ConfigureTest( - SEARCH_TEST search/search_dictionary_test.cpp search/search_struct_test.cpp - search/search_test.cpp + SEARCH_TEST search/search_dictionary_test.cpp search/search_list_test.cpp + search/search_struct_test.cpp search/search_test.cpp ) # ################################################################################################## diff --git a/cpp/tests/search/search_list_test.cpp b/cpp/tests/search/search_list_test.cpp new file mode 100644 index 00000000000..1393095037d --- /dev/null +++ b/cpp/tests/search/search_list_test.cpp @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace cudf::test::iterators; + +using bools_col = cudf::test::fixed_width_column_wrapper; +using int32s_col = cudf::test::fixed_width_column_wrapper; +using structs_col = cudf::test::structs_column_wrapper; +using strings_col = cudf::test::strings_column_wrapper; + +constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::FIRST_ERROR}; +constexpr int32_t null{0}; // Mark for null child elements at the current level +constexpr int32_t XXX{0}; // Mark for null elements at all levels +constexpr int32_t dont_care{0}; // Mark for elements that will be sliced off + +using TestTypes = cudf::test::Concat; + +template +struct TypedListsContainsTestScalarNeedle : public cudf::test::BaseFixture { +}; +TYPED_TEST_SUITE(TypedListsContainsTestScalarNeedle, TestTypes); + +TYPED_TEST(TypedListsContainsTestScalarNeedle, EmptyInput) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + using lists_col = cudf::test::lists_column_wrapper; + + auto const haystack = lists_col{}; + + auto const needle1 = [] { + auto child = tdata_col{}; + return cudf::list_scalar(child); + }(); + auto const needle2 = [] { + auto child = tdata_col{1, 2, 3}; + return cudf::list_scalar(child); + }(); + + EXPECT_FALSE(cudf::contains(haystack, needle1)); + EXPECT_FALSE(cudf::contains(haystack, needle2)); +} + +TYPED_TEST(TypedListsContainsTestScalarNeedle, TrivialInput) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + using lists_col = cudf::test::lists_column_wrapper; + + auto const haystack = lists_col{{1, 2}, {1}, {}, {1, 3}, {4}, {1, 1}}; + + auto const needle1 = [] { + auto child = tdata_col{1, 2}; + return cudf::list_scalar(child); + }(); + auto const needle2 = [] { + auto child = tdata_col{2, 1}; + return cudf::list_scalar(child); + }(); + + EXPECT_TRUE(cudf::contains(haystack, needle1)); + + // Lists are order-sensitive. + EXPECT_FALSE(cudf::contains(haystack, needle2)); +} + +TYPED_TEST(TypedListsContainsTestScalarNeedle, SlicedColumnInput) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + using lists_col = cudf::test::lists_column_wrapper; + + auto const haystack_original = + lists_col{{dont_care, dont_care}, {dont_care}, {1, 2}, {1}, {}, {1, 3}, {dont_care, dont_care}}; + auto const haystack = cudf::slice(haystack_original, {2, 6})[0]; + + auto const needle1 = [] { + auto child = tdata_col{1, 2}; + return cudf::list_scalar(child); + }(); + auto const needle2 = [] { + auto child = tdata_col{}; + return cudf::list_scalar(child); + }(); + auto const needle3 = [] { + auto child = tdata_col{dont_care, dont_care}; + return cudf::list_scalar(child); + }(); + + EXPECT_TRUE(cudf::contains(haystack, needle1)); + EXPECT_TRUE(cudf::contains(haystack, needle2)); + EXPECT_FALSE(cudf::contains(haystack, needle3)); +} + +TYPED_TEST(TypedListsContainsTestScalarNeedle, SimpleInputWithNulls) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + using lists_col = cudf::test::lists_column_wrapper; + + // Test with invalid scalar. + { + auto const haystack = lists_col{{1, 2}, {1}, {}, {1, 3}, {4}, {}, {1, 1}}; + auto const needle = [] { + auto child = tdata_col{}; + return cudf::list_scalar(child, false); + }(); + + EXPECT_FALSE(cudf::contains(haystack, needle)); + } + + // Test with nulls at the top level. + { + auto const haystack = + lists_col{{{1, 2}, {1}, {} /*NULL*/, {1, 3}, {4}, {} /*NULL*/, {1, 1}}, nulls_at({2, 5})}; + + auto const needle1 = [] { + auto child = tdata_col{1, 2}; + return cudf::list_scalar(child); + }(); + auto const needle2 = [] { + auto child = tdata_col{}; + return cudf::list_scalar(child, false); + }(); + + EXPECT_TRUE(cudf::contains(haystack, needle1)); + EXPECT_TRUE(cudf::contains(haystack, needle2)); + } + + // Test with nulls at the children level. + { + auto const haystack = lists_col{{lists_col{1, 2}, + lists_col{1}, + lists_col{{1, null}, null_at(1)}, + lists_col{} /*NULL*/, + lists_col{1, 3}, + lists_col{1, 4}, + lists_col{4}, + lists_col{} /*NULL*/, + lists_col{1, 1}}, + nulls_at({3, 7})}; + + auto const needle1 = [] { + auto child = tdata_col{{1, null}, null_at(1)}; + return cudf::list_scalar(child); + }(); + auto const needle2 = [] { + auto child = tdata_col{{null, 1}, null_at(0)}; + return cudf::list_scalar(child); + }(); + auto const needle3 = [] { + auto child = tdata_col{1, 0}; + return cudf::list_scalar(child); + }(); + + EXPECT_TRUE(cudf::contains(haystack, needle1)); + EXPECT_FALSE(cudf::contains(haystack, needle2)); + EXPECT_FALSE(cudf::contains(haystack, needle3)); + } +} + +TYPED_TEST(TypedListsContainsTestScalarNeedle, SlicedInputHavingNulls) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + using lists_col = cudf::test::lists_column_wrapper; + + auto const haystack_original = lists_col{{{dont_care, dont_care}, + {dont_care} /*NULL*/, + lists_col{{1, null}, null_at(1)}, + {1}, + {} /*NULL*/, + {1, 3}, + {4}, + {} /*NULL*/, + {1, 1}, + {dont_care}}, + nulls_at({1, 4, 7})}; + auto const haystack = cudf::slice(haystack_original, {2, 9})[0]; + + auto const needle1 = [] { + auto child = tdata_col{{1, null}, null_at(1)}; + return cudf::list_scalar(child); + }(); + auto const needle2 = [] { + auto child = tdata_col{}; + return cudf::list_scalar(child); + }(); + auto const needle3 = [] { + auto child = tdata_col{dont_care, dont_care}; + return cudf::list_scalar(child); + }(); + + EXPECT_TRUE(cudf::contains(haystack, needle1)); + EXPECT_FALSE(cudf::contains(haystack, needle2)); + EXPECT_FALSE(cudf::contains(haystack, needle3)); +} + +template +struct TypedListContainsTestColumnNeedles : public cudf::test::BaseFixture { +}; + +TYPED_TEST_SUITE(TypedListContainsTestColumnNeedles, TestTypes); + +TYPED_TEST(TypedListContainsTestColumnNeedles, EmptyInput) +{ + using lists_col = cudf::test::lists_column_wrapper; + + auto const haystack = lists_col{}; + auto const needles = lists_col{}; + auto const expected = bools_col{}; + auto const result = cudf::contains(haystack, needles); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); +} + +TYPED_TEST(TypedListContainsTestColumnNeedles, TrivialInput) +{ + using lists_col = cudf::test::lists_column_wrapper; + + auto const haystack = lists_col{{0, 1}, {2}, {3, 4, 5}, {2, 3, 4}, {}, {0, 2, 0}}; + auto const needles = lists_col{{0, 1}, {1}, {3, 5, 4}, {}}; + + auto const expected = bools_col{1, 0, 0, 1}; + auto const result = cudf::contains(haystack, needles); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); +} + +TYPED_TEST(TypedListContainsTestColumnNeedles, SlicedInputNoNulls) +{ + using lists_col = cudf::test::lists_column_wrapper; + + auto const haystack_original = lists_col{ + {dont_care, dont_care}, {dont_care}, {0, 1}, {2}, {3, 4, 5}, {2, 3, 4}, {}, {0, 2, 0}}; + auto const haystack = cudf::slice(haystack_original, {2, 8})[0]; + + auto const needles_original = + lists_col{{dont_care}, {0, 1}, {0, 0}, {3, 5, 4}, {}, {dont_care, dont_care}, {} /*dont_care*/}; + auto const needles = cudf::slice(needles_original, {1, 5})[0]; + + auto const expected = bools_col{1, 0, 0, 1}; + auto const result = cudf::contains(haystack, needles); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); +} + +TYPED_TEST(TypedListContainsTestColumnNeedles, SlicedInputHavingNulls) +{ + using lists_col = cudf::test::lists_column_wrapper; + + auto const haystack_original = lists_col{{{dont_care, dont_care}, + {dont_care} /*NULL*/, + lists_col{{1, null}, null_at(1)}, + {1}, + {} /*NULL*/, + {1, 3}, + {4}, + {} /*NULL*/, + {1, 1}, + {dont_care}}, + nulls_at({1, 4, 7})}; + auto const haystack = cudf::slice(haystack_original, {2, 9})[0]; + + auto const needles_original = lists_col{{{dont_care, dont_care}, + {dont_care} /*NULL*/, + lists_col{{1, null}, null_at(1)}, + {1}, + {} /*NULL*/, + {1, 3, 1}, + {4}, + {} /*NULL*/, + {}, + {dont_care}}, + nulls_at({1, 4, 7})}; + auto const needles = cudf::slice(needles_original, {2, 9})[0]; + + auto const expected = bools_col{{1, 1, null, 0, 1, null, 0}, nulls_at({2, 5})}; + auto const result = cudf::contains(haystack, needles); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); +} + +TYPED_TEST(TypedListContainsTestColumnNeedles, ListsOfStructs) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const haystack = [] { + auto offsets = int32s_col{0, 2, 3, 5, 8, 10}; + // clang-format off + auto data1 = tdata_col{1, 2, // + 1, // + 0, 1, // + 1, 3, 4, // + 0, 0 // + }; + auto data2 = tdata_col{1, 3, // + 2, // + 1, 1, // + 0, 2, 0, // + 1, 2 // + }; + // clang-format on + auto child = structs_col{{data1, data2}}; + return cudf::make_lists_column(5, offsets.release(), child.release(), 0, {}); + }(); + + auto const needles = [] { + auto offsets = int32s_col{0, 3, 4, 6, 9, 11}; + // clang-format off + auto data1 = tdata_col{1, 2, 1, // + 1, // + 0, 1, // + 1, 3, 4, // + 0, 0 // + }; + auto data2 = tdata_col{1, 3, 0, // + 2, // + 1, 1, // + 0, 2, 2, // + 1, 1 // + }; + // clang-format on + auto child = structs_col{{data1, data2}}; + return cudf::make_lists_column(5, offsets.release(), child.release(), 0, {}); + }(); + + auto const expected = bools_col{0, 1, 1, 0, 0}; + auto const result = cudf::contains(*haystack, *needles); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); +} diff --git a/cpp/tests/search/search_struct_test.cpp b/cpp/tests/search/search_struct_test.cpp index 159b082890a..5d9ef85a249 100644 --- a/cpp/tests/search/search_struct_test.cpp +++ b/cpp/tests/search/search_struct_test.cpp @@ -33,8 +33,9 @@ using structs_col = cudf::test::structs_column_wrapper; using strings_col = cudf::test::strings_column_wrapper; constexpr cudf::test::debug_output_level verbosity{cudf::test::debug_output_level::FIRST_ERROR}; -constexpr int32_t null{0}; // Mark for null child elements -constexpr int32_t XXX{0}; // Mark for null struct elements +constexpr int32_t null{0}; // Mark for null child elements at the current level +constexpr int32_t XXX{0}; // Mark for null elements at all levels +constexpr int32_t dont_care{0}; // Mark for elements that will be sliced off using TestTypes = cudf::test::Concat const& t_col, return search_bounds(t_col->view(), values_col, column_orders, null_precedence); } +template +auto make_struct_scalar(Args&&... args) +{ + return cudf::struct_scalar(std::vector{std::forward(args)...}); +} + } // namespace // Test case when all input columns are empty -TYPED_TEST(TypedStructSearchTest, EmptyInputTest) +TYPED_TEST(TypedStructSearchTest, EmptyInput) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; - auto child_col_t = col_wrapper{}; + auto child_col_t = tdata_col{}; auto const structs_t = structs_col{{child_col_t}, std::vector{}}.release(); - auto child_col_values = col_wrapper{}; + auto child_col_values = tdata_col{}; auto const structs_values = structs_col{{child_col_values}, std::vector{}}.release(); auto const results = search_bounds(structs_t, structs_values); @@ -90,15 +97,15 @@ TYPED_TEST(TypedStructSearchTest, EmptyInputTest) TYPED_TEST(TypedStructSearchTest, TrivialInputTests) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; - auto child_col_t = col_wrapper{10, 20, 30, 40, 50}; + auto child_col_t = tdata_col{10, 20, 30, 40, 50}; auto const structs_t = structs_col{{child_col_t}}.release(); - auto child_col_values1 = col_wrapper{0, 1, 2, 3, 4}; + auto child_col_values1 = tdata_col{0, 1, 2, 3, 4}; auto const structs_values1 = structs_col{{child_col_values1}}.release(); - auto child_col_values2 = col_wrapper{100, 101, 102, 103, 104}; + auto child_col_values2 = tdata_col{100, 101, 102, 103, 104}; auto const structs_values2 = structs_col{{child_col_values2}}.release(); auto const results1 = search_bounds(structs_t, structs_values1); @@ -114,12 +121,12 @@ TYPED_TEST(TypedStructSearchTest, TrivialInputTests) TYPED_TEST(TypedStructSearchTest, SlicedColumnInputTests) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; - auto child_col_values = col_wrapper{0, 1, 2, 3, 4, 5}; + auto child_col_values = tdata_col{0, 1, 2, 3, 4, 5}; auto const structs_values = structs_col{child_col_values}.release(); - auto child_col_t = col_wrapper{0, 1, 2, 2, 2, 2, 3, 3, 4, 4}; + auto child_col_t = tdata_col{0, 1, 2, 2, 2, 2, 3, 3, 4, 4}; auto const structs_t_original = structs_col{child_col_t}.release(); auto structs_t = cudf::slice(structs_t_original->view(), {0, 10})[0]; // the entire column t @@ -146,13 +153,13 @@ TYPED_TEST(TypedStructSearchTest, SlicedColumnInputTests) TYPED_TEST(TypedStructSearchTest, SimpleInputWithNullsTests) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; - auto child_col_values = col_wrapper{{1, null, 70, XXX, 2, 100}, null_at(1)}; + auto child_col_values = tdata_col{{1, null, 70, XXX, 2, 100}, null_at(1)}; auto const structs_values = structs_col{{child_col_values}, null_at(3)}.release(); // Sorted asc, nulls first - auto child_col_t = col_wrapper{{XXX, null, 0, 1, 2, 2, 2, 2, 3, 3, 4}, null_at(1)}; + auto child_col_t = tdata_col{{XXX, null, 0, 1, 2, 2, 2, 2, 3, 3, 4}, null_at(1)}; auto structs_t = structs_col{{child_col_t}, null_at(0)}.release(); auto results = @@ -163,7 +170,7 @@ TYPED_TEST(TypedStructSearchTest, SimpleInputWithNullsTests) CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_upper_bound, results.second->view(), verbosity); // Sorted asc, nulls last - child_col_t = col_wrapper{{0, 1, 2, 2, 2, 2, 3, 3, 4, null, XXX}, null_at(9)}; + child_col_t = tdata_col{{0, 1, 2, 2, 2, 2, 3, 3, 4, null, XXX}, null_at(9)}; structs_t = structs_col{{child_col_t}, null_at(10)}.release(); results = search_bounds(structs_t, structs_values, {cudf::order::ASCENDING}, {cudf::null_order::AFTER}); @@ -173,7 +180,7 @@ TYPED_TEST(TypedStructSearchTest, SimpleInputWithNullsTests) CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_upper_bound, results.second->view(), verbosity); // Sorted dsc, nulls first - child_col_t = col_wrapper{{XXX, null, 4, 3, 3, 2, 2, 2, 2, 1, 0}, null_at(1)}; + child_col_t = tdata_col{{XXX, null, 4, 3, 3, 2, 2, 2, 2, 1, 0}, null_at(1)}; structs_t = structs_col{{child_col_t}, null_at(0)}.release(); results = search_bounds(structs_t, structs_values, {cudf::order::DESCENDING}, {cudf::null_order::BEFORE}); @@ -183,7 +190,7 @@ TYPED_TEST(TypedStructSearchTest, SimpleInputWithNullsTests) CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_upper_bound, results.second->view(), verbosity); // Sorted dsc, nulls last - child_col_t = col_wrapper{{4, 3, 3, 2, 2, 2, 2, 1, 0, null, XXX}, null_at(9)}; + child_col_t = tdata_col{{4, 3, 3, 2, 2, 2, 2, 1, 0, null, XXX}, null_at(9)}; structs_t = structs_col{{child_col_t}, null_at(10)}.release(); results = search_bounds(structs_t, structs_values, {cudf::order::DESCENDING}, {cudf::null_order::AFTER}); @@ -195,13 +202,13 @@ TYPED_TEST(TypedStructSearchTest, SimpleInputWithNullsTests) TYPED_TEST(TypedStructSearchTest, SimpleInputWithValuesHavingNullsTests) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; - auto child_col_values = col_wrapper{{1, null, 70, XXX, 2, 100}, null_at(1)}; + auto child_col_values = tdata_col{{1, null, 70, XXX, 2, 100}, null_at(1)}; auto const structs_values = structs_col{{child_col_values}, null_at(3)}.release(); // Sorted asc, search nulls first - auto child_col_t = col_wrapper{0, 0, 0, 1, 2, 2, 2, 2, 3, 3, 4}; + auto child_col_t = tdata_col{0, 0, 0, 1, 2, 2, 2, 2, 3, 3, 4}; auto structs_t = structs_col{{child_col_t}}.release(); auto results = @@ -220,7 +227,7 @@ TYPED_TEST(TypedStructSearchTest, SimpleInputWithValuesHavingNullsTests) CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_upper_bound, results.second->view(), verbosity); // Sorted dsc, search nulls first - child_col_t = col_wrapper{4, 3, 3, 2, 2, 2, 2, 1, 0, 0, 0}; + child_col_t = tdata_col{4, 3, 3, 2, 2, 2, 2, 1, 0, 0, 0}; structs_t = structs_col{{child_col_t}}.release(); results = search_bounds(structs_t, structs_values, {cudf::order::DESCENDING}, {cudf::null_order::BEFORE}); @@ -240,13 +247,13 @@ TYPED_TEST(TypedStructSearchTest, SimpleInputWithValuesHavingNullsTests) TYPED_TEST(TypedStructSearchTest, SimpleInputWithTargetHavingNullsTests) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; - auto child_col_values = col_wrapper{1, 0, 70, 0, 2, 100}; + auto child_col_values = tdata_col{1, 0, 70, 0, 2, 100}; auto const structs_values = structs_col{{child_col_values}}.release(); // Sorted asc, nulls first - auto child_col_t = col_wrapper{{XXX, null, 0, 1, 2, 2, 2, 2, 3, 3, 4}, null_at(1)}; + auto child_col_t = tdata_col{{XXX, null, 0, 1, 2, 2, 2, 2, 3, 3, 4}, null_at(1)}; auto structs_t = structs_col{{child_col_t}, null_at(0)}.release(); auto results = @@ -257,7 +264,7 @@ TYPED_TEST(TypedStructSearchTest, SimpleInputWithTargetHavingNullsTests) CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_upper_bound, results.second->view(), verbosity); // Sorted asc, nulls last - child_col_t = col_wrapper{{0, 1, 2, 2, 2, 2, 3, 3, 4, null, XXX}, null_at(9)}; + child_col_t = tdata_col{{0, 1, 2, 2, 2, 2, 3, 3, 4, null, XXX}, null_at(9)}; structs_t = structs_col{{child_col_t}, null_at(10)}.release(); results = search_bounds(structs_t, structs_values, {cudf::order::ASCENDING}, {cudf::null_order::AFTER}); @@ -267,7 +274,7 @@ TYPED_TEST(TypedStructSearchTest, SimpleInputWithTargetHavingNullsTests) CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_upper_bound, results.second->view(), verbosity); // Sorted dsc, nulls first - child_col_t = col_wrapper{{XXX, null, 4, 3, 3, 2, 2, 2, 2, 1, 0}, null_at(1)}; + child_col_t = tdata_col{{XXX, null, 4, 3, 3, 2, 2, 2, 2, 1, 0}, null_at(1)}; structs_t = structs_col{{child_col_t}, null_at(0)}.release(); results = search_bounds(structs_t, structs_values, {cudf::order::DESCENDING}, {cudf::null_order::BEFORE}); @@ -277,7 +284,7 @@ TYPED_TEST(TypedStructSearchTest, SimpleInputWithTargetHavingNullsTests) CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected_upper_bound, results.second->view(), verbosity); // Sorted dsc, nulls last - child_col_t = col_wrapper{{4, 3, 3, 2, 2, 2, 2, 1, 0, null, XXX}, null_at(9)}; + child_col_t = tdata_col{{4, 3, 3, 2, 2, 2, 2, 1, 0, null, XXX}, null_at(9)}; structs_t = structs_col{{child_col_t}, null_at(10)}.release(); results = search_bounds(structs_t, structs_values, {cudf::order::DESCENDING}, {cudf::null_order::AFTER}); @@ -289,16 +296,16 @@ TYPED_TEST(TypedStructSearchTest, SimpleInputWithTargetHavingNullsTests) TYPED_TEST(TypedStructSearchTest, OneColumnHasNullMaskButNoNullElementTest) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; - auto child_col1 = col_wrapper{1, 20, 30}; + auto child_col1 = tdata_col{1, 20, 30}; auto const structs_col1 = structs_col{{child_col1}}.release(); - auto child_col2 = col_wrapper{0, 10, 10}; + auto child_col2 = tdata_col{0, 10, 10}; auto const structs_col2 = structs_col{child_col2}.release(); // structs_col3 (and its child column) will have a null mask but no null element - auto child_col3 = col_wrapper{{0, 10, 10}, no_nulls()}; + auto child_col3 = tdata_col{{0, 10, 10}, no_nulls()}; auto const structs_col3 = structs_col{{child_col3}, no_nulls()}.release(); // Search struct elements of structs_col2 and structs_col3 in the column structs_col1 @@ -329,18 +336,18 @@ TYPED_TEST(TypedStructSearchTest, OneColumnHasNullMaskButNoNullElementTest) TYPED_TEST(TypedStructSearchTest, ComplexStructTest) { // Testing on struct. - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; auto names_column_t = strings_col{"Cherry", "Kiwi", "Lemon", "Newton", "Tomato", /*NULL*/ "Washington"}; - auto ages_column_t = col_wrapper{{5, 10, 15, 20, null, XXX}, null_at(4)}; + auto ages_column_t = tdata_col{{5, 10, 15, 20, null, XXX}, null_at(4)}; auto is_human_col_t = bools_col{false, false, false, false, false, /*NULL*/ true}; auto const structs_t = structs_col{{names_column_t, ages_column_t, is_human_col_t}, null_at(5)}.release(); auto names_column_values = strings_col{"Bagel", "Tomato", "Lemonade", /*NULL*/ "Donut", "Butter"}; - auto ages_column_values = col_wrapper{{10, null, 15, XXX, 17}, null_at(1)}; + auto ages_column_values = tdata_col{{10, null, 15, XXX, 17}, null_at(1)}; auto is_human_col_values = bools_col{false, false, true, /*NULL*/ true, true}; auto const structs_values = structs_col{{names_column_values, ages_column_values, is_human_col_values}, null_at(3)} @@ -355,232 +362,391 @@ TYPED_TEST(TypedStructSearchTest, ComplexStructTest) } template -struct TypedScalarStructContainTest : public cudf::test::BaseFixture { +struct TypedStructContainsTestScalarNeedle : public cudf::test::BaseFixture { }; -TYPED_TEST_SUITE(TypedScalarStructContainTest, TestTypes); +TYPED_TEST_SUITE(TypedStructContainsTestScalarNeedle, TestTypes); -TYPED_TEST(TypedScalarStructContainTest, EmptyInputTest) +TYPED_TEST(TypedStructContainsTestScalarNeedle, EmptyInput) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; - auto const col = [] { - auto child = col_wrapper{}; + auto const haystack = [] { + auto child = tdata_col{}; return structs_col{{child}}; }(); - auto const val = [] { - auto child = col_wrapper{1}; - return cudf::struct_scalar(std::vector{child}); + auto const needle1 = [] { + auto child = tdata_col{1}; + return make_struct_scalar(child); + }(); + auto const needle2 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{1}; + return make_struct_scalar(child1, child2); }(); - EXPECT_EQ(false, cudf::contains(col, val)); + EXPECT_FALSE(cudf::contains(haystack, needle1)); + EXPECT_FALSE(cudf::contains(haystack, needle2)); } -TYPED_TEST(TypedScalarStructContainTest, TrivialInputTests) +TYPED_TEST(TypedStructContainsTestScalarNeedle, TrivialInput) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; - auto const col = [] { - auto child1 = col_wrapper{1, 2, 3}; - auto child2 = col_wrapper{4, 5, 6}; + auto const haystack = [] { + auto child1 = tdata_col{1, 2, 3}; + auto child2 = tdata_col{4, 5, 6}; auto child3 = strings_col{"x", "y", "z"}; return structs_col{{child1, child2, child3}}; }(); - auto const val1 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle1 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{"x"}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - auto const val2 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle2 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{"a"}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - EXPECT_EQ(true, cudf::contains(col, val1)); - EXPECT_EQ(false, cudf::contains(col, val2)); + EXPECT_TRUE(cudf::contains(haystack, needle1)); + EXPECT_FALSE(cudf::contains(haystack, needle2)); } -TYPED_TEST(TypedScalarStructContainTest, SlicedColumnInputTests) +TYPED_TEST(TypedStructContainsTestScalarNeedle, SlicedColumnInput) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; - constexpr int32_t dont_care{0}; - - auto const col_original = [] { - auto child1 = col_wrapper{dont_care, dont_care, 1, 2, 3, dont_care}; - auto child2 = col_wrapper{dont_care, dont_care, 4, 5, 6, dont_care}; + auto const haystack_original = [] { + auto child1 = tdata_col{dont_care, dont_care, 1, 2, 3, dont_care}; + auto child2 = tdata_col{dont_care, dont_care, 4, 5, 6, dont_care}; auto child3 = strings_col{"dont_care", "dont_care", "x", "y", "z", "dont_care"}; return structs_col{{child1, child2, child3}}; }(); - auto const col = cudf::slice(col_original, {2, 5})[0]; + auto const haystack = cudf::slice(haystack_original, {2, 5})[0]; - auto const val1 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle1 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{"x"}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - auto const val2 = [] { - auto child1 = col_wrapper{dont_care}; - auto child2 = col_wrapper{dont_care}; + auto const needle2 = [] { + auto child1 = tdata_col{dont_care}; + auto child2 = tdata_col{dont_care}; auto child3 = strings_col{"dont_care"}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - EXPECT_EQ(true, cudf::contains(col, val1)); - EXPECT_EQ(false, cudf::contains(col, val2)); + EXPECT_TRUE(cudf::contains(haystack, needle1)); + EXPECT_FALSE(cudf::contains(haystack, needle2)); } -TYPED_TEST(TypedScalarStructContainTest, SimpleInputWithNullsTests) +TYPED_TEST(TypedStructContainsTestScalarNeedle, SimpleInputWithNulls) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; + using tdata_col = cudf::test::fixed_width_column_wrapper; constexpr int32_t null{0}; // Test with nulls at the top level. { - auto const col = [] { - auto child1 = col_wrapper{1, null, 3}; - auto child2 = col_wrapper{4, null, 6}; + auto const col1 = [] { + auto child1 = tdata_col{1, null, 3}; + auto child2 = tdata_col{4, null, 6}; auto child3 = strings_col{"x", "" /*NULL*/, "z"}; return structs_col{{child1, child2, child3}, null_at(1)}; }(); - auto const val1 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle1 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{"x"}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - auto const val2 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle2 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{"a"}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); + }(); + auto const needle3 = [] { + auto child1 = tdata_col{{null}, null_at(0)}; + auto child2 = tdata_col{{null}, null_at(0)}; + auto child3 = strings_col{{""}, null_at(0)}; + return make_struct_scalar(child1, child2, child3); }(); - EXPECT_EQ(true, cudf::contains(col, val1)); - EXPECT_EQ(false, cudf::contains(col, val2)); + EXPECT_TRUE(cudf::contains(col1, needle1)); + EXPECT_FALSE(cudf::contains(col1, needle2)); + EXPECT_FALSE(cudf::contains(col1, needle3)); } // Test with nulls at the children level. { auto const col = [] { - auto child1 = col_wrapper{{1, null, 3}, null_at(1)}; - auto child2 = col_wrapper{{4, null, 6}, null_at(1)}; - auto child3 = strings_col{{"" /*NULL*/, "y", "z"}, null_at(0)}; + auto child1 = tdata_col{{1, null, 3}, null_at(1)}; + auto child2 = tdata_col{{4, null, 6}, null_at(1)}; + auto child3 = strings_col{{"" /*NULL*/, "" /*NULL*/, "z"}, nulls_at({0, 1})}; return structs_col{{child1, child2, child3}}; }(); - auto const val1 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle1 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{{"" /*NULL*/}, null_at(0)}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - auto const val2 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle2 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{""}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); + }(); + auto const needle3 = [] { + auto child1 = tdata_col{{null}, null_at(0)}; + auto child2 = tdata_col{{null}, null_at(0)}; + auto child3 = strings_col{{""}, null_at(0)}; + return make_struct_scalar(child1, child2, child3); }(); - EXPECT_EQ(true, cudf::contains(col, val1)); - EXPECT_EQ(false, cudf::contains(col, val2)); + EXPECT_TRUE(cudf::contains(col, needle1)); + EXPECT_FALSE(cudf::contains(col, needle2)); + EXPECT_TRUE(cudf::contains(col, needle3)); } // Test with nulls in the input scalar. { - auto const col = [] { - auto child1 = col_wrapper{1, 2, 3}; - auto child2 = col_wrapper{4, 5, 6}; + auto const haystack = [] { + auto child1 = tdata_col{1, 2, 3}; + auto child2 = tdata_col{4, 5, 6}; auto child3 = strings_col{"x", "y", "z"}; return structs_col{{child1, child2, child3}}; }(); - auto const val1 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle1 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{"x"}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - auto const val2 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle2 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{{"" /*NULL*/}, null_at(0)}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - EXPECT_EQ(true, cudf::contains(col, val1)); - EXPECT_EQ(false, cudf::contains(col, val2)); + EXPECT_TRUE(cudf::contains(haystack, needle1)); + EXPECT_FALSE(cudf::contains(haystack, needle2)); } } -TYPED_TEST(TypedScalarStructContainTest, SlicedInputWithNullsTests) +TYPED_TEST(TypedStructContainsTestScalarNeedle, SlicedInputWithNulls) { - using col_wrapper = cudf::test::fixed_width_column_wrapper; - - constexpr int32_t dont_care{0}; - constexpr int32_t null{0}; + using tdata_col = cudf::test::fixed_width_column_wrapper; // Test with nulls at the top level. { - auto const col_original = [] { - auto child1 = col_wrapper{dont_care, dont_care, 1, null, 3, dont_care}; - auto child2 = col_wrapper{dont_care, dont_care, 4, null, 6, dont_care}; + auto const haystack_original = [] { + auto child1 = tdata_col{dont_care, dont_care, 1, null, 3, dont_care}; + auto child2 = tdata_col{dont_care, dont_care, 4, null, 6, dont_care}; auto child3 = strings_col{"dont_care", "dont_care", "x", "" /*NULL*/, "z", "dont_care"}; return structs_col{{child1, child2, child3}, null_at(3)}; }(); - auto const col = cudf::slice(col_original, {2, 5})[0]; + auto const col = cudf::slice(haystack_original, {2, 5})[0]; - auto const val1 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle1 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{"x"}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - auto const val2 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle2 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{"a"}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - EXPECT_EQ(true, cudf::contains(col, val1)); - EXPECT_EQ(false, cudf::contains(col, val2)); + EXPECT_TRUE(cudf::contains(col, needle1)); + EXPECT_FALSE(cudf::contains(col, needle2)); } // Test with nulls at the children level. { - auto const col_original = [] { + auto const haystack_original = [] { auto child1 = - col_wrapper{{dont_care, dont_care /*also NULL*/, 1, null, 3, dont_care}, null_at(3)}; + tdata_col{{dont_care, dont_care /*also NULL*/, 1, null, 3, dont_care}, null_at(3)}; auto child2 = - col_wrapper{{dont_care, dont_care /*also NULL*/, 4, null, 6, dont_care}, null_at(3)}; + tdata_col{{dont_care, dont_care /*also NULL*/, 4, null, 6, dont_care}, null_at(3)}; auto child3 = strings_col{ {"dont_care", "dont_care" /*also NULL*/, "" /*NULL*/, "y", "z", "dont_care"}, null_at(2)}; return structs_col{{child1, child2, child3}, null_at(1)}; }(); - auto const col = cudf::slice(col_original, {2, 5})[0]; + auto const haystack = cudf::slice(haystack_original, {2, 5})[0]; - auto const val1 = [] { - auto child1 = col_wrapper{1}; - auto child2 = col_wrapper{4}; + auto const needle1 = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{4}; auto child3 = strings_col{{"x"}, null_at(0)}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - auto const val2 = [] { - auto child1 = col_wrapper{dont_care}; - auto child2 = col_wrapper{dont_care}; + auto const needle2 = [] { + auto child1 = tdata_col{dont_care}; + auto child2 = tdata_col{dont_care}; auto child3 = strings_col{"dont_care"}; - return cudf::struct_scalar(std::vector{child1, child2, child3}); + return make_struct_scalar(child1, child2, child3); }(); - EXPECT_EQ(true, cudf::contains(col, val1)); - EXPECT_EQ(false, cudf::contains(col, val2)); + EXPECT_TRUE(cudf::contains(haystack, needle1)); + EXPECT_FALSE(cudf::contains(haystack, needle2)); + } +} + +template +struct TypedStructContainsTestColumnNeedles : public cudf::test::BaseFixture { +}; + +TYPED_TEST_SUITE(TypedStructContainsTestColumnNeedles, TestTypes); + +TYPED_TEST(TypedStructContainsTestColumnNeedles, EmptyInput) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const haystack = [] { + auto child1 = tdata_col{}; + auto child2 = tdata_col{}; + auto child3 = strings_col{}; + return structs_col{{child1, child2, child3}}; + }(); + + { + auto const needles = [] { + auto child1 = tdata_col{}; + auto child2 = tdata_col{}; + auto child3 = strings_col{}; + return structs_col{{child1, child2, child3}}; + }(); + auto const expected = bools_col{}; + auto const result = cudf::contains(haystack, needles); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); + } + + { + auto const needles = [] { + auto child1 = tdata_col{1, 2}; + auto child2 = tdata_col{0, 2}; + auto child3 = strings_col{"x", "y"}; + return structs_col{{child1, child2, child3}}; + }(); + auto const result = cudf::contains(haystack, needles); + auto const expected = bools_col{0, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); } } + +TYPED_TEST(TypedStructContainsTestColumnNeedles, TrivialInput) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const haystack = [] { + auto child1 = tdata_col{1, 3, 1, 1, 2, 1, 2, 2, 1, 2}; + auto child2 = tdata_col{1, 0, 0, 0, 1, 0, 1, 2, 1, 1}; + return structs_col{{child1, child2}}; + }(); + + auto const needles = [] { + auto child1 = tdata_col{1, 3, 1, 1, 2, 1, 0, 0, 1, 0}; + auto child2 = tdata_col{1, 0, 2, 3, 2, 1, 0, 0, 1, 0}; + return structs_col{{child1, child2}}; + }(); + + auto const expected = bools_col{1, 1, 0, 0, 1, 1, 0, 0, 1, 0}; + auto const result = cudf::contains(haystack, needles); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); +} + +TYPED_TEST(TypedStructContainsTestColumnNeedles, SlicedInputNoNulls) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const haystack_original = [] { + auto child1 = tdata_col{dont_care, dont_care, 1, 3, 1, 1, 2, dont_care}; + auto child2 = tdata_col{dont_care, dont_care, 1, 0, 0, 0, 1, dont_care}; + auto child3 = strings_col{"dont_care", "dont_care", "x", "y", "z", "a", "b", "dont_care"}; + return structs_col{{child1, child2, child3}}; + }(); + auto const haystack = cudf::slice(haystack_original, {2, 7})[0]; + + auto const needles_original = [] { + auto child1 = tdata_col{dont_care, 1, 1, 1, 1, 2, dont_care, dont_care}; + auto child2 = tdata_col{dont_care, 0, 1, 2, 3, 1, dont_care, dont_care}; + auto child3 = strings_col{"dont_care", "z", "x", "z", "a", "b", "dont_care", "dont_care"}; + return structs_col{{child1, child2, child3}}; + }(); + auto const needles = cudf::slice(needles_original, {1, 6})[0]; + + auto const expected = bools_col{1, 1, 0, 0, 1}; + auto const result = cudf::contains(haystack, needles); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); +} + +TYPED_TEST(TypedStructContainsTestColumnNeedles, SlicedInputHavingNulls) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const haystack_original = [] { + auto child1 = + tdata_col{{dont_care /*null*/, dont_care, 1, null, XXX, 1, 2, null, 2, 2, null, 2, dont_care}, + nulls_at({0, 3, 7, 10})}; + auto child2 = + tdata_col{{dont_care /*null*/, dont_care, 1, null, XXX, 0, null, 0, 1, 2, 1, 1, dont_care}, + nulls_at({0, 3, 6})}; + return structs_col{{child1, child2}, nulls_at({1, 4})}; + }(); + auto const haystack = cudf::slice(haystack_original, {2, 12})[0]; + + auto const needles_original = [] { + auto child1 = + tdata_col{{dont_care, XXX, null, 1, 1, 2, XXX, null, 1, 1, null, dont_care, dont_care}, + nulls_at({2, 7, 10})}; + auto child2 = + tdata_col{{dont_care, XXX, null, 2, 3, 2, XXX, null, null, 1, 0, dont_care, dont_care}, + nulls_at({2, 7, 8})}; + return structs_col{{child1, child2}, nulls_at({1, 6})}; + }(); + auto const needles = cudf::slice(needles_original, {1, 11})[0]; + + auto const expected = bools_col{{null, 1, 0, 0, 1, null, 1, 0, 1, 1}, nulls_at({0, 5})}; + auto const result = cudf::contains(haystack, needles); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); +} + +TYPED_TEST(TypedStructContainsTestColumnNeedles, StructOfLists) +{ + using lists_col = cudf::test::lists_column_wrapper; + + auto const haystack = [] { + // clang-format off + auto child1 = lists_col{{1, 2}, {1}, {}, {1, 3}}; + auto child2 = lists_col{{1, 3, 4}, {2, 3, 4}, {}, {}}; + // clang-format on + return structs_col{{child1, child2}}; + }(); + + auto const needles = [] { + // clang-format off + auto child1 = lists_col{{1, 2}, {1}, {}, {1, 3}, {}}; + auto child2 = lists_col{{1, 3, 4}, {2, 3}, {1, 2}, {}, {}}; + // clang-format on + return structs_col{{child1, child2}}; + }(); + + auto const expected = bools_col{1, 0, 0, 1, 1}; + auto const result = cudf::contains(haystack, needles); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *result, verbosity); +} From 288c81fd59d1b28541acb39235b314476f8a846a Mon Sep 17 00:00:00 2001 From: Ed Seidl Date: Thu, 18 Aug 2022 08:53:54 -0700 Subject: [PATCH 46/58] Support additional dictionary bit widths in Parquet writer (#11547) Continuation of #11216, this adds ability to use 3, 5, 6, 10, and 20 bit dictionary keys in the Parquet encoder. Also adds unit tests for each of the supported bit widths. Authors: - Ed Seidl (https://github.com/etseidl) Approvers: - Mike Wilson (https://github.com/hyperbolic2346) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/11547 --- cpp/src/io/parquet/page_enc.cu | 243 +++++++++++++++++++++--------- cpp/src/io/parquet/writer_impl.cu | 5 +- cpp/tests/io/parquet_test.cpp | 90 +++++++++++ 3 files changed, 263 insertions(+), 75 deletions(-) diff --git a/cpp/src/io/parquet/page_enc.cu b/cpp/src/io/parquet/page_enc.cu index 8181c76c065..7a48ca535c3 100644 --- a/cpp/src/io/parquet/page_enc.cu +++ b/cpp/src/io/parquet/page_enc.cu @@ -460,89 +460,186 @@ inline __device__ uint8_t* VlqEncode(uint8_t* p, uint32_t v) } /** - * @brief Pack literal values in output bitstream (1,2,4,8,12 or 16 bits per value) + * @brief Pack literal values in output bitstream (1,2,3,4,5,6,8,10,12,16,20 or 24 bits per value) */ -inline __device__ void PackLiterals( +inline __device__ void PackLiteralsShuffle( uint8_t* dst, uint32_t v, uint32_t count, uint32_t w, uint32_t t) { - if (w == 1 || w == 2 || w == 4 || w == 8 || w == 12 || w == 16 || w == 24) { - if (t <= (count | 0x1f)) { - if (w == 1 || w == 2 || w == 4) { - uint32_t mask = 0; - if (w == 1) { - v |= shuffle_xor(v, 1) << 1; - v |= shuffle_xor(v, 2) << 2; - v |= shuffle_xor(v, 4) << 4; - mask = 0x7; - } else if (w == 2) { - v |= shuffle_xor(v, 1) << 2; - v |= shuffle_xor(v, 2) << 4; - mask = 0x3; - } else if (w == 4) { - v |= shuffle_xor(v, 1) << 4; - mask = 0x1; - } - if (t < count && mask && !(t & mask)) { dst[(t * w) >> 3] = v; } - return; - } else if (w == 8) { - if (t < count) { dst[t] = v; } - return; - } else if (w == 12) { - v |= shuffle_xor(v, 1) << 12; - if (t < count && !(t & 1)) { - dst[(t >> 1) * 3 + 0] = v; - dst[(t >> 1) * 3 + 1] = v >> 8; - dst[(t >> 1) * 3 + 2] = v >> 16; - } - return; - } else if (w == 16) { - if (t < count) { - dst[t * 2 + 0] = v; - dst[t * 2 + 1] = v >> 8; - } - return; - } else if (w == 24) { - if (t < count) { - dst[t * 3 + 0] = v; - dst[t * 3 + 1] = v >> 8; - dst[t * 3 + 2] = v >> 16; - } - return; + constexpr uint32_t MASK2T = 1; // mask for 2 thread leader + constexpr uint32_t MASK4T = 3; // mask for 4 thread leader + constexpr uint32_t MASK8T = 7; // mask for 8 thread leader + uint64_t vt; + + if (t > (count | 0x1f)) { return; } + + switch (w) { + case 1: + v |= shuffle_xor(v, 1) << 1; + v |= shuffle_xor(v, 2) << 2; + v |= shuffle_xor(v, 4) << 4; + if (t < count && !(t & MASK8T)) { dst[(t * w) >> 3] = v; } + return; + case 2: + v |= shuffle_xor(v, 1) << 2; + v |= shuffle_xor(v, 2) << 4; + if (t < count && !(t & MASK4T)) { dst[(t * w) >> 3] = v; } + return; + case 3: + v |= shuffle_xor(v, 1) << 3; + v |= shuffle_xor(v, 2) << 6; + v |= shuffle_xor(v, 4) << 12; + if (t < count && !(t & MASK8T)) { + dst[(t >> 3) * 3 + 0] = v; + dst[(t >> 3) * 3 + 1] = v >> 8; + dst[(t >> 3) * 3 + 2] = v >> 16; + } + return; + case 4: + v |= shuffle_xor(v, 1) << 4; + if (t < count && !(t & MASK2T)) { dst[(t * w) >> 3] = v; } + return; + case 5: + v |= shuffle_xor(v, 1) << 5; + v |= shuffle_xor(v, 2) << 10; + vt = shuffle_xor(v, 4); + vt = vt << 20 | v; + if (t < count && !(t & MASK8T)) { + dst[(t >> 3) * 5 + 0] = vt; + dst[(t >> 3) * 5 + 1] = vt >> 8; + dst[(t >> 3) * 5 + 2] = vt >> 16; + dst[(t >> 3) * 5 + 3] = vt >> 24; + dst[(t >> 3) * 5 + 4] = vt >> 32; + } + return; + case 6: + v |= shuffle_xor(v, 1) << 6; + v |= shuffle_xor(v, 2) << 12; + if (t < count && !(t & MASK4T)) { + dst[(t >> 2) * 3 + 0] = v; + dst[(t >> 2) * 3 + 1] = v >> 8; + dst[(t >> 2) * 3 + 2] = v >> 16; + } + return; + case 8: + if (t < count) { dst[t] = v; } + return; + case 10: + v |= shuffle_xor(v, 1) << 10; + vt = shuffle_xor(v, 2); + vt = vt << 20 | v; + if (t < count && !(t & MASK4T)) { + dst[(t >> 2) * 5 + 0] = vt; + dst[(t >> 2) * 5 + 1] = vt >> 8; + dst[(t >> 2) * 5 + 2] = vt >> 16; + dst[(t >> 2) * 5 + 3] = vt >> 24; + dst[(t >> 2) * 5 + 4] = vt >> 32; + } + return; + case 12: + v |= shuffle_xor(v, 1) << 12; + if (t < count && !(t & MASK2T)) { + dst[(t >> 1) * 3 + 0] = v; + dst[(t >> 1) * 3 + 1] = v >> 8; + dst[(t >> 1) * 3 + 2] = v >> 16; + } + return; + case 16: + if (t < count) { + dst[t * 2 + 0] = v; + dst[t * 2 + 1] = v >> 8; + } + return; + case 20: + vt = shuffle_xor(v, 1); + vt = vt << 20 | v; + if (t < count && !(t & MASK2T)) { + dst[(t >> 1) * 5 + 0] = vt; + dst[(t >> 1) * 5 + 1] = vt >> 8; + dst[(t >> 1) * 5 + 2] = vt >> 16; + dst[(t >> 1) * 5 + 3] = vt >> 24; + dst[(t >> 1) * 5 + 4] = vt >> 32; + } + return; + case 24: + if (t < count) { + dst[t * 3 + 0] = v; + dst[t * 3 + 1] = v >> 8; + dst[t * 3 + 2] = v >> 16; } - } else { return; - } - } else if (w <= 16) { - // Scratch space to temporarily write to. Needed because we will use atomics to write 32 bit - // words but the destination mem may not be a multiple of 4 bytes. - // TODO (dm): This assumes blockdim = 128 and max bits per value = 16. Reduce magic numbers. - __shared__ uint32_t scratch[64]; - if (t < 64) { scratch[t] = 0; } - __syncthreads(); - if (t <= count) { - uint64_t v64 = v; - v64 <<= (t * w) & 0x1f; + default: CUDF_UNREACHABLE("Unsupported bit width"); + } +} - // Copy 64 bit word into two 32 bit words while following C++ strict aliasing rules. - uint32_t v32[2]; - memcpy(&v32, &v64, sizeof(uint64_t)); +/** + * @brief Pack literals of arbitrary bit-length in output bitstream. + */ +inline __device__ void PackLiteralsRoundRobin( + uint8_t* dst, uint32_t v, uint32_t count, uint32_t w, uint32_t t) +{ + // Scratch space to temporarily write to. Needed because we will use atomics to write 32 bit + // words but the destination mem may not be a multiple of 4 bytes. + // TODO (dm): This assumes blockdim = 128 and max bits per value = 16. Reduce magic numbers. + // To allow up to 24 bit this needs to be sized at 96 words. + __shared__ uint32_t scratch[64]; + if (t < 64) { scratch[t] = 0; } + __syncthreads(); - // Atomically write result to scratch - if (v32[0]) { atomicOr(scratch + ((t * w) >> 5), v32[0]); } - if (v32[1]) { atomicOr(scratch + ((t * w) >> 5) + 1, v32[1]); } - } - __syncthreads(); + if (t <= count) { + // shift symbol left by up to 31 bits + uint64_t v64 = v; + v64 <<= (t * w) & 0x1f; - // Copy scratch data to final destination - auto available_bytes = (count * w + 7) / 8; + // Copy 64 bit word into two 32 bit words while following C++ strict aliasing rules. + uint32_t v32[2]; + memcpy(&v32, &v64, sizeof(uint64_t)); - auto scratch_bytes = reinterpret_cast(&scratch[0]); - if (t < available_bytes) { dst[t] = scratch_bytes[t]; } - if (t + 128 < available_bytes) { dst[t + 128] = scratch_bytes[t + 128]; } - __syncthreads(); - } else { - CUDF_UNREACHABLE("Unsupported bit width"); + // Atomically write result to scratch + if (v32[0]) { atomicOr(scratch + ((t * w) >> 5), v32[0]); } + if (v32[1]) { atomicOr(scratch + ((t * w) >> 5) + 1, v32[1]); } + } + __syncthreads(); + + // Copy scratch data to final destination + auto available_bytes = (count * w + 7) / 8; + + auto scratch_bytes = reinterpret_cast(&scratch[0]); + if (t < available_bytes) { dst[t] = scratch_bytes[t]; } + if (t + 128 < available_bytes) { dst[t + 128] = scratch_bytes[t + 128]; } + // would need the following for up to 24 bits + // if (t + 256 < available_bytes) { dst[t + 256] = scratch_bytes[t + 256]; } + __syncthreads(); +} + +/** + * @brief Pack literal values in output bitstream + */ +inline __device__ void PackLiterals( + uint8_t* dst, uint32_t v, uint32_t count, uint32_t w, uint32_t t) +{ + switch (w) { + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 8: + case 10: + case 12: + case 16: + case 20: + case 24: + // bit widths that lie on easy boundaries can be handled either directly + // (8, 16, 24) or through fast shuffle operations. + PackLiteralsShuffle(dst, v, count, w, t); + break; + default: + if (w > 16) { CUDF_UNREACHABLE("Unsupported bit width"); } + // less efficient bit packing that uses atomics, but can handle arbitrary + // bit widths up to 16. used for repetition and definition level encoding + PackLiteralsRoundRobin(dst, v, count, w, t); } } diff --git a/cpp/src/io/parquet/writer_impl.cu b/cpp/src/io/parquet/writer_impl.cu index 755cec0636c..0510fb77ea8 100644 --- a/cpp/src/io/parquet/writer_impl.cu +++ b/cpp/src/io/parquet/writer_impl.cu @@ -1067,9 +1067,10 @@ auto build_chunk_dictionaries(hostdevice_2dvector& chunks, if (nbits > 24) { return std::pair(false, 0); } // Only these bit sizes are allowed for RLE encoding because it's compute optimized - constexpr auto allowed_bitsizes = std::array{1, 2, 4, 8, 12, 16, 24}; + constexpr auto allowed_bitsizes = + std::array{1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24}; - // ceil to (1/2/4/8/12/16/24) + // ceil to (1/2/3/4/5/6/8/10/12/16/20/24) auto rle_bits = *std::lower_bound(allowed_bitsizes.begin(), allowed_bitsizes.end(), nbits); auto rle_byte_size = util::div_rounding_up_safe(ck.num_values * rle_bits, 8); diff --git a/cpp/tests/io/parquet_test.cpp b/cpp/tests/io/parquet_test.cpp index c218c4088bb..c8fb16ee93b 100644 --- a/cpp/tests/io/parquet_test.cpp +++ b/cpp/tests/io/parquet_test.cpp @@ -229,6 +229,26 @@ void read_footer(const std::unique_ptr& source, CUDF_EXPECTS(res, "Cannot parse file metadata"); } +// returns the number of bits used for dictionary encoding data at the given page location. +// this assumes the data is uncompressed. +// throws cudf::logic_error if the page_loc data is invalid. +int read_dict_bits(const std::unique_ptr& source, + const cudf::io::parquet::PageLocation& page_loc) +{ + CUDF_EXPECTS(page_loc.offset > 0, "Cannot find page header"); + CUDF_EXPECTS(page_loc.compressed_page_size > 0, "Invalid page header length"); + + cudf::io::parquet::PageHeader page_hdr; + const auto page_buf = source->host_read(page_loc.offset, page_loc.compressed_page_size); + cudf::io::parquet::CompactProtocolReader cp(page_buf->data(), page_buf->size()); + bool res = cp.read(&page_hdr); + CUDF_EXPECTS(res, "Cannot parse page header"); + + // cp should be pointing at the start of page data now. the first byte + // should be the encoding bit size + return cp.getb(); +} + // read column index from datasource at location indicated by chunk, // parse and return as a ColumnIndex struct. // throws cudf::logic_error if the chunk data is invalid. @@ -362,6 +382,18 @@ struct ParquetChunkedWriterNumericTypeTest : public ParquetChunkedWriterTest { // Declare typed test cases TYPED_TEST_SUITE(ParquetChunkedWriterNumericTypeTest, SupportedTypes); +// Base test fixture for size-parameterized tests +class ParquetSizedTest : public ::testing::TestWithParam { +}; + +// test the allowed bit widths for dictionary encoding +// values chosen to trigger 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, and 24 bit dictionaries +INSTANTIATE_TEST_SUITE_P( + ParquetDictionaryTest, + ParquetSizedTest, + testing::Values(2, 4, 8, 16, 32, 64, 256, 1024, 4096, 65536, 128 * 1024, 2 * 1024 * 1024), + testing::PrintToStringParamName()); + namespace { // Generates a vector of uniform random values of type T template @@ -4204,4 +4236,62 @@ TEST_F(ParquetReaderTest, StructByteArray) CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); } +TEST_P(ParquetSizedTest, DictionaryTest) +{ + constexpr int nrows = 3'000'000; + + auto elements = cudf::detail::make_counting_transform_iterator(0, [](auto i) { + return "a unique string value suffixed with " + std::to_string(i % GetParam()); + }); + auto const col0 = cudf::test::strings_column_wrapper(elements, elements + nrows); + auto const expected = table_view{{col0}}; + + auto const filepath = temp_env->get_temp_filepath("DictionaryTest.parquet"); + // set row group size so that there will be only one row group + // no compression so we can easily read page data + cudf::io::parquet_writer_options out_opts = + cudf_io::parquet_writer_options::builder(cudf_io::sink_info{filepath}, expected) + .compression(cudf::io::compression_type::NONE) + .stats_level(cudf::io::statistics_freq::STATISTICS_COLUMN) + .row_group_size_rows(nrows) + .row_group_size_bytes(256 * 1024 * 1024); + cudf::io::write_parquet(out_opts); + + cudf::io::parquet_reader_options default_in_opts = + cudf::io::parquet_reader_options::builder(cudf_io::source_info{filepath}); + auto const result = cudf_io::read_parquet(default_in_opts); + + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); + + // make sure dictionary was used + auto const source = cudf::io::datasource::create(filepath); + cudf::io::parquet::FileMetaData fmd; + + read_footer(source, &fmd); + auto used_dict = [&fmd]() { + for (auto enc : fmd.row_groups[0].columns[0].meta_data.encodings) { + if (enc == cudf::io::parquet::Encoding::PLAIN_DICTIONARY or + enc == cudf::io::parquet::Encoding::RLE_DICTIONARY) { + return true; + } + } + return false; + }; + EXPECT_TRUE(used_dict()); + + // and check that the correct number of bits was used + auto const oi = read_offset_index(source, fmd.row_groups[0].columns[0]); + auto const nbits = read_dict_bits(source, oi.page_locations[0]); + auto const expected_bits = + cudf::io::parquet::CompactProtocolReader::NumRequiredBits(GetParam() - 1); + + // copied from writer_impl.cu + constexpr auto allowed_bitsizes = + std::array{1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24}; + auto const rle_bits = + *std::lower_bound(allowed_bitsizes.begin(), allowed_bitsizes.end(), expected_bits); + + EXPECT_EQ(nbits, rle_bits); +} + CUDF_TEST_PROGRAM_MAIN() From be57c5e9f30c8b2d56fe45435cde2d1e2118b1d5 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 18 Aug 2022 18:34:26 -0400 Subject: [PATCH 47/58] Remove unused cpp/img folder (#11554) This folder contains 3 image (.png) files that are not referenced anywhere in the repo. One of them includes mention of GDF which is the original name for cuDF. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Nghia Truong (https://github.com/ttnghia) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11554 --- cpp/img/GPU_df_arch_diagram.png | Bin 321563 -> 0 bytes cpp/img/goai_logo_3.png | Bin 18845 -> 0 bytes cpp/img/mapd-conda-h2o.png | Bin 129602 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cpp/img/GPU_df_arch_diagram.png delete mode 100644 cpp/img/goai_logo_3.png delete mode 100644 cpp/img/mapd-conda-h2o.png diff --git a/cpp/img/GPU_df_arch_diagram.png b/cpp/img/GPU_df_arch_diagram.png deleted file mode 100644 index 53110f2d2abe45c2e92ca01dcbe6a37d07b249c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 321563 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 B=&Mqnk>vSKezLC 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 i8Tq4l( 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�CGXW*Ga=c`| z`K~~7bXlySD6h?5H=}Cz1L{9Vt^8zwoW7wq)?;5>P0rYX<(uDbVV$yz3AKw+UxnNR zgCTyhyB1|eqCDrYOVQ-}4@M0mfH6Emfu1dNK#YO;_Cot}a}Vc0ffeU>btYy$O?AO2 z{d{g~Rn;5f--`Mc-zRdYbVDlS42Q`QUZ5GcXa6M9)V zP|w4l6-`CA4`>!!sylHttE&0Eorro83j%*6uDvYr<_(I!f1px6E>bAWLaeZzGsH=q zDNY@>ncQQ`tRtJlXS~Vfj(ExmuaG9sV<|j{Hxr(s(6>z1OF<(+u9pS2eEfrm_i;9; z;Iy-R5B})c8z!b3ozguugvVyYTX>sY&e$C~K70NCx-nxYGoLTlRc&xyhK7K%U}zfI zs1bF~nB$4pgWKbM*58Nwquo&&%;Nck;G$tkqv!5Rl_Wm3tW^#?uo`&~xxn@Y6fGDp zjb|^7Sf;ssEQkiKdyk+Yr}qWlFy453N|Ry+Xl-|bfN|k}2)9ZmH&DrR%-K#Xu*{vO zu^Vx%pR2pt%q~~r4~;UO@o%g=&t8pqh1g-zGWc^X3h_Gm5M8v04bJtPF{AgLEt}e2 zGChpFiO#N$GDgKAOcyr%bMOU^Ac_G+7FN1H`RA_^Bj{*>&i|;{sz4i$W!9 z`u#<;Q5FQieg{86Mb_^ZxzYaTdvX+|2CD6_;hZignXP>JJ?x^zlCs3Vw+q;-*w3Tt zkYmE^vhy1|XC2dw+0R%e?~3D*l=5QmgGk4%2g$mt6vL>c(Y)I5!4LX5HIC-sL==@X zF12uk<2RYo(S6(Ttj>u!-oUEHm^#s67W3qz6Cx+qVHNnORry@;T4s~w*>o>0M@)Hu z;ByRCRsua~`c_Aj!1yHGI8?a>P-)bW+s5v=%RqCH7^g682>(@*L8n|Hl`au0>-51g z-Cd)G5t{j=S9q*R9elJKv5r;^fD8e^p>C`k;E81N&1(h++ zGY^sFh;`u0*r&FeK##FS2B+@BC}$)whR;Brx-~q>Wv=BZ8~vMM zvl6@#EFsFpy$*ARy06|H`No4=vS#IgEOJmvd;Z5f(Kli>wohcg5a5X@FdE-d{U!~n zIAZi*>q-5ckimz^y!6&@EB5t#K}8Y@oo>#9A6UX%)W_Z0aa%QREF^}}EW~a=KQtS> z74%9U#$N*I%lu?VUG!B|j0A5&Xy1qSobN93xnd%kr}C4%z`nQt?{eKvhyNggf8&Dx zTmfSWO#e;TJMqw&gqp}kx_z}KVN4p6Kf?QDCL4&z#{IDs%9q5lR$}%c7NerH8ReQb zj&s#%76bFve&u@nCcD*k&1zy| znZ0P2s^As0)wu=W4MB@~T3uCiLlu}o@T)5~n9))&Zu0|XpLndr#yUd&9(?RbtoH&* zI*v_(S$KE(N7l`CtIcloOzv5b;TSiOnO79#q=<*$oPDOHVK9Zz2^Nxc@diyNws42D z`bUQb)xr;+dG0^@r0gTxLCUa&_8lwpMoxg_T>u~$umdCF0slIBGlEF0cn}jbf&RsI zx&ywyxJG5u)p@ylc7wJURwNU)^@E_M?&{10=MI75uDOwJ{Pzsk zt){#5fqr>W-Q}Pgx^9geJ&U|i1b=x>Pd)6b--5ag|3htQjaj%3IFG>Ng6>7ab;h!@ z1-lcl0JRab;9u-VH9*+Mz!r$nC6Nm?;KGKX1?|_5nZa4qKJ@iI2z586u>{72U`Z-m zYg!wC4&D3-ORm2^n1f^|Y_Bvb|M;{wKx#D8xF0THL@t)HXj}7|>8>wP;HU3t7qFwj zO3#qTcG05Z))BN*+eF_8%7~Jerq$Q@J}Ugy5_)VSl5`4I z7c$lg^Z?fP*Qon_n63tL4)7%Xj@yi9meT&MNfR{a_;lgzp@H44@L^2md8EIZm`82E zdz*&rDn@(vAoC~uKNqviztW4Ci#m*QQtxP#7bSwS3eo}+^HRWh<_*Dm-((GW=}gqe zbqCE2%o(6keNgh2v z)<$fBFU8-ocV+`x69Us>H(`b^Ce?<;8!$M9!;zN2(%!KSr z^aLx%LHOs95`b+fc*eG)m;=cc-n^psabo|8Xhl=HINw$hHtS<9LfvB`$ZSx!c1N5; zoSYam2~Ld}`{EU^D3ei0U{k8e0(6^a*vN%_vFL9G!f@-SWOkbqviO-mX9y3g>`5V0 z?SQg@d3&=FU-^~(iDKxUR=xMv{8l_YzMHANM!&!}gEn$iSHk%A#w{Iuj`w)pl)5KGR>4(5vz(f2rs3o&hAt3PFx@ zd#QMOe0@H&$+sQN>T*;O^c6#x#W(o3WZQq=Iqq-sk%LMRGv&cf{L{a#Vx281WcHP- z15&<#LCs*eKurp!(|)8^VW(*D1l4S1X_2K_o#u-?GLl5_X>GScEG+tSz=c+M^NN|*RBj8zc+eAr z4mePMwUNi+e_`A=+d2P}{Bb5q(eE9Jo=#GRF=pjJN-1W@LtjkW4W>L#4_rm>^rY0Buf8GlOWXkIB5cBzVx-{L?C)0MhS@vM<%4~wI>H}+Sd zHDwj$eo4Dk^!l$BNjPAWlDUYoCZGN>pA$D^S}Vt1s7UmXC{f*U3D8C~kZ-3u;*;Uk zSTD)RxAYA|KCJojZZ0O@|3o13(+Q1Y$3E687Y)il#wAfbMkk_>_S(#O5$>?|4|Nan zlXmB0O~5}-UcL`n?;rS^y^OHflcl3mdOzzH{<|4CaZdZzn{8`r+to(B4i zC%E*GEK%bmx%9!bQhEI4kxaq#uW9ZVFhP^VLALz}?t-4sd7VuP=Z5Ro7EDv~m$i(v zv|i$|H9vS~xG}Z{3_hw88G+R@gN*&GZ><`J#%KuW5&C9Pd>h3C`_*H>rx|4yvH!uwuro(T? z&jhCDDWFPM1#p7aTZWoXGXX}(Vnf^=dLujFPfbp!lZ} z>Mdf>^fQ9bouhJi8^7vNn^9P%_5#+@bsB&K-b=0{y{Q|Sk(#7}WdZMNg`&C; zUSwK$`K^_Tjn);JA*KFOj{sdGiflT+O)}UT6&&S!Mqa zU?K#mge1DlSio@UdWa0)hku?4e8s9=<{Wageef+e{JLyj1dG9!p)cVT z@TRhWIT}c+mFXD)lX=AtnJ&!Zc!UY0QHJ|uj}6P~mgv1whm$9|H46T6lMonVUG$Dm z`iBN=v1yjh&cQ*OxHI9~M}HJN=E zKWMp|fwdSsp~+3TG+km)pMz*lXpv8 zBjDmheF>{zR%vy`kS9W{{?1rjSoQoGI1PEFFkC1%XfV&EMRe(a?=)<3+gwk2*$T; zeXG_VW#D33t5;c#rY_kf5=%!5FH@*&2N(*zXP-f`Pf{(BdX2=y1{x8RKJ$W1376~M z;g{T15RJhefdZfK?7z__UmX?Qk#wwjZmnNASSD%fS9Vtt`Y7-K+-ca~wm-7H?9Ehx z#!kLDEvKl^e8r;4bmhU135Qf2HvmHWn6g)M-66X7?CGu62tC)vg=wBZk{|ZJ;u^~X ztrYDWpbW>}4O42YZ8;rU8 z1PQ)*l%cNrfWavdXBJgWe{IjL_v;3%Y8b3#b{q9}mJD);sJt)TLExm_1x$SVS_k5A z+K^fr(Dn3|R1kUkCEqWc#NA3a0RZd$hpUxFow;a1M|$CvQ!6ZABL&*vu?Vu6Rvq-l;io z;Jc0H^nLS0lH4YWI_7d}dxPn+p9@eln`MsA9yHSBYaB=)ul0>OE#`Pou|2u?Ctdva ztZ2m|K1msI6!^&uWe)jwq^M!o))DxamRr9zc5IRy6rp5YK*;@AQ`IXK0gh!D%PpD5 z9q4ODC*n7A0Hh_5<_q(?{YWh|gI zz{$NXAmA#VR2wZqm=5K4T%&+Gvnm9;=l+XOWuxs2VAp_0 z15?SGRBv_9dGulYflonY5I?j!+#5;S?n*Nj=;ttA2K>d-YFze#*;7#(hMroB!#cn> zq-5sr2A;zTOExM485yvtBl*wXKO=ZQt;bvfwZ!J4KQjiajGqT$atChawf@#m?|TdI z&)lz;hkSnwq_%xN5+*W0R)t+!4oH7mlSC01YISIaLtn1XgY#a+hWsAkL9@q4Td-o7zvftJAWxRkY-ya7*4s5pYLr2pB1P?+&YMe*2=w zp(|1vy!$4-_C3Y2(Bmbjs<86?1$;WAUY1fA;6(6KfZMPC&0$TYpdF}fpjf7`WeQb4 z@8l|P;yren>zk4gy0hq5hrj;RaR85o92m$&qB9dDM3fS1h~fWiT-1t)+2)GQ6-+v` z&Ix_Q(w*%|34IO!T~j5K*i}hdHVVv@5saRB-CqBqs!Tw*JX_^)-!Gwo`}$HR4V#rN zJJ;VLIY^haZNKcIA*yevkmY^#<%*IqI^kVRRVt!IOcwu7dQZNNiedRxh*AOWxD%tV zDEyRH2y#0{6$~IO0YE`tOnd~5fot-+k2%5IT(a|2nofOkEBqF-MuESXBBwcqi)9kJ ztOA&UWU4rGzx6A}8ey5V^m{;;RMW+Z!{DFw_;0!1`A0OI` zy^PPkCimVEgBl{=jZY7H%rC#&``$HCsxRMUTbCv_xJP(5r)1ARhA!N{Iou3Bp|;B3 z>GW1+G`_gQiu%k4H@TE)$dSG;o8k0KG*Kcq*`SnPVQij`X~?4{)g9^q;{jUd<>V5c zl-cDncNdc%zKj>QJN>eimv$i{#6lePSmwh}shHY}n`|TCvLK_1oF$ZfJVFz=$~L znAe{b_v*X3v1!jdrCJ-^3iTyVyBiGsSy8Yuecr*{$g&_bHcx%qUzxcYF*t7wDB3Ai znhaX|O=la)qxrxk^8dT`j?A(-st#$F9rVEgug{>__PJeLl@#G>z}H&FticP0{4%Lp zifB3$Z(T-=L`TDIS5*Gtb~<2WT9pXEFsK61AJ1`YGYH0TkbYI#3RvTO^@*|TZ8HwQ zlMG4rGzf)L-7f2pcT&(P4S83`YQGPC7d~ z5hTI4)*ws!x4=mfGOVl;lE8~OyR%3NYtpY@bhDOk*&@)P^C0)u6UAY^A8*`|2nO(N zV=C}%Gcsd3I2PL}rnQ%ZJ8WK)5SJFW!F8qjLJOwyqoFiPfak2Rk=uei_qs4hgmNmI zl8S1W#cY6PTfh_=zg9YqiL_YY%&&xGk&aC<7KDiWG6wMJ0T#O_FnRF@-`;r^c(?+5 zW30+Y13zFDOI3*K!)Y~ohuV+;X5(da`BAnhp5xHp zEezqNWebvc!C4WNC1;C}dH;@K17wr0#ZZh(h%|{G;*o$_Svfv6TsOlTd~9RdozU`4 zv!j_b-$h|;P2<;S6Y||KmnFfuv;h}iJE-$^n8)}u)oRoLSoecpKy>Ck2Y%8``**t=~e8s=wR+A*@(i|Li@>CHhsiX+X_6a7EDpX?OcnDRJ5#==Vso^dYHB@<$EU(&XJ?W)`^o48j_vYO0hTuQ7*1Tw$rk7|w~8*tv+`NY8TWQcmPmW26J zYzj%Sm15X^?U_J4XfbmL-a!u@Ay*_$Tp=KTU6v@f>%bZ`l({3h&G-WTFu+MsEmBf1 zje$2rvfZgVj+@ffy3NK1a+-X`Z5K=5cPaPpT?2H?e^d-IJ|G?9)tc!vN%1HKKy0H+D9w&} zb8&m~^aNl9`QD*Rs8KvngAJ&hMsU^oGGoa)sh2V&AnPEkG*hFBSL&-dZ*JcAk}nI!Lw*KpTt*b04&mFnD*)D^RN&kkAPdjH*0;|vqG zTTkY}gGY1Gb?g_(kcY9?A$Roq0v}DuUq`d0fx3*|e_qvV1voHaBU4(|%H^D83G;7F zTw9&*vet2rJBp`(u~Vap(Gc{tw-{~?GU&7Q^eaC-9HTl%I%?`OG@;{tA;gczC1mVG z(tIJoj&<_9?P=YPfOl+vSNzO{Nzt{nnTlR=3ru``$rqZ^PQ2K92Y7+Pk!-A zYhT%He0+S&dEkb0tn2D&(8}|HJpnaHxQJl#JkG?Sjt~)XC}Osf7H)QjuhuUED8lPG z=-6`3{=Ehft4cNRoW0=I-z+J+u|+#n82u`v#`iyg@E_gkt|;-U->A=gGwOlhxzRfs zM(LH)pn(aD-72P_7hBv8Fec{7igFw zTESDD%yBz^J}98ql(TKU$NpPCyry<+GXKgTdOD|szusWUX&uMwM6A@wYFKM52v}xU z<4R)&VH(2b@Z8p7jBF~|v=}F`#Cvis@kB)QCw=8iszR0pOsyaJL-EMXIFLj;uRY(D z-68*ym(QX#l(dEHbnGblb&i0HC3@OLCX8uX2>_|WJ0;fdqhuvKf4-NMXA!eTCq>ll zkHO<-ah8Qo|C$C3)|Vx}2rJ61BAtp44RSB!#ZOf7AP<8M+&0STBhY;)p{OU{(u6fH zS5%%p@_D)_Zs<|T^}498|Bs<{OOW1ej7jL8MJa!ne))Ls@QHXY$~o~JKjTMf=J6kg zmU5Ni?^I5YGx6Q{^Rb4-J{2Q%JkaV|zqNy^p2rA{iW$a76Dfp+bp3JU!1p!ERx9F; zSolT0zH4|Qh2PEBFp<^Z`ZVDw{~NI}*a~m-a_i3W-hEDqeO;dAO(Kga{{Nmsoc|Ad zp)E#%DC~dO3+8lq_J{WQQ*J1OeZ(v{%7sC&b+Jn*P00#Vyly3-=OUgm!|kAtfsb<6 ze^i=bzm3ueCdXVKHy%^4eTYAMIC0?(>2gJBi#^Xyh%4=aae-0eyB7*3yC~N8qmPIi zu$wy9 zpF!$}n^Y#jzc$GNZZCg^d_Hk@VU~b4cG>B7Xnxed>>}_bAEPm%;*oxi(T8C`5KB?m ziS0Fz?al2cC~hBbmT|2o04=YLlTgg@!1Ct~+T@zXt5II6>51G{+hf@bUGG!z)(Zr} z;_JLzX|M?iz9z{!0ee_s6p0ZVOS35fPWa%>A|Y8{LFd(S-a_y4YfcT4ONl|rl+IQF zv=U%>9$tukpI`2?k3D7+!~ghdzoH_o4z)XJH;gYG46V<5^;1cfr&J*vZjXNB_ga}d zBXoc*{a%;*kex&0C2-C9^5uiq(Z^@=UeeEKA^o~GIy8CL_=Z$$E%s+NF)|W3)BM4` z($Mndxo4I(_8k^fogq7W+gn?uNXXrl9nKiRZ7d+LF5>!b?D30n!)CQzIlsdDfqT6=_`TnEDu{H?Vr~w2DtUTY zEk`UrOl1yR;E=0Xp)ZrTnx_Fu`GRMaIafb`I{^fMASvN+$N{+Wl{EceXXlCUA8%lJ z$*_3)sZqmnmd(en6%Z zYPhM!xS8hX{^U5GFjxm~QWzm=uVZfWR=CXE3r?wq6S5?+?iWl{f#0h8M6QDr9ycIA z_}Z5bv*EJoylYBVG7}QtP5mlLr9W{pd4h~Z{oLM*)^6O@ zAcjSGc!T ze-&aFt>Y!qoM3l7cD(7*ZY4#Kb|HotkAT^<^DbdLy}(RY5F2{X;;zofqlLy4!SOE~yhxs2;E&A^!cs z?CkchUvHbmmK%3QGG*Ibi?Al}-ybF&!=J#Yl6)}7404=WgGaM;Xs}ouGbFqR$eG?| z%UG|1`BQK~RMAk2%A};0`u&wa>$ANEU6P}A28RjQbG`C?V#Yy3{;Zn>w~X_@Yhjr^B~y-XP= zA$#;(g!XNfg06QvaZIuwkk2*vS7UJeNaEZE&Wb&O;#g+SA0TH z8o7&lLS1W84N!P!G(o2~cWL=w7N;yJx(t*ifmNgjP9=0FsxMpgn({T)B9De`3InmnT^fq6!-pXqyxsH4FD>D0KM?^r3 ztSBv*#8~!Szq5S&A$Jr-WG(Cus+>;k?H_u7m{0doQ4pS{QO@knLLZNH9p}me=RmE! zUl?#>116q7E8>j;2%6G+kD;_Fm<~v|#TKayDXD<3B=HidmBIx=PGp$W-!!f7P9m)C zVSw1>c|(~8%b7N@OENI*t|(xvWp4*@@fjYp1y~~hRHopz#dY%me2sYl>2+WnBi|CD zO^hEMvi+cEy!bvp`*l64{bpJbvAo%*a3+Rw`rN1#S=7Dl3B=7akmt5rh>*7o25rne zHCQMHYMNTZOT?lPEBtp%F5I5u=-tcv9ro+DO)orr)8vzze7`n{$NbhI4d-m)`y5Ih zF%MW*x=l-QyJA+3B_B}ouo@3{Esx;Yg^MazU|`wKS9x>ExIaT ziVEm|Jwv2S&9{1B8OFHL%dSu{j8Q#6$j9ohS*ZXirffIsygZy{_8;ApB2APAOqANW zOdQKEDZ7+z%?P~ouB(Y>P+CJ$^v>ODJ7-tD%f&W7}VjW&=vA~+~^ zrL_r5G}1!y))^IRJA`atRLAi$Tl7D;(NG5c6wa9GVWMenvhp#}3?cJ2 z`ljB{U<`YIH4DJBeqtlNEG`$6?XpWZ7BgsNdN*65*+yiNEE$u0pPUg>@P zkQgbyG+>QalW4ezg0ptkM9rn%pA4I@G>)v(g;mt$Bsrl+E7H(UL~P%1th|wevvBit zVW&WkS}1<;i^x-v$lH^d6mK>7aE;+dlJ&f%ig3wL95Uu=^6)mO{`k1Tks}<)cdP6; zy*Y6ekt|*uS22l5_Z}qzZC}!8e7caxfzKnUO1(&6O)HoYEx%|~5R|1_7eA{^Yy(&V z2eB>G@>$fbpY#yNf)}|GMfd8w3(;DF4(s|#+N<~MhrHeCEbH%MWS*PAVywEqw$3KT zQxm+g90l>OXjiE$&ouNX&=9c_lI_oQz#aOz+ajGd-sZMMMnA(GZT&u_x5@Y{dOrl% zQ<3=X1u6rBwdfpknM8g!3$gK)%k%LYVtm?fe5P-vQo~aACK2yf{8FCv+ify-;y?NS zD9`^F$N>EdWMDfbYX1CZ>e8FiPC9isOO?~WCqMdyt(j8WrhE8$?CxE}W>fB&@>%qP z6r7?($rtr|#zpe?+vww+<`rZP1o_Jz%!?W8FZ}l|wxyXTR4b6rU|xUSE>4r1?a&HM%9lR&FC-nGALQSkPNvw`SiGe8XGBWnbr73RWVj1=QH z7lIlj{;e?nx|x@#dO`5ZsrtSQoy&BY{=S5H^P`So=HTDkjVW@#(N``}>N0UHpRfRJ z|F5B4n+^PQ`E7x7RGQ_fX=(NFA1|;24Y>8d_jj16i#=K>%zpR=g(abTcFMyBAaNC1whsc9x|9l&pdsGAz!Ep6=ax4YslIi@5gz?hIDa zzrR5kx1m?VNIt`|J#gHfo^xS-ugDqt(afc|3Likdo#noJY4c(R``cmOV8M$ioOQr< zn8FyN&bDjiUJB`J^4sr!M{R9wsUhm*uSX7edUw()A)=&}3{yk+0}ZVPi?0YU;uoL4P_jC>)yNi%}!@kVT zH;N(H%de=@(hiwSSEHE&IUKpL_$z(k#ivC80OzW~P0)+&b0fcM4Q?eBg+@3c7Qqq0 zpS`yQtyNH#mzKo+>^EOjn;0CM=#oaMw>2X&Cem%Gcwf60XxsBO({+#jsAUp`UVgP& zJWcJX7agX;plcJpyqOXBsarK42spy={e<)pu*ZsC6q_GJ-Lb=U8C(#oVu^}Ff~)?^8+MW~-Yy)Sdzg|J}C z>}lQ*(tD(tC;Z@-m}|4VN6#L2C>wT6%KTQkiM>;^Mfn}wsTQQ#lUVjVcWyR74?CzT zERxjS51aV9OtC@F>CSddDx4)I(QRVyrg@C4h0t=sp#nx{7$qP^#hk+e%>Pm7)VYf% zme#z}rQ)s!e58s}up81sv7S0=t~53_w396Mb;Y zCg#1-icfqH&QBX|78wCC8P^SH-u<^1mm@E{+Ne@bN;*vs)fZ1bXX!{R^lDv?vu!#= zH&N!pRO`=M_`& zn~*x5P=o%CY_??D-K1E60w-dKVbuN(1Axkb%TjOH~41@gM-lz`$el2EKqox zQ`U%HU502sdWs9U)rnYa602-y6um#k_>QXUijB(K7JoaoWyCu(nkDH7I$M;AZml(_ zb?7&kIq%K|Tu9X(@%}_wukop#W8l*csu9feJn=0TDz+$+vK!6pln)q!2JwSCXXL!z z^)Amoe+OSr>U|(|EKN+XEF^U6h+|JsRmx|lWqs;Ft4sJ2Mh85XIe24Y7rfwSW-YJt zmR^(Vt&y}ZV+gzCy78}o;IC||91i59?Cft->9^za67~Gk7|&9n-d7-lCIw%2R?U!N zGx#SmLqQbV_0rs;iKg@VpuX3kq1E7T-(nJp(=+hWt39ll2yslt*7Kccow$f3#k^Ko znT^=BPP7#Q6HJnReAUA@iB~gwh`_~e6uMB$*oQixs%(4g1kYxaUsXGr9tNMek8U~| z)()JGsSWN?#6DUrNUt@auB>Lax$W&emWAN&&pk9GV-qXtdw&Ij3GC^^m0RDW5oGA8 zn|%?kN}fGFZb2Xn;67BEEUz$KwgF4e#xC6lVPbHB2#{)s$9megnG}THiWmgpc`i8> z7WyZ(w?XKBPcvf_!v)NpfzzUwcsEI4T*gp4(=0X z2z&Qb6eH-yNGx7FdirVv4BrSMwA2i;c_F?zBQaSk>$Spajo{~8bJ`Rgo_wGJsBp4^ z0+wyCKEKaI{Zl6*?G zw^9*ecckJ{>pl@=c4cCK!^!WJy!-4-I)S?VP4z0&X3o=-KbH zlS;U0VopfvK8{3TwaPxoWwLk@i{ThGjHlN8pgMov?^AW$&Nqb=wrHpPNUG{eWEa`F-+`BTOmd;NNOdj#v4=DWio#u+Vm9=4}dWG{C-k82uG67|HA@~a)a(nT5Y250+W8lw2;29f`cL`QU=VA5gqOtTNG5Fcp_|tzY^Q-F zaFH!e{_w#lFI(I*R+{{*NMI<=73)a(ki*|gt1??kyopEL+rRKZ6=E~r=2VSe)w`}W zbRc!ufA4y-VAQIEcxl)AVYj5s8ZS~09Zo<6$j>kVG@oeF=*gTvuj1|dE1fylI;Nr2 zZvY<}=C&X~5El<*J#Uem=p5CX9e;lHmdO+I3-(n(+{WX6ot);|MwM+64ZcM`QPLwK zbBtFduwu1(As1F*;zttUkPp|iQ?2*{z{d??mDOG87z8}WXb7((sJ$vC~Fb06ZBc+`DxNX?Psgs5?<$d{okgZ zJLlxLpYvre7XNuFPmoV?CEI4!iUAe4$aoNnX6<>Ant#`M%!N4`XLl=^N7YW2DxHuH zP|WsN?`y?ikal&A67ahw7B+yXW!uwgzRv%?Xn2Mb0)N=jwOQit{jDeK9_I8SXuEGD`dPCYx{GuvWx{y(vJDC#FL%Jov{pL8(&-3 zf%8SVUH8U9C&K3WpUNJ@p+^?uCuUw_RDzjB{w_0K#%I`@3v%DFUE4iT4aUrf4U=Q| zUl+_Po{nof_YAj#s_Vs_(VqJnw8dG_{0+mCkFZoooQzDP9b>%5w5>;1=8&nNO-q2c z;qy}GT~nzF@W2O0#_pd1zfwiAGXH#2)?Kb7`QKv#2G)nIuGt?+3Jm`l6THKFVf4bR zLUhThX?IK2n^N8A)k4)y`R15Q<@Du_>5ctl5Z_x5xVK4k!g!?1epWJfLxr=+m;Ee_ zXR08FIU)1fO8Ze|_vYQ>F;VK})>(CxFHtg=VITGT0PUA?-d^+PHb%7@#u(`fii8rj zfj$1G^n7k$rsYIt8M_h}$hmek9J)O`fa=mFS9( zYm%&>F+!|nMR?UThd^PJJwPx$h28$UosJ(^2t2-28nipl#|=De`XMnMT~Z+WocFnj z;hKIrol8D;%yWUs<1SX@QeI~5rcMCS^~FNW=wjFw(E#?JMLrGQD!jf#4)+Tvog9h-Gp3Ldf=x&b^ijnzwwa6(cb; zt$s%_fBH~#-Dw95Z?G|;Mp<6H6O6$K=dIc&y85Zo0Tl-gvlJiJjbM1bSQYgiYNo$G z(a1N%(AzF)SPI)CO2o5l2eIovGCFuxX~;t~EailEIgg_(J0bMNCKmJOAQ&Yp@38r6 z!O$3--_;U>Qa_hd*U35RH$(`8kk#$F@n>o|j%_?vz87lWcZ;AK~dd zHVK(`o0YmF>#*k83B%~G)jeg?ZkU=CR#l7b{{9vBuRoIgwpL{(*1T=BJRadS)W$JY zdnnci<;G*NQHY#+vZNctyMj}nl6TIuMjOu_bQX@Mix7k4R|#V9#e z9C~(lcP9;ngU*O<8%u~!z>ETt-f8){7@?I-6VjOG*Wg4Y?`eHh2w2Sk6F%Wtlpan%Fj@x&>0E(P;%472Qg;Xx@HK_%ZtttC+H?kdlS1I%)#0yJe7RE|P|i;*lI{JE@xBuA!|)kg}f6)jrJA4S3-?ah7Z zNFtuocOIyd#Yzc&>wXJFXtfMqk{WYs`yngTj7V)s+i@wy&cKJet#CU3J+s@&ea&gs$yr--Z(tA{O!rH2FIa|KLf3*z&*oTgMb%D73Mu0UOdykq#K=R z;|A7uv4P)T?}?<+D?jf_wkef@t5?FeU~yT0@niH-A|qo1uxK=&N&70w%`C_GU=AWh zO53&4S51R-J>nB>*!YyOn|S)FXJJUruz6K4#4|x{-L#vYoPE~`;*9FXIKNF|ih^P4 zUFH}f-jg7vSC3U@ISbF%@_47e<>&jEmOm~A?AW9VOC&Pzk?<5mCNXh&YI<(90k7#i z^k5h7dDsc7FvtrlQh~WfeYrb+@Nd^{Avs9KVUygOl?n@Anj&%Mo^$ua`2H`+zp@9V zm4DQ!-rEtf<8V$+YEA7FnZG3b-yhDWY?XANdsyPh7uD-+$lZ45_kfpW>fIB!AVMkh+aqa%Ef z*IL8(hl`%$lq9fzr=^D8yxLj=pWq559?95=%GS05--VfdiTz}Qh{S>=GO;JpNY|=L zdT>Dx&eF!?WVTDVx+Ph%rE9W3w^;l4X(OX^{);^c2NSr~wR2hHyY7)t9}J1fmjv?} z0IKDMW}!fluv>RAKwkhA<+MJ=%V@*RtrP6LMaCYB2Ff{wm*25+&u@tj9%XR{qU^TU z^E0q685n7OY9jv+TW=i{*SBp8Hx?|o1-IZ9JUGD}65QQ`yGtNgaCdiim*ASloyILl z!`)hU8uG&>yYt2399Al2LR)jEkh#rD#7DE>c46s#}Ehh&~1;j3qF0KzFbZeaX+m+k?J_bTf&8F zj6@OMfQe*_l@05+>u0OHl8jUtEUes`Oh?CEEoJH_fTwzELrF}_)$(Sha{Kffdpkl0cYXNJxQ1A8x+6-eQvju69 zh2ouf_FyU>YvSH&)8CaEnP`E0DM}AFlqGI)&`rzO2T{&7j!{s!-5S-vaHGO$`48-kJLC${PBm{Dt}hgi^{2>%gaH8;BQ*^%BA!~o z5;b6G2$@VSVWEx^7j4!$Xku{k$!nuFC|!+-Kei!{ytsF)@yH0Ew~;L^=TV z2A(0Zs?fLK1jB|`A%6S&-|CzX+n@e_sgOcu>o&gcJb9A*+by{Kv9hujo7H#qm0Z-jXIkz_YtXL905Vye3l_VRyPMq@nvU#%U zIw#=UQo+gKitu{J`P>EXcfv?ithn<;&}Ce2(=G9aDIXL69vNA0tI0AOX3g0Xtl6(aQ>_%_@XgbkuVWjz zhyqbp4Y{|*FwCf~5sWsaAiqpWotdC03*y}tOmv1IgLE>E2-4Uw6n(CzRdOyQX0}xM zgC$+uXMW9gDu6yST*HUWDH+G<8;{}Z{RFlUC z8Yg;1XCE}D`QB15IBw(1$2^7(griw_(b_Na%pF&W?TvvfIJ%qwb82s6S8^ekpp58-HVBbPu2_TbR+IlHqD1jWdz4&g+ zesIFRu;*_=HkX5V-6p_d9g!E1$~ zU|@7H6!rs_X?!)t5PrQI7whX{9^XIl!?V3t^lOxmb)QS+7<7IP+Rv^YZ{U+nqMvoQ zVeEb~Vw#G+!}YQ9iBYEU)8&d|*-oK-G3^|EC$-XZ79T3LKYq25p_++xn`fMEkog2^@oFC{6Oi)Ja^sm~x z9@R9upFtF9-9O58*xPItZcO7pc(VXtKVTd!`Wy8z{leJZZ{9;K(z#98`^HTrGQoPX z3JBJ$>3|H|X)f!TxgW;hZf`bZs~pXAF`jWWDg!(bz2Qn?_>2JFQ1K zbn1d?%w4&SC_EY=&nP7NkL0D7rg!rY!xi&UK&lnejF}p>0FR zB5Xh5;h)C6`5QRBx3tqgi5*KVZ+A-6GMNd2w#3a7G`uP<_$0InpF*m z4361H)ZbkGHhn}5WOmARyKq}t&tnN8(13L%4Pu6!M#za0+KUbXcO*W9lLfaBc3{E@ z9a{tjNlt~`%Ow`8vcmv$gLl63z z80{q02Gv0C4tpDpC)w>MVtO(UPtXLp}bTx z_S^iDTIQW@XoheY9z>rvhPx3FkLG*smFGbb1NTRmDoN=1voDm3zm9mJC~fu$*6;*O zJn%(C{k2$-UFnrv=2!Z@&su~|ZeAG)l4x#?0q1l; z&r1+Q0hkGNl$^e6`%*3+iaQLmPC02x>DPS|p=%G4=0iNOFD|E7({+nAbrKFtkS7Cl zV*vDBrsq(!_4s9e7tYhS#Sc2Q>J4yYG{|^Spk!13E&S>&-M#drZ zw*yTO$%A1IQQICZ|KU1D*bA$F0ZvUszz)i|YKO=&A!qJ(>SHhB!}^F1?Sb=y6#n!Q z6y1l=j8;WvI+i{k!R8z$!d+bq5Z7k83aAxb5pp=y$0EO0i{w=g90V@;BLuqQbT~m? z4no^e%c2f`(QZ8k1%u9@5;qrwd48~eowUr)Q)*|272iCdU>tE5qp348@5WYHi*X2f z-)lijcVBUh?nfs6!+lA4)0oO%U!qs#wDM#j<-(W3{!NW{B?F zU`5AJZ}f5(^z6~F1G-OqUV2X6u|3~LJoZakR&dcj@lLao2cWhO>>!U*!(Ek~vAT!X z2HiV>#a@IoCBN+TCcUn!1B{OMGP;l-0!x)ydC1#(bsja(eFj8!bzryf#;?UWkeUH9 zOe%)~T~`F>Cs!lq8vBj*0yCEJVSdz!@-3W4@4lXSI&6(-9pti4SSKG@Be*?(w7a{z z2s>9eQLG^2RJm_G34j^!M027@(%-$ZPTqH#YrY%yhBh`E=8e`LrSAq>{M!qlhyDd^ zt3AmKdsIzGZA$hv)SO~0%P;Q9_ub5PYowWj1k0augFN-BjV0#eoY3>Lp@5*nHNtxmd&8!i z7S;Lwt`;u*)^xS?=o`zGS^LifVIY?u`>kmB3zwaYxnL~%o@7T?%rg(O#*-rAFfOfx zTWdQ4Q_XeFT{R7l_}W7xz;ShYpX-d@eemDz`+>^|>Ngb+z}Bww$EtrPImu#yqeq3# zCRot}Y;^S;ReVh5ecxR!IK(v5gn5>0qCt5)AlG+Kd`mM!eP8;nj>pdQEZyUl)*g@t z+7R8Ab?ElB2^S{tcvd0BD3SUOf(^D1PNk0PetxN=@zwwycaOb43+n40uykPeY;TE+ zZYe~8b<_=%B0e(*DB$9~kJ(7}R$jU5?qzyx&9M>uM$oM78IMr`#8ERvG@bS^(19Tz|B?;a0x$I*L%k4IyL_ zJ-L|duW(tGZXIsY8u2@wRz9WMJ*HJZ?Z>D~0Ed6;4*OINRUGW5miY&Clm|jX2WGdC zLd2d0)*f?rHKR(MHN>WhNBYGcbNbzh2Z!2*RNe#yI@ItOyjIEcmj)blU&97+k9UFP zbsQf7QELUxn-!UleiL~04TfP?0p0Bq>-qM2X~Z``T=rJ^`fC|vS#8m7(%VFL1g2xO zr#ax9lC2iGrqR_op=y$>b;<6h(m`l_4EeUMnu2i8q`;i;IA^Xu5yXL3K+B5UV}I`_ zB%*dUr!)P149DR>N%2?acc|@z(|RmY$S_AHTq*(UDEc$jr^Tk)W+*JenX3j3chMW> z19=$Z(GpSM8==h-feg`h4}mcasaUVPk(DHLB|3UuKm3%$mKRXn7F6l}TSK-rYrRU zQulUtC=}u)JTv^sXPv-hlN5k2e(#i=v{P~~{%>aVLtP|qkI$ZsoeL=pR7>xZ5W!Bt&uoo&t@zF4U!11?fo04x@xs{+!-P!^O1W^-FMsH0$dp2`!|4tX%#iyWBp@KhWYJ> z5h~R{=mh@FUj+{Q>*FqByV%R|eP#9hU(jf8?IEf{Es|kOz@XWC@}p6!6B@tKaoe9* zeGC+r6FY`*1+_LiE3B~KRnLOV*2osa74*2)ge1IU+EezN;EY`jlDMklnpP2ZofnTs z5m%hc#>ub11LZqkZ-C*pLz_tSv+BL|Ho^H^bgz45rUzPCvK5A^deHK|)Yz1UgE;Do zKsoJn5m%Pt-jf#b)kZM$?&2aMCP=&>JY+OHoY}5tc$K}cm$7M)H*dSweEz63kzj20 z$2~0L^%%^3m}$Ufl}~AE?RU-iOH(_E?!$~tqxb;glTB^jbUv`Z`zg72y)Z&tmdo?8 ze@B*Eh@bqzHdF!knkgDVbe_he`2p)?LS$t2)%L!zKEQ+-4vg^}@-lHRvxKdGT>Lo& zeEYudGL^WuWRlaKnq6M*Mg`JS0_G}Tn>`@C$NI&`8`UsCLk6`+mCOy!wKo*t^6bXwlU@gV5Rwl-~79T zYxQ&#N#m}wH8V{9H{d-?3{1}*jN?hmzU&uf@?6|rVe#IoaVOr_qrO~swhHVJD`X8V zkP&*urNV4B;Y8=}o-_e=HRr;yHP(}qeAjGqzS_~hDFAGt&TbOR&)&S<^tESQ$x*SJ zx(_&ZO3TSD@0^xc+bGwra&?N?U2QbWBH zl2V4BM8@8{=y;Qa(bHBD%SvEo_Yt^+J z;NOYV^d8fmIp_9t{9^OEHU2$@Ulv(4y-WIJEndh<oHB~F<^-QaEHE~NwFA$?jONp z+hJJy!k2Ub?to!linUZWPR?e}>uhB!^D}4B`-$I;Y!2NR=Vl&CUEg?21suEFR0nrz zJXHt%{qcZJo}Vq955{*SYi_KnK~ya~uYSP*v`Vkv@{b!eh1|-b9Q5+-+vg2l{(?3= zkZ*j#UoCBL=$x5^cfB5oW3VSJt87ia-;8XiNDWpj)*b{xx^#=|N{AUZ3mm8!Hp_Y? zpK}Hj1eFCyT@D+-aW9g8f7!yI^;mwW>K^r=_nPb$xIsMF_0M+#PVqWW;0y<}+LCI= z&)+qfRI0kzMw_pK<_X9Wx-^sMn^6`ITo$1AXj&%6}I`;gYj+b&pHbH{_Eh<&>^I8pWGkNe7%G3Nyf3G%$heG^MOxEeb zR*}QRK)4wK9F|Zr@GG0_5VB9qmbg3TeWpa>`sT#~ZFy##M)Fk1$xq=X|8xj$-Ki+e zXAU-o4tE0|aSJx}?>(}l=$K{eKfO}|$HXhg2o8s7nf$B}+VI+X$0OoV{-;2mCWS$G z@k?}0hYt4B&7#}MXSv*NvU*kI=*cF`QXyUwX6#3#Q-kfip}n+bCLd&9&AL#Bsvplg zH#M2tH#CWWs8tM@>4htzO&^~=9F2Hkyr831Y2EqUCiM|seokHt++d)<$O$XaZ4 zUpN~e>y^F@rI;18rl-cHzRFK;LyEAsDY|7FlEb|3KVwgqT^$XTuZZOx$T*KHmH@wW zG8;i1DGI#o5w)thVai}iD`A&CEu+5Vg$6)vET^W_!5ymY@_{yRTHW&bTgPD^Uk(xH zsL!)}-t6cpdExT9h%$pt-)Z1Coz2sE_zDX($&gjyE&G)EqUE`xQzY=qO#lZOyb69H zJ$NuX{RwP02CP1V#xSN#mZG6&4tjhV2uHXoY?%69FhTv_>Q;3e{h}!WCOs|>CGrDW zdCsGo5A~C{BkUgaU}#{&^rCoo_1#z9qmG{VqC)Kmm>5`JVC@F?C#TPD>)E!w=6RMP zqGj5P9=fu?B}fVOgEq|8i}$XnCVZaW+4>11mFpz%fq$P!F{bm{YcG29x@e|dUi1(1 z0i}~iDq;`#-CuF7{!N$6OUXW823`8H@APpLOW!&pW6uoxrPyK3A1X4xXZ@yoWZ+He zDj+XYo|N}qnzLj z_z)G`gC+6IY5}MxS3~$-X|%y#p$N_MC6|38OqvvVDCM39LTUY8$aYI%)qSENqlmX` zxG$^Ryqg_hlqtL;QF_s=nB5(xFtkI#o^5A!={+6Fp&U3T`XKSOA(-AJVGoJ?Hy7 zn)3I#*E-5=EGc0pk{S$sW6xc|wwC5=thx9s+BGI$y>M)E`S52eSUVwsLqMvO!&Vrm z9Ikv{JM-C-%gh|RH5R>u^?3z!(TzO9kmc7(M8HqUgR~=>R#ta#cS$Fm_F#ab0b&tA6`fRVxkWg3s^ z*rgua29A9qVLTRTXz@xAOx~>3kg7ZH+V81&>hH5269thj@@j5KpKrF~JX==3AK9gY zoG-syB5SmXCH4ChUCh9>^{RfF`(_49e9o5oTFf+*`XEPN02uVG2o|O~%Cu>2fj??F ztx0)71w8uU!u{Ro8EZwKpA!chB$h|1#J-w(G=NExc9ktw9?iEMvRfP~dV^B*B7eRJ zNI(8u0@M+F<2)BT7qB=M$U)$uvkEUti;Xo`YRe^uf&qEg)7_iyhROb+hK|ggFp_xk z)L);!;1RfccD~fh^CX_jov}?t3WLV4?T)nVxL)=|y0*t_29|NtHn%zz@h3rZnC-K* z6voo*7K_gX4*@vW;Le_hJb|@W{A0kmZjII`TtW`m2hrwEHmueCp3tRFv$~fZuuk+l z&Y&1k8JogB)ZeA>g~_8ch*1%lHq1T8S3&)EZ>$=A)iMKR!~~mw`_-rOrmwD_LW9cm zlRw@n%pAthZMuTdp7MP>E-=>3Hnp!s&U-bg(|}LXb}4V-2Xct%y~Qh(wK*e*Z>yzK z2TEgFCh(eWat$M~I-l)cPQp#8Mh?4(9a zG9@M@gvxqFWjG3qqP0AiRZjh5+HTA!qBHpCRTyh8PjO1lH zZxTO6_&6Mm*i-qe>Y>~SzkVKxp5l*WU2-t${0(QAZ! zPW`}DXMhq;`ESVZf7Y(Q{~EBa1oh|p|1)6S2HzBSkjmq;SXMApVf89Qc>1g81{XUm z#gZL?e5`b%KIoT#aGay*GwGj9)|v9R89^i2S>5kUuN%LA-0Q3Oh~N&8ZPSVIGsCdR z+N8TDhBmp)vUPq;DnhOb#?}h4s$bYSS*d1jMRzAKKJ>cz1&KWM^S5kr#Q%6br3=;O?LOnre#5JrpD=$XA+{hxrZ!e)oy*zJA6qqSTWKckj~mHXqBUPiE4}8uxA=rj2y^t*)#y@*_q2 zJ%Hh;0Wp7qOsL|NO1p(mpdIzjKr;A;vb&uQG80B+I`gcgHGWGXM0mN$sV)dlGr8@_p}hJ zlT-gJHR3($Cz&ErNi6y}NMfx2SR z%l_W#RN^+Ez?lkTs+`44XHQqpjtOFSpM5rOcP{yxo0Ud$Cp|2M{;!5lWp44W^8orF zg_U_j1etrC947UdQ`GsIrgXSEU(JBov<8rzcLsPUFyAt)4rR&rBl%?*nn|R}CZp&~ z&M)&g+kvay(lrN1>AqzK_MGAm(-D=~!v9vlAm$`%U`8~Ae7q)z{D5AeRA zk6W5mTB>U(YbpCJ*z?^rNX*jOg80Q9KAjP6qkE3IosP_4|Cc{S0(=SXaOvv4IlvBK z<}{8=gp}xUX5kc>;HT3S`v$E62&&v1IaCl=`LN2@nsR|-FYC|`)N!`kctbllSTH(x;ikHO)~aNPvj;o(!&e_boSKDC34TM$MKvT1XJ`msjiaGZV2r?(8JraVvAx+7vclQhHIzfRM92eY2RA&c`E9M;60OB>=UK4GU@G zwCz71+nM^$i(H&Ck;WfirB+u?+lJ~1ls}JFk$Ydp#t<^{Ao^h`xC8zw`E>80hW ziVwek!MVOIp71un-|KuTGd+H;#I@FEIzYMWXz~Jb8K$Jaz;rTAK?Aa$UG$5UN2Ofc zqmVolEhVw?)R*=xyIC_bIA_IFTyn`q-Y#0pGsV~@!bkAlN8i%TeoZE2$yLpcIh9eO zc2rMUxtcrHEQ08&b(r2IUIs96xQyE{raFA#3N0tzt=LfR);XtXxhFV=u+bb(i6$de zc<5~A{%SOZELDm44EmRXbF~951H*OFmrMPCvZ4aL)SK3uN6`G}B|~g?1Rsa(kp}Ni z=V_jXr?UFyccA7w^5k^>jP}YIDaO`M=DzUDv(C?svoWa#Ewvuc`A`-SE#Z>V)891h z<&SL}`aSlo#aP1MI?JJ%Fg{mme7~09y#3MfW7mTEot8!e)XvB}SeRgn2KI4?#1sYe z+K&A9vE+;t*vr(4xaK!oLm#WMEA$-go+{w^Ou~^Q;HhA-g2F{5D5x;8!jVor;K`oe zz-yM6v#5Wb`Gp29;_WjDQs>5S$R9J3k1-lmru%mY{n5-@#R)NGk*QZ%(f_lO{=1^v zzi9;By;e)pdXe9mTt}GKy#4f7__bFx`?zoClgv*9W)I*|(2pp{Z zp^@5PDP(^d_Kofe_QsNhGxzGxFk2EftW0*p)9eyM7r|7bcF40nR z8TK}A3k?nY^$Lj ztmw#DbL(q-HwzoqHn{hrQasdGPpziO7_*w|54fB4L+FE@9)Too0s9#tREuW9&@~oY zxUnv*Sml&G#h;j- zL_`13?)xq0mwsMcqAjbz35DAPc=-FF3aL{;Yy9Za3*@-$A|pg>ARqjWy*&dmimILjbmP&hGhW=U)=63a#O2Np~-UlM}+VR!bRCD5nWmPRZi@cAew`j{bUf-m5l4ATB zbR@4Q@{q2L1@t}V9-RmL^~TkX+wVF(+(;rz#a68%dFU?InA$6|+?7J|Ff)#XIGh4% z9^K-nc)9KxIif{Ma%^rF{*j9pqJgr(a%C~uF%SRZk&j$bdwld0qqoW&i) zP<#K0#RbYE?bPkaWx#mm=pOTOHWDJktS1p8uTsMOB^vQ|9@Ukl;V3#RU5mj7%G6o( zg|d=O=Cd^FlU(VUIK5G)S7!S(nfh&(+})~ThR9fGx$QS;%qGSA7%w$TgoZaw4yBLa zg3+{({}qP+M^s*jQN%qVyojmsXmI@J*&5bpq*fVzL(8|+c5%AOq=M!}@^H`u(_QCg}pRDZX3$)5yNz`)#-C9D?g>T*;MOUe&*-nzQy;17D)DrjI+JEYLN7r}>i*LvMuQB}J9 z1>OPTqc1{mHk?0qF_PC2rZb^OJ)be;K6xqv?HgCdbxo^;+JJ6CprLOw4ebwvpcq9# z5HgH~&y=P;k>>_Fp0W&TyExG?-Ko@ZSxxmSr8O5-3cFz@{l&QHGx@+XSPRGFS*xO;Sb-ao@O}=4Y9G*llXQ^(X6`J#>gSArjxXPJTUXB#H$;55BKaL2a`QktFG{QH;aA^FFy1U?(A9nACJt~_t%7>;3paxX2SZK10u zgHa807<$`X_cf^n45IA6eaf(R3niH-bhUaD73y=}2 zK@2N}?2Gz1{}5$s@APviR1%jBNp1ZP1o7z!ag*826WHilvw}f=5f<+sCE@aJt;;)_ z)uRTA401{jPm6q|EI^%&h^J*-amp!%^_F;=qeBdpW0VKo8^yj*QISy8zQsAxCx? zb2Cnh@CZoi0+YpCDN<)_{fm0*Yr;@yHUc&*Ph&JPg!*+7f3cMNrxQUj3gorPyPR=5 zR#=Zu~fAZmf0@3J|3RCm^&S7&nMe@H=W-UA{;t#?kti<&9mQZIkvf`}IHkTJ59$A8! z;OCs`h^EQ4xx{fp31~}dP`dS#`DU$tVHxr zy?lmm4=JM+5vys{T$;?StRlb{)+7+Oiwmm$ zdsLBds~FJzp;AcCzff!AaOVq*Q3vYMu(4?Lo~#6LWEeu;u7O-C-uCG`XZFXgLblSQ zv6xyy^?tP^;2yS*nm=`-auje-^sB?1r4%_j31R`S=BD3g5I>^a9* zZ5N~Gtu8*LHJ`>FQ8=3_-mekgZIY&a=-gL+>EhZ%M3pvkho-sVKT2#_t~G6u$HHJ* zN=%}c`3cSHh)^}&?p92s!Hd-V>81M}irm@iEG|YdQ4!zcbc6BDPahUuR;JwzA>G>Y zCni=bUWoXRYQ=7#9*Xar>|=$(wka!_-F+!Cl0GiCLDX#0BJ=)ciBUp)pja3@(l|CO zQprSw-(8^L9eQ-J!|h9aPBH`Za`4q|ZVHRI+1BPqPTE=aIwMCe!zhuc_U6l@Qrz%u zLKV&yjX~TQM@UY|`#KX)*(e)d3La+65ffPxPKLmRvO#jKGuXlSqE2H@GwK#WNZ`7q zijO>CPf`eIn^5sZCDNTN;Q$ghJY7C6*{&HEIFb``kuBy=$5gv~8M?&WX^U1DK;mX@UJ#(*7re|5f37qGNe>^oHjQ{2XNb?>G+c z)5L+nkC*pS&}}Fp=|AWokj}K!jpe;WnK0yt3QyOA_@Aq5oKUqC? zSM3RW1u4nUNq%RuMe9<*wldrFGx0}j4%UR?G zCYakPv>i4pt9+O50HRYWWzK$-ADus&`f<9>#Np-xx_pX~44lcrGtOT5jmXda1=UY| zb!X~bM$*eC%x49dSrRnYCY{z;b_M&lO*h-8+WNZs6Q~yCdEFzPM;i^2jAtOkGUto` zn$}x0`n_FB^}_T$Ux`NFbhtmnn3e3oCVXlohb zHE3rmC=xho&$NRxYyTt_ZLHZzBGY`1C?P5;{xZJEkh`pIolloB2>iBq(EY^+hvleZ ze5lnsyG4AGgZz?}AP7VmoX8dD3eTm>&S?$H&#>JCNe&3*>MxX~HTEL4bHq+Z|RM1D9nNmqP&Qiwa_!@ZU zP7dGucb?i732)Tpmf<$wvy{Mg3t_72a~<<^@nK1d7(o5GcKdhR(fHl5_TQ^ZNE5^% z1vh3ijId7pIp>>hr+SU|(81!!jlQAtm!E-dA0{$AzS3FOnbrh|9~s@lzm1SR=N)3% zxxBFVeDd35g3azZU%U>cKoCRdv~y4z^pgCrP-JbO#Z>v)&-n`g8;riuALlMq9XR0r z)GvsYqd)3@p*wE#{+QfU9y<&aaKo@ayZGV%YWV&Y9fk<-R6#H5NBhge|J4ZA#bVJP zd~>g=HNrtC3MN-%b@yV`AVLB{}Ju2}IK$+xTOk{ThP-yJ6} z-DdvW9Pw^}pRcHYLG$_=onn4iTutl`(^k}?*acK65D>HO+>zTR})IE)j^8PEi(HGYW zyTBePKmrvTHpZ9OmDRTA(uh}eoiK+L$lunp9N+50PL0uIJymzT zl0Ms&pCHpPFNPk#hc7vqIInZJ4DkkqEeXWQJUng=bX_`%U%3v}iUU{^Y8dS%3zdZF z;I9*TK3qK)*R-C>>?ipZg2N0j=zNpE7O=0Tl4HZXb85N|5Z;A*(z?xhp6GwwvzWEa zz)>2Xy~_&cQ&77<#E9}mFT#5PU1P_KuHn1fJMadSX$Hvah&`jh@JM)XAR>2Y!k}FT zD-`vvTz9%R_|JHAP|ZqG&B^~@WP2D>g0QDx$0PyA*V}K7X#m*X95mGyNc&m0c<)4b zv1^oj7CI%SU++g=2;wKutns4yHa5~nG}%N$1JxokF%su@3<6CuV#H)y9!8zjOgh*N2902R zT73t3Tj1dMBrN^PUWoIR_Fg1vlmK~+F}KA-rVmc6T|O8&!VRCz6~o^6Tc&@%o|IPQ?vde;--tgz@*0a6qw`c-7e6{?t)S#1r>*#mhtH?LO zqn_}e?)X(S$6p8XU;l)9m2mgeoAiA!d7}6@$DsIFFkXt$y3@YY90BM z5bV(Is){P^qLY<@X&rr#aQ-9hzKGV7@KYrdwYrr3E~408`-J5vzq3JG;9r~$_?}=Gb z2dhIU4eQn>a#t1f;TuA_pPEOSnuV--W=U-to3bctZqDUK*Un@;JkZ(k6xb|7cRLQ8 zWK>{_1kFAa-5h?&QGHiLxY}r?R2w-TI-QypYxo5TaW?V=ji_lI>zpBPgV_%9{T)@b ztl(*xB}J=_(^zxd4q*VL_H9hk0d(x|1Qqd*pDq-Ia16gJT=~_Ta@2ZT;b1T?ZZs-@ zT|%r?sqb0r@)yF%% zDK33tf&_ojms#FNQRy2ZQ{?NTEp-+0=6iB(Yi063xi`G?vGoIAb(Sp_da`nS!lxq@ zp6;vAj`auc$^Aqlg4XxFwL{o;iBQo6!m7_U`KJEVcllsF9L3}F^wZB%62e)T%pnFf zZ9-m3quu&-c<&=|*^VJwd`Cd?8@?o`dhU=rc7utl>llGwvmH;}(5Cje!NyzrKP1XR zg$U6e115j9MQ#K&J;YuV@T zBQo6^j>&7wiJ8SRWpwqUEVwLCkS)UTtZWqVQT6RObJOYkQPu~t@Y<}*6ENHx<#QZA zvOvNTg+~3t^m?D@>#pB)yJ%P5{{FuyrX%jX#GoOk!&L#owe5YE{#P9WT|}nxENC~} z3OOwZ!@;_cCAPl6Q^QCKzDN(KD0d*C6B!@E+ zC(zy*$GdCOtq!$^bN;xX5PUVQrkvZq=7{%mfiyfR#J2i+`|OiH%?$iRJ`0{CG41dA z=yLa9>%RzaineHd7-gd|lhoLjglvac_%vf?Akh{^QEl`dso?&0FLL${Gs*sY?S;#E zMb={^n65+lh*|N_HIBgwcaf`)Y%WOGn-?o;rM>61@=yYGhUG-}Q<401jf!qi69QMU zc%1T%OjGdK#arZ>EBO?1qzrH2xO8bgCFzTJT+7(1bn*GAe-roS;YWanlS3L-KUOdM z|5d2}t8O>=_^-Nz>QW*s0k33Hvk9r4;+$lPCN11C_po39GkjM@L~fRUTQmq& z!kLAc*^5k%>ihbV+^&^u_seef`j6Dj3hD5f+qmJxa2ZNB`Y3o3x_kT1*oidv z(ixq)+G3!#kc(bCkj{&Y3m{2@z#6eCq8|9U+M1Y{^p=HvThW3UzW~qoXxfI5;g$O>LL-R1)mM!mK8K2CK(N>S=DSO){x;@Nn-BEi8q4zeckodDVq}R%Rw9 z5pma(@Q)o4GQMMnXkxsR1v?pIQs#1jtHw4IgOGozhXi#+F>Wox~0A zM_e&Lde7^BKyZ}cQ&l(E;BBmsV{6tMbSKEy=%Pd#ix<)_ezh1FWAOCfHW!&vS}?j7gtkJ=dquA5 zq{xfn6n9B(uo#iG^~#%qoGQt))Q3!mHKN?r8OH}9((}8_IVGi+C~%^;#&@)4Ou{jI zcl8>Nf|8eK!BaKUy!G~dnz0V)>wMf%^zaXhMRqzJR?RBr9jI}SE5jPHUo>C%%-QOA zT7|xj4z`z$2l*NyvZo+v&6hfA>wEMcHm%W{GSd2ln`?05Q&TSIpAT9}{n>%Xh%z_6 z!gL(EQugM~yw&Vvd zMb5t5ctzeL%;f>0KmwzTh09w9$ zNL9yv^GRrRtEZaZB$XODyS{iv$GMf_m`e&l{*)WoGOi&NK32Ze+L|16Hri4cAmJ6f-|6|ld@3g z80{c$-1@2(;UepErGq+oOom4HWL|Q}#GtmKyH$mVc6Rl`c?V9X(FJo54?YTqGBeKY+SppigB@4P zoXc3mg~;N?2}dl>!Myx zD2BVoK4)k9MgGE4$_=enQ)Mw_y8U@e;}Ce+XgkiY^c7>{W4)l*i8}tlR4L(FF5Quv zLMv~{S#HV8xYP+R&4;>z`vj1!mh1|H%oGYTscb zAa~}WHPe=gViaA~+M_vbFTLUou$iCz`ag`}KVR*>9O{cA zrfDC8pfuzEV5d1~L3N`bRhH$5H*(O$$AETBk?4E19-MT?ZAdby{WTuhnTKBKp;Qac zj`wbtTLd)GOi77JIO4+7AIa{xOW_YCQd<(P5(!qwqOdG6mF`RtHL}R~z}j&%vXg)1 z>-Um-VnO4CL@lAj7MWh=9uHm4IonE(`Uk;x;iE%C2)Bn7ey+#jCpb3vy;$JgiU+`{AxXhiw~6$0c@N!I!McSy{JVaK^nfK^`YU;S@} z`t{!Q?+H(oFsy42y?3*(&8aIa{B?RyP7?OmpjC9z(Q z4yl*#UVg&cXw%#;_fcRHMjpa)7e#f!EeZlJUS|p85Y(MgVOCp4$v9CvTX9luyhm}S^ z8#;=l$v{VW7wu>1Ukm0ag+hEEhg?zQl4S~tX}&RG&&qnM`F!Gxs)+etdR!-V7{cS# znpln7zsEzh;_EU9L?KZtO;!ur!gL(QFi)o;cFD!a;~%8IH^*x#u?X3M!fuT~^`Ky$ zxT%V3U*$+xr^8=p!%mK7D~*_HYCmH~DzRL^mr3P{EA;V)f^t^R1BBCd)`dSvo(XZL zV#wCQ2~I8Z?>{iFx1{zaRt%58Y*BLKJ?<4q=L3cnB}@M zL5|aA_6BAkw~8O3vSQ32-9etaPuOej_F%V!MB&6Y!zeX^G~?U;{u0L@rlZ3lvWTke z0j>s-!C1d$>wE&^Iz6{rm7`QWNH08Jt;y{<*OPBuHB7Kt%9ejHeE+T8}k7J_sF|4N7%UKdT* z0oI95N2=2Nm$Vnt|0Vc_A-6Li3ws6M%UH{lTmGk?-o@^I@dV1QuM5wcHx)bGPE=UO zAHbz3Ut-TxRq(%NHQiI$IVtTlR>v2~cqRI>4$D}ZY>7n2@YN~~ooE{BgBK~p5`1jXJU3d2? zN-aT_FC2^mUV>^_O<$px0I&U+QcaS>E$`py7f31F-e8N#Mf7tpE{`5#(CG-w5 zvLk4R^pxE&=T0rkAQ1=+In#-2eBRj!ibfsPYqD?2 zL*l1Q>Iq@?dw%u>oZc*ys{#i08bjT>x`|dU5GgmP<*p{YyvChg?}4oZGFhW|NC5&) zLJ4^3`a?$osp)qLjp_w5ee0QzNP1Qy?e>#d$oHZ`zdeD+ZQH1&-g=zKkW zNn)_ka&jh=}Y`{2dsh{1X@yMs)lBe0v7#jauDD#(nLdH>LQ1UJC@V3$z-2QKOiOWXuKG4iXaKqT-!PCq~> zzV-72_&$F!TXY|~?!tYCn6s4{# zxCriW;}+vbN062!0s76?H(e&9z>1^??2b}z;EvyJ7{rUidx)wos%3tBZA8;NC5bnh)a*&zJVdCEc}q&vJFh=GOw9gh{pIj~b} z!gv;5M1uV4L7L8|rg#_fdn`@W@>A#~+s>1A{Q1|y0E3{@q5K}A^>k#~U1!hS-*rpp z>n~drYLjJTk4u;7`@6?$`yH=Yx`r>4RUyw|UyC)PKfX`~iQE_@Pr-r7wAD8|wjs^J zio){C^Q=b0+0+c4-+aXU!?p=J2|GRbc_!wxMscF_Q|2-xdH2EZlS}pYgw@R3Xq_hsb^s`rn&zjN$$|epfM9^v-ZNv3%WIhW^u$_7+xgi5XcR0)F^0d)`~l_Kjc4oU zzm>av3fZjTHIX=6RA^p{x1wWt@Xq3Nxsvd!9j8 z{Zl{)|6cVpf9Oe8ZtfML=~(OJ~~bE;G}F7Sl|mPMq}EV(G3aZZHAfMjl|+|B41 z7(@D9ECg1z9ak$xZS!LNo?M0ple5wa_?Y(m6UbOW7bYrMZR&;p$pXlk(Ui+C@Hw@E z;9HCG^IJKf*7XZYdLfOum9kDgWzhX9u*)>1jw&u_&pz`3HQ#da(@kM;gl#^^P&6qDE!B$I1L`QYOx-l{Oj%0Tj?zCqCa_vq zmV-_juLU!X+j1ed@bP=PGbtIUw|9v^#@zjdlc$#4F#D2~AG&+raafl_@d_w2=GtI4 zF%TW2v%H%R?<(ThL|Jas|57_@Rq>hmOS=Am8g5)56U}lZ88I}*I&KphQ*X!!YUX*#xr)apSr@Oz%AaWABvu}zYp&gN>nvB9*J>HT6PS5)DM1RSKhk&A*gG4 zbP#9c`lP+%Zkqn~Vz=yeB)E=u5o}2?kuwElV!N`kI?T4f`K6zd*HfCXq^x*&bU4@Nq#}p)tk4`xOfIem|3cFs` zqhp}UGwWG&G$0f9o?TG9L|u}QiSB?RS>o8jrep(&bGPI>h6uhVL|1p@^h->A6_axg z^Qa3GfARew`v-`!mvo|35Y=1-onwt1kEzB=mj`&cYTxD?8Ix#lJpCg}LLf!j5 zeJlc%xxAd^0iM-$jWLe>MpfxHA;ej3->ufH7~oDSi(CICYZ{*4lini)afbq-uB{zQ zH;!J#B-;9>ZhxW;Iop%|63O=UBcLOi=hxaGt^;m6PT@ghj(wZHY!*n> z=eiz$9#C6F{c`?u64dWo(>gIx*`+do0kHbR8e7im;RBb`!nB-TMa!>as_+}k01o=x zEy8G_=w=an{(w&zF9noc?TuE{%uOc~BbK*^m+WS}2F}aN5l0j49|yoH!NvMc1JWs4 zjzbt| zw@~L+`pF&jh5Lt&_{lhPZ12}JtlCoCbyiGN{jx)!P710HHXJ~)`%d4P&%DRp-BODu z{Uf_khskd$5?mXuD8-ye+(8Vzs-QuYY~eHA_S_~t+~uyKyV0H5`e+rUrPZ;lu*uqN z0p$n8+$L*2SEMQ(gDx>adn6hczjtC_bhUOV_3VMU+3WJe`I+UrTel@~qbi?mn<@od zXk?S)lx^h_Y%3wU%m{E@A#PL&g$<(BL0izqomg0W&M(ZFkD(29zkkSLkY8`#?wfp; zWHjAQ6b*?PZk57%Tmfvt?QJUQrcloYb@swBn==z-f}|($&0Di8HX!L=n%0`?I#@6G z{|_?ypY~b{d86_2J{&hEdtN6Mvfkp;U}*GEf_y!#NyvR_{ui(I|9jeQAX_~c*>KN& zpPwKZp?3m_$*yRFf{qCTAvx z7#5H5W()XCtf0t8Q@~~XK$bMdapxA(CE5!r5J5(A9P4ar(+l!!$`*88M&x-kYZRe2 zI+i?E(^zyuUxbfA#J;Dcmp#W*hW;Ubsx|1dReaYboIimQbB)RWRdu9iLV;bxx@``K$Q?mdp4J{l_LuUS_2?Y;TGpXW=UPhabZX47|ajy7$3rdxo*)y$TjT1TO^HiIkewokWNGItApq-y{fFa?C~TC<(HCFr$`N##)%x} z?dnuAOUV1@k7-W^28&N-m={iN*RY8XUdPpeprHM1+#&E6U`s4^cxQbzi<9PZ7mltY zLpz(@Zb^6a4^KqvrNFe{Z}O*-fCMy=TR5eTyvE#$QqbQ)s`hK32}29cVGz@`m(u~{TY?_vK8xZ%LZWOms+*wSKjyo!tFU>Chk;dHi%(GJRP~ytl z%VOR4dDOk4t7|ZXc4`K^Kplx*;ogLkfUhOnETav&GgSsVmOUEM4US_?B>2j0H7$r( zFIosX6<6`TvNsGkA{t2m>K2sRo*l_m+5g*vB8~96$CJ&leNnUc2G55Ed|xL9t;@|7 z_Pn%M36AcQ%X1LN#{9R3Fz%w9(!pP%8^3&>2!QpC3|=M3iFXj{3@z*5tCc+ooBM$D zC!xy+zqabRW;;PUks;FcR7*8qva*hc-X12@mp>}DbxbEs9$qsyruoXtQ{d{j{@C5- zvwN{_OChOMjS&%uw9b@#qvv#9|9J7275TcnesY&xpJvpnReed$^#bvI+_vpKALQ(? z!Kq=3Xg02GW`?^`Bx>O9(rN0kazO^)8ZJRbynd9DTu4Ug_maVM$7_mC`uD#(yYv3% z*Ll@>A{3|WVY8IjN3*Dj$7z|NVSlsiaBx7$UJ}NV8Q+hH;nS|#k;kG2Fr+z8dx4f8 z)#WiNL;}+|Z$H4DUbW-vspbmqF3%6D=F}GcMO<(*N*NFeLQ2D>2s6-A*f~ns|Ga<` zrJ5_Uy}Wv?i=`66m{CE9y>8_>(`bo~)fS3Lo|2-mblm6tgU-<3Us8YK$pZvRW_kPT{`5c<+X=l%D_twd) z7ViXlulp1&xBw~D8>I?y-I`sN1#8zT*Rqp&Dfd9Ryxjmv5PRwB>jH_Tq@gQC)UMjY zIVkU1JALnmJEYj*j+^I%P1~}eD(E4I*sbQ0HQgSP%Io#O$tSk9+bUONE&{M}&KV-9 zsp`6j=&Hr$YWaB;Gd`k!TdY+jpr$c@1-$H6i$Bpj<`V1rxGMa|KQ6n#v{E(5$fJ@H z)GJ{2hi{gdBy8{ejEeVO57W*%LSJQ1xZKe;L4Wf#g|8{S3|XgRWl914Ii}C`+z?Ly zy+qq#gE)OUPtJhh$lwbIN>}X3fiedC6z}sXz0UGEpLEP_r$gQ9k8eBo)E?baDYB$e ztW+L^=}!+bwQ;Stbq(G4roh7OM42!G-uqq-lqhy&8KnwthfBjBgMp1sJ{ZhQINf=6 zW7enELpC~^@20*yU%fya+8t!iVplfAbCY97+8g&wE(c?UidG2gtgvQePfg5qv`Z^S zy<~T8I=9!p9L8-dHl~_4ySqwNWAuoT`I|-UT$9Gx0aU-f5GMOTpTpKV`HlhtOx3wH zaf{3wikT>df(#zL+D_+ECln|u`(nsNYz??jC+?6<=TY?LB1OAd;k1oS3U;>D;O%#n zl@fWUElsi*-J{15^gIz~7uCq69^X6gaHYhG0l7n%g;LeM{tvgP^kVawOc29mcY>HU0=L_{*n}trtK6ac53Ahz7 zIqb5G6>v0~Hhqsya~JXW4W87==B?qia_Cs6DSoa)@NM;j{w6#UvqY~%GpAvn``)|* z`EtS-;!fX3)X9QQJq@HFB0@2FTu+OVnIHsGpD5(tGCse@S;v-&7idrN1uswGG`;zX zJ~D+#gv|J>e;uofTKH=aPPi!uLB;O-Tu;}#@IiHQzbJ}i`)^rXp9 zyEr7^l?MG8dFai>Kem;BrOfA+oBcNcq)jR-7`h96Z2dOzb5Bn-9I3c-1`QUaMn75M zpG-VybZDSvc&sG>Tby-ew1V^liDp7)9i2=@sI9sg*Fx?}cUiI^FZpdW=& zsAXlk_f>Lz7WRSCmhn}TPJZ2zv>B^$1ZyCSkb*t$#EsRqkh%jD6ty_uLd1w)bEtP( zb_IV)4}1yI-0&yY`Zp+yZ^BV1)j&Bi@@Ed1hb|OfJD6A44?YSvvV$BHFQ?Q8#BTm5 zr1>EpITfmBs3}r?NHKsC+3M8qVjEwyX$O5=Pj5F@YbpD!7+Hw}ajJRsMtk~Qcq_mm;ZkGu3wlD|xRQT_WfyH(WMxF>g+QX3AR%_{#Q*za8>JVjwj z!Eo|5oSno0Tw*Ir_Y63j$jBZZpm;Id2fZhvy}~A{YGI-C2y6s8(fdTZ-Otps4^uC3 zKaZA_F6TM5_XSZ;rr}h3s6cOjWfinHgKZFcn-wZ-W=BNi44x+m7WQoW?b8leN!b_w zUS@4F`*^xe5BzYqD-vL1X5dOaqr^Ih{)BSNLr}rs+;+^Wd|hZ;m8g}r#%e}c3x62K zfd(PHCGH_dzFs60lc=j1ZchG#AGFnbR6fp2e`+e0y0;x2CA4L%=xDfz}*~ zm~k-v;i9P3#bO7a61W&bOW`N5ajFZ(9Nm-Ori@6_^T6=zRE?zA*Rf)&rav_^%`i=q&hYyHQG9YKpYUs7N|mm^&?@i0sx;Y_c})%3xPz5K!>?OyI`L zJ^M##l75>gbxgzkX}AD&|4PGthz%>1{|HC$)?6`6!C_)Ttd#G!m5lW12`mBEd6Y^C zHDg7Qsg7m~H=hf6@e@iQOC*0k5>F6=oir8`O5DuYiaIL@8z%?aT_s|y0{E&aYrmS1 zOkjd+%}+A}TQkU>2hZ)^s~nMvFC`_`6UKQKbF*0#Sv+|dLc)GM(iIEOtJliCyXv_LdKl66_MU1#kr_4H6%tuVq#Kg1byu3S=bIM#?xb}mOV-q7s z(T^P1^MQ_)uJUihClM!Sg2oO*ZQ{;cA))K*WOENcAq>byDBtD8x1NWZk20K@Kc}_Q z+uMF$h6Q6va*L|^A$2cuB}Dswm<3^cxD;28 zNiuxTMtZRR^^&kc0#4sN=+=TZi4|sh;+;J5TW?vYT-Q23{NC1dR>6jRB)6viP2$|Q zNhz}q{B&|;U5`~xkKKa&GB24e*HoJE`@0 z8Cuc~`VKafj?Qn**al7D+F5(Z$iI@1s2Af?6v8u{9(QV@tZ!x(s-;e0`Rd(lSpmdH zV|FJh?q)Q@vX!Y!p+0S5?g@I4d59<(;w0td$a{RGlbXCHHEFM#A&^r0o~Tx7u&UJ; zna8m47pqCCEAqY!Ox&!bT*nvJ2g_XgrjOF1q0?&9G$EdTfwZ3gHX2w^@kw2+%k&xI zqrddGGZnRNbFEq)`o&MI`vddc^2G_iXqrRRk`00aP5?vLIc!GiB~b1X@Q=7^y!P&q%!qQ<{FP1wBqAm1mqSmMUet$jM`fZzOBFJAN(Q-3sOk3Y?-!VQo3HxEuma|Y zS!p>GuG8F?O-KvMc_3F0x>`uO2Tvrn=0hU+a}4U1__wFWj#&L;k1x7^MW`G(2~>^7 zM}_ZPw#|cPo*p4B~xWlg;ln6zuzhYI-7D)Slqm{AM6hRvh#cK)%vI%y*gsDoYUUDwJ8w z74QTdUtc~rR~EkKjD2?}5}zdYcmXcWp~6lrz-9&!`}ZF=K^~#`vhbhvg4juV#>tb; zX^-fsREeIsbVQk00BwLEpMfHd!m7@LxVR;V&z;?!2|D9T=h#WtT@U^3$lp3Hqct1L z2@?07_v>G@Q9mY&L4o&64*I`gMz9M^o_!NSvpS_1-{(yCQ|@%zQ0-v;7l-HT*puFt zL@p=sd+hXHl z+|*^lZp42KZd=K%h;AGR{4b~sHAb9f%IF*;YVIG{=%1c{jjG#;hH@#iE*q$`_&r!L zGjWLp#!PD)7>eJMoh3*tL4v*PElWm&FP~#?h3)b!GGJ8N_M86+E2-SA=J&1>bEq0K zdyIV9-wM|X$4|sa4O6trb=4_lsBPA`=_jdi^?X>VzA4f^hstPp^2i$ambtXzye6&g zZG`%lvQj9~m6n(lf{J}f`PWu#9zs(U&TBiUy|LwEw*c~VkI=}?XM`0}9EKkbqvCSg zkth|7v^PLA`V%|iS5&AjrN+6E(+z^P;h8v5tuaH@aKq+Nc&vw0St{Ufkoe$&U|Y&H zJdv}xv%HD5SfQ2NmGjX-&`R$2YZ}D%coGk*!C2c)BO_HX$9DCTfN)$&3(t6FtyK-3vbuGrA}}*GwXxBCxi9Yv*GRVu zbh=kY=$mlKX^`tLQp~oe79_N5jX`dZ2=EQD5fpweAsi?w}y-}J{$pSw;T`Qi%7 zOKj!II-9}5hn54~wL9s8d>vh!;RCuBb3xeAYwTGY;QJ2{1}Xo>uH^i8&h7aMC{?IK zYm23l0B#op`VwNiwbh7Z`R|+dQc-_2sVhQ|=QrIhavvuhhN?(Mp*${T`{&n11Zwd& zrXZ%u6>RJRy02vDT3e#c*WuodRoc4v*lbw>yOKy&d8!IRZH!bduBMt z!1{&I%A9)W_DFHx{O7w=R^|95st}piOhI4iAYd#ih4{PJQHAcH)wb)Aq}xp?DKD?O zGqRl=?`rOM_{j?`GlSA!Mq!=rU8V&{DP|EF0hfzvRm4y8I;V~jhy&xzVJn$UXN#WL zjaGhD^4-Rt3q(zw)O2Ts-I3pkcS^B=Q;hq{kzej;{k}}O_ru+Y5p8S75Sp~NV*V8#|5G&KLQx3Xg3q>T zuY`8gsBVwi18vnG5#uP{M^X_2teyIcLg!`;YXx!B{-R)3-ScY!h&hK#T(4?bU~h~d zHH#C_NW#7+IF!Sq5k$3s3I_azKvAHBAZtkK))67C?{HvoFDW6PdPWPR8qNBWyBrd6& zmDSL(x5vHT5WxC^S+!ikkG}HFM3(R#;tW0lt!T50PTE8%(1NCZ3et;r%sH%;j*or9s^Q~A!1=A;uw(rsnKFYz7jw=(x+)YAF3dt8 zCN9M}w`298k+ocTMai^Rxg`=O6%{u<$t`i;6DadwQ*N$AYdqxzIWYS~D?vI9PAqG( zGkC7~3(y@lW7E;y*v<0ZFOuFGzH2`40GfxBU0pL@{$AHEiwsM~-+xT=aC*3#U@%(O zX(#fmsR<^>AS3gjkI+z1JZ@w_htB*>-OYU}K*y6I8A$?o)QQ4wJ6e7DUj`uSThwr*!|K-Q!u4 zs)jilC1^qqw?oZ2O~n`HWip}{+aN#|_@~Cq)X$08ZskKt0tlTdCuzu*avVc)`f7Pk zsGGjq6Cw_ijM(L)-@)z)ksT@N?8tCH`3Om>8!Z2=lnVYGN}oHvXwA=b_AWZR3p^Aq z5}XG#0U`FfC96yJvfOpewyCJk$3P`($a_U4)iOSjTsPpQJS&9~Wh1n;&F)W?s_uY< z6kkiu38kP^+(31GK6c3c&ZR5%W0QeRm+JBy*cWQ!c9^It<5Ajq+1A0(6WUf4BS?p6 z5*Q8`fG_HtR7kv!)?$xnPnilpz_(xRq?U-pj(!Osbq8?REzr|}Dx=THG z7ly1g<{s)0{VBRwK7N(!r7K|4IT{EIqcx*$C!kG(dx&`O$iW_<$hgME@succrJy? z0r!3Cz@0DQiPu)1T1f<^7Qgj$JNhPu9h04pCrAm{dRRo4kNa>&_yI=+Yhz%I7jKqJ zUyz{Su5aOQlI2POmS+0Y$IvgIai8@ZI_$ZR_;S{mo9yN9&8Ao2ykS~U#@!q>G}3^vxAv3Kf2yy200j;gR>!n2 zg?-R0A)e=NNg5S_gRn{1n~aqb+r14c?ooIn{mcjUQl8w+LqMjHroib?yh6JAb`UNr zykY1B3;(~l>R0SF4Otrg>F-IC_VIs|!d2|4?_Z$rv#Zc)QKc(vbkDD(PlK8kBF#Bj z8Lds2vW}D@mphmDBMtkHvP{A7qj9>4kh7iDL@QUewXfU+jIrVBG7RL6*(n$;j3G}! zZ%}tWVZXBy!0Cl&S>C_L{d*!Ps=2MbsOm$uf`jex72#aNP`_;X;WD$azSG#%+TU_j zR?s5PHQD`LRjh0XN9<7a*=LZhf@k5*orm4}aDM%zQW8V<^ECARFEXyslihf|1sR=? zLngE9MJK)FuNPzHhd1q{Y*{tig{dbuXAm8C!{uolggC={YMU+sf+yCpJHfp)N}2^) z89(x)WI^3ds?tJpR7QRZoMYJ1jAmtuJ~i>oB$g3-u9yw`a1fW7fkOXvw`7yy5+igN z{G`;sTCW_ARy8ulw>)@H7!oDJOh}dBL@~SOQeU4cv|m=a<_&pn2CuBlFej85i6*0h zTtdPO65d^hA@61=wPJ>OoW&2Dw`@$^A&b_v707rQWt6Ao(>W}x6AZL?&oh9e3*BS-XG=Fx2PZOX?p^+2(N@bfkOJ*+N_4A^NhODDc`Nql0InuYc!A6%KO2 zEx!;*?`cY}V?^D^Q9s7$k*CECaInFjKVY~$9~`7JC_vj~iCzfIwn*EW_{Ld?AnLz{1R@-7-_xNn-W#|8}E5WQ5%RmZj3L$roW^iigetoDtB zofWom5^pqw7<?zCc}6HUvzvOb!E_fDj>Ybyde>fVD2ESV_w*yGx zQC;l-Ly+hsx?LFMf6*XD|JW~lPnQ-e|1A=WZxMRoHHQA2YKKyadmmeE3x`{Fv4Um} zLuIOVhz3zmCRuG*Hhz>~%LZMt9$P;NHpOWQEut0T<2qrc^rW}BT_Z8{1d=}2LEf+3{B8(O(%V^0b@aWKRMnoxE_?8^t@xL;Q4K1 z4pf`7HGCbRlc?jqm;cE+EfJ1pX1<%`Z{fTsEm5>Enb!Qg5`(048M1hLGbP)**htf*y}tc`Zb?hpOTvYft?~~kS6K4*D#h|k14CtFfj!}A zq~W+S8RJ5Z1oGFM)YORtIFWYfn|26%3e8#>%R62V#L5e^AL!d zR^Ys5wYa2Y)RGo$Ma93tCK?gws2lx3kkOYm65{a}=S^q*aOTCg)b5ZdXPc|gQ79Vj znU$hDs*(2>+#@GU>GbQa%s>0a)CjfsFisQjp>*p&;UlgIYW>*jD=Xt424I3m*tHr1 zNq=ss(uA%A5=ouf#5)uvPxr;L#^D@rKu`J5x>>+z|@d;A} zJ$fNz$aid;dJ5}{(huT^sqe2Pp-AdVBtFdhsf-HXSeeY33VrP|c zm9_Cmk?x+b8i*P(WrU)_JZ9;YgZ=(UPGgnb%Lv#cW;*P1;#t%~X7X;J8Vl#_laA(8 z)=asM3Iw0fS!-lXbnUcdaQna1j>y1&)Q+sqh)>b~p62XmaQH>WWin*Vt)%-Ey?T+F z{^M?+SJnFPmXUY+Uo!b)l*4$6IchU!p4_&3dIh zGztu5Q{&VxeAwLu{9ScvJgbne*&YZjL{oaJp(1R|r!DG(+5O590TksCANtVT?C`D* zYw18iJ9rmNtgg$zrvU@{x(ksp?{#V%qSFgui!K?2U*@ka-1CBZmuY>vb2h&}fn%BR zypL(HTA=rCYvTOl*YkVN1tvCKJjycOdzc}1q}67vyw=4iha+i-Y9e{cgr0%HTK_E)H>#)&UqRu4NH_RU#0deajtePB}w z*qQ}TIA~v}$ur8I;LYw65O67^n5ozZkur=ZNdn!^(PnUk$CN0lUz%0!(my$MTIu zFLrFwRBfx1XV+6Sl4|ftAn$FqI8q5s&_(oIknh^v(cdfk+NT8w&Yew}NN=01E*oTqoR$oM96R}E%+_S%Iy#8XHr(%6! z>d;*WaGg=-Rprwh7}o!3s|$L&a9Y7Q~91|AY4``6U^KcOs!AO7*<{JQ@3 zKOQm^TLA9Z}ecr$%ca!GjS5J)MC!Vn*(lK6G6iNlO3lFz7Op6h7b=PhF`kMYXJ?7XdMv@tG#1Hs@TATK zl_Gyk!|MLu$fhbT_zL+v8GS^BZ@&G?dzsu+W_3Qi(;wg%x6f{pym6StLmj61!6=;_ zzT*NGyOHzBiDK($JDyxEw9ss(7Wbde5nAbI zsM;B+>;}T5Fw3$^JZrz-jLXcxr8Inp1?>=TJ-hPAs7%*iTqW4fywbuMBXN#t6)HAw zpYor57nC{ykj}*74yxSgywBTs&J#(pzW2tT6+Nx0Rq0j%A~s>3>u+VO|GFJ=!zZp2 zn0*zs0sf3a4pa(6dr%_`EdqnkDXq4pT)uzKgiR^Qwh^dbr*R??AWt4Q zoqbGsOL2j+ffSSNr6tOe#~CYsyi|yoX_)3NiPyw%UucghagM;Hg;2AMx;dy$t6+yrrc!c>P4UYw#vWNm)yVs`2ilIAK0E|ujy^Ei6ijxT^yLN_yPUT$nzg({ z>b}`lbVw}fP@`j{v+vBH)4DLjNr6N3Ue+De9|$brNA8fL~s0;jr{ zkO7h+l3G;ftM$>RkUe^btwda7)h*E9g7h9OxQWVb-BXl_AQ$W|lY`2&%UEH3gCdEk z&6j6bA8rYc3yZSUr=igIdvNBdYzon&{+7y-(lNIV7*FO-89?W_LNha-hZPyB?oZNA&M~ohf@^2(U zUUDfUX6pu$Xt};_>H+Z1ZVoQ}h**fD?D^b9=IT4s+blr&OSR)Z(Y}0A@m#<+C-#e+9DpA2PhGh>jr5~(lKJ=djCDB> zYjc*gy`9>MiF8(@M7w!n3)EEU%HKD;T3eH%vVOs9=CD_X96f6FDQZ^Qd4+ghr1bgU zW1kqA*WnZeRkaWT?*VEDJ?Pk)xj+FVe&&WFl6sW1n8JfO#%O}GzL3mEGw z!{$M_(gzw*yRIxOOJ6u`PxEyNnAa?f>?=W z>Qqzvd+9G)g%{x=P2Q!ID6I7}J?qpo8R~{nQ=e zxZX?9gMfg*Zyxqtfvj$I2h$|X`V@z5T}CC*LXK|s!qw;M)`#!3gQ|!7@*qye4{lTD zF%)sg?YzTfS3NU14}RB}$y>-Dqh(Ym-51TG0`<`wmyo&E!%))RfU8DX`-6Y%+#8;& z1(%9d>O?5kLB*|bIoY)ujurRH=P4B3XDjup5g;F+@wNZ_v^hW;E0!m=@;ICC&NHVM zQQV%v^K{H5q@t6f-_ocRS(~4E$n2rs&-5M?i#?MBsgAgy70IpE88Y=8}`K-e3QCF?Yyj7g~3&MTEJ*&f?E|(Z-p5QgptYy3X zo>c-C;vZ3Pz5B=o`58tDeB8DUs&iV5js)81;WDxL{Kc4FPQXul3#+u^4W*tSS?L7z z|Hfi(k>KOS!Q@XL^|1eq=KzHHMd?Ra4cTC$l84;?QW|n(Wt9;2Kt`Qmq59tIWEvdx zR#mEX9v*)|B!^hXr7w;Ya92$!A!%VE*ckk6%slWm2akFY^<=aP=KZp?qNa#zQu|Vqo>BHJf6oBEeQtTr^?}{_(Z41)!bxW z^G2|WreR{st-6~w-3s8+hYGQ9spIEN&g@n=mx-@I@{$ETb2k6CzcMr;L6Mj3(+a@d zEy4$h+&Xu26%sDM)<*s0Ok8b0UZKyr=Z2q6ocqwzI}`2x56^=ONe|^pOp`#bwdV~0 z*sb@-PpLswNqGhQvc;#85_;_WsM3^2kILi$tXG5mI zX(_H#q-jAGONSSuKtC5v&wajWBbR&INyM{Vm=8IV-NRqKb14qvuLqJdxcr9qYAWf( znzYIQCk!8$V`o{NfLnmz^;cjK0Qs$YGOoq0uzll~2pXR9dVuNN$-WSXJx=mE`)JZ# zI4qSU4!-8ApKHU_MeeH{iRj|_=C-ulyqvz=yT8tStD2YY6dJZX^5bKdY+zCzhxx3- z2U^-2m`1QbYI9ozk5$ zbi)kIf7E-w_rBkKzI&fP&wkdNbM~CQ&tCCcd#^LBZCBpR!$$FJ0m@pBA5yUyc(a=V zP=EckSmE>m=Qo`4%!^g}tMiuk(J|4}gML>PI><-Ygz>F4R=fk^$TwL`^-ppe05S#N;Q$7EznLMz=jgXp5?%)c%qtce zI}d!p8dL{NZv__U7F&~N;+|zq^tCK~mcuLtH$v(HR)$&O8IEsciu93|I3uf4A5b)%Y ztDTauuvWw)l+|_O&64Bn-L|aV_H)T+6SQK_wyD;0zu)!^r1UBfTZHF)I@|(iKfW1~ zV|3(Zxc;{}ERymUd|1&^rT#Nv(_m;oLimX?zUh_@r)Rghw>-WyEBSDF$;)z!^!31S zT0FJl!LVN9Qd96x=5AO0*YP-|wBlnS{m>gWiy`5X~3?$4f z6+|K{&gJj17mYX7%wJpO*|F{Bjdxc5dKgI9EyrhwsEpYxH}g6f_t8QohvRR8k^;lR zuluJ<6(c!FY#t8w<%Au34=dOx8?kRGE$6as{IshzhgL+~{YjW_$mA8VW?%hyVg9oj zWMbc{3%4G&kKwhUn{B)vAWTCxOi$J=6A@diUlxb{RcVdos(yEWaO~TF?>#K;Rb3sW z05*UJrZk^!s&$$aS+Tfnyxaum`*OAk+EQ^QQ(B`*1180Tr8Mi8R`D5wA0&uDcZqe9 zNHWv#(9W=CAn$gF104nZBVYG|Eov!eqVl4KqKm4k@JbJN&2%y-pfmQ!AkJq_BW|B* z$iiH&EqxaAl>)qs3gA+76H+Kl(GOg#>LCT%M zJ6um_lb2;ly}UIOg4Orl1A3bMYM6$6t@)+WOuyY4xv1|Xy|fWrqioNmq&)|;@Fhj; zk-(F#I&WS~4@ctn9^QkSvnbOLF5K#2*ihpRyTjsTYtM{;)0#CtRF_GRN(%GKW{yx3 zjauc3tARc}+anJYW?WbTydbkU^s)8U9mKAHj^j@ zrN4jM|3v3RXceXsT9s(^+M%UbVCNWbE74D8@pfRac>(+Mip_)R%cuwYF1KiV$!(WMV!}uUZIMs)*K!cC>l2{PUg{B{;?BUNCgokhv{Sp`Jaizqei&74Er- z_|l`H)geGm7~!3e_}p(dT1h+XPzK_!G#>$Ey&mE#>T3yGPX7?yJNq&)W7u!Uv_~QO-mHJ{~zKD(p6otA3seBu(uMGGzW)i)h4dmO=mga!w z%?)zkTA*lCbj8Z(QK2D6cLIk(6MXG)2NcEa@7`|!#ol&Y z@WB}$A)-25NR;Ub+m_U7PCpyYt(-V}G~?kzLq2}de+VX& z+?U&y@>@P7E`xn}qez^N!z-&>=6A4!W1qRF1JI}U+4tW~7t7$mh%(jT%5*onDJyOv zp}zQ*Yea^9F~q&A0tfW|APmnU({Gh<{;)d%Cc@3U=(U=yqY!mn6Fu+p^*$@TGR=k_ z7;1l`3s}9pH7%)W;N<9ko{h8U;x}|P(R*Vzj^J)M{*G&357Y)y+@%rnA=+I+1>iWo zf0HYA)mDlE)*khu70ubiqP4c#sT6g)DX-7dj}+ zkJ85R_J_cVTep~J<)kIlC8fo@mSMPgWJpb+q(t)NoBgVVphq{HsMH1c&Okdwt z?9-o)4P-s{p|Sx>*$uk~XJP%H#VB}vuKLt=H{Vk4?v(A-vJ=Jl6Rx*e=x62Gt|waAC9HFrsKBGtDSn$;sR-)VxaA5g&u zHH3pG_G+hwX+NzRZh;ZlS@}F$FNZ2gJ7bboeSmEv_RO&mhgcyWCo(=Ncia|{$+$dg zMGWdlTOwJCIZ|z+ra}x)qk{|+z0C8T+#+B?zppR$jPfO$FikxeNmDR#^kQDNL)P9| z63(;ctwMo4#qwjpdGutaY9NL8sJFNQlp}gZ-?nCBZk|aUnum$kP8KNpBy_IQ^I_x# zz_-6e?+%t5@?`%Mqne$*^647?5qLNnn>eg#XfCUH5KsIvr%flBwTU74#ub8Nqhb#QJ_vo;*9o%uS==T*&FTv2+IwI8A z{DHN4b@#qAhz`O(u+Y}=xp?JLpauMHy6%9Xav@%@b58P(jcd6uhOWX7-i@CI+Qds3 z=M{Dnv+a=-4hvw>p-*22!m7Qrv7C+O&17S1d@qnwQr03x_Ge34IMT+cfH$DS@eFM3 zdvu#AP8Wv{K-J*Beon>IBtP&H4`7`KM0fFtTOEelz5JBEBs`_aNygYUBq@Rvp{Ph> zveK%g#_u<-0SrDCsIM)BnL;o%efmzv$At^n{^MEo zZOPHwq_TY^ZkI!VwrUYXMzRG`(jG#<)my{ug|UjWH8Mn!ptjB-eyk+%4$@^n-(ZoD z6AA11Qo)77%}aV|=W;=q{RZDT?gAD|u*ZjQ6Ut3brlrCx^SZo>bo{F;wj#bm%u~>< zG9`MNb-t(EIC1Y~F>;o>oO+~3NeZ-WRO$eHGsrn8Bxrf%H2Y~;Lf+Ux&mJdUtv!38 zO{Vvy%FoPZ%n;SF2bdQklM9ZKfKUdcd&?MayU*S*HHx|5*s)14C1Kf@=qB_DA=x== zEqYUP!kINn$&<;+`&b=*+9a)ZP3FW2jYs2mYDMzal_{owPT~jh;_$7Nw|v1qyg ze&#VFI##v&dO8Szw=UbMLdW65$~BW@CLG%;!{52G&xqHi|4_nX3cg25#E6ny{4{eA zZ1w=x&xW4nxTF6?S%##a>uarc&k;jEfG=S+)C-XH#BfrHG(dKkZ8} zejvzGWAUp0!joqC#nvA5)F0<^S4pw?kcKBFdX5e-(!g99R)|CAx6gmS6_a}Tgs4r6 zOL(z`B%*H6Z%6LQPN22{Y(d0+MATFF^w2EN;kEt2o$3D4nm3((i7S1T+pTMf1=aVO zemua*r&&SW;=Js>#?NDh5jm5QU-6t?4(Cf2rv^BQ+~UR&5fMSe2e^Tk6n9&j2d&;%W9}K0PExUZNi&5KOHe}{71d(0=6V|V ze90>irA2oDq!F`S5#p-~czSqaxhS#_2=Tb)WuH>lq89E{3&9aCrYmEC!5y^I3hr<-uL6zlw(x2*c*5`*J98Sh^}uJAl(m zc5j;|8@Bf9)J$`0sjpc7X1`Z8`9yGqp=iaVYzGWS&3F#u_8!7zmm-g8LQZ-I3~oF= zHH&V;I&{Ai)U_Y`p}ZEvYTNtXvFDj!k1*qK78RQUyAuu19jleok!c68cU%uqckeDW zU(ulo9ewN0#VZtRcOqc=%fZ9S+`gM2DXi1Qt0Sj5d<@K8T^j*&py@$YB|h-O1{^tM z)%f;^4)SD%i(nLphRpN_us2*aLfQqqHBPPlri>Fmm+$hG0U~>96qt+5CQLlskWBTZ z#z3>KlnR_jClSKsJAMOcJukf!+O>*53}s z$xN2wM1D9b25z*deU?WJ#;Yf5di-TrFje+(EU256piFVwEH@`P1|qVM{7J~I(J2Ux zOxghu`H2FAT;xTkC8y z^po@ujPVZRzH+Uig(n#M3)d@&mM8tbvzY=%_sR;2j21StXX#TS??jYokG@h`l-)Am zGF3?Kg?fsyf$k(M&eAOrk`oSn=DQGj0^7D>DEGn`KcKno@_DhV9gD^`l=J09&IhCM zSqI30z`SZle=Y$cd$RATxF&dPupO(s3AwxEE4)YR&kdR5#i?4t!h<+e*Q~*3?G>hW zx$8Q=4o*9cer*r0i-g$HMdDTu#pZxK3yl|u__cUfi<3*~~95LRsb_h(@ zAHh&xMvJXoqIY|rkE6w$+RL#`gpXlb`v6{hmv28!$TOCQR;>6AHl-B|_5qSb@q=rN zdeFi#xU#YD8tvRi zf5_Erx`T-PdK*aKc*&A+pIl{7n9h`uM$=m%9z%;mctw87x#}b3%ecu&b=e%;eMVM# zQSQZNcBqRk-~eOqIXU3X^Tx;q)kvrAp66u!XRr@I_A~JhIP{B$*(XCoFd@(VENc3& zcq)#qZ3)jDNKwbz06;ljAO{?LegrX4h65LPh~{8of--KpSyF;!FxF}>ZnI+7h*cEb zy@})vC=7a4Vi2a2g)(0%qTV~pkrA9rwla>Up;+JLv|5vTEIcCl0aN?fYA!mb!h!Ol z{^%mzLM5r}8TCi$gP04IyhTf~jF=@uVaIpQGEY$42*b76g(L^QIN$=~5@6M0zd=1P zfie+!Km@%od^OQl1ITG+c3j>kL}<8@Hbc2dmV(=QeR1(1s1gV$??c|r!< zhpK(%{96Q|J)Jv^TC8dQo(F2^^j3+pkGAy&8q#8;qHDvHGMs?TrAeSEK5tj??Y`!g z1juMvqP<}I^h;mkbK$mWCGDr+K)^-wi(=vG&YCNP8P@QV^tlK8R!cFN!OItY^-=jE zo{mGc=_=P?vX$V4aE|%JZk|0_?&o2u1-0wZW1Z7)@}Mg~=*ekw>H3Q_e2VI7#;Dt! z)jygcF`yosALulb!}$HZUY;@%bJ|(*NcOhUe&!cEAr28tEe$lf)oHC&AshC}sPD3o ztM#HX>5VE4<;zu-hd?%MoiJVu|7USYgIMORKZQM+scSW?W>y|~TGrF&v?a8zEj1_= z9lP&S&Neo>Aw@iPuVQyl3}QmSbipM$0be}L<$ZDtZeTY2YdA#7wS@j!fSAcaMZ5#` zc$S(mW^D*5qW!aVFg_X^Gv6%8jw@-&mvW%5^Q?YeWPtyFDL)A7VOG^l_;;~CHlZh( zwY~oY5BQdR7UN^0d*rS#RsgH8b5M9jkc=l$)7kTW!i95 zh~X!1p^Py_)5#fw)7qkRh8l-Hu(4J0lC?7*1d)CfebA}^oplSXBZM|1%>PNZ20C8#z7k-q+&(oWC{nw6vmT5CNq#wwAp4P=!R;u6;2f&<-({G6 zPuPs-Z~eI1_by6}T~#GjMjY$CbilwI*d{!bsBxzWoqDkCQ1Uy)0>pKOkiGx#qmKMx z?R;FT2Z{YNT65v>FFF(P2fJlkZ(p1ovOKb9hdZttLUzI^#`ih39%NixUhe8a^OOMELk$+of=FUomCO8W_dWy*1XGWx70GqJVq_B&?NSl`_AM2(?n zc-N0gS`#f$0O*A_0ryoyJ=4(qOB6=}!x?aZlLZ z{1g0sO;6C6A{_YgrUi5s^X>LOz!Ro}N%qZZgcLLoU3YevwZ-PYD44({Zh1-ul*&8! z1XSi>bUiWw*%DVIS2l?eH4-B@2b1rB961Srr9u0kwXaa*P^|Ley6H$*e4@4D@T%oA zqo6vZ3E$X3h+fa|VcPPUEwC-;nclXvzN=U_PMptvZbj>PZ0ms9XLNu1$hzkaxg6dRGM})W8Mzwt!Nk({;=cBc>cn z)5S+RsI|j@$kxp!wUvFxPH4W5cj-Dlq+#Z&Z%UKTeYjJ7hxE^~@i(;=!z3A+aeO`Y z;LlQ$^MQEH`^r~de3?|>d(}AxuXbAP`YhOF64{M9=hsIjNqwg+U>^2(nh>t&FnvTR zEwJ{rbnL^>tmkcm^*VOKS=-w8+op{w+iB3bPM~s#z`GPfz{si-!g@U`byIT(IVOS}1 zhq%}CONi0Ml`&)~sLWkVQ#bjDQ4HI4_ntidr=_K)KydYE3Ou?i<--z@;9V2Azj>R8FM>cC+^Ec2I8b9`$)ev+Zk#>f=a#)^ zF3w&nT(CL3!E28-BmZPn^S3tti=O|All-$-6JxSxN-Jm2eXtke$W?WU47IAR5vIG9 zx>f>NR)lI+l*Sp3^~HS|9%0=j`FHfhu?*`RlA0T@2V0FjSvm7 zA5|j}Ypbp-hX{rgsfS^8$Y+IpJidsG2292|z0|74_Ol*HIQbOocHPX}j)8W|{Ta7b z7f7vDhoZ8g%*ymtq@5A20<`wiYzl$My+VwuSIQ8mR_~ ze-7Y(M)KD|pgRLhRs059&@F*>rp{w8Vo*>a~@wa#?K>%3R@C9al2CHG+hi0>wQXcm`Z zEjajgD5LJ+gzBqmtdgO*Rc7WG?keirG@o2z8Yu>HAr=DhMgJPA`uAh^fF^m2tGne( zzLhIKyFXlH`XZByN`k|f56=imhc3>~-Lu|)SN(?$R%QlJ@YjgdG|DGqd%Y7`*?HT- zkaHBCCl1PdLbonuOX5v@k2r3~(3gJrkcMI@*NI(Zz3A=?1^1`+B1P*j7T%E+b<_F_ zPCcvNE^5skr<61y`S^e7!C&0_Cu6N~Fxjz>#X^)C_FM477^%`)c#KXn$E4<(WUT(+ zqk;4-BY%l1!%8y4Ys~`NiOZCbA*Jjp(;RL}6O_wCfQ_bV{*|T0%a8bW7GLrfb-}Q- zdlKZa9nixqnTe0teE;xF|GM=4`!VVfT8@p5cOLyy58auFs*r}{+0Lss9+L?|@5^#0 zSEaeO)j>IY!w35&mS6(Doj!cVD^ccAKge+ednfehYlb`}R9A1GMfFG}2gq|mgS)+L zSM)^cBcHx{3)?Nb?CU=s^FPq%Obi$e#FWrKi~n-5K!)clug|7J_C1R^g&#^vM-wSg zrXE&zPsLf%Dp0qpwt*gJrSA9R`Sy=3qmxv|xM`h$6P3y`y3W(SY^YR>cIR6>@$K6E zaFkB?p9Ju0(199&SK>z4F**OhoX7y&skoUV23s|~7l|8hGJ;RjI-uPn8WP@6fYRrb z&F3N8uAdBf8gM5i+Fv$rxP})F#!Xl0gvs8+iF8j;P;en%ZFfN^*SLhnm+D%&!nCb0 z7jB2@<@Y{;%EDHmHbP*S|2;>GTE7+K)@8EK7_|S3=TtyEW?y83Wi^KU7eEB=wc0oIBkF&d3yT8OEu16Z+ zO@n1k`kpZ*_2h+MdXO7GkjiO~Y`4U5rEJ~r{vl{G0)mIBQ zL2a+j-jbGGR44Lju~ozF1!>Po!g?G(T?Nbd&#$qQdg&KmWo4=Q%g8%s=#8+6NH#hC zC1CV-t^K3cb??yL!hnB$v-H;hw(nq$hOov#Bm_t@&N6OPgbC$&pkO%fBU+03(ED`V zeaQJ14wm3VsC%uBl_fi=j!%heqmo3%>XQ&1Q@lzjM<-{Ub(jJ&%k@EKyoHzIF0#hQ zSq0agai;WH4^53;7R9u0iY;jl0qb*E-^*oyg|s8*YyW>2*x#-g^O5W$JnBl;Hp_l9 zxoMXXCh3V(^{Bs)C_kQjLosf<{s&X~Q}r%@W5|vE&v7{;MzwmFjDSOliS90E3nSG? zkMOIWF?O|#Z>%dpn(OW%>b=%J1gc9+faUp?S-r@xkIm<^nGGi%FzMJl9XLWm-RM)) z2yMLwC4EICt1+FS8X+}VDYSPJBHam5Y-4*MtA7$(gm3@o67H=SOSgBl$!a&JL39z7c}G7 zH)Zar0dQ-4Lb&r(1KYi(6V<-$>cmjxv|Ybea2ut^?m!6?Ym(F%Cksn@+vz=yT@oE{ zM?HEBP?c`$I->%5M~Jbq!$q$1X9{m@s6jL9kBhiQp6DlsT#DcK&d zM!^@jy)${8S-Guk>@=BU)CtRQOEB|*Cp}qYlSQ;9UCuIeL-#-R^Gohee|6=#Kj!wU zD{Ba5IhHI7J5le>BCf3fEo?+`c3xFA6w_g%nOA7ubH?qrM1NSjL?SgDhJyHik)x z3K+hp{YG8e9O;nvMzBw8t>L)j$%j*4?A{Yid?fy@uQ{ph^{bK4g9g@pQK^c% z73tp-zWm4O!Ee*SU9ZLYBP@4n4AQg**_6I0^4Uq?EE%GUB>lVK$K&tr{Z-Bf!ll;S z+|BYdJrKcCeU_d#rQJIc1Z#JGM#m&&(Z1)gA*yc(6p)t*Xnd*g?wt)g(PjU5N_8MwGOYo|6S`S`@v5DG`s zY!7ggtJj>AvlmOJ(xT-^rpx1AykSV?QmR4k8pRhpD94@}dw3MryQ=Tu2rTayzlVHo zEK7Lg%spQmzdGR4@!YD=&8Lz(9Z|4j){u}g?LVN45B>S+M_PPZ34jy5 zz4)t#D+L1Vlq*EK*#hkEUJdAgHC;M_&^7S|2SdMzvE6g4M|Q7&5#irtlB!>)o9csm zYT3Fi zhzvgLnZbjkbd=Q#b01G3Gef?0o4~z7w#I|o=sBS&0JK5#> z#a*6X1o)d&z65KQc1ZssQol%W;V;=d{jUNd{ohS|nnQ4Gs-uL4Q3_d}|7}Y7FK{w> z7mG?aYwhUl+td_P%>3E6ObxbgzWpHwXD4kga@fFsDy3|Eupn9BxfSn+-Hx8| zw2G&o{+`bQ`8wu&nnj3$XlFZ1G~fF_)m@jO#h$6d1GXNpWuA+UFBGsdn@x$&NAs>L zjJ|~uMPFNcc(-CU6!V;86zu8QcxUg2e~{W9+)~1-h7UBG1_!UXgKWO}QLwc$O1Q|tnxhq(L0=0;T5W95QHcwWUj3_M1w@kg60~U0b|m9Mj42L zexBJL67!sp=)7q1QCPqwEug+`Jtk_-MfSNIg%hQb$p zJlu`@v*5x0M|l=oc5e{{CkM9fwrJTh*TWCdvW=Z^|AZbs?Z*kjFhoH`*>ucDG^>IS z&o|KsA!^vKR49-?6*8d(|8?hd%;@jR=`D1nyXH(B8aLeE7#h){^{F#|>Km^WZ|eSe z&&01gXX12!S3+oaURYU_O&twz!m&Ah;Ka~SNqx5X?+m0KDvdWvo6+({kkjKAV`WHk z2L=o5&f{a(jWI1FPC79^AD7!Q4<9KI-(9M%Du~><^?oIA!agz^f#SXtIKKT{2A{EN{2Cic<4Zu~!*82_&elE1#t dgd{rkZykM<)P#-x?J4?CPDV+(NYXgy{{a#03@iWu diff --git a/cpp/img/goai_logo_3.png b/cpp/img/goai_logo_3.png deleted file mode 100644 index 1fe79f898a320fdd98897a4c90388dbc39d3d1c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18845 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萝vT*GO%l~R}) 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 Date: Thu, 18 Aug 2022 19:41:14 -0400 Subject: [PATCH 48/58] xfail custreamz display test for now (#11567) Should unblock CI that is now failing due to upstream breakage. It appears that ipywidgets 8.0.0 is incompatible with streamz 0.6.4. Authors: - Ashwin Srinath (https://github.com/shwina) - Bradley Dice (https://github.com/bdice) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11567 --- python/custreamz/custreamz/tests/test_dataframes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/custreamz/custreamz/tests/test_dataframes.py b/python/custreamz/custreamz/tests/test_dataframes.py index 51f55684500..b07f3f9d946 100644 --- a/python/custreamz/custreamz/tests/test_dataframes.py +++ b/python/custreamz/custreamz/tests/test_dataframes.py @@ -502,6 +502,10 @@ def test_cumulative_aggregations(op, getter, stream): assert_eq(cudf.concat(L), expected) +@pytest.mark.xfail( + reason="IPyWidgets 8.0 broke streamz 0.6.4. " + "We should remove this xfail when this is fixed in streamz." +) def test_display(stream): pytest.importorskip("ipywidgets") pytest.importorskip("IPython") From 5ffee5cbcef128c6384a77e1b743270309ac6de4 Mon Sep 17 00:00:00 2001 From: Jason Lowe Date: Thu, 18 Aug 2022 20:43:40 -0500 Subject: [PATCH 49/58] Fix JNI for TableWithMeta to use schema_info instead of column_names (#11566) After #11364 `TableWithMeta` no longer returns the column names for a table since it is looking at the old `column_names` field instead of the newer `schema_info` field in the table metadata. This updates the JNI to use the `schema_info` to get the column names and adds a test for this API which was missing before. Authors: - Jason Lowe (https://github.com/jlowe) Approvers: - Ryan Lee (https://github.com/rwlee) - Peixin (https://github.com/pxLi) URL: https://github.com/rapidsai/cudf/pull/11566 --- .../java/ai/rapids/cudf/TableWithMeta.java | 2 +- java/src/main/native/src/TableJni.cpp | 5 ++-- .../test/java/ai/rapids/cudf/TableTest.java | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/java/src/main/java/ai/rapids/cudf/TableWithMeta.java b/java/src/main/java/ai/rapids/cudf/TableWithMeta.java index 9baa127d39d..b6b8ad6bc28 100644 --- a/java/src/main/java/ai/rapids/cudf/TableWithMeta.java +++ b/java/src/main/java/ai/rapids/cudf/TableWithMeta.java @@ -52,7 +52,7 @@ public String[] getColumnNames() { } @Override - public void close() throws Exception { + public void close() { if (handle != 0) { close(handle); handle = 0; diff --git a/java/src/main/native/src/TableJni.cpp b/java/src/main/native/src/TableJni.cpp index c1841afb419..a4f4f26838d 100644 --- a/java/src/main/native/src/TableJni.cpp +++ b/java/src/main/native/src/TableJni.cpp @@ -1357,11 +1357,12 @@ JNIEXPORT jobjectArray JNICALL Java_ai_rapids_cudf_TableWithMeta_getColumnNames( try { cudf::jni::auto_set_device(env); auto ptr = reinterpret_cast(handle); - auto length = ptr->metadata.column_names.size(); + auto length = ptr->metadata.schema_info.size(); auto ret = static_cast( env->NewObjectArray(length, env->FindClass("java/lang/String"), nullptr)); for (size_t i = 0; i < length; i++) { - env->SetObjectArrayElement(ret, i, env->NewStringUTF(ptr->metadata.column_names[i].c_str())); + env->SetObjectArrayElement(ret, i, + env->NewStringUTF(ptr->metadata.schema_info[i].name.c_str())); } return ret; diff --git a/java/src/test/java/ai/rapids/cudf/TableTest.java b/java/src/test/java/ai/rapids/cudf/TableTest.java index 9b1f7a2012f..df8dce245ca 100644 --- a/java/src/test/java/ai/rapids/cudf/TableTest.java +++ b/java/src/test/java/ai/rapids/cudf/TableTest.java @@ -407,6 +407,30 @@ void testReadJSONBufferWithOffset() { } } + @Test + void testReadJSONTableWithMeta() { + JSONOptions opts = JSONOptions.builder() + .build(); + byte[] data = ("{ \"A\": 1, \"B\": 2, \"C\": \"X\"}\n" + + "{ \"A\": 2, \"B\": 4, \"C\": \"Y\"}\n" + + "{ \"A\": 3, \"B\": 6, \"C\": \"Z\"}\n" + + "{ \"A\": 4, \"B\": 8, \"C\": \"W\"}\n").getBytes(StandardCharsets.UTF_8); + final int numBytes = data.length; + try (HostMemoryBuffer hostbuf = HostMemoryBuffer.allocate(numBytes)) { + hostbuf.setBytes(0, data, 0, numBytes); + try (Table expected = new Table.TestBuilder() + .column(1L, 2L, 3L, 4L) + .column(2L, 4L, 6L, 8L) + .column("X", "Y", "Z", "W") + .build(); + TableWithMeta tablemeta = Table.readJSON(opts, hostbuf, 0, numBytes); + Table table = tablemeta.releaseTable()) { + assertArrayEquals(new String[] { "A", "B", "C" }, tablemeta.getColumnNames()); + assertTablesAreEqual(expected, table); + } + } + } + @Test void testReadCSVPrune() { Schema schema = Schema.builder() From f42d117621cb73d09a9c0e2b7d95d6fe00a92cfb Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Fri, 19 Aug 2022 05:24:56 -0500 Subject: [PATCH 50/58] Fix groupby failures in dask_cudf CI (#11561) Dask-cudf groupby tests *should* be failing as a result of https://github.com/dask/dask/pull/9302 (see [failures](https://gpuci.gpuopenanalytics.com/job/rapidsai/job/gpuci/job/cudf/job/prb/job/cudf-gpu-test/CUDA=11.5,GPU_LABEL=driver-495,LINUX_VER=ubuntu20.04,PYTHON=3.9/9946/) in #11565 is merged - where dask/main is being installed correctly). This PR updates the dask_cudf groupby code to fix these failures. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/11561 --- python/dask_cudf/dask_cudf/groupby.py | 18 ++++++++++++++---- .../dask_cudf/dask_cudf/tests/test_groupby.py | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/python/dask_cudf/dask_cudf/groupby.py b/python/dask_cudf/dask_cudf/groupby.py index 58024e1b71a..3a1557e77f4 100644 --- a/python/dask_cudf/dask_cudf/groupby.py +++ b/python/dask_cudf/dask_cudf/groupby.py @@ -249,7 +249,7 @@ def last(self, split_every=None, split_out=1): ) @_dask_cudf_nvtx_annotate - def aggregate(self, arg, split_every=None, split_out=1): + def aggregate(self, arg, split_every=None, split_out=1, shuffle=None): if arg == "size": return self.size() @@ -274,7 +274,12 @@ def aggregate(self, arg, split_every=None, split_out=1): ) return super().aggregate( - arg, split_every=split_every, split_out=split_out + arg, + split_every=split_every, + split_out=split_out, + # TODO: Change following line to `shuffle=shuffle,` + # when dask_cudf is pinned to dask>2022.8.0 + **({} if shuffle is None else {"shuffle": shuffle}), ) @@ -436,7 +441,7 @@ def last(self, split_every=None, split_out=1): )[self._slice] @_dask_cudf_nvtx_annotate - def aggregate(self, arg, split_every=None, split_out=1): + def aggregate(self, arg, split_every=None, split_out=1, shuffle=None): if arg == "size": return self.size() @@ -459,7 +464,12 @@ def aggregate(self, arg, split_every=None, split_out=1): )[self._slice] return super().aggregate( - arg, split_every=split_every, split_out=split_out + arg, + split_every=split_every, + split_out=split_out, + # TODO: Change following line to `shuffle=shuffle,` + # when dask_cudf is pinned to dask>2022.8.0 + **({} if shuffle is None else {"shuffle": shuffle}), ) diff --git a/python/dask_cudf/dask_cudf/tests/test_groupby.py b/python/dask_cudf/dask_cudf/tests/test_groupby.py index 9d2fc5196e8..e6c23992c4e 100644 --- a/python/dask_cudf/dask_cudf/tests/test_groupby.py +++ b/python/dask_cudf/dask_cudf/tests/test_groupby.py @@ -575,12 +575,12 @@ def test_groupby_categorical_key(): ddf = gddf.to_dask_dataframe() got = ( - gddf.groupby("name") + gddf.groupby("name", sort=True) .agg({"x": ["mean", "max"], "y": ["mean", "count"]}) .compute() ) expect = ( - ddf.groupby("name") + ddf.groupby("name", sort=True) .agg({"x": ["mean", "max"], "y": ["mean", "count"]}) .compute() ) From 732207054688f8e4356ec33f7e0e7481bcada040 Mon Sep 17 00:00:00 2001 From: Shaswat Anand <33908100+shaswat-indian@users.noreply.github.com> Date: Fri, 19 Aug 2022 15:13:12 -0700 Subject: [PATCH 51/58] Fix for: error when assigning a value to an empty series (#11523) Resolves: https://github.com/rapidsai/cudf/issues/10116 Authors: - Shaswat Anand (https://github.com/shaswat-indian) Approvers: - Ashwin Srinath (https://github.com/shwina) URL: https://github.com/rapidsai/cudf/pull/11523 --- python/cudf/cudf/core/dataframe.py | 2 +- python/cudf/cudf/tests/test_dataframe.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index dd4f5e33e5f..fc8dd20f101 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -1225,7 +1225,7 @@ def __setitem__(self, arg, value): ) else: if arg in self._data: - if len(self) == 0: + if not is_scalar(value) and len(self) == 0: if isinstance(value, (pd.Series, Series)): self._index = as_index(value.index) elif len(value) > 0: diff --git a/python/cudf/cudf/tests/test_dataframe.py b/python/cudf/cudf/tests/test_dataframe.py index 1a7acd01ce9..76d7eff6d31 100644 --- a/python/cudf/cudf/tests/test_dataframe.py +++ b/python/cudf/cudf/tests/test_dataframe.py @@ -9506,3 +9506,11 @@ def test_multiindex_wildcard_selection_three_level_all(): expect = df.to_pandas().loc[:, (slice("a", "c"), slice("a", "b"), "b")] got = df.loc[:, (slice(None), "b")] assert_eq(expect, got) + + +def test_dataframe_assign_scalar_to_empty_series(): + expected = pd.DataFrame({"a": []}) + actual = cudf.DataFrame({"a": []}) + expected.a = 0 + actual.a = 0 + assert_eq(expected, actual) From be783516a0689100a5f93c9839601d8276eebd61 Mon Sep 17 00:00:00 2001 From: Shaswat Anand <33908100+shaswat-indian@users.noreply.github.com> Date: Mon, 22 Aug 2022 05:05:31 -0700 Subject: [PATCH 52/58] Fix for pivot: error when 'values' is a multicharacter string (#11538) Resolves: https://github.com/rapidsai/cudf/issues/10529 Authors: - Shaswat Anand (https://github.com/shaswat-indian) Approvers: - Bradley Dice (https://github.com/bdice) - Ashwin Srinath (https://github.com/shwina) URL: https://github.com/rapidsai/cudf/pull/11538 --- python/cudf/cudf/core/column_accessor.py | 1 + python/cudf/cudf/core/reshape.py | 12 ++++++++++- python/cudf/cudf/tests/test_reshape.py | 27 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/python/cudf/cudf/core/column_accessor.py b/python/cudf/cudf/core/column_accessor.py index c4de910ef4c..525dfde5d33 100644 --- a/python/cudf/cudf/core/column_accessor.py +++ b/python/cudf/cudf/core/column_accessor.py @@ -616,6 +616,7 @@ def droplevel(self, level): len(self._level_names) == 1 ): # can't use nlevels, as it depends on multiindex self.multiindex = False + self._clear_cache() def _compare_keys(target: Any, key: Any) -> bool: diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 98eaabcab70..8e5d0ece729 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -951,11 +951,15 @@ def pivot(data, index=None, columns=None, values=None): """ df = data + values_is_list = True if values is None: values = df._columns_view( col for col in df._column_names if col not in (index, columns) ) else: + if not isinstance(values, (list, tuple)): + values = [values] + values_is_list = False values = df._columns_view(values) if index is None: index = df.index @@ -978,7 +982,13 @@ def pivot(data, index=None, columns=None, values=None): if len(columns_index) != len(columns_index.drop_duplicates()): raise ValueError("Duplicate index-column pairs found. Cannot reshape.") - return _pivot(values, index, columns) + result = _pivot(values, index, columns) + + # MultiIndex to Index + if not values_is_list: + result._data.droplevel(0) + + return result def unstack(df, level, fill_value=None): diff --git a/python/cudf/cudf/tests/test_reshape.py b/python/cudf/cudf/tests/test_reshape.py index 118cb4cf53c..df03104eda4 100644 --- a/python/cudf/cudf/tests/test_reshape.py +++ b/python/cudf/cudf/tests/test_reshape.py @@ -408,6 +408,33 @@ def test_pivot_multi_values(): ) +@pytest.mark.parametrize( + "values", ["z", "z123", ["z123"], ["z", "z123", "123z"]] +) +def test_pivot_values(values): + data = [ + ["A", "a", 0, 0, 0], + ["A", "b", 1, 1, 1], + ["A", "c", 2, 2, 2], + ["B", "a", 0, 0, 0], + ["B", "b", 1, 1, 1], + ["B", "c", 2, 2, 2], + ["C", "a", 0, 0, 0], + ["C", "b", 1, 1, 1], + ["C", "c", 2, 2, 2], + ] + columns = ["x", "y", "z", "z123", "123z"] + pdf = pd.DataFrame(data, columns=columns) + cdf = cudf.DataFrame(data, columns=columns) + expected = pd.pivot(pdf, index="x", columns="y", values=values) + actual = cudf.pivot(cdf, index="x", columns="y", values=values) + assert_eq( + expected, + actual, + check_dtype=False, + ) + + @pytest.mark.parametrize( "level", [ From dd1c27ace951788396fbfffba0d7bdffc239e49a Mon Sep 17 00:00:00 2001 From: Tobias Ribizel Date: Mon, 22 Aug 2022 22:59:07 +0200 Subject: [PATCH 53/58] Add byte_range to multibyte_split benchmark + NVBench refactor (#11562) This adds a second benchmark testing byte ranges with 1% - 50% of the input size to the multibyte_split benchmarks. Example output (--run-once):
``` # Benchmark Results ## multibyte_split ### [0] Tesla T4 | source_type | delim_size | delim_percent | size_approx | byte_range_percent | Samples | CPU Time | Noise | GPU Time | Noise | Peak Memory Usage | Encoded file size | |-------------|------------|---------------|-------------------|--------------------|---------|------------|-------|------------|-------|-------------------|-------------------| | 0 | 1 | 1 | 2^15 = 32768 | 1 | 1x | 683.062 us | inf% | 677.888 us | inf% | 86.672 KiB | 315.000 B | | 1 | 1 | 1 | 2^15 = 32768 | 1 | 1x | 11.870 ms | inf% | 11.864 ms | inf% | 117.055 KiB | 315.000 B | | 2 | 1 | 1 | 2^15 = 32768 | 1 | 1x | 8.232 ms | inf% | 8.226 ms | inf% | 112.094 KiB | 315.000 B | | 0 | 4 | 1 | 2^15 = 32768 | 1 | 1x | 647.178 us | inf% | 642.464 us | inf% | 81.938 KiB | 325.000 B | | 1 | 4 | 1 | 2^15 = 32768 | 1 | 1x | 8.410 ms | inf% | 8.405 ms | inf% | 113.719 KiB | 325.000 B | | 2 | 4 | 1 | 2^15 = 32768 | 1 | 1x | 8.194 ms | inf% | 8.188 ms | inf% | 113.086 KiB | 325.000 B | | 0 | 7 | 1 | 2^15 = 32768 | 1 | 1x | 644.414 us | inf% | 638.944 us | inf% | 82.352 KiB | 328.000 B | | 1 | 7 | 1 | 2^15 = 32768 | 1 | 1x | 9.467 ms | inf% | 9.461 ms | inf% | 113.703 KiB | 328.000 B | | 2 | 7 | 1 | 2^15 = 32768 | 1 | 1x | 8.199 ms | inf% | 8.192 ms | inf% | 113.344 KiB | 328.000 B | | 0 | 1 | 25 | 2^15 = 32768 | 1 | 1x | 661.234 us | inf% | 656.864 us | inf% | 146.719 KiB | 243.000 B | | 1 | 1 | 25 | 2^15 = 32768 | 1 | 1x | 9.465 ms | inf% | 9.458 ms | inf% | 169.945 KiB | 243.000 B | | 2 | 1 | 25 | 2^15 = 32768 | 1 | 1x | 8.310 ms | inf% | 8.304 ms | inf% | 105.070 KiB | 243.000 B | | 0 | 4 | 25 | 2^15 = 32768 | 1 | 1x | 722.079 us | inf% | 717.024 us | inf% | 97.547 KiB | 304.000 B | | 1 | 4 | 25 | 2^15 = 32768 | 1 | 1x | 9.565 ms | inf% | 9.558 ms | inf% | 126.891 KiB | 304.000 B | | 2 | 4 | 25 | 2^15 = 32768 | 1 | 1x | 8.242 ms | inf% | 8.237 ms | inf% | 111.031 KiB | 304.000 B | | 0 | 7 | 25 | 2^15 = 32768 | 1 | 1x | 735.674 us | inf% | 729.984 us | inf% | 90.633 KiB | 309.000 B | | 1 | 7 | 25 | 2^15 = 32768 | 1 | 1x | 9.623 ms | inf% | 9.617 ms | inf% | 120.508 KiB | 309.000 B | | 2 | 7 | 25 | 2^15 = 32768 | 1 | 1x | 8.448 ms | inf% | 8.443 ms | inf% | 111.547 KiB | 309.000 B | | 0 | 1 | 1 | 2^30 = 1073741824 | 1 | 1x | 457.785 ms | inf% | 457.785 ms | inf% | 161.148 MiB | 10.066 MiB | | 1 | 1 | 1 | 2^30 = 1073741824 | 1 | 1x | 1.283 s | inf% | 1.283 s | inf% | 165.148 MiB | 10.066 MiB | | 2 | 1 | 1 | 2^30 = 1073741824 | 1 | 1x | 3.676 s | inf% | 3.676 s | inf% | 4.079 MiB | 10.066 MiB | | 0 | 4 | 1 | 2^30 = 1073741824 | 1 | 1x | 466.860 ms | inf% | 466.861 ms | inf% | 30.663 MiB | 10.144 MiB | | 1 | 4 | 1 | 2^30 = 1073741824 | 1 | 1x | 1.251 s | inf% | 1.251 s | inf% | 34.663 MiB | 10.144 MiB | | 2 | 4 | 1 | 2^30 = 1073741824 | 1 | 1x | 3.517 s | inf% | 3.517 s | inf% | 4.079 MiB | 10.144 MiB | | 0 | 7 | 1 | 2^30 = 1073741824 | 1 | 1x | 479.563 ms | inf% | 479.564 ms | inf% | 21.915 MiB | 10.156 MiB | | 1 | 7 | 1 | 2^30 = 1073741824 | 1 | 1x | 1.207 s | inf% | 1.207 s | inf% | 25.915 MiB | 10.156 MiB | | 2 | 7 | 1 | 2^30 = 1073741824 | 1 | 1x | 3.218 s | inf% | 3.218 s | inf% | 4.079 MiB | 10.156 MiB | | 0 | 1 | 25 | 2^30 = 1073741824 | 1 | 1x | 357.260 ms | inf% | 357.258 ms | inf% | 2.043 GiB | 7.625 MiB | | 1 | 1 | 25 | 2^30 = 1073741824 | 1 | 1x | 948.294 ms | inf% | 948.298 ms | inf% | 2.047 GiB | 7.625 MiB | | 2 | 1 | 25 | 2^30 = 1073741824 | 1 | 1x | 2.471 s | inf% | 2.471 s | inf% | 4.079 MiB | 7.625 MiB | | 0 | 4 | 25 | 2^30 = 1073741824 | 1 | 1x | 477.226 ms | inf% | 477.227 ms | inf% | 520.458 MiB | 9.531 MiB | | 1 | 4 | 25 | 2^30 = 1073741824 | 1 | 1x | 1.180 s | inf% | 1.180 s | inf% | 524.458 MiB | 9.531 MiB | | 2 | 4 | 25 | 2^30 = 1073741824 | 1 | 1x | 3.095 s | inf% | 3.095 s | inf% | 4.079 MiB | 9.531 MiB | | 0 | 7 | 25 | 2^30 = 1073741824 | 1 | 1x | 481.854 ms | inf% | 481.854 ms | inf% | 301.800 MiB | 9.803 MiB | | 1 | 7 | 25 | 2^30 = 1073741824 | 1 | 1x | 1.234 s | inf% | 1.234 s | inf% | 305.800 MiB | 9.803 MiB | | 2 | 7 | 25 | 2^30 = 1073741824 | 1 | 1x | 3.100 s | inf% | 3.100 s | inf% | 4.079 MiB | 9.803 MiB | | 0 | 1 | 1 | 2^15 = 32768 | 5 | 1x | 580.967 us | inf% | 577.184 us | inf% | 87.977 KiB | 1.540 KiB | | 1 | 1 | 1 | 2^15 = 32768 | 5 | 1x | 12.855 ms | inf% | 12.851 ms | inf% | 117.055 KiB | 1.540 KiB | | 2 | 1 | 1 | 2^15 = 32768 | 5 | 1x | 7.830 ms | inf% | 7.826 ms | inf% | 112.094 KiB | 1.540 KiB | | 0 | 4 | 1 | 2^15 = 32768 | 5 | 1x | 434.534 us | inf% | 430.720 us | inf% | 83.531 KiB | 1.589 KiB | | 1 | 4 | 1 | 2^15 = 32768 | 5 | 1x | 8.882 ms | inf% | 8.877 ms | inf% | 113.719 KiB | 1.589 KiB | | 2 | 4 | 1 | 2^15 = 32768 | 5 | 1x | 7.756 ms | inf% | 7.751 ms | inf% | 113.086 KiB | 1.589 KiB | | 0 | 7 | 1 | 2^15 = 32768 | 5 | 1x | 395.562 us | inf% | 391.616 us | inf% | 83.742 KiB | 1.602 KiB | | 1 | 7 | 1 | 2^15 = 32768 | 5 | 1x | 8.917 ms | inf% | 8.911 ms | inf% | 113.703 KiB | 1.602 KiB | | 2 | 7 | 1 | 2^15 = 32768 | 5 | 1x | 7.763 ms | inf% | 7.758 ms | inf% | 113.344 KiB | 1.602 KiB | | 0 | 1 | 25 | 2^15 = 32768 | 5 | 1x | 445.522 us | inf% | 441.760 us | inf% | 149.008 KiB | 1.188 KiB | | 1 | 1 | 25 | 2^15 = 32768 | 5 | 1x | 8.994 ms | inf% | 8.989 ms | inf% | 169.945 KiB | 1.188 KiB | | 2 | 1 | 25 | 2^15 = 32768 | 5 | 1x | 7.975 ms | inf% | 7.971 ms | inf% | 105.070 KiB | 1.188 KiB | | 0 | 4 | 25 | 2^15 = 32768 | 5 | 1x | 425.132 us | inf% | 421.248 us | inf% | 99.031 KiB | 1.486 KiB | | 1 | 4 | 25 | 2^15 = 32768 | 5 | 1x | 8.846 ms | inf% | 8.841 ms | inf% | 126.891 KiB | 1.486 KiB | | 2 | 4 | 25 | 2^15 = 32768 | 5 | 1x | 7.740 ms | inf% | 7.735 ms | inf% | 111.031 KiB | 1.486 KiB | | 0 | 7 | 25 | 2^15 = 32768 | 5 | 1x | 502.938 us | inf% | 499.392 us | inf% | 92.023 KiB | 1.512 KiB | | 1 | 7 | 25 | 2^15 = 32768 | 5 | 1x | 8.887 ms | inf% | 8.882 ms | inf% | 120.508 KiB | 1.512 KiB | | 2 | 7 | 25 | 2^15 = 32768 | 5 | 1x | 7.789 ms | inf% | 7.784 ms | inf% | 111.547 KiB | 1.512 KiB | | 0 | 1 | 1 | 2^30 = 1073741824 | 5 | 1x | 454.848 ms | inf% | 454.849 ms | inf% | 204.424 MiB | 50.332 MiB | | 1 | 1 | 1 | 2^30 = 1073741824 | 5 | 1x | 1.203 s | inf% | 1.203 s | inf% | 208.424 MiB | 50.332 MiB | | 2 | 1 | 1 | 2^30 = 1073741824 | 5 | 1x | 3.307 s | inf% | 3.307 s | inf% | 4.079 MiB | 50.332 MiB | | 0 | 4 | 1 | 2^30 = 1073741824 | 5 | 1x | 476.083 ms | inf% | 476.083 ms | inf% | 71.647 MiB | 50.722 MiB | | 1 | 4 | 1 | 2^30 = 1073741824 | 5 | 1x | 1.267 s | inf% | 1.267 s | inf% | 75.647 MiB | 50.722 MiB | | 2 | 4 | 1 | 2^30 = 1073741824 | 5 | 1x | 3.208 s | inf% | 3.208 s | inf% | 4.079 MiB | 50.722 MiB | | 0 | 7 | 1 | 2^30 = 1073741824 | 5 | 1x | 473.810 ms | inf% | 473.810 ms | inf% | 62.772 MiB | 50.780 MiB | | 1 | 7 | 1 | 2^30 = 1073741824 | 5 | 1x | 1.202 s | inf% | 1.202 s | inf% | 66.772 MiB | 50.780 MiB | | 2 | 7 | 1 | 2^30 = 1073741824 | 5 | 1x | 3.216 s | inf% | 3.216 s | inf% | 4.079 MiB | 50.780 MiB | | 0 | 1 | 25 | 2^30 = 1073741824 | 5 | 1x | 428.377 ms | inf% | 428.376 ms | inf% | 2.113 GiB | 38.123 MiB | | 1 | 1 | 25 | 2^30 = 1073741824 | 5 | 1x | 986.342 ms | inf% | 986.346 ms | inf% | 2.117 GiB | 38.123 MiB | | 2 | 1 | 25 | 2^30 = 1073741824 | 5 | 1x | 2.498 s | inf% | 2.498 s | inf% | 4.079 MiB | 38.123 MiB | | 0 | 4 | 25 | 2^30 = 1073741824 | 5 | 1x | 446.426 ms | inf% | 446.427 ms | inf% | 568.747 MiB | 47.654 MiB | | 1 | 4 | 25 | 2^30 = 1073741824 | 5 | 1x | 1.167 s | inf% | 1.167 s | inf% | 572.747 MiB | 47.654 MiB | | 2 | 4 | 25 | 2^30 = 1073741824 | 5 | 1x | 2.998 s | inf% | 2.998 s | inf% | 4.079 MiB | 47.654 MiB | | 0 | 7 | 25 | 2^30 = 1073741824 | 5 | 1x | 451.670 ms | inf% | 451.670 ms | inf% | 346.822 MiB | 49.016 MiB | | 1 | 7 | 25 | 2^30 = 1073741824 | 5 | 1x | 1.184 s | inf% | 1.184 s | inf% | 350.822 MiB | 49.016 MiB | | 2 | 7 | 25 | 2^30 = 1073741824 | 5 | 1x | 3.174 s | inf% | 3.174 s | inf% | 4.079 MiB | 49.016 MiB | | 0 | 1 | 1 | 2^15 = 32768 | 25 | 1x | 501.600 us | inf% | 497.728 us | inf% | 94.703 KiB | 7.702 KiB | | 1 | 1 | 1 | 2^15 = 32768 | 25 | 1x | 12.835 ms | inf% | 12.831 ms | inf% | 117.055 KiB | 7.702 KiB | | 2 | 1 | 1 | 2^15 = 32768 | 25 | 1x | 7.827 ms | inf% | 7.822 ms | inf% | 112.094 KiB | 7.702 KiB | | 0 | 4 | 1 | 2^15 = 32768 | 25 | 1x | 400.909 us | inf% | 396.960 us | inf% | 89.906 KiB | 7.947 KiB | | 1 | 4 | 1 | 2^15 = 32768 | 25 | 1x | 8.860 ms | inf% | 8.855 ms | inf% | 113.719 KiB | 7.947 KiB | | 2 | 4 | 1 | 2^15 = 32768 | 25 | 1x | 7.785 ms | inf% | 7.780 ms | inf% | 113.086 KiB | 7.947 KiB | | 0 | 7 | 1 | 2^15 = 32768 | 25 | 1x | 394.719 us | inf% | 390.816 us | inf% | 89.344 KiB | 8.010 KiB | | 1 | 7 | 1 | 2^15 = 32768 | 25 | 1x | 8.853 ms | inf% | 8.848 ms | inf% | 113.703 KiB | 8.010 KiB | | 2 | 7 | 1 | 2^15 = 32768 | 25 | 1x | 7.728 ms | inf% | 7.723 ms | inf% | 113.344 KiB | 8.010 KiB | | 0 | 1 | 25 | 2^15 = 32768 | 25 | 1x | 411.273 us | inf% | 407.424 us | inf% | 160.180 KiB | 5.945 KiB | | 1 | 1 | 25 | 2^15 = 32768 | 25 | 1x | 8.919 ms | inf% | 8.913 ms | inf% | 169.945 KiB | 5.945 KiB | | 2 | 1 | 25 | 2^15 = 32768 | 25 | 1x | 7.732 ms | inf% | 7.728 ms | inf% | 105.070 KiB | 5.945 KiB | | 0 | 4 | 25 | 2^15 = 32768 | 25 | 1x | 413.954 us | inf% | 409.376 us | inf% | 106.562 KiB | 7.434 KiB | | 1 | 4 | 25 | 2^15 = 32768 | 25 | 1x | 8.895 ms | inf% | 8.889 ms | inf% | 126.891 KiB | 7.434 KiB | | 2 | 4 | 25 | 2^15 = 32768 | 25 | 1x | 7.756 ms | inf% | 7.751 ms | inf% | 111.031 KiB | 7.434 KiB | | 0 | 7 | 25 | 2^15 = 32768 | 25 | 1x | 424.539 us | inf% | 420.160 us | inf% | 98.930 KiB | 7.561 KiB | | 1 | 7 | 25 | 2^15 = 32768 | 25 | 1x | 8.947 ms | inf% | 8.943 ms | inf% | 120.508 KiB | 7.561 KiB | | 2 | 7 | 25 | 2^15 = 32768 | 25 | 1x | 7.770 ms | inf% | 7.766 ms | inf% | 111.547 KiB | 7.561 KiB | | 0 | 1 | 1 | 2^30 = 1073741824 | 25 | 1x | 454.401 ms | inf% | 454.400 ms | inf% | 420.754 MiB | 251.660 MiB | | 1 | 1 | 1 | 2^30 = 1073741824 | 25 | 1x | 1.216 s | inf% | 1.216 s | inf% | 424.754 MiB | 251.660 MiB | | 2 | 1 | 1 | 2^30 = 1073741824 | 25 | 1x | 3.169 s | inf% | 3.169 s | inf% | 4.079 MiB | 251.660 MiB | | 0 | 4 | 1 | 2^30 = 1073741824 | 25 | 1x | 473.311 ms | inf% | 473.311 ms | inf% | 276.569 MiB | 253.610 MiB | | 1 | 4 | 1 | 2^30 = 1073741824 | 25 | 1x | 1.265 s | inf% | 1.265 s | inf% | 280.569 MiB | 253.610 MiB | | 2 | 4 | 1 | 2^30 = 1073741824 | 25 | 1x | 3.215 s | inf% | 3.215 s | inf% | 4.079 MiB | 253.610 MiB | | 0 | 7 | 1 | 2^30 = 1073741824 | 25 | 1x | 460.715 ms | inf% | 460.715 ms | inf% | 267.056 MiB | 253.902 MiB | | 1 | 7 | 1 | 2^30 = 1073741824 | 25 | 1x | 1.294 s | inf% | 1.294 s | inf% | 271.056 MiB | 253.902 MiB | | 2 | 7 | 1 | 2^30 = 1073741824 | 25 | 1x | 3.179 s | inf% | 3.179 s | inf% | 4.079 MiB | 253.902 MiB | | 0 | 1 | 25 | 2^30 = 1073741824 | 25 | 1x | 433.565 ms | inf% | 433.564 ms | inf% | 2.465 GiB | 190.615 MiB | | 1 | 1 | 25 | 2^30 = 1073741824 | 25 | 1x | 1.012 s | inf% | 1.012 s | inf% | 2.469 GiB | 190.615 MiB | | 2 | 1 | 25 | 2^30 = 1073741824 | 25 | 1x | 2.436 s | inf% | 2.436 s | inf% | 4.079 MiB | 190.615 MiB | | 0 | 4 | 25 | 2^30 = 1073741824 | 25 | 1x | 473.925 ms | inf% | 473.927 ms | inf% | 810.193 MiB | 238.269 MiB | | 1 | 4 | 25 | 2^30 = 1073741824 | 25 | 1x | 1.222 s | inf% | 1.222 s | inf% | 814.193 MiB | 238.269 MiB | | 2 | 4 | 25 | 2^30 = 1073741824 | 25 | 1x | 2.954 s | inf% | 2.954 s | inf% | 4.079 MiB | 238.269 MiB | | 0 | 7 | 25 | 2^30 = 1073741824 | 25 | 1x | 475.940 ms | inf% | 475.940 ms | inf% | 571.933 MiB | 245.080 MiB | | 1 | 7 | 25 | 2^30 = 1073741824 | 25 | 1x | 1.216 s | inf% | 1.216 s | inf% | 575.933 MiB | 245.080 MiB | | 2 | 7 | 25 | 2^30 = 1073741824 | 25 | 1x | 3.035 s | inf% | 3.035 s | inf% | 4.079 MiB | 245.080 MiB | | 0 | 1 | 1 | 2^15 = 32768 | 50 | 1x | 453.082 us | inf% | 449.248 us | inf% | 103.055 KiB | 15.404 KiB | | 1 | 1 | 1 | 2^15 = 32768 | 50 | 1x | 12.784 ms | inf% | 12.778 ms | inf% | 118.523 KiB | 15.404 KiB | | 2 | 1 | 1 | 2^15 = 32768 | 50 | 1x | 8.021 ms | inf% | 8.017 ms | inf% | 112.094 KiB | 15.404 KiB | | 0 | 4 | 1 | 2^15 = 32768 | 50 | 1x | 450.433 us | inf% | 445.536 us | inf% | 97.781 KiB | 15.895 KiB | | 1 | 4 | 1 | 2^15 = 32768 | 50 | 1x | 8.740 ms | inf% | 8.735 ms | inf% | 113.719 KiB | 15.895 KiB | | 2 | 4 | 1 | 2^15 = 32768 | 50 | 1x | 7.715 ms | inf% | 7.711 ms | inf% | 113.086 KiB | 15.895 KiB | | 0 | 7 | 1 | 2^15 = 32768 | 50 | 1x | 400.055 us | inf% | 395.264 us | inf% | 97.750 KiB | 16.020 KiB | | 1 | 7 | 1 | 2^15 = 32768 | 50 | 1x | 8.755 ms | inf% | 8.751 ms | inf% | 113.750 KiB | 16.020 KiB | | 2 | 7 | 1 | 2^15 = 32768 | 50 | 1x | 7.687 ms | inf% | 7.682 ms | inf% | 113.344 KiB | 16.020 KiB | | 0 | 1 | 25 | 2^15 = 32768 | 50 | 1x | 410.891 us | inf% | 407.360 us | inf% | 174.227 KiB | 11.892 KiB | | 1 | 1 | 25 | 2^15 = 32768 | 50 | 1x | 8.795 ms | inf% | 8.790 ms | inf% | 186.125 KiB | 11.892 KiB | | 2 | 1 | 25 | 2^15 = 32768 | 50 | 1x | 7.757 ms | inf% | 7.753 ms | inf% | 105.070 KiB | 11.892 KiB | | 0 | 4 | 25 | 2^15 = 32768 | 50 | 1x | 414.914 us | inf% | 411.488 us | inf% | 115.992 KiB | 14.868 KiB | | 1 | 4 | 25 | 2^15 = 32768 | 50 | 1x | 8.790 ms | inf% | 8.784 ms | inf% | 130.867 KiB | 14.868 KiB | | 2 | 4 | 25 | 2^15 = 32768 | 50 | 1x | 7.702 ms | inf% | 7.698 ms | inf% | 111.031 KiB | 14.868 KiB | | 0 | 7 | 25 | 2^15 = 32768 | 50 | 1x | 426.890 us | inf% | 422.528 us | inf% | 107.648 KiB | 15.121 KiB | | 1 | 7 | 25 | 2^15 = 32768 | 50 | 1x | 8.816 ms | inf% | 8.811 ms | inf% | 122.789 KiB | 15.121 KiB | | 2 | 7 | 25 | 2^15 = 32768 | 50 | 1x | 7.710 ms | inf% | 7.706 ms | inf% | 111.547 KiB | 15.121 KiB | | 0 | 1 | 1 | 2^30 = 1073741824 | 50 | 1x | 465.599 ms | inf% | 465.599 ms | inf% | 691.190 MiB | 503.319 MiB | | 1 | 1 | 1 | 2^30 = 1073741824 | 50 | 1x | 1.362 s | inf% | 1.362 s | inf% | 695.190 MiB | 503.319 MiB | | 2 | 1 | 1 | 2^30 = 1073741824 | 50 | 1x | 3.214 s | inf% | 3.214 s | inf% | 4.079 MiB | 503.319 MiB | | 0 | 4 | 1 | 2^30 = 1073741824 | 50 | 1x | 494.805 ms | inf% | 494.807 ms | inf% | 532.722 MiB | 507.221 MiB | | 1 | 4 | 1 | 2^30 = 1073741824 | 50 | 1x | 1.282 s | inf% | 1.282 s | inf% | 536.722 MiB | 507.221 MiB | | 2 | 4 | 1 | 2^30 = 1073741824 | 50 | 1x | 3.178 s | inf% | 3.178 s | inf% | 4.079 MiB | 507.221 MiB | | 0 | 7 | 1 | 2^30 = 1073741824 | 50 | 1x | 478.472 ms | inf% | 478.473 ms | inf% | 522.410 MiB | 507.803 MiB | | 1 | 7 | 1 | 2^30 = 1073741824 | 50 | 1x | 1.287 s | inf% | 1.287 s | inf% | 526.410 MiB | 507.803 MiB | | 2 | 7 | 1 | 2^30 = 1073741824 | 50 | 1x | 3.229 s | inf% | 3.229 s | inf% | 4.079 MiB | 507.803 MiB | | 0 | 1 | 25 | 2^30 = 1073741824 | 50 | 1x | 451.871 ms | inf% | 451.872 ms | inf% | 2.904 GiB | 381.231 MiB | | 1 | 1 | 25 | 2^30 = 1073741824 | 50 | 1x | 1.029 s | inf% | 1.029 s | inf% | 2.908 GiB | 381.231 MiB | | 2 | 1 | 25 | 2^30 = 1073741824 | 50 | 1x | 2.379 s | inf% | 2.379 s | inf% | 4.079 MiB | 381.231 MiB | | 0 | 4 | 25 | 2^30 = 1073741824 | 50 | 1x | 456.983 ms | inf% | 456.983 ms | inf% | 1.086 GiB | 476.537 MiB | | 1 | 4 | 25 | 2^30 = 1073741824 | 50 | 1x | 1.245 s | inf% | 1.245 s | inf% | 1.090 GiB | 476.537 MiB | | 2 | 4 | 25 | 2^30 = 1073741824 | 50 | 1x | 2.932 s | inf% | 2.932 s | inf% | 4.079 MiB | 476.537 MiB | | 0 | 7 | 25 | 2^30 = 1073741824 | 50 | 1x | 505.587 ms | inf% | 505.586 ms | inf% | 853.321 MiB | 490.160 MiB | | 1 | 7 | 25 | 2^30 = 1073741824 | 50 | 1x | 1.264 s | inf% | 1.264 s | inf% | 857.321 MiB | 490.160 MiB | | 2 | 7 | 25 | 2^30 = 1073741824 | 50 | 1x | 2.991 s | inf% | 2.991 s | inf% | 4.079 MiB | 490.160 MiB | | 0 | 1 | 1 | 2^15 = 32768 | 100 | 1x | 452.555 us | inf% | 448.512 us | inf% | 119.547 KiB | 30.809 KiB | | 1 | 1 | 1 | 2^15 = 32768 | 100 | 1x | 12.755 ms | inf% | 12.750 ms | inf% | 150.359 KiB | 30.809 KiB | | 2 | 1 | 1 | 2^15 = 32768 | 100 | 1x | 8.670 ms | inf% | 8.666 ms | inf% | 142.914 KiB | 30.809 KiB | | 0 | 4 | 1 | 2^15 = 32768 | 100 | 1x | 402.123 us | inf% | 398.688 us | inf% | 114.047 KiB | 31.790 KiB | | 1 | 4 | 1 | 2^15 = 32768 | 100 | 1x | 8.997 ms | inf% | 8.993 ms | inf% | 145.844 KiB | 31.790 KiB | | 2 | 4 | 1 | 2^15 = 32768 | 100 | 1x | 8.703 ms | inf% | 8.698 ms | inf% | 144.891 KiB | 31.790 KiB | | 0 | 7 | 1 | 2^15 = 32768 | 100 | 1x | 396.810 us | inf% | 393.184 us | inf% | 113.891 KiB | 32.040 KiB | | 1 | 7 | 1 | 2^15 = 32768 | 100 | 1x | 8.775 ms | inf% | 8.770 ms | inf% | 145.938 KiB | 32.040 KiB | | 2 | 7 | 1 | 2^15 = 32768 | 100 | 1x | 8.561 ms | inf% | 8.555 ms | inf% | 145.398 KiB | 32.040 KiB | | 0 | 1 | 25 | 2^15 = 32768 | 100 | 1x | 410.632 us | inf% | 407.136 us | inf% | 202.391 KiB | 23.783 KiB | | 1 | 1 | 25 | 2^15 = 32768 | 100 | 1x | 8.663 ms | inf% | 8.658 ms | inf% | 226.180 KiB | 23.783 KiB | | 2 | 1 | 25 | 2^15 = 32768 | 100 | 1x | 8.569 ms | inf% | 8.564 ms | inf% | 128.867 KiB | 23.783 KiB | | 0 | 4 | 25 | 2^15 = 32768 | 100 | 1x | 420.616 us | inf% | 416.288 us | inf% | 134.828 KiB | 29.736 KiB | | 1 | 4 | 25 | 2^15 = 32768 | 100 | 1x | 8.701 ms | inf% | 8.697 ms | inf% | 164.570 KiB | 29.736 KiB | | 2 | 4 | 25 | 2^15 = 32768 | 100 | 1x | 8.700 ms | inf% | 8.696 ms | inf% | 140.781 KiB | 29.736 KiB | | 0 | 7 | 25 | 2^15 = 32768 | 100 | 1x | 420.545 us | inf% | 417.184 us | inf% | 125.000 KiB | 30.243 KiB | | 1 | 7 | 25 | 2^15 = 32768 | 100 | 1x | 8.689 ms | inf% | 8.684 ms | inf% | 155.250 KiB | 30.243 KiB | | 2 | 7 | 25 | 2^15 = 32768 | 100 | 1x | 8.651 ms | inf% | 8.646 ms | inf% | 141.805 KiB | 30.243 KiB | | 0 | 1 | 1 | 2^30 = 1073741824 | 100 | 1x | 473.237 ms | inf% | 473.238 ms | inf% | 1.203 GiB | 1006.638 MiB | | 1 | 1 | 1 | 2^30 = 1073741824 | 100 | 1x | 1.388 s | inf% | 1.388 s | inf% | 1.207 GiB | 1006.638 MiB | | 2 | 1 | 1 | 2^30 = 1073741824 | 100 | 1x | 3.482 s | inf% | 3.483 s | inf% | 1010.718 MiB | 1006.638 MiB | | 0 | 4 | 1 | 2^30 = 1073741824 | 100 | 1x | 475.645 ms | inf% | 475.645 ms | inf% | 1.021 GiB | 1014.442 MiB | | 1 | 4 | 1 | 2^30 = 1073741824 | 100 | 1x | 1.384 s | inf% | 1.384 s | inf% | 1.024 GiB | 1014.442 MiB | | 2 | 4 | 1 | 2^30 = 1073741824 | 100 | 1x | 3.432 s | inf% | 3.432 s | inf% | 1018.521 MiB | 1014.442 MiB | | 0 | 7 | 1 | 2^30 = 1073741824 | 100 | 1x | 488.596 ms | inf% | 488.597 ms | inf% | 1.009 GiB | 1015.606 MiB | | 1 | 7 | 1 | 2^30 = 1073741824 | 100 | 1x | 1.404 s | inf% | 1.404 s | inf% | 1.013 GiB | 1015.606 MiB | | 2 | 7 | 1 | 2^30 = 1073741824 | 100 | 1x | 3.407 s | inf% | 3.407 s | inf% | 1019.686 MiB | 1015.606 MiB | | 0 | 1 | 25 | 2^30 = 1073741824 | 100 | 1x | 444.580 ms | inf% | 444.580 ms | inf% | 3.783 GiB | 762.462 MiB | | 1 | 1 | 25 | 2^30 = 1073741824 | 100 | 1x | 1.086 s | inf% | 1.086 s | inf% | 3.787 GiB | 762.462 MiB | | 2 | 1 | 25 | 2^30 = 1073741824 | 100 | 1x | 2.761 s | inf% | 2.761 s | inf% | 766.541 MiB | 762.462 MiB | | 0 | 4 | 25 | 2^30 = 1073741824 | 100 | 1x | 517.450 ms | inf% | 517.450 ms | inf% | 1.675 GiB | 953.075 MiB | | 1 | 4 | 25 | 2^30 = 1073741824 | 100 | 1x | 1.627 s | inf% | 1.627 s | inf% | 1.679 GiB | 953.075 MiB | | 2 | 4 | 25 | 2^30 = 1073741824 | 100 | 1x | 4.586 s | inf% | 4.586 s | inf% | 957.154 MiB | 953.075 MiB | | 0 | 7 | 25 | 2^30 = 1073741824 | 100 | 1x | 527.723 ms | inf% | 527.723 ms | inf% | 1.383 GiB | 980.321 MiB | | 1 | 7 | 25 | 2^30 = 1073741824 | 100 | 1x | 1.584 s | inf% | 1.584 s | inf% | 1.387 GiB | 980.321 MiB | | 2 | 7 | 25 | 2^30 = 1073741824 | 100 | 1x | 4.653 s | inf% | 4.653 s | inf% | 984.400 MiB | 980.321 MiB | ```
Authors: - Tobias Ribizel (https://github.com/upsj) Approvers: - Christopher Harris (https://github.com/cwharris) - Yunsong Wang (https://github.com/PointKernel) - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/11562 --- cpp/benchmarks/CMakeLists.txt | 2 +- cpp/benchmarks/io/text/multibyte_split.cpp | 111 +++++++++++---------- 2 files changed, 59 insertions(+), 54 deletions(-) diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 4a4da2f28b9..5b6c118f7bb 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -297,7 +297,7 @@ ConfigureNVBench(FST_NVBENCH io/fst.cu) # ################################################################################################## # * io benchmark --------------------------------------------------------------------- -ConfigureBench(MULTIBYTE_SPLIT_BENCHMARK io/text/multibyte_split.cpp) +ConfigureNVBench(MULTIBYTE_SPLIT_BENCHMARK io/text/multibyte_split.cpp) add_custom_target( run_benchmarks diff --git a/cpp/benchmarks/io/text/multibyte_split.cpp b/cpp/benchmarks/io/text/multibyte_split.cpp index f6e69452456..3faa49de0be 100644 --- a/cpp/benchmarks/io/text/multibyte_split.cpp +++ b/cpp/benchmarks/io/text/multibyte_split.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -32,11 +33,13 @@ #include #include +#include + #include #include #include -temp_directory const temp_dir("cudf_gbench"); +temp_directory const temp_dir("cudf_nvbench"); enum data_chunk_source_type { device, @@ -77,22 +80,30 @@ static cudf::string_scalar create_random_input(int32_t num_chars, return cudf::string_scalar(std::move(*chars_buffer)); } -static void BM_multibyte_split(benchmark::State& state) +static void bench_multibyte_split(nvbench::state& state) { - auto source_type = static_cast(state.range(0)); - auto delim_size = state.range(1); - auto delim_percent = state.range(2); - auto file_size_approx = state.range(3); + cudf::rmm_pool_raii pool_raii; + + auto const source_type = static_cast(state.get_int64("source_type")); + auto const delim_size = state.get_int64("delim_size"); + auto const delim_percent = state.get_int64("delim_percent"); + auto const file_size_approx = state.get_int64("size_approx"); + auto const byte_range_percent = state.get_int64("byte_range_percent"); + auto const byte_range_factor = static_cast(byte_range_percent) / 100; CUDF_EXPECTS(delim_percent >= 1, "delimiter percent must be at least 1"); CUDF_EXPECTS(delim_percent <= 50, "delimiter percent must be at most 50"); + CUDF_EXPECTS(byte_range_percent >= 1, "byte range percent must be at least 1"); + CUDF_EXPECTS(byte_range_percent <= 100, "byte range percent must be at most 100"); - auto delim = std::string(":", delim_size); + auto delim = std::string(delim_size, '0'); + // the algorithm can only support 7 equal characters, so use different chars in the delimiter + std::iota(delim.begin(), delim.end(), '1'); - auto delim_factor = static_cast(delim_percent) / 100; - auto device_input = create_random_input(file_size_approx, delim_factor, 0.05, delim); - auto host_input = thrust::host_vector(device_input.size()); - auto host_string = std::string(host_input.data(), host_input.size()); + auto const delim_factor = static_cast(delim_percent) / 100; + auto device_input = create_random_input(file_size_approx, delim_factor, 0.05, delim); + auto host_input = thrust::host_vector(device_input.size()); + auto const host_string = std::string(host_input.data(), host_input.size()); cudaMemcpyAsync(host_input.data(), device_input.data(), @@ -100,7 +111,7 @@ static void BM_multibyte_split(benchmark::State& state) cudaMemcpyDeviceToHost, cudf::default_stream_value); - auto temp_file_name = random_file_in_dir(temp_dir.path()); + auto const temp_file_name = random_file_in_dir(temp_dir.path()); { auto temp_fostream = std::ofstream(temp_file_name, std::ofstream::out); @@ -109,48 +120,42 @@ static void BM_multibyte_split(benchmark::State& state) cudaDeviceSynchronize(); - auto source = std::unique_ptr(nullptr); - - switch (source_type) { - case data_chunk_source_type::file: // - source = cudf::io::text::make_source_from_file(temp_file_name); - break; - case data_chunk_source_type::host: // - source = cudf::io::text::make_source(host_string); - break; - case data_chunk_source_type::device: // - source = cudf::io::text::make_source(device_input); - break; - default: CUDF_FAIL(); - } - - auto mem_stats_logger = cudf::memory_stats_logger(); - for (auto _ : state) { + auto source = [&] { + switch (source_type) { + case data_chunk_source_type::file: // + return cudf::io::text::make_source_from_file(temp_file_name); + case data_chunk_source_type::host: // + return cudf::io::text::make_source(host_string); + case data_chunk_source_type::device: // + return cudf::io::text::make_source(device_input); + default: CUDF_FAIL(); + } + }(); + + auto mem_stats_logger = cudf::memory_stats_logger(); + auto const range_size = static_cast(device_input.size() * byte_range_factor); + auto const range_offset = (device_input.size() - range_size) / 2; + cudf::io::text::byte_range_info range{range_offset, range_size}; + std::unique_ptr output; + + state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::default_stream_value.value())); + state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { try_drop_l3_cache(); - cuda_event_timer raii(state, true); - auto output = cudf::io::text::multibyte_split(*source, delim); - } + output = cudf::io::text::multibyte_split(*source, delim, range); + }); - state.SetBytesProcessed(state.iterations() * device_input.size()); - state.counters["peak_memory_usage"] = mem_stats_logger.peak_memory_usage(); + state.add_buffer_size(mem_stats_logger.peak_memory_usage(), "pmu", "Peak Memory Usage"); + // TODO adapt to consistent naming scheme once established + state.add_buffer_size(range_size, "efs", "Encoded file size"); } -class MultibyteSplitBenchmark : public cudf::benchmark { -}; - -#define TRANSPOSE_BM_BENCHMARK_DEFINE(name) \ - BENCHMARK_DEFINE_F(MultibyteSplitBenchmark, name)(::benchmark::State & state) \ - { \ - BM_multibyte_split(state); \ - } \ - BENCHMARK_REGISTER_F(MultibyteSplitBenchmark, name) \ - ->ArgsProduct({{data_chunk_source_type::device, \ - data_chunk_source_type::file, \ - data_chunk_source_type::host}, \ - {1, 4, 7}, \ - {1, 25}, \ - {1 << 15, 1 << 30}}) \ - ->UseManualTime() \ - ->Unit(::benchmark::kMillisecond); - -TRANSPOSE_BM_BENCHMARK_DEFINE(multibyte_split_simple); +NVBENCH_BENCH(bench_multibyte_split) + .set_name("multibyte_split") + .add_int64_axis("source_type", + {data_chunk_source_type::device, + data_chunk_source_type::file, + data_chunk_source_type::host}) + .add_int64_axis("delim_size", {1, 4, 7}) + .add_int64_axis("delim_percent", {1, 25}) + .add_int64_power_of_two_axis("size_approx", {15, 30}) + .add_int64_axis("byte_range_percent", {1, 5, 25, 50, 100}); From 1d488098366eff2d885bce85f6de1c5e574aae7e Mon Sep 17 00:00:00 2001 From: Ed Seidl Date: Mon, 22 Aug 2022 14:54:31 -0700 Subject: [PATCH 54/58] Truncate parquet column indexes (#11403) Adds truncation of the min and max values of the Parquet column indexes, as recommended by the [Parquet format](https://github.com/apache/parquet-format/blob/master/PageIndex.md). Adds a parameter column_index_truncate_length to the writer options/builder. It currently defaults to 64, which is the default used by parquet-mr. Authors: - Ed Seidl (https://github.com/etseidl) Approvers: - Mike Wilson (https://github.com/hyperbolic2346) - Nghia Truong (https://github.com/ttnghia) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/11403 --- cpp/include/cudf/io/parquet.hpp | 85 +++++- cpp/include/cudf/strings/detail/utf8.hpp | 37 ++- cpp/src/io/parquet/page_enc.cu | 345 ++++++++++++++++++----- cpp/src/io/parquet/parquet_gpu.hpp | 2 + cpp/src/io/parquet/writer_impl.cu | 66 +++-- cpp/src/io/parquet/writer_impl.hpp | 23 +- cpp/tests/io/parquet_test.cpp | 151 ++++++++++ 7 files changed, 596 insertions(+), 113 deletions(-) diff --git a/cpp/include/cudf/io/parquet.hpp b/cpp/include/cudf/io/parquet.hpp index 8ed90f0ab31..5d95bf9812f 100644 --- a/cpp/include/cudf/io/parquet.hpp +++ b/cpp/include/cudf/io/parquet.hpp @@ -39,9 +39,10 @@ namespace io { */ constexpr size_t default_row_group_size_bytes = 128 * 1024 * 1024; ///< 128MB per row group -constexpr size_type default_row_group_size_rows = 1000000; ///< 1 million rows per row group -constexpr size_t default_max_page_size_bytes = 512 * 1024; ///< 512KB per page -constexpr size_type default_max_page_size_rows = 20000; ///< 20k rows per page +constexpr size_type default_row_group_size_rows = 1000000; ///< 1 million rows per row group +constexpr size_t default_max_page_size_bytes = 512 * 1024; ///< 512KB per page +constexpr size_type default_max_page_size_rows = 20000; ///< 20k rows per page +constexpr size_type default_column_index_truncate_length = 64; ///< truncate to 64 bytes class parquet_reader_options_builder; @@ -365,6 +366,8 @@ class parquet_writer_options { size_t _max_page_size_bytes = default_max_page_size_bytes; // Maximum number of rows in a page size_type _max_page_size_rows = default_max_page_size_rows; + // Maximum size of min or max values in column index + size_type _column_index_truncate_length = default_column_index_truncate_length; /** * @brief Constructor from sink and table. @@ -511,6 +514,13 @@ class parquet_writer_options { return std::min(_max_page_size_rows, get_row_group_size_rows()); } + /** + * @brief Returns maximum length of min or max values in column index, in bytes. + * + * @return length min/max will be truncated to + */ + auto get_column_index_truncate_length() const { return _column_index_truncate_length; } + /** * @brief Sets partitions. * @@ -627,6 +637,17 @@ class parquet_writer_options { "The maximum page size cannot be smaller than the fragment size, which is 5000 rows."); _max_page_size_rows = size_rows; } + + /** + * @brief Sets the maximum length of min or max values in column index, in bytes. + * + * @param size_bytes length min/max will be truncated to + */ + void set_column_index_truncate_length(size_type size_bytes) + { + CUDF_EXPECTS(size_bytes >= 0, "Column index truncate length cannot be negative."); + _column_index_truncate_length = size_bytes; + } }; /** @@ -788,6 +809,25 @@ class parquet_writer_options_builder { return *this; } + /** + * @brief Sets the desired maximum size in bytes for min and max values in the column index. + * + * Values exceeding this limit will be truncated, but modified such that they will still + * be valid lower and upper bounds. This only applies to variable length types, such as string. + * Maximum values will not be truncated if there is no suitable truncation that results in + * a valid upper bound. + * + * Default value is 64. + * + * @param val length min/max will be truncated to, with 0 indicating no truncation + * @return this for chaining + */ + parquet_writer_options_builder& column_index_truncate_length(size_type val) + { + options.set_column_index_truncate_length(val); + return *this; + } + /** * @brief Sets whether int96 timestamps are written or not in parquet_writer_options. * @@ -875,6 +915,8 @@ class chunked_parquet_writer_options { size_t _max_page_size_bytes = default_max_page_size_bytes; // Maximum number of rows in a page size_type _max_page_size_rows = default_max_page_size_rows; + // Maximum size of min or max values in column index + size_type _column_index_truncate_length = default_column_index_truncate_length; /** * @brief Constructor from sink. @@ -977,6 +1019,13 @@ class chunked_parquet_writer_options { return std::min(_max_page_size_rows, get_row_group_size_rows()); } + /** + * @brief Returns maximum length of min or max values in column index, in bytes. + * + * @return length min/max will be truncated to + */ + auto get_column_index_truncate_length() const { return _column_index_truncate_length; } + /** * @brief Sets metadata. * @@ -1069,6 +1118,17 @@ class chunked_parquet_writer_options { _max_page_size_rows = size_rows; } + /** + * @brief Sets the maximum length of min or max values in column index, in bytes. + * + * @param size_bytes length min/max will be truncated to + */ + void set_column_index_truncate_length(size_type size_bytes) + { + CUDF_EXPECTS(size_bytes >= 0, "Column index truncate length cannot be negative."); + _column_index_truncate_length = size_bytes; + } + /** * @brief creates builder to build chunked_parquet_writer_options. * @@ -1218,6 +1278,25 @@ class chunked_parquet_writer_options_builder { return *this; } + /** + * @brief Sets the desired maximum size in bytes for min and max values in the column index. + * + * Values exceeding this limit will be truncated, but modified such that they will still + * be valid lower and upper bounds. This only applies to variable length types, such as string. + * Maximum values will not be truncated if there is no suitable truncation that results in + * a valid upper bound. + * + * Default value is 64. + * + * @param val length min/max will be truncated to, with 0 indicating no truncation + * @return this for chaining + */ + chunked_parquet_writer_options_builder& column_index_truncate_length(size_type val) + { + options.set_column_index_truncate_length(val); + return *this; + } + /** * @brief move chunked_parquet_writer_options member once it's built. */ diff --git a/cpp/include/cudf/strings/detail/utf8.hpp b/cpp/include/cudf/strings/detail/utf8.hpp index c8d7ec4ae43..83b242a5271 100644 --- a/cpp/include/cudf/strings/detail/utf8.hpp +++ b/cpp/include/cudf/strings/detail/utf8.hpp @@ -29,16 +29,45 @@ using char_utf8 = uint32_t; ///< UTF-8 characters are 1-4 bytes namespace strings { namespace detail { +/** + * @brief This will return true if passed a continuation byte of a UTF-8 character. + * + * @param chr Any single byte from a valid UTF-8 character + * @return true if this is not the first byte of the character + */ +constexpr bool is_utf8_continuation_char(unsigned char chr) +{ + // The (0xC0 & 0x80) bit pattern identifies a continuation byte of a character. + return (chr & 0xC0) == 0x80; +} + /** * @brief This will return true if passed the first byte of a UTF-8 character. * - * @param byte Any byte from a valid UTF-8 character + * @param chr Any single byte from a valid UTF-8 character * @return true if this the first byte of the character */ -constexpr bool is_begin_utf8_char(uint8_t byte) +constexpr bool is_begin_utf8_char(unsigned char chr) { return not is_utf8_continuation_char(chr); } + +/** + * @brief This will return true if the passed in byte could be the start of + * a valid UTF-8 character. + * + * This differs from is_begin_utf8_char(uint8_t) in that byte may not be valid + * UTF-8, so a more rigorous check is performed. + * + * @param byte The byte to be tested + * @return true if this can be the first byte of a character + */ +constexpr bool is_valid_begin_utf8_char(uint8_t byte) { - // The (0xC0 & 0x80) bit pattern identifies a continuation byte of a character. - return (byte & 0xC0) != 0x80; + // to be the first byte of a valid (up to 4 byte) UTF-8 char, byte must be one of: + // 0b0vvvvvvv a 1 byte character + // 0b110vvvvv start of a 2 byte character + // 0b1110vvvv start of a 3 byte character + // 0b11110vvv start of a 4 byte character + return (byte & 0x80) == 0 || (byte & 0xE0) == 0xC0 || (byte & 0xF0) == 0xE0 || + (byte & 0xF8) == 0xF0; } /** diff --git a/cpp/src/io/parquet/page_enc.cu b/cpp/src/io/parquet/page_enc.cu index 7a48ca535c3..f06488671c3 100644 --- a/cpp/src/io/parquet/page_enc.cu +++ b/cpp/src/io/parquet/page_enc.cu @@ -55,6 +55,12 @@ using ::cudf::detail::device_2dspan; constexpr uint32_t rle_buffer_size = (1 << 9); +// do not truncate statistics +constexpr int32_t NO_TRUNC_STATS = 0; + +// minimum scratch space required for encoding statistics +constexpr size_t MIN_STATS_SCRATCH_SIZE = sizeof(__int128_t); + struct frag_init_state_s { parquet_column_device_view col; PageFragment frag; @@ -1417,9 +1423,11 @@ class header_encoder { inline __device__ void set_ptr(uint8_t* ptr) { current_header_ptr = ptr; } }; +namespace { + // byteswap 128 bit integer, placing result in dst in network byte order. // dst must point to at least 16 bytes of memory. -static __device__ void byte_reverse128(__int128_t v, void* dst) +__device__ void byte_reverse128(__int128_t v, void* dst) { auto const v_char_ptr = reinterpret_cast(&v); auto const d_char_ptr = static_cast(dst); @@ -1429,51 +1437,223 @@ static __device__ void byte_reverse128(__int128_t v, void* dst) d_char_ptr); } -__device__ void get_extremum(const statistics_val* stats_val, - statistics_dtype dtype, - void* scratch, - const void** val, - uint32_t* len) +/** + * @brief Test to see if a span contains all valid UTF-8 characters. + * + * @param span device_span to test. + * @return true if the span contains all valid UTF-8 characters. + */ +__device__ bool is_valid_utf8(device_span span) +{ + auto idx = 0; + while (idx < span.size_bytes()) { + // UTF-8 character should start with valid beginning bit pattern + if (not strings::detail::is_valid_begin_utf8_char(span[idx])) { return false; } + // subsequent elements of the character should be continuation chars + auto const width = strings::detail::bytes_in_utf8_byte(span[idx++]); + for (size_type i = 1; i < width && idx < span.size_bytes(); i++, idx++) { + if (not strings::detail::is_utf8_continuation_char(span[idx])) { return false; } + } + } + + return true; +} + +/** + * @brief Increment part of a UTF-8 character. + * + * Attempt to increment the char pointed to by ptr, which is assumed to be part of a valid UTF-8 + * character. Returns true if successful, false if the increment caused an overflow, in which case + * the data at ptr will be set to the lowest valid UTF-8 bit pattern (start or continuation). + * Will halt execution if passed invalid UTF-8. + */ +__device__ bool increment_utf8_at(unsigned char* ptr) +{ + unsigned char elem = *ptr; + // elem is one of (no 5 or 6 byte chars allowed): + // 0b0vvvvvvv a 1 byte character + // 0b10vvvvvv a continuation byte + // 0b110vvvvv start of a 2 byte characther + // 0b1110vvvv start of a 3 byte characther + // 0b11110vvv start of a 4 byte characther + + // TODO(ets): starting at 4 byte and working down. Should probably start low and work higher. + uint8_t mask = 0xF8; + uint8_t valid = 0xF0; + + while (mask != 0) { + if ((elem & mask) == valid) { + elem++; + if ((elem & mask) != mask) { // no overflow + *ptr = elem; + return true; + } + *ptr = valid; + return false; + } + mask <<= 1; + valid <<= 1; + } + + // should not reach here since we test for valid UTF-8 higher up the call chain + CUDF_UNREACHABLE("Trying to increment non-utf8"); +} + +/** + * @brief Attempt to truncate a span of UTF-8 characters to at most truncate_length_bytes. + * + * If is_min is false, then the final character (or characters if there is overflow) will be + * incremented so that the resultant UTF-8 will still be a valid maximum. scratch is only used when + * is_min is false, and must be at least truncate_length bytes in size. If the span cannot be + * truncated, leave it untouched and return the original length. + * + * @return Pair object containing a pointer to the truncated data and its length. + */ +__device__ std::pair truncate_utf8(device_span span, + bool is_min, + void* scratch, + size_type truncate_length) +{ + // we know at this point that truncate_length < size_bytes, so + // there is data at [len]. work backwards until we find + // the start of a UTF-8 encoded character, since UTF-8 characters may be multi-byte. + auto len = truncate_length; + while (not strings::detail::is_begin_utf8_char(span[len]) && len > 0) { + len--; + } + + if (len != 0) { + if (is_min) { return {span.data(), len}; } + memcpy(scratch, span.data(), len); + // increment last byte, working backwards if the byte overflows + auto const ptr = static_cast(scratch); + for (int32_t i = len - 1; i >= 0; i--) { + if (increment_utf8_at(&ptr[i])) { // true if no overflow + return {scratch, len}; + } + } + // cannot increment, so fall through + } + + // couldn't truncate, return original value + return {span.data(), span.size_bytes()}; +} + +/** + * @brief Attempt to truncate a span of binary data to at most truncate_length bytes. + * + * If is_min is false, then the final byte (or bytes if there is overflow) will be + * incremented so that the resultant binary will still be a valid maximum. scratch is only used when + * is_min is false, and must be at least truncate_length bytes in size. If the span cannot be + * truncated, leave it untouched and return the original length. + * + * @return Pair object containing a pointer to the truncated data and its length. + */ +__device__ std::pair truncate_binary(device_span arr, + bool is_min, + void* scratch, + size_type truncate_length) +{ + if (is_min) { return {arr.data(), truncate_length}; } + memcpy(scratch, arr.data(), truncate_length); + // increment last byte, working backwards if the byte overflows + auto const ptr = static_cast(scratch); + for (int32_t i = truncate_length - 1; i >= 0; i--) { + ptr[i]++; + if (ptr[i] != 0) { // no overflow + return {scratch, i + 1}; + } + } + + // couldn't truncate, return original value + return {arr.data(), arr.size_bytes()}; +} + +// TODO (ets): the assumption here is that string columns might have UTF-8 or plain binary, +// while binary columns are assumed to be binary and will be treated as such. If this assumption +// is incorrect, then truncate_byte_array() and truncate_string() should just be combined into +// a single function. +/** + * @brief Attempt to truncate a UTF-8 string to at most truncate_length bytes. + */ +__device__ std::pair truncate_string(const string_view& str, + bool is_min, + void* scratch, + size_type truncate_length) +{ + if (truncate_length == NO_TRUNC_STATS or str.size_bytes() <= truncate_length) { + return {str.data(), str.size_bytes()}; + } + + // convert char to unsigned since UTF-8 is just bytes, not chars. can't use std::byte because + // that can't be incremented. + auto const span = device_span( + reinterpret_cast(str.data()), str.size_bytes()); + + // if str is all 8-bit chars, or is actually not UTF-8, then we can just use truncate_binary() + if (str.size_bytes() != str.length() and is_valid_utf8(span.first(truncate_length))) { + return truncate_utf8(span, is_min, scratch, truncate_length); + } + return truncate_binary(span, is_min, scratch, truncate_length); +} + +/** + * @brief Attempt to truncate a binary array to at most truncate_length bytes. + */ +__device__ std::pair truncate_byte_array( + const statistics::byte_array_view& arr, bool is_min, void* scratch, size_type truncate_length) +{ + if (truncate_length == NO_TRUNC_STATS or arr.size_bytes() <= truncate_length) { + return {arr.data(), arr.size_bytes()}; + } + + // convert std::byte to uint8_t since bytes can't be incremented + device_span const span{reinterpret_cast(arr.data()), + arr.size_bytes()}; + return truncate_binary(span, is_min, scratch, truncate_length); +} + +/** + * @brief Find a min or max value of the proper form to be included in Parquet statistics + * structures. + * + * Given a statistics_val union and a data type, perform any transformations needed to produce a + * valid min or max binary value. String and byte array types will be truncated if they exceed + * truncate_length. + */ +__device__ std::pair get_extremum(const statistics_val* stats_val, + statistics_dtype dtype, + void* scratch, + bool is_min, + size_type truncate_length) { - uint8_t dtype_len; switch (dtype) { - case dtype_bool: dtype_len = 1; break; + case dtype_bool: return {stats_val, sizeof(bool)}; case dtype_int8: case dtype_int16: case dtype_int32: - case dtype_date32: - case dtype_float32: dtype_len = 4; break; + case dtype_date32: return {stats_val, sizeof(int32_t)}; + case dtype_float32: { + auto const fp_scratch = static_cast(scratch); + fp_scratch[0] = stats_val->fp_val; + return {scratch, sizeof(float)}; + } case dtype_int64: case dtype_timestamp64: case dtype_float64: - case dtype_decimal64: dtype_len = 8; break; - case dtype_decimal128: dtype_len = 16; break; - case dtype_string: - case dtype_byte_array: - default: dtype_len = 0; break; - } - - if (dtype == dtype_string) { - *len = stats_val->str_val.length; - *val = stats_val->str_val.ptr; - } else if (dtype == dtype_byte_array) { - *len = stats_val->byte_val.length; - *val = stats_val->byte_val.ptr; - } else { - *len = dtype_len; - if (dtype == dtype_float32) { // Convert from double to float32 - auto const fp_scratch = static_cast(scratch); - fp_scratch[0] = stats_val->fp_val; - *val = scratch; - } else if (dtype == dtype_decimal128) { + case dtype_decimal64: return {stats_val, sizeof(int64_t)}; + case dtype_decimal128: byte_reverse128(stats_val->d128_val, scratch); - *val = scratch; - } else { - *val = stats_val; - } + return {scratch, sizeof(__int128_t)}; + case dtype_string: return truncate_string(stats_val->str_val, is_min, scratch, truncate_length); + case dtype_byte_array: + return truncate_byte_array(stats_val->byte_val, is_min, scratch, truncate_length); + default: CUDF_UNREACHABLE("Invalid statistics data type"); } } +} // namespace + __device__ uint8_t* EncodeStatistics(uint8_t* start, const statistics_chunk* s, statistics_dtype dtype, @@ -1483,13 +1663,12 @@ __device__ uint8_t* EncodeStatistics(uint8_t* start, header_encoder encoder(start); encoder.field_int64(3, s->null_count); if (s->has_minmax) { - const void *vmin, *vmax; - uint32_t lmin, lmax; - - get_extremum(&s->max_value, dtype, scratch, &vmax, &lmax); - encoder.field_binary(5, vmax, lmax); - get_extremum(&s->min_value, dtype, scratch, &vmin, &lmin); - encoder.field_binary(6, vmin, lmin); + auto const [max_ptr, max_size] = + get_extremum(&s->max_value, dtype, scratch, false, NO_TRUNC_STATS); + encoder.field_binary(5, max_ptr, max_size); + auto const [min_ptr, min_size] = + get_extremum(&s->min_value, dtype, scratch, true, NO_TRUNC_STATS); + encoder.field_binary(6, min_ptr, min_size); } encoder.end(&end); return end; @@ -1506,7 +1685,7 @@ __global__ void __launch_bounds__(128) __shared__ __align__(8) parquet_column_device_view col_g; __shared__ __align__(8) EncColumnChunk ck_g; __shared__ __align__(8) EncPage page_g; - __shared__ __align__(8) unsigned char scratch[16]; + __shared__ __align__(8) unsigned char scratch[MIN_STATS_SCRATCH_SIZE]; uint32_t t = threadIdx.x; @@ -1630,11 +1809,13 @@ __global__ void __launch_bounds__(1024) } } +namespace { + /** * @brief Tests if statistics are comparable given the column's * physical and converted types */ -static __device__ bool is_comparable(Type ptype, ConvertedType ctype) +__device__ bool is_comparable(Type ptype, ConvertedType ctype) { switch (ptype) { case Type::BOOLEAN: @@ -1664,10 +1845,10 @@ constexpr __device__ int32_t compare(T& v1, T& v2) * @brief Compares two statistics_val structs. * @return < 0 if v1 < v2, 0 if v1 == v2, > 0 if v1 > v2 */ -static __device__ int32_t compare_values(Type ptype, - ConvertedType ctype, - const statistics_val& v1, - const statistics_val& v2) +__device__ int32_t compare_values(Type ptype, + ConvertedType ctype, + const statistics_val& v1, + const statistics_val& v2) { switch (ptype) { case Type::BOOLEAN: return compare(v1.u_val, v2.u_val); @@ -1695,10 +1876,10 @@ static __device__ int32_t compare_values(Type ptype, /** * @brief Determine if a set of statstistics are in ascending order. */ -static __device__ bool is_ascending(const statistics_chunk* s, - Type ptype, - ConvertedType ctype, - uint32_t num_pages) +__device__ bool is_ascending(const statistics_chunk* s, + Type ptype, + ConvertedType ctype, + uint32_t num_pages) { for (uint32_t i = 1; i < num_pages; i++) { if (compare_values(ptype, ctype, s[i - 1].min_value, s[i].min_value) > 0 || @@ -1712,10 +1893,10 @@ static __device__ bool is_ascending(const statistics_chunk* s, /** * @brief Determine if a set of statstistics are in descending order. */ -static __device__ bool is_descending(const statistics_chunk* s, - Type ptype, - ConvertedType ctype, - uint32_t num_pages) +__device__ bool is_descending(const statistics_chunk* s, + Type ptype, + ConvertedType ctype, + uint32_t num_pages) { for (uint32_t i = 1; i < num_pages; i++) { if (compare_values(ptype, ctype, s[i - 1].min_value, s[i].min_value) < 0 || @@ -1729,10 +1910,10 @@ static __device__ bool is_descending(const statistics_chunk* s, /** * @brief Determine the ordering of a set of statistics. */ -static __device__ int32_t calculate_boundary_order(const statistics_chunk* s, - Type ptype, - ConvertedType ctype, - uint32_t num_pages) +__device__ int32_t calculate_boundary_order(const statistics_chunk* s, + Type ptype, + ConvertedType ctype, + uint32_t num_pages) { if (not is_comparable(ptype, ctype)) { return BoundaryOrder::UNORDERED; } if (is_ascending(s, ptype, ctype, num_pages)) { @@ -1743,15 +1924,24 @@ static __device__ int32_t calculate_boundary_order(const statistics_chunk* s, return BoundaryOrder::UNORDERED; } +// align ptr to an 8-byte boundary. address returned will be <= ptr. +constexpr __device__ void* align8(void* ptr) +{ + // it's ok to round down because we have an extra 7 bytes in the buffer + auto algn = 3 & reinterpret_cast(ptr); + return static_cast(ptr) - algn; +} + +} // namespace + // blockDim(1, 1, 1) __global__ void __launch_bounds__(1) gpuEncodeColumnIndexes(device_span chunks, - device_span column_stats) + device_span column_stats, + size_type column_index_truncate_length) { - const void *vmin, *vmax; - uint32_t lmin, lmax; + __align__(8) unsigned char s_scratch[MIN_STATS_SCRATCH_SIZE]; uint8_t* col_idx_end; - unsigned char scratch[16]; if (column_stats.empty()) { return; } @@ -1763,6 +1953,14 @@ __global__ void __launch_bounds__(1) header_encoder encoder(ck_g->column_index_blob); + // make sure scratch is aligned properly. here column_index_size indicates + // how much scratch space is available for this chunk, including space for + // truncation scratch + padding for alignment. + void* scratch = + column_index_truncate_length < MIN_STATS_SCRATCH_SIZE + ? s_scratch + : align8(ck_g->column_index_blob + ck_g->column_index_size - column_index_truncate_length); + // null_pages encoder.field_list_begin(1, num_pages - first_data_page, ST_FLD_TRUE); for (uint32_t page = first_data_page; page < num_pages; page++) { @@ -1772,15 +1970,23 @@ __global__ void __launch_bounds__(1) // min_values encoder.field_list_begin(2, num_pages - first_data_page, ST_FLD_BINARY); for (uint32_t page = first_data_page; page < num_pages; page++) { - get_extremum(&column_stats[pageidx + page].min_value, col_g.stats_dtype, scratch, &vmin, &lmin); - encoder.put_binary(vmin, lmin); + auto const [min_ptr, min_size] = get_extremum(&column_stats[pageidx + page].min_value, + col_g.stats_dtype, + scratch, + true, + column_index_truncate_length); + encoder.put_binary(min_ptr, min_size); } encoder.field_list_end(2); // max_values encoder.field_list_begin(3, num_pages - first_data_page, ST_FLD_BINARY); for (uint32_t page = first_data_page; page < num_pages; page++) { - get_extremum(&column_stats[pageidx + page].max_value, col_g.stats_dtype, scratch, &vmax, &lmax); - encoder.put_binary(vmax, lmax); + auto const [max_ptr, max_size] = get_extremum(&column_stats[pageidx + page].max_value, + col_g.stats_dtype, + scratch, + false, + column_index_truncate_length); + encoder.put_binary(max_ptr, max_size); } encoder.field_list_end(3); // boundary_order @@ -1797,6 +2003,7 @@ __global__ void __launch_bounds__(1) encoder.field_list_end(5); encoder.end(&col_idx_end, false); + // now reset column_index_size to the actual size of the encoded column index blob ck_g->column_index_size = static_cast(col_idx_end - ck_g->column_index_blob); } @@ -1890,9 +2097,11 @@ void GatherPages(device_span chunks, void EncodeColumnIndexes(device_span chunks, device_span column_stats, + size_type column_index_truncate_length, rmm::cuda_stream_view stream) { - gpuEncodeColumnIndexes<<>>(chunks, column_stats); + gpuEncodeColumnIndexes<<>>( + chunks, column_stats, column_index_truncate_length); } } // namespace gpu diff --git a/cpp/src/io/parquet/parquet_gpu.hpp b/cpp/src/io/parquet/parquet_gpu.hpp index 81f802ff9ca..823cb8fcc2b 100644 --- a/cpp/src/io/parquet/parquet_gpu.hpp +++ b/cpp/src/io/parquet/parquet_gpu.hpp @@ -594,10 +594,12 @@ void GatherPages(device_span chunks, * * @param[in,out] chunks Column chunks * @param[in] column_stats Page-level statistics to be encoded + * @param[in] column_index_truncate_length Max length of min/max values * @param[in] stream CUDA stream to use */ void EncodeColumnIndexes(device_span chunks, device_span column_stats, + size_type column_index_truncate_length, rmm::cuda_stream_view stream); } // namespace gpu diff --git a/cpp/src/io/parquet/writer_impl.cu b/cpp/src/io/parquet/writer_impl.cu index 0510fb77ea8..dcbb3f07101 100644 --- a/cpp/src/io/parquet/writer_impl.cu +++ b/cpp/src/io/parquet/writer_impl.cu @@ -84,35 +84,6 @@ parquet::Compression to_parquet_compression(compression_type compression) } } -/** - * @brief Function to calculate the memory needed to encode the column index of the given - * column chunk - */ -size_t column_index_buffer_size(gpu::EncColumnChunk* ck) -{ - // encoding the column index for a given chunk requires: - // each list (4 of them) requires 6 bytes of overhead - // (1 byte field header, 1 byte type, 4 bytes length) - // 1 byte overhead for boundary_order - // 1 byte overhead for termination - // sizeof(char) for boundary_order - // sizeof(bool) * num_pages for null_pages - // (ck_max_stats_len + 4) * num_pages * 2 for min/max values - // (each binary requires 4 bytes length + ck_max_stats_len) - // sizeof(int64_t) * num_pages for null_counts - // - // so 26 bytes overhead + sizeof(char) + - // (sizeof(bool) + sizeof(int64_t) + 2 * (4 + ck_max_stats_len)) * num_pages - // - // we already have ck->ck_stat_size = 48 + 2 * ck_max_stats_len - // all of the overhead and non-stats data can fit in under 48 bytes - // - // so we can simply use ck_stat_size * num_pages - // - // calculating this per-chunk because the sizes can be wildly different - return ck->ck_stat_size * ck->num_pages; -} - } // namespace struct aggregate_writer_metadata { @@ -1263,7 +1234,8 @@ void writer::impl::encode_pages(hostdevice_2dvector& chunks if (column_stats != nullptr) { auto batch_column_stats = device_span(column_stats + first_page_in_batch, pages_in_batch); - EncodeColumnIndexes(d_chunks_in_batch.flat_view(), batch_column_stats, stream); + EncodeColumnIndexes( + d_chunks_in_batch.flat_view(), batch_column_stats, column_index_truncate_length, stream); } auto h_chunks_in_batch = chunks.host_view().subspan(first_rowgroup, rowgroups_in_batch); @@ -1275,6 +1247,35 @@ void writer::impl::encode_pages(hostdevice_2dvector& chunks stream.synchronize(); } +size_t writer::impl::column_index_buffer_size(gpu::EncColumnChunk* ck) const +{ + // encoding the column index for a given chunk requires: + // each list (4 of them) requires 6 bytes of overhead + // (1 byte field header, 1 byte type, 4 bytes length) + // 1 byte overhead for boundary_order + // 1 byte overhead for termination + // sizeof(char) for boundary_order + // sizeof(bool) * num_pages for null_pages + // (ck_max_stats_len + 4) * num_pages * 2 for min/max values + // (each binary requires 4 bytes length + ck_max_stats_len) + // sizeof(int64_t) * num_pages for null_counts + // + // so 26 bytes overhead + sizeof(char) + + // (sizeof(bool) + sizeof(int64_t) + 2 * (4 + ck_max_stats_len)) * num_pages + // + // we already have ck->ck_stat_size = 48 + 2 * ck_max_stats_len + // all of the overhead and non-stats data can fit in under 48 bytes + // + // so we can simply use ck_stat_size * num_pages + // + // add on some extra padding at the end (plus extra 7 bytes of alignment padding) + // for scratch space to do stats truncation. + // + // calculating this per-chunk because the sizes can be wildly different. + constexpr size_t padding = 7; + return ck->ck_stat_size * ck->num_pages + column_index_truncate_length + padding; +} + writer::impl::impl(std::vector> sinks, parquet_writer_options const& options, SingleWriteMode mode, @@ -1289,6 +1290,7 @@ writer::impl::impl(std::vector> sinks, compression_(to_parquet_compression(options.get_compression())), stats_granularity_(options.get_stats_level()), int96_timestamps(options.is_enabled_int96_timestamps()), + column_index_truncate_length(options.get_column_index_truncate_length()), kv_md(options.get_key_value_metadata()), single_write_mode(mode == SingleWriteMode::YES), out_sink_(std::move(sinks)) @@ -1313,6 +1315,7 @@ writer::impl::impl(std::vector> sinks, compression_(to_parquet_compression(options.get_compression())), stats_granularity_(options.get_stats_level()), int96_timestamps(options.is_enabled_int96_timestamps()), + column_index_truncate_length(options.get_column_index_truncate_length()), kv_md(options.get_key_value_metadata()), single_write_mode(mode == SingleWriteMode::YES), out_sink_(std::move(sinks)) @@ -1652,7 +1655,8 @@ void writer::impl::write(table_view const& table, std::vector co bfr += ck.bfr_size; bfr_c += ck.compressed_size; if (stats_granularity_ == statistics_freq::STATISTICS_COLUMN) { - bfr_i += column_index_buffer_size(&ck); + ck.column_index_size = column_index_buffer_size(&ck); + bfr_i += ck.column_index_size; } } } diff --git a/cpp/src/io/parquet/writer_impl.hpp b/cpp/src/io/parquet/writer_impl.hpp index 0a21c73ae7d..c6309488d6b 100644 --- a/cpp/src/io/parquet/writer_impl.hpp +++ b/cpp/src/io/parquet/writer_impl.hpp @@ -194,19 +194,28 @@ class writer::impl { const statistics_chunk* chunk_stats, const statistics_chunk* column_stats); + /** + * @brief Function to calculate the memory needed to encode the column index of the given + * column chunk + * + * @param chunk pointer to column chunk + */ + size_t column_index_buffer_size(gpu::EncColumnChunk* chunk) const; + private: // TODO : figure out if we want to keep this. It is currently unused. rmm::mr::device_memory_resource* _mr = nullptr; // Cuda stream to be used rmm::cuda_stream_view stream; - size_t max_row_group_size = default_row_group_size_bytes; - size_type max_row_group_rows = default_row_group_size_rows; - size_t max_page_size_bytes = default_max_page_size_bytes; - size_type max_page_size_rows = default_max_page_size_rows; - Compression compression_ = Compression::UNCOMPRESSED; - statistics_freq stats_granularity_ = statistics_freq::STATISTICS_NONE; - bool int96_timestamps = false; + size_t max_row_group_size = default_row_group_size_bytes; + size_type max_row_group_rows = default_row_group_size_rows; + size_t max_page_size_bytes = default_max_page_size_bytes; + size_type max_page_size_rows = default_max_page_size_rows; + Compression compression_ = Compression::UNCOMPRESSED; + statistics_freq stats_granularity_ = statistics_freq::STATISTICS_NONE; + bool int96_timestamps = false; + size_type column_index_truncate_length = default_column_index_truncate_length; // Overall file metadata. Filled in during the process and written during write_chunked_end() std::unique_ptr md; // File footer key-value metadata. Written during write_chunked_end() diff --git a/cpp/tests/io/parquet_test.cpp b/cpp/tests/io/parquet_test.cpp index c8fb16ee93b..b65488e9e43 100644 --- a/cpp/tests/io/parquet_test.cpp +++ b/cpp/tests/io/parquet_test.cpp @@ -3999,6 +3999,157 @@ TEST_F(ParquetWriterTest, CheckColumnOffsetIndexStruct) } } +TEST_F(ParquetWriterTest, CheckColumnIndexTruncation) +{ + const char* coldata[] = { + // in-range 7 bit. should truncate to "yyyyyyyz" + "yyyyyyyyy", + // max 7 bit. should truncate to "x7fx7fx7fx7fx7fx7fx7fx80", since it's + // considered binary, not UTF-8. If UTF-8 it should not truncate. + "\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f", + // max binary. this should not truncate + "\xff\xff\xff\xff\xff\xff\xff\xff\xff", + // in-range 2-byte UTF8 (U+00E9). should truncate to "éééê" + "ééééé", + // max 2-byte UTF8 (U+07FF). should not truncate + "߿߿߿߿߿", + // in-range 3-byte UTF8 (U+0800). should truncate to "ࠀࠁ" + "ࠀࠀࠀ", + // max 3-byte UTF8 (U+FFFF). should not truncate + "\xef\xbf\xbf\xef\xbf\xbf\xef\xbf\xbf", + // in-range 4-byte UTF8 (U+10000). should truncate to "𐀀𐀁" + "𐀀𐀀𐀀", + // max unicode (U+10FFFF). should truncate to \xf4\x8f\xbf\xbf\xf4\x90\x80\x80, + // which is no longer valid unicode, but is still ok UTF-8??? + "\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf", + // max 4-byte UTF8 (U+1FFFFF). should not truncate + "\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf"}; + + // NOTE: UTF8 min is initialized with 0xf7bfbfbf. Binary values larger + // than that will not become minimum value (when written as UTF-8). + const char* truncated_min[] = {"yyyyyyyy", + "\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f", + "\xf7\xbf\xbf\xbf", + "éééé", + "߿߿߿߿", + "ࠀࠀ", + "\xef\xbf\xbf\xef\xbf\xbf", + "𐀀𐀀", + "\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf", + "\xf7\xbf\xbf\xbf"}; + + const char* truncated_max[] = {"yyyyyyyz", + "\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x80", + "\xff\xff\xff\xff\xff\xff\xff\xff\xff", + "éééê", + "߿߿߿߿߿", + "ࠀࠁ", + "\xef\xbf\xbf\xef\xbf\xbf\xef\xbf\xbf", + "𐀀𐀁", + "\xf4\x8f\xbf\xbf\xf4\x90\x80\x80", + "\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf\xf7\xbf\xbf\xbf"}; + + auto cols = [&]() { + using string_wrapper = column_wrapper; + std::vector> cols; + for (auto const str : coldata) { + cols.push_back(string_wrapper{str}.release()); + } + return cols; + }(); + auto expected = std::make_unique

(std::move(cols)); + + auto const filepath = temp_env->get_temp_filepath("CheckColumnIndexTruncation.parquet"); + cudf::io::parquet_writer_options out_opts = + cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected->view()) + .stats_level(cudf::io::statistics_freq::STATISTICS_COLUMN) + .column_index_truncate_length(8); + cudf::io::write_parquet(out_opts); + + auto const source = cudf::io::datasource::create(filepath); + cudf::io::parquet::FileMetaData fmd; + + read_footer(source, &fmd); + + for (size_t r = 0; r < fmd.row_groups.size(); r++) { + auto const& rg = fmd.row_groups[r]; + for (size_t c = 0; c < rg.columns.size(); c++) { + auto const& chunk = rg.columns[c]; + + auto const ci = read_column_index(source, chunk); + auto const stats = parse_statistics(chunk); + + // check trunc(page.min) <= stats.min && trun(page.max) >= stats.max + auto const ptype = fmd.schema[c + 1].type; + auto const ctype = fmd.schema[c + 1].converted_type; + EXPECT_TRUE(compare_binary(ci.min_values[0], stats.min_value, ptype, ctype) <= 0); + EXPECT_TRUE(compare_binary(ci.max_values[0], stats.max_value, ptype, ctype) >= 0); + + // check that truncated values == expected + EXPECT_EQ(memcmp(ci.min_values[0].data(), truncated_min[c], ci.min_values[0].size()), 0); + EXPECT_EQ(memcmp(ci.max_values[0].data(), truncated_max[c], ci.max_values[0].size()), 0); + } + } +} + +TEST_F(ParquetWriterTest, BinaryColumnIndexTruncation) +{ + std::vector truncated_min[] = {{0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe}, + {0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}; + + std::vector truncated_max[] = {{0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xff}, + {0xff}, + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}; + + cudf::test::lists_column_wrapper col0{ + {0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe}}; + cudf::test::lists_column_wrapper col1{ + {0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}; + cudf::test::lists_column_wrapper col2{ + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}; + + auto expected = table_view{{col0, col1, col2}}; + + cudf::io::table_input_metadata output_metadata(expected); + output_metadata.column_metadata[0].set_name("col_binary0").set_output_as_binary(true); + output_metadata.column_metadata[1].set_name("col_binary1").set_output_as_binary(true); + output_metadata.column_metadata[2].set_name("col_binary2").set_output_as_binary(true); + + auto const filepath = temp_env->get_temp_filepath("BinaryColumnIndexTruncation.parquet"); + cudf::io::parquet_writer_options out_opts = + cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, expected) + .metadata(&output_metadata) + .stats_level(cudf::io::statistics_freq::STATISTICS_COLUMN) + .column_index_truncate_length(8); + cudf::io::write_parquet(out_opts); + + auto const source = cudf::io::datasource::create(filepath); + cudf::io::parquet::FileMetaData fmd; + + read_footer(source, &fmd); + + for (size_t r = 0; r < fmd.row_groups.size(); r++) { + auto const& rg = fmd.row_groups[r]; + for (size_t c = 0; c < rg.columns.size(); c++) { + auto const& chunk = rg.columns[c]; + + auto const ci = read_column_index(source, chunk); + auto const stats = parse_statistics(chunk); + + // check trunc(page.min) <= stats.min && trun(page.max) >= stats.max + auto const ptype = fmd.schema[c + 1].type; + auto const ctype = fmd.schema[c + 1].converted_type; + EXPECT_TRUE(compare_binary(ci.min_values[0], stats.min_value, ptype, ctype) <= 0); + EXPECT_TRUE(compare_binary(ci.max_values[0], stats.max_value, ptype, ctype) >= 0); + + // check that truncated values == expected + EXPECT_EQ(ci.min_values[0], truncated_min[c]); + EXPECT_EQ(ci.max_values[0], truncated_max[c]); + } + } +} + TEST_F(ParquetReaderTest, EmptyColumnsParam) { srand(31337); From a5dcb32db7b2859e9273d65ab42e6b07067c8775 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Mon, 22 Aug 2022 19:05:34 -0400 Subject: [PATCH 55/58] Handle hyphen as literal for regex cclass when incomplete range (#11557) Handles an incomplete range in a regex cclass pattern `[a-], [-z], [-]` by interpretting the hyphen `-` as a literal. Added gtest to test this new behavior. Closes #11537 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Mark Harris (https://github.com/harrism) - Tobias Ribizel (https://github.com/upsj) URL: https://github.com/rapidsai/cudf/pull/11557 --- cpp/doxygen/regex.md | 2 +- cpp/src/strings/regex/regcomp.cpp | 23 +++++++++++++++----- cpp/tests/strings/contains_tests.cpp | 32 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/cpp/doxygen/regex.md b/cpp/doxygen/regex.md index 0f6762564aa..8377cd2f988 100644 --- a/cpp/doxygen/regex.md +++ b/cpp/doxygen/regex.md @@ -56,7 +56,7 @@ The details are based on features documented at https://www.regular-expressions. | Character class | `[` | `[` begins a character class. | | | Literal character | Any character except `\^-]` | All characters except the listed special characters are literal characters that add themselves to the character class. | `[abc]` matches `a`, `b` or `c` | | Backslash escapes a metacharacter | `\` (backslash) followed by any of `\^-]` | A backslash escapes special characters to suppress their special meaning. | `[\^\]]` matches `^` or `]` | -| Range | `-` (hyphen) between two tokens that each specify a single character. | Adds a range of characters to the character class. | `[a-zA-Z0-9]` matches any ASCII letter or digit | +| Range | `-` (hyphen) between two tokens that each specify a single character. | Adds a range of characters to the character class. If '`-`' is the first or last character (e.g. `[a-]` or `[-z]`), it will match a literal '`-`' and not infer a range. | `[a-zA-Z0-9]` matches any ASCII letter or digit | | Negated character class | `^` (caret) immediately after the opening `[` | Negates the character class, causing it to match a single character not listed in the character class. | `[^a-d]` matches `x` (any character except `a`, `b`, `c` or `d`) | | Literal opening bracket | `[` | An opening square bracket is a literal character that adds an opening square bracket to the character class. | `[ab[cd]ef]` matches `aef]`, `bef]`, `[ef]`, `cef]`, and `def]` | | Character escape | `\n`, `\r` and `\t` | Add an LF character, a CR character, or a tab character to the character class, respectively. | `[\n\r\t]` matches a line feed, a carriage return, or a tab character | diff --git a/cpp/src/strings/regex/regcomp.cpp b/cpp/src/strings/regex/regcomp.cpp index 1dc89433b82..c84b1e630c9 100644 --- a/cpp/src/strings/regex/regcomp.cpp +++ b/cpp/src/strings/regex/regcomp.cpp @@ -354,12 +354,25 @@ class regex_parser { } } if (!is_quoted && chr == ']' && count_char > 1) { break; } // done - if (!is_quoted && chr == '-') { - if (literals.empty()) { return 0; } // malformed '[]' - std::tie(is_quoted, chr) = next_char(); - if ((!is_quoted && chr == ']') || chr == 0) { return 0; } // malformed '[]' - literals.back() = chr; + + // A hyphen '-' here signifies a range of characters in a '[]' class definition. + // The logic here also gracefully handles a dangling '-' appearing unquoted + // at the beginning '[-x]' or at the end '[x-]' or by itself '[-]' + // and treats the '-' as a literal value in this cclass in this case. + if (!is_quoted && chr == '-' && !literals.empty()) { + auto [q, n_chr] = next_char(); + if (n_chr == 0) { return 0; } // malformed: '[x-' + + if (!q && n_chr == ']') { // handles: '[x-]' + literals.push_back(chr); + literals.push_back(chr); // add '-' as literal + break; + } + // normal case: '[a-z]' + // update end-range character + literals.back() = n_chr; } else { + // add single literal literals.push_back(chr); literals.push_back(chr); } diff --git a/cpp/tests/strings/contains_tests.cpp b/cpp/tests/strings/contains_tests.cpp index e97640986e9..960ccaec274 100644 --- a/cpp/tests/strings/contains_tests.cpp +++ b/cpp/tests/strings/contains_tests.cpp @@ -460,6 +460,38 @@ TEST_F(StringsContainsTests, OverlappedClasses) } } +TEST_F(StringsContainsTests, IncompleteClassesRange) +{ + auto input = cudf::test::strings_column_wrapper({"abc-def", "---", "", "ghijkl", "-wxyz-"}); + auto sv = cudf::strings_column_view(input); + + { + cudf::test::fixed_width_column_wrapper expected({1, 0, 0, 1, 1}); + auto results = cudf::strings::contains_re(sv, "[a-z]"); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + results = cudf::strings::contains_re(sv, "[a-m-z]"); // same as [a-z] + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + } + { + cudf::test::fixed_width_column_wrapper expected({1, 1, 0, 1, 1}); + auto results = cudf::strings::contains_re(sv, "[g-]"); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + results = cudf::strings::contains_re(sv, "[-k]"); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + } + { + cudf::test::fixed_width_column_wrapper expected({1, 1, 0, 0, 1}); + auto results = cudf::strings::contains_re(sv, "[-]"); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + results = cudf::strings::contains_re(sv, "[+--]"); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + results = cudf::strings::contains_re(sv, "[a-c-]"); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + results = cudf::strings::contains_re(sv, "[-d-f]"); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + } +} + TEST_F(StringsContainsTests, MultiLine) { auto input = From 8f3cc74b280918953d83158814d9d56b0310ea7b Mon Sep 17 00:00:00 2001 From: Nghia Truong Date: Mon, 22 Aug 2022 16:21:41 -0700 Subject: [PATCH 56/58] Support nested types in `lists::contains` (#10548) This extends the `lists::contains` API to support nested types (lists + structs) with arbitrarily nested levels. As such, `lists::contains` will work with literally any type of input data. In addition, the related implementation has been significantly refactored to facilitate adding new implementation. Closes https://github.com/rapidsai/cudf/issues/8958. Depends on: * https://github.com/rapidsai/cudf/pull/10730 * https://github.com/rapidsai/cudf/pull/10883 * https://github.com/rapidsai/cudf/pull/10999 * https://github.com/rapidsai/cudf/pull/11019 * https://github.com/rapidsai/cudf/issues/11037 Authors: - Nghia Truong (https://github.com/ttnghia) - Bradley Dice (https://github.com/bdice) Approvers: - MithunR (https://github.com/mythrocks) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/10548 --- cpp/include/cudf/lists/list_device_view.cuh | 7 + cpp/src/lists/contains.cu | 296 ++++-- cpp/tests/lists/contains_tests.cpp | 1061 ++++++++++++++++--- 3 files changed, 1144 insertions(+), 220 deletions(-) diff --git a/cpp/include/cudf/lists/list_device_view.cuh b/cpp/include/cudf/lists/list_device_view.cuh index 1653a03ce37..07346e78261 100644 --- a/cpp/include/cudf/lists/list_device_view.cuh +++ b/cpp/include/cudf/lists/list_device_view.cuh @@ -129,6 +129,13 @@ class list_device_view { */ [[nodiscard]] __device__ inline size_type size() const { return _size; } + /** + * @brief Returns the row index of this list in the original lists column. + * + * @return The row index of this list + */ + [[nodiscard]] __device__ inline size_type row_index() const { return _row_index; } + /** * @brief Fetches the lists_column_device_view that contains this list. * diff --git a/cpp/src/lists/contains.cu b/cpp/src/lists/contains.cu index 16e155e9e6c..b4223a1c0c1 100644 --- a/cpp/src/lists/contains.cu +++ b/cpp/src/lists/contains.cu @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,6 @@ #include #include -#include #include #include #include @@ -42,7 +42,6 @@ #include namespace cudf::lists { - namespace { /** @@ -54,13 +53,13 @@ auto constexpr __device__ NOT_FOUND_SENTINEL = size_type{-1}; /** * @brief A sentinel value used for marking that a given output row should be null. + * + * This value should be different from `NOT_FOUND_SENTINEL`. */ auto constexpr __device__ NULL_SENTINEL = std::numeric_limits::min(); /** - * @brief Indicate the current supported types in `cudf::lists::contains`. - * - * TODO: Add supported nested types. + * @brief Check if the given type is a supported non-nested type in `cudf::lists::contains`. */ template static auto constexpr is_supported_non_nested_type() @@ -69,13 +68,52 @@ static auto constexpr is_supported_non_nested_type() } /** - * @brief Functor to perform searching for index of a key element in a given list. + * @brief Check if the given type is supported in `cudf::lists::contains`. + */ +template +auto constexpr is_supported_type() +{ + return is_supported_non_nested_type() || cudf::is_nested(); +} + +/** + * @brief Return a pair of index iterators {begin, end} to loop through elements within a + * list. + * + * Depending on the value of `forward`, a pair of forward or reverse iterators will be + * returned, allowing to loop through elements in the list in first-to-last or last-to-first + * order. + * + * Note that the element indices always restart to `0` at the first position in each list. + * + * @tparam forward A boolean value indicating whether we want to iterate elements in the list + * by forward or reverse order. + * @param size The number of elements in the list. + * @return A pair of {begin, end} iterators to iterate through the range `[0, size)`. + */ +template +__device__ auto element_index_pair_iter(size_type const size) +{ + auto const begin = thrust::make_counting_iterator(0); + auto const end = thrust::make_counting_iterator(size); + + if constexpr (forward) { + return thrust::pair{begin, end}; + } else { + return thrust::pair{thrust::make_reverse_iterator(end), thrust::make_reverse_iterator(begin)}; + } +} + +/** + * @brief Functor to perform searching for index of a key element in a given list, specialized + * for non-nested types. */ -struct search_list_fn { +struct search_list_non_nested_types_fn { duplicate_find_option const find_option; template ())> - __device__ size_type operator()(list_device_view list, thrust::optional key_opt) const + __device__ size_type operator()(list_device_view const list, + thrust::optional const key_opt) const { // A null list or null key will result in a null output row. if (list.is_null() || !key_opt) { return NULL_SENTINEL; } @@ -86,7 +124,7 @@ struct search_list_fn { } template ())> - __device__ size_type operator()(list_device_view, thrust::optional) const + __device__ size_type operator()(list_device_view const, thrust::optional const) const { CUDF_UNREACHABLE("Unsupported type."); } @@ -98,55 +136,179 @@ struct search_list_fn { { auto const [begin, end] = element_index_pair_iter(list.size()); auto const found_iter = - thrust::find_if(thrust::seq, begin, end, [&] __device__(auto const idx) { + thrust::find_if(thrust::seq, begin, end, [=] __device__(auto const idx) { return !list.is_null(idx) && cudf::equality_compare(list.template element(idx), search_key); }); // If the key is found, return its found position in the list from `found_iter`. return found_iter == end ? NOT_FOUND_SENTINEL : *found_iter; } +}; + +/** + * @brief Functor to perform searching for index of a key element in a given list, specialized + * for nested types. + */ +template +struct search_list_nested_types_fn { + duplicate_find_option const find_option; + KeyValidityIter const key_validity_iter; + EqComparator const d_comp; + bool const search_key_is_scalar; + + search_list_nested_types_fn(duplicate_find_option const find_option, + KeyValidityIter const key_validity_iter, + EqComparator const& d_comp, + bool search_key_is_scalar) + : find_option(find_option), + key_validity_iter(key_validity_iter), + d_comp(d_comp), + search_key_is_scalar(search_key_is_scalar) + { + } + + __device__ size_type operator()(list_device_view const list) const + { + // A null list or null key will result in a null output row. + if (list.is_null() || !key_validity_iter[list.row_index()]) { return NULL_SENTINEL; } + + return find_option == duplicate_find_option::FIND_FIRST ? search_list(list) + : search_list(list); + } - /** - * @brief Return a pair of index iterators {begin, end} to loop through elements within a list. - * - * Depending on the value of `forward`, a pair of forward or reverse iterators will be - * returned, allowing to loop through elements in the list in first-to-last or last-to-first - * order. - * - * Note that the element indices always restart to `0` at the first position in each list. - * - * @tparam forward A boolean value indicating whether we want to iterate elements in the list by - * forward or reverse order. - * @param size The number of elements in the list. - * @return A pair of {begin, end} iterators to iterate through the range `[0, size)`. - */ + private: template - static __device__ auto element_index_pair_iter(size_type const size) + __device__ inline size_type search_list(list_device_view const list) const { - if constexpr (forward) { - return thrust::pair(thrust::make_counting_iterator(0), thrust::make_counting_iterator(size)); + using cudf::experimental::row::lhs_index_type; + using cudf::experimental::row::rhs_index_type; + + auto const [begin, end] = element_index_pair_iter(list.size()); + auto const found_iter = + thrust::find_if(thrust::seq, begin, end, [=] __device__(auto const idx) { + return !list.is_null(idx) && + d_comp(static_cast(list.element_offset(idx)), + static_cast(search_key_is_scalar ? 0 : list.row_index())); + }); + // If the key is found, return its found position in the list from `found_iter`. + return found_iter == end ? NOT_FOUND_SENTINEL : *found_iter; + } +}; + +/** + * @brief Function to search for key element(s) in the corresponding rows of a lists column, + * specialized for non-nested types. + */ +template +void index_of_non_nested_types(InputIterator input_it, + size_type num_rows, + OutputIterator output_it, + SearchKeyType const& search_keys, + bool search_keys_have_nulls, + duplicate_find_option find_option, + rmm::cuda_stream_view stream) +{ + auto const do_search = [=](auto const keys_iter) { + thrust::transform(rmm::exec_policy(stream), + input_it, + input_it + num_rows, + keys_iter, + output_it, + search_list_non_nested_types_fn{find_option}); + }; + + if constexpr (search_key_is_scalar) { + auto const keys_iter = cudf::detail::make_optional_iterator( + search_keys, nullate::DYNAMIC{search_keys_have_nulls}); + do_search(keys_iter); + } else { + auto const keys_cdv_ptr = column_device_view::create(search_keys, stream); + auto const keys_iter = cudf::detail::make_optional_iterator( + *keys_cdv_ptr, nullate::DYNAMIC{search_keys_have_nulls}); + do_search(keys_iter); + } +} + +/** + * @brief Function to search for index of key element(s) in the corresponding rows of a lists + * column, specialized for nested types. + */ +template +void index_of_nested_types(InputIterator input_it, + size_type num_rows, + OutputIterator output_it, + column_view const& child, + SearchKeyType const& search_keys, + duplicate_find_option find_option, + rmm::cuda_stream_view stream) +{ + // Create a `table_view` from the search key(s). + // If the input search key is a (nested type) scalar, a new column is materialized from that + // scalar before a `table_view` is generated from it. As such, the new created column will also be + // returned to keep the result `table_view` valid. + [[maybe_unused]] auto const [keys_tview, unused_column] = + [&]() -> std::pair> { + if constexpr (std::is_same_v) { + auto tmp_column = make_column_from_scalar(search_keys, 1, stream); + auto const keys_tview = tmp_column->view(); + return {table_view{{keys_tview}}, std::move(tmp_column)}; } else { - return thrust::pair(thrust::make_reverse_iterator(thrust::make_counting_iterator(size)), - thrust::make_reverse_iterator(thrust::make_counting_iterator(0))); + return {table_view{{search_keys}}, nullptr}; } + }(); + auto const child_tview = table_view{{child}}; + auto const has_nulls = has_nested_nulls(child_tview) || has_nested_nulls(keys_tview); + auto const comparator = + cudf::experimental::row::equality::two_table_comparator(child_tview, keys_tview, stream); + auto const d_comp = comparator.equal_to(nullate::DYNAMIC{has_nulls}); + + auto const do_search = [=](auto const key_validity_iter) { + thrust::transform( + rmm::exec_policy(stream), + input_it, + input_it + num_rows, + output_it, + search_list_nested_types_fn{find_option, key_validity_iter, d_comp, search_key_is_scalar}); + }; + + if constexpr (search_key_is_scalar) { + auto const key_validity_iter = cudf::detail::make_validity_iterator(search_keys); + do_search(key_validity_iter); + } else { + auto const keys_dv_ptr = column_device_view::create(search_keys, stream); + auto const key_validity_iter = cudf::detail::make_validity_iterator(*keys_dv_ptr); + do_search(key_validity_iter); } -}; +} /** - * @brief Dispatch functor to search for key element(s) in the corresponding rows of a lists column. + * @brief Dispatch functor to search for index of key element(s) in the corresponding rows of a + * lists column. */ struct dispatch_index_of { + // SFINAE with conditional return type because we need to support device lambda in this function. + // This is required due to a limitation of nvcc. template - std::enable_if_t(), std::unique_ptr> operator()( + std::enable_if_t(), std::unique_ptr> operator()( lists_column_view const& lists, SearchKeyType const& search_keys, duplicate_find_option find_option, rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr) const { - CUDF_EXPECTS(!cudf::is_nested(lists.child().type()), - "Nested types not supported in list search operations."); - CUDF_EXPECTS(lists.child().type() == search_keys.type(), + // Access the child column through `child()` method, not `get_sliced_child()`. + // This is because slicing offset has already been taken into account during row + // comparisons. + auto const child = lists.child(); + + CUDF_EXPECTS(child.type() == search_keys.type(), "Type/Scale of search key does not match list column element type."); CUDF_EXPECTS(search_keys.type().id() != type_id::EMPTY, "Type cannot be empty."); @@ -159,12 +321,14 @@ struct dispatch_index_of { } }(); + auto const num_rows = lists.size(); + if (search_key_is_scalar && search_keys_have_nulls) { // If the scalar key is invalid/null, the entire output column will be all nulls. return make_numeric_column(data_type{cudf::type_to_id()}, - lists.size(), - cudf::create_null_mask(lists.size(), mask_state::ALL_NULL, mr), - lists.size(), + num_rows, + cudf::create_null_mask(num_rows, mask_state::ALL_NULL, mr), + num_rows, stream, mr); } @@ -177,33 +341,21 @@ struct dispatch_index_of { }); auto out_positions = make_numeric_column( - data_type{type_to_id()}, lists.size(), cudf::mask_state::UNALLOCATED, stream, mr); - auto const out_begin = out_positions->mutable_view().template begin(); - - auto const do_search = [&](auto const keys_iter) { - thrust::transform(rmm::exec_policy(stream), - input_it, - input_it + lists.size(), - keys_iter, - out_begin, - search_list_fn{find_option}); - }; - - if constexpr (search_key_is_scalar) { - auto const keys_iter = cudf::detail::make_optional_iterator( - search_keys, nullate::DYNAMIC{search_keys_have_nulls}); - do_search(keys_iter); - } else { - auto const keys_cdv_ptr = column_device_view::create(search_keys, stream); - auto const keys_iter = cudf::detail::make_optional_iterator( - *keys_cdv_ptr, nullate::DYNAMIC{search_keys_have_nulls}); - do_search(keys_iter); + data_type{type_to_id()}, num_rows, cudf::mask_state::UNALLOCATED, stream, mr); + auto const output_it = out_positions->mutable_view().template begin(); + + if constexpr (not cudf::is_nested()) { + index_of_non_nested_types( + input_it, num_rows, output_it, search_keys, search_keys_have_nulls, find_option, stream); + } else { // list + struct + index_of_nested_types( + input_it, num_rows, output_it, child, search_keys, find_option, stream); } if (search_keys_have_nulls || lists.has_nulls()) { auto [null_mask, null_count] = cudf::detail::valid_if( - out_begin, - out_begin + lists.size(), + output_it, + output_it + num_rows, [] __device__(auto const idx) { return idx != NULL_SENTINEL; }, stream, mr); @@ -213,7 +365,7 @@ struct dispatch_index_of { } template - std::enable_if_t(), std::unique_ptr> operator()( + std::enable_if_t(), std::unique_ptr> operator()( lists_column_view const&, SearchKeyType const&, duplicate_find_option, @@ -226,7 +378,7 @@ struct dispatch_index_of { /** * @brief Converts key-positions vector (from `index_of()`) to a BOOL8 vector, indicating if - * the search key(s) were found. + * the search key(s) were found. */ std::unique_ptr to_contains(std::unique_ptr&& key_positions, rmm::cuda_stream_view stream, @@ -282,8 +434,12 @@ std::unique_ptr contains(lists_column_view const& lists, rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr) { - return to_contains( - index_of(lists, search_key, duplicate_find_option::FIND_FIRST, stream), stream, mr); + auto key_indices = index_of(lists, + search_key, + duplicate_find_option::FIND_FIRST, + stream, + rmm::mr::get_current_device_resource()); + return to_contains(std::move(key_indices), stream, mr); } std::unique_ptr contains(lists_column_view const& lists, @@ -294,8 +450,12 @@ std::unique_ptr contains(lists_column_view const& lists, CUDF_EXPECTS(search_keys.size() == lists.size(), "Number of search keys must match list column size."); - return to_contains( - index_of(lists, search_keys, duplicate_find_option::FIND_FIRST, stream), stream, mr); + auto key_indices = index_of(lists, + search_keys, + duplicate_find_option::FIND_FIRST, + stream, + rmm::mr::get_current_device_resource()); + return to_contains(std::move(key_indices), stream, mr); } std::unique_ptr contains_nulls(lists_column_view const& lists, @@ -305,7 +465,7 @@ std::unique_ptr contains_nulls(lists_column_view const& lists, auto const lists_cv = lists.parent(); auto output = make_numeric_column(data_type{type_to_id()}, lists.size(), - copy_bitmask(lists_cv), + copy_bitmask(lists_cv, stream, mr), lists_cv.null_count(), stream, mr); diff --git a/cpp/tests/lists/contains_tests.cpp b/cpp/tests/lists/contains_tests.cpp index 4cc0c4155b8..a93ef4f8b1d 100644 --- a/cpp/tests/lists/contains_tests.cpp +++ b/cpp/tests/lists/contains_tests.cpp @@ -17,10 +17,8 @@ #include #include -#include #include #include -#include #include #include @@ -31,24 +29,7 @@ namespace cudf { namespace test { -struct ContainsTest : public BaseFixture { -}; - -using ContainsTestTypes = Concat; - -template -struct TypedContainsTest : public ContainsTest { -}; - -TYPED_TEST_SUITE(TypedContainsTest, ContainsTestTypes); - namespace { - -auto constexpr x = int32_t{-1}; // Placeholder for nulls. -auto constexpr absent = size_type{-1}; // Index when key is not found in a list. -auto constexpr FIND_FIRST = lists::duplicate_find_option::FIND_FIRST; -auto constexpr FIND_LAST = lists::duplicate_find_option::FIND_LAST; - template (), void>* = nullptr> auto create_scalar_search_key(T const& value) { @@ -82,6 +63,12 @@ auto create_scalar_search_key(typename T::rep const& value) return search_key; } +template +auto make_struct_scalar(Args&&... args) +{ + return cudf::struct_scalar(std::vector{std::forward(args)...}); +} + template (), void>* = nullptr> auto create_null_search_key() { @@ -108,11 +95,30 @@ auto create_null_search_key() } // namespace +auto constexpr X = int32_t{0}; // Placeholder for nulls. +auto constexpr ABSENT = size_type{-1}; // Index when key is not found in a list. +auto constexpr FIND_FIRST = lists::duplicate_find_option::FIND_FIRST; +auto constexpr FIND_LAST = lists::duplicate_find_option::FIND_LAST; + +using bools_col = cudf::test::fixed_width_column_wrapper; +using indices_col = cudf::test::fixed_width_column_wrapper; +using structs_col = cudf::test::structs_column_wrapper; +using strings_col = cudf::test::strings_column_wrapper; + using iterators::all_nulls; using iterators::null_at; using iterators::nulls_at; -using bools = fixed_width_column_wrapper; -using indices = fixed_width_column_wrapper; + +using ContainsTestTypes = Concat; + +struct ContainsTest : public BaseFixture { +}; + +template +struct TypedContainsTest : public ContainsTest { +}; + +TYPED_TEST_SUITE(TypedContainsTest, ContainsTestTypes); TYPED_TEST(TypedContainsTest, ScalarKeyWithNoNulls) { @@ -134,25 +140,25 @@ TYPED_TEST(TypedContainsTest, ScalarKeyWithNoNulls) { // CONTAINS auto result = lists::contains(search_space, *search_key_one); - auto expected = bools{1, 0, 0, 1, 0, 0, 0, 0, 1, 0}; + auto expected = bools_col{1, 0, 0, 1, 0, 0, 0, 0, 1, 0}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // CONTAINS NULLS auto result = lists::contains_nulls(search_space); - auto expected = bools{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + auto expected = bools_col{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space, *search_key_one, FIND_FIRST); - auto expected = indices{1, absent, absent, 2, absent, absent, absent, absent, 0, absent}; + auto expected = indices_col{1, ABSENT, ABSENT, 2, ABSENT, ABSENT, ABSENT, ABSENT, 0, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space, *search_key_one, FIND_LAST); - auto expected = indices{3, absent, absent, 4, absent, absent, absent, absent, 2, absent}; + auto expected = indices_col{3, ABSENT, ABSENT, 4, ABSENT, ABSENT, ABSENT, ABSENT, 2, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -179,27 +185,27 @@ TYPED_TEST(TypedContainsTest, ScalarKeyWithNullLists) { // CONTAINS auto result = lists::contains(search_space, *search_key_one); - auto expected = bools{{1, 0, 0, x, 1, 0, 0, 0, 0, 1, x}, nulls_at({3, 10})}; + auto expected = bools_col{{1, 0, 0, X, 1, 0, 0, 0, 0, 1, X}, nulls_at({3, 10})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // CONTAINS NULLS auto result = lists::contains_nulls(search_space); - auto expected = bools{{0, 0, 0, x, 0, 0, 0, 0, 0, 0, x}, nulls_at({3, 10})}; + auto expected = bools_col{{0, 0, 0, X, 0, 0, 0, 0, 0, 0, X}, nulls_at({3, 10})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST - auto result = lists::index_of(search_space, *search_key_one, FIND_FIRST); - auto expected = - indices{{1, absent, absent, x, 2, absent, absent, absent, absent, 0, x}, nulls_at({3, 10})}; + auto result = lists::index_of(search_space, *search_key_one, FIND_FIRST); + auto expected = indices_col{{1, ABSENT, ABSENT, X, 2, ABSENT, ABSENT, ABSENT, ABSENT, 0, X}, + nulls_at({3, 10})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST - auto result = lists::index_of(search_space, *search_key_one, FIND_LAST); - auto expected = - indices{{3, absent, absent, x, 4, absent, absent, absent, absent, 0, x}, nulls_at({3, 10})}; + auto result = lists::index_of(search_space, *search_key_one, FIND_LAST); + auto expected = indices_col{{3, ABSENT, ABSENT, X, 4, ABSENT, ABSENT, ABSENT, ABSENT, 0, X}, + nulls_at({3, 10})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -230,25 +236,27 @@ TYPED_TEST(TypedContainsTest, SlicedLists) { // CONTAINS auto result = lists::contains(sliced_column_1, *search_key_one); - auto expected_result = bools{{0, 0, x, 1, 0, 0, 0}, null_at(2)}; + auto expected_result = bools_col{{0, 0, X, 1, 0, 0, 0}, null_at(2)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected_result, result->view()); } { // CONTAINS NULLS auto result = lists::contains_nulls(sliced_column_1); - auto expected_result = bools{{0, 0, x, 0, 0, 0, 0}, null_at(2)}; + auto expected_result = bools_col{{0, 0, X, 0, 0, 0, 0}, null_at(2)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected_result, result->view()); } { // FIND_FIRST - auto result = lists::index_of(sliced_column_1, *search_key_one, FIND_FIRST); - auto expected_result = indices{{absent, absent, 0, 2, absent, absent, absent}, null_at(2)}; + auto result = lists::index_of(sliced_column_1, *search_key_one, FIND_FIRST); + auto expected_result = + indices_col{{ABSENT, ABSENT, 0, 2, ABSENT, ABSENT, ABSENT}, null_at(2)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected_result, result->view()); } { // FIND_LAST - auto result = lists::index_of(sliced_column_1, *search_key_one, FIND_LAST); - auto expected_result = indices{{absent, absent, 0, 4, absent, absent, absent}, null_at(2)}; + auto result = lists::index_of(sliced_column_1, *search_key_one, FIND_LAST); + auto expected_result = + indices_col{{ABSENT, ABSENT, 0, 4, ABSENT, ABSENT, ABSENT}, null_at(2)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected_result, result->view()); } } @@ -260,25 +268,25 @@ TYPED_TEST(TypedContainsTest, SlicedLists) { // CONTAINS auto result = lists::contains(sliced_column_2, *search_key_one); - auto expected_result = bools{{x, 1, 0, 0, 0, 0, 1}, null_at(0)}; + auto expected_result = bools_col{{X, 1, 0, 0, 0, 0, 1}, null_at(0)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected_result, result->view()); } { // CONTAINS NULLS auto result = lists::contains_nulls(sliced_column_2); - auto expected_result = bools{{x, 0, 0, 0, 0, 0, 0}, null_at(0)}; + auto expected_result = bools_col{{X, 0, 0, 0, 0, 0, 0}, null_at(0)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected_result, result->view()); } { // FIND_FIRST auto result = lists::index_of(sliced_column_2, *search_key_one, FIND_FIRST); - auto expected_result = indices{{0, 2, absent, absent, absent, absent, 0}, null_at(0)}; + auto expected_result = indices_col{{0, 2, ABSENT, ABSENT, ABSENT, ABSENT, 0}, null_at(0)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected_result, result->view()); } { // FIND_LAST auto result = lists::index_of(sliced_column_2, *search_key_one, FIND_LAST); - auto expected_result = indices{{0, 4, absent, absent, absent, absent, 2}, null_at(0)}; + auto expected_result = indices_col{{0, 4, ABSENT, ABSENT, ABSENT, ABSENT, 2}, null_at(0)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected_result, result->view()); } } @@ -289,34 +297,34 @@ TYPED_TEST(TypedContainsTest, ScalarKeyNonNullListsWithNullValues) // Test List columns that have no NULL list rows, but NULL elements in some list rows. using T = TypeParam; - auto numerals = fixed_width_column_wrapper{{x, 1, 2, x, 4, 5, x, 7, 8, x, x, 1, 2, x, 1}, + auto numerals = fixed_width_column_wrapper{{X, 1, 2, X, 4, 5, X, 7, 8, X, X, 1, 2, X, 1}, nulls_at({0, 3, 6, 9, 10, 13})}; auto search_space = make_lists_column( - 8, indices{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), numerals.release(), 0, {}); + 8, indices_col{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), numerals.release(), 0, {}); // Search space: [ [x], [1,2], [x,4,5,x], [], [], [7,8,x], [x], [1,2,x,1] ] auto search_key_one = create_scalar_search_key(1); { // CONTAINS auto result = lists::contains(search_space->view(), *search_key_one); - auto expected = bools{0, 1, 0, 0, 0, 0, 0, 1}; + auto expected = bools_col{0, 1, 0, 0, 0, 0, 0, 1}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // CONTAINS NULLS auto result = lists::contains_nulls(search_space->view()); - auto expected = bools{1, 0, 1, 0, 0, 1, 1, 1}; + auto expected = bools_col{1, 0, 1, 0, 0, 1, 1, 1}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), *search_key_one, FIND_FIRST); - auto expected = indices{absent, 0, absent, absent, absent, absent, absent, 0}; + auto expected = indices_col{ABSENT, 0, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 0}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), *search_key_one, FIND_LAST); - auto expected = indices{absent, 0, absent, absent, absent, absent, absent, 3}; + auto expected = indices_col{ABSENT, 0, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 3}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -325,13 +333,13 @@ TYPED_TEST(TypedContainsTest, ScalarKeysWithNullsInLists) { using T = TypeParam; - auto numerals = fixed_width_column_wrapper{{x, 1, 2, x, 4, 5, x, 7, 8, x, x, 1, 2, x, 1}, + auto numerals = fixed_width_column_wrapper{{X, 1, 2, X, 4, 5, X, 7, 8, X, X, 1, 2, X, 1}, nulls_at({0, 3, 6, 9, 10, 13})}; auto input_null_mask_iter = null_at(4); auto search_space = make_lists_column( 8, - indices{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), + indices_col{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), numerals.release(), 1, cudf::test::detail::make_null_mask(input_null_mask_iter, input_null_mask_iter + 8)); @@ -341,25 +349,25 @@ TYPED_TEST(TypedContainsTest, ScalarKeysWithNullsInLists) { // CONTAINS. auto result = lists::contains(search_space->view(), *search_key_one); - auto expected = bools{{0, 1, 0, 0, x, 0, 0, 1}, null_at(4)}; + auto expected = bools_col{{0, 1, 0, 0, X, 0, 0, 1}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // CONTAINS NULLS. auto result = lists::contains_nulls(search_space->view()); - auto expected = bools{{1, 0, 1, 0, x, 1, 1, 1}, null_at(4)}; + auto expected = bools_col{{1, 0, 1, 0, X, 1, 1, 1}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST. auto result = lists::index_of(search_space->view(), *search_key_one, FIND_FIRST); - auto expected = indices{{absent, 0, absent, absent, x, absent, absent, 0}, null_at(4)}; + auto expected = indices_col{{ABSENT, 0, ABSENT, ABSENT, X, ABSENT, ABSENT, 0}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST. auto result = lists::index_of(search_space->view(), *search_key_one, FIND_LAST); - auto expected = indices{{absent, 0, absent, absent, x, absent, absent, 3}, null_at(4)}; + auto expected = indices_col{{ABSENT, 0, ABSENT, ABSENT, X, ABSENT, ABSENT, 3}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -368,7 +376,7 @@ TEST_F(ContainsTest, BoolScalarWithNullsInLists) { using T = bool; - auto numerals = fixed_width_column_wrapper{{x, 1, 1, x, 1, 1, x, 1, 1, x, x, 1, 1, x, 1}, + auto numerals = fixed_width_column_wrapper{{X, 1, 1, X, 1, 1, X, 1, 1, X, X, 1, 1, X, 1}, nulls_at({0, 3, 6, 9, 10, 13})}; auto input_null_mask_iter = null_at(4); auto search_space = make_lists_column( @@ -383,25 +391,25 @@ TEST_F(ContainsTest, BoolScalarWithNullsInLists) { // CONTAINS auto result = lists::contains(search_space->view(), *search_key_one); - auto expected = bools{{0, 1, 1, 0, x, 1, 0, 1}, null_at(4)}; + auto expected = bools_col{{0, 1, 1, 0, X, 1, 0, 1}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // CONTAINS NULLS auto result = lists::contains_nulls(search_space->view()); - auto expected = bools{{1, 0, 1, 0, x, 1, 1, 1}, null_at(4)}; + auto expected = bools_col{{1, 0, 1, 0, X, 1, 1, 1}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST. auto result = lists::index_of(search_space->view(), *search_key_one, FIND_FIRST); - auto expected = indices{{absent, 0, 1, absent, x, 0, absent, 0}, null_at(4)}; + auto expected = indices_col{{ABSENT, 0, 1, ABSENT, X, 0, ABSENT, 0}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST. auto result = lists::index_of(search_space->view(), *search_key_one, FIND_LAST); - auto expected = indices{{absent, 1, 2, absent, x, 1, absent, 3}, null_at(4)}; + auto expected = indices_col{{ABSENT, 1, 2, ABSENT, X, 1, ABSENT, 3}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -416,7 +424,7 @@ TEST_F(ContainsTest, StringScalarWithNullsInLists) auto input_null_mask_iter = null_at(4); auto search_space = make_lists_column( 8, - indices{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), + indices_col{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), strings.release(), 1, cudf::test::detail::make_null_mask(input_null_mask_iter, input_null_mask_iter + 8)); @@ -426,25 +434,25 @@ TEST_F(ContainsTest, StringScalarWithNullsInLists) { // CONTAINS auto result = lists::contains(search_space->view(), *search_key_one); - auto expected = bools{{0, 1, 0, 0, x, 0, 0, 1}, null_at(4)}; + auto expected = bools_col{{0, 1, 0, 0, X, 0, 0, 1}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // CONTAINS NULLS auto result = lists::contains_nulls(search_space->view()); - auto expected = bools{{1, 0, 1, 0, x, 1, 1, 1}, null_at(4)}; + auto expected = bools_col{{1, 0, 1, 0, X, 1, 1, 1}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST. auto result = lists::index_of(search_space->view(), *search_key_one, FIND_FIRST); - auto expected = indices{{absent, 0, absent, absent, x, absent, absent, 0}, null_at(4)}; + auto expected = indices_col{{ABSENT, 0, ABSENT, ABSENT, X, ABSENT, ABSENT, 0}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST. auto result = lists::index_of(search_space->view(), *search_key_one, FIND_LAST); - auto expected = indices{{absent, 0, absent, absent, x, absent, absent, 3}, null_at(4)}; + auto expected = indices_col{{ABSENT, 0, ABSENT, ABSENT, X, ABSENT, ABSENT, 3}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -470,19 +478,19 @@ TYPED_TEST(TypedContainsTest, ScalarNullSearchKey) { // CONTAINS auto result = lists::contains(search_space->view(), *search_key_null); - auto expected = bools{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, all_nulls()}; + auto expected = bools_col{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, all_nulls()}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), *search_key_null, FIND_FIRST); - auto expected = indices{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, all_nulls()}; + auto expected = indices_col{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, all_nulls()}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), *search_key_null, FIND_LAST); - auto expected = indices{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, all_nulls()}; + auto expected = indices_col{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, all_nulls()}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -497,12 +505,9 @@ TEST_F(ContainsTest, ScalarTypeRelatedExceptions) {{1, 2, 3}, {4, 5, 6}}}.release(); auto skey = create_scalar_search_key(10); - CUDF_EXPECT_THROW_MESSAGE(lists::contains(list_of_lists->view(), *skey), - "Nested types not supported in list search operations."); - CUDF_EXPECT_THROW_MESSAGE(lists::index_of(list_of_lists->view(), *skey, FIND_FIRST), - "Nested types not supported in list search operations."); - CUDF_EXPECT_THROW_MESSAGE(lists::index_of(list_of_lists->view(), *skey, FIND_LAST), - "Nested types not supported in list search operations."); + EXPECT_THROW(lists::contains(list_of_lists->view(), *skey), cudf::logic_error); + EXPECT_THROW(lists::index_of(list_of_lists->view(), *skey, FIND_FIRST), cudf::logic_error); + EXPECT_THROW(lists::index_of(list_of_lists->view(), *skey, FIND_LAST), cudf::logic_error); } { // Search key must match list elements in type. @@ -513,12 +518,9 @@ TEST_F(ContainsTest, ScalarTypeRelatedExceptions) } .release(); auto skey = create_scalar_search_key("Hello, World!"); - CUDF_EXPECT_THROW_MESSAGE(lists::contains(list_of_ints->view(), *skey), - "Type/Scale of search key does not match list column element type."); - CUDF_EXPECT_THROW_MESSAGE(lists::index_of(list_of_ints->view(), *skey, FIND_FIRST), - "Type/Scale of search key does not match list column element type."); - CUDF_EXPECT_THROW_MESSAGE(lists::index_of(list_of_ints->view(), *skey, FIND_LAST), - "Type/Scale of search key does not match list column element type."); + EXPECT_THROW(lists::contains(list_of_ints->view(), *skey), cudf::logic_error); + EXPECT_THROW(lists::index_of(list_of_ints->view(), *skey, FIND_FIRST), cudf::logic_error); + EXPECT_THROW(lists::index_of(list_of_ints->view(), *skey, FIND_LAST), cudf::logic_error); } } @@ -551,19 +553,19 @@ TYPED_TEST(TypedVectorContainsTest, VectorKeysWithNoNulls) { // CONTAINS auto result = lists::contains(search_space->view(), search_key); - auto expected = bools{1, 0, 0, 1, 1, 0, 0, 0, 1, 0}; + auto expected = bools_col{1, 0, 0, 1, 1, 0, 0, 0, 1, 0}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), search_key, FIND_FIRST); - auto expected = indices{1, absent, absent, 2, 0, absent, absent, absent, 2, absent}; + auto expected = indices_col{1, ABSENT, ABSENT, 2, 0, ABSENT, ABSENT, ABSENT, 2, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), search_key, FIND_LAST); - auto expected = indices{3, absent, absent, 4, 0, absent, absent, absent, 3, absent}; + auto expected = indices_col{3, ABSENT, ABSENT, 4, 0, ABSENT, ABSENT, ABSENT, 3, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -593,21 +595,21 @@ TYPED_TEST(TypedVectorContainsTest, VectorWithNullLists) { // CONTAINS auto result = lists::contains(search_space->view(), search_keys); - auto expected = bools{{1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0}, nulls_at({3, 10})}; + auto expected = bools_col{{1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0}, nulls_at({3, 10})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST - auto result = lists::index_of(search_space->view(), search_keys, FIND_FIRST); - auto expected = - indices{{1, absent, absent, x, absent, 1, absent, absent, absent, 0, x}, nulls_at({3, 10})}; + auto result = lists::index_of(search_space->view(), search_keys, FIND_FIRST); + auto expected = indices_col{{1, ABSENT, ABSENT, X, ABSENT, 1, ABSENT, ABSENT, ABSENT, 0, X}, + nulls_at({3, 10})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST - auto result = lists::index_of(search_space->view(), search_keys, FIND_LAST); - auto expected = - indices{{3, absent, absent, x, absent, 1, absent, absent, absent, 0, x}, nulls_at({3, 10})}; + auto result = lists::index_of(search_space->view(), search_keys, FIND_LAST); + auto expected = indices_col{{3, ABSENT, ABSENT, X, ABSENT, 1, ABSENT, ABSENT, ABSENT, 0, X}, + nulls_at({3, 10})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -617,29 +619,29 @@ TYPED_TEST(TypedVectorContainsTest, VectorNonNullListsWithNullValues) // Test List columns that have no NULL list rows, but NULL elements in some list rows. using T = TypeParam; - auto numerals = fixed_width_column_wrapper{{x, 1, 2, x, 4, 5, x, 7, 8, x, x, 1, 2, x, 1}, + auto numerals = fixed_width_column_wrapper{{X, 1, 2, X, 4, 5, X, 7, 8, X, X, 1, 2, X, 1}, nulls_at({0, 3, 6, 9, 10, 13})}; auto search_space = make_lists_column( - 8, indices{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), numerals.release(), 0, {}); + 8, indices_col{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), numerals.release(), 0, {}); // Search space: [ [x], [1,2], [x,4,5,x], [], [], [7,8,x], [x], [1,2,x,1] ] auto search_keys = fixed_width_column_wrapper{1, 2, 3, 1, 2, 3, 1, 1}; { // CONTAINS auto result = lists::contains(search_space->view(), search_keys); - auto expected = bools{0, 1, 0, 0, 0, 0, 0, 1}; + auto expected = bools_col{0, 1, 0, 0, 0, 0, 0, 1}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), search_keys, FIND_FIRST); - auto expected = indices{absent, 1, absent, absent, absent, absent, absent, 0}; + auto expected = indices_col{ABSENT, 1, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 0}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), search_keys, FIND_LAST); - auto expected = indices{absent, 1, absent, absent, absent, absent, absent, 3}; + auto expected = indices_col{ABSENT, 1, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 3}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -648,14 +650,14 @@ TYPED_TEST(TypedVectorContainsTest, VectorWithNullsInLists) { using T = TypeParam; - auto numerals = fixed_width_column_wrapper{{x, 1, 2, x, 4, 5, x, 7, 8, x, x, 1, 2, x, 1}, + auto numerals = fixed_width_column_wrapper{{X, 1, 2, X, 4, 5, X, 7, 8, X, X, 1, 2, X, 1}, nulls_at({0, 3, 6, 9, 10, 13})}; auto input_null_mask_iter = null_at(4); auto search_space = make_lists_column( 8, - indices{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), + indices_col{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), numerals.release(), 1, cudf::test::detail::make_null_mask(input_null_mask_iter, input_null_mask_iter + 8)); @@ -665,19 +667,19 @@ TYPED_TEST(TypedVectorContainsTest, VectorWithNullsInLists) { // CONTAINS auto result = lists::contains(search_space->view(), search_keys); - auto expected = bools{{0, 1, 0, 0, x, 0, 0, 1}, null_at(4)}; + auto expected = bools_col{{0, 1, 0, 0, X, 0, 0, 1}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), search_keys, FIND_FIRST); - auto expected = indices{{absent, 1, absent, absent, x, absent, absent, 0}, null_at(4)}; + auto expected = indices_col{{ABSENT, 1, ABSENT, ABSENT, X, ABSENT, ABSENT, 0}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), search_keys, FIND_LAST); - auto expected = indices{{absent, 1, absent, absent, x, absent, absent, 3}, null_at(4)}; + auto expected = indices_col{{ABSENT, 1, ABSENT, ABSENT, X, ABSENT, ABSENT, 3}, null_at(4)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -686,36 +688,36 @@ TYPED_TEST(TypedVectorContainsTest, ListContainsVectorWithNullsInListsAndInSearc { using T = TypeParam; - auto numerals = fixed_width_column_wrapper{{x, 1, 2, x, 4, 5, x, 7, 8, x, x, 1, 2, x, 1}, + auto numerals = fixed_width_column_wrapper{{X, 1, 2, X, 4, 5, X, 7, 8, X, X, 1, 2, X, 1}, nulls_at({0, 3, 6, 9, 10, 13})}; auto input_null_mask_iter = null_at(4); auto search_space = make_lists_column( 8, - indices{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), + indices_col{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), numerals.release(), 1, cudf::test::detail::make_null_mask(input_null_mask_iter, input_null_mask_iter + 8)); // Search space: [ [x], [1,2], [x,4,5,x], [], x, [7,8,x], [x], [1,2,x,1] ] - auto search_keys = fixed_width_column_wrapper{{1, 2, 3, x, 2, 3, 1, 1}, null_at(3)}; + auto search_keys = fixed_width_column_wrapper{{1, 2, 3, X, 2, 3, 1, 1}, null_at(3)}; { // CONTAINS auto result = lists::contains(search_space->view(), search_keys); - auto expected = bools{{0, 1, 0, x, x, 0, 0, 1}, nulls_at({3, 4})}; + auto expected = bools_col{{0, 1, 0, X, X, 0, 0, 1}, nulls_at({3, 4})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), search_keys, FIND_FIRST); - auto expected = indices{{absent, 1, absent, x, x, absent, absent, 0}, nulls_at({3, 4})}; + auto expected = indices_col{{ABSENT, 1, ABSENT, X, X, ABSENT, ABSENT, 0}, nulls_at({3, 4})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), search_keys, FIND_LAST); - auto expected = indices{{absent, 1, absent, x, x, absent, absent, 3}, nulls_at({3, 4})}; + auto expected = indices_col{{ABSENT, 1, ABSENT, X, X, ABSENT, ABSENT, 3}, nulls_at({3, 4})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -724,37 +726,37 @@ TEST_F(ContainsTest, BoolKeyVectorWithNullsInListsAndInSearchKeys) { using T = bool; - auto numerals = fixed_width_column_wrapper{{x, 0, 1, x, 1, 1, x, 1, 1, x, x, 0, 1, x, 1}, + auto numerals = fixed_width_column_wrapper{{X, 0, 1, X, 1, 1, X, 1, 1, X, X, 0, 1, X, 1}, nulls_at({0, 3, 6, 9, 10, 13})}; auto input_null_mask_iter = null_at(4); auto search_space = make_lists_column( 8, - indices{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), + indices_col{0, 1, 3, 7, 7, 7, 10, 11, 15}.release(), numerals.release(), 1, cudf::test::detail::make_null_mask(input_null_mask_iter, input_null_mask_iter + 8)); - auto search_keys = fixed_width_column_wrapper{{0, 1, 0, x, 0, 0, 1, 1}, null_at(3)}; + auto search_keys = fixed_width_column_wrapper{{0, 1, 0, X, 0, 0, 1, 1}, null_at(3)}; // Search space: [ [x], [0,1], [x,1,1,x], [], x, [1,1,x], [x], [0,1,x,1] ] // Search keys : [ 0, 1, 0, x, 0, 0, 1, 1 ] { // CONTAINS auto result = lists::contains(search_space->view(), search_keys); - auto expected = bools{{0, 1, 0, x, x, 0, 0, 1}, nulls_at({3, 4})}; + auto expected = bools_col{{0, 1, 0, X, X, 0, 0, 1}, nulls_at({3, 4})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), search_keys, FIND_FIRST); - auto expected = indices{{absent, 1, absent, x, x, absent, absent, 1}, nulls_at({3, 4})}; + auto expected = indices_col{{ABSENT, 1, ABSENT, X, X, ABSENT, ABSENT, 1}, nulls_at({3, 4})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), search_keys, FIND_LAST); - auto expected = indices{{absent, 1, absent, x, x, absent, absent, 3}, nulls_at({3, 4})}; + auto expected = indices_col{{ABSENT, 1, ABSENT, X, X, ABSENT, ABSENT, 3}, nulls_at({3, 4})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -780,19 +782,19 @@ TEST_F(ContainsTest, StringKeyVectorWithNullsInListsAndInSearchKeys) { // CONTAINS auto result = lists::contains(search_space->view(), search_keys); - auto expected = bools{{0, 1, 0, x, x, 0, 0, 1}, nulls_at({3, 4})}; + auto expected = bools_col{{0, 1, 0, X, X, 0, 0, 1}, nulls_at({3, 4})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), search_keys, FIND_FIRST); - auto expected = indices{{absent, 1, absent, x, x, absent, absent, 0}, nulls_at({3, 4})}; + auto expected = indices_col{{ABSENT, 1, ABSENT, X, X, ABSENT, ABSENT, 0}, nulls_at({3, 4})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), search_keys, FIND_LAST); - auto expected = indices{{absent, 1, absent, x, x, absent, absent, 3}, nulls_at({3, 4})}; + auto expected = indices_col{{ABSENT, 1, ABSENT, X, X, ABSENT, ABSENT, 3}, nulls_at({3, 4})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -807,12 +809,9 @@ TEST_F(ContainsTest, VectorTypeRelatedExceptions) {{1, 2, 3}, {4, 5, 6}}}.release(); auto skey = fixed_width_column_wrapper{0, 1, 2}; - CUDF_EXPECT_THROW_MESSAGE(lists::contains(list_of_lists->view(), skey), - "Nested types not supported in list search operations."); - CUDF_EXPECT_THROW_MESSAGE(lists::index_of(list_of_lists->view(), skey, FIND_FIRST), - "Nested types not supported in list search operations."); - CUDF_EXPECT_THROW_MESSAGE(lists::index_of(list_of_lists->view(), skey, FIND_LAST), - "Nested types not supported in list search operations."); + EXPECT_THROW(lists::contains(list_of_lists->view(), skey), cudf::logic_error); + EXPECT_THROW(lists::index_of(list_of_lists->view(), skey, FIND_FIRST), cudf::logic_error); + EXPECT_THROW(lists::index_of(list_of_lists->view(), skey, FIND_LAST), cudf::logic_error); } { // Search key must match list elements in type. @@ -823,23 +822,17 @@ TEST_F(ContainsTest, VectorTypeRelatedExceptions) } .release(); auto skey = strings_column_wrapper{"Hello", "World"}; - CUDF_EXPECT_THROW_MESSAGE(lists::contains(list_of_ints->view(), skey), - "Type/Scale of search key does not match list column element type."); - CUDF_EXPECT_THROW_MESSAGE(lists::index_of(list_of_ints->view(), skey, FIND_FIRST), - "Type/Scale of search key does not match list column element type."); - CUDF_EXPECT_THROW_MESSAGE(lists::index_of(list_of_ints->view(), skey, FIND_LAST), - "Type/Scale of search key does not match list column element type."); + EXPECT_THROW(lists::contains(list_of_ints->view(), skey), cudf::logic_error); + EXPECT_THROW(lists::index_of(list_of_ints->view(), skey, FIND_FIRST), cudf::logic_error); + EXPECT_THROW(lists::index_of(list_of_ints->view(), skey, FIND_LAST), cudf::logic_error); } { // Search key column size must match lists column size. auto list_of_ints = lists_column_wrapper{{0, 1, 2}, {3, 4, 5}, {6, 7, 8}}.release(); auto skey = fixed_width_column_wrapper{0, 1, 2, 3}; - CUDF_EXPECT_THROW_MESSAGE(lists::contains(list_of_ints->view(), skey), - "Number of search keys must match list column size."); - CUDF_EXPECT_THROW_MESSAGE(lists::index_of(list_of_ints->view(), skey, FIND_FIRST), - "Number of search keys must match list column size."); - CUDF_EXPECT_THROW_MESSAGE(lists::index_of(list_of_ints->view(), skey, FIND_LAST), - "Number of search keys must match list column size."); + EXPECT_THROW(lists::contains(list_of_ints->view(), skey), cudf::logic_error); + EXPECT_THROW(lists::index_of(list_of_ints->view(), skey, FIND_FIRST), cudf::logic_error); + EXPECT_THROW(lists::index_of(list_of_ints->view(), skey, FIND_LAST), cudf::logic_error); } } @@ -887,19 +880,21 @@ TYPED_TEST(TypedContainsNaNsTest, ListWithNaNsScalar) { // CONTAINS auto result = lists::contains(search_space->view(), *search_key_nan); - auto expected = bools{0, 0, 0, 0, 1, 0, 1, 0, 0, 0}; + auto expected = bools_col{0, 0, 0, 0, 1, 0, 1, 0, 0, 0}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST - auto result = lists::index_of(search_space->view(), *search_key_nan, FIND_FIRST); - auto expected = indices{absent, absent, absent, absent, 0, absent, 1, absent, absent, absent}; + auto result = lists::index_of(search_space->view(), *search_key_nan, FIND_FIRST); + auto expected = + indices_col{ABSENT, ABSENT, ABSENT, ABSENT, 0, ABSENT, 1, ABSENT, ABSENT, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST - auto result = lists::index_of(search_space->view(), *search_key_nan, FIND_LAST); - auto expected = indices{absent, absent, absent, absent, 0, absent, 1, absent, absent, absent}; + auto result = lists::index_of(search_space->view(), *search_key_nan, FIND_LAST); + auto expected = + indices_col{ABSENT, ABSENT, ABSENT, ABSENT, 0, ABSENT, 1, ABSENT, ABSENT, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -944,21 +939,21 @@ TYPED_TEST(TypedContainsNaNsTest, ListWithNaNsContainsVector) { // CONTAINS auto result = lists::contains(search_space->view(), search_keys->view()); - auto expected = bools{{1, 0, 0, 0, 1, 0, 1, 0, 1, 0}, null_at(2)}; + auto expected = bools_col{{1, 0, 0, 0, 1, 0, 1, 0, 1, 0}, null_at(2)}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), search_keys->view(), FIND_FIRST); auto expected = - indices{{1, absent, x, absent, 0, absent, 2, absent, 1, absent}, nulls_at({2})}; + indices_col{{1, ABSENT, X, ABSENT, 0, ABSENT, 2, ABSENT, 1, ABSENT}, nulls_at({2})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), search_keys->view(), FIND_LAST); auto expected = - indices{{1, absent, x, absent, 0, absent, 2, absent, 1, absent}, nulls_at({2})}; + indices_col{{1, ABSENT, X, ABSENT, 0, ABSENT, 2, ABSENT, 1, ABSENT}, nulls_at({2})}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -969,19 +964,19 @@ TYPED_TEST(TypedContainsNaNsTest, ListWithNaNsContainsVector) { // CONTAINS auto result = lists::contains(search_space->view(), search_keys->view()); - auto expected = bools{1, 0, 0, 0, 1, 0, 1, 0, 1, 0}; + auto expected = bools_col{1, 0, 0, 0, 1, 0, 1, 0, 1, 0}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), search_keys->view(), FIND_FIRST); - auto expected = indices{1, absent, absent, absent, 0, absent, 2, absent, 1, absent}; + auto expected = indices_col{1, ABSENT, ABSENT, ABSENT, 0, ABSENT, 2, ABSENT, 1, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), search_keys->view(), FIND_LAST); - auto expected = indices{1, absent, absent, absent, 0, absent, 2, absent, 1, absent}; + auto expected = indices_col{1, ABSENT, ABSENT, ABSENT, 0, ABSENT, 2, ABSENT, 1, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -1002,7 +997,7 @@ TYPED_TEST(TypedContainsDecimalsTest, ScalarKey) 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3}; auto decimals = fixed_point_column_wrapper{ values.begin(), values.end(), numeric::scale_type{0}}; - auto list_offsets = indices{0, 3, 6, 9, 12, 15, 18, 21, 21, 24, 24}; + auto list_offsets = indices_col{0, 3, 6, 9, 12, 15, 18, 21, 21, 24, 24}; return make_lists_column(10, list_offsets.release(), decimals.release(), 0, {}); }(); auto search_key_one = make_fixed_point_scalar(typename T::rep{1}, numeric::scale_type{0}); @@ -1011,19 +1006,19 @@ TYPED_TEST(TypedContainsDecimalsTest, ScalarKey) { // CONTAINS auto result = lists::contains(search_space->view(), *search_key_one); - auto expected = bools{1, 0, 0, 1, 0, 0, 0, 0, 1, 0}; + auto expected = bools_col{1, 0, 0, 1, 0, 0, 0, 0, 1, 0}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), *search_key_one, FIND_FIRST); - auto expected = indices{1, absent, absent, 2, absent, absent, absent, absent, 0, absent}; + auto expected = indices_col{1, ABSENT, ABSENT, 2, ABSENT, ABSENT, ABSENT, ABSENT, 0, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), *search_key_one, FIND_LAST); - auto expected = indices{1, absent, absent, 2, absent, absent, absent, absent, 0, absent}; + auto expected = indices_col{1, ABSENT, ABSENT, 2, ABSENT, ABSENT, ABSENT, ABSENT, 0, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } @@ -1037,7 +1032,7 @@ TYPED_TEST(TypedContainsDecimalsTest, VectorKey) 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3}; auto decimals = fixed_point_column_wrapper{ values.begin(), values.end(), numeric::scale_type{0}}; - auto list_offsets = indices{0, 3, 6, 9, 12, 15, 18, 21, 21, 24, 24}; + auto list_offsets = indices_col{0, 3, 6, 9, 12, 15, 18, 21, 21, 24, 24}; return make_lists_column(10, list_offsets.release(), decimals.release(), 0, {}); }(); @@ -1051,19 +1046,781 @@ TYPED_TEST(TypedContainsDecimalsTest, VectorKey) { // CONTAINS auto result = lists::contains(search_space->view(), search_key->view()); - auto expected = bools{1, 0, 0, 1, 1, 0, 0, 0, 1, 0}; + auto expected = bools_col{1, 0, 0, 1, 1, 0, 0, 0, 1, 0}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_FIRST auto result = lists::index_of(search_space->view(), search_key->view(), FIND_FIRST); - auto expected = indices{1, absent, absent, 2, 0, absent, absent, absent, 2, absent}; + auto expected = indices_col{1, ABSENT, ABSENT, 2, 0, ABSENT, ABSENT, ABSENT, 2, ABSENT}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } { // FIND_LAST auto result = lists::index_of(search_space->view(), search_key->view(), FIND_LAST); - auto expected = indices{1, absent, absent, 2, 0, absent, absent, absent, 2, absent}; + auto expected = indices_col{1, ABSENT, ABSENT, 2, 0, ABSENT, ABSENT, ABSENT, 2, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } +} + +template +struct TypedStructContainsTest : public ContainsTest { +}; +TYPED_TEST_SUITE(TypedStructContainsTest, ContainsTestTypes); + +TYPED_TEST(TypedStructContainsTest, EmptyInputTest) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const lists = [] { + auto offsets = indices_col{}; + auto data = tdata_col{}; + auto child = structs_col{{data}}; + return make_lists_column(0, offsets.release(), child.release(), 0, {}); + }(); + + auto const scalar_key = [] { + auto child = tdata_col{0}; + return make_struct_scalar(child); + }(); + auto const column_key = [] { + auto child = tdata_col{}; + return structs_col{{child}}; + }(); + + auto const result1 = lists::contains(lists->view(), scalar_key); + auto const result2 = lists::contains(lists->view(), column_key); + auto const expected = bools_col{}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result1); + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result2); +} + +TYPED_TEST(TypedStructContainsTest, ScalarKeyNoNullLists) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const lists = [] { + auto offsets = indices_col{0, 4, 7, 10, 15, 18, 21, 24, 24, 28, 28}; + // clang-format off + auto data1 = tdata_col{0, 1, 2, 1, + 3, 4, 5, + 6, 7, 8, + 9, 0, 1, 3, 1, + 2, 3, 4, + 5, 6, 7, + 8, 9, 0, + 1, 2, 1, 3 + }; + auto data2 = tdata_col{0, 1, 2, 3, + 0, 1, 2, + 0, 1, 2, + 1, 1, 2, 2, 2, + 0, 1, 2, + 0, 1, 2, + 0, 1, 2, + 1, 0, 1, 1 + }; + // clang-format on + auto child = structs_col{{data1, data2}}; + return make_lists_column(10, offsets.release(), child.release(), 0, {}); + }(); + + auto const key = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{1}; + return make_struct_scalar(child1, child2); + }(); + + { + // CONTAINS + auto const result = lists::contains(lists->view(), key); + auto const expected = bools_col{1, 0, 0, 0, 0, 0, 0, 0, 1, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // CONTAINS NULLS + auto const result = lists::contains_nulls(lists->view()); + auto const expected = bools_col{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists->view(), key, FIND_FIRST); + auto const expected = + indices_col{1, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 0, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists->view(), key, FIND_LAST); + auto const expected = + indices_col{1, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 2, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } +} + +TYPED_TEST(TypedStructContainsTest, ScalarKeyWithNullLists) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const lists = [] { + auto offsets = indices_col{0, 4, 7, 10, 10, 15, 18, 21, 24, 24, 28, 28}; + // clang-format off + auto data1 = tdata_col{0, 1, 2, 1, + 3, 4, 5, + 6, 7, 8, + 9, 0, 1, 3, 1, + 2, 3, 4, + 5, 6, 7, + 8, 9, 0, + 1, 2, 1, 3 + }; + auto data2 = tdata_col{0, 1, 2, 3, + 0, 1, 2, + 0, 1, 2, + 1, 1, 2, 2, 2, + 0, 1, 2, + 0, 1, 2, + 0, 1, 2, + 1, 0, 1, 1 + }; + // clang-format on + auto child = structs_col{{data1, data2}}; + auto const validity_iter = nulls_at({3, 10}); + return make_lists_column(11, + offsets.release(), + child.release(), + 2, + detail::make_null_mask(validity_iter, validity_iter + 11)); + }(); + + auto const key = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{1}; + return make_struct_scalar(child1, child2); + }(); + + { + // CONTAINS + auto const result = lists::contains(lists->view(), key); + auto const expected = bools_col{{1, 0, 0, X, 0, 0, 0, 0, 0, 1, X}, nulls_at({3, 10})}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // CONTAINS NULLS + auto const result = lists::contains_nulls(lists->view()); + auto const expected = bools_col{{0, 0, 0, X, 0, 0, 0, 0, 0, 0, X}, nulls_at({3, 10})}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists->view(), key, FIND_FIRST); + auto const expected = indices_col{ + {1, ABSENT, ABSENT, X, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 0, X}, nulls_at({3, 10})}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists->view(), key, FIND_LAST); + auto const expected = indices_col{ + {1, ABSENT, ABSENT, X, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 2, X}, nulls_at({3, 10})}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } +} + +TYPED_TEST(TypedStructContainsTest, SlicedListsColumnNoNulls) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const lists_original = [] { + auto offsets = indices_col{0, 4, 7, 10, 15, 18, 21, 24, 24, 28, 28}; + // clang-format off + auto data1 = tdata_col{0, 1, 2, 1, + 3, 4, 5, + 6, 7, 8, + 9, 0, 1, 3, 1, + 2, 3, 4, + 5, 6, 7, + 8, 9, 0, + 1, 2, 1, 3 + }; + auto data2 = tdata_col{0, 1, 2, 3, + 0, 1, 2, + 0, 1, 2, + 1, 1, 2, 2, 2, + 0, 1, 2, + 0, 1, 2, + 0, 1, 2, + 1, 0, 1, 1 + }; + // clang-format on + auto child = structs_col{{data1, data2}}; + return make_lists_column(10, offsets.release(), child.release(), 0, {}); + }(); + auto const lists = cudf::slice(lists_original->view(), {3, 10})[0]; + + auto const key = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{1}; + return make_struct_scalar(child1, child2); + }(); + + { + // CONTAINS + auto const result = lists::contains(lists, key); + auto const expected = bools_col{0, 0, 0, 0, 0, 1, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // CONTAINS NULLS + auto const result = lists::contains_nulls(lists); + auto const expected = bools_col{0, 0, 0, 0, 0, 0, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists, key, FIND_FIRST); + auto const expected = indices_col{ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 0, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists, key, FIND_LAST); + auto const expected = indices_col{ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 2, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } +} + +TYPED_TEST(TypedStructContainsTest, ScalarKeyNoNullListsWithNullStructs) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const lists = [] { + auto offsets = indices_col{0, 4, 7, 10, 15, 18, 21, 24, 24, 28, 28}; + // clang-format off + auto data1 = tdata_col{0, X, 2, 1, + 3, 4, 5, + 6, 7, 8, + X, 0, 1, 3, 1, + X, 3, 4, + 5, 6, 7, + 8, 9, 0, + X, 2, 1, 3 + }; + auto data2 = tdata_col{0, X, 2, 1, + 0, 1, 2, + 0, 1, 2, + X, 1, 2, 2, 2, + X, 1, 2, + 0, 1, 2, + 0, 1, 2, + X, 0, 1, 1 + }; + // clang-format on + auto child = structs_col{{data1, data2}, nulls_at({1, 10, 15, 24})}; + return make_lists_column(10, offsets.release(), child.release(), 0, {}); + }(); + + auto const key = [] { + auto child1 = tdata_col{1}; + auto child2 = tdata_col{1}; + return make_struct_scalar(child1, child2); + }(); + + { + // CONTAINS + auto const result = lists::contains(lists->view(), key); + auto const expected = bools_col{1, 0, 0, 0, 0, 0, 0, 0, 1, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // CONTAINS NULLS + auto const result = lists::contains_nulls(lists->view()); + auto const expected = bools_col{1, 0, 0, 1, 1, 0, 0, 0, 1, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists->view(), key, FIND_FIRST); + auto const expected = + indices_col{3, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 2, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists->view(), key, FIND_LAST); + auto const expected = + indices_col{3, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 2, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } +} + +TYPED_TEST(TypedStructContainsTest, ColumnKeyNoNullLists) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const lists = [] { + auto offsets = indices_col{0, 4, 7, 10, 15, 18, 21, 24, 24, 28, 28}; + // clang-format off + auto data1 = tdata_col{0, 1, 2, 1, + 3, 4, 3, + 6, 7, 8, + 9, 0, 1, 3, 1, + 2, 3, 4, + 5, 6, 7, + 8, 9, 0, + 1, 2, 1, 3 + }; + auto data2 = tdata_col{0, 1, 2, 3, + 0, 0, 0, + 0, 1, 2, + 1, 1, 2, 2, 2, + 0, 1, 2, + 0, 1, 2, + 0, 1, 2, + 1, 0, 1, 1 + }; + // clang-format on + auto child = structs_col{{data1, data2}}; + return make_lists_column(10, offsets.release(), child.release(), 0, {}); + }(); + + auto const keys = [] { + auto child1 = tdata_col{1, 3, 1, 1, 2, 1, 0, 0, 1, 0}; + auto child2 = tdata_col{1, 0, 1, 1, 2, 1, 0, 0, 1, 0}; + return structs_col{{child1, child2}}; + }(); + + { + // CONTAINS + auto const result = lists::contains(lists->view(), keys); + auto const expected = bools_col{1, 1, 0, 0, 0, 0, 0, 0, 1, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists->view(), keys, FIND_FIRST); + auto const expected = + indices_col{1, 0, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 0, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists->view(), keys, FIND_LAST); + auto const expected = + indices_col{1, 2, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, ABSENT, 2, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } +} + +TYPED_TEST(TypedStructContainsTest, ColumnKeyWithSlicedListsNoNulls) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const lists_original = [] { + auto offsets = indices_col{0, 4, 7, 10, 15, 18, 21, 24, 24, 28, 28}; + // clang-format off + auto data1 = tdata_col{0, 1, 2, 1, + 3, 4, 3, + 6, 7, 8, + 9, 0, 1, 3, 1, + 2, 3, 4, + 5, 6, 7, + 8, 9, 0, + 1, 2, 1, 3 + }; + auto data2 = tdata_col{0, 1, 2, 3, + 0, 0, 0, + 0, 1, 2, + 1, 1, 2, 2, 2, + 0, 1, 2, + 0, 1, 2, + 0, 1, 2, + 1, 0, 1, 1 + }; + // clang-format on + auto child = structs_col{{data1, data2}}; + return make_lists_column(10, offsets.release(), child.release(), 0, {}); + }(); + + auto const keys_original = [] { + auto child1 = tdata_col{1, 9, 1, 6, 2, 1, 0, 0, 1, 0}; + auto child2 = tdata_col{1, 1, 1, 1, 2, 1, 0, 0, 1, 0}; + return structs_col{{child1, child2}}; + }(); + + auto const lists = cudf::slice(lists_original->view(), {3, 7})[0]; + auto const keys = cudf::slice(keys_original, {1, 5})[0]; + + { + // CONTAINS + auto const result = lists::contains(lists, keys); + auto const expected = bools_col{1, 0, 1, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists, keys, FIND_FIRST); + auto const expected = indices_col{0, ABSENT, 1, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists, keys, FIND_LAST); + auto const expected = indices_col{0, ABSENT, 1, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } +} + +TYPED_TEST(TypedStructContainsTest, ColumnKeyWithSlicedListsHavingNulls) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const lists_original = [] { + auto offsets = indices_col{0, 4, 7, 10, 10, 15, 18, 21, 24, 24, 28, 28}; + // clang-format off + auto data1 = tdata_col{0, X, 2, 1, + 3, 4, 5, + 6, 7, 8, + X, 0, 1, 3, 1, + X, 3, 4, + 5, 6, 6, + 8, 9, 0, + X, 2, 1, 3 + }; + auto data2 = tdata_col{0, X, 2, 1, + 0, 1, 2, + 0, 1, 2, + X, 1, 2, 2, 2, + X, 1, 2, + 0, 1, 1, + 0, 1, 2, + X, 0, 1, 1 + }; + // clang-format on + auto child = structs_col{{data1, data2}, nulls_at({1, 10, 15, 24})}; + auto const validity_iter = nulls_at({3, 10}); + return make_lists_column(11, + offsets.release(), + child.release(), + 2, + detail::make_null_mask(validity_iter, validity_iter + 11)); + }(); + + auto const keys_original = [] { + auto child1 = tdata_col{{1, X, 1, 6, X, 1, 0, 0, 1, 0, 1}, null_at(4)}; + auto child2 = tdata_col{{1, X, 1, 1, X, 1, 0, 0, 1, 0, 1}, null_at(4)}; + return structs_col{{child1, child2}, null_at(1)}; + }(); + + auto const lists = cudf::slice(lists_original->view(), {4, 8})[0]; + auto const keys = cudf::slice(keys_original, {1, 5})[0]; + + { + // CONTAINS + auto const result = lists::contains(lists, keys); + auto const expected = bools_col{{X, 0, 1, 0}, null_at(0)}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists, keys, FIND_FIRST); + auto const expected = indices_col{{X, ABSENT, 1, ABSENT}, null_at(0)}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists, keys, FIND_LAST); + auto const expected = indices_col{{X, ABSENT, 2, ABSENT}, null_at(0)}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } +} + +template +struct TypedListContainsTest : public ContainsTest { +}; +TYPED_TEST_SUITE(TypedListContainsTest, ContainsTestTypes); + +TYPED_TEST(TypedListContainsTest, ScalarKeyLists) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + using lists_col = cudf::test::lists_column_wrapper; + + auto const lists_no_nulls = lists_col{lists_col{{0, 1, 2}, // list0 + {3, 4, 5}, + {0, 1, 2}, + {9, 0, 1, 3, 1}}, + lists_col{{2, 3, 4}, // list1 + {3, 4, 5}, + {8, 9, 0}, + {}}, + lists_col{{0, 2, 1}, // list2 + {}}}; + + auto const lists_have_nulls = lists_col{lists_col{{{0, 1, 2}, // list0 + {} /*NULL*/, + {0, 1, 2}, + {9, 0, 1, 3, 1}}, + null_at(1)}, + lists_col{{{} /*NULL*/, // list1 + {3, 4, 5}, + {8, 9, 0}, + {}}, + null_at(0)}, + lists_col{{0, 2, 1}, // list2 + {}}}; + + auto const key = [] { + auto const child = tdata_col{0, 1, 2}; + return list_scalar(child); + }(); + + auto const do_test = [&](auto const& lists, bool has_nulls) { + { + // CONTAINS + auto const result = lists::contains(lists_column_view{lists}, key); + auto const expected = bools_col{1, 0, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // CONTAINS NULLS + auto const result = lists::contains_nulls(lists_column_view{lists}); + auto const expected = has_nulls ? bools_col{1, 1, 0} : bools_col{0, 0, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists_column_view{lists}, key, FIND_FIRST); + auto const expected = indices_col{0, ABSENT, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists_column_view{lists}, key, FIND_LAST); + auto const expected = indices_col{2, ABSENT, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + }; + + do_test(lists_no_nulls, false); + do_test(lists_have_nulls, true); +} + +TYPED_TEST(TypedListContainsTest, SlicedListsColumn) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + using lists_col = cudf::test::lists_column_wrapper; + + auto const lists_no_nulls_original = lists_col{lists_col{{0, 0, 0}, // list-2 (don't care) + {0, 1, 2}, + {0, 1, 2}, + {0, 0, 0}}, + lists_col{{0, 0, 0}, // list-1 (don't care) + {0, 1, 2}, + {0, 1, 2}, + {0, 0, 0}}, + lists_col{{0, 1, 2}, // list0 + {3, 4, 5}, + {0, 1, 2}, + {9, 0, 1, 3, 1}}, + lists_col{{2, 3, 4}, // list1 + {3, 4, 5}, + {8, 9, 0}, + {}}, + lists_col{{0, 2, 1}, // list2 + {}}, + lists_col{{0, 0, 0}, // list3 (don't care) + {0, 1, 2}, + {0, 1, 2}, + {0, 0, 0}}, + lists_col{{0, 0, 0}, // list4 (don't care) + {0, 1, 2}, + {0, 1, 2}, + {0, 0, 0}}}; + + auto const lists_have_nulls_original = lists_col{lists_col{{0, 0, 0}, // list-1 (don't care) + {0, 1, 2}, + {0, 1, 2}, + {0, 0, 0}}, + lists_col{{{0, 1, 2}, // list0 + {} /*NULL*/, + {0, 1, 2}, + {9, 0, 1, 3, 1}}, + null_at(1)}, + lists_col{{{} /*NULL*/, // list1 + {3, 4, 5}, + {8, 9, 0}, + {}}, + null_at(0)}, + lists_col{{0, 2, 1}, // list2 + {}}, + lists_col{{0, 0, 0}, // list3 (don't care) + {0, 1, 2}, + {0, 1, 2}, + {0, 0, 0}}}; + + auto const lists_no_nulls = cudf::slice(lists_no_nulls_original, {2, 5})[0]; + auto const lists_have_nulls = cudf::slice(lists_have_nulls_original, {1, 4})[0]; + + auto const key = [] { + auto const child = tdata_col{0, 1, 2}; + return list_scalar(child); + }(); + + auto const do_test = [&](auto const& lists, bool has_nulls) { + { + // CONTAINS + auto const result = lists::contains(lists_column_view{lists}, key); + auto const expected = bools_col{1, 0, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // CONTAINS NULLS + auto const result = lists::contains_nulls(lists_column_view{lists}); + auto const expected = has_nulls ? bools_col{1, 1, 0} : bools_col{0, 0, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists_column_view{lists}, key, FIND_FIRST); + auto const expected = indices_col{0, ABSENT, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists_column_view{lists}, key, FIND_LAST); + auto const expected = indices_col{2, ABSENT, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + }; + + do_test(lists_no_nulls, false); + do_test(lists_have_nulls, true); +} + +TYPED_TEST(TypedListContainsTest, ColumnKeyLists) +{ + using lists_col = cudf::test::lists_column_wrapper; + auto constexpr null = int32_t{0}; + + auto const lists_no_nulls = lists_col{lists_col{{0, 0, 2}, // list0 + {3, 4, 5}, + {0, 0, 2}, + {9, 0, 1, 3, 1}}, + lists_col{{2, 3, 4}, // list1 + {3, 4, 5}, + {2, 3, 4}, + {}}, + lists_col{{0, 2, 0}, // list2 + {0, 2, 0}, + {3, 4, 5}, + {}}}; + + auto const lists_have_nulls = lists_col{lists_col{{lists_col{{0, null, 2}, null_at(1)}, // list0 + lists_col{} /*NULL*/, + lists_col{{0, null, 2}, null_at(1)}, + lists_col{9, 0, 1, 3, 1}}, + null_at(1)}, + lists_col{{lists_col{} /*NULL*/, // list1 + lists_col{3, 4, 5}, + lists_col{2, 3, 4}, + lists_col{}}, + null_at(0)}, + lists_col{lists_col{0, 2, 1}, // list2 + lists_col{{0, 2, null}, null_at(2)}, + lists_col{3, 4, 5}, + lists_col{}}}; + + auto const key = lists_col{ + lists_col{{0, null, 2}, null_at(1)}, lists_col{2, 3, 4}, lists_col{{0, 2, null}, null_at(2)}}; + + auto const do_test = [&](auto const& lists, bool has_nulls) { + { + // CONTAINS + auto const result = lists::contains(lists_column_view{lists}, key); + auto const expected = has_nulls ? bools_col{1, 1, 1} : bools_col{0, 1, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // CONTAINS NULLS + auto const result = lists::contains_nulls(lists_column_view{lists}); + auto const expected = has_nulls ? bools_col{1, 1, 0} : bools_col{0, 0, 0}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists_column_view{lists}, key, FIND_FIRST); + auto const expected = has_nulls ? indices_col{0, 2, 1} : indices_col{ABSENT, 0, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists_column_view{lists}, key, FIND_LAST); + auto const expected = has_nulls ? indices_col{2, 2, 1} : indices_col{ABSENT, 2, ABSENT}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + }; + + do_test(lists_no_nulls, false); + do_test(lists_have_nulls, true); +} + +TYPED_TEST(TypedListContainsTest, ColumnKeyWithListsOfStructsNoNulls) +{ + using tdata_col = cudf::test::fixed_width_column_wrapper; + + auto const lists = [] { + auto child_offsets = indices_col{0, 3, 6, 9, 14, 17, 20, 23, 23}; + // clang-format off + auto data1 = tdata_col{0, 0, 2, + 3, 4, 5, + 0, 0, 2, + 9, 0, 1, 3, 1, + 0, 2, 0, + 0, 0, 2, + 3, 4, 5 + + }; + auto data2 = tdata_col{10, 10, 12, + 13, 14, 15, + 10, 10, 12, + 19, 10, 11, 13, 11, + 10, 12, 10, + 10, 10, 12, + 13, 14, 15 + + }; + // clang-format on + auto structs = structs_col{{data1, data2}}; + auto child = make_lists_column(8, child_offsets.release(), structs.release(), 0, {}); + + auto offsets = indices_col{0, 4, 8}; + return make_lists_column(2, offsets.release(), std::move(child), 0, {}); + }(); + + auto const key = [] { + auto data1 = tdata_col{0, 0, 2}; + auto data2 = tdata_col{10, 10, 12}; + auto const child = structs_col{{data1, data2}}; + return list_scalar(child); + }(); + + { + // CONTAINS + auto const result = lists::contains(lists_column_view{lists->view()}, key); + auto const expected = bools_col{1, 1}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_FIRST + auto const result = lists::index_of(lists_column_view{lists->view()}, key, FIND_FIRST); + auto const expected = indices_col{0, 1}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); + } + { + // FIND_LAST + auto const result = lists::index_of(lists_column_view{lists->view()}, key, FIND_LAST); + auto const expected = indices_col{2, 1}; CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expected, *result); } } From 2c06e512b7d087b97946a2cdcae5ceae5dab8e6c Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Mon, 22 Aug 2022 20:12:50 -0500 Subject: [PATCH 57/58] Add ability to write `list(struct)` columns as `map` type in orc writer (#11568) Resolves #11293 This PR introduces support to write a column of type `ListDtype(StructDtype)` to be written as a `map` type in orc file. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Matthew Roeschke (https://github.com/mroeschke) - Vukasin Milovanovic (https://github.com/vuule) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/11568 --- cpp/include/cudf/io/types.hpp | 2 +- .../cudf/_lib/cpp/lists/lists_column_view.pxd | 7 ++- python/cudf/cudf/_lib/orc.pyx | 50 ++++++++++++---- python/cudf/cudf/io/orc.py | 6 ++ python/cudf/cudf/tests/test_orc.py | 59 +++++++++++++++++++ python/cudf/cudf/utils/ioutils.py | 5 +- 6 files changed, 116 insertions(+), 13 deletions(-) diff --git a/cpp/include/cudf/io/types.hpp b/cpp/include/cudf/io/types.hpp index 3cba2b428ca..838151fbaf9 100644 --- a/cpp/include/cudf/io/types.hpp +++ b/cpp/include/cudf/io/types.hpp @@ -440,7 +440,7 @@ class column_in_metadata { } /** - * @brief Specify that this list column should be encoded as a map in the written parquet file + * @brief Specify that this list column should be encoded as a map in the written file * * The column must have the structure list>. This option is invalid otherwise * diff --git a/python/cudf/cudf/_lib/cpp/lists/lists_column_view.pxd b/python/cudf/cudf/_lib/cpp/lists/lists_column_view.pxd index aa18ede41bd..793f4b8750d 100644 --- a/python/cudf/cudf/_lib/cpp/lists/lists_column_view.pxd +++ b/python/cudf/cudf/_lib/cpp/lists/lists_column_view.pxd @@ -1,6 +1,7 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2020-2022, NVIDIA CORPORATION. from cudf._lib.cpp.column.column_view cimport column_view, mutable_column_view +from cudf._lib.cpp.types cimport size_type cdef extern from "cudf/lists/lists_column_view.hpp" namespace "cudf" nogil: @@ -9,3 +10,7 @@ cdef extern from "cudf/lists/lists_column_view.hpp" namespace "cudf" nogil: column_view parent() except + column_view offsets() except + column_view child() except + + + cdef enum: + offsets_column_index "cudf::lists_column_view::offsets_column_index" + child_column_index "cudf::lists_column_view::child_column_index" diff --git a/python/cudf/cudf/_lib/orc.pyx b/python/cudf/cudf/_lib/orc.pyx index 11c70317a39..66b841fd273 100644 --- a/python/cudf/cudf/_lib/orc.pyx +++ b/python/cudf/cudf/_lib/orc.pyx @@ -10,6 +10,8 @@ from libcpp.utility cimport move from libcpp.vector cimport vector from collections import OrderedDict +cimport cudf._lib.cpp.lists.lists_column_view as cpp_lists_column_view + try: import ujson as json except ImportError: @@ -242,7 +244,8 @@ cpdef write_orc(table, object statistics="ROWGROUP", object stripe_size_bytes=None, object stripe_size_rows=None, - object row_index_stride=None): + object row_index_stride=None, + object cols_as_map_type=None): """ Cython function to call into libcudf API, see `write_orc`. @@ -274,10 +277,16 @@ cpdef write_orc(table, tbl_meta = make_unique[table_input_metadata](tv) num_index_cols_meta = 0 + if cols_as_map_type is not None: + cols_as_map_type = set(cols_as_map_type) + for i, name in enumerate(table._column_names, num_index_cols_meta): tbl_meta.get().column_metadata[i].set_name(name.encode()) - _set_col_children_names( - table[name]._column, tbl_meta.get().column_metadata[i] + _set_col_children_metadata( + table[name]._column, + tbl_meta.get().column_metadata[i], + (cols_as_map_type is not None) + and (name in cols_as_map_type), ) cdef orc_writer_options c_orc_writer_options = move( @@ -365,13 +374,21 @@ cdef class ORCWriter: cdef compression_type comp_type cdef object index cdef unique_ptr[table_input_metadata] tbl_meta + cdef object cols_as_map_type + + def __cinit__(self, + object path, + object index=None, + object compression=None, + object statistics="ROWGROUP", + object cols_as_map_type=None): - def __cinit__(self, object path, object index=None, - object compression=None, object statistics="ROWGROUP"): self.sink = make_sink_info(path, self._data_sink) self.stat_freq = _get_orc_stat_freq(statistics) self.comp_type = _get_comp_type(compression) self.index = index + self.cols_as_map_type = cols_as_map_type \ + if cols_as_map_type is None else set(cols_as_map_type) self.initialized = False def write_table(self, table): @@ -428,8 +445,11 @@ cdef class ORCWriter: for i, name in enumerate(table._column_names, num_index_cols_meta): self.tbl_meta.get().column_metadata[i].set_name(name.encode()) - _set_col_children_names( - table[name]._column, self.tbl_meta.get().column_metadata[i] + _set_col_children_metadata( + table[name]._column, + self.tbl_meta.get().column_metadata[i], + (self.cols_as_map_type is not None) + and (name in self.cols_as_map_type), ) cdef map[string, string] user_data @@ -450,14 +470,24 @@ cdef class ORCWriter: self.initialized = True -cdef _set_col_children_names(Column col, column_in_metadata& col_meta): +cdef _set_col_children_metadata(Column col, + column_in_metadata& col_meta, + list_column_as_map=False): if is_struct_dtype(col): for i, (child_col, name) in enumerate( zip(col.children, list(col.dtype.fields)) ): col_meta.child(i).set_name(name.encode()) - _set_col_children_names(child_col, col_meta.child(i)) + _set_col_children_metadata( + child_col, col_meta.child(i), list_column_as_map + ) elif is_list_dtype(col): - _set_col_children_names(col.children[1], col_meta.child(1)) + if list_column_as_map: + col_meta.set_list_column_as_map() + _set_col_children_metadata( + col.children[cpp_lists_column_view.child_column_index], + col_meta.child(cpp_lists_column_view.child_column_index), + list_column_as_map + ) else: return diff --git a/python/cudf/cudf/io/orc.py b/python/cudf/cudf/io/orc.py index 9b8e56f4f10..378cb25fafb 100644 --- a/python/cudf/cudf/io/orc.py +++ b/python/cudf/cudf/io/orc.py @@ -412,6 +412,7 @@ def to_orc( stripe_size_bytes=None, stripe_size_rows=None, row_index_stride=None, + cols_as_map_type=None, **kwargs, ): """{docstring}""" @@ -434,6 +435,9 @@ def to_orc( "Categorical columns." ) + if cols_as_map_type is not None and not isinstance(cols_as_map_type, list): + raise TypeError("cols_as_map_type must be a list of column names.") + path_or_buf = ioutils.get_writer_filepath_or_buffer( path_or_data=fname, mode="wb", **kwargs ) @@ -448,6 +452,7 @@ def to_orc( stripe_size_bytes, stripe_size_rows, row_index_stride, + cols_as_map_type, ) else: liborc.write_orc( @@ -458,6 +463,7 @@ def to_orc( stripe_size_bytes, stripe_size_rows, row_index_stride, + cols_as_map_type, ) diff --git a/python/cudf/cudf/tests/test_orc.py b/python/cudf/cudf/tests/test_orc.py index 4373ef9afdf..db52e51bd33 100644 --- a/python/cudf/cudf/tests/test_orc.py +++ b/python/cudf/cudf/tests/test_orc.py @@ -1780,3 +1780,62 @@ def test_orc_columns_and_index_param(index, columns): ) else: assert_eq(expected, got, check_index_type=True) + + +@pytest.mark.parametrize( + "df_data,cols_as_map_type,expected_data", + [ + ( + {"a": [[{"a": 10, "b": 20}], [{"a": 1, "b": 21}]]}, + ["a"], + {"a": [[(10, 20)], [(1, 21)]]}, + ), + ( + { + "a": [[{"a": 10, "b": 20}], [{"a": 1, "b": 21}]], + "b": [[{"a": 10, "b": 20}], [{"a": 1, "b": 21}]], + }, + ["b"], + { + "a": [[{"a": 10, "b": 20}], [{"a": 1, "b": 21}]], + "b": [[(10, 20)], [(1, 21)]], + }, + ), + ( + { + "a": [[{"a": 10, "b": 20}], [{"a": 1, "b": 21}]], + "b": [[{"a": 10, "b": 20}], [{"a": 1, "b": 21}]], + "c": [ + [{"a": {"a": 10}, "b": 20}], + [{"a": {"a": 12}, "b": 21}], + ], + }, + ["b", "c"], + { + "a": [[{"a": 10, "b": 20}], [{"a": 1, "b": 21}]], + "b": [[(10, 20)], [(1, 21)]], + "c": [[({"a": 10}, 20)], [({"a": 12}, 21)]], + }, + ), + ], +) +def test_orc_writer_cols_as_map_type(df_data, cols_as_map_type, expected_data): + df = cudf.DataFrame(df_data) + buffer = BytesIO() + df.to_orc(buffer, cols_as_map_type=cols_as_map_type) + + got = pd.read_orc(buffer) + expected = pd.DataFrame(expected_data) + + assert_eq(got, expected) + + +def test_orc_writer_cols_as_map_type_error(): + df = cudf.DataFrame( + {"a": cudf.Series([[{"a": 10, "b": 20}], [{"a": 1, "b": 21}]])} + ) + buffer = BytesIO() + with pytest.raises( + TypeError, match="cols_as_map_type must be a list of column names." + ): + df.to_orc(buffer, cols_as_map_type=1) diff --git a/python/cudf/cudf/utils/ioutils.py b/python/cudf/cudf/utils/ioutils.py index 3e7fb4c4f02..c7b47648be1 100644 --- a/python/cudf/cudf/utils/ioutils.py +++ b/python/cudf/cudf/utils/ioutils.py @@ -442,7 +442,10 @@ row_index_stride: integer or None, default None Row index stride (maximum number of rows in each row group). If None, 10000 will be used. - +cols_as_map_type : list of column names or None, default None + A list of column names which should be written as map type in the ORC file. + Note that this option only affects columns of ListDtype. Names of other + column types will be ignored. Notes ----- From e431440dbd111f04fbc3911ca49ef0c328fed0b0 Mon Sep 17 00:00:00 2001 From: Yunsong Wang Date: Tue, 23 Aug 2022 10:00:22 -0400 Subject: [PATCH 58/58] Clean up ORC reader benchmarks with NVBench (#11543) Issue #10941 This PR rewrites the existing ORC reader benchmarks with nvbench w.r.t. #7960. It improves the `input` test case in which all data types were benchmarked with all compression and IO types. By splitting `input` into `decode` and `io_compression`, it reduces the number of test cases from 112 to 44. The PR also removes the current `row_selection` test suite. Authors: - Yunsong Wang (https://github.com/PointKernel) Approvers: - Nghia Truong (https://github.com/ttnghia) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/11543 --- cpp/benchmarks/CMakeLists.txt | 2 +- cpp/benchmarks/io/orc/orc_reader.cpp | 205 ------------------ cpp/benchmarks/io/orc/orc_reader_input.cpp | 185 ++++++++++++++++ cpp/benchmarks/io/orc/orc_reader_options.cpp | 214 +++++++++++++++++++ 4 files changed, 400 insertions(+), 206 deletions(-) delete mode 100644 cpp/benchmarks/io/orc/orc_reader.cpp create mode 100644 cpp/benchmarks/io/orc/orc_reader_input.cpp create mode 100644 cpp/benchmarks/io/orc/orc_reader_options.cpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 5b6c118f7bb..3280fc026dc 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -228,7 +228,7 @@ ConfigureBench(PARQUET_READER_BENCH io/parquet/parquet_reader.cpp) # ################################################################################################## # * orc reader benchmark -------------------------------------------------------------------------- -ConfigureBench(ORC_READER_BENCH io/orc/orc_reader.cpp) +ConfigureNVBench(ORC_READER_NVBENCH io/orc/orc_reader_input.cpp io/orc/orc_reader_options.cpp) # ################################################################################################## # * csv reader benchmark -------------------------------------------------------------------------- diff --git a/cpp/benchmarks/io/orc/orc_reader.cpp b/cpp/benchmarks/io/orc/orc_reader.cpp deleted file mode 100644 index 3ea1c09875e..00000000000 --- a/cpp/benchmarks/io/orc/orc_reader.cpp +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (c) 2020-2022, NVIDIA CORPORATION. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include -#include - -#include - -// to enable, run cmake with -DBUILD_BENCHMARKS=ON - -constexpr int64_t data_size = 512 << 20; -constexpr cudf::size_type num_cols = 64; - -namespace cudf_io = cudf::io; - -class OrcRead : public cudf::benchmark { -}; - -void BM_orc_read_varying_input(benchmark::State& state) -{ - auto const data_types = get_type_or_group(state.range(0)); - cudf::size_type const cardinality = state.range(1); - cudf::size_type const run_length = state.range(2); - cudf_io::compression_type const compression = - state.range(3) ? cudf_io::compression_type::SNAPPY : cudf_io::compression_type::NONE; - auto const source_type = static_cast(state.range(4)); - - auto const tbl = - create_random_table(cycle_dtypes(data_types, num_cols), - table_size_bytes{data_size}, - data_profile_builder().cardinality(cardinality).avg_run_length(run_length)); - auto const view = tbl->view(); - - cuio_source_sink_pair source_sink(source_type); - cudf_io::orc_writer_options opts = - cudf_io::orc_writer_options::builder(source_sink.make_sink_info(), view) - .compression(compression); - cudf_io::write_orc(opts); - - cudf_io::orc_reader_options read_opts = - cudf_io::orc_reader_options::builder(source_sink.make_source_info()); - - auto mem_stats_logger = cudf::memory_stats_logger(); - for (auto _ : state) { - try_drop_l3_cache(); - cuda_event_timer raii(state, true); // flush_l2_cache = true, stream = 0 - cudf_io::read_orc(read_opts); - } - - state.SetBytesProcessed(data_size * state.iterations()); - state.counters["peak_memory_usage"] = mem_stats_logger.peak_memory_usage(); - state.counters["encoded_file_size"] = source_sink.size(); -} - -std::vector get_col_names(cudf_io::source_info const& source) -{ - cudf_io::orc_reader_options const read_options = - cudf_io::orc_reader_options::builder(source).num_rows(1); - return cudf_io::read_orc(read_options).metadata.column_names; -} - -void BM_orc_read_varying_options(benchmark::State& state) -{ - auto state_idx = 0; - auto const col_sel = static_cast(state.range(state_idx++)); - auto const row_sel = static_cast(state.range(state_idx++)); - auto const num_chunks = state.range(state_idx++); - - auto const flags = state.range(state_idx++); - auto const use_index = (flags & 1) != 0; - auto const use_np_dtypes = (flags & 2) != 0; - auto const ts_type = cudf::data_type{static_cast(state.range(state_idx++))}; - - // skip_rows is not supported on nested types - auto const data_types = - dtypes_for_column_selection(get_type_or_group({int32_t(type_group_id::INTEGRAL_SIGNED), - int32_t(type_group_id::FLOATING_POINT), - int32_t(type_group_id::FIXED_POINT), - int32_t(type_group_id::TIMESTAMP), - int32_t(cudf::type_id::STRING)}), - col_sel); - auto const tbl = create_random_table(data_types, table_size_bytes{data_size}); - auto const view = tbl->view(); - - cuio_source_sink_pair source_sink(io_type::HOST_BUFFER); - cudf_io::orc_writer_options options = - cudf_io::orc_writer_options::builder(source_sink.make_sink_info(), view); - cudf_io::write_orc(options); - - auto const cols_to_read = - select_column_names(get_col_names(source_sink.make_source_info()), col_sel); - cudf_io::orc_reader_options read_options = - cudf_io::orc_reader_options::builder(source_sink.make_source_info()) - .columns(cols_to_read) - .use_index(use_index) - .use_np_dtypes(use_np_dtypes) - .timestamp_type(ts_type); - - auto const num_stripes = data_size / (64 << 20); - cudf::size_type const chunk_row_cnt = view.num_rows() / num_chunks; - auto mem_stats_logger = cudf::memory_stats_logger(); - for (auto _ : state) { - try_drop_l3_cache(); - cuda_event_timer raii(state, true); // flush_l2_cache = true, stream = 0 - - cudf::size_type rows_read = 0; - for (int32_t chunk = 0; chunk < num_chunks; ++chunk) { - auto const is_last_chunk = chunk == (num_chunks - 1); - switch (row_sel) { - case row_selection::ALL: break; - case row_selection::STRIPES: { - auto stripes_to_read = segments_in_chunk(num_stripes, num_chunks, chunk); - if (is_last_chunk) { - // Need to assume that an additional "overflow" stripe is present - stripes_to_read.push_back(num_stripes); - } - read_options.set_stripes({stripes_to_read}); - } break; - case row_selection::NROWS: - read_options.set_skip_rows(chunk * chunk_row_cnt); - read_options.set_num_rows(chunk_row_cnt); - if (is_last_chunk) read_options.set_num_rows(-1); - break; - default: CUDF_FAIL("Unsupported row selection method"); - } - - rows_read += cudf_io::read_orc(read_options).tbl->num_rows(); - } - - CUDF_EXPECTS(rows_read == view.num_rows(), "Benchmark did not read the entire table"); - } - - auto const data_processed = data_size * cols_to_read.size() / view.num_columns(); - state.SetBytesProcessed(data_processed * state.iterations()); - state.counters["peak_memory_usage"] = mem_stats_logger.peak_memory_usage(); - state.counters["encoded_file_size"] = source_sink.size(); -} - -#define ORC_RD_BM_INPUTS_DEFINE(name, type_or_group, src_type) \ - BENCHMARK_DEFINE_F(OrcRead, name) \ - (::benchmark::State & state) { BM_orc_read_varying_input(state); } \ - BENCHMARK_REGISTER_F(OrcRead, name) \ - ->ArgsProduct({{int32_t(type_or_group)}, {0, 1000}, {1, 32}, {true, false}, {src_type}}) \ - ->Unit(benchmark::kMillisecond) \ - ->UseManualTime(); - -RD_BENCHMARK_DEFINE_ALL_SOURCES(ORC_RD_BM_INPUTS_DEFINE, integral, type_group_id::INTEGRAL_SIGNED); -RD_BENCHMARK_DEFINE_ALL_SOURCES(ORC_RD_BM_INPUTS_DEFINE, floats, type_group_id::FLOATING_POINT); -RD_BENCHMARK_DEFINE_ALL_SOURCES(ORC_RD_BM_INPUTS_DEFINE, decimal, type_group_id::FIXED_POINT); -RD_BENCHMARK_DEFINE_ALL_SOURCES(ORC_RD_BM_INPUTS_DEFINE, timestamps, type_group_id::TIMESTAMP); -RD_BENCHMARK_DEFINE_ALL_SOURCES(ORC_RD_BM_INPUTS_DEFINE, string, cudf::type_id::STRING); -RD_BENCHMARK_DEFINE_ALL_SOURCES(ORC_RD_BM_INPUTS_DEFINE, list, cudf::type_id::LIST); -RD_BENCHMARK_DEFINE_ALL_SOURCES(ORC_RD_BM_INPUTS_DEFINE, struct, cudf::type_id::STRUCT); - -BENCHMARK_DEFINE_F(OrcRead, column_selection) -(::benchmark::State& state) { BM_orc_read_varying_options(state); } -BENCHMARK_REGISTER_F(OrcRead, column_selection) - ->ArgsProduct({{int32_t(column_selection::ALL), - int32_t(column_selection::ALTERNATE), - int32_t(column_selection::FIRST_HALF), - int32_t(column_selection::SECOND_HALF)}, - {int32_t(row_selection::ALL)}, - {1}, - {0b11}, // defaults - {int32_t(cudf::type_id::EMPTY)}}) - ->Unit(benchmark::kMillisecond) - ->UseManualTime(); - -// Need an API to get the number of stripes to enable row_selection::STRIPES here -BENCHMARK_DEFINE_F(OrcRead, row_selection) -(::benchmark::State& state) { BM_orc_read_varying_options(state); } -BENCHMARK_REGISTER_F(OrcRead, row_selection) - ->ArgsProduct({{int32_t(column_selection::ALL)}, - {int32_t(row_selection::NROWS)}, - {1, 8}, - {0b11}, // defaults - {int32_t(cudf::type_id::EMPTY)}}) - ->Unit(benchmark::kMillisecond) - ->UseManualTime(); - -BENCHMARK_DEFINE_F(OrcRead, misc_options) -(::benchmark::State& state) { BM_orc_read_varying_options(state); } -BENCHMARK_REGISTER_F(OrcRead, misc_options) - ->ArgsProduct({{int32_t(column_selection::ALL)}, - {int32_t(row_selection::NROWS)}, - {1}, - {0b11, 0b10, 0b01}, // `true` is default for each boolean parameter here - {int32_t(cudf::type_id::EMPTY), int32_t(cudf::type_id::TIMESTAMP_NANOSECONDS)}}) - ->Unit(benchmark::kMillisecond) - ->UseManualTime(); diff --git a/cpp/benchmarks/io/orc/orc_reader_input.cpp b/cpp/benchmarks/io/orc/orc_reader_input.cpp new file mode 100644 index 00000000000..8dd7e138f45 --- /dev/null +++ b/cpp/benchmarks/io/orc/orc_reader_input.cpp @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include + +constexpr int64_t data_size = 512 << 20; +constexpr cudf::size_type num_cols = 64; + +enum class data_type : int32_t { + INTEGRAL = static_cast(type_group_id::INTEGRAL_SIGNED), + FLOAT = static_cast(type_group_id::FLOATING_POINT), + DECIMAL = static_cast(type_group_id::FIXED_POINT), + TIMESTAMP = static_cast(type_group_id::TIMESTAMP), + STRING = static_cast(cudf::type_id::STRING), + LIST = static_cast(cudf::type_id::LIST), + STRUCT = static_cast(cudf::type_id::STRUCT) +}; + +// NVBENCH_DECLARE_ENUM_TYPE_STRINGS macro must be used from global namespace scope +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + data_type, + [](data_type value) { + switch (value) { + case data_type::INTEGRAL: return "INTEGRAL"; + case data_type::FLOAT: return "FLOAT"; + case data_type::DECIMAL: return "DECIMAL"; + case data_type::TIMESTAMP: return "TIMESTAMP"; + case data_type::STRING: return "STRING"; + case data_type::LIST: return "LIST"; + case data_type::STRUCT: return "STRUCT"; + default: return "Unknown"; + } + }, + [](auto) { return std::string{}; }) + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + cudf::io::io_type, + [](auto value) { + switch (value) { + case cudf::io::io_type::FILEPATH: return "FILEPATH"; + case cudf::io::io_type::HOST_BUFFER: return "HOST_BUFFER"; + default: return "Unknown"; + } + }, + [](auto) { return std::string{}; }) + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + cudf::io::compression_type, + [](auto value) { + switch (value) { + case cudf::io::compression_type::SNAPPY: return "SNAPPY"; + case cudf::io::compression_type::NONE: return "NONE"; + default: return "Unknown"; + } + }, + [](auto) { return std::string{}; }) + +void orc_read_common(cudf::io::orc_writer_options const& opts, + cuio_source_sink_pair& source_sink, + nvbench::state& state) +{ + cudf::io::write_orc(opts); + + cudf::io::orc_reader_options read_opts = + cudf::io::orc_reader_options::builder(source_sink.make_source_info()); + + auto mem_stats_logger = cudf::memory_stats_logger(); // init stats logger + state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::default_stream_value.value())); + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, + [&](nvbench::launch& launch, auto& timer) { + try_drop_l3_cache(); + + timer.start(); + cudf::io::read_orc(read_opts); + timer.stop(); + }); + + auto const time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value"); + state.add_element_count(static_cast(data_size) / time, "bytes_per_second"); + state.add_buffer_size( + mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); + state.add_buffer_size(source_sink.size(), "encoded_file_size", "encoded_file_size"); +} + +template +void BM_orc_read_data(nvbench::state& state, nvbench::type_list>) +{ + cudf::rmm_pool_raii rmm_pool; + + auto const d_type = get_type_or_group(static_cast(DataType)); + cudf::size_type const cardinality = state.get_int64("cardinality"); + cudf::size_type const run_length = state.get_int64("run_length"); + + auto const tbl = + create_random_table(cycle_dtypes(d_type, num_cols), + table_size_bytes{data_size}, + data_profile_builder().cardinality(cardinality).avg_run_length(run_length)); + auto const view = tbl->view(); + + cuio_source_sink_pair source_sink(io_type::HOST_BUFFER); + cudf::io::orc_writer_options opts = + cudf::io::orc_writer_options::builder(source_sink.make_sink_info(), view); + + orc_read_common(opts, source_sink, state); +} + +template +void BM_orc_read_io_compression( + nvbench::state& state, + nvbench::type_list, nvbench::enum_type>) +{ + cudf::rmm_pool_raii rmm_pool; + + auto const d_type = get_type_or_group({static_cast(data_type::INTEGRAL), + static_cast(data_type::FLOAT), + static_cast(data_type::DECIMAL), + static_cast(data_type::TIMESTAMP), + static_cast(data_type::STRING), + static_cast(data_type::LIST), + static_cast(data_type::STRUCT)}); + + cudf::size_type const cardinality = state.get_int64("cardinality"); + cudf::size_type const run_length = state.get_int64("run_length"); + + auto const tbl = + create_random_table(cycle_dtypes(d_type, num_cols), + table_size_bytes{data_size}, + data_profile_builder().cardinality(cardinality).avg_run_length(run_length)); + auto const view = tbl->view(); + + cuio_source_sink_pair source_sink(IO); + cudf::io::orc_writer_options opts = + cudf::io::orc_writer_options::builder(source_sink.make_sink_info(), view) + .compression(Compression); + + orc_read_common(opts, source_sink, state); +} + +using d_type_list = nvbench::enum_type_list; + +using io_list = + nvbench::enum_type_list; + +using compression_list = + nvbench::enum_type_list; + +NVBENCH_BENCH_TYPES(BM_orc_read_data, NVBENCH_TYPE_AXES(d_type_list)) + .set_name("orc_read_decode") + .set_type_axes_names({"data_type"}) + .add_int64_axis("cardinality", {0, 1000}) + .add_int64_axis("run_length", {1, 32}); + +NVBENCH_BENCH_TYPES(BM_orc_read_io_compression, NVBENCH_TYPE_AXES(io_list, compression_list)) + .set_name("orc_read_io_compression") + .set_type_axes_names({"io", "compression"}) + .add_int64_axis("cardinality", {0, 1000}) + .add_int64_axis("run_length", {1, 32}); diff --git a/cpp/benchmarks/io/orc/orc_reader_options.cpp b/cpp/benchmarks/io/orc/orc_reader_options.cpp new file mode 100644 index 00000000000..cc92eae101a --- /dev/null +++ b/cpp/benchmarks/io/orc/orc_reader_options.cpp @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include + +constexpr int64_t data_size = 512 << 20; + +enum class uses_index : bool { YES, NO }; + +enum class uses_numpy_dtype : bool { YES, NO }; + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + uses_index, + [](auto value) { + switch (value) { + case uses_index::YES: return "YES"; + case uses_index::NO: return "NO"; + default: return "Unknown"; + } + }, + [](auto) { return std::string{}; }) + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + uses_numpy_dtype, + [](auto value) { + switch (value) { + case uses_numpy_dtype::YES: return "YES"; + case uses_numpy_dtype::NO: return "NO"; + default: return "Unknown"; + } + }, + [](auto) { return std::string{}; }) + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + column_selection, + [](auto value) { + switch (value) { + case column_selection::ALL: return "ALL"; + case column_selection::ALTERNATE: return "ALTERNATE"; + case column_selection::FIRST_HALF: return "FIRST_HALF"; + case column_selection::SECOND_HALF: return "SECOND_HALF"; + default: return "Unknown"; + } + }, + [](auto) { return std::string{}; }) + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + row_selection, + [](auto value) { + switch (value) { + case row_selection::ALL: return "ALL"; + case row_selection::NROWS: return "NROWS"; + default: return "Unknown"; + } + }, + [](auto) { return std::string{}; }) + +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + cudf::type_id, + [](auto value) { + switch (value) { + case cudf::type_id::EMPTY: return "EMPTY"; + case cudf::type_id::TIMESTAMP_NANOSECONDS: return "TIMESTAMP_NANOSECONDS"; + default: return "Unknown"; + } + }, + [](auto) { return std::string{}; }) + +std::vector get_col_names(cudf::io::source_info const& source) +{ + cudf::io::orc_reader_options const read_options = + cudf::io::orc_reader_options::builder(source).num_rows(1); + return cudf::io::read_orc(read_options).metadata.column_names; +} + +template +void BM_orc_read_varying_options(nvbench::state& state, + nvbench::type_list, + nvbench::enum_type, + nvbench::enum_type, + nvbench::enum_type, + nvbench::enum_type>) +{ + cudf::rmm_pool_raii rmm_pool; + + auto constexpr num_chunks = 1; + + auto const use_index = UsesIndex == uses_index::YES; + auto const use_np_dtypes = UsesNumpyDType == uses_numpy_dtype::YES; + auto const ts_type = cudf::data_type{Timestamp}; + + // skip_rows is not supported on nested types + auto const data_types = + dtypes_for_column_selection(get_type_or_group({int32_t(type_group_id::INTEGRAL_SIGNED), + int32_t(type_group_id::FLOATING_POINT), + int32_t(type_group_id::FIXED_POINT), + int32_t(type_group_id::TIMESTAMP), + int32_t(cudf::type_id::STRING)}), + ColSelection); + auto const tbl = create_random_table(data_types, table_size_bytes{data_size}); + auto const view = tbl->view(); + + cuio_source_sink_pair source_sink(io_type::HOST_BUFFER); + cudf::io::orc_writer_options options = + cudf::io::orc_writer_options::builder(source_sink.make_sink_info(), view); + cudf::io::write_orc(options); + + auto const cols_to_read = + select_column_names(get_col_names(source_sink.make_source_info()), ColSelection); + cudf::io::orc_reader_options read_options = + cudf::io::orc_reader_options::builder(source_sink.make_source_info()) + .columns(cols_to_read) + .use_index(use_index) + .use_np_dtypes(use_np_dtypes) + .timestamp_type(ts_type); + + auto const num_stripes = data_size / (64 << 20); + cudf::size_type const chunk_row_cnt = view.num_rows() / num_chunks; + + auto mem_stats_logger = cudf::memory_stats_logger(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::default_stream_value.value())); + state.exec( + nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { + try_drop_l3_cache(); + + timer.start(); + cudf::size_type rows_read = 0; + for (int32_t chunk = 0; chunk < num_chunks; ++chunk) { + auto const is_last_chunk = chunk == (num_chunks - 1); + switch (RowSelection) { + case row_selection::ALL: break; + case row_selection::STRIPES: { + auto stripes_to_read = segments_in_chunk(num_stripes, num_chunks, chunk); + if (is_last_chunk) { + // Need to assume that an additional "overflow" stripe is present + stripes_to_read.push_back(num_stripes); + } + read_options.set_stripes({stripes_to_read}); + } break; + case row_selection::NROWS: + read_options.set_skip_rows(chunk * chunk_row_cnt); + read_options.set_num_rows(chunk_row_cnt); + if (is_last_chunk) read_options.set_num_rows(-1); + break; + default: CUDF_FAIL("Unsupported row selection method"); + } + + rows_read += cudf::io::read_orc(read_options).tbl->num_rows(); + } + + CUDF_EXPECTS(rows_read == view.num_rows(), "Benchmark did not read the entire table"); + timer.stop(); + }); + + auto const elapsed_time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value"); + auto const data_processed = data_size * cols_to_read.size() / view.num_columns(); + state.add_element_count(static_cast(data_processed) / elapsed_time, "bytes_per_second"); + state.add_buffer_size( + mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); + state.add_buffer_size(source_sink.size(), "encoded_file_size", "encoded_file_size"); +} + +using col_selections = nvbench::enum_type_list; + +NVBENCH_BENCH_TYPES(BM_orc_read_varying_options, + NVBENCH_TYPE_AXES(col_selections, + nvbench::enum_type_list, + nvbench::enum_type_list, + nvbench::enum_type_list, + nvbench::enum_type_list)) + .set_name("orc_read_column_selection") + .set_type_axes_names( + {"column_selection", "row_selection", "uses_index", "uses_numpy_dtype", "timestamp_type"}); + +NVBENCH_BENCH_TYPES( + BM_orc_read_varying_options, + NVBENCH_TYPE_AXES( + nvbench::enum_type_list, + nvbench::enum_type_list, + nvbench::enum_type_list, + nvbench::enum_type_list, + nvbench::enum_type_list)) + .set_name("orc_read_misc_options") + .set_type_axes_names( + {"column_selection", "row_selection", "uses_index", "uses_numpy_dtype", "timestamp_type"});