From 5efd72f64e3b1e25337c30ba0ab246051d3fe396 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 1 Jul 2024 07:37:12 -1000 Subject: [PATCH 01/42] Ensure cudf objects can astype to any type when empty (#16106) pandas allows objects to `astype` to any other type if the object is empty. The PR mirrors that behavior for cudf. This PR also more consistently uses `astype` instead of `as_*_column` and fixes a bug in `IntervalDtype.__eq__` discovered when writing a unit test for this bug. Authors: - Matthew Roeschke (https://github.com/mroeschke) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16106 --- python/cudf/cudf/core/column/column.py | 9 ++++++ python/cudf/cudf/core/column/datetime.py | 36 +++++++++++---------- python/cudf/cudf/core/column/decimal.py | 2 +- python/cudf/cudf/core/column/interval.py | 26 +++++++-------- python/cudf/cudf/core/column/timedelta.py | 34 +++++++++++--------- python/cudf/cudf/core/dataframe.py | 2 +- python/cudf/cudf/core/dtypes.py | 2 +- python/cudf/cudf/core/frame.py | 4 +-- python/cudf/cudf/core/indexing_utils.py | 2 +- python/cudf/cudf/core/series.py | 8 +++-- python/cudf/cudf/core/tools/numeric.py | 14 ++++---- python/cudf/cudf/tests/test_interval.py | 6 ++++ python/cudf/cudf/tests/test_series.py | 39 +++++++++++++++++++++++ 13 files changed, 121 insertions(+), 63 deletions(-) diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index 5db6fd904a9..e7a2863da8c 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -959,6 +959,15 @@ def can_cast_safely(self, to_dtype: Dtype) -> bool: raise NotImplementedError() def astype(self, dtype: Dtype, copy: bool = False) -> ColumnBase: + if len(self) == 0: + dtype = cudf.dtype(dtype) + if self.dtype == dtype: + if copy: + return self.copy() + else: + return self + else: + return column_empty(0, dtype=dtype, masked=self.nullable) if copy: col = self.copy() else: diff --git a/python/cudf/cudf/core/column/datetime.py b/python/cudf/cudf/core/column/datetime.py index 121076b69ce..c10aceba9f4 100644 --- a/python/cudf/cudf/core/column/datetime.py +++ b/python/cudf/cudf/core/column/datetime.py @@ -280,8 +280,8 @@ def __contains__(self, item: ScalarLike) -> bool: return False elif ts.tzinfo is not None: ts = ts.tz_convert(None) - return ts.to_numpy().astype("int64") in self.as_numerical_column( - "int64" + return ts.to_numpy().astype("int64") in cast( + "cudf.core.column.NumericalColumn", self.astype("int64") ) @functools.cached_property @@ -503,9 +503,9 @@ def mean( self, skipna=None, min_count: int = 0, dtype=np.float64 ) -> ScalarLike: return pd.Timestamp( - self.as_numerical_column("int64").mean( - skipna=skipna, min_count=min_count, dtype=dtype - ), + cast( + "cudf.core.column.NumericalColumn", self.astype("int64") + ).mean(skipna=skipna, min_count=min_count, dtype=dtype), unit=self.time_unit, ).as_unit(self.time_unit) @@ -517,7 +517,7 @@ def std( ddof: int = 1, ) -> pd.Timedelta: return pd.Timedelta( - self.as_numerical_column("int64").std( + cast("cudf.core.column.NumericalColumn", self.astype("int64")).std( skipna=skipna, min_count=min_count, dtype=dtype, ddof=ddof ) * _unit_to_nanoseconds_conversion[self.time_unit], @@ -525,7 +525,9 @@ def std( def median(self, skipna: bool | None = None) -> pd.Timestamp: return pd.Timestamp( - self.as_numerical_column("int64").median(skipna=skipna), + cast( + "cudf.core.column.NumericalColumn", self.astype("int64") + ).median(skipna=skipna), unit=self.time_unit, ).as_unit(self.time_unit) @@ -534,18 +536,18 @@ def cov(self, other: DatetimeColumn) -> float: raise TypeError( f"cannot perform cov with types {self.dtype}, {other.dtype}" ) - return self.as_numerical_column("int64").cov( - other.as_numerical_column("int64") - ) + return cast( + "cudf.core.column.NumericalColumn", self.astype("int64") + ).cov(cast("cudf.core.column.NumericalColumn", other.astype("int64"))) def corr(self, other: DatetimeColumn) -> float: if not isinstance(other, DatetimeColumn): raise TypeError( f"cannot perform corr with types {self.dtype}, {other.dtype}" ) - return self.as_numerical_column("int64").corr( - other.as_numerical_column("int64") - ) + return cast( + "cudf.core.column.NumericalColumn", self.astype("int64") + ).corr(cast("cudf.core.column.NumericalColumn", other.astype("int64"))) def quantile( self, @@ -554,7 +556,7 @@ def quantile( exact: bool, return_scalar: bool, ) -> ColumnBase: - result = self.as_numerical_column("int64").quantile( + result = self.astype("int64").quantile( q=q, interpolation=interpolation, exact=exact, @@ -645,12 +647,12 @@ def indices_of( ) -> cudf.core.column.NumericalColumn: value = column.as_column( pd.to_datetime(value), dtype=self.dtype - ).as_numerical_column("int64") - return self.as_numerical_column("int64").indices_of(value) + ).astype("int64") + return self.astype("int64").indices_of(value) @property def is_unique(self) -> bool: - return self.as_numerical_column("int64").is_unique + return self.astype("int64").is_unique def isin(self, values: Sequence) -> ColumnBase: return cudf.core.tools.datetimes._isin_datetimelike(self, values) diff --git a/python/cudf/cudf/core/column/decimal.py b/python/cudf/cudf/core/column/decimal.py index d66908b5f94..3e238d65cff 100644 --- a/python/cudf/cudf/core/column/decimal.py +++ b/python/cudf/cudf/core/column/decimal.py @@ -157,7 +157,7 @@ def normalize_binop_value(self, other): "Decimal columns only support binary operations with " "integer numerical columns." ) - other = other.as_decimal_column( + other = other.astype( self.dtype.__class__(self.dtype.__class__.MAX_PRECISION, 0) ) elif not isinstance(other, DecimalBaseColumn): diff --git a/python/cudf/cudf/core/column/interval.py b/python/cudf/cudf/core/column/interval.py index f24ca3fdad1..d09a1f66539 100644 --- a/python/cudf/cudf/core/column/interval.py +++ b/python/cudf/cudf/core/column/interval.py @@ -4,7 +4,7 @@ import cudf from cudf.core.column import StructColumn -from cudf.core.dtypes import CategoricalDtype, IntervalDtype +from cudf.core.dtypes import IntervalDtype class IntervalColumn(StructColumn): @@ -87,20 +87,16 @@ def copy(self, deep=True): def as_interval_column(self, dtype): if isinstance(dtype, IntervalDtype): - if isinstance(self.dtype, CategoricalDtype): - new_struct = self._get_decategorized_column() - return IntervalColumn.from_struct_column(new_struct) - else: - return IntervalColumn( - size=self.size, - dtype=dtype, - mask=self.mask, - offset=self.offset, - null_count=self.null_count, - children=tuple( - child.astype(dtype.subtype) for child in self.children - ), - ) + return IntervalColumn( + size=self.size, + dtype=dtype, + mask=self.mask, + offset=self.offset, + null_count=self.null_count, + children=tuple( + child.astype(dtype.subtype) for child in self.children + ), + ) else: raise ValueError("dtype must be IntervalDtype") diff --git a/python/cudf/cudf/core/column/timedelta.py b/python/cudf/cudf/core/column/timedelta.py index 8f41bcb6422..5a0171bbbdc 100644 --- a/python/cudf/cudf/core/column/timedelta.py +++ b/python/cudf/cudf/core/column/timedelta.py @@ -107,7 +107,9 @@ def __contains__(self, item: DatetimeLikeScalar) -> bool: # np.timedelta64 raises ValueError, hence `item` # cannot exist in `self`. return False - return item.view("int64") in self.as_numerical_column("int64") + return item.view("int64") in cast( + "cudf.core.column.NumericalColumn", self.astype("int64") + ) @property def values(self): @@ -132,9 +134,7 @@ def to_arrow(self) -> pa.Array: self.mask_array_view(mode="read").copy_to_host() ) data = pa.py_buffer( - self.as_numerical_column("int64") - .data_array_view(mode="read") - .copy_to_host() + self.astype("int64").data_array_view(mode="read").copy_to_host() ) pa_dtype = np_to_pa_dtype(self.dtype) return pa.Array.from_buffers( @@ -295,13 +295,17 @@ def as_timedelta_column( def mean(self, skipna=None, dtype: Dtype = np.float64) -> pd.Timedelta: return pd.Timedelta( - self.as_numerical_column("int64").mean(skipna=skipna, dtype=dtype), + cast( + "cudf.core.column.NumericalColumn", self.astype("int64") + ).mean(skipna=skipna, dtype=dtype), unit=self.time_unit, ).as_unit(self.time_unit) def median(self, skipna: bool | None = None) -> pd.Timedelta: return pd.Timedelta( - self.as_numerical_column("int64").median(skipna=skipna), + cast( + "cudf.core.column.NumericalColumn", self.astype("int64") + ).median(skipna=skipna), unit=self.time_unit, ).as_unit(self.time_unit) @@ -315,7 +319,7 @@ def quantile( exact: bool, return_scalar: bool, ) -> ColumnBase: - result = self.as_numerical_column("int64").quantile( + result = self.astype("int64").quantile( q=q, interpolation=interpolation, exact=exact, @@ -337,7 +341,7 @@ def sum( # Since sum isn't overridden in Numerical[Base]Column, mypy only # sees the signature from Reducible (which doesn't have the extra # parameters from ColumnBase._reduce) so we have to ignore this. - self.as_numerical_column("int64").sum( # type: ignore + self.astype("int64").sum( # type: ignore skipna=skipna, min_count=min_count, dtype=dtype ), unit=self.time_unit, @@ -351,7 +355,7 @@ def std( ddof: int = 1, ) -> pd.Timedelta: return pd.Timedelta( - self.as_numerical_column("int64").std( + cast("cudf.core.column.NumericalColumn", self.astype("int64")).std( skipna=skipna, min_count=min_count, ddof=ddof, dtype=dtype ), unit=self.time_unit, @@ -362,18 +366,18 @@ def cov(self, other: TimeDeltaColumn) -> float: raise TypeError( f"cannot perform cov with types {self.dtype}, {other.dtype}" ) - return self.as_numerical_column("int64").cov( - other.as_numerical_column("int64") - ) + return cast( + "cudf.core.column.NumericalColumn", self.astype("int64") + ).cov(cast("cudf.core.column.NumericalColumn", other.astype("int64"))) def corr(self, other: TimeDeltaColumn) -> float: if not isinstance(other, TimeDeltaColumn): raise TypeError( f"cannot perform corr with types {self.dtype}, {other.dtype}" ) - return self.as_numerical_column("int64").corr( - other.as_numerical_column("int64") - ) + return cast( + "cudf.core.column.NumericalColumn", self.astype("int64") + ).corr(cast("cudf.core.column.NumericalColumn", other.astype("int64"))) def components(self) -> dict[str, ColumnBase]: """ diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index 4dfeb68b7ba..b249410c2e4 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -2404,7 +2404,7 @@ def scatter_by_map( if isinstance(map_index, cudf.core.column.StringColumn): cat_index = cast( cudf.core.column.CategoricalColumn, - map_index.as_categorical_column("category"), + map_index.astype("category"), ) map_index = cat_index.codes warnings.warn( diff --git a/python/cudf/cudf/core/dtypes.py b/python/cudf/cudf/core/dtypes.py index 034849d0e71..de715191c08 100644 --- a/python/cudf/cudf/core/dtypes.py +++ b/python/cudf/cudf/core/dtypes.py @@ -937,7 +937,7 @@ def to_pandas(self) -> pd.IntervalDtype: def __eq__(self, other): if isinstance(other, str): # This means equality isn't transitive but mimics pandas - return other == self.name + return other in (self.name, str(self)) return ( type(self) == type(other) and self.subtype == other.subtype diff --git a/python/cudf/cudf/core/frame.py b/python/cudf/cudf/core/frame.py index 9bac75dc6ac..253d200f7d4 100644 --- a/python/cudf/cudf/core/frame.py +++ b/python/cudf/cudf/core/frame.py @@ -927,7 +927,7 @@ def from_arrow(cls, data: pa.Table) -> Self: # of column is 0 (i.e., empty) then we will have an # int8 column in result._data[name] returned by libcudf, # which needs to be type-casted to 'category' dtype. - result[name] = result[name].as_categorical_column("category") + result[name] = result[name].astype("category") elif ( pandas_dtypes.get(name) == "empty" and np_dtypes.get(name) == "object" @@ -936,7 +936,7 @@ def from_arrow(cls, data: pa.Table) -> Self: # is specified as 'empty' and np_dtypes as 'object', # hence handling this special case to type-cast the empty # float column to str column. - result[name] = result[name].as_string_column(cudf.dtype("str")) + result[name] = result[name].astype(cudf.dtype("str")) elif name in data.column_names and isinstance( data[name].type, ( diff --git a/python/cudf/cudf/core/indexing_utils.py b/python/cudf/cudf/core/indexing_utils.py index 73a1cd26367..a5fed02cbed 100644 --- a/python/cudf/cudf/core/indexing_utils.py +++ b/python/cudf/cudf/core/indexing_utils.py @@ -229,7 +229,7 @@ def parse_row_iloc_indexer(key: Any, n: int) -> IndexingSpec: else: key = cudf.core.column.as_column(key) if isinstance(key, cudf.core.column.CategoricalColumn): - key = key.as_numerical_column(key.codes.dtype) + key = key.astype(key.codes.dtype) if is_bool_dtype(key.dtype): return MaskIndexer(BooleanMask(key, n)) elif len(key) == 0: diff --git a/python/cudf/cudf/core/series.py b/python/cudf/cudf/core/series.py index 97b6bbec2d4..4a60470fafa 100644 --- a/python/cudf/cudf/core/series.py +++ b/python/cudf/cudf/core/series.py @@ -3107,10 +3107,12 @@ def value_counts( # Pandas returns an IntervalIndex as the index of res # this condition makes sure we do too if bins is given if bins is not None and len(res) == len(res.index.categories): - int_index = IntervalColumn.as_interval_column( - res.index._column, res.index.categories.dtype + interval_col = IntervalColumn.from_struct_column( + res.index._column._get_decategorized_column() + ) + res.index = cudf.IntervalIndex._from_data( + {res.index.name: interval_col} ) - res.index = int_index res.name = result_name return res diff --git a/python/cudf/cudf/core/tools/numeric.py b/python/cudf/cudf/core/tools/numeric.py index 68b23f1e059..ef6b86a04a7 100644 --- a/python/cudf/cudf/core/tools/numeric.py +++ b/python/cudf/cudf/core/tools/numeric.py @@ -115,11 +115,11 @@ def to_numeric(arg, errors="raise", downcast=None): dtype = col.dtype if is_datetime_dtype(dtype) or is_timedelta_dtype(dtype): - col = col.as_numerical_column(cudf.dtype("int64")) + col = col.astype(cudf.dtype("int64")) elif isinstance(dtype, CategoricalDtype): cat_dtype = col.dtype.type if _is_non_decimal_numeric_dtype(cat_dtype): - col = col.as_numerical_column(cat_dtype) + col = col.astype(cat_dtype) else: try: col = _convert_str_col( @@ -146,8 +146,8 @@ def to_numeric(arg, errors="raise", downcast=None): raise ValueError("Unrecognized datatype") # str->float conversion may require lower precision - if col.dtype == cudf.dtype("f"): - col = col.as_numerical_column("d") + if col.dtype == cudf.dtype("float32"): + col = col.astype("float64") if downcast: if downcast == "float": @@ -205,7 +205,7 @@ def _convert_str_col(col, errors, _downcast=None): is_integer = libstrings.is_integer(col) if is_integer.all(): - return col.as_numerical_column(dtype=cudf.dtype("i8")) + return col.astype(dtype=cudf.dtype("i8")) col = _proc_inf_empty_strings(col) @@ -218,9 +218,9 @@ def _convert_str_col(col, errors, _downcast=None): "limited by float32 precision." ) ) - return col.as_numerical_column(dtype=cudf.dtype("f")) + return col.astype(dtype=cudf.dtype("float32")) else: - return col.as_numerical_column(dtype=cudf.dtype("d")) + return col.astype(dtype=cudf.dtype("float64")) else: if errors == "coerce": col = libcudf.string_casting.stod(col) diff --git a/python/cudf/cudf/tests/test_interval.py b/python/cudf/cudf/tests/test_interval.py index 1b395c09ba8..5eeea87d8e0 100644 --- a/python/cudf/cudf/tests/test_interval.py +++ b/python/cudf/cudf/tests/test_interval.py @@ -188,3 +188,9 @@ def test_from_pandas_intervaldtype(): result = cudf.from_pandas(dtype) expected = cudf.IntervalDtype("int64", closed="left") assert_eq(result, expected) + + +def test_intervaldtype_eq_string_with_attributes(): + dtype = cudf.IntervalDtype("int64", closed="left") + assert dtype == "interval" + assert dtype == "interval[int64, left]" diff --git a/python/cudf/cudf/tests/test_series.py b/python/cudf/cudf/tests/test_series.py index 467d0c46ae7..f2501041f25 100644 --- a/python/cudf/cudf/tests/test_series.py +++ b/python/cudf/cudf/tests/test_series.py @@ -2873,3 +2873,42 @@ def test_nunique_all_null(dropna): result = pd_ser.nunique(dropna=dropna) expected = cudf_ser.nunique(dropna=dropna) assert result == expected + + +@pytest.mark.parametrize( + "type1", + [ + "category", + "interval[int64, right]", + "int64", + "float64", + "str", + "datetime64[ns]", + "timedelta64[ns]", + ], +) +@pytest.mark.parametrize( + "type2", + [ + "category", + "interval[int64, right]", + "int64", + "float64", + "str", + "datetime64[ns]", + "timedelta64[ns]", + ], +) +@pytest.mark.parametrize( + "as_dtype", [lambda x: x, cudf.dtype], ids=["string", "object"] +) +@pytest.mark.parametrize("copy", [True, False]) +def test_empty_astype_always_castable(type1, type2, as_dtype, copy): + ser = cudf.Series([], dtype=as_dtype(type1)) + result = ser.astype(as_dtype(type2), copy=copy) + expected = cudf.Series([], dtype=as_dtype(type2)) + assert_eq(result, expected) + if not copy and cudf.dtype(type1) == cudf.dtype(type2): + assert ser._column is result._column + else: + assert ser._column is not result._column From b691b1c1cd99a5721230ac8db2afa8ad99835b9c Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:25:11 -0400 Subject: [PATCH 02/42] Add stream parameter to cudf::io::text::multibyte_split (#16034) Adds stream support the `cudf::io::text::multibyte_split` API. Also adds a stream test and deprecates an overloaded API. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Mark Harris (https://github.com/harrism) - Karthikeyan (https://github.com/karthikeyann) URL: https://github.com/rapidsai/cudf/pull/16034 --- cpp/include/cudf/io/text/byte_range_info.hpp | 15 +++- .../cudf/io/text/data_chunk_source.hpp | 10 ++- cpp/include/cudf/io/text/multibyte_split.hpp | 27 ++++++- cpp/src/io/text/multibyte_split.cu | 19 ++--- cpp/tests/CMakeLists.txt | 1 + cpp/tests/io/text/multibyte_split_test.cpp | 81 ++++++++++++------- cpp/tests/streams/io/multibyte_split_test.cpp | 36 +++++++++ docs/cudf/source/conf.py | 2 +- 8 files changed, 141 insertions(+), 50 deletions(-) create mode 100644 cpp/tests/streams/io/multibyte_split_test.cpp diff --git a/cpp/include/cudf/io/text/byte_range_info.hpp b/cpp/include/cudf/io/text/byte_range_info.hpp index 0086432d003..60ee867f058 100644 --- a/cpp/include/cudf/io/text/byte_range_info.hpp +++ b/cpp/include/cudf/io/text/byte_range_info.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, NVIDIA CORPORATION. + * Copyright (c) 2022-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,17 +24,22 @@ namespace cudf { namespace io { namespace text { +/** + * @addtogroup io_readers + * @{ + * @file + */ /** * @brief stores offset and size used to indicate a byte range */ class byte_range_info { private: - int64_t _offset; ///< offset in bytes - int64_t _size; ///< size in bytes + int64_t _offset{}; ///< offset in bytes + int64_t _size{}; ///< size in bytes public: - constexpr byte_range_info() noexcept : _offset(0), _size(0) {} + constexpr byte_range_info() = default; /** * @brief Constructs a byte_range_info object * @@ -104,6 +109,8 @@ std::vector create_byte_range_infos_consecutive(int64_t total_b */ byte_range_info create_byte_range_info_max(); +/** @} */ // end of group + } // namespace text } // namespace io } // namespace cudf diff --git a/cpp/include/cudf/io/text/data_chunk_source.hpp b/cpp/include/cudf/io/text/data_chunk_source.hpp index 28204c82780..13aff4b3b8f 100644 --- a/cpp/include/cudf/io/text/data_chunk_source.hpp +++ b/cpp/include/cudf/io/text/data_chunk_source.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,12 @@ namespace cudf { namespace io { namespace text { +/** + * @addtogroup io_readers + * @{ + * @file + */ + /** * @brief A contract guaranteeing stream-ordered memory access to the underlying device data. * @@ -110,6 +116,8 @@ class data_chunk_source { [[nodiscard]] virtual std::unique_ptr create_reader() const = 0; }; +/** @} */ // end of group + } // namespace text } // namespace io } // namespace cudf diff --git a/cpp/include/cudf/io/text/multibyte_split.hpp b/cpp/include/cudf/io/text/multibyte_split.hpp index 7abae7c754b..e29ab78ae46 100644 --- a/cpp/include/cudf/io/text/multibyte_split.hpp +++ b/cpp/include/cudf/io/text/multibyte_split.hpp @@ -30,6 +30,11 @@ namespace cudf { namespace io { namespace text { +/** + * @addtogroup io_readers + * @{ + * @file + */ /** * @brief Parsing options for multibyte_split. @@ -79,6 +84,7 @@ struct parse_options { * @param source The source string * @param delimiter UTF-8 encoded string for which to find offsets in the source * @param options the parsing options to use (including byte range) + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Memory resource to use for the device memory allocation * @return The strings found by splitting the source by the delimiter within the relevant byte * range. @@ -87,17 +93,30 @@ std::unique_ptr multibyte_split( data_chunk_source const& source, std::string const& delimiter, parse_options options = {}, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = rmm::mr::get_current_device_resource()); -std::unique_ptr multibyte_split( +/** + * @brief Splits the source text into a strings column using a multiple byte delimiter. + * + * @deprecated Since 24.08 + * + * @param source The source input data encoded in UTF-8 + * @param delimiter UTF-8 encoded string for which to find offsets in the source + * @param byte_range The position and size within `source` to produce the column from + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Memory resource to use for the device memory allocation + * @return The strings found by splitting the source by the delimiter within the relevant byte + * range. + */ +[[deprecated]] std::unique_ptr multibyte_split( data_chunk_source const& source, std::string const& delimiter, std::optional byte_range, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = rmm::mr::get_current_device_resource()); -std::unique_ptr multibyte_split(data_chunk_source const& source, - std::string const& delimiter, - rmm::device_async_resource_ref mr); +/** @} */ // end of group } // namespace text } // namespace io diff --git a/cpp/src/io/text/multibyte_split.cu b/cpp/src/io/text/multibyte_split.cu index 9c406369068..51dc0ca90af 100644 --- a/cpp/src/io/text/multibyte_split.cu +++ b/cpp/src/io/text/multibyte_split.cu @@ -565,35 +565,32 @@ std::unique_ptr multibyte_split(cudf::io::text::data_chunk_source } // namespace detail +// deprecated in 24.08 std::unique_ptr multibyte_split(cudf::io::text::data_chunk_source const& source, std::string const& delimiter, std::optional byte_range, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - return multibyte_split( - source, delimiter, parse_options{byte_range.value_or(create_byte_range_info_max())}, mr); + return multibyte_split(source, + delimiter, + parse_options{byte_range.value_or(create_byte_range_info_max())}, + stream, + mr); } std::unique_ptr multibyte_split(cudf::io::text::data_chunk_source const& source, std::string const& delimiter, parse_options options, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - auto stream = cudf::get_default_stream(); - auto result = detail::multibyte_split( source, delimiter, options.byte_range, options.strip_delimiters, stream, mr); return result; } -std::unique_ptr multibyte_split(cudf::io::text::data_chunk_source const& source, - std::string const& delimiter, - rmm::device_async_resource_ref mr) -{ - return multibyte_split(source, delimiter, parse_options{}, mr); -} - } // namespace text } // namespace io } // namespace cudf diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index eef09954647..244bcb7d897 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -691,6 +691,7 @@ ConfigureTest(STREAM_INTEROP_TEST streams/interop_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_JSONIO_TEST streams/io/json_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_LABELING_BINS_TEST streams/labeling_bins_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_LISTS_TEST streams/lists_test.cpp STREAM_MODE testing) +ConfigureTest(STREAM_MULTIBYTE_SPLIT_TEST streams/io/multibyte_split_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_NULL_MASK_TEST streams/null_mask_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_ORCIO_TEST streams/io/orc_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_PARQUETIO_TEST streams/io/parquet_test.cpp STREAM_MODE testing) diff --git a/cpp/tests/io/text/multibyte_split_test.cpp b/cpp/tests/io/text/multibyte_split_test.cpp index 36338253c9b..408d54bd5ff 100644 --- a/cpp/tests/io/text/multibyte_split_test.cpp +++ b/cpp/tests/io/text/multibyte_split_test.cpp @@ -97,10 +97,9 @@ TEST_F(MultibyteSplitTest, DelimiterAtEndByteRange) auto expected = strings_column_wrapper{"abcdefg:"}; auto source = cudf::io::text::make_source(host_input); - auto out = cudf::io::text::multibyte_split( - *source, - delimiter, - cudf::io::text::byte_range_info{0, static_cast(host_input.size())}); + cudf::io::text::parse_options options{ + cudf::io::text::byte_range_info{0, static_cast(host_input.size())}}; + auto out = cudf::io::text::multibyte_split(*source, delimiter, options); CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *out); } @@ -113,10 +112,9 @@ TEST_F(MultibyteSplitTest, DelimiterAtEndByteRange2) auto expected = strings_column_wrapper{"abcdefg:"}; auto source = cudf::io::text::make_source(host_input); - auto out = cudf::io::text::multibyte_split( - *source, - delimiter, - cudf::io::text::byte_range_info{0, static_cast(host_input.size() - 1)}); + cudf::io::text::parse_options options{ + cudf::io::text::byte_range_info{0, static_cast(host_input.size() - 1)}}; + auto out = cudf::io::text::multibyte_split(*source, delimiter, options); CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *out); } @@ -277,9 +275,12 @@ TEST_F(MultibyteSplitTest, LargeInputMultipleRange) auto source = cudf::io::text::make_source(host_input); auto byte_ranges = cudf::io::text::create_byte_range_infos_consecutive(host_input.size(), 3); - auto out0 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[0]); - auto out1 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[1]); - auto out2 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[2]); + auto out0 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[0]}); + auto out1 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[1]}); + auto out2 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[2]}); auto out_views = std::vector({out0->view(), out1->view(), out2->view()}); auto out = cudf::concatenate(out_views); @@ -303,9 +304,12 @@ TEST_F(MultibyteSplitTest, LargeInputSparseMultipleRange) auto source = cudf::io::text::make_source(host_input); auto byte_ranges = cudf::io::text::create_byte_range_infos_consecutive(host_input.size(), 3); - auto out0 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[0]); - auto out1 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[1]); - auto out2 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[2]); + auto out0 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[0]}); + auto out1 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[1]}); + auto out2 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[2]}); auto out_views = std::vector({out0->view(), out1->view(), out2->view()}); auto out = cudf::concatenate(out_views); @@ -327,9 +331,12 @@ TEST_F(MultibyteSplitTest, LargeInputMultipleRangeSingleByte) auto source = cudf::io::text::make_source(host_input); auto byte_ranges = cudf::io::text::create_byte_range_infos_consecutive(host_input.size(), 3); - auto out0 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[0]); - auto out1 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[1]); - auto out2 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[2]); + auto out0 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[0]}); + auto out1 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[1]}); + auto out2 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[2]}); auto out_views = std::vector({out0->view(), out1->view(), out2->view()}); auto out = cudf::concatenate(out_views); @@ -352,9 +359,12 @@ TEST_F(MultibyteSplitTest, LargeInputSparseMultipleRangeSingleByte) auto source = cudf::io::text::make_source(host_input); auto byte_ranges = cudf::io::text::create_byte_range_infos_consecutive(host_input.size(), 3); - auto out0 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[0]); - auto out1 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[1]); - auto out2 = cudf::io::text::multibyte_split(*source, delimiter, byte_ranges[2]); + auto out0 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[0]}); + auto out1 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[1]}); + auto out2 = cudf::io::text::multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_ranges[2]}); auto out_views = std::vector({out0->view(), out1->view(), out2->view()}); auto out = cudf::concatenate(out_views); @@ -383,9 +393,14 @@ TEST_F(MultibyteSplitTest, SmallInputAllPossibleRanges) SCOPED_TRACE(split1); for (int split2 = split1 + 1; split2 < size; split2++) { SCOPED_TRACE(split2); - auto out1 = multibyte_split(*source, delimiter, byte_range_info{0, split1}); - auto out2 = multibyte_split(*source, delimiter, byte_range_info{split1, split2 - split1}); - auto out3 = multibyte_split(*source, delimiter, byte_range_info{split2, size - split2}); + auto out1 = multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_range_info{0, split1}}); + auto out2 = + multibyte_split(*source, + delimiter, + cudf::io::text::parse_options{byte_range_info{split1, split2 - split1}}); + auto out3 = multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_range_info{split2, size - split2}}); auto out_views = std::vector({out1->view(), out2->view(), out3->view()}); auto out = cudf::concatenate(out_views); @@ -416,9 +431,14 @@ TEST_F(MultibyteSplitTest, SmallInputAllPossibleRangesSingleByte) SCOPED_TRACE(split1); for (int split2 = split1 + 1; split2 < size; split2++) { SCOPED_TRACE(split2); - auto out1 = multibyte_split(*source, delimiter, byte_range_info{0, split1}); - auto out2 = multibyte_split(*source, delimiter, byte_range_info{split1, split2 - split1}); - auto out3 = multibyte_split(*source, delimiter, byte_range_info{split2, size - split2}); + auto out1 = multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_range_info{0, split1}}); + auto out2 = + multibyte_split(*source, + delimiter, + cudf::io::text::parse_options{byte_range_info{split1, split2 - split1}}); + auto out3 = multibyte_split( + *source, delimiter, cudf::io::text::parse_options{byte_range_info{split2, size - split2}}); auto out_views = std::vector({out1->view(), out2->view(), out3->view()}); auto out = cudf::concatenate(out_views); @@ -441,7 +461,8 @@ TEST_F(MultibyteSplitTest, SingletonRangeAtEnd) auto source = make_source(host_input); auto expected = strings_column_wrapper{}; - auto out = multibyte_split(*source, delimiter, byte_range_info{5, 1}); + auto out = + multibyte_split(*source, delimiter, cudf::io::text::parse_options{byte_range_info{5, 1}}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *out, cudf::test::debug_output_level::ALL_ERRORS); } @@ -480,7 +501,8 @@ TEST_F(MultibyteSplitTest, EmptyRange) auto source = make_source(host_input); auto expected = strings_column_wrapper{}; - auto out = multibyte_split(*source, delimiter, byte_range_info{4, 0}); + auto out = + multibyte_split(*source, delimiter, cudf::io::text::parse_options{byte_range_info{4, 0}}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *out, cudf::test::debug_output_level::ALL_ERRORS); } @@ -493,7 +515,8 @@ TEST_F(MultibyteSplitTest, EmptyRangeSingleByte) auto source = make_source(host_input); auto expected = strings_column_wrapper{}; - auto out = multibyte_split(*source, delimiter, byte_range_info{3, 0}); + auto out = + multibyte_split(*source, delimiter, cudf::io::text::parse_options{byte_range_info{3, 0}}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, *out, cudf::test::debug_output_level::ALL_ERRORS); } diff --git a/cpp/tests/streams/io/multibyte_split_test.cpp b/cpp/tests/streams/io/multibyte_split_test.cpp new file mode 100644 index 00000000000..b0eff1d3340 --- /dev/null +++ b/cpp/tests/streams/io/multibyte_split_test.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024, 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 + +class MultibyteSplitTest : public cudf::test::BaseFixture {}; + +TEST_F(MultibyteSplitTest, Reader) +{ + auto delimiter = std::string(":"); + auto host_input = std::string("abc:def"); + auto source = cudf::io::text::make_source(host_input); + cudf::io::text::parse_options options{}; + auto result = + cudf::io::text::multibyte_split(*source, delimiter, options, cudf::test::get_default_stream()); +} diff --git a/docs/cudf/source/conf.py b/docs/cudf/source/conf.py index 108f12bc099..c3c14ac8cad 100644 --- a/docs/cudf/source/conf.py +++ b/docs/cudf/source/conf.py @@ -372,7 +372,7 @@ def _generate_namespaces(namespaces): _all_namespaces = _generate_namespaces( { # Note that io::datasource is actually a nested class - "cudf": {"io", "io::datasource", "strings", "ast", "ast::expression"}, + "cudf": {"io", "io::datasource", "strings", "ast", "ast::expression", "io::text"}, "numeric": {}, "nvtext": {}, } From 760c15cbd4231e4987149b3a5d68fdcd22654dce Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Mon, 1 Jul 2024 14:27:30 -0400 Subject: [PATCH 03/42] Use verify-alpha-spec hook (#16144) With the deployment of rapids-build-backend, we need to make sure our dependencies have alpha specs. Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16144 --- .pre-commit-config.yaml | 3 ++- conda/environments/all_cuda-118_arch-x86_64.yaml | 5 ++--- conda/environments/all_cuda-122_arch-x86_64.yaml | 7 +++---- dependencies.yaml | 10 +++++----- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f8c4f4b9143..d0457d2c641 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -149,7 +149,7 @@ repos: - id: ruff-format files: python/.*$ - repo: https://github.com/rapidsai/pre-commit-hooks - rev: v0.0.3 + rev: v0.2.0 hooks: - id: verify-copyright exclude: | @@ -158,6 +158,7 @@ repos: cpp/src/io/parquet/ipc/Message_generated[.]h$| cpp/src/io/parquet/ipc/Schema_generated[.]h$ ) + - id: verify-alpha-spec default_language_version: python: python3 diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 946e2d1cd32..cc9238ab80a 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -26,7 +26,6 @@ dependencies: - cupy>=12.0.0 - cxx-compiler - cython>=3.0.3 -- dask-cuda==24.8.* - dask-cuda==24.8.*,>=0.0.0a0 - dlpack>=0.8,<1.0 - doxygen=1.9.1 @@ -44,10 +43,10 @@ dependencies: - libcufile=1.4.0.31 - libcurand-dev=10.3.0.86 - libcurand=10.3.0.86 -- libkvikio==24.8.* +- libkvikio==24.8.*,>=0.0.0a0 - libparquet==16.1.0.* - librdkafka>=1.9.0,<1.10.0a0 -- librmm==24.8.* +- librmm==24.8.*,>=0.0.0a0 - make - moto>=4.0.8 - msgpack-python diff --git a/conda/environments/all_cuda-122_arch-x86_64.yaml b/conda/environments/all_cuda-122_arch-x86_64.yaml index f069616ddbe..9fecd452248 100644 --- a/conda/environments/all_cuda-122_arch-x86_64.yaml +++ b/conda/environments/all_cuda-122_arch-x86_64.yaml @@ -27,7 +27,6 @@ dependencies: - cupy>=12.0.0 - cxx-compiler - cython>=3.0.3 -- dask-cuda==24.8.* - dask-cuda==24.8.*,>=0.0.0a0 - dlpack>=0.8,<1.0 - doxygen=1.9.1 @@ -43,10 +42,10 @@ dependencies: - libarrow==16.1.0.* - libcufile-dev - libcurand-dev -- libkvikio==24.8.* +- libkvikio==24.8.*,>=0.0.0a0 - libparquet==16.1.0.* - librdkafka>=1.9.0,<1.10.0a0 -- librmm==24.8.* +- librmm==24.8.*,>=0.0.0a0 - make - moto>=4.0.8 - msgpack-python @@ -66,7 +65,7 @@ dependencies: - pre-commit - pyarrow==16.1.0.* - pydata-sphinx-theme!=0.14.2 -- pynvjitlink +- pynvjitlink>=0.0.0a0 - pytest-benchmark - pytest-cases>=3.8.2 - pytest-cov diff --git a/dependencies.yaml b/dependencies.yaml index 38ec30a8033..9efbc47896c 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -287,8 +287,8 @@ dependencies: - output_types: conda packages: - fmt>=10.1.1,<11 - - librmm==24.8.* - - libkvikio==24.8.* + - librmm==24.8.*,>=0.0.0a0 + - libkvikio==24.8.*,>=0.0.0a0 - librdkafka>=1.9.0,<1.10.0a0 # Align nvcomp version with rapids-cmake - nvcomp==3.0.6 @@ -500,7 +500,7 @@ dependencies: - output_types: [conda] packages: - breathe>=4.35.0 - - dask-cuda==24.8.* + - dask-cuda==24.8.*,>=0.0.0a0 - *doxygen - make - myst-nb @@ -582,7 +582,7 @@ dependencies: matrices: - matrix: {cuda: "12.*"} packages: - - pynvjitlink + - pynvjitlink>=0.0.0a0 - matrix: {cuda: "11.*"} packages: - cubinlinker @@ -592,7 +592,7 @@ dependencies: - matrix: {cuda: "12.*"} packages: - rmm-cu12==24.8.*,>=0.0.0a0 - - pynvjitlink-cu12 + - pynvjitlink-cu12>=0.0.0a0 - matrix: {cuda: "11.*"} packages: - rmm-cu11==24.8.*,>=0.0.0a0 From 08552f816ddf21288448997e4998c3e1e0e58f5f Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 2 Jul 2024 03:12:50 +0100 Subject: [PATCH 04/42] Update cudf-polars for v1 release of polars (#16149) Minor changes to the IR, which we adapt to, and request `polars>=1.0` in dependencies. Authors: - Lawrence Mitchell (https://github.com/wence-) - Thomas Li (https://github.com/lithomas1) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16149 --- ci/test_cudf_polars.sh | 4 +--- dependencies.yaml | 2 +- python/cudf_polars/cudf_polars/dsl/expr.py | 6 +++--- python/cudf_polars/cudf_polars/dsl/ir.py | 11 +++++++++-- python/cudf_polars/cudf_polars/dsl/translate.py | 6 ++++-- python/cudf_polars/pyproject.toml | 2 +- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/ci/test_cudf_polars.sh b/ci/test_cudf_polars.sh index 669e049ab26..95fb4b431bf 100755 --- a/ci/test_cudf_polars.sh +++ b/ci/test_cudf_polars.sh @@ -28,10 +28,8 @@ rapids-logger "Install cudf wheel" # echo to expand wildcard before adding `[extra]` requires for pip python -m pip install $(echo ./dist/cudf*.whl)[test] -rapids-logger "Install polars (allow pre-release versions)" -python -m pip install 'polars>=1.0.0a0' - rapids-logger "Install cudf_polars" +python -m pip install 'polars>=1.0' python -m pip install --no-deps python/cudf_polars rapids-logger "Run cudf_polars tests" diff --git a/dependencies.yaml b/dependencies.yaml index 9efbc47896c..e3f8a72e76c 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -603,7 +603,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - polars>=0.20.30 + - polars>=1.0 run_dask_cudf: common: - output_types: [conda, requirements, pyproject] diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index 16cfd9b9749..fe859c8d958 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -978,15 +978,15 @@ def collect_agg(self, *, depth: int) -> AggInfo: class Agg(Expr): __slots__ = ("name", "options", "op", "request", "children") _non_child = ("dtype", "name", "options") - children: tuple[Expr] + children: tuple[Expr, ...] def __init__( - self, dtype: plc.DataType, name: str, options: Any, value: Expr + self, dtype: plc.DataType, name: str, options: Any, *children: Expr ) -> None: super().__init__(dtype) self.name = name self.options = options - self.children = (value,) + self.children = children if name not in Agg._SUPPORTED: raise NotImplementedError( f"Unsupported aggregation {name=}" diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index abe26b14a90..9b3096becd4 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -15,6 +15,7 @@ import dataclasses import itertools +import json import types from functools import cache from typing import TYPE_CHECKING, Any, Callable, ClassVar @@ -180,8 +181,10 @@ def __post_init__(self): class Scan(IR): """Input from files.""" - typ: Any + typ: str """What type of file are we reading? Parquet, CSV, etc...""" + options: tuple[Any, ...] + """Type specific options, as json-encoded strings.""" paths: list[str] """List of paths to read from.""" file_options: Any @@ -211,17 +214,21 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: with_columns = options.with_columns row_index = options.row_index if self.typ == "csv": + opts, cloud_opts = map(json.loads, self.options) df = DataFrame.from_cudf( cudf.concat( [cudf.read_csv(p, usecols=with_columns) for p in self.paths] ) ) elif self.typ == "parquet": + opts, cloud_opts = map(json.loads, self.options) cdf = cudf.read_parquet(self.paths, columns=with_columns) assert isinstance(cdf, cudf.DataFrame) df = DataFrame.from_cudf(cdf) else: - assert_never(self.typ) + raise NotImplementedError( + f"Unhandled scan type: {self.typ}" + ) # pragma: no cover; post init trips first if row_index is not None: name, offset = row_index dtype = self.schema[name] diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index f4bf07ae1e0..a2fdb3c3d79 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -87,9 +87,11 @@ def _( def _( node: pl_ir.Scan, visitor: NodeTraverser, schema: dict[str, plc.DataType] ) -> ir.IR: + typ, *options = node.scan_type return ir.Scan( schema, - node.scan_type, + typ, + tuple(options), node.paths, node.file_options, translate_named_expr(visitor, n=node.predicate) @@ -445,7 +447,7 @@ def _(node: pl_expr.Agg, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Ex dtype, node.name, node.options, - translate_expr(visitor, n=node.arguments), + *(translate_expr(visitor, n=n) for n in node.arguments), ) diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index effa4861e0c..bf4673fcc50 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -20,7 +20,7 @@ license = { text = "Apache 2.0" } requires-python = ">=3.9" dependencies = [ "cudf==24.8.*,>=0.0.0a0", - "polars>=0.20.30", + "polars>=1.0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ "Intended Audience :: Developers", From a4be7bd1365ec7ede5191a4b5d74e7c514a2b5fe Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Tue, 2 Jul 2024 00:50:42 -0700 Subject: [PATCH 05/42] Use Arrow C Data Interface functions for Python interop (#15904) This PR replaces the internals of `from_arrow` in pylibcudf with an implementation that uses the [Arrow C Data Interface](https://arrow.apache.org/docs/format/CDataInterface.html) using the [Python Capsule interface](https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html). This allows us to decouple our Python builds from using pyarrow Cython (partially, we haven't replaced the `to_arrow` conversion yet) and it will also allow us to support any other Python package that is a producer of the data interface. To support the above functionality, the following additional changes were needed in this PR: - Added the ability to produce cudf tables from `ArrowArrayStream` objects since that is what `pyarrow.Table` produces. This function is a simple wrapper around the existing `from_arrrow(ArrowArray)` API. - Added support for the large strings type, for which support has improved throughout cudf since the `from_arrow_host` API was added and for which we now require a basic overload for tests to pass. I did not add corresponding support for `from_arrow_device` to avoid ballooning the scope of this PR, so that work can be done in a follow-up. - Proper handling of `type_id::EMPTY` in concatenate because the most natural implementation of the ArrowArrayStream processing is to run `from_arrow` on each chunk and then concatenate the outputs, and from the Python side we can produce chunks of all null arrays from arrow. Contributes to #14926 Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Matthew Roeschke (https://github.com/mroeschke) - Robert Maynard (https://github.com/robertmaynard) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/15904 --- cpp/CMakeLists.txt | 1 + cpp/include/cudf/interop.hpp | 38 ++++- cpp/src/copying/concatenate.cu | 28 +++- cpp/src/interop/arrow_utilities.cpp | 3 +- cpp/src/interop/from_arrow_device.cu | 3 + cpp/src/interop/from_arrow_host.cu | 32 +++- cpp/src/interop/from_arrow_stream.cu | 143 ++++++++++++++++++ cpp/tests/CMakeLists.txt | 1 + cpp/tests/copying/concatenate_tests.cpp | 60 ++++++++ cpp/tests/interop/from_arrow_stream_test.cpp | 121 +++++++++++++++ cpp/tests/interop/nanoarrow_utils.hpp | 3 + python/cudf/cudf/_lib/pylibcudf/interop.pyx | 36 ++++- .../cudf/_lib/pylibcudf/libcudf/interop.pxd | 20 +++ python/cudf/cudf/tests/test_series.py | 2 - 14 files changed, 466 insertions(+), 25 deletions(-) create mode 100644 cpp/src/interop/from_arrow_stream.cu create mode 100644 cpp/tests/interop/from_arrow_stream_test.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 35cf90411f2..54070ab6f5a 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -365,6 +365,7 @@ add_library( src/interop/to_arrow_device.cu src/interop/from_arrow_device.cu src/interop/from_arrow_host.cu + src/interop/from_arrow_stream.cu src/interop/to_arrow_schema.cpp src/interop/detail/arrow_allocator.cpp src/io/avro/avro.cpp diff --git a/cpp/include/cudf/interop.hpp b/cpp/include/cudf/interop.hpp index 56ec62fa6e1..502ffb9ba4f 100644 --- a/cpp/include/cudf/interop.hpp +++ b/cpp/include/cudf/interop.hpp @@ -50,6 +50,8 @@ struct ArrowSchema; struct ArrowArray; +struct ArrowArrayStream; + namespace cudf { /** * @addtogroup interop_dlpack @@ -367,10 +369,11 @@ std::unique_ptr from_arrow( * @param mr Device memory resource used to allocate `cudf::table` * @return cudf table generated from given arrow data */ -std::unique_ptr from_arrow(ArrowSchema const* schema, - ArrowArray const* input, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr); +std::unique_ptr from_arrow( + ArrowSchema const* schema, + ArrowArray const* input, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); /** * @brief Create `cudf::column` from a given ArrowArray and ArrowSchema input @@ -385,10 +388,11 @@ std::unique_ptr from_arrow(ArrowSchema const* schema, * @param mr Device memory resource used to allocate `cudf::column` * @return cudf column generated from given arrow data */ -std::unique_ptr from_arrow_column(ArrowSchema const* schema, - ArrowArray const* input, - rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr); +std::unique_ptr from_arrow_column( + ArrowSchema const* schema, + ArrowArray const* input, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); /** * @brief Create `cudf::table` from given ArrowDeviceArray input @@ -414,6 +418,24 @@ std::unique_ptr from_arrow_host( rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); +/** + * @brief Create `cudf::table` from given ArrowArrayStream input + * + * @throws std::invalid_argument if input is NULL + * + * The conversion WILL release the input ArrayArrayStream and its constituent + * arrays or schema since Arrow streams are not suitable for multiple reads. + * + * @param input `ArrowArrayStream` pointer to object that will produce ArrowArray data + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used to perform cuda allocation + * @return cudf table generated from the given Arrow data + */ +std::unique_ptr
from_arrow_stream( + ArrowArrayStream* input, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + /** * @brief Create `cudf::column` from given ArrowDeviceArray input * diff --git a/cpp/src/copying/concatenate.cu b/cpp/src/copying/concatenate.cu index 47e74a5cb48..6acbafd24fb 100644 --- a/cpp/src/copying/concatenate.cu +++ b/cpp/src/copying/concatenate.cu @@ -463,10 +463,6 @@ void traverse_children::operator()(host_span */ void bounds_and_type_check(host_span cols, rmm::cuda_stream_view stream) { - CUDF_EXPECTS(cudf::all_have_same_types(cols.begin(), cols.end()), - "Type mismatch in columns to concatenate.", - cudf::data_type_error); - // total size of all concatenated rows size_t const total_row_count = std::accumulate(cols.begin(), cols.end(), std::size_t{}, [](size_t a, auto const& b) { @@ -476,6 +472,21 @@ void bounds_and_type_check(host_span cols, rmm::cuda_stream_v "Total number of concatenated rows exceeds the column size limit", std::overflow_error); + if (std::any_of(cols.begin(), cols.end(), [](column_view const& c) { + return c.type().id() == cudf::type_id::EMPTY; + })) { + CUDF_EXPECTS( + std::all_of(cols.begin(), + cols.end(), + [](column_view const& c) { return c.type().id() == cudf::type_id::EMPTY; }), + "Mismatch in columns to concatenate.", + cudf::data_type_error); + return; + } + CUDF_EXPECTS(cudf::all_have_same_types(cols.begin(), cols.end()), + "Type mismatch in columns to concatenate.", + cudf::data_type_error); + // traverse children cudf::type_dispatcher(cols.front().type(), traverse_children{}, cols, stream); } @@ -498,6 +509,15 @@ std::unique_ptr concatenate(host_span columns_to_conc return empty_like(columns_to_concat.front()); } + // For empty columns, we can just create an EMPTY column of the appropriate length. + if (columns_to_concat.front().type().id() == cudf::type_id::EMPTY) { + auto length = std::accumulate( + columns_to_concat.begin(), columns_to_concat.end(), 0, [](auto a, auto const& b) { + return a + b.size(); + }); + return std::make_unique( + data_type(type_id::EMPTY), length, rmm::device_buffer{}, rmm::device_buffer{}, length); + } return type_dispatcher( columns_to_concat.front().type(), concatenate_dispatch{columns_to_concat, stream, mr}); } diff --git a/cpp/src/interop/arrow_utilities.cpp b/cpp/src/interop/arrow_utilities.cpp index dd9e9600a87..605d813ed1e 100644 --- a/cpp/src/interop/arrow_utilities.cpp +++ b/cpp/src/interop/arrow_utilities.cpp @@ -39,7 +39,8 @@ data_type arrow_to_cudf_type(ArrowSchemaView const* arrow_view) case NANOARROW_TYPE_FLOAT: return data_type(type_id::FLOAT32); case NANOARROW_TYPE_DOUBLE: return data_type(type_id::FLOAT64); case NANOARROW_TYPE_DATE32: return data_type(type_id::TIMESTAMP_DAYS); - case NANOARROW_TYPE_STRING: return data_type(type_id::STRING); + case NANOARROW_TYPE_STRING: + case NANOARROW_TYPE_LARGE_STRING: return data_type(type_id::STRING); case NANOARROW_TYPE_LIST: return data_type(type_id::LIST); case NANOARROW_TYPE_DICTIONARY: return data_type(type_id::DICTIONARY32); case NANOARROW_TYPE_STRUCT: return data_type(type_id::STRUCT); diff --git a/cpp/src/interop/from_arrow_device.cu b/cpp/src/interop/from_arrow_device.cu index 002a8ec1f14..73c1a474310 100644 --- a/cpp/src/interop/from_arrow_device.cu +++ b/cpp/src/interop/from_arrow_device.cu @@ -143,6 +143,9 @@ dispatch_tuple_t dispatch_from_arrow_device::operator()( rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr) { + CUDF_EXPECTS(schema->type != NANOARROW_TYPE_LARGE_STRING, + "Large strings are not yet supported in from_arrow_device", + cudf::data_type_error); if (input->length == 0) { return std::make_tuple( {type, diff --git a/cpp/src/interop/from_arrow_host.cu b/cpp/src/interop/from_arrow_host.cu index 854a1d68fdc..b7e07056686 100644 --- a/cpp/src/interop/from_arrow_host.cu +++ b/cpp/src/interop/from_arrow_host.cu @@ -188,8 +188,16 @@ std::unique_ptr dispatch_copy_from_arrow_host::operator()(offset_buffers[1])[input->length + input->offset]; + int64_t const char_data_length = [&]() { + if (schema->type == NANOARROW_TYPE_LARGE_STRING) { + return reinterpret_cast(offset_buffers[1])[input->length + input->offset]; + } else if (schema->type == NANOARROW_TYPE_STRING) { + return static_cast( + reinterpret_cast(offset_buffers[1])[input->length + input->offset]); + } else { + CUDF_FAIL("Unsupported string type", cudf::data_type_error); + } + }(); void const* char_buffers[2] = {nullptr, input->buffers[2]}; ArrowArray char_array = { .length = char_data_length, @@ -210,15 +218,27 @@ std::unique_ptr dispatch_copy_from_arrow_host::operator()operator()(&view, &offsets_array, data_type(type_id::INT32), true); + auto offsets_column = [&]() { + if (schema->type == NANOARROW_TYPE_LARGE_STRING) { + return this->operator()(&view, &offsets_array, data_type(type_id::INT64), true); + } else if (schema->type == NANOARROW_TYPE_STRING) { + return this->operator()(&view, &offsets_array, data_type(type_id::INT32), true); + } else { + CUDF_FAIL("Unsupported string type", cudf::data_type_error); + } + }(); NANOARROW_THROW_NOT_OK(ArrowSchemaViewInit(&view, char_data_schema.get(), nullptr)); - auto chars_column = this->operator()(&view, &char_array, data_type(type_id::INT8), true); + rmm::device_buffer chars(char_data_length, stream, mr); + CUDF_CUDA_TRY(cudaMemcpyAsync(chars.data(), + reinterpret_cast(char_array.buffers[1]), + chars.size(), + cudaMemcpyDefault, + stream.value())); auto const num_rows = offsets_column->size() - 1; auto out_col = make_strings_column(num_rows, std::move(offsets_column), - std::move(chars_column->release().data.release()[0]), + std::move(chars), input->null_count, std::move(*get_mask_buffer(input))); diff --git a/cpp/src/interop/from_arrow_stream.cu b/cpp/src/interop/from_arrow_stream.cu new file mode 100644 index 00000000000..0c85b561944 --- /dev/null +++ b/cpp/src/interop/from_arrow_stream.cu @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024, 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 "arrow_utilities.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace cudf { +namespace detail { + +namespace { + +std::unique_ptr make_empty_column_from_schema(ArrowSchema const* schema, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + ArrowSchemaView schema_view; + NANOARROW_THROW_NOT_OK(ArrowSchemaViewInit(&schema_view, schema, nullptr)); + + auto const type{arrow_to_cudf_type(&schema_view)}; + switch (type.id()) { + case type_id::EMPTY: { + return std::make_unique( + data_type(type_id::EMPTY), 0, rmm::device_buffer{}, rmm::device_buffer{}, 0); + } + case type_id::LIST: { + return cudf::make_lists_column(0, + cudf::make_empty_column(data_type{type_id::INT32}), + make_empty_column_from_schema(schema->children[0], stream, mr), + 0, + {}, + stream, + mr); + } + case type_id::STRUCT: { + std::vector> child_columns; + child_columns.reserve(schema->n_children); + std::transform( + schema->children, + schema->children + schema->n_children, + std::back_inserter(child_columns), + [&](auto const& child) { return make_empty_column_from_schema(child, stream, mr); }); + return cudf::make_structs_column(0, std::move(child_columns), 0, {}, stream, mr); + } + default: { + return cudf::make_empty_column(type); + } + } +} + +} // namespace + +std::unique_ptr
from_arrow_stream(ArrowArrayStream* input, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + CUDF_EXPECTS(input != nullptr, "input ArrowArrayStream must not be NULL", std::invalid_argument); + + // Potential future optimization: Since the from_arrow API accepts an + // ArrowSchema we're allocating one here instead of using a view, which we + // could avoid with a different underlying implementation. + ArrowSchema schema; + NANOARROW_THROW_NOT_OK(ArrowArrayStreamGetSchema(input, &schema, nullptr)); + + std::vector> chunks; + ArrowArray chunk; + while (true) { + NANOARROW_THROW_NOT_OK(ArrowArrayStreamGetNext(input, &chunk, nullptr)); + if (chunk.release == nullptr) { break; } + chunks.push_back(from_arrow(&schema, &chunk, stream, mr)); + chunk.release(&chunk); + } + input->release(input); + + if (chunks.empty()) { + if (schema.n_children == 0) { + schema.release(&schema); + return std::make_unique(); + } + + // If there are no chunks but the schema has children, we need to construct a suitable empty + // table. + std::vector> columns; + columns.reserve(chunks.size()); + std::transform( + schema.children, + schema.children + schema.n_children, + std::back_inserter(columns), + [&](auto const& child) { return make_empty_column_from_schema(child, stream, mr); }); + schema.release(&schema); + return std::make_unique(std::move(columns)); + } + + schema.release(&schema); + + auto chunk_views = std::vector{}; + chunk_views.reserve(chunks.size()); + std::transform( + chunks.begin(), chunks.end(), std::back_inserter(chunk_views), [](auto const& chunk) { + return chunk->view(); + }); + return cudf::detail::concatenate(chunk_views, stream, mr); +} + +} // namespace detail + +std::unique_ptr
from_arrow_stream(ArrowArrayStream* input, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + CUDF_FUNC_RANGE(); + return detail::from_arrow_stream(input, stream, mr); +} +} // namespace cudf diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 244bcb7d897..0eab9ba61d8 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -273,6 +273,7 @@ ConfigureTest( interop/from_arrow_test.cpp interop/from_arrow_device_test.cpp interop/from_arrow_host_test.cpp + interop/from_arrow_stream_test.cpp interop/dlpack_test.cpp EXTRA_LIB nanoarrow diff --git a/cpp/tests/copying/concatenate_tests.cpp b/cpp/tests/copying/concatenate_tests.cpp index 054441788d0..18140c34abd 100644 --- a/cpp/tests/copying/concatenate_tests.cpp +++ b/cpp/tests/copying/concatenate_tests.cpp @@ -1667,3 +1667,63 @@ TEST_F(DictionaryConcatTest, ErrorsTest) std::vector empty; EXPECT_THROW(cudf::concatenate(empty), cudf::logic_error); } + +struct EmptyColumnTest : public cudf::test::BaseFixture {}; + +TEST_F(EmptyColumnTest, SimpleTest) +{ + std::vector columns; + constexpr auto num_copies = 10; + constexpr auto num_rows = 10; + for (auto i = 0; i < num_copies; ++i) { + columns.emplace_back(cudf::data_type(cudf::type_id::EMPTY), + num_rows, + rmm::device_buffer{}, + rmm::device_buffer{}, + 0); + } + + // Create views from columns + std::vector views; + for (auto& col : columns) { + views.push_back(col.view()); + } + auto result = cudf::concatenate(views); + + ASSERT_EQ(result->size(), num_copies * num_rows); + ASSERT_EQ(result->type().id(), cudf::type_id::EMPTY); +} + +struct TableOfEmptyColumnsTest : public cudf::test::BaseFixture {}; + +TEST_F(TableOfEmptyColumnsTest, SimpleTest) +{ + std::vector tables; + constexpr auto num_copies = 10; + constexpr auto num_rows = 10; + constexpr auto num_columns = 10; + for (auto i = 0; i < num_copies; ++i) { + std::vector> columns; + for (auto j = 0; j < num_columns; ++j) { + columns.push_back(std::make_unique(cudf::data_type(cudf::type_id::EMPTY), + num_rows, + rmm::device_buffer{}, + rmm::device_buffer{}, + 0)); + } + tables.emplace_back(std::move(columns)); + } + + // Create views from columns + std::vector views; + for (auto& tbl : tables) { + views.push_back(tbl.view()); + } + auto result = cudf::concatenate(views); + + ASSERT_EQ(result->num_rows(), num_copies * num_rows); + ASSERT_EQ(result->num_columns(), num_columns); + for (auto i = 0; i < num_columns; ++i) { + ASSERT_EQ(result->get_column(i).type().id(), cudf::type_id::EMPTY); + } +} diff --git a/cpp/tests/interop/from_arrow_stream_test.cpp b/cpp/tests/interop/from_arrow_stream_test.cpp new file mode 100644 index 00000000000..418ec057303 --- /dev/null +++ b/cpp/tests/interop/from_arrow_stream_test.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024, 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 "nanoarrow_utils.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +struct VectorOfArrays { + std::vector arrays; + nanoarrow::UniqueSchema schema; + size_t index{0}; + + static int get_schema(ArrowArrayStream* stream, ArrowSchema* out_schema) + { + auto private_data = static_cast(stream->private_data); + ArrowSchemaDeepCopy(private_data->schema.get(), out_schema); + return 0; + } + + static int get_next(ArrowArrayStream* stream, ArrowArray* out_array) + { + auto private_data = static_cast(stream->private_data); + if (private_data->index >= private_data->arrays.size()) { + out_array->release = nullptr; + return 0; + } + ArrowArrayMove(private_data->arrays[private_data->index++].get(), out_array); + return 0; + } + + static const char* get_last_error(ArrowArrayStream* stream) { return nullptr; } + + static void release(ArrowArrayStream* stream) + { + delete static_cast(stream->private_data); + } +}; + +struct FromArrowStreamTest : public cudf::test::BaseFixture {}; + +void makeStreamFromArrays(std::vector arrays, + nanoarrow::UniqueSchema schema, + ArrowArrayStream* out) +{ + auto* private_data = new VectorOfArrays{std::move(arrays), std::move(schema)}; + out->get_schema = VectorOfArrays::get_schema; + out->get_next = VectorOfArrays::get_next; + out->get_last_error = VectorOfArrays::get_last_error; + out->release = VectorOfArrays::release; + out->private_data = private_data; +} + +TEST_F(FromArrowStreamTest, BasicTest) +{ + constexpr auto num_copies = 3; + std::vector> tables; + // The schema is unique across all tables. + nanoarrow::UniqueSchema schema; + std::vector arrays; + for (auto i = 0; i < num_copies; ++i) { + auto [tbl, sch, arr] = get_nanoarrow_host_tables(0); + tables.push_back(std::move(tbl)); + arrays.push_back(std::move(arr)); + if (i == 0) { sch.move(schema.get()); } + } + std::vector table_views; + for (auto const& table : tables) { + table_views.push_back(table->view()); + } + auto expected = cudf::concatenate(table_views); + + ArrowArrayStream stream; + makeStreamFromArrays(std::move(arrays), std::move(schema), &stream); + auto result = cudf::from_arrow_stream(&stream); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected->view(), result->view()); +} + +TEST_F(FromArrowStreamTest, EmptyTest) +{ + auto [tbl, sch, arr] = get_nanoarrow_host_tables(0); + std::vector table_views{tbl->view()}; + auto expected = cudf::concatenate(table_views); + + ArrowArrayStream stream; + makeStreamFromArrays({}, std::move(sch), &stream); + auto result = cudf::from_arrow_stream(&stream); + cudf::have_same_types(expected->view(), result->view()); +} diff --git a/cpp/tests/interop/nanoarrow_utils.hpp b/cpp/tests/interop/nanoarrow_utils.hpp index 94c4372e74a..4147728b2a6 100644 --- a/cpp/tests/interop/nanoarrow_utils.hpp +++ b/cpp/tests/interop/nanoarrow_utils.hpp @@ -375,3 +375,6 @@ nanoarrow::UniqueArray get_nanoarrow_list_array(std::initializer_list data, std::tuple, nanoarrow::UniqueSchema, generated_test_data> get_nanoarrow_cudf_table(cudf::size_type length); + +std::tuple, nanoarrow::UniqueSchema, nanoarrow::UniqueArray> +get_nanoarrow_host_tables(cudf::size_type length); diff --git a/python/cudf/cudf/_lib/pylibcudf/interop.pyx b/python/cudf/cudf/_lib/pylibcudf/interop.pyx index 07e9d1ead11..adf7e1fd7e8 100644 --- a/python/cudf/cudf/_lib/pylibcudf/interop.pyx +++ b/python/cudf/cudf/_lib/pylibcudf/interop.pyx @@ -1,5 +1,6 @@ # Copyright (c) 2023-2024, NVIDIA CORPORATION. +from cpython cimport pycapsule from cython.operator cimport dereference from libcpp.memory cimport shared_ptr, unique_ptr from libcpp.utility cimport move @@ -11,9 +12,15 @@ from functools import singledispatch from pyarrow import lib as pa +from cudf._lib.pylibcudf.libcudf.column.column cimport column from cudf._lib.pylibcudf.libcudf.interop cimport ( + ArrowArray, + ArrowArrayStream, + ArrowSchema, column_metadata, from_arrow as cpp_from_arrow, + from_arrow_column as cpp_from_arrow_column, + from_arrow_stream as cpp_from_arrow_stream, to_arrow as cpp_to_arrow, ) from cudf._lib.pylibcudf.libcudf.scalar.scalar cimport ( @@ -124,11 +131,15 @@ def _from_arrow_datatype(pyarrow_object): def _from_arrow_table(pyarrow_object, *, DataType data_type=None): if data_type is not None: raise ValueError("data_type may not be passed for tables") - cdef shared_ptr[pa.CTable] arrow_table = pa.pyarrow_unwrap_table(pyarrow_object) + stream = pyarrow_object.__arrow_c_stream__() + cdef ArrowArrayStream* c_stream = ( + pycapsule.PyCapsule_GetPointer(stream, "arrow_array_stream") + ) cdef unique_ptr[table] c_result with nogil: - c_result = move(cpp_from_arrow(dereference(arrow_table))) + # The libcudf function here will release the stream. + c_result = move(cpp_from_arrow_stream(c_stream)) return Table.from_libcudf(move(c_result)) @@ -190,8 +201,25 @@ def _from_arrow_scalar(pyarrow_object, *, DataType data_type=None): def _from_arrow_column(pyarrow_object, *, DataType data_type=None): if data_type is not None: raise ValueError("data_type may not be passed for arrays") - pa_table = pa.table([pyarrow_object], [""]) - return from_arrow(pa_table).columns()[0] + + schema, array = pyarrow_object.__arrow_c_array__() + cdef ArrowSchema* c_schema = ( + pycapsule.PyCapsule_GetPointer(schema, "arrow_schema") + ) + cdef ArrowArray* c_array = ( + pycapsule.PyCapsule_GetPointer(array, "arrow_array") + ) + + cdef unique_ptr[column] c_result + with nogil: + c_result = move(cpp_from_arrow_column(c_schema, c_array)) + + # The capsule destructors should release automatically for us, but we + # choose to do it explicitly here for clarity. + c_schema.release(c_schema) + c_array.release(c_array) + + return Column.from_libcudf(move(c_result)) @singledispatch diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/interop.pxd b/python/cudf/cudf/_lib/pylibcudf/libcudf/interop.pxd index 471b78505fb..2151da28d4b 100644 --- a/python/cudf/cudf/_lib/pylibcudf/libcudf/interop.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/interop.pxd @@ -7,6 +7,7 @@ from pyarrow.lib cimport CScalar, CTable from cudf._lib.types import cudf_to_np_types, np_to_cudf_types +from cudf._lib.pylibcudf.libcudf.column.column cimport column from cudf._lib.pylibcudf.libcudf.scalar.scalar cimport scalar from cudf._lib.pylibcudf.libcudf.table.table cimport table from cudf._lib.pylibcudf.libcudf.table.table_view cimport table_view @@ -16,6 +17,19 @@ cdef extern from "dlpack/dlpack.h" nogil: ctypedef struct DLManagedTensor: void(*deleter)(DLManagedTensor*) except + + +# The Arrow structs are not namespaced. +cdef extern from "cudf/interop.hpp" nogil: + cdef struct ArrowSchema: + void (*release)(ArrowSchema*) noexcept nogil + + cdef struct ArrowArray: + void (*release)(ArrowArray*) noexcept nogil + + cdef struct ArrowArrayStream: + void (*release)(ArrowArrayStream*) noexcept nogil + + cdef extern from "cudf/interop.hpp" namespace "cudf" \ nogil: cdef unique_ptr[table] from_dlpack(const DLManagedTensor* tensor @@ -42,3 +56,9 @@ cdef extern from "cudf/interop.hpp" namespace "cudf" \ const scalar& input, column_metadata metadata, ) except + + + cdef unique_ptr[table] from_arrow_stream(ArrowArrayStream* input) except + + cdef unique_ptr[column] from_arrow_column( + const ArrowSchema* schema, + const ArrowArray* input + ) except + diff --git a/python/cudf/cudf/tests/test_series.py b/python/cudf/cudf/tests/test_series.py index f2501041f25..8ed78d804bf 100644 --- a/python/cudf/cudf/tests/test_series.py +++ b/python/cudf/cudf/tests/test_series.py @@ -2757,8 +2757,6 @@ def test_series_from_large_string(pa_type): assert_eq(expected, got) - assert pa_string_array.equals(got.to_arrow()) - @pytest.mark.parametrize( "scalar", From a1447c78b8290277b7dbc680479de0c9f4ce0b19 Mon Sep 17 00:00:00 2001 From: Robert Maynard Date: Tue, 2 Jul 2024 09:34:29 -0400 Subject: [PATCH 06/42] Promote has_nested_columns to cudf public API (#16131) The `has_nested_columns` functionality is used in numerous tests. It looks like it should be part of our stable public API. Authors: - Robert Maynard (https://github.com/robertmaynard) Approvers: - Muhammad Haseeb (https://github.com/mhaseeb123) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/16131 --- .../cudf/table/experimental/row_operators.cuh | 12 +++---- cpp/include/cudf/table/table_view.hpp | 19 ++++++++-- cpp/src/table/table_view.cpp | 9 ++--- .../table/experimental_row_operator_tests.cu | 36 +++++++++---------- .../table/row_operator_tests_utilities.cu | 4 +-- .../table/row_operator_tests_utilities2.cu | 2 +- 6 files changed, 44 insertions(+), 38 deletions(-) diff --git a/cpp/include/cudf/table/experimental/row_operators.cuh b/cpp/include/cudf/table/experimental/row_operators.cuh index e9b81a525fc..c181ac7d402 100644 --- a/cpp/include/cudf/table/experimental/row_operators.cuh +++ b/cpp/include/cudf/table/experimental/row_operators.cuh @@ -252,7 +252,7 @@ using optional_dremel_view = thrust::optional; * * @tparam has_nested_columns compile-time optimization for primitive types. * This template parameter is to be used by the developer by querying - * `cudf::detail::has_nested_columns(input)`. `true` compiles operator + * `cudf::has_nested_columns(input)`. `true` compiles operator * overloads for nested types, while `false` only compiles operator * overloads for primitive types. * @tparam Nullate A cudf::nullate type describing whether to check for nulls. @@ -1014,7 +1014,7 @@ class self_comparator { * * @tparam has_nested_columns compile-time optimization for primitive types. * This template parameter is to be used by the developer by querying - * `cudf::detail::has_nested_columns(input)`. `true` compiles operator + * `cudf::has_nested_columns(input)`. `true` compiles operator * overloads for nested types, while `false` only compiles operator * overloads for primitive types. * @tparam Nullate A cudf::nullate type describing whether to check for nulls. @@ -1186,7 +1186,7 @@ class two_table_comparator { * * @tparam has_nested_columns compile-time optimization for primitive types. * This template parameter is to be used by the developer by querying - * `cudf::detail::has_nested_columns(input)`. `true` compiles operator + * `cudf::has_nested_columns(input)`. `true` compiles operator * overloads for nested types, while `false` only compiles operator * overloads for primitive types. * @tparam Nullate A cudf::nullate type describing whether to check for nulls. @@ -1326,7 +1326,7 @@ struct nan_equal_physical_equality_comparator { * * @tparam has_nested_columns compile-time optimization for primitive types. * This template parameter is to be used by the developer by querying - * `cudf::detail::has_nested_columns(input)`. `true` compiles operator + * `cudf::has_nested_columns(input)`. `true` compiles operator * overloads for nested types, while `false` only compiles operator * overloads for primitive types. * @tparam Nullate A cudf::nullate type describing whether to check for nulls. @@ -1643,7 +1643,7 @@ class self_comparator { * * @tparam has_nested_columns compile-time optimization for primitive types. * This template parameter is to be used by the developer by querying - * `cudf::detail::has_nested_columns(input)`. `true` compiles operator + * `cudf::has_nested_columns(input)`. `true` compiles operator * overloads for nested types, while `false` only compiles operator * overloads for primitive types. * @tparam Nullate A cudf::nullate type describing whether to check for nulls. @@ -1757,7 +1757,7 @@ class two_table_comparator { * * @tparam has_nested_columns compile-time optimization for primitive types. * This template parameter is to be used by the developer by querying - * `cudf::detail::has_nested_columns(input)`. `true` compiles operator + * `cudf::has_nested_columns(input)`. `true` compiles operator * overloads for nested types, while `false` only compiles operator * overloads for primitive types. * @tparam Nullate A cudf::nullate type describing whether to check for nulls. diff --git a/cpp/include/cudf/table/table_view.hpp b/cpp/include/cudf/table/table_view.hpp index a71e0558dec..4a990f67ce4 100644 --- a/cpp/include/cudf/table/table_view.hpp +++ b/cpp/include/cudf/table/table_view.hpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -32,7 +33,7 @@ * passed by value. */ -namespace cudf { +namespace CUDF_EXPORT cudf { namespace detail { /** * @brief Base class for a table of `ColumnView`s @@ -123,7 +124,10 @@ class table_view_base { * @param column_index The index of the desired column * @return A reference to the desired column */ - [[nodiscard]] ColumnView const& column(size_type column_index) const; + [[nodiscard]] ColumnView const& column(size_type column_index) const + { + return _columns.at(column_index); + } /** * @brief Returns the number of columns @@ -174,8 +178,17 @@ class table_view_base { * @return Whether nested columns exist in the input table */ bool has_nested_columns(table_view const& table); + } // namespace detail +/** + * @brief Determine if any nested columns exist in a given table. + * + * @param table The input table + * @return Whether nested columns exist in the input table + */ +bool has_nested_columns(table_view const& table); + /** * @brief A set of cudf::column_view's of the same size. * @@ -374,4 +387,4 @@ extern template bool is_relationally_comparable(mutable_tabl mutable_table_view const& rhs); // @endcond } // namespace detail -} // namespace cudf +} // namespace CUDF_EXPORT cudf diff --git a/cpp/src/table/table_view.cpp b/cpp/src/table/table_view.cpp index 13832b0d9dc..8a5340dc20d 100644 --- a/cpp/src/table/table_view.cpp +++ b/cpp/src/table/table_view.cpp @@ -52,12 +52,6 @@ auto concatenate_column_views(std::vector const& views) return concat_cols; } -template -ColumnView const& table_view_base::column(size_type column_index) const -{ - return _columns.at(column_index); -} - // Explicit instantiation for a table of `column_view`s template class table_view_base; @@ -172,6 +166,7 @@ bool has_nested_columns(table_view const& table) return std::any_of( table.begin(), table.end(), [](column_view const& col) { return is_nested(col.type()); }); } - } // namespace detail + +bool has_nested_columns(table_view const& table) { return detail::has_nested_columns(table); } } // namespace cudf diff --git a/cpp/tests/table/experimental_row_operator_tests.cu b/cpp/tests/table/experimental_row_operator_tests.cu index 896cc7a82d4..0d9e4e27f2c 100644 --- a/cpp/tests/table/experimental_row_operator_tests.cu +++ b/cpp/tests/table/experimental_row_operator_tests.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, NVIDIA CORPORATION. + * Copyright (c) 2022-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -109,15 +109,14 @@ TYPED_TEST(TypedTableViewTest, TestSortSameTableFromTwoTables) auto const lhs = cudf::table_view{{col1}}; auto const empty_rhs = cudf::table_view{{col2}}; - auto const stream = cudf::get_default_stream(); - auto const test_sort = [stream](auto const& preprocessed, - auto const& input, - auto const& comparator, - auto const& expected) { - auto const order = sorted_order( - preprocessed, input.num_rows(), cudf::detail::has_nested_columns(input), comparator, stream); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, order->view()); - }; + auto const stream = cudf::get_default_stream(); + auto const test_sort = + [stream]( + auto const& preprocessed, auto const& input, auto const& comparator, auto const& expected) { + auto const order = sorted_order( + preprocessed, input.num_rows(), cudf::has_nested_columns(input), comparator, stream); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, order->view()); + }; auto const test_sort_two_tables = [&](auto const& preprocessed_lhs, auto const& preprocessed_empty_rhs) { @@ -188,15 +187,14 @@ TYPED_TEST(TypedTableViewTest, TestSortSameTableFromTwoTablesWithListsOfStructs) auto const lhs = cudf::table_view{{*col1}}; auto const empty_rhs = cudf::table_view{{*col2}}; - auto const stream = cudf::get_default_stream(); - auto const test_sort = [stream](auto const& preprocessed, - auto const& input, - auto const& comparator, - auto const& expected) { - auto const order = sorted_order( - preprocessed, input.num_rows(), cudf::detail::has_nested_columns(input), comparator, stream); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, order->view()); - }; + auto const stream = cudf::get_default_stream(); + auto const test_sort = + [stream]( + auto const& preprocessed, auto const& input, auto const& comparator, auto const& expected) { + auto const order = sorted_order( + preprocessed, input.num_rows(), cudf::has_nested_columns(input), comparator, stream); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(expected, order->view()); + }; auto const test_sort_two_tables = [&](auto const& preprocessed_lhs, auto const& preprocessed_empty_rhs) { diff --git a/cpp/tests/table/row_operator_tests_utilities.cu b/cpp/tests/table/row_operator_tests_utilities.cu index cfffa1cdd54..6127864987d 100644 --- a/cpp/tests/table/row_operator_tests_utilities.cu +++ b/cpp/tests/table/row_operator_tests_utilities.cu @@ -42,7 +42,7 @@ std::unique_ptr two_table_comparison(cudf::table_view lhs, auto output = cudf::make_numeric_column( cudf::data_type(cudf::type_id::BOOL8), lhs.num_rows(), cudf::mask_state::UNALLOCATED); - if (cudf::detail::has_nested_columns(lhs) || cudf::detail::has_nested_columns(rhs)) { + if (cudf::has_nested_columns(lhs) || cudf::has_nested_columns(rhs)) { thrust::transform(rmm::exec_policy(stream), lhs_it, lhs_it + lhs.num_rows(), @@ -129,7 +129,7 @@ std::unique_ptr two_table_equality(cudf::table_view lhs, auto output = cudf::make_numeric_column( cudf::data_type(cudf::type_id::BOOL8), lhs.num_rows(), cudf::mask_state::UNALLOCATED); - if (cudf::detail::has_nested_columns(lhs) or cudf::detail::has_nested_columns(rhs)) { + if (cudf::has_nested_columns(lhs) or cudf::has_nested_columns(rhs)) { auto const equal_comparator = table_comparator.equal_to(cudf::nullate::NO{}, cudf::null_equality::EQUAL, comparator); diff --git a/cpp/tests/table/row_operator_tests_utilities2.cu b/cpp/tests/table/row_operator_tests_utilities2.cu index 057d9ee1004..17d274eba13 100644 --- a/cpp/tests/table/row_operator_tests_utilities2.cu +++ b/cpp/tests/table/row_operator_tests_utilities2.cu @@ -41,7 +41,7 @@ std::unique_ptr self_comparison(cudf::table_view input, auto output = cudf::make_numeric_column( cudf::data_type(cudf::type_id::BOOL8), input.num_rows(), cudf::mask_state::UNALLOCATED); - if (cudf::detail::has_nested_columns(input)) { + if (cudf::has_nested_columns(input)) { thrust::transform(rmm::exec_policy(stream), thrust::make_counting_iterator(0), thrust::make_counting_iterator(input.num_rows()), From 1a4c2aa38c6e7de8c6937b787a1263a4ccddadea Mon Sep 17 00:00:00 2001 From: Thomas Li <47963215+lithomas1@users.noreply.github.com> Date: Tue, 2 Jul 2024 07:38:18 -0700 Subject: [PATCH 07/42] Start migrating I/O writers to pylibcudf (starting with JSON) (#15952) Switches the JSON writer to use pylibcudf. xref #15162 Authors: - Thomas Li (https://github.com/lithomas1) - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Lawrence Mitchell (https://github.com/wence-) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/15952 --- .../api_docs/pylibcudf/io/index.rst | 1 + .../user_guide/api_docs/pylibcudf/io/json.rst | 6 + python/cudf/cudf/_lib/json.pyx | 98 +++----- .../cudf/_lib/pylibcudf/io/CMakeLists.txt | 6 +- .../cudf/cudf/_lib/pylibcudf/io/__init__.pxd | 2 +- .../cudf/cudf/_lib/pylibcudf/io/__init__.py | 4 +- python/cudf/cudf/_lib/pylibcudf/io/avro.pyx | 4 +- python/cudf/cudf/_lib/pylibcudf/io/json.pxd | 18 ++ python/cudf/cudf/_lib/pylibcudf/io/json.pyx | 68 ++++++ python/cudf/cudf/_lib/pylibcudf/io/types.pxd | 11 + python/cudf/cudf/_lib/pylibcudf/io/types.pyx | 125 +++++++++- .../cudf/cudf/pylibcudf_tests/common/utils.py | 122 ++++++++-- python/cudf/cudf/pylibcudf_tests/conftest.py | 104 ++++++-- .../pylibcudf_tests/{ => io}/test_avro.py | 0 .../cudf/cudf/pylibcudf_tests/io/test_json.py | 116 +++++++++ .../test_source_sink_info.py} | 34 ++- .../cudf/cudf/pylibcudf_tests/test_copying.py | 226 +++++++++++++----- 17 files changed, 768 insertions(+), 177 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/io/json.rst create mode 100644 python/cudf/cudf/_lib/pylibcudf/io/json.pxd create mode 100644 python/cudf/cudf/_lib/pylibcudf/io/json.pyx rename python/cudf/cudf/pylibcudf_tests/{ => io}/test_avro.py (100%) create mode 100644 python/cudf/cudf/pylibcudf_tests/io/test_json.py rename python/cudf/cudf/pylibcudf_tests/{test_source_info.py => io/test_source_sink_info.py} (72%) diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst index 0d53ac92db9..bde6d8094ce 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/index.rst @@ -16,3 +16,4 @@ I/O Functions :maxdepth: 1 avro + json diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/io/json.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/json.rst new file mode 100644 index 00000000000..6aeae1f322a --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/io/json.rst @@ -0,0 +1,6 @@ +==== +JSON +==== + +.. automodule:: cudf._lib.pylibcudf.io.json + :members: diff --git a/python/cudf/cudf/_lib/json.pyx b/python/cudf/cudf/_lib/json.pyx index a8fef907bad..22e34feb547 100644 --- a/python/cudf/cudf/_lib/json.pyx +++ b/python/cudf/cudf/_lib/json.pyx @@ -9,38 +9,27 @@ from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool from libcpp.map cimport map -from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move from libcpp.vector cimport vector cimport cudf._lib.pylibcudf.libcudf.io.types as cudf_io_types -from cudf._lib.column cimport Column -from cudf._lib.io.utils cimport ( - make_sink_info, - make_source_info, - update_struct_field_names, -) -from cudf._lib.pylibcudf.libcudf.io.data_sink cimport data_sink +from cudf._lib.io.utils cimport make_source_info, update_struct_field_names from cudf._lib.pylibcudf.libcudf.io.json cimport ( json_reader_options, json_recovery_mode_t, - json_writer_options, read_json as libcudf_read_json, schema_element, - write_json as libcudf_write_json, ) from cudf._lib.pylibcudf.libcudf.io.types cimport ( - column_name_info, compression_type, - sink_info, - table_metadata, table_with_metadata, ) -from cudf._lib.pylibcudf.libcudf.table.table_view cimport table_view from cudf._lib.pylibcudf.libcudf.types cimport data_type, size_type from cudf._lib.types cimport dtype_to_data_type -from cudf._lib.utils cimport data_from_unique_ptr, table_view_from_table +from cudf._lib.utils cimport data_from_unique_ptr + +import cudf._lib.pylibcudf as plc cdef json_recovery_mode_t _get_json_recovery_mode(object on_bad_lines): @@ -175,45 +164,27 @@ def write_json( -------- cudf.to_json """ - cdef table_view input_table_view = table_view_from_table( - table, ignore_index=True - ) - - cdef unique_ptr[data_sink] data_sink_c - cdef sink_info sink_info_c = make_sink_info(path_or_buf, data_sink_c) - cdef string na_c = na_rep.encode() - cdef bool include_nulls_c = include_nulls - cdef bool lines_c = lines - cdef int rows_per_chunk_c = rows_per_chunk - cdef string true_value_c = 'true'.encode() - cdef string false_value_c = 'false'.encode() - cdef table_metadata tbl_meta - - num_index_cols_meta = 0 - cdef column_name_info child_info - for i, name in enumerate(table._column_names, num_index_cols_meta): - child_info.name = name.encode() - tbl_meta.schema_info.push_back(child_info) - _set_col_children_metadata( - table[name]._column, - tbl_meta.schema_info[i] - ) + cdef list colnames = [] - cdef json_writer_options options = move( - json_writer_options.builder(sink_info_c, input_table_view) - .metadata(tbl_meta) - .na_rep(na_c) - .include_nulls(include_nulls_c) - .lines(lines_c) - .rows_per_chunk(rows_per_chunk_c) - .true_value(true_value_c) - .false_value(false_value_c) - .build() - ) + for name in table._column_names: + colnames.append((name, _dtype_to_names_list(table[name]._column))) try: - with nogil: - libcudf_write_json(options) + plc.io.json.write_json( + plc.io.SinkInfo([path_or_buf]), + plc.io.TableWithMetadata( + plc.Table([ + c.to_pylibcudf(mode="read") for c in table._columns + ]), + colnames + ), + na_rep, + include_nulls, + lines, + rows_per_chunk, + true_value="true", + false_value="false" + ) except OverflowError: raise OverflowError( f"Writing JSON file with rows_per_chunk={rows_per_chunk} failed. " @@ -254,23 +225,12 @@ cdef data_type _get_cudf_data_type_from_dtype(object dtype) except *: ) return dtype_to_data_type(dtype) -cdef _set_col_children_metadata(Column col, - column_name_info& col_meta): - cdef column_name_info child_info + +def _dtype_to_names_list(col): if isinstance(col.dtype, cudf.StructDtype): - for i, (child_col, name) in enumerate( - zip(col.children, list(col.dtype.fields)) - ): - child_info.name = name.encode() - col_meta.children.push_back(child_info) - _set_col_children_metadata( - child_col, col_meta.children[i] - ) + return [(name, _dtype_to_names_list(child)) + for name, child in zip(col.dtype.fields, col.children)] elif isinstance(col.dtype, cudf.ListDtype): - for i, child_col in enumerate(col.children): - col_meta.children.push_back(child_info) - _set_col_children_metadata( - child_col, col_meta.children[i] - ) - else: - return + return [("", _dtype_to_names_list(child)) + for child in col.children] + return [] diff --git a/python/cudf/cudf/_lib/pylibcudf/io/CMakeLists.txt b/python/cudf/cudf/_lib/pylibcudf/io/CMakeLists.txt index 32f0f5543e4..084b341ec48 100644 --- a/python/cudf/cudf/_lib/pylibcudf/io/CMakeLists.txt +++ b/python/cudf/cudf/_lib/pylibcudf/io/CMakeLists.txt @@ -12,7 +12,7 @@ # the License. # ============================================================================= -set(cython_sources avro.pyx datasource.pyx types.pyx) +set(cython_sources avro.pyx datasource.pyx json.pyx types.pyx) set(linked_libraries cudf::cudf) rapids_cython_create_modules( @@ -21,5 +21,7 @@ rapids_cython_create_modules( LINKED_LIBRARIES "${linked_libraries}" MODULE_PREFIX pylibcudf_io_ ASSOCIATED_TARGETS cudf ) -set(targets_using_arrow_headers pylibcudf_io_avro pylibcudf_io_datasource pylibcudf_io_types) +set(targets_using_arrow_headers pylibcudf_io_avro pylibcudf_io_datasource pylibcudf_io_json + pylibcudf_io_types +) link_to_pyarrow_headers("${targets_using_arrow_headers}") diff --git a/python/cudf/cudf/_lib/pylibcudf/io/__init__.pxd b/python/cudf/cudf/_lib/pylibcudf/io/__init__.pxd index cfd6d2cd281..ef4c65b277e 100644 --- a/python/cudf/cudf/_lib/pylibcudf/io/__init__.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/io/__init__.pxd @@ -1,4 +1,4 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . cimport avro, datasource, types +from . cimport avro, datasource, json, types from .types cimport SourceInfo, TableWithMetadata diff --git a/python/cudf/cudf/_lib/pylibcudf/io/__init__.py b/python/cudf/cudf/_lib/pylibcudf/io/__init__.py index a54ba1834dc..fb4e4c7e4bb 100644 --- a/python/cudf/cudf/_lib/pylibcudf/io/__init__.py +++ b/python/cudf/cudf/_lib/pylibcudf/io/__init__.py @@ -1,4 +1,4 @@ # Copyright (c) 2024, NVIDIA CORPORATION. -from . import avro, datasource, types -from .types import SourceInfo, TableWithMetadata +from . import avro, datasource, json, types +from .types import SinkInfo, SourceInfo, TableWithMetadata diff --git a/python/cudf/cudf/_lib/pylibcudf/io/avro.pyx b/python/cudf/cudf/_lib/pylibcudf/io/avro.pyx index 946e0896fc8..538bd8aa322 100644 --- a/python/cudf/cudf/_lib/pylibcudf/io/avro.pyx +++ b/python/cudf/cudf/_lib/pylibcudf/io/avro.pyx @@ -19,7 +19,7 @@ cpdef TableWithMetadata read_avro( size_type num_rows = -1 ): """ - Reads an Avro dataset into a set of columns. + Reads an Avro dataset into a :py:class:`~.types.TableWithMetadata`. Parameters ---------- @@ -36,7 +36,7 @@ cpdef TableWithMetadata read_avro( Returns ------- TableWithMetadata - The Table and its corresponding metadata that was read in. + The Table and its corresponding metadata (column names) that were read in. """ cdef vector[string] c_columns if columns is not None and len(columns) > 0: diff --git a/python/cudf/cudf/_lib/pylibcudf/io/json.pxd b/python/cudf/cudf/_lib/pylibcudf/io/json.pxd new file mode 100644 index 00000000000..a91d574131f --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/io/json.pxd @@ -0,0 +1,18 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp cimport bool + +from cudf._lib.pylibcudf.io.types cimport SinkInfo, TableWithMetadata +from cudf._lib.pylibcudf.libcudf.types cimport size_type + + +cpdef void write_json( + SinkInfo sink_info, + TableWithMetadata tbl, + str na_rep = *, + bool include_nulls = *, + bool lines = *, + size_type rows_per_chunk = *, + str true_value = *, + str false_value = * +) diff --git a/python/cudf/cudf/_lib/pylibcudf/io/json.pyx b/python/cudf/cudf/_lib/pylibcudf/io/json.pyx new file mode 100644 index 00000000000..7530eba3803 --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/io/json.pyx @@ -0,0 +1,68 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp cimport bool +from libcpp.limits cimport numeric_limits +from libcpp.string cimport string + +from cudf._lib.pylibcudf.io.types cimport SinkInfo, TableWithMetadata +from cudf._lib.pylibcudf.libcudf.io.json cimport ( + json_writer_options, + write_json as cpp_write_json, +) +from cudf._lib.pylibcudf.libcudf.io.types cimport table_metadata +from cudf._lib.pylibcudf.types cimport size_type + + +cpdef void write_json( + SinkInfo sink_info, + TableWithMetadata table_w_meta, + str na_rep = "", + bool include_nulls = False, + bool lines = False, + size_type rows_per_chunk = numeric_limits[size_type].max(), + str true_value = "true", + str false_value = "false" +): + """ + Writes a :py:class:`~cudf._lib.pylibcudf.table.Table` to JSON format. + + Parameters + ---------- + sink_info: SinkInfo + The SinkInfo object to write the JSON to. + table_w_meta: TableWithMetadata + The TableWithMetadata object containing the Table to write + na_rep: str, default "" + The string representation for null values. + include_nulls: bool, default False + Enables/Disables output of nulls as 'null'. + lines: bool, default False + If `True`, write output in the JSON lines format. + rows_per_chunk: size_type, defaults to length of the input table + The maximum number of rows to write at a time. + true_value: str, default "true" + The string representation for values != 0 in INT8 types. + false_value: str, default "false" + The string representation for values == 0 in INT8 types. + """ + cdef table_metadata tbl_meta = table_w_meta.metadata + cdef string na_rep_c = na_rep.encode() + + cdef json_writer_options options = ( + json_writer_options.builder(sink_info.c_obj, table_w_meta.tbl.view()) + .metadata(tbl_meta) + .na_rep(na_rep_c) + .include_nulls(include_nulls) + .lines(lines) + .build() + ) + + if rows_per_chunk != numeric_limits[size_type].max(): + options.set_rows_per_chunk(rows_per_chunk) + if true_value != "true": + options.set_true_value(true_value.encode()) + if false_value != "false": + options.set_false_value(false_value.encode()) + + with nogil: + cpp_write_json(options) diff --git a/python/cudf/cudf/_lib/pylibcudf/io/types.pxd b/python/cudf/cudf/_lib/pylibcudf/io/types.pxd index aa846a47343..88daf54f33b 100644 --- a/python/cudf/cudf/_lib/pylibcudf/io/types.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/io/types.pxd @@ -1,4 +1,8 @@ # Copyright (c) 2024, NVIDIA CORPORATION. +from libcpp.memory cimport unique_ptr +from libcpp.vector cimport vector + +from cudf._lib.pylibcudf.libcudf.io.data_sink cimport data_sink from cudf._lib.pylibcudf.libcudf.io.types cimport ( column_encoding, column_in_metadata, @@ -22,8 +26,15 @@ cdef class TableWithMetadata: cdef public Table tbl cdef table_metadata metadata + cdef vector[column_name_info] _make_column_info(self, list column_names) + @staticmethod cdef TableWithMetadata from_libcudf(table_with_metadata& tbl) cdef class SourceInfo: cdef source_info c_obj + +cdef class SinkInfo: + # This vector just exists to keep the unique_ptrs to the sinks alive + cdef vector[unique_ptr[data_sink]] sink_storage + cdef sink_info c_obj diff --git a/python/cudf/cudf/_lib/pylibcudf/io/types.pyx b/python/cudf/cudf/_lib/pylibcudf/io/types.pyx index ab3375da662..f94e20970a4 100644 --- a/python/cudf/cudf/_lib/pylibcudf/io/types.pyx +++ b/python/cudf/cudf/_lib/pylibcudf/io/types.pyx @@ -1,17 +1,23 @@ # Copyright (c) 2024, NVIDIA CORPORATION. +from cpython.buffer cimport PyBUF_READ +from cpython.memoryview cimport PyMemoryView_FromMemory +from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move from libcpp.vector cimport vector from cudf._lib.pylibcudf.io.datasource cimport Datasource +from cudf._lib.pylibcudf.libcudf.io.data_sink cimport data_sink from cudf._lib.pylibcudf.libcudf.io.datasource cimport datasource from cudf._lib.pylibcudf.libcudf.io.types cimport ( + column_name_info, host_buffer, source_info, table_with_metadata, ) +import codecs import errno import io import os @@ -22,7 +28,39 @@ cdef class TableWithMetadata: (e.g. column names) For details, see :cpp:class:`cudf::io::table_with_metadata`. + + Parameters + ---------- + tbl : Table + The input table. + column_names : list + A list of tuples each containing the name of each column + and the names of its child columns (in the same format). + e.g. + [("id", []), ("name", [("first", []), ("last", [])])] + """ + def __init__(self, Table tbl, list column_names): + self.tbl = tbl + + self.metadata.schema_info = self._make_column_info(column_names) + + cdef vector[column_name_info] _make_column_info(self, list column_names): + cdef vector[column_name_info] col_name_infos + cdef column_name_info info + + col_name_infos.reserve(len(column_names)) + + for name, child_names in column_names: + if not isinstance(name, str): + raise ValueError("Column name must be a string!") + + info.name = name.encode() + info.children = self._make_column_info(child_names) + + col_name_infos.push_back(info) + + return col_name_infos @property def columns(self): @@ -51,6 +89,7 @@ cdef class TableWithMetadata: out.metadata = tbl_with_meta.metadata return out + cdef class SourceInfo: """A class containing details on a source to read from. @@ -119,7 +158,87 @@ cdef class SourceInfo: raise ValueError("Sources must be a list of str/paths, " "bytes, io.BytesIO, or a Datasource") - if empty_buffer is True: - c_host_buffers.push_back(host_buffer(NULL, 0)) + self.c_obj = source_info(c_host_buffers) + + +# Adapts a python io.IOBase object as a libcudf IO data_sink. This lets you +# write from cudf to any python file-like object (File/BytesIO/SocketIO etc) +cdef cppclass iobase_data_sink(data_sink): + object buf + + iobase_data_sink(object buf_): + this.buf = buf_ + + void host_write(const void * data, size_t size) with gil: + if isinstance(buf, io.TextIOBase): + buf.write(PyMemoryView_FromMemory(data, size, PyBUF_READ) + .tobytes().decode()) + else: + buf.write(PyMemoryView_FromMemory(data, size, PyBUF_READ)) + + void flush() with gil: + buf.flush() + + size_t bytes_written() with gil: + return buf.tell() + + +cdef class SinkInfo: + """A class containing details on a source to read from. + + For details, see :cpp:class:`cudf::io::sink_info`. + + Parameters + ---------- + sinks : list of str, PathLike, BytesIO, StringIO + + A homogeneous list of sinks (this can be a string filename, + bytes, or one of the Python I/O classes) to read from. + + Mixing different types of sinks will raise a `ValueError`. + """ + + def __init__(self, list sinks): + cdef vector[data_sink *] data_sinks + cdef vector[string] paths + + if not sinks: + raise ValueError("Need to pass at least one sink") + + if isinstance(sinks[0], os.PathLike): + sinks = [os.path.expanduser(s) for s in sinks] + + cdef object initial_sink_cls = type(sinks[0]) + + if not all(isinstance(s, initial_sink_cls) for s in sinks): + raise ValueError("All sinks must be of the same type!") + + if initial_sink_cls in {io.StringIO, io.BytesIO, io.TextIOBase}: + data_sinks.reserve(len(sinks)) + if isinstance(sinks[0], (io.StringIO, io.BytesIO)): + for s in sinks: + self.sink_storage.push_back( + unique_ptr[data_sink](new iobase_data_sink(s)) + ) + elif isinstance(sinks[0], io.TextIOBase): + for s in sinks: + if codecs.lookup(s).name not in ('utf-8', 'ascii'): + raise NotImplementedError(f"Unsupported encoding {s.encoding}") + self.sink_storage.push_back( + unique_ptr[data_sink](new iobase_data_sink(s.buffer)) + ) + data_sinks.push_back(self.sink_storage.back().get()) + elif initial_sink_cls is str: + paths.reserve(len(sinks)) + for s in sinks: + paths.push_back( s.encode()) + else: + raise TypeError( + "Unrecognized input type: {}".format(type(sinks[0])) + ) - self.c_obj = move(source_info(c_host_buffers)) + if data_sinks.size() > 0: + self.c_obj = sink_info(data_sinks) + else: + # we don't have sinks so we must have paths to sinks + self.c_obj = sink_info(paths) diff --git a/python/cudf/cudf/pylibcudf_tests/common/utils.py b/python/cudf/cudf/pylibcudf_tests/common/utils.py index bf927e661fe..f8bfe340ae5 100644 --- a/python/cudf/cudf/pylibcudf_tests/common/utils.py +++ b/python/cudf/cudf/pylibcudf_tests/common/utils.py @@ -1,24 +1,39 @@ # Copyright (c) 2024, NVIDIA CORPORATION. from __future__ import annotations +import io +import os + import pyarrow as pa import pytest from cudf._lib import pylibcudf as plc -def metadata_from_arrow_array( - pa_array: pa.Array, +def metadata_from_arrow_type( + pa_type: pa.Array, + name: str = "", ) -> plc.interop.ColumnMetadata | None: - metadata = None - if pa.types.is_list(dtype := pa_array.type) or pa.types.is_struct(dtype): + metadata = plc.interop.ColumnMetadata(name) # None + if pa.types.is_list(pa_type): + child_meta = [plc.interop.ColumnMetadata("offsets")] + for i in range(pa_type.num_fields): + field_meta = metadata_from_arrow_type( + pa_type.field(i).type, pa_type.field(i).name + ) + child_meta.append(field_meta) + metadata = plc.interop.ColumnMetadata(name, child_meta) + elif pa.types.is_struct(pa_type): + child_meta = [] + for i in range(pa_type.num_fields): + field_meta = metadata_from_arrow_type( + pa_type.field(i).type, pa_type.field(i).name + ) + child_meta.append(field_meta) metadata = plc.interop.ColumnMetadata( - "", + name, # libcudf does not store field names, so just match pyarrow's. - [ - plc.interop.ColumnMetadata(pa_array.type.field(i).name) - for i in range(pa_array.type.num_fields) - ], + child_meta, ) return metadata @@ -32,13 +47,13 @@ def assert_column_eq( rhs, plc.Column ): rhs = plc.interop.to_arrow( - rhs, metadata=metadata_from_arrow_array(lhs) + rhs, metadata=metadata_from_arrow_type(lhs.type) ) elif isinstance(lhs, plc.Column) and isinstance( rhs, (pa.Array, pa.ChunkedArray) ): lhs = plc.interop.to_arrow( - lhs, metadata=metadata_from_arrow_array(rhs) + lhs, metadata=metadata_from_arrow_type(rhs.type) ) else: raise ValueError( @@ -94,21 +109,16 @@ def is_signed_integer(plc_dtype: plc.DataType): ) -def is_unsigned_integer(plc_dtype: plc.DataType): - return plc_dtype.id() in ( - plc.TypeId.UINT8, - plc.TypeId.UINT16, - plc.TypeId.UINT32, - plc.TypeId.UINT64, - ) - - def is_integer(plc_dtype: plc.DataType): return plc_dtype.id() in ( plc.TypeId.INT8, plc.TypeId.INT16, plc.TypeId.INT32, plc.TypeId.INT64, + plc.TypeId.UINT8, + plc.TypeId.UINT16, + plc.TypeId.UINT32, + plc.TypeId.UINT64, ) @@ -135,8 +145,80 @@ def is_fixed_width(plc_dtype: plc.DataType): ) +def nesting_level(typ) -> tuple[int, int]: + """Return list and struct nesting of a pyarrow type.""" + if isinstance(typ, pa.ListType): + list_, struct = nesting_level(typ.value_type) + return list_ + 1, struct + elif isinstance(typ, pa.StructType): + lists, structs = map(max, zip(*(nesting_level(t.type) for t in typ))) + return lists, structs + 1 + else: + return 0, 0 + + +def is_nested_struct(typ): + return nesting_level(typ)[1] > 1 + + +def is_nested_list(typ): + return nesting_level(typ)[0] > 1 + + +def sink_to_str(sink): + """ + Takes a sink (e.g. StringIO/BytesIO, filepath, etc.) + and reads in the contents into a string (str not bytes) + for comparison + """ + if isinstance(sink, (str, os.PathLike)): + with open(sink, "r") as f: + str_result = f.read() + elif isinstance(sink, io.BytesIO): + sink.seek(0) + str_result = sink.read().decode() + else: + sink.seek(0) + str_result = sink.read() + return str_result + + +NUMERIC_PA_TYPES = [pa.int64(), pa.float64(), pa.uint64()] +STRING_PA_TYPES = [pa.string()] +BOOL_PA_TYPES = [pa.bool_()] +LIST_PA_TYPES = [ + pa.list_(pa.int64()), + # Nested case + pa.list_(pa.list_(pa.int64())), +] + # We must explicitly specify this type via a field to ensure we don't include # nullability accidentally. DEFAULT_STRUCT_TESTING_TYPE = pa.struct( [pa.field("v", pa.int64(), nullable=False)] ) +NESTED_STRUCT_TESTING_TYPE = pa.struct( + [ + pa.field("a", pa.int64(), nullable=False), + pa.field( + "b_struct", + pa.struct([pa.field("b", pa.float64(), nullable=False)]), + nullable=False, + ), + ] +) + +DEFAULT_PA_STRUCT_TESTING_TYPES = [ + DEFAULT_STRUCT_TESTING_TYPE, + NESTED_STRUCT_TESTING_TYPE, +] + +DEFAULT_PA_TYPES = ( + NUMERIC_PA_TYPES + + STRING_PA_TYPES + + BOOL_PA_TYPES + + LIST_PA_TYPES + + DEFAULT_PA_STRUCT_TESTING_TYPES +) + +ALL_PA_TYPES = DEFAULT_PA_TYPES diff --git a/python/cudf/cudf/pylibcudf_tests/conftest.py b/python/cudf/cudf/pylibcudf_tests/conftest.py index b169bbdee5b..e4760ea7ac8 100644 --- a/python/cudf/cudf/pylibcudf_tests/conftest.py +++ b/python/cudf/cudf/pylibcudf_tests/conftest.py @@ -1,9 +1,12 @@ # Copyright (c) 2024, NVIDIA CORPORATION. # Tell ruff it's OK that some imports occur after the sys.path.insert # ruff: noqa: E402 +import io import os +import pathlib import sys +import numpy as np import pyarrow as pa import pytest @@ -11,7 +14,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "common")) -from utils import DEFAULT_STRUCT_TESTING_TYPE +from utils import ALL_PA_TYPES, DEFAULT_PA_TYPES, NUMERIC_PA_TYPES # This fixture defines the standard set of types that all tests should default to @@ -20,14 +23,7 @@ # across modules. Otherwise it may be defined on a per-module basis. @pytest.fixture( scope="session", - params=[ - pa.int64(), - pa.float64(), - pa.string(), - pa.bool_(), - pa.list_(pa.int64()), - DEFAULT_STRUCT_TESTING_TYPE, - ], + params=DEFAULT_PA_TYPES, ) def pa_type(request): return request.param @@ -35,16 +31,96 @@ def pa_type(request): @pytest.fixture( scope="session", - params=[ - pa.int64(), - pa.float64(), - pa.uint64(), - ], + params=NUMERIC_PA_TYPES, ) def numeric_pa_type(request): return request.param +# TODO: Consider adding another fixture/adapting this +# fixture to consider nullability +@pytest.fixture(scope="session", params=[0, 100]) +def table_data(request): + """ + Returns (TableWithMetadata, pa_table). + + This is the default fixture you should be using for testing + pylibcudf I/O writers. + + Contains one of each category (e.g. int, bool, list, struct) + of dtypes. + """ + nrows = request.param + + table_dict = {} + # Colnames in the format expected by + # plc.io.TableWithMetadata + colnames = [] + + np.random.seed(42) + + for typ in ALL_PA_TYPES: + rand_vals = np.random.randint(0, nrows, nrows) + child_colnames = [] + + def _generate_nested_data(typ): + child_colnames = [] + + # recurse to get vals for children + rand_arrs = [] + for i in range(typ.num_fields): + rand_arr, grandchild_colnames = _generate_nested_data( + typ.field(i).type + ) + rand_arrs.append(rand_arr) + child_colnames.append((typ.field(i).name, grandchild_colnames)) + + if isinstance(typ, pa.StructType): + pa_array = pa.StructArray.from_arrays( + [rand_arr for rand_arr in rand_arrs], + names=[typ.field(i).name for i in range(typ.num_fields)], + ) + elif isinstance(typ, pa.ListType): + pa_array = pa.array( + [list(row_vals) for row_vals in zip(rand_arrs[0])], + type=typ, + ) + child_colnames.append(("", grandchild_colnames)) + else: + # typ is scalar type + pa_array = pa.array(rand_vals).cast(typ) + return pa_array, child_colnames + + if isinstance(typ, (pa.ListType, pa.StructType)): + rand_arr, child_colnames = _generate_nested_data(typ) + else: + rand_arr = pa.array(rand_vals).cast(typ) + + table_dict[f"col_{typ}"] = rand_arr + colnames.append((f"col_{typ}", child_colnames)) + + pa_table = pa.Table.from_pydict(table_dict) + + return plc.io.TableWithMetadata( + plc.interop.from_arrow(pa_table), column_names=colnames + ), pa_table + + +@pytest.fixture( + params=["a.txt", pathlib.Path("a.txt"), io.BytesIO, io.StringIO], +) +def source_or_sink(request, tmp_path): + fp_or_buf = request.param + if isinstance(fp_or_buf, str): + return f"{tmp_path}/{fp_or_buf}" + elif isinstance(fp_or_buf, os.PathLike): + return tmp_path.joinpath(fp_or_buf) + elif issubclass(fp_or_buf, io.IOBase): + # Must construct io.StringIO/io.BytesIO inside + # fixture, or we'll end up re-using it + return fp_or_buf() + + @pytest.fixture( scope="session", params=[opt for opt in plc.types.Interpolation] ) diff --git a/python/cudf/cudf/pylibcudf_tests/test_avro.py b/python/cudf/cudf/pylibcudf_tests/io/test_avro.py similarity index 100% rename from python/cudf/cudf/pylibcudf_tests/test_avro.py rename to python/cudf/cudf/pylibcudf_tests/io/test_avro.py diff --git a/python/cudf/cudf/pylibcudf_tests/io/test_json.py b/python/cudf/cudf/pylibcudf_tests/io/test_json.py new file mode 100644 index 00000000000..d6b8bfa6976 --- /dev/null +++ b/python/cudf/cudf/pylibcudf_tests/io/test_json.py @@ -0,0 +1,116 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +import io + +import pyarrow as pa +import pytest +from utils import sink_to_str + +import cudf._lib.pylibcudf as plc + + +@pytest.mark.parametrize("rows_per_chunk", [8, 100]) +@pytest.mark.parametrize("lines", [True, False]) +def test_write_json_basic(table_data, source_or_sink, lines, rows_per_chunk): + plc_table_w_meta, pa_table = table_data + sink = source_or_sink + + plc.io.json.write_json( + plc.io.SinkInfo([sink]), + plc_table_w_meta, + lines=lines, + rows_per_chunk=rows_per_chunk, + ) + + exp = pa_table.to_pandas() + + # Convert everything to string to make + # comparisons easier + str_result = sink_to_str(sink) + + pd_result = exp.to_json(orient="records", lines=lines) + + assert str_result == pd_result + + +@pytest.mark.parametrize("include_nulls", [True, False]) +@pytest.mark.parametrize("na_rep", ["null", "awef", ""]) +def test_write_json_nulls(na_rep, include_nulls): + names = ["a", "b"] + pa_tbl = pa.Table.from_arrays( + [pa.array([1.0, 2.0, None]), pa.array([True, None, False])], + names=names, + ) + plc_tbl = plc.interop.from_arrow(pa_tbl) + plc_tbl_w_meta = plc.io.types.TableWithMetadata( + plc_tbl, column_names=[(name, []) for name in names] + ) + + sink = io.StringIO() + + plc.io.json.write_json( + plc.io.SinkInfo([sink]), + plc_tbl_w_meta, + na_rep=na_rep, + include_nulls=include_nulls, + ) + + exp = pa_tbl.to_pandas() + + # Convert everything to string to make + # comparisons easier + str_result = sink_to_str(sink) + pd_result = exp.to_json(orient="records") + + if not include_nulls: + # No equivalent in pandas, so we just + # sanity check by making sure na_rep + # doesn't appear in the output + + # don't quote null + for name in names: + assert f'{{"{name}":{na_rep}}}' not in str_result + return + + # pandas doesn't suppport na_rep + # let's just manually do str.replace + pd_result = pd_result.replace("null", na_rep) + + assert str_result == pd_result + + +@pytest.mark.parametrize("true_value", ["True", "correct"]) +@pytest.mark.parametrize("false_value", ["False", "wrong"]) +def test_write_json_bool_opts(true_value, false_value): + names = ["a"] + pa_tbl = pa.Table.from_arrays([pa.array([True, None, False])], names=names) + plc_tbl = plc.interop.from_arrow(pa_tbl) + plc_tbl_w_meta = plc.io.types.TableWithMetadata( + plc_tbl, column_names=[(name, []) for name in names] + ) + + sink = io.StringIO() + + plc.io.json.write_json( + plc.io.SinkInfo([sink]), + plc_tbl_w_meta, + include_nulls=True, + na_rep="null", + true_value=true_value, + false_value=false_value, + ) + + exp = pa_tbl.to_pandas() + + # Convert everything to string to make + # comparisons easier + str_result = sink_to_str(sink) + pd_result = exp.to_json(orient="records") + + # pandas doesn't suppport na_rep + # let's just manually do str.replace + if true_value != "true": + pd_result = pd_result.replace("true", true_value) + if false_value != "false": + pd_result = pd_result.replace("false", false_value) + + assert str_result == pd_result diff --git a/python/cudf/cudf/pylibcudf_tests/test_source_info.py b/python/cudf/cudf/pylibcudf_tests/io/test_source_sink_info.py similarity index 72% rename from python/cudf/cudf/pylibcudf_tests/test_source_info.py rename to python/cudf/cudf/pylibcudf_tests/io/test_source_sink_info.py index 019321b7259..287dd8f21c8 100644 --- a/python/cudf/cudf/pylibcudf_tests/test_source_info.py +++ b/python/cudf/cudf/pylibcudf_tests/io/test_source_sink_info.py @@ -9,6 +9,21 @@ from cudf._lib.pylibcudf.io.datasource import NativeFileDatasource +@pytest.fixture(params=[plc.io.SourceInfo, plc.io.SinkInfo]) +def io_class(request): + return request.param + + +def _skip_invalid_sinks(io_class, sink): + """ + Skip invalid sinks for SinkInfo + """ + if io_class is plc.io.SinkInfo and isinstance( + sink, (bytes, NativeFileDatasource) + ): + pytest.skip(f"{sink} is not a valid input for SinkInfo") + + @pytest.mark.parametrize( "source", [ @@ -18,16 +33,15 @@ NativeFileDatasource(pa.PythonFile(io.BytesIO(), mode="r")), ], ) -def test_source_info_ctor(source, tmp_path): +def test_source_info_ctor(io_class, source, tmp_path): if isinstance(source, str): file = tmp_path / source file.write_bytes("hello world".encode("utf-8")) source = str(file) - plc.io.SourceInfo([source]) + _skip_invalid_sinks(io_class, source) - # TODO: test contents of source_info buffer is correct - # once buffers are exposed on python side + io_class([source]) @pytest.mark.parametrize( @@ -42,7 +56,7 @@ def test_source_info_ctor(source, tmp_path): ], ], ) -def test_source_info_ctor_multiple(sources, tmp_path): +def test_source_info_ctor_multiple(io_class, sources, tmp_path): for i in range(len(sources)): source = sources[i] if isinstance(source, str): @@ -50,10 +64,9 @@ def test_source_info_ctor_multiple(sources, tmp_path): file.write_bytes("hello world".encode("utf-8")) sources[i] = str(file) - plc.io.SourceInfo(sources) + _skip_invalid_sinks(io_class, source) - # TODO: test contents of source_info buffer is correct - # once buffers are exposed on python side + io_class(sources) @pytest.mark.parametrize( @@ -73,7 +86,7 @@ def test_source_info_ctor_multiple(sources, tmp_path): ], ], ) -def test_source_info_ctor_mixing_invalid(sources, tmp_path): +def test_source_info_ctor_mixing_invalid(io_class, sources, tmp_path): # Unlike the previous test # don't create files so that they are missing for i in range(len(sources)): @@ -82,8 +95,9 @@ def test_source_info_ctor_mixing_invalid(sources, tmp_path): file = tmp_path / source file.write_bytes("hello world".encode("utf-8")) sources[i] = str(file) + _skip_invalid_sinks(io_class, source) with pytest.raises(ValueError): - plc.io.SourceInfo(sources) + io_class(sources) def test_source_info_invalid(): diff --git a/python/cudf/cudf/pylibcudf_tests/test_copying.py b/python/cudf/cudf/pylibcudf_tests/test_copying.py index da3ca3a6d1e..0a6df198d46 100644 --- a/python/cudf/cudf/pylibcudf_tests/test_copying.py +++ b/python/cudf/cudf/pylibcudf_tests/test_copying.py @@ -5,19 +5,24 @@ import pytest from utils import ( DEFAULT_STRUCT_TESTING_TYPE, + NESTED_STRUCT_TESTING_TYPE, assert_column_eq, assert_table_eq, cudf_raises, is_fixed_width, is_floating, is_integer, + is_nested_list, + is_nested_struct, is_string, - metadata_from_arrow_array, + metadata_from_arrow_type, ) from cudf._lib import pylibcudf as plc +# TODO: consider moving this to conftest and "pairing" +# it with pa_type, so that they don't get out of sync # TODO: Test nullable data @pytest.fixture(scope="module") def input_column(pa_type): @@ -28,10 +33,27 @@ def input_column(pa_type): elif pa.types.is_boolean(pa_type): pa_array = pa.array([True, True, False], type=pa_type) elif pa.types.is_list(pa_type): - # TODO: Add heterogenous sizes - pa_array = pa.array([[1], [2], [3]], type=pa_type) + if pa_type.value_type == pa.int64(): + pa_array = pa.array([[1], [2, 3], [3]], type=pa_type) + elif ( + isinstance(pa_type.value_type, pa.ListType) + and pa_type.value_type.value_type == pa.int64() + ): + pa_array = pa.array([[[1]], [[2, 3]], [[3]]], type=pa_type) + else: + raise ValueError("Unsupported type " + pa_type.value_type) elif pa.types.is_struct(pa_type): - pa_array = pa.array([{"v": 1}, {"v": 2}, {"v": 3}], type=pa_type) + if not is_nested_struct(pa_type): + pa_array = pa.array([{"v": 1}, {"v": 2}, {"v": 3}], type=pa_type) + else: + pa_array = pa.array( + [ + {"a": 1, "b_struct": {"b": 1.0}}, + {"a": 2, "b_struct": {"b": 2.0}}, + {"a": 3, "b_struct": {"b": 3.0}}, + ], + type=pa_type, + ) else: raise ValueError("Unsupported type") return pa_array, plc.interop.from_arrow(pa_array) @@ -55,13 +77,37 @@ def target_column(pa_type): [False, True, True, False, True, False], type=pa_type ) elif pa.types.is_list(pa_type): - # TODO: Add heterogenous sizes - pa_array = pa.array([[4], [5], [6], [7], [8], [9]], type=pa_type) + if pa_type.value_type == pa.int64(): + pa_array = pa.array( + [[4], [5, 6], [7], [8], [9], [10]], type=pa_type + ) + elif ( + isinstance(pa_type.value_type, pa.ListType) + and pa_type.value_type.value_type == pa.int64() + ): + pa_array = pa.array( + [[[4]], [[5, 6]], [[7]], [[8]], [[9]], [[10]]], type=pa_type + ) + else: + raise ValueError("Unsupported type") elif pa.types.is_struct(pa_type): - pa_array = pa.array( - [{"v": 4}, {"v": 5}, {"v": 6}, {"v": 7}, {"v": 8}, {"v": 9}], - type=pa_type, - ) + if not is_nested_struct(pa_type): + pa_array = pa.array( + [{"v": 4}, {"v": 5}, {"v": 6}, {"v": 7}, {"v": 8}, {"v": 9}], + type=pa_type, + ) + else: + pa_array = pa.array( + [ + {"a": 4, "b_struct": {"b": 4.0}}, + {"a": 5, "b_struct": {"b": 5.0}}, + {"a": 6, "b_struct": {"b": 6.0}}, + {"a": 7, "b_struct": {"b": 7.0}}, + {"a": 8, "b_struct": {"b": 8.0}}, + {"a": 9, "b_struct": {"b": 9.0}}, + ], + type=pa_type, + ) else: raise ValueError("Unsupported type") return pa_array, plc.interop.from_arrow(pa_array) @@ -96,10 +142,22 @@ def source_scalar(pa_type): elif pa.types.is_boolean(pa_type): pa_scalar = pa.scalar(False, type=pa_type) elif pa.types.is_list(pa_type): - # TODO: Longer list? - pa_scalar = pa.scalar([1], type=pa_type) + if pa_type.value_type == pa.int64(): + pa_scalar = pa.scalar([1, 2, 3, 4], type=pa_type) + elif ( + isinstance(pa_type.value_type, pa.ListType) + and pa_type.value_type.value_type == pa.int64() + ): + pa_scalar = pa.scalar([[1, 2, 3, 4]], type=pa_type) + else: + raise ValueError("Unsupported type") elif pa.types.is_struct(pa_type): - pa_scalar = pa.scalar({"v": 1}, type=pa_type) + if not is_nested_struct(pa_type): + pa_scalar = pa.scalar({"v": 1}, type=pa_type) + else: + pa_scalar = pa.scalar( + {"a": 1, "b_struct": {"b": 1.0}}, type=pa_type + ) else: raise ValueError("Unsupported type") return pa_scalar, plc.interop.from_arrow(pa_scalar) @@ -196,27 +254,54 @@ def test_scatter_table( ) if pa.types.is_list(dtype := pa_target_table[0].type): - expected = pa.table( - [pa.array([[4], [1], [2], [3], [8], [9]])] * 3, [""] * 3 - ) + if is_nested_list(dtype): + expected = pa.table( + [pa.array([[[4]], [[1]], [[2, 3]], [[3]], [[9]], [[10]]])] + * 3, + [""] * 3, + ) + else: + expected = pa.table( + [pa.array([[4], [1], [2, 3], [3], [9], [10]])] * 3, + [""] * 3, + ) elif pa.types.is_struct(dtype): - expected = pa.table( - [ - pa.array( - [ - {"v": 4}, - {"v": 1}, - {"v": 2}, - {"v": 3}, - {"v": 8}, - {"v": 9}, - ], - type=DEFAULT_STRUCT_TESTING_TYPE, - ) - ] - * 3, - [""] * 3, - ) + if is_nested_struct(dtype): + expected = pa.table( + [ + pa.array( + [ + {"a": 4, "b_struct": {"b": 4.0}}, + {"a": 1, "b_struct": {"b": 1.0}}, + {"a": 2, "b_struct": {"b": 2.0}}, + {"a": 3, "b_struct": {"b": 3.0}}, + {"a": 8, "b_struct": {"b": 8.0}}, + {"a": 9, "b_struct": {"b": 9.0}}, + ], + type=NESTED_STRUCT_TESTING_TYPE, + ) + ] + * 3, + [""] * 3, + ) + else: + expected = pa.table( + [ + pa.array( + [ + {"v": 4}, + {"v": 1}, + {"v": 2}, + {"v": 3}, + {"v": 8}, + {"v": 9}, + ], + type=DEFAULT_STRUCT_TESTING_TYPE, + ) + ] + * 3, + [""] * 3, + ) else: expected = _pyarrow_boolean_mask_scatter_table( pa_source_table, @@ -627,6 +712,7 @@ def test_split_column_out_of_bounds(target_column): def test_split_table(target_table): pa_target_table, plc_target_table = target_table + upper_bounds = [1, 3, 5] lower_bounds = [0] + upper_bounds[:-1] result = plc.copying.split(plc_target_table, upper_bounds) @@ -718,6 +804,7 @@ def test_copy_if_else_column_scalar( pa_target_column, plc_target_column = target_column pa_source_scalar, plc_source_scalar = source_scalar pa_mask, plc_mask = mask + args = ( (plc_target_column, plc_source_scalar) if array_left @@ -766,27 +853,58 @@ def test_boolean_mask_scatter_from_table( ) if pa.types.is_list(dtype := pa_target_table[0].type): - expected = pa.table( - [pa.array([[1], [5], [2], [7], [3], [9]])] * 3, [""] * 3 - ) + if is_nested_list(dtype): + expected = pa.table( + [ + pa.array( + [[[1]], [[5, 6]], [[2, 3]], [[8]], [[3]], [[10]]] + ) + ] + * 3, + [""] * 3, + ) + else: + expected = pa.table( + [pa.array([[1], [5, 6], [2, 3], [8], [3], [10]])] * 3, + [""] * 3, + ) elif pa.types.is_struct(dtype): - expected = pa.table( - [ - pa.array( - [ - {"v": 1}, - {"v": 5}, - {"v": 2}, - {"v": 7}, - {"v": 3}, - {"v": 9}, - ], - type=DEFAULT_STRUCT_TESTING_TYPE, - ) - ] - * 3, - [""] * 3, - ) + if is_nested_struct(dtype): + expected = pa.table( + [ + pa.array( + [ + {"a": 1, "b_struct": {"b": 1.0}}, + {"a": 5, "b_struct": {"b": 5.0}}, + {"a": 2, "b_struct": {"b": 2.0}}, + {"a": 7, "b_struct": {"b": 7.0}}, + {"a": 3, "b_struct": {"b": 3.0}}, + {"a": 9, "b_struct": {"b": 9.0}}, + ], + type=NESTED_STRUCT_TESTING_TYPE, + ) + ] + * 3, + [""] * 3, + ) + else: + expected = pa.table( + [ + pa.array( + [ + {"v": 1}, + {"v": 5}, + {"v": 2}, + {"v": 7}, + {"v": 3}, + {"v": 9}, + ], + type=DEFAULT_STRUCT_TESTING_TYPE, + ) + ] + * 3, + [""] * 3, + ) else: expected = _pyarrow_boolean_mask_scatter_table( pa_source_table, pa_mask, pa_target_table @@ -887,7 +1005,7 @@ def test_get_element(input_column): assert ( plc.interop.to_arrow( - result, metadata_from_arrow_array(pa_input_column) + result, metadata_from_arrow_type(pa_input_column.type) ).as_py() == pa_input_column[index].as_py() ) From 64325a1bafeb97e8399e497cc9f4f6ffaee0fd14 Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Tue, 2 Jul 2024 11:52:02 -0400 Subject: [PATCH 08/42] Run DFG after verify-alpha-spec (#16151) Because `verify-alpha-spec` potentially modifies `dependencies.yaml`, we want to run DFG after it. This should have been included in #16144 but was forgotten. Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) Approvers: - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cudf/pull/16151 --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d0457d2c641..bbcd78d051f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -136,11 +136,6 @@ repos: .*test.*| ^CHANGELOG.md$ ) - - repo: https://github.com/rapidsai/dependency-file-generator - rev: v1.13.11 - hooks: - - id: rapids-dependency-file-generator - args: ["--clean"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.4.8 hooks: @@ -159,6 +154,11 @@ repos: cpp/src/io/parquet/ipc/Schema_generated[.]h$ ) - id: verify-alpha-spec + - repo: https://github.com/rapidsai/dependency-file-generator + rev: v1.13.11 + hooks: + - id: rapids-dependency-file-generator + args: ["--clean"] default_language_version: python: python3 From 04e3aa9ffad64cf6682b5d1677d9df66a44d8f53 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Tue, 2 Jul 2024 09:55:13 -0700 Subject: [PATCH 09/42] Remove the (unused) implementation of `host_parse_nested_json` (#16135) Follow-up for #15537 and #15813 to remove some missed code. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Bradley Dice (https://github.com/bdice) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/16135 --- cpp/src/io/json/nested_json_gpu.cu | 125 ----------------------------- 1 file changed, 125 deletions(-) diff --git a/cpp/src/io/json/nested_json_gpu.cu b/cpp/src/io/json/nested_json_gpu.cu index 031edfde4f6..a007754ef4f 100644 --- a/cpp/src/io/json/nested_json_gpu.cu +++ b/cpp/src/io/json/nested_json_gpu.cu @@ -2244,131 +2244,6 @@ std::pair, std::vector> json_column_to return {}; } -table_with_metadata host_parse_nested_json(device_span d_input, - cudf::io::json_reader_options const& options, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - // Range of orchestrating/encapsulating function - CUDF_FUNC_RANGE(); - - auto const h_input = cudf::detail::make_std_vector_async(d_input, stream); - - auto const new_line_delimited_json = options.is_enabled_lines(); - - // Get internal JSON column - json_column root_column{}; - std::stack data_path{}; - - constexpr uint32_t row_offset_zero = 0; - constexpr uint32_t token_begin_offset_zero = 0; - constexpr uint32_t token_end_offset_zero = 0; - constexpr uint32_t node_init_child_count_zero = 0; - - // Whether the tokenizer stage should keep quote characters for string values - // If the tokenizer keeps the quote characters, they may be stripped during type casting - constexpr bool include_quote_chars = true; - - // We initialize the very root node and root column, which represent the JSON document being - // parsed. That root node is a list node and that root column is a list column. The column has the - // root node as its only row. The values parsed from the JSON input will be treated as follows: - // (1) For JSON lines: we expect to find a list of JSON values that all - // will be inserted into this root list column. (2) For regular JSON: we expect to have only a - // single value (list, struct, string, number, literal) that will be inserted into this root - // column. - root_column.append_row( - row_offset_zero, json_col_t::ListColumn, token_begin_offset_zero, token_end_offset_zero, 1); - - // Push the root node onto the stack for the data path - data_path.push({&root_column, row_offset_zero, nullptr, node_init_child_count_zero}); - - make_json_column( - root_column, data_path, h_input, d_input, options, include_quote_chars, stream, mr); - - // data_root refers to the root column of the data represented by the given JSON string - auto const& data_root = - new_line_delimited_json ? root_column : root_column.child_columns.begin()->second; - - // Zero row entries - if (data_root.type == json_col_t::ListColumn && data_root.child_columns.empty()) { - return table_with_metadata{std::make_unique
(std::vector>{})}; - } - - // 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(data_root.type == json_col_t::ListColumn and - data_root.child_columns.size() == single_child_col_count and - data_root.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 = data_root.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 - size_type column_index = 0; - for (auto const& col_name : root_struct_col.column_order) { - auto const& json_col = root_struct_col.child_columns.find(col_name)->second; - // Insert this columns name into the schema - out_column_names.emplace_back(col_name); - - std::optional child_schema_element = std::visit( - cudf::detail::visitor_overload{ - [column_index](std::vector const& user_dtypes) -> std::optional { - auto ret = (static_cast(column_index) < user_dtypes.size()) - ? std::optional{{user_dtypes[column_index]}} - : std::optional{}; -#ifdef NJP_DEBUG_PRINT - std::cout << "Column by index: #" << column_index << ", type id: " - << (ret.has_value() ? std::to_string(static_cast(ret->type.id())) : "n/a") - << ", with " << (ret.has_value() ? ret->child_types.size() : 0) << " children" - << "\n"; -#endif - return ret; - }, - [col_name]( - std::map const& user_dtypes) -> std::optional { - auto ret = (user_dtypes.find(col_name) != std::end(user_dtypes)) - ? std::optional{{user_dtypes.find(col_name)->second}} - : std::optional{}; -#ifdef NJP_DEBUG_PRINT - std::cout << "Column by flat name: '" << col_name << "', type id: " - << (ret.has_value() ? std::to_string(static_cast(ret->type.id())) : "n/a") - << ", with " << (ret.has_value() ? ret->child_types.size() : 0) << " children" - << "\n"; -#endif - return ret; - }, - [col_name](std::map const& user_dtypes) - -> std::optional { - auto ret = (user_dtypes.find(col_name) != std::end(user_dtypes)) - ? user_dtypes.find(col_name)->second - : std::optional{}; -#ifdef NJP_DEBUG_PRINT - std::cout << "Column by nested name: #" << col_name << ", type id: " - << (ret.has_value() ? std::to_string(static_cast(ret->type.id())) : "n/a") - << ", with " << (ret.has_value() ? ret->child_types.size() : 0) << " children" - << "\n"; -#endif - return ret; - }}, - options.get_dtypes()); - - // 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, options, child_schema_element, stream, mr); - out_column_names.back().children = std::move(col_name_info); - out_columns.emplace_back(std::move(cudf_col)); - - column_index++; - } - - return table_with_metadata{std::make_unique
(std::move(out_columns)), {out_column_names}}; -} - } // namespace detail } // namespace cudf::io::json From 31ed9fd1eab1b2d4a5d0a839357ed53530daea97 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Tue, 2 Jul 2024 13:07:36 -0500 Subject: [PATCH 10/42] Use provided memory resource for allocating mixed join results. (#16153) This PR fixes a few places where certain code paths for mixed joins are not using the user-provided memory resource. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Mark Harris (https://github.com/harrism) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/16153 --- cpp/src/join/mixed_join.cu | 7 ++----- cpp/src/join/mixed_join_semi.cu | 4 +--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/cpp/src/join/mixed_join.cu b/cpp/src/join/mixed_join.cu index 42e0e4f45ee..90748e6f322 100644 --- a/cpp/src/join/mixed_join.cu +++ b/cpp/src/join/mixed_join.cu @@ -82,9 +82,7 @@ mixed_join( // Left and full joins all return all the row indices from // left with a corresponding NULL from the right. case join_kind::LEFT_JOIN: - case join_kind::FULL_JOIN: - return get_trivial_left_join_indices( - left_conditional, stream, rmm::mr::get_current_device_resource()); + case join_kind::FULL_JOIN: return get_trivial_left_join_indices(left_conditional, stream, mr); // Inner joins return empty output because no matches can exist. case join_kind::INNER_JOIN: return std::pair(std::make_unique>(0, stream, mr), @@ -100,8 +98,7 @@ mixed_join( std::make_unique>(0, stream, mr)); // Full joins need to return the trivial complement. case join_kind::FULL_JOIN: { - auto ret_flipped = get_trivial_left_join_indices( - right_conditional, stream, rmm::mr::get_current_device_resource()); + auto ret_flipped = get_trivial_left_join_indices(right_conditional, stream, mr); return std::pair(std::move(ret_flipped.second), std::move(ret_flipped.first)); } default: CUDF_FAIL("Invalid join kind."); break; diff --git a/cpp/src/join/mixed_join_semi.cu b/cpp/src/join/mixed_join_semi.cu index 8500b248fcf..c147ea3c253 100644 --- a/cpp/src/join/mixed_join_semi.cu +++ b/cpp/src/join/mixed_join_semi.cu @@ -117,9 +117,7 @@ std::unique_ptr> mixed_join_semi( // Anti and semi return all the row indices from left // with a corresponding NULL from the right. case join_kind::LEFT_ANTI_JOIN: - return get_trivial_left_join_indices( - left_conditional, stream, rmm::mr::get_current_device_resource()) - .first; + return get_trivial_left_join_indices(left_conditional, stream, mr).first; // Inner and left semi joins return empty output because no matches can exist. case join_kind::LEFT_SEMI_JOIN: return std::make_unique>(0, stream, mr); From 3bd9975e867c9d2a077ed50fa339cecfd9bc8d9b Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:20:03 -0400 Subject: [PATCH 11/42] Add compile option to enable large strings support (#16037) Adds `CUDF_LARGE_STRINGS_DISABLED` compile-time option to disable large strings support. The default is to now enable large strings support with this PR. This changes the default behavior of the `LIBCUDF_LARGE_STRINGS_ENABLED` environment variable -- when the variable is not set. If the environment variable is not set, then the default behavior depends on the compile option. If `CUDF_LARGE_STRINGS_DISABLED` is compiled `ON` then setting `LIBCUDF_LARGE_STRINGS_ENABLED=1` will turn it **on** at runtime. If `CUDF_LARGE_STRINGS_DISABLED` is not compiled on then setting `LIBCUDF_LARGE_STRINGS_ENABLED=0` will turn it **off** at runtime. This PR also sets `CUDF_LARGE_STRINGS_DISABLED=OFF` by default in the `build.sh` Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Karthikeyan (https://github.com/karthikeyann) - Robert Maynard (https://github.com/robertmaynard) - Vyas Ramasubramani (https://github.com/vyasr) - Jason Lowe (https://github.com/jlowe) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16037 --- build.sh | 9 ++++++++- ci/test_java.sh | 3 +++ cpp/CMakeLists.txt | 7 +++++++ cpp/src/strings/utilities.cu | 5 +++++ python/cudf/cudf/tests/test_column.py | 11 ----------- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/build.sh b/build.sh index 4291c88ea12..52bb1e64d16 100755 --- a/build.sh +++ b/build.sh @@ -17,7 +17,7 @@ ARGS=$* # script, and that this script resides in the repo dir! REPODIR=$(cd $(dirname $0); pwd) -VALIDARGS="clean libcudf cudf cudfjar dask_cudf benchmarks tests libcudf_kafka cudf_kafka custreamz -v -g -n --pydevelop -l --allgpuarch --disable_nvtx --opensource_nvcomp --show_depr_warn --ptds -h --build_metrics --incl_cache_stats" +VALIDARGS="clean libcudf cudf cudfjar dask_cudf benchmarks tests libcudf_kafka cudf_kafka custreamz -v -g -n --pydevelop -l --allgpuarch --disable_nvtx --opensource_nvcomp --show_depr_warn --ptds -h --build_metrics --incl_cache_stats --disable_large_strings" HELP="$0 [clean] [libcudf] [cudf] [cudfjar] [dask_cudf] [benchmarks] [tests] [libcudf_kafka] [cudf_kafka] [custreamz] [-v] [-g] [-n] [-h] [--cmake-args=\\\"\\\"] clean - remove all existing build artifacts and configuration (start over) @@ -39,6 +39,7 @@ HELP="$0 [clean] [libcudf] [cudf] [cudfjar] [dask_cudf] [benchmarks] [tests] [li --opensource_nvcomp - disable use of proprietary nvcomp extensions --show_depr_warn - show cmake deprecation warnings --ptds - enable per-thread default stream + --disable_large_strings - disable large strings support --build_metrics - generate build metrics report for libcudf --incl_cache_stats - include cache statistics in build metrics report --cmake-args=\\\"\\\" - pass arbitrary list of CMake configuration options (escape all quotes in argument) @@ -69,6 +70,7 @@ BUILD_DISABLE_DEPRECATION_WARNINGS=ON BUILD_PER_THREAD_DEFAULT_STREAM=OFF BUILD_REPORT_METRICS=OFF BUILD_REPORT_INCL_CACHE_STATS=OFF +BUILD_DISABLE_LARGE_STRINGS=OFF USE_PROPRIETARY_NVCOMP=ON PYTHON_ARGS_FOR_INSTALL="-m pip install --no-build-isolation --no-deps --config-settings rapidsai.disable-cuda=true" @@ -153,6 +155,7 @@ function buildLibCudfJniInDocker { -DCUDF_ENABLE_ARROW_S3=OFF \ -DBUILD_TESTS=OFF \ -DCUDF_USE_PER_THREAD_DEFAULT_STREAM=ON \ + -DCUDF_LARGE_STRINGS_DISABLED=ON \ -DRMM_LOGGING_LEVEL=OFF \ -DBUILD_SHARED_LIBS=OFF && \ cmake --build . --parallel ${PARALLEL_LEVEL} && \ @@ -239,6 +242,9 @@ if [[ "${EXTRA_CMAKE_ARGS}" != *"DFIND_CUDF_CPP"* ]]; then EXTRA_CMAKE_ARGS="${EXTRA_CMAKE_ARGS} -DFIND_CUDF_CPP=ON" fi +if hasArg --disable_large_strings; then + BUILD_DISABLE_LARGE_STRINGS="ON" +fi # If clean given, run it prior to any other steps if hasArg clean; then @@ -292,6 +298,7 @@ if buildAll || hasArg libcudf; then -DBUILD_BENCHMARKS=${BUILD_BENCHMARKS} \ -DDISABLE_DEPRECATION_WARNINGS=${BUILD_DISABLE_DEPRECATION_WARNINGS} \ -DCUDF_USE_PER_THREAD_DEFAULT_STREAM=${BUILD_PER_THREAD_DEFAULT_STREAM} \ + -DCUDF_LARGE_STRINGS_DISABLED=${BUILD_DISABLE_LARGE_STRINGS} \ -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ ${EXTRA_CMAKE_ARGS} diff --git a/ci/test_java.sh b/ci/test_java.sh index 9713eb192d2..629ad11014a 100755 --- a/ci/test_java.sh +++ b/ci/test_java.sh @@ -39,6 +39,9 @@ EXITCODE=0 trap "EXITCODE=1" ERR set +e +# disable large strings +export LIBCUDF_LARGE_STRINGS_ENABLED=0 + rapids-logger "Run Java tests" pushd java mvn test -B -DCUDF_JNI_ENABLE_PROFILING=OFF diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 54070ab6f5a..2811711d58c 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -52,6 +52,8 @@ option(JITIFY_USE_CACHE "Use a file cache for JIT compiled kernels" ON) option(CUDF_BUILD_TESTUTIL "Whether to build the test utilities contained in libcudf" ON) mark_as_advanced(CUDF_BUILD_TESTUTIL) option(CUDF_USE_PROPRIETARY_NVCOMP "Download and use NVCOMP with proprietary extensions" ON) +option(CUDF_LARGE_STRINGS_DISABLED "Build with large string support disabled" OFF) +mark_as_advanced(CUDF_LARGE_STRINGS_DISABLED) 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) @@ -783,6 +785,11 @@ if(NOT USE_NVTX) target_compile_definitions(cudf PUBLIC NVTX_DISABLE) endif() +# Disable large strings support +if(CUDF_LARGE_STRINGS_DISABLED) + target_compile_definitions(cudf PRIVATE CUDF_LARGE_STRINGS_DISABLED) +endif() + # Define RMM logging level target_compile_definitions(cudf PRIVATE "RMM_LOGGING_LEVEL=LIBCUDF_LOGGING_LEVEL") diff --git a/cpp/src/strings/utilities.cu b/cpp/src/strings/utilities.cu index 101004a5d06..f70598f33be 100644 --- a/cpp/src/strings/utilities.cu +++ b/cpp/src/strings/utilities.cu @@ -158,8 +158,13 @@ int64_t get_offset64_threshold() bool is_large_strings_enabled() { + // default depends on compile-time switch but can be overridden by the environment variable auto const env = std::getenv("LIBCUDF_LARGE_STRINGS_ENABLED"); +#ifdef CUDF_LARGE_STRINGS_DISABLED return env != nullptr && std::string(env) == "1"; +#else + return env == nullptr || std::string(env) == "1"; +#endif } int64_t get_offset_value(cudf::column_view const& offsets, diff --git a/python/cudf/cudf/tests/test_column.py b/python/cudf/cudf/tests/test_column.py index ea919c786b9..c288155112c 100644 --- a/python/cudf/cudf/tests/test_column.py +++ b/python/cudf/cudf/tests/test_column.py @@ -515,17 +515,6 @@ def test_build_series_from_nullable_pandas_dtype(pd_dtype, expect_dtype): np.testing.assert_array_equal(expect_mask, got_mask) -def test_concatenate_large_column_strings(): - num_strings = 1_000_000 - string_scale_f = 100 - - s_1 = cudf.Series(["very long string " * string_scale_f] * num_strings) - s_2 = cudf.Series(["very long string " * string_scale_f] * num_strings) - - with pytest.raises(OverflowError): - cudf.concat([s_1, s_2]) - - @pytest.mark.parametrize( "alias,expect_dtype", [ From f534e2026a8437190be0b3ea441b1b622b72cef6 Mon Sep 17 00:00:00 2001 From: Robert Maynard Date: Tue, 2 Jul 2024 16:20:03 -0400 Subject: [PATCH 12/42] cudf::merge public API now support passing a user stream (#16124) Expands the `cudf::merge` function to support a user stream Found as part of https://github.com/rapidsai/cudf/pull/15982 when building benchmarks Authors: - Robert Maynard (https://github.com/robertmaynard) - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Muhammad Haseeb (https://github.com/mhaseeb123) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/16124 --- cpp/include/cudf/detail/merge.hpp | 1 + cpp/include/cudf/merge.hpp | 3 +- cpp/src/merge/merge.cu | 4 +- cpp/tests/CMakeLists.txt | 1 + cpp/tests/streams/merge_test.cpp | 137 ++++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 cpp/tests/streams/merge_test.cpp diff --git a/cpp/include/cudf/detail/merge.hpp b/cpp/include/cudf/detail/merge.hpp index 837eda0d7b5..56ac0554403 100644 --- a/cpp/include/cudf/detail/merge.hpp +++ b/cpp/include/cudf/detail/merge.hpp @@ -46,6 +46,7 @@ using index_vector = rmm::device_uvector; * std::vector const& key_cols, * std::vector const& column_order, * std::vector const& null_precedence, + * rmm::cuda_stream_view stream, * rmm::device_async_resource_ref mr) * * @param stream CUDA stream used for device memory operations and kernel launches diff --git a/cpp/include/cudf/merge.hpp b/cpp/include/cudf/merge.hpp index 29aa3ffe934..301e56c19b8 100644 --- a/cpp/include/cudf/merge.hpp +++ b/cpp/include/cudf/merge.hpp @@ -97,6 +97,7 @@ namespace cudf { * @param[in] column_order Sort order types of columns indexed by key_cols * @param[in] null_precedence Array indicating the order of nulls with respect * to non-nulls for the indexing columns (key_cols) + * @param stream CUDA stream used for device memory operations and kernel launches * @param mr Device memory resource used to allocate the returned table's device memory * * @returns A table containing sorted data from all input tables @@ -106,7 +107,7 @@ std::unique_ptr merge( std::vector const& key_cols, std::vector const& column_order, std::vector const& null_precedence = {}, + rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = rmm::mr::get_current_device_resource()); - /** @} */ // end of group } // namespace cudf diff --git a/cpp/src/merge/merge.cu b/cpp/src/merge/merge.cu index 630cf328579..7ecaa0fba56 100644 --- a/cpp/src/merge/merge.cu +++ b/cpp/src/merge/merge.cu @@ -694,11 +694,11 @@ std::unique_ptr merge(std::vector const& tables_to_merg std::vector const& key_cols, std::vector const& column_order, std::vector const& null_precedence, + rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); - return detail::merge( - tables_to_merge, key_cols, column_order, null_precedence, cudf::get_default_stream(), mr); + return detail::merge(tables_to_merge, key_cols, column_order, null_precedence, stream, mr); } } // namespace cudf diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 0eab9ba61d8..8e2017ccb97 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -692,6 +692,7 @@ ConfigureTest(STREAM_INTEROP_TEST streams/interop_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_JSONIO_TEST streams/io/json_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_LABELING_BINS_TEST streams/labeling_bins_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_LISTS_TEST streams/lists_test.cpp STREAM_MODE testing) +ConfigureTest(STREAM_MERGE_TEST streams/merge_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_MULTIBYTE_SPLIT_TEST streams/io/multibyte_split_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_NULL_MASK_TEST streams/null_mask_test.cpp STREAM_MODE testing) ConfigureTest(STREAM_ORCIO_TEST streams/io/orc_test.cpp STREAM_MODE testing) diff --git a/cpp/tests/streams/merge_test.cpp b/cpp/tests/streams/merge_test.cpp new file mode 100644 index 00000000000..1dfe877878d --- /dev/null +++ b/cpp/tests/streams/merge_test.cpp @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2024, 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 + +template +class MergeTest_ : public cudf::test::BaseFixture {}; + +TYPED_TEST_SUITE(MergeTest_, cudf::test::FixedWidthTypes); + +TYPED_TEST(MergeTest_, MergeIsZeroWhenShouldNotBeZero) +{ + using columnFactoryT = cudf::test::fixed_width_column_wrapper; + + columnFactoryT leftColWrap1({1, 2, 3, 4, 5}); + cudf::test::fixed_width_column_wrapper rightColWrap1{}; + + std::vector key_cols{0}; + std::vector column_order; + column_order.push_back(cudf::order::ASCENDING); + std::vector null_precedence(column_order.size(), cudf::null_order::AFTER); + + cudf::table_view left_view{{leftColWrap1}}; + cudf::table_view right_view{{rightColWrap1}}; + cudf::table_view expected{{leftColWrap1}}; + + auto result = cudf::merge({left_view, right_view}, + key_cols, + column_order, + null_precedence, + cudf::test::get_default_stream()); + + int expected_len = 5; + ASSERT_EQ(result->num_rows(), expected_len); + CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result->view()); +} + +TYPED_TEST(MergeTest_, SingleTableInput) +{ + cudf::size_type inputRows = 40; + + auto sequence = cudf::detail::make_counting_transform_iterator(0, [](auto i) { return i; }); + cudf::test::fixed_width_column_wrapper + colWrap1(sequence, sequence + inputRows); + + std::vector key_cols{0}; + std::vector column_order{cudf::order::ASCENDING}; + std::vector null_precedence{}; + + cudf::table_view left_view{{colWrap1}}; + + std::unique_ptr p_outputTable; + CUDF_EXPECT_NO_THROW( + p_outputTable = cudf::merge( + {left_view}, key_cols, column_order, null_precedence, cudf::test::get_default_stream())); + + auto input_column_view{left_view.column(0)}; + auto output_column_view{p_outputTable->view().column(0)}; + + CUDF_TEST_EXPECT_COLUMNS_EQUAL(input_column_view, output_column_view); +} + +class MergeTest : public cudf::test::BaseFixture {}; + +TEST_F(MergeTest, KeysWithNulls) +{ + cudf::size_type nrows = 13200; // Ensures that thrust::merge uses more than one tile/block + auto data_iter = thrust::make_counting_iterator(0); + auto valids1 = + cudf::detail::make_counting_transform_iterator(0, [](auto row) { return row % 10 != 0; }); + cudf::test::fixed_width_column_wrapper data1(data_iter, data_iter + nrows, valids1); + auto valids2 = + cudf::detail::make_counting_transform_iterator(0, [](auto row) { return row % 15 != 0; }); + cudf::test::fixed_width_column_wrapper data2(data_iter, data_iter + nrows, valids2); + auto all_data = cudf::concatenate(std::vector{{data1, data2}}, + cudf::test::get_default_stream()); + + std::vector column_orders{cudf::order::ASCENDING, cudf::order::DESCENDING}; + std::vector null_precedences{cudf::null_order::AFTER, cudf::null_order::BEFORE}; + + for (auto co : column_orders) + for (auto np : null_precedences) { + std::vector column_order{co}; + std::vector null_precedence{np}; + auto sorted1 = cudf::sort(cudf::table_view({data1}), + column_order, + null_precedence, + cudf::test::get_default_stream()) + ->release(); + auto col1 = sorted1.front()->view(); + auto sorted2 = cudf::sort(cudf::table_view({data2}), + column_order, + null_precedence, + cudf::test::get_default_stream()) + ->release(); + auto col2 = sorted2.front()->view(); + + auto result = cudf::merge({cudf::table_view({col1}), cudf::table_view({col2})}, + {0}, + column_order, + null_precedence, + cudf::test::get_default_stream()); + auto sorted_all = cudf::sort(cudf::table_view({all_data->view()}), + column_order, + null_precedence, + cudf::test::get_default_stream()); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(sorted_all->view().column(0), result->view().column(0)); + } +} + +CUDF_TEST_PROGRAM_MAIN() From 9b69d88866aca94b3a7eabbb2e6a82cce6f55e60 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 2 Jul 2024 18:00:30 -0400 Subject: [PATCH 13/42] Fix unused-return-value debug build error in from_arrow_stream_test.cpp (#16168) Fixes a debug build error reporting an unused return value in `from_arrow_stream_test.cpp` ``` g++ -DFMT_HEADER_ONLY=1 -DLIBCUDACXX_ENABLE_EXPERIMENTAL_MEMORY_RESOURCE -DNANOARROW_DEBUG -DSPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO -DSPDLOG_FMT_EXTERNAL -DTHRUST_DEVICE_SYSTEM=THRUST_DEVICE_SYSTEM_CUDA -DTHRUST_DISABLE_ABI_NAMESPACE -DTHRUST_HOST_SYSTEM=THRUST_HOST_SYSTEM_CPP -DTHRUST_IGNORE_ABI_NAMESPACE_ERROR -I/cudf/cpp -I/cudf/cpp/src -I/cudf/cpp/build/_deps/dlpack-src/include -I/cudf/cpp/build/_deps/jitify-src -I/cudf/cpp/include -I/cudf/cpp/build/include -I/cudf/cpp/build/_deps/cccl-src/thrust/thrust/cmake/../.. -I/cudf/cpp/build/_deps/cccl-src/libcudacxx/lib/cmake/libcudacxx/../../../include -I/cudf/cpp/build/_deps/cccl-src/cub/cub/cmake/../.. -I/cudf/cpp/build/_deps/nanoarrow-src/src -I/cudf/cpp/build/_deps/nanoarrow-build/generated -isystem /cudf/cpp/build/_deps/gtest-src/googlemock/include -isystem /cudf/cpp/build/_deps/gtest-src/googlemock -isystem /cudf/cpp/build/_deps/gtest-src/googletest/include -isystem /cudf/cpp/build/_deps/gtest-src/googletest -fvisibility-inlines-hidden -fmessage-length=0 -march=nocona -mtune=haswell -ftree-vectorize -fPIC -fstack-protector-strong -fno-plt -O2 -ffunction-sections -pipe -isystem /conda/envs/rapids/include -fdiagnostics-color=always -I/conda/envs/rapids/targets/x86_64-linux/include -L/conda/envs/rapids/targets/x86_64-linux/lib -L/conda/envs/rapids/targets/x86_64-linux/lib/stubs -g -std=gnu++17 -fPIE -Wall -Werror -Wno-unknown-pragmas -Wno-error=deprecated-declarations -pthread -MD -MT tests/CMakeFiles/INTEROP_TEST.dir/interop/from_arrow_stream_test.cpp.o -MF tests/CMakeFiles/INTEROP_TEST.dir/interop/from_arrow_stream_test.cpp.o.d -o tests/CMakeFiles/INTEROP_TEST.dir/interop/from_arrow_stream_test.cpp.o -c /cudf/cpp/tests/interop/from_arrow_stream_test.cpp /cudf/cpp/tests/interop/from_arrow_stream_test.cpp: In static member function 'static int VectorOfArrays::get_schema(ArrowArrayStream*, ArrowSchema*)': /cudf/cpp/tests/interop/from_arrow_stream_test.cpp:49:24: error: ignoring return value of 'ArrowErrorCode cudfArrowSchemaDeepCopy(const ArrowSchema*, ArrowSchema*)' declared with attribute 'warn_unused_result' [-Werror=unused-result] 49 | ArrowSchemaDeepCopy(private_data->schema.get(), out_schema); cc1plus: all warnings being treated as errors ``` Adding a variable decorated with `[[maybe_unused]]` clears the error. Error introduced in #15904 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Shruti Shivakumar (https://github.com/shrshi) URL: https://github.com/rapidsai/cudf/pull/16168 --- cpp/tests/interop/from_arrow_stream_test.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/tests/interop/from_arrow_stream_test.cpp b/cpp/tests/interop/from_arrow_stream_test.cpp index 418ec057303..80a2e4b2ffd 100644 --- a/cpp/tests/interop/from_arrow_stream_test.cpp +++ b/cpp/tests/interop/from_arrow_stream_test.cpp @@ -46,7 +46,8 @@ struct VectorOfArrays { static int get_schema(ArrowArrayStream* stream, ArrowSchema* out_schema) { auto private_data = static_cast(stream->private_data); - ArrowSchemaDeepCopy(private_data->schema.get(), out_schema); + + [[maybe_unused]] auto rc = ArrowSchemaDeepCopy(private_data->schema.get(), out_schema); return 0; } From 25febbcade60d5eefb5568cdc036c845d29dc932 Mon Sep 17 00:00:00 2001 From: Jihoon Son Date: Tue, 2 Jul 2024 16:05:38 -0700 Subject: [PATCH 14/42] Add throughput metrics for REDUCTION_BENCH/REDUCTION_NVBENCH benchmarks (#16126) This PR addresses https://github.com/rapidsai/cudf/issues/13735 for reduction benchmarks. There are 3 new utils added. - `int64_t estimate_size(cudf::table_view)` returns a size estimate for the given table. https://github.com/rapidsai/cudf/pull/13984 was a previous attempt to add a similar utility, but this implementation uses `cudf::row_bit_count()` as suggested in https://github.com/rapidsai/cudf/pull/13984#issuecomment-2189916570 instead of manually estimating the size. - `void set_items_processed(State& state, int64_t items_processed_per_iteration)` is a thin wrapper of `State.SetItemsProcessed()`. This wrapper takes `items_processed_per_iteration` as a parameter instead of `total_items_processed`. This could be useful to avoid repeating `State.iterations() * items_processed_per_iteration` in each benchmark class. - `void set_throughputs(nvbench::state& state)` is added as a workaround for https://github.com/NVIDIA/nvbench/issues/175. We sometimes want to set throughput statistics after `state.exec()` calls especially when it is hard to estimate the result size upfront. Here are snippets of reduction benchmarks after this change. ``` $ cpp/build/benchmarks/REDUCTION_BENCH ... ----------------------------------------------------------------------------------------------------------------- Benchmark Time CPU Iterations UserCounters... ----------------------------------------------------------------------------------------------------------------- Reduction/bool_all/10000/manual_time 10257 ns 26845 ns 68185 bytes_per_second=929.907M/s items_per_second=975.078M/s Reduction/bool_all/100000/manual_time 11000 ns 27454 ns 63634 bytes_per_second=8.46642G/s items_per_second=9.09075G/s Reduction/bool_all/1000000/manual_time 12671 ns 28658 ns 55261 bytes_per_second=73.5018G/s items_per_second=78.922G/s ... $ cpp/build/benchmarks/REDUCTION_NVBENCH ... ## rank_scan ### [0] NVIDIA RTX A5500 | T | null_probability | data_size | Samples | CPU Time | Noise | GPU Time | Noise | Elem/s | GlobalMem BW | BWUtil | |-----------------|------------------|-----------|---------|------------|--------|------------|-------|----------|--------------|-----------| | I32 | 0 | 10000 | 16992x | 33.544 us | 14.95% | 29.446 us | 5.58% | 82.321M | 5.596 TB/s | 728.54% | | I32 | 0.1 | 10000 | 16512x | 34.358 us | 13.66% | 30.292 us | 2.87% | 80.020M | 5.286 TB/s | 688.17% | | I32 | 0.5 | 10000 | 16736x | 34.058 us | 14.31% | 29.890 us | 3.40% | 81.097M | 5.430 TB/s | 706.89% | ... ``` Note that, when the data type is a 1-byte-width type in the google benchmark result summary, `bytes_per_second` appears to be smaller than `items_per_second`. This is because the former is a multiple of 1000 whereas the latter is a multiple of 1024. They are in fact the same number. Implementation-wise, these are what I'm not sure if I made a best decision. - Each of new utils above is declared and defined in different files. I did this because I could not find a good place to have them all, and they seem to belong to different utilities. Please let me know if there is a better place for them. - All the new utils are defined in the global namespace since other util functions seem to have been defined in the same way. Please let me know if this is not the convention. Authors: - Jihoon Son (https://github.com/jihoonson) Approvers: - Mark Harris (https://github.com/harrism) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/16126 --- cpp/benchmarks/CMakeLists.txt | 9 ++- cpp/benchmarks/common/benchmark_utilities.cpp | 27 +++++++++ cpp/benchmarks/common/benchmark_utilities.hpp | 41 +++++++++++++ cpp/benchmarks/common/nvbench_utilities.cpp | 60 +++++++++++++++++++ cpp/benchmarks/common/nvbench_utilities.hpp | 31 ++++++++++ cpp/benchmarks/common/table_utilities.cpp | 41 +++++++++++++ cpp/benchmarks/common/table_utilities.hpp | 41 +++++++++++++ cpp/benchmarks/reduction/anyall.cpp | 8 ++- cpp/benchmarks/reduction/dictionary.cpp | 10 +++- cpp/benchmarks/reduction/minmax.cpp | 13 +++- cpp/benchmarks/reduction/rank.cpp | 13 +++- cpp/benchmarks/reduction/reduce.cpp | 8 ++- cpp/benchmarks/reduction/scan.cpp | 11 +++- cpp/benchmarks/reduction/scan_structs.cpp | 16 ++++- 14 files changed, 314 insertions(+), 15 deletions(-) create mode 100644 cpp/benchmarks/common/benchmark_utilities.cpp create mode 100644 cpp/benchmarks/common/benchmark_utilities.hpp create mode 100644 cpp/benchmarks/common/nvbench_utilities.cpp create mode 100644 cpp/benchmarks/common/nvbench_utilities.hpp create mode 100644 cpp/benchmarks/common/table_utilities.cpp create mode 100644 cpp/benchmarks/common/table_utilities.hpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 8a48126e195..a5b248135c1 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -40,8 +40,13 @@ target_include_directories( # Use an OBJECT library so we only compile these helper source files only once add_library( - cudf_benchmark_common OBJECT "${CUDF_SOURCE_DIR}/tests/utilities/random_seed.cpp" - synchronization/synchronization.cpp io/cuio_common.cpp + cudf_benchmark_common OBJECT + "${CUDF_SOURCE_DIR}/tests/utilities/random_seed.cpp" + synchronization/synchronization.cpp + io/cuio_common.cpp + common/table_utilities.cpp + common/benchmark_utilities.cpp + common/nvbench_utilities.cpp ) target_link_libraries(cudf_benchmark_common PRIVATE cudf_datagen $) add_custom_command( diff --git a/cpp/benchmarks/common/benchmark_utilities.cpp b/cpp/benchmarks/common/benchmark_utilities.cpp new file mode 100644 index 00000000000..0b9fc17e779 --- /dev/null +++ b/cpp/benchmarks/common/benchmark_utilities.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024, 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 "benchmark_utilities.hpp" + +void set_items_processed(::benchmark::State& state, int64_t items_processed_per_iteration) +{ + state.SetItemsProcessed(state.iterations() * items_processed_per_iteration); +} + +void set_bytes_processed(::benchmark::State& state, int64_t bytes_processed_per_iteration) +{ + state.SetBytesProcessed(state.iterations() * bytes_processed_per_iteration); +} diff --git a/cpp/benchmarks/common/benchmark_utilities.hpp b/cpp/benchmarks/common/benchmark_utilities.hpp new file mode 100644 index 00000000000..c5c80e73674 --- /dev/null +++ b/cpp/benchmarks/common/benchmark_utilities.hpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024, 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 + +/** + * @brief Sets the number of items processed during the benchmark. + * + * This function could be used instead of ::benchmark::State.SetItemsProcessed() + * to avoid repeatedly computing ::benchmark::State.iterations() * items_processed_per_iteration. + * + * @param state the benchmark state + * @param items_processed_per_iteration number of items processed per iteration + */ +void set_items_processed(::benchmark::State& state, int64_t items_processed_per_iteration); + +/** + * @brief Sets the number of bytes processed during the benchmark. + * + * This function could be used instead of ::benchmark::State.SetItemsProcessed() + * to avoid repeatedly computing ::benchmark::State.iterations() * bytes_processed_per_iteration. + * + * @param state the benchmark state + * @param bytes_processed_per_iteration number of bytes processed per iteration + */ +void set_bytes_processed(::benchmark::State& state, int64_t bytes_processed_per_iteration); diff --git a/cpp/benchmarks/common/nvbench_utilities.cpp b/cpp/benchmarks/common/nvbench_utilities.cpp new file mode 100644 index 00000000000..c740eaa52f4 --- /dev/null +++ b/cpp/benchmarks/common/nvbench_utilities.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024, 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 "nvbench_utilities.hpp" + +#include + +// This function is copied over from +// https://github.com/NVIDIA/nvbench/blob/a171514056e5d6a7f52a035dd6c812fa301d4f4f/nvbench/detail/measure_cold.cu#L190-L224. +void set_throughputs(nvbench::state& state) +{ + double avg_cuda_time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value"); + + if (const auto items = state.get_element_count(); items != 0) { + auto& summ = state.add_summary("nv/cold/bw/item_rate"); + summ.set_string("name", "Elem/s"); + summ.set_string("hint", "item_rate"); + summ.set_string("description", "Number of input elements processed per second"); + summ.set_float64("value", static_cast(items) / avg_cuda_time); + } + + if (const auto bytes = state.get_global_memory_rw_bytes(); bytes != 0) { + const auto avg_used_gmem_bw = static_cast(bytes) / avg_cuda_time; + { + auto& summ = state.add_summary("nv/cold/bw/global/bytes_per_second"); + summ.set_string("name", "GlobalMem BW"); + summ.set_string("hint", "byte_rate"); + summ.set_string("description", + "Number of bytes read/written per second to the CUDA " + "device's global memory"); + summ.set_float64("value", avg_used_gmem_bw); + } + + { + const auto peak_gmem_bw = + static_cast(state.get_device()->get_global_memory_bus_bandwidth()); + + auto& summ = state.add_summary("nv/cold/bw/global/utilization"); + summ.set_string("name", "BWUtil"); + summ.set_string("hint", "percentage"); + summ.set_string("description", + "Global device memory utilization as a percentage of the " + "device's peak bandwidth"); + summ.set_float64("value", avg_used_gmem_bw / peak_gmem_bw); + } + } +} diff --git a/cpp/benchmarks/common/nvbench_utilities.hpp b/cpp/benchmarks/common/nvbench_utilities.hpp new file mode 100644 index 00000000000..98d879efac5 --- /dev/null +++ b/cpp/benchmarks/common/nvbench_utilities.hpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, 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 nvbench { +struct state; +} + +/** + * @brief Sets throughput statistics, such as "Elem/s", "GlobalMem BW", and "BWUtil" for the + * nvbench results summary. + * + * This function could be used to work around a known issue that the throughput statistics + * should be added before the nvbench::state.exec() call, otherwise they will not be printed + * in the summary. See https://github.com/NVIDIA/nvbench/issues/175 for more details. + */ +void set_throughputs(nvbench::state& state); diff --git a/cpp/benchmarks/common/table_utilities.cpp b/cpp/benchmarks/common/table_utilities.cpp new file mode 100644 index 00000000000..a6fbdac9fb8 --- /dev/null +++ b/cpp/benchmarks/common/table_utilities.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024, 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 "table_utilities.hpp" + +#include +#include + +#include + +int64_t estimate_size(cudf::column_view const& col) +{ + return estimate_size(cudf::table_view({col})); +} + +int64_t estimate_size(cudf::table_view const& view) +{ + // Compute the size in bits for each row. + auto const row_sizes = cudf::row_bit_count(view); + // Accumulate the row sizes to compute a sum. + auto const agg = cudf::make_sum_aggregation(); + cudf::data_type sum_dtype{cudf::type_id::INT64}; + auto const total_size_scalar = cudf::reduce(*row_sizes, *agg, sum_dtype); + auto const total_size_in_bits = + static_cast*>(total_size_scalar.get())->value(); + // Convert the size in bits to the size in bytes. + return static_cast(std::ceil(static_cast(total_size_in_bits) / 8)); +} diff --git a/cpp/benchmarks/common/table_utilities.hpp b/cpp/benchmarks/common/table_utilities.hpp new file mode 100644 index 00000000000..04ee847d397 --- /dev/null +++ b/cpp/benchmarks/common/table_utilities.hpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024, 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 + +/** + * @brief Estimates the column size in bytes. + * + * @remark As this function internally uses cudf::row_bit_count() to estimate each row size + * and accumulates them, the returned estimate may be an inexact approximation in some + * cases. See cudf::row_bit_count() for more details. + * + * @param view The column view to estimate its size + */ +int64_t estimate_size(cudf::column_view const& view); + +/** + * @brief Estimates the table size in bytes. + * + * @remark As this function internally uses cudf::row_bit_count() to estimate each row size + * and accumulates them, the returned estimate may be an inexact approximation in some + * cases. See cudf::row_bit_count() for more details. + * + * @param view The table view to estimate its size + */ +int64_t estimate_size(cudf::table_view const& view); diff --git a/cpp/benchmarks/reduction/anyall.cpp b/cpp/benchmarks/reduction/anyall.cpp index 8b1e71c1585..e9d23881764 100644 --- a/cpp/benchmarks/reduction/anyall.cpp +++ b/cpp/benchmarks/reduction/anyall.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ * limitations under the License. */ +#include #include +#include #include #include @@ -42,6 +44,10 @@ void BM_reduction_anyall(benchmark::State& state, cuda_event_timer timer(state, true); auto result = cudf::reduce(*values, *agg, output_dtype); } + + // The benchmark takes a column and produces one scalar. + set_items_processed(state, column_size + 1); + set_bytes_processed(state, estimate_size(values->view()) + cudf::size_of(output_dtype)); } #define concat(a, b, c) a##b##c diff --git a/cpp/benchmarks/reduction/dictionary.cpp b/cpp/benchmarks/reduction/dictionary.cpp index c1c44c919ac..5095337dbb3 100644 --- a/cpp/benchmarks/reduction/dictionary.cpp +++ b/cpp/benchmarks/reduction/dictionary.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ * limitations under the License. */ +#include #include #include #include @@ -52,6 +53,13 @@ void BM_reduction_dictionary(benchmark::State& state, cuda_event_timer timer(state, true); auto result = cudf::reduce(*values, *agg, output_dtype); } + + // The benchmark takes a column and produces two scalars. + set_items_processed(state, column_size + 1); + + // We don't set the metrics for the size read/written as row_bit_count() doesn't + // support the dictionary type yet (and so is estimate_size()). + // See https://github.com/rapidsai/cudf/issues/16121 for details. } #define concat(a, b, c) a##b##c diff --git a/cpp/benchmarks/reduction/minmax.cpp b/cpp/benchmarks/reduction/minmax.cpp index 963c26692e7..050f2887221 100644 --- a/cpp/benchmarks/reduction/minmax.cpp +++ b/cpp/benchmarks/reduction/minmax.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ * limitations under the License. */ +#include #include +#include #include #include @@ -28,14 +30,19 @@ template void BM_reduction(benchmark::State& state) { cudf::size_type const column_size{(cudf::size_type)state.range(0)}; - auto const dtype = cudf::type_to_id(); + auto const dtype_id = cudf::type_to_id(); auto const input_column = - create_random_column(dtype, row_count{column_size}, data_profile_builder().no_validity()); + create_random_column(dtype_id, row_count{column_size}, data_profile_builder().no_validity()); for (auto _ : state) { cuda_event_timer timer(state, true); auto result = cudf::minmax(*input_column); } + + // The benchmark takes a column and produces two scalars. + set_items_processed(state, column_size + 2); + cudf::data_type dtype = cudf::data_type{dtype_id}; + set_bytes_processed(state, estimate_size(input_column->view()) + 2 * cudf::size_of(dtype)); } #define concat(a, b, c) a##b##c diff --git a/cpp/benchmarks/reduction/rank.cpp b/cpp/benchmarks/reduction/rank.cpp index e55f3b9e09f..14876c80d3e 100644 --- a/cpp/benchmarks/reduction/rank.cpp +++ b/cpp/benchmarks/reduction/rank.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, NVIDIA CORPORATION. + * Copyright (c) 2022-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ */ #include +#include +#include #include #include @@ -39,11 +41,18 @@ static void nvbench_reduction_scan(nvbench::state& state, nvbench::type_listview(), 2); cudf::column_view input(new_tbl->view().column(0)); + std::unique_ptr result = nullptr; state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { rmm::cuda_stream_view stream_view{launch.get_stream()}; - auto result = cudf::detail::inclusive_dense_rank_scan( + result = cudf::detail::inclusive_dense_rank_scan( input, stream_view, rmm::mr::get_current_device_resource()); }); + + state.add_element_count(input.size()); + state.add_global_memory_reads(estimate_size(input)); + state.add_global_memory_writes(estimate_size(result->view())); + + set_throughputs(state); } using data_type = nvbench::type_list; diff --git a/cpp/benchmarks/reduction/reduce.cpp b/cpp/benchmarks/reduction/reduce.cpp index 5bd3e2e3bba..63c96f4fe9e 100644 --- a/cpp/benchmarks/reduction/reduce.cpp +++ b/cpp/benchmarks/reduction/reduce.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023, NVIDIA CORPORATION. + * Copyright (c) 2020-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ * limitations under the License. */ +#include #include +#include #include #include @@ -46,6 +48,10 @@ void BM_reduction(benchmark::State& state, std::unique_ptrview()) + cudf::size_of(output_dtype)); } #define concat(a, b, c) a##b##c diff --git a/cpp/benchmarks/reduction/scan.cpp b/cpp/benchmarks/reduction/scan.cpp index 8c9883ece9c..dc05aad9807 100644 --- a/cpp/benchmarks/reduction/scan.cpp +++ b/cpp/benchmarks/reduction/scan.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ * limitations under the License. */ +#include #include +#include #include #include @@ -34,11 +36,16 @@ static void BM_reduction_scan(benchmark::State& state, bool include_nulls) auto const column = create_random_column(dtype, row_count{n_rows}); if (!include_nulls) column->set_null_mask(rmm::device_buffer{}, 0); + std::unique_ptr result = nullptr; for (auto _ : state) { cuda_event_timer timer(state, true); - auto result = cudf::scan( + result = cudf::scan( *column, *cudf::make_min_aggregation(), cudf::scan_type::INCLUSIVE); } + + // The benchmark takes a column and produces a new column of the same size as input. + set_items_processed(state, n_rows * 2); + set_bytes_processed(state, estimate_size(column->view()) + estimate_size(result->view())); } #define SCAN_BENCHMARK_DEFINE(name, type, nulls) \ diff --git a/cpp/benchmarks/reduction/scan_structs.cpp b/cpp/benchmarks/reduction/scan_structs.cpp index ee97b54fbef..a781f75a314 100644 --- a/cpp/benchmarks/reduction/scan_structs.cpp +++ b/cpp/benchmarks/reduction/scan_structs.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, NVIDIA CORPORATION. + * Copyright (c) 2022-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ */ #include +#include +#include #include #include @@ -45,16 +47,24 @@ static void nvbench_structs_scan(nvbench::state& state) auto [null_mask, null_count] = create_random_null_mask(size, null_probability); auto const input = cudf::make_structs_column( size, std::move(data_table->release()), null_count, std::move(null_mask)); + auto input_view = input->view(); auto const agg = cudf::make_min_aggregation(); auto const null_policy = static_cast(state.get_int64("null_policy")); auto const stream = cudf::get_default_stream(); state.set_cuda_stream(nvbench::make_cuda_stream_view(stream.value())); + std::unique_ptr result = nullptr; state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { - auto const result = cudf::detail::scan_inclusive( - *input, *agg, null_policy, stream, rmm::mr::get_current_device_resource()); + result = cudf::detail::scan_inclusive( + input_view, *agg, null_policy, stream, rmm::mr::get_current_device_resource()); }); + + state.add_element_count(input_view.size()); + state.add_global_memory_reads(estimate_size(input_view)); + state.add_global_memory_writes(estimate_size(result->view())); + + set_throughputs(state); } NVBENCH_BENCH(nvbench_structs_scan) From 3aedeeaaaa08bb99695bbbc34098a5660e4c94e0 Mon Sep 17 00:00:00 2001 From: brandon-b-miller <53796099+brandon-b-miller@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:40:23 -0500 Subject: [PATCH 15/42] `cudf-polars` string slicing (#16082) This PR plumbs the libcudf/pylibcudf `slice_strings` function through to cudf-polars. Depends on https://github.com/rapidsai/cudf/pull/15988 Authors: - https://github.com/brandon-b-miller - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/16082 --- python/cudf_polars/cudf_polars/dsl/expr.py | 36 +++++++++++++++ .../tests/expressions/test_stringfunction.py | 46 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index fe859c8d958..cfc2947f8de 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -703,6 +703,7 @@ def _validate_input(self): pl_expr.StringFunction.EndsWith, pl_expr.StringFunction.StartsWith, pl_expr.StringFunction.Contains, + pl_expr.StringFunction.Slice, ): raise NotImplementedError(f"String function {self.name}") if self.name == pl_expr.StringFunction.Contains: @@ -716,6 +717,11 @@ def _validate_input(self): raise NotImplementedError( "Regex contains only supports a scalar pattern" ) + elif self.name == pl_expr.StringFunction.Slice: + if not all(isinstance(child, Literal) for child in self.children[1:]): + raise NotImplementedError( + "Slice only supports literal start and stop values" + ) def do_evaluate( self, @@ -744,6 +750,36 @@ def do_evaluate( flags=plc.strings.regex_flags.RegexFlags.DEFAULT, ) return Column(plc.strings.contains.contains_re(column.obj, prog)) + elif self.name == pl_expr.StringFunction.Slice: + child, expr_offset, expr_length = self.children + assert isinstance(expr_offset, Literal) + assert isinstance(expr_length, Literal) + + column = child.evaluate(df, context=context, mapping=mapping) + # libcudf slices via [start,stop). + # polars slices with offset + length where start == offset + # stop = start + length. Negative values for start look backward + # from the last element of the string. If the end index would be + # below zero, an empty string is returned. + # Do this maths on the host + start = expr_offset.value.as_py() + length = expr_length.value.as_py() + + if length == 0: + stop = start + else: + # No length indicates a scan to the end + # The libcudf equivalent is a null stop + stop = start + length if length else None + if length and start < 0 and length >= -start: + stop = None + return Column( + plc.strings.slice.slice_strings( + column.obj, + plc.interop.from_arrow(pa.scalar(start, type=pa.int32())), + plc.interop.from_arrow(pa.scalar(stop, type=pa.int32())), + ) + ) columns = [ child.evaluate(df, context=context, mapping=mapping) for child in self.children diff --git a/python/cudf_polars/tests/expressions/test_stringfunction.py b/python/cudf_polars/tests/expressions/test_stringfunction.py index 9729e765948..8cf65dd51ac 100644 --- a/python/cudf_polars/tests/expressions/test_stringfunction.py +++ b/python/cudf_polars/tests/expressions/test_stringfunction.py @@ -37,6 +37,30 @@ def ldf(with_nulls): return pl.LazyFrame({"a": a, "b": range(len(a))}) +slice_cases = [ + (1, 3), + (0, 3), + (0, 0), + (-3, 1), + (-100, 5), + (1, 1), + (100, 100), + (-3, 4), + (-3, 3), +] + + +@pytest.fixture(params=slice_cases) +def slice_column_data(ldf, request): + start, length = request.param + if length: + return ldf.with_columns( + pl.lit(start).alias("start"), pl.lit(length).alias("length") + ) + else: + return ldf.with_columns(pl.lit(start).alias("start")) + + def test_supported_stringfunction_expression(ldf): query = ldf.select( pl.col("a").str.starts_with("Z"), @@ -104,3 +128,25 @@ def test_contains_invalid(ldf): query.collect() with pytest.raises(pl.exceptions.ComputeError): query.collect(post_opt_callback=partial(execute_with_cudf, raise_on_fail=True)) + + +@pytest.mark.parametrize("offset", [1, -1, 0, 100, -100]) +def test_slice_scalars_offset(ldf, offset): + query = ldf.select(pl.col("a").str.slice(offset)) + assert_gpu_result_equal(query) + + +@pytest.mark.parametrize("offset,length", slice_cases) +def test_slice_scalars_length_and_offset(ldf, offset, length): + query = ldf.select(pl.col("a").str.slice(offset, length)) + assert_gpu_result_equal(query) + + +def test_slice_column(slice_column_data): + if "length" in slice_column_data.collect_schema(): + query = slice_column_data.select( + pl.col("a").str.slice(pl.col("start"), pl.col("length")) + ) + else: + query = slice_column_data.select(pl.col("a").str.slice(pl.col("start"))) + assert_ir_translation_raises(query, NotImplementedError) From 39de5a2527b297ba79c625993a49b28c3baf5b00 Mon Sep 17 00:00:00 2001 From: Mark Harris <783069+harrism@users.noreply.github.com> Date: Thu, 4 Jul 2024 06:49:06 +1000 Subject: [PATCH 16/42] Refactor from_arrow_device/host to use resource_ref (#16160) Fixes #16159 Also fixes typos / leftovers in dictionary `add_keys` copydocs. Authors: - Mark Harris (https://github.com/harrism) Approvers: - Paul Mattione (https://github.com/pmattione-nvidia) - David Wendt (https://github.com/davidwendt) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/16160 --- .../cudf/dictionary/detail/update_keys.hpp | 10 +++---- cpp/include/cudf/interop.hpp | 29 ++++++++++--------- cpp/src/interop/from_arrow_device.cu | 27 ++++++++--------- cpp/src/interop/from_arrow_host.cu | 19 ++++++------ cpp/src/interop/from_arrow_stream.cu | 6 ++-- 5 files changed, 47 insertions(+), 44 deletions(-) diff --git a/cpp/include/cudf/dictionary/detail/update_keys.hpp b/cpp/include/cudf/dictionary/detail/update_keys.hpp index e8486a80afc..9cdda773dbb 100644 --- a/cpp/include/cudf/dictionary/detail/update_keys.hpp +++ b/cpp/include/cudf/dictionary/detail/update_keys.hpp @@ -29,7 +29,7 @@ namespace dictionary { namespace detail { /** * @copydoc cudf::dictionary::add_keys(dictionary_column_view const&,column_view - * const&,mm::mr::device_memory_resource*) + * const&,rmm::device_async_resource_ref) * * @param stream CUDA stream used for device memory operations and kernel launches. */ @@ -40,7 +40,7 @@ std::unique_ptr add_keys(dictionary_column_view const& dictionary_column /** * @copydoc cudf::dictionary::remove_keys(dictionary_column_view const&,column_view - * const&,mm::mr::device_memory_resource*) + * const&,rmm::device_async_resource_ref) * * @param stream CUDA stream used for device memory operations and kernel launches. */ @@ -51,7 +51,7 @@ std::unique_ptr remove_keys(dictionary_column_view const& dictionary_col /** * @copydoc cudf::dictionary::remove_unused_keys(dictionary_column_view - * const&,mm::mr::device_memory_resource*) + * const&,rmm::device_async_resource_ref) * * @param stream CUDA stream used for device memory operations and kernel launches. */ @@ -61,7 +61,7 @@ std::unique_ptr remove_unused_keys(dictionary_column_view const& diction /** * @copydoc cudf::dictionary::set_keys(dictionary_column_view - * const&,mm::mr::device_memory_resource*) + * const&,rmm::device_async_resource_ref) * * @param stream CUDA stream used for device memory operations and kernel launches. */ @@ -72,7 +72,7 @@ std::unique_ptr set_keys(dictionary_column_view const& dictionary_column /** * @copydoc - * cudf::dictionary::match_dictionaries(std::vector,mm::mr::device_memory_resource*) + * cudf::dictionary::match_dictionaries(std::vector,rmm::device_async_resource_ref) * * @param stream CUDA stream used for device memory operations and kernel launches. */ diff --git a/cpp/include/cudf/interop.hpp b/cpp/include/cudf/interop.hpp index 502ffb9ba4f..11f6ce2bad7 100644 --- a/cpp/include/cudf/interop.hpp +++ b/cpp/include/cudf/interop.hpp @@ -39,6 +39,7 @@ #include #include +#include #include @@ -372,8 +373,8 @@ std::unique_ptr from_arrow( std::unique_ptr from_arrow( ArrowSchema const* schema, ArrowArray const* input, - rmm::cuda_stream_view stream = cudf::get_default_stream(), - rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = rmm::mr::get_current_device_resource()); /** * @brief Create `cudf::column` from a given ArrowArray and ArrowSchema input @@ -391,8 +392,8 @@ std::unique_ptr from_arrow( std::unique_ptr from_arrow_column( ArrowSchema const* schema, ArrowArray const* input, - rmm::cuda_stream_view stream = cudf::get_default_stream(), - rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = rmm::mr::get_current_device_resource()); /** * @brief Create `cudf::table` from given ArrowDeviceArray input @@ -415,8 +416,8 @@ std::unique_ptr from_arrow_column( std::unique_ptr
from_arrow_host( ArrowSchema const* schema, ArrowDeviceArray const* input, - rmm::cuda_stream_view stream = cudf::get_default_stream(), - rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = rmm::mr::get_current_device_resource()); /** * @brief Create `cudf::table` from given ArrowArrayStream input @@ -433,8 +434,8 @@ std::unique_ptr
from_arrow_host( */ std::unique_ptr
from_arrow_stream( ArrowArrayStream* input, - rmm::cuda_stream_view stream = cudf::get_default_stream(), - rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = rmm::mr::get_current_device_resource()); /** * @brief Create `cudf::column` from given ArrowDeviceArray input @@ -456,8 +457,8 @@ std::unique_ptr
from_arrow_stream( std::unique_ptr from_arrow_host_column( ArrowSchema const* schema, ArrowDeviceArray const* input, - rmm::cuda_stream_view stream = cudf::get_default_stream(), - rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = rmm::mr::get_current_device_resource()); /** * @brief typedef for a vector of owning columns, used for conversion from ArrowDeviceArray @@ -537,8 +538,8 @@ using unique_table_view_t = unique_table_view_t from_arrow_device( ArrowSchema const* schema, ArrowDeviceArray const* input, - rmm::cuda_stream_view stream = cudf::get_default_stream(), - rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = rmm::mr::get_current_device_resource()); /** * @brief typedef for a unique_ptr to a `cudf::column_view` with custom deleter @@ -580,8 +581,8 @@ using unique_column_view_t = unique_column_view_t from_arrow_device_column( ArrowSchema const* schema, ArrowDeviceArray const* input, - rmm::cuda_stream_view stream = cudf::get_default_stream(), - rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()); + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = rmm::mr::get_current_device_resource()); /** @} */ // end of group } // namespace cudf diff --git a/cpp/src/interop/from_arrow_device.cu b/cpp/src/interop/from_arrow_device.cu index 73c1a474310..e1d289e67a3 100644 --- a/cpp/src/interop/from_arrow_device.cu +++ b/cpp/src/interop/from_arrow_device.cu @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -56,7 +57,7 @@ struct dispatch_from_arrow_device { data_type, bool, rmm::cuda_stream_view, - rmm::mr::device_memory_resource*) + rmm::device_async_resource_ref) { CUDF_FAIL("Unsupported type in from_arrow_device", cudf::data_type_error); } @@ -68,7 +69,7 @@ struct dispatch_from_arrow_device { data_type type, bool skip_mask, rmm::cuda_stream_view, - rmm::mr::device_memory_resource*) + rmm::device_async_resource_ref mr) { size_type const num_rows = input->length; size_type const offset = input->offset; @@ -90,7 +91,7 @@ dispatch_tuple_t get_column(ArrowSchemaView* schema, data_type type, bool skip_mask, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr); + rmm::device_async_resource_ref mr); template <> dispatch_tuple_t dispatch_from_arrow_device::operator()(ArrowSchemaView* schema, @@ -98,7 +99,7 @@ dispatch_tuple_t dispatch_from_arrow_device::operator()(ArrowSchemaView* s data_type type, bool skip_mask, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { if (input->length == 0) { return std::make_tuple( @@ -141,7 +142,7 @@ dispatch_tuple_t dispatch_from_arrow_device::operator()( data_type type, bool skip_mask, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_EXPECTS(schema->type != NANOARROW_TYPE_LARGE_STRING, "Large strings are not yet supported in from_arrow_device", @@ -182,7 +183,7 @@ dispatch_tuple_t dispatch_from_arrow_device::operator()( data_type type, bool skip_mask, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { ArrowSchemaView keys_schema_view; NANOARROW_THROW_NOT_OK( @@ -238,7 +239,7 @@ dispatch_tuple_t dispatch_from_arrow_device::operator()( data_type type, bool skip_mask, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { std::vector children; owned_columns_t out_owned_cols; @@ -283,7 +284,7 @@ dispatch_tuple_t dispatch_from_arrow_device::operator()( data_type type, bool skip_mask, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { size_type const num_rows = input->length; size_type const offset = input->offset; @@ -324,7 +325,7 @@ dispatch_tuple_t get_column(ArrowSchemaView* schema, data_type type, bool skip_mask, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { return type.id() != type_id::EMPTY ? std::move(type_dispatcher( @@ -342,7 +343,7 @@ dispatch_tuple_t get_column(ArrowSchemaView* schema, unique_table_view_t from_arrow_device(ArrowSchema const* schema, ArrowDeviceArray const* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_EXPECTS(schema != nullptr && input != nullptr, "input ArrowSchema and ArrowDeviceArray must not be NULL", @@ -397,7 +398,7 @@ unique_table_view_t from_arrow_device(ArrowSchema const* schema, unique_column_view_t from_arrow_device_column(ArrowSchema const* schema, ArrowDeviceArray const* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_EXPECTS(schema != nullptr && input != nullptr, "input ArrowSchema and ArrowDeviceArray must not be NULL", @@ -429,7 +430,7 @@ unique_column_view_t from_arrow_device_column(ArrowSchema const* schema, unique_table_view_t from_arrow_device(ArrowSchema const* schema, ArrowDeviceArray const* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -439,7 +440,7 @@ unique_table_view_t from_arrow_device(ArrowSchema const* schema, unique_column_view_t from_arrow_device_column(ArrowSchema const* schema, ArrowDeviceArray const* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); diff --git a/cpp/src/interop/from_arrow_host.cu b/cpp/src/interop/from_arrow_host.cu index b7e07056686..b3087dedf98 100644 --- a/cpp/src/interop/from_arrow_host.cu +++ b/cpp/src/interop/from_arrow_host.cu @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -49,7 +50,7 @@ namespace { struct dispatch_copy_from_arrow_host { rmm::cuda_stream_view stream; - rmm::mr::device_memory_resource* mr; + rmm::device_async_resource_ref mr; std::unique_ptr get_mask_buffer(ArrowArray const* array) { @@ -131,7 +132,7 @@ std::unique_ptr get_column_copy(ArrowSchemaView* schema, data_type type, bool skip_mask, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr); + rmm::device_async_resource_ref mr); template <> std::unique_ptr dispatch_copy_from_arrow_host::operator()(ArrowSchemaView* schema, @@ -388,7 +389,7 @@ std::unique_ptr get_column_copy(ArrowSchemaView* schema, data_type type, bool skip_mask, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { return type.id() != type_id::EMPTY ? std::move(type_dispatcher( @@ -405,7 +406,7 @@ std::unique_ptr get_column_copy(ArrowSchemaView* schema, std::unique_ptr
from_arrow_host(ArrowSchema const* schema, ArrowDeviceArray const* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_EXPECTS(schema != nullptr && input != nullptr, "input ArrowSchema and ArrowDeviceArray must not be NULL", @@ -441,7 +442,7 @@ std::unique_ptr
from_arrow_host(ArrowSchema const* schema, std::unique_ptr from_arrow_host_column(ArrowSchema const* schema, ArrowDeviceArray const* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_EXPECTS(schema != nullptr && input != nullptr, "input ArrowSchema and ArrowDeviceArray must not be NULL", @@ -462,7 +463,7 @@ std::unique_ptr from_arrow_host_column(ArrowSchema const* schema, std::unique_ptr
from_arrow_host(ArrowSchema const* schema, ArrowDeviceArray const* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -472,7 +473,7 @@ std::unique_ptr
from_arrow_host(ArrowSchema const* schema, std::unique_ptr from_arrow_host_column(ArrowSchema const* schema, ArrowDeviceArray const* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -482,7 +483,7 @@ std::unique_ptr from_arrow_host_column(ArrowSchema const* schema, std::unique_ptr
from_arrow(ArrowSchema const* schema, ArrowArray const* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); @@ -497,7 +498,7 @@ std::unique_ptr
from_arrow(ArrowSchema const* schema, std::unique_ptr from_arrow_column(ArrowSchema const* schema, ArrowArray const* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); diff --git a/cpp/src/interop/from_arrow_stream.cu b/cpp/src/interop/from_arrow_stream.cu index 0c85b561944..578105aa90a 100644 --- a/cpp/src/interop/from_arrow_stream.cu +++ b/cpp/src/interop/from_arrow_stream.cu @@ -41,7 +41,7 @@ namespace { std::unique_ptr make_empty_column_from_schema(ArrowSchema const* schema, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { ArrowSchemaView schema_view; NANOARROW_THROW_NOT_OK(ArrowSchemaViewInit(&schema_view, schema, nullptr)); @@ -81,7 +81,7 @@ std::unique_ptr make_empty_column_from_schema(ArrowSchema const* schema, std::unique_ptr
from_arrow_stream(ArrowArrayStream* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_EXPECTS(input != nullptr, "input ArrowArrayStream must not be NULL", std::invalid_argument); @@ -135,7 +135,7 @@ std::unique_ptr
from_arrow_stream(ArrowArrayStream* input, std::unique_ptr
from_arrow_stream(ArrowArrayStream* input, rmm::cuda_stream_view stream, - rmm::mr::device_memory_resource* mr) + rmm::device_async_resource_ref mr) { CUDF_FUNC_RANGE(); return detail::from_arrow_stream(input, stream, mr); From dab6a447ca418073ec50c4e95aee5f0448fc95c2 Mon Sep 17 00:00:00 2001 From: Paul Taylor <178183+trxcllnt@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:30:24 -0700 Subject: [PATCH 17/42] Add environment-agnostic `ci/run_cudf_polars_pytest.sh` (#16178) Adds environment-agnostic `ci/run_cudf_polars_pytest.sh` script, similar to the scripts added in https://github.com/rapidsai/cudf/pull/14992. Authors: - Paul Taylor (https://github.com/trxcllnt) Approvers: - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cudf/pull/16178 --- ci/run_cudf_polars_pytests.sh | 11 +++++++++++ ci/test_cudf_polars.sh | 6 ++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100755 ci/run_cudf_polars_pytests.sh diff --git a/ci/run_cudf_polars_pytests.sh b/ci/run_cudf_polars_pytests.sh new file mode 100755 index 00000000000..78683b057a5 --- /dev/null +++ b/ci/run_cudf_polars_pytests.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Copyright (c) 2024, NVIDIA CORPORATION. + +set -euo pipefail + +# It is essential to cd into python/cudf_polars as `pytest-xdist` + `coverage` seem to work only at this directory level. + +# Support invoking run_cudf_polars_pytests.sh outside the script directory +cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../python/cudf_polars/ + +pytest --cache-clear "$@" tests diff --git a/ci/test_cudf_polars.sh b/ci/test_cudf_polars.sh index 95fb4b431bf..ca98c4dadb3 100755 --- a/ci/test_cudf_polars.sh +++ b/ci/test_cudf_polars.sh @@ -42,13 +42,11 @@ EXITCODE=0 trap set_exitcode ERR set +e -python -m pytest \ - --cache-clear \ +./ci/run_cudf_polars_pytests.sh \ --cov cudf_polars \ --cov-fail-under=100 \ --cov-config=python/cudf_polars/pyproject.toml \ - --junitxml="${RAPIDS_TESTS_DIR}/junit-cudf_polars.xml" \ - python/cudf_polars/tests + --junitxml="${RAPIDS_TESTS_DIR}/junit-cudf_polars.xml" trap ERR set -e From 769e94ffcebaabe33ddec4ab8f178f6d1c7545aa Mon Sep 17 00:00:00 2001 From: Paul Taylor <178183+trxcllnt@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:31:28 -0700 Subject: [PATCH 18/42] Make `test_python_cudf_pandas` generate `requirements.txt` (#16181) Authors: - Paul Taylor (https://github.com/trxcllnt) Approvers: - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cudf/pull/16181 --- dependencies.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies.yaml b/dependencies.yaml index e3f8a72e76c..6d4ba0c38d1 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -755,7 +755,7 @@ dependencies: - {matrix: null, packages: *cupy_packages_cu11} test_python_pandas_cudf: common: - - output_types: pyproject + - output_types: [requirements, pyproject] packages: # dependencies to run pandas tests # https://github.com/pandas-dev/pandas/blob/main/environment.yml @@ -766,7 +766,7 @@ dependencies: - pytest-reportlog test_python_cudf_pandas: common: - - output_types: pyproject + - output_types: [requirements, pyproject] packages: - ipython - openpyxl From aa4033c5fe0be9e3d235d5722f1030c60b04e34d Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 4 Jul 2024 10:10:02 +0100 Subject: [PATCH 19/42] Cast count aggs to correct dtype in translation (#16192) Polars default dtypes for some aggregations, particularly count, don't match ours, so insert casts. Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/16192 --- python/cudf_polars/cudf_polars/dsl/translate.py | 17 +++++++++++++---- python/cudf_polars/tests/test_groupby.py | 5 +---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index a2fdb3c3d79..0019b3aa98a 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -432,8 +432,11 @@ def _(node: pl_expr.Cast, visitor: NodeTraverser, dtype: plc.DataType) -> expr.E # Push casts into literals so we can handle Cast(Literal(Null)) if isinstance(inner, expr.Literal): return expr.Literal(dtype, inner.value.cast(plc.interop.to_arrow(dtype))) - else: - return expr.Cast(dtype, inner) + elif isinstance(inner, expr.Cast): + # Translation of Len/Count-agg put in a cast, remove double + # casts if we have one. + (inner,) = inner.children + return expr.Cast(dtype, inner) @_translate_expr.register @@ -443,12 +446,15 @@ def _(node: pl_expr.Column, visitor: NodeTraverser, dtype: plc.DataType) -> expr @_translate_expr.register def _(node: pl_expr.Agg, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: - return expr.Agg( + value = expr.Agg( dtype, node.name, node.options, *(translate_expr(visitor, n=n) for n in node.arguments), ) + if value.name == "count" and value.dtype.id() != plc.TypeId.INT32: + return expr.Cast(value.dtype, value) + return value @_translate_expr.register @@ -475,7 +481,10 @@ def _( @_translate_expr.register def _(node: pl_expr.Len, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: - return expr.Len(dtype) + value = expr.Len(dtype) + if dtype.id() != plc.TypeId.INT32: + return expr.Cast(dtype, value) + return value # pragma: no cover; never reached since polars len has uint32 dtype def translate_expr(visitor: NodeTraverser, *, n: int) -> expr.Expr: diff --git a/python/cudf_polars/tests/test_groupby.py b/python/cudf_polars/tests/test_groupby.py index aefad59eb91..8a6732b7063 100644 --- a/python/cudf_polars/tests/test_groupby.py +++ b/python/cudf_polars/tests/test_groupby.py @@ -83,10 +83,7 @@ def test_groupby(df: pl.LazyFrame, maintain_order, keys, exprs): def test_groupby_len(df, keys): q = df.group_by(*keys).agg(pl.len()) - # TODO: polars returns UInt32, libcudf returns Int32 - with pytest.raises(AssertionError): - assert_gpu_result_equal(q, check_row_order=False) - assert_gpu_result_equal(q, check_dtypes=False, check_row_order=False) + assert_gpu_result_equal(q, check_row_order=False) @pytest.mark.parametrize( From 5f57bc9034311f5461981644dec86c9c2e3434c7 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Thu, 4 Jul 2024 11:55:36 +0100 Subject: [PATCH 20/42] Some small fixes in cudf-polars (#16191) These catch a few more edge cases. Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/16191 --- python/cudf_polars/cudf_polars/callback.py | 13 +++++++++++-- .../cudf_polars/cudf_polars/containers/dataframe.py | 6 +++++- python/cudf_polars/cudf_polars/dsl/ir.py | 2 ++ python/cudf_polars/tests/test_union.py | 9 +++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/python/cudf_polars/cudf_polars/callback.py b/python/cudf_polars/cudf_polars/callback.py index 979087d5273..764cdd3b3ca 100644 --- a/python/cudf_polars/cudf_polars/callback.py +++ b/python/cudf_polars/cudf_polars/callback.py @@ -34,7 +34,12 @@ def _callback( return ir.evaluate(cache={}).to_polars() -def execute_with_cudf(nt: NodeTraverser, *, raise_on_fail: bool = False) -> None: +def execute_with_cudf( + nt: NodeTraverser, + *, + raise_on_fail: bool = False, + exception: type[Exception] | tuple[type[Exception], ...] = Exception, +) -> None: """ A post optimization callback that attempts to execute the plan with cudf. @@ -47,11 +52,15 @@ def execute_with_cudf(nt: NodeTraverser, *, raise_on_fail: bool = False) -> None Should conversion raise an exception rather than continuing without setting a callback. + exception + Optional exception, or tuple of exceptions, to catch during + translation. Defaults to ``Exception``. + The NodeTraverser is mutated if the libcudf executor can handle the plan. """ try: with nvtx.annotate(message="ConvertIR", domain="cudf_polars"): nt.set_udf(partial(_callback, translate_ir(nt))) - except NotImplementedError: + except exception: if raise_on_fail: raise diff --git a/python/cudf_polars/cudf_polars/containers/dataframe.py b/python/cudf_polars/cudf_polars/containers/dataframe.py index ec8d00c3123..d86656578d7 100644 --- a/python/cudf_polars/cudf_polars/containers/dataframe.py +++ b/python/cudf_polars/cudf_polars/containers/dataframe.py @@ -5,6 +5,7 @@ from __future__ import annotations +import itertools from functools import cached_property from typing import TYPE_CHECKING, cast @@ -160,7 +161,10 @@ def with_columns(self, columns: Sequence[NamedColumn]) -> Self: ----- If column names overlap, newer names replace older ones. """ - return type(self)([*self.columns, *columns]) + columns = list( + {c.name: c for c in itertools.chain(self.columns, columns)}.values() + ) + return type(self)(columns) def discard_columns(self, names: Set[str]) -> Self: """Drop columns by name.""" diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index 9b3096becd4..31a0be004ea 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -96,6 +96,8 @@ def broadcast( ``target_length`` is provided and not all columns are length-1 (i.e. ``n != 1``), then ``target_length`` must be equal to ``n``. """ + if len(columns) == 0: + return [] lengths: set[int] = {column.obj.size() for column in columns} if lengths == {1}: if target_length is None: diff --git a/python/cudf_polars/tests/test_union.py b/python/cudf_polars/tests/test_union.py index b021d832910..865b95a7d91 100644 --- a/python/cudf_polars/tests/test_union.py +++ b/python/cudf_polars/tests/test_union.py @@ -46,3 +46,12 @@ def test_concat_vertical(): q = pl.concat([ldf, ldf2], how="vertical") assert_gpu_result_equal(q) + + +def test_concat_diagonal_empty(): + df1 = pl.LazyFrame() + df2 = pl.LazyFrame({"a": [1, 2]}) + + q = pl.concat([df1, df2], how="diagonal_relaxed") + + assert_gpu_result_equal(q, collect_kwargs={"no_optimization": True}) From c1c62f1c02cf3929fb7536d67d14a24a9e2950ea Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 4 Jul 2024 04:31:06 -1000 Subject: [PATCH 21/42] Fix `memory_usage` when calculating nested list column (#16193) The offset column of a nested empty list column may be empty as discussed in https://github.com/rapidsai/cudf/issues/16164. `ListColumn.memory_usage` assumed that this column was non-empty Unblocks https://github.com/rapidsai/cuspatial/pull/1400 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/16193 --- python/cudf/cudf/core/column/lists.py | 11 ++++++++--- python/cudf/cudf/tests/test_list.py | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/python/cudf/cudf/core/column/lists.py b/python/cudf/cudf/core/column/lists.py index c548db67344..1992d471947 100644 --- a/python/cudf/cudf/core/column/lists.py +++ b/python/cudf/cudf/core/column/lists.py @@ -73,10 +73,15 @@ def memory_usage(self): child0_size = ( current_base_child.size + 1 - current_offset ) * current_base_child.base_children[0].dtype.itemsize - current_offset = current_base_child.base_children[ - 0 - ].element_indexing(current_offset) n += child0_size + current_offset_col = current_base_child.base_children[0] + if not len(current_offset_col): + # See https://github.com/rapidsai/cudf/issues/16164 why + # offset column can be uninitialized + break + current_offset = current_offset_col.element_indexing( + current_offset + ) current_base_child = current_base_child.base_children[1] n += ( diff --git a/python/cudf/cudf/tests/test_list.py b/python/cudf/cudf/tests/test_list.py index f76143cb381..ec9d7995b05 100644 --- a/python/cudf/cudf/tests/test_list.py +++ b/python/cudf/cudf/tests/test_list.py @@ -12,6 +12,7 @@ from cudf import NA from cudf._lib.copying import get_element from cudf.api.types import is_scalar +from cudf.core.column.column import column_empty from cudf.testing import assert_eq from cudf.testing._utils import DATETIME_TYPES, NUMERIC_TYPES, TIMEDELTA_TYPES @@ -926,3 +927,29 @@ def test_list_iterate_error(): def test_list_struct_list_memory_usage(): df = cudf.DataFrame({"a": [[{"b": [1]}]]}) assert df.memory_usage().sum() == 16 + + +def test_empty_nested_list_uninitialized_offsets_memory_usage(): + col = column_empty(0, cudf.ListDtype(cudf.ListDtype("int64"))) + nested_col = col.children[1] + empty_inner = type(nested_col)( + size=nested_col.size, + dtype=nested_col.dtype, + mask=nested_col.mask, + offset=nested_col.offset, + null_count=nested_col.null_count, + children=( + column_empty(0, nested_col.children[0].dtype), + nested_col.children[1], + ), + ) + col_empty_offset = type(col)( + size=col.size, + dtype=col.dtype, + mask=col.mask, + offset=col.offset, + null_count=col.null_count, + children=(column_empty(0, col.children[0].dtype), empty_inner), + ) + ser = cudf.Series._from_data({None: col_empty_offset}) + assert ser.memory_usage() == 8 From f3a1216bb9bac07667b05cef01fe007fe6dc52ce Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 4 Jul 2024 12:49:10 -0400 Subject: [PATCH 22/42] Migrate lists/modifying to pylibcudf (#16185) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) - Lawrence Mitchell (https://github.com/wence-) Approvers: - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/16185 --- .../_lib/pylibcudf/libcudf/lists/reverse.pxd | 14 ++++++++++ python/cudf/cudf/_lib/pylibcudf/lists.pxd | 2 ++ python/cudf/cudf/_lib/pylibcudf/lists.pyx | 26 +++++++++++++++++++ .../cudf/cudf/pylibcudf_tests/test_lists.py | 12 +++++++++ 4 files changed, 54 insertions(+) create mode 100644 python/cudf/cudf/_lib/pylibcudf/libcudf/lists/reverse.pxd diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/lists/reverse.pxd b/python/cudf/cudf/_lib/pylibcudf/libcudf/lists/reverse.pxd new file mode 100644 index 00000000000..0382a5d42c3 --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/lists/reverse.pxd @@ -0,0 +1,14 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp.memory cimport unique_ptr + +from cudf._lib.pylibcudf.libcudf.column.column cimport column +from cudf._lib.pylibcudf.libcudf.lists.lists_column_view cimport ( + lists_column_view, +) + + +cdef extern from "cudf/lists/reverse.hpp" namespace "cudf::lists" nogil: + cdef unique_ptr[column] reverse( + const lists_column_view& lists_column, + ) except + diff --git a/python/cudf/cudf/_lib/pylibcudf/lists.pxd b/python/cudf/cudf/_lib/pylibcudf/lists.pxd index 2ccf0139e90..c9d0a84e8ac 100644 --- a/python/cudf/cudf/_lib/pylibcudf/lists.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/lists.pxd @@ -23,3 +23,5 @@ cpdef Column contains(Column, ColumnOrScalar) cpdef Column contains_nulls(Column) cpdef Column index_of(Column, ColumnOrScalar, bool) + +cpdef Column reverse(Column) diff --git a/python/cudf/cudf/_lib/pylibcudf/lists.pyx b/python/cudf/cudf/_lib/pylibcudf/lists.pyx index a94d940accd..651f1346f88 100644 --- a/python/cudf/cudf/_lib/pylibcudf/lists.pyx +++ b/python/cudf/cudf/_lib/pylibcudf/lists.pyx @@ -9,6 +9,7 @@ from cudf._lib.pylibcudf.libcudf.column.column cimport column from cudf._lib.pylibcudf.libcudf.lists cimport ( contains as cpp_contains, explode as cpp_explode, + reverse as cpp_reverse, ) from cudf._lib.pylibcudf.libcudf.lists.combine cimport ( concatenate_list_elements as cpp_concatenate_list_elements, @@ -206,3 +207,28 @@ cpdef Column index_of(Column input, ColumnOrScalar search_key, bool find_first_o find_option, )) return Column.from_libcudf(move(c_result)) + + +cpdef Column reverse(Column input): + """Reverse the element order within each list of the input column. + + For details, see :cpp:func:`reverse`. + + Parameters + ---------- + input : Column + The input column. + + Returns + ------- + Column + A new Column with reversed lists. + """ + cdef unique_ptr[column] c_result + cdef ListColumnView list_view = input.list_view() + + with nogil: + c_result = move(cpp_reverse.reverse( + list_view.view(), + )) + return Column.from_libcudf(move(c_result)) diff --git a/python/cudf/cudf/pylibcudf_tests/test_lists.py b/python/cudf/cudf/pylibcudf_tests/test_lists.py index c781126e388..58a1dcf8d56 100644 --- a/python/cudf/cudf/pylibcudf_tests/test_lists.py +++ b/python/cudf/cudf/pylibcudf_tests/test_lists.py @@ -134,3 +134,15 @@ def test_index_of_list_column(test_data, column): expect = pa.array(column[1], type=pa.int32()) assert_column_eq(expect, res) + + +def test_reverse(test_data): + list_column = test_data[0][0] + arr = pa.array(list_column) + plc_column = plc.interop.from_arrow(arr) + + res = plc.lists.reverse(plc_column) + + expect = pa.array([lst[::-1] for lst in list_column]) + + assert_column_eq(expect, res) From ae422187743af5b9081028de7405b9ded73787b8 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 5 Jul 2024 12:27:50 +0100 Subject: [PATCH 23/42] Expose type traits to pylibcudf (#16197) Rather than recreating the classification, OAOO by using the libcudf definitions. Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16197 --- .../user_guide/api_docs/pylibcudf/index.rst | 7 +- .../user_guide/api_docs/pylibcudf/traits.rst | 6 + .../cudf/cudf/_lib/pylibcudf/CMakeLists.txt | 1 + python/cudf/cudf/_lib/pylibcudf/__init__.pxd | 3 + python/cudf/cudf/_lib/pylibcudf/__init__.py | 4 + .../pylibcudf/libcudf/utilities/traits.pxd | 27 ++++ python/cudf/cudf/_lib/pylibcudf/traits.pxd | 25 +++ python/cudf/cudf/_lib/pylibcudf/traits.pyx | 151 ++++++++++++++++++ .../cudf/cudf/pylibcudf_tests/common/utils.py | 39 ----- .../cudf/cudf/pylibcudf_tests/test_copying.py | 47 +++--- .../cudf/cudf/pylibcudf_tests/test_traits.py | 110 +++++++++++++ python/cudf_polars/cudf_polars/dsl/expr.py | 3 +- .../cudf_polars/cudf_polars/utils/dtypes.py | 13 -- 13 files changed, 361 insertions(+), 75 deletions(-) create mode 100644 docs/cudf/source/user_guide/api_docs/pylibcudf/traits.rst create mode 100644 python/cudf/cudf/_lib/pylibcudf/libcudf/utilities/traits.pxd create mode 100644 python/cudf/cudf/_lib/pylibcudf/traits.pxd create mode 100644 python/cudf/cudf/_lib/pylibcudf/traits.pyx create mode 100644 python/cudf/cudf/pylibcudf_tests/test_traits.py diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst index e9dad705cbf..bd6f0f77357 100644 --- a/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/index.rst @@ -18,22 +18,22 @@ This page provides API documentation for pylibcudf. filling gpumemoryview groupby - io/index.rst interop join lists merge quantiles reduce + replace reshape rolling round scalar search - stream_compaction sorting - replace + stream_compaction table + traits types unary @@ -41,4 +41,5 @@ This page provides API documentation for pylibcudf. :maxdepth: 2 :caption: Subpackages + io/index.rst strings/index.rst diff --git a/docs/cudf/source/user_guide/api_docs/pylibcudf/traits.rst b/docs/cudf/source/user_guide/api_docs/pylibcudf/traits.rst new file mode 100644 index 00000000000..294ca8dc78c --- /dev/null +++ b/docs/cudf/source/user_guide/api_docs/pylibcudf/traits.rst @@ -0,0 +1,6 @@ +====== +traits +====== + +.. automodule:: cudf._lib.pylibcudf.traits + :members: diff --git a/python/cudf/cudf/_lib/pylibcudf/CMakeLists.txt b/python/cudf/cudf/_lib/pylibcudf/CMakeLists.txt index 0a198f431a7..d22096081af 100644 --- a/python/cudf/cudf/_lib/pylibcudf/CMakeLists.txt +++ b/python/cudf/cudf/_lib/pylibcudf/CMakeLists.txt @@ -38,6 +38,7 @@ set(cython_sources stream_compaction.pyx sorting.pyx table.pyx + traits.pyx types.pyx unary.pyx utils.pyx diff --git a/python/cudf/cudf/_lib/pylibcudf/__init__.pxd b/python/cudf/cudf/_lib/pylibcudf/__init__.pxd index 5131df9a5cd..d4d615cde34 100644 --- a/python/cudf/cudf/_lib/pylibcudf/__init__.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/__init__.pxd @@ -23,6 +23,7 @@ from . cimport ( sorting, stream_compaction, strings, + traits, types, unary, ) @@ -54,12 +55,14 @@ __all__ = [ "quantiles", "reduce", "replace", + "reshape", "rolling", "round", "search", "stream_compaction", "strings", "sorting", + "traits", "types", "unary", ] diff --git a/python/cudf/cudf/_lib/pylibcudf/__init__.py b/python/cudf/cudf/_lib/pylibcudf/__init__.py index 43a9e2aca31..91f8acaf682 100644 --- a/python/cudf/cudf/_lib/pylibcudf/__init__.py +++ b/python/cudf/cudf/_lib/pylibcudf/__init__.py @@ -23,6 +23,7 @@ sorting, stream_compaction, strings, + traits, types, unary, ) @@ -35,6 +36,7 @@ __all__ = [ "Column", "DataType", + "MaskState", "Scalar", "Table", "TypeId", @@ -54,12 +56,14 @@ "quantiles", "reduce", "replace", + "reshape", "rolling", "round", "search", "stream_compaction", "strings", "sorting", + "traits", "types", "unary", ] diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/utilities/traits.pxd b/python/cudf/cudf/_lib/pylibcudf/libcudf/utilities/traits.pxd new file mode 100644 index 00000000000..0cc58af735b --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/utilities/traits.pxd @@ -0,0 +1,27 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp cimport bool +from libcpp.vector cimport vector + +from cudf._lib.pylibcudf.libcudf.types cimport data_type + + +cdef extern from "cudf/utilities/traits.hpp" namespace "cudf" nogil: + cdef bool is_relationally_comparable(data_type) + cdef bool is_equality_comparable(data_type) + cdef bool is_numeric(data_type) + cdef bool is_index_type(data_type) + cdef bool is_unsigned(data_type) + cdef bool is_integral(data_type) + cdef bool is_integral_not_bool(data_type) + cdef bool is_floating_point(data_type) + cdef bool is_boolean(data_type) + cdef bool is_timestamp(data_type) + cdef bool is_fixed_point(data_type) + cdef bool is_duration(data_type) + cdef bool is_chrono(data_type) + cdef bool is_dictionary(data_type) + cdef bool is_fixed_width(data_type) + cdef bool is_compound(data_type) + cdef bool is_nested(data_type) + cdef bool is_bit_castable(data_type, data_type) diff --git a/python/cudf/cudf/_lib/pylibcudf/traits.pxd b/python/cudf/cudf/_lib/pylibcudf/traits.pxd new file mode 100644 index 00000000000..668fa775202 --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/traits.pxd @@ -0,0 +1,25 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp cimport bool + +from .types cimport DataType + + +cpdef bool is_relationally_comparable(DataType typ) +cpdef bool is_equality_comparable(DataType typ) +cpdef bool is_numeric(DataType typ) +cpdef bool is_index_type(DataType typ) +cpdef bool is_unsigned(DataType typ) +cpdef bool is_integral(DataType typ) +cpdef bool is_integral_not_bool(DataType typ) +cpdef bool is_floating_point(DataType typ) +cpdef bool is_boolean(DataType typ) +cpdef bool is_timestamp(DataType typ) +cpdef bool is_fixed_point(DataType typ) +cpdef bool is_duration(DataType typ) +cpdef bool is_chrono(DataType typ) +cpdef bool is_dictionary(DataType typ) +cpdef bool is_fixed_width(DataType typ) +cpdef bool is_compound(DataType typ) +cpdef bool is_nested(DataType typ) +cpdef bool is_bit_castable(DataType source, DataType target) diff --git a/python/cudf/cudf/_lib/pylibcudf/traits.pyx b/python/cudf/cudf/_lib/pylibcudf/traits.pyx new file mode 100644 index 00000000000..d2370f8d641 --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/traits.pyx @@ -0,0 +1,151 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from libcpp cimport bool + +from cudf._lib.pylibcudf.libcudf.utilities cimport traits + +from .types cimport DataType + + +cpdef bool is_relationally_comparable(DataType typ): + """Checks if the given data type supports relational comparisons. + + For details, see :cpp:func:`is_relationally_comparable`. + """ + return traits.is_relationally_comparable(typ.c_obj) + + +cpdef bool is_equality_comparable(DataType typ): + """Checks if the given data type supports equality comparisons. + + For details, see :cpp:func:`is_equality_comparable`. + """ + return traits.is_equality_comparable(typ.c_obj) + + +cpdef bool is_numeric(DataType typ): + """Checks if the given data type is numeric. + + For details, see :cpp:func:`is_numeric`. + """ + return traits.is_numeric(typ.c_obj) + + +cpdef bool is_index_type(DataType typ): + """Checks if the given data type is an index type. + + For details, see :cpp:func:`is_index_type`. + """ + return traits.is_index_type(typ.c_obj) + + +cpdef bool is_unsigned(DataType typ): + """Checks if the given data type is an unsigned type. + + For details, see :cpp:func:`is_unsigned`. + """ + return traits.is_unsigned(typ.c_obj) + + +cpdef bool is_integral(DataType typ): + """Checks if the given data type is an integral type. + + For details, see :cpp:func:`is_integral`. + """ + return traits.is_integral(typ.c_obj) + + +cpdef bool is_integral_not_bool(DataType typ): + """Checks if the given data type is an integral type excluding booleans. + + For details, see :cpp:func:`is_integral_not_bool`. + """ + return traits.is_integral_not_bool(typ.c_obj) + + +cpdef bool is_floating_point(DataType typ): + """Checks if the given data type is a floating point type. + + For details, see :cpp:func:`is_floating_point`. + """ + return traits.is_floating_point(typ.c_obj) + + +cpdef bool is_boolean(DataType typ): + """Checks if the given data type is a boolean type. + + For details, see :cpp:func:`is_boolean`. + """ + return traits.is_boolean(typ.c_obj) + + +cpdef bool is_timestamp(DataType typ): + """Checks if the given data type is a timestamp type. + + For details, see :cpp:func:`is_timestamp`. + """ + return traits.is_timestamp(typ.c_obj) + + +cpdef bool is_fixed_point(DataType typ): + """Checks if the given data type is a fixed point type. + + For details, see :cpp:func:`is_fixed_point`. + """ + return traits.is_fixed_point(typ.c_obj) + + +cpdef bool is_duration(DataType typ): + """Checks if the given data type is a duration type. + + For details, see :cpp:func:`is_duration`. + """ + return traits.is_duration(typ.c_obj) + + +cpdef bool is_chrono(DataType typ): + """Checks if the given data type is a chrono type. + + For details, see :cpp:func:`is_chrono`. + """ + return traits.is_chrono(typ.c_obj) + + +cpdef bool is_dictionary(DataType typ): + """Checks if the given data type is a dictionary type. + + For details, see :cpp:func:`is_dictionary`. + """ + return traits.is_dictionary(typ.c_obj) + + +cpdef bool is_fixed_width(DataType typ): + """Checks if the given data type is a fixed width type. + + For details, see :cpp:func:`is_fixed_width`. + """ + return traits.is_fixed_width(typ.c_obj) + + +cpdef bool is_compound(DataType typ): + """Checks if the given data type is a compound type. + + For details, see :cpp:func:`is_compound`. + """ + return traits.is_compound(typ.c_obj) + + +cpdef bool is_nested(DataType typ): + """Checks if the given data type is a nested type. + + For details, see :cpp:func:`is_nested`. + """ + return traits.is_nested(typ.c_obj) + + +cpdef bool is_bit_castable(DataType source, DataType target): + """Checks if the source type is bit-castable to the target type. + + For details, see :cpp:func:`is_bit_castable`. + """ + return traits.is_bit_castable(source.c_obj, target.c_obj) diff --git a/python/cudf/cudf/pylibcudf_tests/common/utils.py b/python/cudf/cudf/pylibcudf_tests/common/utils.py index f8bfe340ae5..d41e6c720bf 100644 --- a/python/cudf/cudf/pylibcudf_tests/common/utils.py +++ b/python/cudf/cudf/pylibcudf_tests/common/utils.py @@ -102,49 +102,10 @@ def cudf_raises(expected_exception: BaseException, *args, **kwargs): return pytest.raises(expected_exception, *args, **kwargs) -# TODO: Consider moving these type utilities into pylibcudf.types itself. -def is_signed_integer(plc_dtype: plc.DataType): - return ( - plc.TypeId.INT8.value <= plc_dtype.id().value <= plc.TypeId.INT64.value - ) - - -def is_integer(plc_dtype: plc.DataType): - return plc_dtype.id() in ( - plc.TypeId.INT8, - plc.TypeId.INT16, - plc.TypeId.INT32, - plc.TypeId.INT64, - plc.TypeId.UINT8, - plc.TypeId.UINT16, - plc.TypeId.UINT32, - plc.TypeId.UINT64, - ) - - -def is_floating(plc_dtype: plc.DataType): - return plc_dtype.id() in ( - plc.TypeId.FLOAT32, - plc.TypeId.FLOAT64, - ) - - -def is_boolean(plc_dtype: plc.DataType): - return plc_dtype.id() == plc.TypeId.BOOL8 - - def is_string(plc_dtype: plc.DataType): return plc_dtype.id() == plc.TypeId.STRING -def is_fixed_width(plc_dtype: plc.DataType): - return ( - is_integer(plc_dtype) - or is_floating(plc_dtype) - or is_boolean(plc_dtype) - ) - - def nesting_level(typ) -> tuple[int, int]: """Return list and struct nesting of a pyarrow type.""" if isinstance(typ, pa.ListType): diff --git a/python/cudf/cudf/pylibcudf_tests/test_copying.py b/python/cudf/cudf/pylibcudf_tests/test_copying.py index 0a6df198d46..f27fe4e942e 100644 --- a/python/cudf/cudf/pylibcudf_tests/test_copying.py +++ b/python/cudf/cudf/pylibcudf_tests/test_copying.py @@ -9,9 +9,6 @@ assert_column_eq, assert_table_eq, cudf_raises, - is_fixed_width, - is_floating, - is_integer, is_nested_list, is_nested_struct, is_string, @@ -359,9 +356,9 @@ def test_scatter_table_type_mismatch(source_table, index_column, target_table): _, plc_index_column = index_column _, plc_target_table = target_table with cudf_raises(TypeError): - if is_integer( + if plc.traits.is_integral_not_bool( dtype := plc_target_table.columns()[0].type() - ) or is_floating(dtype): + ) or plc.traits.is_floating_point(dtype): pa_array = pa.array([True] * plc_source_table.num_rows()) else: pa_array = pa.array([1] * plc_source_table.num_rows()) @@ -428,9 +425,9 @@ def test_scatter_scalars_type_mismatch(index_column, target_table): _, plc_index_column = index_column _, plc_target_table = target_table with cudf_raises(TypeError): - if is_integer( + if plc.traits.is_integral_not_bool( dtype := plc_target_table.columns()[0].type() - ) or is_floating(dtype): + ) or plc.traits.is_floating_point(dtype): plc_source_scalar = [plc.interop.from_arrow(pa.scalar(True))] else: plc_source_scalar = [plc.interop.from_arrow(pa.scalar(1))] @@ -458,7 +455,7 @@ def test_empty_like_table(source_table): @pytest.mark.parametrize("size", [None, 10]) def test_allocate_like(input_column, size): _, plc_input_column = input_column - if is_fixed_width(plc_input_column.type()): + if plc.traits.is_fixed_width(plc_input_column.type()): result = plc.copying.allocate_like( plc_input_column, plc.copying.MaskAllocationPolicy.RETAIN, @@ -484,7 +481,7 @@ def test_copy_range_in_place( pa_target_column, _ = target_column - if not is_fixed_width(mutable_target_column.type()): + if not plc.traits.is_fixed_width(mutable_target_column.type()): with pytest.raises(TypeError): plc.copying.copy_range_in_place( plc_input_column, @@ -516,7 +513,7 @@ def test_copy_range_in_place_out_of_bounds( ): _, plc_input_column = input_column - if is_fixed_width(mutable_target_column.type()): + if plc.traits.is_fixed_width(mutable_target_column.type()): with cudf_raises(IndexError): plc.copying.copy_range_in_place( plc_input_column, @@ -528,7 +525,9 @@ def test_copy_range_in_place_out_of_bounds( def test_copy_range_in_place_different_types(mutable_target_column): - if is_integer(dtype := mutable_target_column.type()) or is_floating(dtype): + if plc.traits.is_integral_not_bool( + dtype := mutable_target_column.type() + ) or plc.traits.is_floating_point(dtype): plc_input_column = plc.interop.from_arrow(pa.array(["a", "b", "c"])) else: plc_input_column = plc.interop.from_arrow(pa.array([1, 2, 3])) @@ -548,7 +547,7 @@ def test_copy_range_in_place_null_mismatch( ): pa_input_column, _ = input_column - if is_fixed_width(mutable_target_column.type()): + if plc.traits.is_fixed_width(mutable_target_column.type()): pa_input_column = pc.if_else( _pyarrow_index_to_mask([0], len(pa_input_column)), pa_input_column, @@ -568,7 +567,9 @@ def test_copy_range_in_place_null_mismatch( def test_copy_range(input_column, target_column): pa_input_column, plc_input_column = input_column pa_target_column, plc_target_column = target_column - if is_fixed_width(dtype := plc_target_column.type()) or is_string(dtype): + if plc.traits.is_fixed_width( + dtype := plc_target_column.type() + ) or is_string(dtype): result = plc.copying.copy_range( plc_input_column, plc_target_column, @@ -610,7 +611,9 @@ def test_copy_range_out_of_bounds(input_column, target_column): def test_copy_range_different_types(target_column): _, plc_target_column = target_column - if is_integer(dtype := plc_target_column.type()) or is_floating(dtype): + if plc.traits.is_integral_not_bool( + dtype := plc_target_column.type() + ) or plc.traits.is_floating_point(dtype): plc_input_column = plc.interop.from_arrow(pa.array(["a", "b", "c"])) else: plc_input_column = plc.interop.from_arrow(pa.array([1, 2, 3])) @@ -629,7 +632,9 @@ def test_shift(target_column, source_scalar): pa_source_scalar, plc_source_scalar = source_scalar pa_target_column, plc_target_column = target_column shift = 2 - if is_fixed_width(dtype := plc_target_column.type()) or is_string(dtype): + if plc.traits.is_fixed_width( + dtype := plc_target_column.type() + ) or is_string(dtype): result = plc.copying.shift(plc_target_column, shift, plc_source_scalar) expected = pa.concat_arrays( [pa.array([pa_source_scalar] * shift), pa_target_column[:-shift]] @@ -642,7 +647,9 @@ def test_shift(target_column, source_scalar): def test_shift_type_mismatch(target_column): _, plc_target_column = target_column - if is_integer(dtype := plc_target_column.type()) or is_floating(dtype): + if plc.traits.is_integral_not_bool( + dtype := plc_target_column.type() + ) or plc.traits.is_floating_point(dtype): fill_value = plc.interop.from_arrow(pa.scalar("a")) else: fill_value = plc.interop.from_arrow(pa.scalar(1)) @@ -747,7 +754,9 @@ def test_copy_if_else_column_column(target_column, mask, source_scalar): def test_copy_if_else_wrong_type(target_column, mask): _, plc_target_column = target_column _, plc_mask = mask - if is_integer(dtype := plc_target_column.type()) or is_floating(dtype): + if plc.traits.is_integral_not_bool( + dtype := plc_target_column.type() + ) or plc.traits.is_floating_point(dtype): plc_input_column = plc.interop.from_arrow( pa.array(["a"] * plc_target_column.size()) ) @@ -951,9 +960,9 @@ def test_boolean_mask_scatter_from_wrong_num_true(source_table, target_table): def test_boolean_mask_scatter_from_wrong_col_type(target_table, mask): _, plc_target_table = target_table _, plc_mask = mask - if is_integer( + if plc.traits.is_integral_not_bool( dtype := plc_target_table.columns()[0].type() - ) or is_floating(dtype): + ) or plc.traits.is_floating_point(dtype): input_column = plc.interop.from_arrow(pa.array(["a", "b", "c"])) else: input_column = plc.interop.from_arrow(pa.array([1, 2, 3])) diff --git a/python/cudf/cudf/pylibcudf_tests/test_traits.py b/python/cudf/cudf/pylibcudf_tests/test_traits.py new file mode 100644 index 00000000000..6c22cb02f21 --- /dev/null +++ b/python/cudf/cudf/pylibcudf_tests/test_traits.py @@ -0,0 +1,110 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +from cudf._lib import pylibcudf as plc + + +def test_is_relationally_comparable(): + assert plc.traits.is_relationally_comparable(plc.DataType(plc.TypeId.INT8)) + assert not plc.traits.is_relationally_comparable( + plc.DataType(plc.TypeId.LIST) + ) + + +def test_is_equality_comparable(): + assert plc.traits.is_equality_comparable(plc.DataType(plc.TypeId.INT8)) + assert not plc.traits.is_equality_comparable(plc.DataType(plc.TypeId.LIST)) + + +def test_is_numeric(): + assert plc.traits.is_numeric(plc.DataType(plc.TypeId.FLOAT64)) + assert not plc.traits.is_numeric(plc.DataType(plc.TypeId.LIST)) + + +def test_is_index_type(): + assert plc.traits.is_index_type(plc.DataType(plc.TypeId.INT8)) + assert not plc.traits.is_index_type(plc.DataType(plc.TypeId.BOOL8)) + + +def test_is_unsigned(): + assert plc.traits.is_unsigned(plc.DataType(plc.TypeId.UINT8)) + assert not plc.traits.is_unsigned(plc.DataType(plc.TypeId.INT8)) + + +def test_is_integral(): + assert plc.traits.is_integral(plc.DataType(plc.TypeId.BOOL8)) + assert not plc.traits.is_integral(plc.DataType(plc.TypeId.DECIMAL32)) + + +def test_is_integral_not_bool(): + assert plc.traits.is_integral_not_bool(plc.DataType(plc.TypeId.INT8)) + assert not plc.traits.is_integral_not_bool(plc.DataType(plc.TypeId.BOOL8)) + + +def test_is_floating_point(): + assert plc.traits.is_floating_point(plc.DataType(plc.TypeId.FLOAT64)) + assert not plc.traits.is_floating_point(plc.DataType(plc.TypeId.UINT8)) + + +def test_is_boolean(): + assert plc.traits.is_boolean(plc.DataType(plc.TypeId.BOOL8)) + assert not plc.traits.is_boolean(plc.DataType(plc.TypeId.UINT8)) + + +def test_is_timestamp(): + assert plc.traits.is_timestamp( + plc.DataType(plc.TypeId.TIMESTAMP_MICROSECONDS) + ) + assert not plc.traits.is_timestamp( + plc.DataType(plc.TypeId.DURATION_MICROSECONDS) + ) + + +def test_is_fixed_point(): + assert plc.traits.is_fixed_point(plc.DataType(plc.TypeId.DECIMAL128)) + assert not plc.traits.is_fixed_point(plc.DataType(plc.TypeId.FLOAT32)) + + +def test_is_duration(): + assert plc.traits.is_duration( + plc.DataType(plc.TypeId.DURATION_MICROSECONDS) + ) + assert not plc.traits.is_duration( + plc.DataType(plc.TypeId.TIMESTAMP_MICROSECONDS) + ) + + +def test_is_chrono(): + assert plc.traits.is_chrono(plc.DataType(plc.TypeId.DURATION_MICROSECONDS)) + assert plc.traits.is_chrono( + plc.DataType(plc.TypeId.TIMESTAMP_MICROSECONDS) + ) + assert not plc.traits.is_chrono(plc.DataType(plc.TypeId.UINT8)) + + +def test_is_dictionary(): + assert plc.traits.is_dictionary(plc.DataType(plc.TypeId.DICTIONARY32)) + assert not plc.traits.is_dictionary(plc.DataType(plc.TypeId.UINT8)) + + +def test_is_fixed_width(): + assert plc.traits.is_fixed_width(plc.DataType(plc.TypeId.INT8)) + assert not plc.traits.is_fixed_width(plc.DataType(plc.TypeId.STRING)) + + +def test_is_compound(): + assert plc.traits.is_compound(plc.DataType(plc.TypeId.STRUCT)) + assert not plc.traits.is_compound(plc.DataType(plc.TypeId.UINT8)) + + +def test_is_nested(): + assert plc.traits.is_nested(plc.DataType(plc.TypeId.STRUCT)) + assert not plc.traits.is_nested(plc.DataType(plc.TypeId.STRING)) + + +def test_is_bit_castable(): + assert plc.traits.is_bit_castable( + plc.DataType(plc.TypeId.INT8), plc.DataType(plc.TypeId.UINT8) + ) + assert not plc.traits.is_bit_castable( + plc.DataType(plc.TypeId.UINT8), plc.DataType(plc.TypeId.UINT16) + ) diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index cfc2947f8de..69bc85b109d 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -1218,7 +1218,8 @@ def __init__( self.children = (left, right) if ( op in (plc.binaryop.BinaryOperator.ADD, plc.binaryop.BinaryOperator.SUB) - and ({left.dtype.id(), right.dtype.id()}.issubset(dtypes.TIMELIKE_TYPES)) + and plc.traits.is_chrono(left.dtype) + and plc.traits.is_chrono(right.dtype) and not dtypes.have_compatible_resolution(left.dtype.id(), right.dtype.id()) ): raise NotImplementedError("Casting rules for timelike types") diff --git a/python/cudf_polars/cudf_polars/utils/dtypes.py b/python/cudf_polars/cudf_polars/utils/dtypes.py index 507acb5d33a..918cd024fa2 100644 --- a/python/cudf_polars/cudf_polars/utils/dtypes.py +++ b/python/cudf_polars/cudf_polars/utils/dtypes.py @@ -17,19 +17,6 @@ __all__ = ["from_polars", "downcast_arrow_lists", "have_compatible_resolution"] -TIMELIKE_TYPES: frozenset[plc.TypeId] = frozenset( - [ - plc.TypeId.TIMESTAMP_MILLISECONDS, - plc.TypeId.TIMESTAMP_MICROSECONDS, - plc.TypeId.TIMESTAMP_NANOSECONDS, - plc.TypeId.TIMESTAMP_DAYS, - plc.TypeId.DURATION_MILLISECONDS, - plc.TypeId.DURATION_MICROSECONDS, - plc.TypeId.DURATION_NANOSECONDS, - ] -) - - def have_compatible_resolution(lid: plc.TypeId, rid: plc.TypeId): """ Do two datetime typeids have matching resolution for a binop. From 37defc6b943094921200146c5f6042a91e68c75a Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:44:05 -0400 Subject: [PATCH 24/42] Use strings concatenate to support large strings in CSV writer (#16148) Changes the CSV writer logic to use `cudf::strings::concatenate` instead of `cudf::strings::join_strings` when output size exceeds `join_strings` limit. Closes #16137 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Nghia Truong (https://github.com/ttnghia) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/16148 --- cpp/src/io/csv/writer_impl.cu | 38 ++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/cpp/src/io/csv/writer_impl.cu b/cpp/src/io/csv/writer_impl.cu index 7c4d5711281..63eb0b03c5f 100644 --- a/cpp/src/io/csv/writer_impl.cu +++ b/cpp/src/io/csv/writer_impl.cu @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -372,15 +373,33 @@ void write_chunked(data_sink* out_sink, CUDF_EXPECTS(str_column_view.size() > 0, "Unexpected empty strings column."); cudf::string_scalar newline{options.get_line_terminator(), true, stream}; - auto p_str_col_w_nl = cudf::strings::detail::join_strings(str_column_view, - newline, - string_scalar{"", false, stream}, - stream, - rmm::mr::get_current_device_resource()); - strings_column_view strings_column{p_str_col_w_nl->view()}; - auto total_num_bytes = strings_column.chars_size(stream); - char const* ptr_all_bytes = strings_column.chars_begin(stream); + // use strings concatenate to build the final CSV output in device memory + auto contents_w_nl = [&] { + auto const total_size = + str_column_view.chars_size(stream) + (newline.size() * str_column_view.size()); + auto const empty_str = string_scalar("", true, stream); + // use join_strings when the output will be less than 2GB + if (total_size < static_cast(std::numeric_limits::max())) { + return cudf::strings::detail::join_strings(str_column_view, newline, empty_str, stream, mr) + ->release(); + } + auto nl_col = cudf::make_column_from_scalar(newline, str_column_view.size(), stream); + // convert the last element into an empty string by resetting the last offset value + auto& offsets = nl_col->child(strings_column_view::offsets_column_index); + auto offsets_view = offsets.mutable_view(); + cudf::fill_in_place(offsets_view, + offsets.size() - 1, // set the last element with + offsets.size(), // the value from 2nd to last element + *cudf::detail::get_element(offsets.view(), offsets.size() - 2, stream, mr), + stream); + auto const nl_tbl = cudf::table_view({str_column_view.parent(), nl_col->view()}); + return cudf::strings::detail::concatenate( + nl_tbl, empty_str, empty_str, strings::separator_on_nulls::NO, stream, mr) + ->release(); + }(); + auto const total_num_bytes = contents_w_nl.data->size(); + auto const ptr_all_bytes = static_cast(contents_w_nl.data->data()); if (out_sink->is_device_write_preferred(total_num_bytes)) { // Direct write from device memory @@ -491,7 +510,8 @@ void write_csv(data_sink* out_sink, str_table_view.column(0), options_narep, stream, rmm::mr::get_current_device_resource()); }(); - write_chunked(out_sink, str_concat_col->view(), options, stream, mr); + write_chunked( + out_sink, str_concat_col->view(), options, stream, rmm::mr::get_current_device_resource()); } } } From 7dd69452bb72ca8cc440af52cb6ca8386950c264 Mon Sep 17 00:00:00 2001 From: Thomas Li <47963215+lithomas1@users.noreply.github.com> Date: Fri, 5 Jul 2024 07:58:30 -0700 Subject: [PATCH 25/42] CI: Build wheels for cudf-polars (#16156) Authors: - Thomas Li (https://github.com/lithomas1) - Lawrence Mitchell (https://github.com/wence-) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16156 --- .github/workflows/pr.yaml | 16 +++++++++++++--- ci/build_wheel_cudf_polars.sh | 11 +++++++++++ ci/run_cudf_polars_pytests.sh | 2 +- ...df_polars.sh => test_wheel_cudf_polars.sh} | 19 +++++++------------ ci/test_wheel_dask_cudf.sh | 2 +- python/cudf_polars/pyproject.toml | 2 -- 6 files changed, 33 insertions(+), 19 deletions(-) create mode 100755 ci/build_wheel_cudf_polars.sh rename ci/{test_cudf_polars.sh => test_wheel_cudf_polars.sh} (70%) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index a35802f2ab0..ceee9074b93 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -25,7 +25,8 @@ jobs: - docs-build - wheel-build-cudf - wheel-tests-cudf - - test-cudf-polars + - wheel-build-cudf-polars + - wheel-tests-cudf-polars - wheel-build-dask-cudf - wheel-tests-dask-cudf - devcontainer @@ -133,9 +134,18 @@ jobs: with: build_type: pull-request script: ci/test_wheel_cudf.sh - test-cudf-polars: + wheel-build-cudf-polars: needs: wheel-build-cudf secrets: inherit + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.08 + with: + # This selects "ARCH=amd64 + the latest supported Python + CUDA". + matrix_filter: map(select(.ARCH == "amd64")) | group_by(.CUDA_VER|split(".")|map(tonumber)|.[0]) | map(max_by([(.PY_VER|split(".")|map(tonumber)), (.CUDA_VER|split(".")|map(tonumber))])) + build_type: pull-request + script: "ci/build_wheel_cudf_polars.sh" + wheel-tests-cudf-polars: + needs: wheel-build-cudf-polars + secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.08 with: # This selects "ARCH=amd64 + the latest supported Python + CUDA". @@ -143,7 +153,7 @@ jobs: build_type: pull-request # This always runs, but only fails if this PR touches code in # pylibcudf or cudf_polars - script: "ci/test_cudf_polars.sh" + script: "ci/test_wheel_cudf_polars.sh" wheel-build-dask-cudf: needs: wheel-build-cudf secrets: inherit diff --git a/ci/build_wheel_cudf_polars.sh b/ci/build_wheel_cudf_polars.sh new file mode 100755 index 00000000000..9c945e11c00 --- /dev/null +++ b/ci/build_wheel_cudf_polars.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Copyright (c) 2023-2024, NVIDIA CORPORATION. + +set -euo pipefail + +package_dir="python/cudf_polars" + +./ci/build_wheel.sh ${package_dir} + +RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" +RAPIDS_PY_WHEEL_NAME="cudf_polars_${RAPIDS_PY_CUDA_SUFFIX}" RAPIDS_PY_WHEEL_PURE="1" rapids-upload-wheels-to-s3 ${package_dir}/dist diff --git a/ci/run_cudf_polars_pytests.sh b/ci/run_cudf_polars_pytests.sh index 78683b057a5..c10612a065a 100755 --- a/ci/run_cudf_polars_pytests.sh +++ b/ci/run_cudf_polars_pytests.sh @@ -8,4 +8,4 @@ set -euo pipefail # Support invoking run_cudf_polars_pytests.sh outside the script directory cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../python/cudf_polars/ -pytest --cache-clear "$@" tests +python -m pytest --cache-clear "$@" tests diff --git a/ci/test_cudf_polars.sh b/ci/test_wheel_cudf_polars.sh similarity index 70% rename from ci/test_cudf_polars.sh rename to ci/test_wheel_cudf_polars.sh index ca98c4dadb3..900acd5d473 100755 --- a/ci/test_cudf_polars.sh +++ b/ci/test_wheel_cudf_polars.sh @@ -18,19 +18,14 @@ else fi RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})" -RAPIDS_PY_WHEEL_NAME="cudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 ./dist +RAPIDS_PY_WHEEL_NAME="cudf_polars_${RAPIDS_PY_CUDA_SUFFIX}" RAPIDS_PY_WHEEL_PURE="1" rapids-download-wheels-from-s3 ./dist -RESULTS_DIR=${RAPIDS_TESTS_DIR:-"$(mktemp -d)"} -RAPIDS_TESTS_DIR=${RAPIDS_TESTS_DIR:-"${RESULTS_DIR}/test-results"}/ -mkdir -p "${RAPIDS_TESTS_DIR}" - -rapids-logger "Install cudf wheel" -# echo to expand wildcard before adding `[extra]` requires for pip -python -m pip install $(echo ./dist/cudf*.whl)[test] +# Download the cudf built in the previous step +RAPIDS_PY_WHEEL_NAME="cudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 ./local-cudf-dep +python -m pip install ./local-cudf-dep/cudf*.whl rapids-logger "Install cudf_polars" -python -m pip install 'polars>=1.0' -python -m pip install --no-deps python/cudf_polars +python -m pip install $(echo ./dist/cudf_polars*.whl)[test] rapids-logger "Run cudf_polars tests" @@ -45,8 +40,8 @@ set +e ./ci/run_cudf_polars_pytests.sh \ --cov cudf_polars \ --cov-fail-under=100 \ - --cov-config=python/cudf_polars/pyproject.toml \ - --junitxml="${RAPIDS_TESTS_DIR}/junit-cudf_polars.xml" + --cov-config=./pyproject.toml \ + --junitxml="${RAPIDS_TESTS_DIR}/junit-cudf-polars.xml" trap ERR set -e diff --git a/ci/test_wheel_dask_cudf.sh b/ci/test_wheel_dask_cudf.sh index 2b20b9d9ce4..c3800d3cc25 100755 --- a/ci/test_wheel_dask_cudf.sh +++ b/ci/test_wheel_dask_cudf.sh @@ -8,7 +8,7 @@ RAPIDS_PY_WHEEL_NAME="dask_cudf_${RAPIDS_PY_CUDA_SUFFIX}" RAPIDS_PY_WHEEL_PURE=" # Download the cudf built in the previous step RAPIDS_PY_WHEEL_NAME="cudf_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 ./local-cudf-dep -python -m pip install --no-deps ./local-cudf-dep/cudf*.whl +python -m pip install ./local-cudf-dep/cudf*.whl # echo to expand wildcard before adding `[extra]` requires for pip python -m pip install $(echo ./dist/dask_cudf*.whl)[test] diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index bf4673fcc50..0b559f7a8e9 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -182,5 +182,3 @@ docstring-code-format = true [tool.rapids-build-backend] build-backend = "setuptools.build_meta" dependencies-file = "../../dependencies.yaml" -# Pure python -disable-cuda = true From c978181a3a721ed75cf016c6f083648c65bd24cd Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Fri, 5 Jul 2024 16:11:07 +0100 Subject: [PATCH 26/42] Implement translation for some unary functions and a single datetime extraction (#16173) - Closes #16169 Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - Thomas Li (https://github.com/lithomas1) URL: https://github.com/rapidsai/cudf/pull/16173 --- python/cudf_polars/cudf_polars/dsl/expr.py | 124 ++++++++++++++++++ python/cudf_polars/cudf_polars/dsl/ir.py | 2 +- .../cudf_polars/cudf_polars/dsl/translate.py | 19 ++- .../tests/expressions/test_datetime_basic.py | 28 ++++ .../tests/expressions/test_round.py | 32 +++++ .../tests/expressions/test_unique.py | 24 ++++ python/cudf_polars/tests/test_groupby.py | 2 + 7 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 python/cudf_polars/tests/expressions/test_round.py create mode 100644 python/cudf_polars/tests/expressions/test_unique.py diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index 69bc85b109d..93cb9db7cbd 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -44,6 +44,7 @@ "Col", "BooleanFunction", "StringFunction", + "TemporalFunction", "Sort", "SortBy", "Gather", @@ -815,6 +816,129 @@ def do_evaluate( ) # pragma: no cover; handled by init raising +class TemporalFunction(Expr): + __slots__ = ("name", "options", "children") + _non_child = ("dtype", "name", "options") + children: tuple[Expr, ...] + + def __init__( + self, + dtype: plc.DataType, + name: pl_expr.TemporalFunction, + options: tuple[Any, ...], + *children: Expr, + ) -> None: + super().__init__(dtype) + self.options = options + self.name = name + self.children = children + if self.name != pl_expr.TemporalFunction.Year: + raise NotImplementedError(f"String function {self.name}") + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + columns = [ + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ] + if self.name == pl_expr.TemporalFunction.Year: + (column,) = columns + return Column(plc.datetime.extract_year(column.obj)) + raise NotImplementedError( + f"TemporalFunction {self.name}" + ) # pragma: no cover; init trips first + + +class UnaryFunction(Expr): + __slots__ = ("name", "options", "children") + _non_child = ("dtype", "name", "options") + children: tuple[Expr, ...] + + def __init__( + self, dtype: plc.DataType, name: str, options: tuple[Any, ...], *children: Expr + ) -> None: + super().__init__(dtype) + self.name = name + self.options = options + self.children = children + if self.name not in ("round", "unique"): + raise NotImplementedError(f"Unary function {name=}") + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + if self.name == "round": + (decimal_places,) = self.options + (values,) = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + return Column( + plc.round.round( + values.obj, decimal_places, plc.round.RoundingMethod.HALF_UP + ) + ).sorted_like(values) + elif self.name == "unique": + (maintain_order,) = self.options + (values,) = ( + child.evaluate(df, context=context, mapping=mapping) + for child in self.children + ) + # Only one column, so keep_any is the same as keep_first + # for stable distinct + keep = plc.stream_compaction.DuplicateKeepOption.KEEP_ANY + if values.is_sorted: + maintain_order = True + result = plc.stream_compaction.unique( + plc.Table([values.obj]), + [0], + keep, + plc.types.NullEquality.EQUAL, + ) + else: + distinct = ( + plc.stream_compaction.stable_distinct + if maintain_order + else plc.stream_compaction.distinct + ) + result = distinct( + plc.Table([values.obj]), + [0], + keep, + plc.types.NullEquality.EQUAL, + plc.types.NanEquality.ALL_EQUAL, + ) + (column,) = result.columns() + if maintain_order: + return Column(column).sorted_like(values) + return Column(column) + raise NotImplementedError( + f"Unimplemented unary function {self.name=}" + ) # pragma: no cover; init trips first + + def collect_agg(self, *, depth: int) -> AggInfo: + """Collect information about aggregations in groupbys.""" + if depth == 1: + # inside aggregation, need to pre-evaluate, groupby + # construction has checked that we don't have nested aggs, + # so stop the recursion and return ourselves for pre-eval + return AggInfo([(self, plc.aggregation.collect_list(), self)]) + else: + (child,) = self.children + return child.collect_agg(depth=depth) + + class Sort(Expr): __slots__ = ("options", "children") _non_child = ("dtype", "options") diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index 31a0be004ea..6b552642e88 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -433,7 +433,7 @@ def check_agg(agg: expr.Expr) -> int: NotImplementedError For unsupported expression nodes. """ - if isinstance(agg, (expr.BinOp, expr.Cast)): + if isinstance(agg, (expr.BinOp, expr.Cast, expr.UnaryFunction)): return max(GroupBy.check_agg(child) for child in agg.children) elif isinstance(agg, expr.Agg): return 1 + max(GroupBy.check_agg(child) for child in agg.children) diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index 0019b3aa98a..5a1e682abe7 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -361,8 +361,23 @@ def _(node: pl_expr.Function, visitor: NodeTraverser, dtype: plc.DataType) -> ex options, *(translate_expr(visitor, n=n) for n in node.input), ) - else: - raise NotImplementedError(f"No handler for Expr function node with {name=}") + elif isinstance(name, pl_expr.TemporalFunction): + return expr.TemporalFunction( + dtype, + name, + options, + *(translate_expr(visitor, n=n) for n in node.input), + ) + elif isinstance(name, str): + return expr.UnaryFunction( + dtype, + name, + options, + *(translate_expr(visitor, n=n) for n in node.input), + ) + raise NotImplementedError( + f"No handler for Expr function node with {name=}" + ) # pragma: no cover; polars raises on the rust side for now @_translate_expr.register diff --git a/python/cudf_polars/tests/expressions/test_datetime_basic.py b/python/cudf_polars/tests/expressions/test_datetime_basic.py index 6ba2a1dce1e..218101bf87c 100644 --- a/python/cudf_polars/tests/expressions/test_datetime_basic.py +++ b/python/cudf_polars/tests/expressions/test_datetime_basic.py @@ -2,6 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations +import datetime +from operator import methodcaller + import pytest import polars as pl @@ -32,3 +35,28 @@ def test_datetime_dataframe_scan(dtype): query = ldf.select(pl.col("b"), pl.col("a")) assert_gpu_result_equal(query) + + +@pytest.mark.parametrize( + "field", + [ + methodcaller("year"), + pytest.param( + methodcaller("day"), + marks=pytest.mark.xfail(reason="day extraction not implemented"), + ), + ], +) +def test_datetime_extract(field): + ldf = pl.LazyFrame( + {"dates": [datetime.date(2024, 1, 1), datetime.date(2024, 10, 11)]} + ) + q = ldf.select(field(pl.col("dates").dt)) + + with pytest.raises(AssertionError): + # polars produces int32, libcudf produces int16 for the year extraction + # libcudf can lose data here. + # https://github.com/rapidsai/cudf/issues/16196 + assert_gpu_result_equal(q) + + assert_gpu_result_equal(q, check_dtypes=False) diff --git a/python/cudf_polars/tests/expressions/test_round.py b/python/cudf_polars/tests/expressions/test_round.py new file mode 100644 index 00000000000..3af3a0ce6d1 --- /dev/null +++ b/python/cudf_polars/tests/expressions/test_round.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +import math + +import pytest + +import polars as pl + +from cudf_polars.testing.asserts import assert_gpu_result_equal + + +@pytest.fixture(params=[pl.Float32, pl.Float64]) +def dtype(request): + return request.param + + +@pytest.fixture +def df(dtype, with_nulls): + a = [-math.e, 10, 22.5, 1.5, 2.5, -1.5, math.pi, 8] + if with_nulls: + a[2] = None + a[-1] = None + return pl.LazyFrame({"a": a}, schema={"a": dtype}) + + +@pytest.mark.parametrize("decimals", [0, 2, 4]) +def test_round(df, decimals): + q = df.select(pl.col("a").round(decimals=decimals)) + + assert_gpu_result_equal(q, check_exact=False) diff --git a/python/cudf_polars/tests/expressions/test_unique.py b/python/cudf_polars/tests/expressions/test_unique.py new file mode 100644 index 00000000000..9b009a422c2 --- /dev/null +++ b/python/cudf_polars/tests/expressions/test_unique.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +import pytest + +import polars as pl + +from cudf_polars.testing.asserts import assert_gpu_result_equal + + +@pytest.mark.parametrize("maintain_order", [False, True], ids=["unstable", "stable"]) +@pytest.mark.parametrize("pre_sorted", [False, True], ids=["unsorted", "sorted"]) +def test_unique(maintain_order, pre_sorted): + ldf = pl.DataFrame( + { + "b": [1.5, 2.5, None, 1.5, 3, float("nan"), 3], + } + ).lazy() + if pre_sorted: + ldf = ldf.sort("b") + + query = ldf.select(pl.col("b").unique(maintain_order=maintain_order)) + assert_gpu_result_equal(query, check_row_order=maintain_order) diff --git a/python/cudf_polars/tests/test_groupby.py b/python/cudf_polars/tests/test_groupby.py index 8a6732b7063..b84e2c16b43 100644 --- a/python/cudf_polars/tests/test_groupby.py +++ b/python/cudf_polars/tests/test_groupby.py @@ -47,6 +47,8 @@ def keys(request): [pl.col("float").max() - pl.col("int").min()], [pl.col("float").mean(), pl.col("int").std()], [(pl.col("float") - pl.lit(2)).max()], + [pl.col("float").sum().round(decimals=1)], + [pl.col("float").round(decimals=1).sum()], ], ids=lambda aggs: "-".join(map(str, aggs)), ) From a583c97ca977041e3cc3399739e29962982d6aad Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:45:38 -0400 Subject: [PATCH 27/42] Fix cudf::strings::replace_multiple hang on empty target (#16167) Fixes logic in `cudf::strings::replace_multiple` to ignore empty targets correctly in the `replace_multi_fn` functor. Also updated the doxygen and added a gtest for this case. 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/16167 --- cpp/include/cudf/strings/replace.hpp | 2 +- cpp/src/strings/replace/multi.cu | 9 ++++----- cpp/tests/strings/replace_tests.cpp | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/cpp/include/cudf/strings/replace.hpp b/cpp/include/cudf/strings/replace.hpp index a19aa9be0c0..a714f762a19 100644 --- a/cpp/include/cudf/strings/replace.hpp +++ b/cpp/include/cudf/strings/replace.hpp @@ -122,7 +122,7 @@ std::unique_ptr replace_slice( * If a target string is found, it is replaced by the corresponding entry in the repls column. * All occurrences found in each string are replaced. * - * This does not use regex to match targets in the string. + * This does not use regex to match targets in the string. Empty string targets are ignored. * * Null string entries will return null output string entries. * diff --git a/cpp/src/strings/replace/multi.cu b/cpp/src/strings/replace/multi.cu index 43a3d69091a..2ca22f0e017 100644 --- a/cpp/src/strings/replace/multi.cu +++ b/cpp/src/strings/replace/multi.cu @@ -451,8 +451,8 @@ struct replace_multi_fn { while (spos < d_str.size_bytes()) { for (int tgt_idx = 0; tgt_idx < d_targets.size(); ++tgt_idx) { auto const d_tgt = d_targets.element(tgt_idx); - if ((d_tgt.size_bytes() <= (d_str.size_bytes() - spos)) && // check fit - (d_tgt.compare(in_ptr + spos, d_tgt.size_bytes()) == 0)) // and match + if (!d_tgt.empty() && (d_tgt.size_bytes() <= (d_str.size_bytes() - spos)) && // check fit + (d_tgt.compare(in_ptr + spos, d_tgt.size_bytes()) == 0)) // and match { auto const d_repl = (d_repls.size() == 1) ? d_repls.element(0) : d_repls.element(tgt_idx); @@ -468,9 +468,8 @@ struct replace_multi_fn { } ++spos; } - if (out_ptr) // copy remainder - { - memcpy(out_ptr, in_ptr + lpos, d_str.size_bytes() - lpos); + if (out_ptr) { + memcpy(out_ptr, in_ptr + lpos, d_str.size_bytes() - lpos); // copy remainder } else { d_sizes[idx] = bytes; } diff --git a/cpp/tests/strings/replace_tests.cpp b/cpp/tests/strings/replace_tests.cpp index 3aa7467d156..6c4afbb435a 100644 --- a/cpp/tests/strings/replace_tests.cpp +++ b/cpp/tests/strings/replace_tests.cpp @@ -532,6 +532,23 @@ TEST_F(StringsReplaceTest, ReplaceMultiLong) } } +TEST_F(StringsReplaceTest, EmptyTarget) +{ + auto const input = cudf::test::strings_column_wrapper({"hello", "world", "", "accénted"}); + auto const sv = cudf::strings_column_view(input); + + auto const targets = cudf::test::strings_column_wrapper({"e", "", "d"}); + auto const tv = cudf::strings_column_view(targets); + + auto const repls = cudf::test::strings_column_wrapper({"E", "_", "D"}); + auto const rv = cudf::strings_column_view(repls); + + // empty target should be ignored + auto results = cudf::strings::replace_multiple(sv, tv, rv); + auto expected = cudf::test::strings_column_wrapper({"hEllo", "worlD", "", "accéntED"}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(results->view(), expected); +} + TEST_F(StringsReplaceTest, EmptyStringsColumn) { auto const zero_size_strings_column = cudf::make_empty_column(cudf::type_id::STRING)->view(); From f6b355d7761ee3ecc0b243f09dc0c1d3b214a7ad Mon Sep 17 00:00:00 2001 From: James Lamb Date: Fri, 5 Jul 2024 16:49:23 -0500 Subject: [PATCH 28/42] skip CMake 3.30.0 (#16202) Contributes to https://github.com/rapidsai/build-planning/issues/80 Adds constraints to avoid pulling in CMake 3.30.0, for the reasons described in that issue. Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16202 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-122_arch-x86_64.yaml | 2 +- conda/recipes/cudf/conda_build_config.yaml | 2 +- conda/recipes/cudf_kafka/conda_build_config.yaml | 2 +- conda/recipes/libcudf/conda_build_config.yaml | 2 +- dependencies.yaml | 2 +- python/cudf/pyproject.toml | 2 +- python/cudf_kafka/pyproject.toml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index cc9238ab80a..b8d73a01f96 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -15,7 +15,7 @@ dependencies: - cachetools - clang-tools=16.0.6 - clang==16.0.6 -- cmake>=3.26.4 +- cmake>=3.26.4,!=3.30.0 - cramjam - cubinlinker - cuda-nvtx=11.8 diff --git a/conda/environments/all_cuda-122_arch-x86_64.yaml b/conda/environments/all_cuda-122_arch-x86_64.yaml index 9fecd452248..c32d21c5d36 100644 --- a/conda/environments/all_cuda-122_arch-x86_64.yaml +++ b/conda/environments/all_cuda-122_arch-x86_64.yaml @@ -15,7 +15,7 @@ dependencies: - cachetools - clang-tools=16.0.6 - clang==16.0.6 -- cmake>=3.26.4 +- cmake>=3.26.4,!=3.30.0 - cramjam - cuda-cudart-dev - cuda-nvcc diff --git a/conda/recipes/cudf/conda_build_config.yaml b/conda/recipes/cudf/conda_build_config.yaml index d399e440edd..af894cccda0 100644 --- a/conda/recipes/cudf/conda_build_config.yaml +++ b/conda/recipes/cudf/conda_build_config.yaml @@ -11,7 +11,7 @@ c_stdlib_version: - "2.17" cmake_version: - - ">=3.26.4" + - ">=3.26.4,!=3.30.0" cuda_compiler: - cuda-nvcc diff --git a/conda/recipes/cudf_kafka/conda_build_config.yaml b/conda/recipes/cudf_kafka/conda_build_config.yaml index d399e440edd..af894cccda0 100644 --- a/conda/recipes/cudf_kafka/conda_build_config.yaml +++ b/conda/recipes/cudf_kafka/conda_build_config.yaml @@ -11,7 +11,7 @@ c_stdlib_version: - "2.17" cmake_version: - - ">=3.26.4" + - ">=3.26.4,!=3.30.0" cuda_compiler: - cuda-nvcc diff --git a/conda/recipes/libcudf/conda_build_config.yaml b/conda/recipes/libcudf/conda_build_config.yaml index c01178bf732..4f99411e978 100644 --- a/conda/recipes/libcudf/conda_build_config.yaml +++ b/conda/recipes/libcudf/conda_build_config.yaml @@ -17,7 +17,7 @@ c_stdlib_version: - "2.17" cmake_version: - - ">=3.26.4" + - ">=3.26.4,!=3.30.0" libarrow_version: - "==16.1.0" diff --git a/dependencies.yaml b/dependencies.yaml index 6d4ba0c38d1..27621ff9a3f 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -243,7 +243,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - &cmake_ver cmake>=3.26.4 + - &cmake_ver cmake>=3.26.4,!=3.30.0 - &ninja ninja build_all: common: diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index 20b731624df..dcb33b1fc1a 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -121,7 +121,7 @@ skip = [ build-backend = "scikit_build_core.build" dependencies-file = "../../dependencies.yaml" requires = [ - "cmake>=3.26.4", + "cmake>=3.26.4,!=3.30.0", "cython>=3.0.3", "ninja", "numpy==1.23.*", diff --git a/python/cudf_kafka/pyproject.toml b/python/cudf_kafka/pyproject.toml index 11e18cd4f32..badfdf06d15 100644 --- a/python/cudf_kafka/pyproject.toml +++ b/python/cudf_kafka/pyproject.toml @@ -101,7 +101,7 @@ regex = "(?P.*)" build-backend = "scikit_build_core.build" dependencies-file = "../../dependencies.yaml" requires = [ - "cmake>=3.26.4", + "cmake>=3.26.4,!=3.30.0", "cython>=3.0.3", "ninja", "numpy==1.23.*", From d9a3728d37e0223afd9cfa525bd7ac8b43b39e63 Mon Sep 17 00:00:00 2001 From: Paul Taylor <178183+trxcllnt@users.noreply.github.com> Date: Mon, 8 Jul 2024 09:18:30 -0700 Subject: [PATCH 29/42] Define PTDS for the stream hook libs (#16182) We must define `CUDA_API_PER_THREAD_DEFAULT_STREAM` for the stream hook lib, since `cudaLaunchKernel` in CUDA 12.4+ is now a macro that expands to a different function when it's not defined. Authors: - Paul Taylor (https://github.com/trxcllnt) Approvers: - Robert Maynard (https://github.com/robertmaynard) URL: https://github.com/rapidsai/cudf/pull/16182 --- cpp/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 2811711d58c..7999ada9282 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -925,6 +925,11 @@ if(CUDF_BUILD_STREAMS_TEST_UTIL) add_library( ${_tgt} SHARED src/utilities/stacktrace.cpp tests/utilities/identify_stream_usage.cpp ) + if(CUDF_USE_PER_THREAD_DEFAULT_STREAM) + target_compile_definitions( + ${_tgt} PUBLIC CUDA_API_PER_THREAD_DEFAULT_STREAM CUDF_USE_PER_THREAD_DEFAULT_STREAM + ) + endif() set_target_properties( ${_tgt} From 6169ee17d31669d8930576003bc3ebaadca8a1fa Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:28:52 -0400 Subject: [PATCH 30/42] Add missing methods to lists/list_column_view.pxd in pylibcudf (#16175) Apart of #15162 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16175 --- .../cudf/_lib/pylibcudf/libcudf/lists/lists_column_view.pxd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/lists/lists_column_view.pxd b/python/cudf/cudf/_lib/pylibcudf/libcudf/lists/lists_column_view.pxd index fd21e7b334b..8917a6ac899 100644 --- a/python/cudf/cudf/_lib/pylibcudf/libcudf/lists/lists_column_view.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/lists/lists_column_view.pxd @@ -10,7 +10,9 @@ from cudf._lib.pylibcudf.libcudf.types cimport size_type cdef extern from "cudf/lists/lists_column_view.hpp" namespace "cudf" nogil: cdef cppclass lists_column_view(column_view): lists_column_view() except + + lists_column_view(const lists_column_view& lists_column) except + lists_column_view(const column_view& lists_column) except + + lists_column_view& operator=(const lists_column_view&) except + column_view parent() except + column_view offsets() except + column_view child() except + From 036e0ef5b99fd6ea09061af45854d28e44d21212 Mon Sep 17 00:00:00 2001 From: Thomas Li <47963215+lithomas1@users.noreply.github.com> Date: Mon, 8 Jul 2024 10:06:13 -0700 Subject: [PATCH 31/42] Migrate JSON reader to pylibcudf (#15966) Switches the JSON reader to use pylibcudf. xref #15162 Authors: - Thomas Li (https://github.com/lithomas1) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/15966 --- python/cudf/cudf/_lib/io/utils.pxd | 4 + python/cudf/cudf/_lib/io/utils.pyx | 27 ++ python/cudf/cudf/_lib/json.pyx | 127 ++++---- python/cudf/cudf/_lib/pylibcudf/io/json.pxd | 23 +- python/cudf/cudf/_lib/pylibcudf/io/json.pyx | 122 +++++++- python/cudf/cudf/_lib/pylibcudf/io/types.pxd | 5 + python/cudf/cudf/_lib/pylibcudf/io/types.pyx | 57 +++- .../_lib/pylibcudf/libcudf/CMakeLists.txt | 1 + .../_lib/pylibcudf/libcudf/io/CMakeLists.txt | 26 ++ .../cudf/_lib/pylibcudf/libcudf/io/json.pxd | 8 +- .../cudf/_lib/pylibcudf/libcudf/io/json.pyx | 0 .../cudf/_lib/pylibcudf/libcudf/io/types.pyx | 0 python/cudf/cudf/_lib/utils.pyx | 2 +- .../cudf/cudf/pylibcudf_tests/common/utils.py | 84 +++++- python/cudf/cudf/pylibcudf_tests/conftest.py | 5 + .../cudf/cudf/pylibcudf_tests/io/test_avro.py | 2 +- .../cudf/cudf/pylibcudf_tests/io/test_json.py | 275 +++++++++++++++++- python/cudf/cudf/tests/test_json.py | 7 +- 18 files changed, 674 insertions(+), 101 deletions(-) create mode 100644 python/cudf/cudf/_lib/pylibcudf/libcudf/io/CMakeLists.txt create mode 100644 python/cudf/cudf/_lib/pylibcudf/libcudf/io/json.pyx create mode 100644 python/cudf/cudf/_lib/pylibcudf/libcudf/io/types.pyx diff --git a/python/cudf/cudf/_lib/io/utils.pxd b/python/cudf/cudf/_lib/io/utils.pxd index 252d986843a..680a87c789e 100644 --- a/python/cudf/cudf/_lib/io/utils.pxd +++ b/python/cudf/cudf/_lib/io/utils.pxd @@ -16,6 +16,10 @@ cdef source_info make_source_info(list src) except* cdef sink_info make_sinks_info( list src, vector[unique_ptr[data_sink]] & data) except* cdef sink_info make_sink_info(src, unique_ptr[data_sink] & data) except* +cdef add_df_col_struct_names( + df, + child_names_dict +) cdef update_struct_field_names( table, vector[column_name_info]& schema_info) diff --git a/python/cudf/cudf/_lib/io/utils.pyx b/python/cudf/cudf/_lib/io/utils.pyx index 1d7c56888d9..58956b9e9b7 100644 --- a/python/cudf/cudf/_lib/io/utils.pyx +++ b/python/cudf/cudf/_lib/io/utils.pyx @@ -147,10 +147,37 @@ cdef cppclass iobase_data_sink(data_sink): return buf.tell() +cdef add_df_col_struct_names(df, child_names_dict): + for name, child_names in child_names_dict.items(): + col = df._data[name] + + df._data[name] = update_col_struct_field_names(col, child_names) + + +cdef update_col_struct_field_names(Column col, child_names): + if col.children: + children = list(col.children) + for i, (child, names) in enumerate(zip(children, child_names.values())): + children[i] = update_col_struct_field_names( + child, + names + ) + col.set_base_children(tuple(children)) + + if isinstance(col.dtype, StructDtype): + col = col._rename_fields( + child_names.keys() + ) + + return col + + cdef update_struct_field_names( table, vector[column_name_info]& schema_info ): + # Deprecated, remove in favor of add_col_struct_names + # when a reader is ported to pylibcudf for i, (name, col) in enumerate(table._data.items()): table._data[name] = update_column_struct_field_names( col, schema_info[i] diff --git a/python/cudf/cudf/_lib/json.pyx b/python/cudf/cudf/_lib/json.pyx index 22e34feb547..9c646e3357b 100644 --- a/python/cudf/cudf/_lib/json.pyx +++ b/python/cudf/cudf/_lib/json.pyx @@ -8,26 +8,16 @@ import cudf from cudf.core.buffer import acquire_spill_lock from libcpp cimport bool -from libcpp.map cimport map -from libcpp.string cimport string -from libcpp.utility cimport move -from libcpp.vector cimport vector cimport cudf._lib.pylibcudf.libcudf.io.types as cudf_io_types -from cudf._lib.io.utils cimport make_source_info, update_struct_field_names -from cudf._lib.pylibcudf.libcudf.io.json cimport ( - json_reader_options, - json_recovery_mode_t, - read_json as libcudf_read_json, - schema_element, -) -from cudf._lib.pylibcudf.libcudf.io.types cimport ( - compression_type, - table_with_metadata, -) -from cudf._lib.pylibcudf.libcudf.types cimport data_type, size_type +from cudf._lib.io.utils cimport add_df_col_struct_names +from cudf._lib.pylibcudf.io.types cimport compression_type +from cudf._lib.pylibcudf.libcudf.io.json cimport json_recovery_mode_t +from cudf._lib.pylibcudf.libcudf.io.types cimport compression_type +from cudf._lib.pylibcudf.libcudf.types cimport data_type, type_id +from cudf._lib.pylibcudf.types cimport DataType from cudf._lib.types cimport dtype_to_data_type -from cudf._lib.utils cimport data_from_unique_ptr +from cudf._lib.utils cimport data_from_pylibcudf_io import cudf._lib.pylibcudf as plc @@ -62,6 +52,7 @@ cpdef read_json(object filepaths_or_buffers, # If input data is a JSON string (or StringIO), hold a reference to # the encoded memoryview externally to ensure the encoded buffer # isn't destroyed before calling libcudf `read_json()` + for idx in range(len(filepaths_or_buffers)): if isinstance(filepaths_or_buffers[idx], io.StringIO): filepaths_or_buffers[idx] = \ @@ -71,17 +62,7 @@ cpdef read_json(object filepaths_or_buffers, filepaths_or_buffers[idx] = filepaths_or_buffers[idx].encode() # Setup arguments - cdef vector[data_type] c_dtypes_list - cdef map[string, schema_element] c_dtypes_schema_map cdef cudf_io_types.compression_type c_compression - # Determine byte read offsets if applicable - cdef size_type c_range_offset = ( - byte_range[0] if byte_range is not None else 0 - ) - cdef size_type c_range_size = ( - byte_range[1] if byte_range is not None else 0 - ) - cdef bool c_lines = lines if compression is not None: if compression == 'gzip': @@ -94,56 +75,50 @@ cpdef read_json(object filepaths_or_buffers, c_compression = cudf_io_types.compression_type.AUTO else: c_compression = cudf_io_types.compression_type.NONE - is_list_like_dtypes = False + + processed_dtypes = None + if dtype is False: raise ValueError("False value is unsupported for `dtype`") elif dtype is not True: + processed_dtypes = [] if isinstance(dtype, abc.Mapping): for k, v in dtype.items(): - c_dtypes_schema_map[str(k).encode()] = \ - _get_cudf_schema_element_from_dtype(v) + # Make sure keys are string + k = str(k) + lib_type, child_types = _get_cudf_schema_element_from_dtype(v) + processed_dtypes.append((k, lib_type, child_types)) elif isinstance(dtype, abc.Collection): - is_list_like_dtypes = True - c_dtypes_list.reserve(len(dtype)) for col_dtype in dtype: - c_dtypes_list.push_back( - _get_cudf_data_type_from_dtype( - col_dtype)) + processed_dtypes.append( + # Ignore child columns since we cannot specify their dtypes + # when passing a list + _get_cudf_schema_element_from_dtype(col_dtype)[0] + ) else: raise TypeError("`dtype` must be 'list like' or 'dict'") - cdef json_reader_options opts = move( - json_reader_options.builder(make_source_info(filepaths_or_buffers)) - .compression(c_compression) - .lines(c_lines) - .byte_range_offset(c_range_offset) - .byte_range_size(c_range_size) - .recovery_mode(_get_json_recovery_mode(on_bad_lines)) - .build() + table_w_meta = plc.io.json.read_json( + plc.io.SourceInfo(filepaths_or_buffers), + processed_dtypes, + c_compression, + lines, + byte_range_offset = byte_range[0] if byte_range is not None else 0, + byte_range_size = byte_range[1] if byte_range is not None else 0, + keep_quotes = keep_quotes, + mixed_types_as_string = mixed_types_as_string, + prune_columns = prune_columns, + recovery_mode = _get_json_recovery_mode(on_bad_lines) ) - if is_list_like_dtypes: - opts.set_dtypes(c_dtypes_list) - else: - opts.set_dtypes(c_dtypes_schema_map) - - opts.enable_keep_quotes(keep_quotes) - opts.enable_mixed_types_as_string(mixed_types_as_string) - opts.enable_prune_columns(prune_columns) - - # Read JSON - cdef cudf_io_types.table_with_metadata c_result - with nogil: - c_result = move(libcudf_read_json(opts)) - - 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 - )) - - update_struct_field_names(df, c_result.metadata.schema_info) + df = cudf.DataFrame._from_data( + *data_from_pylibcudf_io( + table_w_meta + ) + ) + # Post-processing to add in struct column names + add_df_col_struct_names(df, table_w_meta.child_names) return df @@ -192,28 +167,32 @@ def write_json( ) -cdef schema_element _get_cudf_schema_element_from_dtype(object dtype) except *: - cdef schema_element s_element - cdef data_type lib_type +cdef _get_cudf_schema_element_from_dtype(object dtype) except *: dtype = cudf.dtype(dtype) if isinstance(dtype, cudf.CategoricalDtype): raise NotImplementedError( "CategoricalDtype as dtype is not yet " "supported in JSON reader" ) - lib_type = dtype_to_data_type(dtype) - s_element.type = lib_type + + lib_type = DataType.from_libcudf(dtype_to_data_type(dtype)) + child_types = [] + if isinstance(dtype, cudf.StructDtype): for name, child_type in dtype.fields.items(): - s_element.child_types[name.encode()] = \ + child_lib_type, grandchild_types = \ _get_cudf_schema_element_from_dtype(child_type) + child_types.append((name, child_lib_type, grandchild_types)) elif isinstance(dtype, cudf.ListDtype): - s_element.child_types["offsets".encode()] = \ - _get_cudf_schema_element_from_dtype(cudf.dtype("int32")) - s_element.child_types["element".encode()] = \ + child_lib_type, grandchild_types = \ _get_cudf_schema_element_from_dtype(dtype.element_type) - return s_element + child_types = [ + ("offsets", DataType.from_libcudf(data_type(type_id.INT32)), []), + ("element", child_lib_type, grandchild_types) + ] + + return lib_type, child_types cdef data_type _get_cudf_data_type_from_dtype(object dtype) except *: diff --git a/python/cudf/cudf/_lib/pylibcudf/io/json.pxd b/python/cudf/cudf/_lib/pylibcudf/io/json.pxd index a91d574131f..f7f733a493d 100644 --- a/python/cudf/cudf/_lib/pylibcudf/io/json.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/io/json.pxd @@ -1,11 +1,30 @@ # Copyright (c) 2024, NVIDIA CORPORATION. - from libcpp cimport bool -from cudf._lib.pylibcudf.io.types cimport SinkInfo, TableWithMetadata +from cudf._lib.pylibcudf.io.types cimport ( + SinkInfo, + SourceInfo, + TableWithMetadata, + compression_type, +) +from cudf._lib.pylibcudf.libcudf.io.json cimport json_recovery_mode_t from cudf._lib.pylibcudf.libcudf.types cimport size_type +cpdef TableWithMetadata read_json( + SourceInfo source_info, + list dtypes = *, + compression_type compression = *, + bool lines = *, + size_type byte_range_offset = *, + size_type byte_range_size = *, + bool keep_quotes = *, + bool mixed_types_as_string = *, + bool prune_columns = *, + json_recovery_mode_t recovery_mode = *, +) + + cpdef void write_json( SinkInfo sink_info, TableWithMetadata tbl, diff --git a/python/cudf/cudf/_lib/pylibcudf/io/json.pyx b/python/cudf/cudf/_lib/pylibcudf/io/json.pyx index 7530eba3803..354cb4981de 100644 --- a/python/cudf/cudf/_lib/pylibcudf/io/json.pyx +++ b/python/cudf/cudf/_lib/pylibcudf/io/json.pyx @@ -1,16 +1,130 @@ # Copyright (c) 2024, NVIDIA CORPORATION. - from libcpp cimport bool from libcpp.limits cimport numeric_limits +from libcpp.map cimport map from libcpp.string cimport string +from libcpp.utility cimport move +from libcpp.vector cimport vector -from cudf._lib.pylibcudf.io.types cimport SinkInfo, TableWithMetadata +from cudf._lib.pylibcudf.io.types cimport ( + SinkInfo, + SourceInfo, + TableWithMetadata, +) from cudf._lib.pylibcudf.libcudf.io.json cimport ( + json_reader_options, + json_recovery_mode_t, json_writer_options, + read_json as cpp_read_json, + schema_element, write_json as cpp_write_json, ) -from cudf._lib.pylibcudf.libcudf.io.types cimport table_metadata -from cudf._lib.pylibcudf.types cimport size_type +from cudf._lib.pylibcudf.libcudf.io.types cimport ( + compression_type, + table_metadata, + table_with_metadata, +) +from cudf._lib.pylibcudf.libcudf.types cimport data_type, size_type +from cudf._lib.pylibcudf.types cimport DataType + + +cdef map[string, schema_element] _generate_schema_map(list dtypes): + cdef map[string, schema_element] schema_map + cdef schema_element s_elem + cdef string c_name + + for name, dtype, child_dtypes in dtypes: + if not (isinstance(name, str) and + isinstance(dtype, DataType) and + isinstance(child_dtypes, list)): + + raise ValueError("Must pass a list of a tuple containing " + "(column_name, column_dtype, list of child_dtypes)") + + c_name = name.encode() + + s_elem.type = (dtype).c_obj + s_elem.child_types = _generate_schema_map(child_dtypes) + + schema_map[c_name] = s_elem + return schema_map + + +cpdef TableWithMetadata read_json( + SourceInfo source_info, + list dtypes = None, + compression_type compression = compression_type.AUTO, + bool lines = False, + size_type byte_range_offset = 0, + size_type byte_range_size = 0, + bool keep_quotes = False, + bool mixed_types_as_string = False, + bool prune_columns = False, + json_recovery_mode_t recovery_mode = json_recovery_mode_t.FAIL, +): + """Reads an JSON file into a :py:class:`~.types.TableWithMetadata`. + + Parameters + ---------- + source_info : SourceInfo + The SourceInfo object to read the JSON file from. + dtypes : list, default None + Set data types for the columns in the JSON file. + + Each element of the list has the format + (column_name, column_dtype, list of child dtypes), where + the list of child dtypes is an empty list if the child is not + a nested type (list or struct dtype), and is of format + (column_child_name, column_child_type, list of grandchild dtypes). + compression_type: CompressionType, default CompressionType.AUTO + The compression format of the JSON source. + byte_range_offset : size_type, default 0 + Number of bytes to skip from source start. + byte_range_size : size_type, default 0 + Number of bytes to read. By default, will read all bytes. + keep_quotes : bool, default False + Whether the reader should keep quotes of string values. + prune_columns : bool, default False + Whether to only read columns specified in dtypes. + recover_mode : JSONRecoveryMode, default JSONRecoveryMode.FAIL + Whether to raise an error or set corresponding values to null + when encountering an invalid JSON line. + + Returns + ------- + TableWithMetadata + The Table and its corresponding metadata (column names) that were read in. + """ + cdef vector[data_type] types_vec + cdef json_reader_options opts = move( + json_reader_options.builder(source_info.c_obj) + .compression(compression) + .lines(lines) + .byte_range_offset(byte_range_offset) + .byte_range_size(byte_range_size) + .recovery_mode(recovery_mode) + .build() + ) + + if dtypes is not None: + if isinstance(dtypes[0], tuple): + opts.set_dtypes(move(_generate_schema_map(dtypes))) + else: + for dtype in dtypes: + types_vec.push_back((dtype).c_obj) + opts.set_dtypes(types_vec) + + opts.enable_keep_quotes(keep_quotes) + opts.enable_mixed_types_as_string(mixed_types_as_string) + opts.enable_prune_columns(prune_columns) + + # Read JSON + cdef table_with_metadata c_result + + with nogil: + c_result = move(cpp_read_json(opts)) + + return TableWithMetadata.from_libcudf(c_result) cpdef void write_json( diff --git a/python/cudf/cudf/_lib/pylibcudf/io/types.pxd b/python/cudf/cudf/_lib/pylibcudf/io/types.pxd index 88daf54f33b..ab223c16a72 100644 --- a/python/cudf/cudf/_lib/pylibcudf/io/types.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/io/types.pxd @@ -28,6 +28,11 @@ cdef class TableWithMetadata: cdef vector[column_name_info] _make_column_info(self, list column_names) + cdef list _make_columns_list(self, dict child_dict) + + @staticmethod + cdef dict _parse_col_names(vector[column_name_info] infos) + @staticmethod cdef TableWithMetadata from_libcudf(table_with_metadata& tbl) diff --git a/python/cudf/cudf/_lib/pylibcudf/io/types.pyx b/python/cudf/cudf/_lib/pylibcudf/io/types.pyx index f94e20970a4..df0b729b711 100644 --- a/python/cudf/cudf/_lib/pylibcudf/io/types.pyx +++ b/python/cudf/cudf/_lib/pylibcudf/io/types.pyx @@ -22,6 +22,11 @@ import errno import io import os +from cudf._lib.pylibcudf.libcudf.io.json import \ + json_recovery_mode_t as JSONRecoveryMode # no-cython-lint +from cudf._lib.pylibcudf.libcudf.io.types import \ + compression_type as CompressionType # no-cython-lint + cdef class TableWithMetadata: """A container holding a table and its associated metadata @@ -69,16 +74,44 @@ cdef class TableWithMetadata: """ return self.tbl.columns() - @property - def column_names(self): + cdef list _make_columns_list(self, dict child_dict): + cdef list names = [] + for child in child_dict: + grandchildren = self._make_columns_list(child_dict[child]) + names.append((child, grandchildren)) + return names + + def column_names(self, include_children=False): """ Return a list containing the column names of the table """ cdef list names = [] + cdef str name + cdef dict child_names = self.child_names for col_info in self.metadata.schema_info: - # TODO: Handle nesting (columns with child columns) - assert col_info.children.size() == 0, "Child column names are not handled!" - names.append(col_info.name.decode()) + name = col_info.name.decode() + if include_children: + children = self._make_columns_list(child_names[name]) + names.append((name, children)) + else: + names.append(name) + return names + + @property + def child_names(self): + """ + Return a dictionary mapping the names of columns with children + to the names of their child columns + """ + return TableWithMetadata._parse_col_names(self.metadata.schema_info) + + @staticmethod + cdef dict _parse_col_names(vector[column_name_info] infos): + cdef dict child_names = dict() + cdef dict names = dict() + for col_info in infos: + child_names = TableWithMetadata._parse_col_names(col_info.children) + names[col_info.name.decode()] = child_names return names @staticmethod @@ -137,6 +170,15 @@ cdef class SourceInfo: cdef vector[host_buffer] c_host_buffers cdef const unsigned char[::1] c_buffer cdef bint empty_buffer = False + cdef list new_sources = [] + + if isinstance(sources[0], io.StringIO): + for buffer in sources: + if not isinstance(buffer, io.StringIO): + raise ValueError("All sources must be of the same type!") + new_sources.append(buffer.read().encode()) + sources = new_sources + if isinstance(sources[0], bytes): empty_buffer = True for buffer in sources: @@ -156,7 +198,10 @@ cdef class SourceInfo: c_buffer.shape[0])) else: raise ValueError("Sources must be a list of str/paths, " - "bytes, io.BytesIO, or a Datasource") + "bytes, io.BytesIO, io.StringIO, or a Datasource") + + if empty_buffer is True: + c_host_buffers.push_back(host_buffer(NULL, 0)) self.c_obj = source_info(c_host_buffers) diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/CMakeLists.txt b/python/cudf/cudf/_lib/pylibcudf/libcudf/CMakeLists.txt index 6c66d01ca57..699e85ce567 100644 --- a/python/cudf/cudf/_lib/pylibcudf/libcudf/CMakeLists.txt +++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/CMakeLists.txt @@ -22,4 +22,5 @@ rapids_cython_create_modules( SOURCE_FILES "${cython_sources}" LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cudf MODULE_PREFIX cpp ) +add_subdirectory(io) add_subdirectory(strings) diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/io/CMakeLists.txt b/python/cudf/cudf/_lib/pylibcudf/libcudf/io/CMakeLists.txt new file mode 100644 index 00000000000..6831063ecb9 --- /dev/null +++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/io/CMakeLists.txt @@ -0,0 +1,26 @@ +# ============================================================================= +# Copyright (c) 2024, 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. +# ============================================================================= + +set(cython_sources json.pyx types.pyx) + +set(linked_libraries cudf::cudf) + +rapids_cython_create_modules( + CXX + SOURCE_FILES "${cython_sources}" + LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cudf MODULE_PREFIX cpp_io_ +) + +set(targets_using_arrow_headers cpp_io_json cpp_io_types) +link_to_pyarrow_headers("${targets_using_arrow_headers}") diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/io/json.pxd b/python/cudf/cudf/_lib/pylibcudf/libcudf/io/json.pxd index 2e50cccd132..86621ae184f 100644 --- a/python/cudf/cudf/_lib/pylibcudf/libcudf/io/json.pxd +++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/io/json.pxd @@ -1,6 +1,6 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. -from libc.stdint cimport uint8_t +from libc.stdint cimport int32_t, uint8_t from libcpp cimport bool from libcpp.map cimport map from libcpp.memory cimport shared_ptr, unique_ptr @@ -19,9 +19,9 @@ cdef extern from "cudf/io/json.hpp" \ data_type type map[string, schema_element] child_types - cdef enum json_recovery_mode_t: - FAIL "cudf::io::json_recovery_mode_t::FAIL" - RECOVER_WITH_NULL "cudf::io::json_recovery_mode_t::RECOVER_WITH_NULL" + cpdef enum class json_recovery_mode_t(int32_t): + FAIL + RECOVER_WITH_NULL cdef cppclass json_reader_options: json_reader_options() except + diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/io/json.pyx b/python/cudf/cudf/_lib/pylibcudf/libcudf/io/json.pyx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/io/types.pyx b/python/cudf/cudf/_lib/pylibcudf/libcudf/io/types.pyx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/cudf/cudf/_lib/utils.pyx b/python/cudf/cudf/_lib/utils.pyx index de6b9f690b6..f136cd997a7 100644 --- a/python/cudf/cudf/_lib/utils.pyx +++ b/python/cudf/cudf/_lib/utils.pyx @@ -322,7 +322,7 @@ cdef data_from_pylibcudf_io(tbl_with_meta): """ return _data_from_columns( columns=[Column.from_pylibcudf(plc) for plc in tbl_with_meta.columns], - column_names=tbl_with_meta.column_names, + column_names=tbl_with_meta.column_names(include_children=False), index_names=None ) diff --git a/python/cudf/cudf/pylibcudf_tests/common/utils.py b/python/cudf/cudf/pylibcudf_tests/common/utils.py index d41e6c720bf..46603ff32b8 100644 --- a/python/cudf/cudf/pylibcudf_tests/common/utils.py +++ b/python/cudf/cudf/pylibcudf_tests/common/utils.py @@ -8,13 +8,14 @@ import pytest from cudf._lib import pylibcudf as plc +from cudf._lib.pylibcudf.io.types import CompressionType def metadata_from_arrow_type( pa_type: pa.Array, name: str = "", ) -> plc.interop.ColumnMetadata | None: - metadata = plc.interop.ColumnMetadata(name) # None + metadata = plc.interop.ColumnMetadata(name) if pa.types.is_list(pa_type): child_meta = [plc.interop.ColumnMetadata("offsets")] for i in range(pa_type.num_fields): @@ -39,9 +40,25 @@ def metadata_from_arrow_type( def assert_column_eq( - lhs: pa.Array | plc.Column, rhs: pa.Array | plc.Column + lhs: pa.Array | plc.Column, + rhs: pa.Array | plc.Column, + check_field_nullability=True, ) -> None: - """Verify that a pylibcudf array and PyArrow array are equal.""" + """Verify that a pylibcudf array and PyArrow array are equal. + + Parameters + ---------- + lhs: Union[pa.Array, plc.Column] + The array with the expected values + rhs: Union[pa.Array, plc.Column] + The array to check + check_field_nullability: + For list/struct dtypes, whether to check if the nullable attributes + on child fields are equal. + + Useful for checking roundtripping of lossy formats like JSON that may not + preserve this information. + """ # Nested types require children metadata to be passed to the conversion function. if isinstance(lhs, (pa.Array, pa.ChunkedArray)) and isinstance( rhs, plc.Column @@ -65,6 +82,33 @@ def assert_column_eq( if isinstance(rhs, pa.ChunkedArray): rhs = rhs.combine_chunks() + def _make_fields_nullable(typ): + new_fields = [] + for i in range(typ.num_fields): + child_field = typ.field(i) + if not child_field.nullable: + child_type = child_field.type + if isinstance(child_field.type, (pa.StructType, pa.ListType)): + child_type = _make_fields_nullable(child_type) + new_fields.append( + pa.field(child_field.name, child_type, nullable=True) + ) + else: + new_fields.append(child_field) + + if isinstance(typ, pa.StructType): + return pa.struct(new_fields) + elif isinstance(typ, pa.ListType): + return pa.list_(new_fields[0]) + return typ + + if not check_field_nullability: + rhs_type = _make_fields_nullable(rhs.type) + rhs = rhs.cast(rhs_type) + + lhs_type = _make_fields_nullable(lhs.type) + lhs = rhs.cast(lhs_type) + assert lhs.equals(rhs) @@ -78,20 +122,24 @@ def assert_table_eq(pa_table: pa.Table, plc_table: plc.Table) -> None: def assert_table_and_meta_eq( - plc_table_w_meta: plc.io.types.TableWithMetadata, pa_table: pa.Table + pa_table: pa.Table, + plc_table_w_meta: plc.io.types.TableWithMetadata, + check_field_nullability=True, ) -> None: """Verify that the pylibcudf TableWithMetadata and PyArrow table are equal""" plc_table = plc_table_w_meta.tbl plc_shape = (plc_table.num_rows(), plc_table.num_columns()) - assert plc_shape == pa_table.shape + assert ( + plc_shape == pa_table.shape + ), f"{plc_shape} is not equal to {pa_table.shape}" for plc_col, pa_col in zip(plc_table.columns(), pa_table.columns): - assert_column_eq(plc_col, pa_col) + assert_column_eq(pa_col, plc_col, check_field_nullability) # Check column name equality - assert plc_table_w_meta.column_names == pa_table.column_names + assert plc_table_w_meta.column_names() == pa_table.column_names def cudf_raises(expected_exception: BaseException, *args, **kwargs): @@ -182,4 +230,26 @@ def sink_to_str(sink): + DEFAULT_PA_STRUCT_TESTING_TYPES ) +# Map pylibcudf compression types to pandas ones +# Not all compression types map cleanly, read the comments to learn more! +# If a compression type is unsupported, it maps to False. + +COMPRESSION_TYPE_TO_PANDAS = { + CompressionType.NONE: None, + # Users of this dict will have to special case + # AUTO + CompressionType.AUTO: None, + CompressionType.GZIP: "gzip", + CompressionType.BZIP2: "bz2", + CompressionType.ZIP: "zip", + CompressionType.XZ: "xz", + CompressionType.ZSTD: "zstd", + # Unsupported + CompressionType.ZLIB: False, + CompressionType.LZ4: False, + CompressionType.LZO: False, + # These only work for parquet + CompressionType.SNAPPY: "snappy", + CompressionType.BROTLI: "brotli", +} ALL_PA_TYPES = DEFAULT_PA_TYPES diff --git a/python/cudf/cudf/pylibcudf_tests/conftest.py b/python/cudf/cudf/pylibcudf_tests/conftest.py index e4760ea7ac8..39832eb4bba 100644 --- a/python/cudf/cudf/pylibcudf_tests/conftest.py +++ b/python/cudf/cudf/pylibcudf_tests/conftest.py @@ -121,6 +121,11 @@ def source_or_sink(request, tmp_path): return fp_or_buf() +@pytest.fixture(params=[opt for opt in plc.io.types.CompressionType]) +def compression_type(request): + return request.param + + @pytest.fixture( scope="session", params=[opt for opt in plc.types.Interpolation] ) diff --git a/python/cudf/cudf/pylibcudf_tests/io/test_avro.py b/python/cudf/cudf/pylibcudf_tests/io/test_avro.py index d6cd86768cd..061d6792ce3 100644 --- a/python/cudf/cudf/pylibcudf_tests/io/test_avro.py +++ b/python/cudf/cudf/pylibcudf_tests/io/test_avro.py @@ -120,4 +120,4 @@ def test_read_avro(avro_dtypes, avro_dtype_data, row_opts, columns, nullable): if columns != []: expected = expected.select(columns) - assert_table_and_meta_eq(res, expected) + assert_table_and_meta_eq(expected, res) diff --git a/python/cudf/cudf/pylibcudf_tests/io/test_json.py b/python/cudf/cudf/pylibcudf_tests/io/test_json.py index d6b8bfa6976..c13eaf40625 100644 --- a/python/cudf/cudf/pylibcudf_tests/io/test_json.py +++ b/python/cudf/cudf/pylibcudf_tests/io/test_json.py @@ -1,11 +1,49 @@ # Copyright (c) 2024, NVIDIA CORPORATION. import io +import pandas as pd import pyarrow as pa import pytest -from utils import sink_to_str +from utils import ( + COMPRESSION_TYPE_TO_PANDAS, + assert_table_and_meta_eq, + sink_to_str, +) import cudf._lib.pylibcudf as plc +from cudf._lib.pylibcudf.io.types import CompressionType + + +def make_json_source(path_or_buf, pa_table, **kwargs): + """ + Uses pandas to write a pyarrow Table to a JSON file. + + The caller is responsible for making sure that no arguments + unsupported by pandas are passed in. + """ + df = pa_table.to_pandas() + if "compression" in kwargs: + kwargs["compression"] = COMPRESSION_TYPE_TO_PANDAS[ + kwargs["compression"] + ] + df.to_json(path_or_buf, orient="records", **kwargs) + if isinstance(path_or_buf, io.IOBase): + path_or_buf.seek(0) + return path_or_buf + + +def write_json_bytes(source, json_str): + """ + Write a JSON string to the source + """ + if not isinstance(source, io.IOBase): + with open(source, "w") as source_f: + source_f.write(json_str) + else: + if isinstance(source, io.BytesIO): + json_str = json_str.encode("utf-8") + source.write(json_str) + source.seek(0) @pytest.mark.parametrize("rows_per_chunk", [8, 100]) @@ -114,3 +152,238 @@ def test_write_json_bool_opts(true_value, false_value): pd_result = pd_result.replace("false", false_value) assert str_result == pd_result + + +@pytest.mark.parametrize("lines", [True, False]) +def test_read_json_basic( + table_data, source_or_sink, lines, compression_type, request +): + if compression_type in { + # Not supported by libcudf + CompressionType.SNAPPY, + CompressionType.XZ, + CompressionType.ZSTD, + # Not supported by pandas + # TODO: find a way to test these + CompressionType.BROTLI, + CompressionType.LZ4, + CompressionType.LZO, + CompressionType.ZLIB, + }: + pytest.skip("unsupported compression type by pandas/libcudf") + + # can't compress non-binary data with pandas + if isinstance(source_or_sink, io.StringIO): + compression_type = CompressionType.NONE + + _, pa_table = table_data + + source = make_json_source( + source_or_sink, pa_table, lines=lines, compression=compression_type + ) + + request.applymarker( + pytest.mark.xfail( + condition=( + len(pa_table) > 0 + and compression_type + not in {CompressionType.NONE, CompressionType.AUTO} + ), + # note: wasn't able to narrow down the specific types that were failing + # seems to be a little non-deterministic, but always fails with + # cudaErrorInvalidValue invalid argument + reason="libcudf json reader crashes on compressed non empty table_data", + ) + ) + + if isinstance(source, io.IOBase): + source.seek(0) + + res = plc.io.json.read_json( + plc.io.SourceInfo([source]), + compression=compression_type, + lines=lines, + ) + + # Adjustments to correct for the fact orient=records is lossy + # and doesn't + # 1) preserve colnames when zero rows in table + # 2) preserve struct nullability + # 3) differentiate int64/uint64 + if len(pa_table) == 0: + pa_table = pa.table([]) + + new_fields = [] + for i in range(len(pa_table.schema)): + curr_field = pa_table.schema.field(i) + if curr_field.type == pa.uint64(): + try: + curr_field = curr_field.with_type(pa.int64()) + except OverflowError: + # There will be no confusion, values are too large + # for int64 anyways + pass + new_fields.append(curr_field) + + pa_table = pa_table.cast(pa.schema(new_fields)) + + # Convert non-nullable struct fields to nullable fields + # since nullable=False cannot roundtrip through orient='records' + # JSON format + assert_table_and_meta_eq(pa_table, res, check_field_nullability=False) + + +def test_read_json_dtypes(table_data, source_or_sink): + # Simple test for dtypes where we read in + # all numeric data as floats + _, pa_table = table_data + source = make_json_source( + source_or_sink, + pa_table, + lines=True, + ) + + dtypes = [] + new_fields = [] + for i in range(len(pa_table.schema)): + field = pa_table.schema.field(i) + child_types = [] + + def get_child_types(typ): + typ_child_types = [] + for i in range(typ.num_fields): + curr_field = typ.field(i) + typ_child_types.append( + ( + curr_field.name, + curr_field.type, + get_child_types(curr_field.type), + ) + ) + return typ_child_types + + plc_type = plc.interop.from_arrow(field.type) + if pa.types.is_integer(field.type) or pa.types.is_unsigned_integer( + field.type + ): + plc_type = plc.interop.from_arrow(pa.float64()) + field = field.with_type(pa.float64()) + + dtypes.append((field.name, plc_type, child_types)) + + new_fields.append(field) + + new_schema = pa.schema(new_fields) + + res = plc.io.json.read_json( + plc.io.SourceInfo([source]), dtypes=dtypes, lines=True + ) + new_table = pa_table.cast(new_schema) + + # orient=records is lossy + # and doesn't preserve column names when there's zero rows in the table + if len(new_table) == 0: + new_table = pa.table([]) + + assert_table_and_meta_eq(new_table, res, check_field_nullability=False) + + +@pytest.mark.parametrize("chunk_size", [10, 15, 20]) +def test_read_json_lines_byte_range(source_or_sink, chunk_size): + source = source_or_sink + if isinstance(source_or_sink, io.StringIO): + pytest.skip("byte_range doesn't work on StringIO") + + json_str = "[1, 2, 3]\n[4, 5, 6]\n[7, 8, 9]\n" + write_json_bytes(source, json_str) + + tbls_w_meta = [] + for chunk_start in range(0, len(json_str.encode("utf-8")), chunk_size): + tbls_w_meta.append( + plc.io.json.read_json( + plc.io.SourceInfo([source]), + lines=True, + byte_range_offset=chunk_start, + byte_range_size=chunk_start + chunk_size, + ) + ) + + if isinstance(source, io.IOBase): + source.seek(0) + exp = pd.read_json(source, orient="records", lines=True) + + # TODO: can do this operation using pylibcudf + tbls = [] + for tbl_w_meta in tbls_w_meta: + if tbl_w_meta.tbl.num_rows() > 0: + tbls.append(plc.interop.to_arrow(tbl_w_meta.tbl)) + full_tbl = pa.concat_tables(tbls) + + full_tbl_plc = plc.io.TableWithMetadata( + plc.interop.from_arrow(full_tbl), + tbls_w_meta[0].column_names(include_children=True), + ) + assert_table_and_meta_eq(pa.Table.from_pandas(exp), full_tbl_plc) + + +@pytest.mark.parametrize("keep_quotes", [True, False]) +def test_read_json_lines_keep_quotes(keep_quotes, source_or_sink): + source = source_or_sink + + json_bytes = '["a", "b", "c"]\n' + write_json_bytes(source, json_bytes) + + tbl_w_meta = plc.io.json.read_json( + plc.io.SourceInfo([source]), lines=True, keep_quotes=keep_quotes + ) + + template = "{0}" + if keep_quotes: + template = '"{0}"' + + exp = pa.Table.from_arrays( + [ + [template.format("a")], + [template.format("b")], + [template.format("c")], + ], + names=["0", "1", "2"], + ) + + assert_table_and_meta_eq(exp, tbl_w_meta) + + +@pytest.mark.parametrize( + "recovery_mode", [opt for opt in plc.io.types.JSONRecoveryMode] +) +def test_read_json_lines_recovery_mode(recovery_mode, source_or_sink): + source = source_or_sink + + json_bytes = '{"a":1,"b":10}\n{"a":2,"b":11}\nabc\n{"a":3,"b":12}\n' + write_json_bytes(source, json_bytes) + + if recovery_mode == plc.io.types.JSONRecoveryMode.FAIL: + with pytest.raises(RuntimeError): + plc.io.json.read_json( + plc.io.SourceInfo([source]), + lines=True, + recovery_mode=recovery_mode, + ) + else: + # Recover case (bad values replaced with nulls) + tbl_w_meta = plc.io.json.read_json( + plc.io.SourceInfo([source]), + lines=True, + recovery_mode=recovery_mode, + ) + exp = pa.Table.from_arrays( + [[1, 2, None, 3], [10, 11, None, 12]], names=["a", "b"] + ) + assert_table_and_meta_eq(exp, tbl_w_meta) + + +# TODO: Add tests for these! +# Tests were not added in the initial PR porting the JSON reader to pylibcudf +# to save time (and since there are no existing tests for these in Python cuDF) +# mixed_types_as_string = mixed_types_as_string, +# prune_columns = prune_columns, diff --git a/python/cudf/cudf/tests/test_json.py b/python/cudf/cudf/tests/test_json.py index 297040b6d95..9222f6d23db 100644 --- a/python/cudf/cudf/tests/test_json.py +++ b/python/cudf/cudf/tests/test_json.py @@ -1077,8 +1077,13 @@ def test_json_dtypes_nested_data(): ) pdf = pd.read_json( - StringIO(expected_json_str), orient="records", lines=True + StringIO(expected_json_str), + orient="records", + lines=True, ) + + assert_eq(df, pdf) + pdf.columns = pdf.columns.astype("str") pa_table_pdf = pa.Table.from_pandas( pdf, schema=df.to_arrow().schema, safe=False From 2664427d5eb427cb4c7682d51a37fde71f7c6c8f Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:00:30 -0400 Subject: [PATCH 32/42] Add single offset to an empty ListArray in cudf::to_arrow (#16201) Closes #16164 Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - Nghia Truong (https://github.com/ttnghia) - Jayjeet Chakraborty (https://github.com/JayjeetAtGithub) URL: https://github.com/rapidsai/cudf/pull/16201 --- cpp/src/interop/to_arrow.cu | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cpp/src/interop/to_arrow.cu b/cpp/src/interop/to_arrow.cu index 2b3aa2f08f1..62b85891adb 100644 --- a/cpp/src/interop/to_arrow.cu +++ b/cpp/src/interop/to_arrow.cu @@ -376,7 +376,12 @@ std::shared_ptr dispatch_to_arrow::operator()( metadata.children_meta.empty() ? std::vector{{}, {}} : metadata.children_meta; auto child_arrays = fetch_child_array(input_view, children_meta, ar_mr, stream); if (child_arrays.empty()) { - return std::make_shared(arrow::list(arrow::null()), 0, nullptr, nullptr); + // Empty list will have only one value in offset of 4 bytes + auto tmp_offset_buffer = allocate_arrow_buffer(sizeof(int32_t), ar_mr); + memset(tmp_offset_buffer->mutable_data(), 0, sizeof(int32_t)); + + return std::make_shared( + arrow::list(arrow::null()), 0, std::move(tmp_offset_buffer), nullptr); } auto offset_buffer = child_arrays[0]->data()->buffers[1]; From e9cb7dd7d3d9b810c4575cbdbead8148d85e990f Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 8 Jul 2024 08:36:34 -1000 Subject: [PATCH 33/42] Support at/iat indexers in cudf.pandas (#16177) closes #16112 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/16177 --- python/cudf/cudf/core/dataframe.py | 12 ++++++++++-- python/cudf/cudf/pandas/_wrappers/pandas.py | 12 ++++++++++++ .../cudf_pandas_tests/test_cudf_pandas.py | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/python/cudf/cudf/core/dataframe.py b/python/cudf/cudf/core/dataframe.py index b249410c2e4..3e5ff9c18b5 100644 --- a/python/cudf/cudf/core/dataframe.py +++ b/python/cudf/cudf/core/dataframe.py @@ -462,6 +462,10 @@ def _setitem_tuple_arg(self, key, value): self._frame[col].loc[key[0]] = value[i] +class _DataFrameAtIndexer(_DataFrameLocIndexer): + pass + + class _DataFrameIlocIndexer(_DataFrameIndexer): """ For selection by index. @@ -584,6 +588,10 @@ def _setitem_tuple_arg(self, key, value): self._frame[col].iloc[key[0]] = value[i] +class _DataFrameiAtIndexer(_DataFrameIlocIndexer): + pass + + class DataFrame(IndexedFrame, Serializable, GetAttrGetItemMixin): """ A GPU Dataframe object. @@ -2581,14 +2589,14 @@ def iat(self): """ Alias for ``DataFrame.iloc``; provided for compatibility with Pandas. """ - return self.iloc + return _DataFrameiAtIndexer(self) @property def at(self): """ Alias for ``DataFrame.loc``; provided for compatibility with Pandas. """ - return self.loc + return _DataFrameAtIndexer(self) @property # type: ignore @_external_only_api( diff --git a/python/cudf/cudf/pandas/_wrappers/pandas.py b/python/cudf/cudf/pandas/_wrappers/pandas.py index a64bf7772fe..dd6f6fe76ba 100644 --- a/python/cudf/cudf/pandas/_wrappers/pandas.py +++ b/python/cudf/cudf/pandas/_wrappers/pandas.py @@ -775,6 +775,18 @@ def Index__new__(cls, *args, **kwargs): pd.core.indexing._LocIndexer, ) +_AtIndexer = make_intermediate_proxy_type( + "_AtIndexer", + cudf.core.dataframe._DataFrameAtIndexer, + pd.core.indexing._AtIndexer, +) + +_iAtIndexer = make_intermediate_proxy_type( + "_iAtIndexer", + cudf.core.dataframe._DataFrameiAtIndexer, + pd.core.indexing._iAtIndexer, +) + FixedForwardWindowIndexer = make_final_proxy_type( "FixedForwardWindowIndexer", _Unusable, diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index f51ce103677..b0aeaba3916 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -1566,3 +1566,22 @@ def test_arrow_string_arrays(): ) tm.assert_equal(cu_arr, pd_arr) + + +@pytest.mark.parametrize("indexer", ["at", "iat"]) +def test_at_iat(indexer): + df = xpd.DataFrame(range(3)) + result = getattr(df, indexer)[0, 0] + assert result == 0 + + getattr(df, indexer)[0, 0] = 1 + expected = pd.DataFrame([1, 1, 2]) + tm.assert_frame_equal(df, expected) + + +def test_at_setitem_empty(): + df = xpd.DataFrame({"name": []}, dtype="float64") + df.at[0, "name"] = 1.0 + df.at[0, "new"] = 2.0 + expected = pd.DataFrame({"name": [1.0], "new": [2.0]}) + tm.assert_frame_equal(df, expected) From cc8c86857df92801561d2fa3311d8da85895ff33 Mon Sep 17 00:00:00 2001 From: Jason Lowe Date: Mon, 8 Jul 2024 16:54:45 -0500 Subject: [PATCH 34/42] Disable large string support for Java build (#16216) Disables libcudf large string support for the Java bindings build. The Java bindings need to be updated to handle large strings which is tracked by #16215. Closes #16199. Authors: - Jason Lowe (https://github.com/jlowe) Approvers: - Nghia Truong (https://github.com/ttnghia) URL: https://github.com/rapidsai/cudf/pull/16216 --- java/README.md | 10 +++++++--- java/ci/build-in-docker.sh | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/java/README.md b/java/README.md index 2d8e2190fee..0d9e060b7cd 100644 --- a/java/README.md +++ b/java/README.md @@ -51,9 +51,13 @@ CUDA 11.0: ## Build From Source Build [libcudf](../cpp) first, and make sure the JDK is installed and available. Specify -the cmake option `-DCUDF_USE_ARROW_STATIC=ON -DCUDF_ENABLE_ARROW_S3=OFF` when building so -that Apache Arrow is linked statically to libcudf, as this will help create a jar that -does not require Arrow and its dependencies to be available in the runtime environment. +the following cmake options to the libcudf build: +``` +-DCUDF_LARGE_STRINGS_DISABLED=ON -DCUDF_USE_ARROW_STATIC=ON -DCUDF_ENABLE_ARROW_S3=OFF +``` +These options: +- Disable large string support, see https://github.com/rapidsai/cudf/issues/16215 +- Statically link Arrow to libcudf to remove Arrow as a runtime dependency. After building libcudf, the Java bindings can be built via Maven, e.g.: ``` diff --git a/java/ci/build-in-docker.sh b/java/ci/build-in-docker.sh index 72b1742f7cb..5a429bdc739 100755 --- a/java/ci/build-in-docker.sh +++ b/java/ci/build-in-docker.sh @@ -1,7 +1,7 @@ #!/bin/bash # -# Copyright (c) 2020-2023, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -58,6 +58,7 @@ cmake .. -G"${CMAKE_GENERATOR}" \ -DCMAKE_INSTALL_PREFIX=$INSTALL_PREFIX \ -DCUDA_STATIC_RUNTIME=$ENABLE_CUDA_STATIC_RUNTIME \ -DUSE_NVTX=$ENABLE_NVTX \ + -DCUDF_LARGE_STRINGS_DISABLED=ON \ -DCUDF_USE_ARROW_STATIC=ON \ -DCUDF_ENABLE_ARROW_S3=OFF \ -DBUILD_TESTS=$BUILD_CPP_TESTS \ From 58b7dc9f186c1860d4f9df80188bf21214381b1b Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:01:25 -1000 Subject: [PATCH 35/42] interpolate returns new column if no values are interpolated (#16158) While cleaning up the `interpolate` implementation, I noticed that a interpolation no-op did not return a new column. Authors: - Matthew Roeschke (https://github.com/mroeschke) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16158 --- python/cudf/cudf/core/algorithms.py | 61 ++++++++-------------- python/cudf/cudf/core/indexed_frame.py | 14 +++-- python/cudf/cudf/core/multiindex.py | 4 +- python/cudf/cudf/tests/test_interpolate.py | 6 +++ 4 files changed, 39 insertions(+), 46 deletions(-) diff --git a/python/cudf/cudf/core/algorithms.py b/python/cudf/cudf/core/algorithms.py index e8b82ff60c2..6c69fbd2637 100644 --- a/python/cudf/cudf/core/algorithms.py +++ b/python/cudf/cudf/core/algorithms.py @@ -1,17 +1,22 @@ # Copyright (c) 2020-2024, NVIDIA CORPORATION. +from __future__ import annotations + import warnings +from typing import TYPE_CHECKING import cupy as cp import numpy as np from cudf.core.column import as_column -from cudf.core.copy_types import BooleanMask from cudf.core.index import RangeIndex, ensure_index -from cudf.core.indexed_frame import IndexedFrame from cudf.core.scalar import Scalar from cudf.options import get_option from cudf.utils.dtypes import can_convert_to_column +if TYPE_CHECKING: + from cudf.core.column.column import ColumnBase + from cudf.core.index import BaseIndex + def factorize(values, sort=False, use_na_sentinel=True, size_hint=None): """Encode the input values as integer labels @@ -110,55 +115,31 @@ def factorize(values, sort=False, use_na_sentinel=True, size_hint=None): return labels, cats.values if return_cupy_array else ensure_index(cats) -def _linear_interpolation(column, index=None): - """ - Interpolate over a float column. Implicitly assumes that values are - evenly spaced with respect to the x-axis, for example the data - [1.0, NaN, 3.0] will be interpolated assuming the NaN is half way - between the two valid values, yielding [1.0, 2.0, 3.0] - """ - - index = RangeIndex(start=0, stop=len(column), step=1) - return _index_or_values_interpolation(column, index=index) - - -def _index_or_values_interpolation(column, index=None): +def _interpolation(column: ColumnBase, index: BaseIndex) -> ColumnBase: """ Interpolate over a float column. assumes a linear interpolation strategy using the index of the data to denote spacing of the x values. For example the data and index [1.0, NaN, 4.0], [1, 3, 4] - would result in [1.0, 3.0, 4.0] + would result in [1.0, 3.0, 4.0]. """ # figure out where the nans are - mask = cp.isnan(column) + mask = column.isnull() # trivial cases, all nan or no nans - num_nan = mask.sum() - if num_nan == 0 or num_nan == len(column): - return column + if not mask.any() or mask.all(): + return column.copy() - to_interp = IndexedFrame(data={None: column}, index=index) - known_x_and_y = to_interp._apply_boolean_mask( - BooleanMask(~mask, len(to_interp)) - ) - - known_x = known_x_and_y.index.to_cupy() - known_y = known_x_and_y._data.columns[0].values + valid_locs = ~mask + if isinstance(index, RangeIndex): + # Each point is evenly spaced, index values don't matter + known_x = cp.flatnonzero(valid_locs.values) + else: + known_x = index._column.apply_boolean_mask(valid_locs).values # type: ignore[attr-defined] + known_y = column.apply_boolean_mask(valid_locs).values result = cp.interp(index.to_cupy(), known_x, known_y) # find the first nan - first_nan_idx = (mask == 0).argmax().item() + first_nan_idx = valid_locs.values.argmax().item() result[:first_nan_idx] = np.nan - return result - - -def get_column_interpolator(method): - interpolator = { - "linear": _linear_interpolation, - "index": _index_or_values_interpolation, - "values": _index_or_values_interpolation, - }.get(method, None) - if not interpolator: - raise ValueError(f"Interpolation method `{method}` not found") - return interpolator + return as_column(result) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index ff10051c52d..63fa96d0db0 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -26,6 +26,8 @@ import cudf import cudf._lib as libcudf +import cudf.core +import cudf.core.algorithms from cudf.api.extensions import no_default from cudf.api.types import ( _is_non_decimal_numeric_dtype, @@ -1987,6 +1989,8 @@ def interpolate( "Use obj.ffill() or obj.bfill() instead.", FutureWarning, ) + elif method not in {"linear", "values", "index"}: + raise ValueError(f"Interpolation method `{method}` not found") data = self @@ -2000,7 +2004,10 @@ def interpolate( ) ) - interpolator = cudf.core.algorithms.get_column_interpolator(method) + if method == "linear": + interp_index = RangeIndex(self._num_rows) + else: + interp_index = data.index columns = [] for col in data._columns: if isinstance(col, cudf.core.column.StringColumn): @@ -2012,8 +2019,9 @@ def interpolate( if col.nullable: col = col.astype("float64").fillna(np.nan) - # Interpolation methods may or may not need the index - columns.append(interpolator(col, index=data.index)) + columns.append( + cudf.core.algorithms._interpolation(col, index=interp_index) + ) result = self._from_data_like_self( self._data._from_columns_like_self(columns) diff --git a/python/cudf/cudf/core/multiindex.py b/python/cudf/cudf/core/multiindex.py index 9cbe863142b..dbbd1eab6c8 100644 --- a/python/cudf/cudf/core/multiindex.py +++ b/python/cudf/cudf/core/multiindex.py @@ -23,6 +23,7 @@ from cudf.api.types import is_integer, is_list_like, is_object_dtype from cudf.core import column from cudf.core._base_index import _return_get_indexer_result +from cudf.core.algorithms import factorize from cudf.core.column_accessor import ColumnAccessor from cudf.core.frame import Frame from cudf.core.index import ( @@ -1373,9 +1374,6 @@ def from_arrays( (2, 'blue')], names=['number', 'color']) """ - # Imported here due to circular import - from cudf.core.algorithms import factorize - error_msg = "Input must be a list / sequence of array-likes." if not is_list_like(arrays): raise TypeError(error_msg) diff --git a/python/cudf/cudf/tests/test_interpolate.py b/python/cudf/cudf/tests/test_interpolate.py index 4a0dc331e1a..a4f0b9fc97e 100644 --- a/python/cudf/cudf/tests/test_interpolate.py +++ b/python/cudf/cudf/tests/test_interpolate.py @@ -135,3 +135,9 @@ def test_interpolate_dataframe_error_cases(data, kwargs): lfunc_args_and_kwargs=([], kwargs), rfunc_args_and_kwargs=([], kwargs), ) + + +def test_interpolate_noop_new_column(): + ser = cudf.Series([1.0, 2.0, 3.0]) + result = ser.interpolate() + assert ser._column is not result._column From cf88f8e045b279cbe5caa2e19ffadc7c6400aa58 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:04:51 -1000 Subject: [PATCH 36/42] Defer copying in Column.astype(copy=True) (#16095) Avoids: 1. Copying `self` when the `astype` would already produce a new column with its own data 2. Copying `self` when the `astype` would raise an Exception Also cleans up some `as_categorical_column` logic. Authors: - Matthew Roeschke (https://github.com/mroeschke) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/16095 --- python/cudf/cudf/core/column/categorical.py | 20 ++--- python/cudf/cudf/core/column/column.py | 91 ++++++++++----------- 2 files changed, 51 insertions(+), 60 deletions(-) diff --git a/python/cudf/cudf/core/column/categorical.py b/python/cudf/cudf/core/column/categorical.py index 231af30c06d..cec7d5e6663 100644 --- a/python/cudf/cudf/core/column/categorical.py +++ b/python/cudf/cudf/core/column/categorical.py @@ -1113,24 +1113,18 @@ def is_monotonic_decreasing(self) -> bool: def as_categorical_column(self, dtype: Dtype) -> CategoricalColumn: if isinstance(dtype, str) and dtype == "category": return self + if isinstance(dtype, pd.CategoricalDtype): + dtype = cudf.CategoricalDtype.from_pandas(dtype) if ( - isinstance( - dtype, (cudf.core.dtypes.CategoricalDtype, pd.CategoricalDtype) - ) - and (dtype.categories is None) - and (dtype.ordered is None) + isinstance(dtype, cudf.CategoricalDtype) + and dtype.categories is None + and dtype.ordered is None ): return self - - if isinstance(dtype, pd.CategoricalDtype): - dtype = CategoricalDtype( - categories=dtype.categories, ordered=dtype.ordered - ) - - if not isinstance(dtype, CategoricalDtype): + elif not isinstance(dtype, CategoricalDtype): raise ValueError("dtype must be CategoricalDtype") - if not isinstance(self.categories, type(dtype.categories._values)): + if not isinstance(self.categories, type(dtype.categories._column)): # If both categories are of different Column types, # return a column full of Nulls. return _create_empty_categorical_column(self, dtype) diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index e7a2863da8c..adc783c20c4 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -962,59 +962,59 @@ def astype(self, dtype: Dtype, copy: bool = False) -> ColumnBase: if len(self) == 0: dtype = cudf.dtype(dtype) if self.dtype == dtype: - if copy: - return self.copy() - else: - return self + result = self else: - return column_empty(0, dtype=dtype, masked=self.nullable) - if copy: - col = self.copy() - else: - col = self - if dtype == "category": + result = column_empty(0, dtype=dtype, masked=self.nullable) + elif dtype == "category": # TODO: Figure out why `cudf.dtype("category")` # astype's different than just the string - return col.as_categorical_column(dtype) + result = self.as_categorical_column(dtype) elif ( isinstance(dtype, str) and dtype == "interval" and isinstance(self.dtype, cudf.IntervalDtype) ): # astype("interval") (the string only) should no-op - return col - was_object = dtype == object or dtype == np.dtype(object) - dtype = cudf.dtype(dtype) - if self.dtype == dtype: - return col - elif isinstance(dtype, CategoricalDtype): - return col.as_categorical_column(dtype) - elif isinstance(dtype, IntervalDtype): - return col.as_interval_column(dtype) - elif isinstance(dtype, (ListDtype, StructDtype)): - if not col.dtype == dtype: - raise NotImplementedError( - f"Casting {self.dtype} columns not currently supported" - ) - return col - elif isinstance(dtype, cudf.core.dtypes.DecimalDtype): - return col.as_decimal_column(dtype) - elif dtype.kind == "M": - return col.as_datetime_column(dtype) - elif dtype.kind == "m": - return col.as_timedelta_column(dtype) - elif dtype.kind == "O": - if cudf.get_option("mode.pandas_compatible") and was_object: - raise ValueError( - f"Casting to {dtype} is not supported, use " - "`.astype('str')` instead." - ) - return col.as_string_column(dtype) + result = self else: - return col.as_numerical_column(dtype) + was_object = dtype == object or dtype == np.dtype(object) + dtype = cudf.dtype(dtype) + if self.dtype == dtype: + result = self + elif isinstance(dtype, CategoricalDtype): + result = self.as_categorical_column(dtype) + elif isinstance(dtype, IntervalDtype): + result = self.as_interval_column(dtype) + elif isinstance(dtype, (ListDtype, StructDtype)): + if not self.dtype == dtype: + raise NotImplementedError( + f"Casting {self.dtype} columns not currently supported" + ) + result = self + elif isinstance(dtype, cudf.core.dtypes.DecimalDtype): + result = self.as_decimal_column(dtype) + elif dtype.kind == "M": + result = self.as_datetime_column(dtype) + elif dtype.kind == "m": + result = self.as_timedelta_column(dtype) + elif dtype.kind == "O": + if cudf.get_option("mode.pandas_compatible") and was_object: + raise ValueError( + f"Casting to {dtype} is not supported, use " + "`.astype('str')` instead." + ) + result = self.as_string_column(dtype) + else: + result = self.as_numerical_column(dtype) + + if copy and result is self: + return result.copy() + return result def as_categorical_column(self, dtype) -> ColumnBase: - if isinstance(dtype, (cudf.CategoricalDtype, pd.CategoricalDtype)): + if isinstance(dtype, pd.CategoricalDtype): + dtype = cudf.CategoricalDtype.from_pandas(dtype) + if isinstance(dtype, cudf.CategoricalDtype): ordered = dtype.ordered else: ordered = False @@ -1023,14 +1023,11 @@ def as_categorical_column(self, dtype) -> ColumnBase: if ( isinstance(dtype, cudf.CategoricalDtype) and dtype._categories is not None - ) or ( - isinstance(dtype, pd.CategoricalDtype) - and dtype.categories is not None ): - labels = self._label_encoding(cats=as_column(dtype.categories)) - + cat_col = dtype._categories + labels = self._label_encoding(cats=cat_col) return build_categorical_column( - categories=as_column(dtype.categories), + categories=cat_col, codes=labels, mask=self.mask, ordered=dtype.ordered, From 65e4e99d702aedbbfd489840d112faecfaeb43b9 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Mon, 8 Jul 2024 23:10:23 -0500 Subject: [PATCH 37/42] Remove CCCL patch for PR 211. (#16207) While upgrading CCCL, we ran into a test failure in cuSpatial. We added a patch to revert some changes from CCCL but the root cause was a bug in cuSpatial. I have fixed that bug here: https://github.com/rapidsai/cuspatial/pull/1402 Once that PR is merged, we can remove this CCCL patch. See also: - rapids-cmake patch removal: https://github.com/rapidsai/rapids-cmake/pull/640 - Original rapids-cmake patch: https://github.com/rapidsai/rapids-cmake/pull/511 - CCCL epic to remove RAPIDS patches: https://github.com/NVIDIA/cccl/issues/1939 Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Robert Maynard (https://github.com/robertmaynard) URL: https://github.com/rapidsai/cudf/pull/16207 --- cpp/cmake/thirdparty/patches/cccl_override.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cpp/cmake/thirdparty/patches/cccl_override.json b/cpp/cmake/thirdparty/patches/cccl_override.json index e61102dffac..2f29578f7ae 100644 --- a/cpp/cmake/thirdparty/patches/cccl_override.json +++ b/cpp/cmake/thirdparty/patches/cccl_override.json @@ -3,11 +3,6 @@ "packages" : { "CCCL" : { "patches" : [ - { - "file" : "cccl/revert_pr_211.diff", - "issue" : "thrust::copy introduced a change in behavior that causes failures with cudaErrorInvalidValue.", - "fixed_in" : "" - }, { "file" : "${current_json_dir}/thrust_disable_64bit_dispatching.diff", "issue" : "Remove 64bit dispatching as not needed by libcudf and results in compiling twice as many kernels [https://github.com/rapidsai/cudf/pull/11437]", From b693e79b1813276700f70c2cb251d6fef71851a1 Mon Sep 17 00:00:00 2001 From: Lawrence Mitchell Date: Tue, 9 Jul 2024 13:22:35 +0100 Subject: [PATCH 38/42] Handler csv reader options in cudf-polars (#16211) Previously we were just relying on the default cudf read_csv options which doesn't do the right thing if the user has configured things. Now that polars passes through the information to us, we can handle things properly, and raise for unsupported cases. While here, update to new polars release and adapt tests to bug fixes that have been made upstream. Authors: - Lawrence Mitchell (https://github.com/wence-) Approvers: - James Lamb (https://github.com/jameslamb) - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/16211 --- python/cudf/cudf/_lib/csv.pyx | 2 +- python/cudf_polars/cudf_polars/dsl/expr.py | 4 +- python/cudf_polars/cudf_polars/dsl/ir.py | 104 +++++++++++++++-- .../cudf_polars/cudf_polars/dsl/translate.py | 12 +- python/cudf_polars/tests/test_scan.py | 107 ++++++++++++++++-- 5 files changed, 206 insertions(+), 23 deletions(-) diff --git a/python/cudf/cudf/_lib/csv.pyx b/python/cudf/cudf/_lib/csv.pyx index c706351a683..9fecff5f5f6 100644 --- a/python/cudf/cudf/_lib/csv.pyx +++ b/python/cudf/cudf/_lib/csv.pyx @@ -450,7 +450,7 @@ def read_csv( col_name = df._data.names[index] df._data[col_name] = df._data[col_name].astype(col_dtype) - if names is not None and isinstance(names[0], (int)): + if names is not None and len(names) and isinstance(names[0], (int)): df.columns = [int(x) for x in df._data] # Set index if the index_col parameter is passed diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index 93cb9db7cbd..f83d9e82d30 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -32,7 +32,7 @@ if TYPE_CHECKING: from collections.abc import Mapping, Sequence - import polars.polars as plrs + import polars as pl import polars.type_aliases as pl_types from cudf_polars.containers import DataFrame @@ -377,7 +377,7 @@ class LiteralColumn(Expr): value: pa.Array[Any, Any] children: tuple[()] - def __init__(self, dtype: plc.DataType, value: plrs.PySeries) -> None: + def __init__(self, dtype: plc.DataType, value: pl.Series) -> None: super().__init__(dtype) data = value.to_arrow() self.value = data.cast(dtypes.downcast_arrow_lists(data.type)) diff --git a/python/cudf_polars/cudf_polars/dsl/ir.py b/python/cudf_polars/cudf_polars/dsl/ir.py index 6b552642e88..b32fa9c273e 100644 --- a/python/cudf_polars/cudf_polars/dsl/ir.py +++ b/python/cudf_polars/cudf_polars/dsl/ir.py @@ -15,9 +15,9 @@ import dataclasses import itertools -import json import types from functools import cache +from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, ClassVar import pyarrow as pa @@ -185,8 +185,10 @@ class Scan(IR): typ: str """What type of file are we reading? Parquet, CSV, etc...""" - options: tuple[Any, ...] - """Type specific options, as json-encoded strings.""" + reader_options: dict[str, Any] + """Reader-specific options, as dictionary.""" + cloud_options: dict[str, Any] | None + """Cloud-related authentication options, currently ignored.""" paths: list[str] """List of paths to read from.""" file_options: Any @@ -206,9 +208,33 @@ def __post_init__(self) -> None: if self.file_options.n_rows is not None: raise NotImplementedError("row limit in scan") if self.typ not in ("csv", "parquet"): + raise NotImplementedError(f"Unhandled scan type: {self.typ}") + if self.cloud_options is not None and any( + self.cloud_options[k] is not None for k in ("aws", "azure", "gcp") + ): raise NotImplementedError( - f"Unhandled scan type: {self.typ}" - ) # pragma: no cover; polars raises on the rust side for now + "Read from cloud storage" + ) # pragma: no cover; no test yet + if self.typ == "csv": + if self.reader_options["skip_rows_after_header"] != 0: + raise NotImplementedError("Skipping rows after header in CSV reader") + parse_options = self.reader_options["parse_options"] + if ( + null_values := parse_options["null_values"] + ) is not None and "Named" in null_values: + raise NotImplementedError( + "Per column null value specification not supported for CSV reader" + ) + if ( + comment := parse_options["comment_prefix"] + ) is not None and "Multi" in comment: + raise NotImplementedError( + "Multi-character comment prefix not supported for CSV reader" + ) + if not self.reader_options["has_header"]: + # Need to do some file introspection to get the number + # of columns so that column projection works right. + raise NotImplementedError("Reading CSV without header") def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: """Evaluate and return a dataframe.""" @@ -216,14 +242,70 @@ def evaluate(self, *, cache: MutableMapping[int, DataFrame]) -> DataFrame: with_columns = options.with_columns row_index = options.row_index if self.typ == "csv": - opts, cloud_opts = map(json.loads, self.options) - df = DataFrame.from_cudf( - cudf.concat( - [cudf.read_csv(p, usecols=with_columns) for p in self.paths] + dtype_map = { + name: cudf._lib.types.PYLIBCUDF_TO_SUPPORTED_NUMPY_TYPES[typ.id()] + for name, typ in self.schema.items() + } + parse_options = self.reader_options["parse_options"] + sep = chr(parse_options["separator"]) + quote = chr(parse_options["quote_char"]) + eol = chr(parse_options["eol_char"]) + if self.reader_options["schema"] is not None: + # Reader schema provides names + column_names = list(self.reader_options["schema"]["inner"].keys()) + else: + # file provides column names + column_names = None + usecols = with_columns + # TODO: support has_header=False + header = 0 + + # polars defaults to no null recognition + null_values = [""] + if parse_options["null_values"] is not None: + ((typ, nulls),) = parse_options["null_values"].items() + if typ == "AllColumnsSingle": + # Single value + null_values.append(nulls) + else: + # List of values + null_values.extend(nulls) + if parse_options["comment_prefix"] is not None: + comment = chr(parse_options["comment_prefix"]["Single"]) + else: + comment = None + decimal = "," if parse_options["decimal_comma"] else "." + + # polars skips blank lines at the beginning of the file + pieces = [] + for p in self.paths: + skiprows = self.reader_options["skip_rows"] + # TODO: read_csv expands globs which we should not do, + # because polars will already have handled them. + path = Path(p) + with path.open() as f: + while f.readline() == "\n": + skiprows += 1 + pieces.append( + cudf.read_csv( + path, + sep=sep, + quotechar=quote, + lineterminator=eol, + names=column_names, + header=header, + usecols=usecols, + na_filter=True, + na_values=null_values, + keep_default_na=False, + skiprows=skiprows, + comment=comment, + decimal=decimal, + dtype=dtype_map, + ) ) - ) + df = DataFrame.from_cudf(cudf.concat(pieces)) elif self.typ == "parquet": - opts, cloud_opts = map(json.loads, self.options) cdf = cudf.read_parquet(self.paths, columns=with_columns) assert isinstance(cdf, cudf.DataFrame) df = DataFrame.from_cudf(cdf) diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index 5a1e682abe7..dec45679c75 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -5,6 +5,7 @@ from __future__ import annotations +import json from contextlib import AbstractContextManager, nullcontext from functools import singledispatch from typing import Any @@ -12,6 +13,7 @@ import pyarrow as pa from typing_extensions import assert_never +import polars as pl import polars.polars as plrs from polars.polars import _expr_nodes as pl_expr, _ir_nodes as pl_ir @@ -88,10 +90,16 @@ def _( node: pl_ir.Scan, visitor: NodeTraverser, schema: dict[str, plc.DataType] ) -> ir.IR: typ, *options = node.scan_type + if typ == "ndjson": + (reader_options,) = map(json.loads, options) + cloud_options = None + else: + reader_options, cloud_options = map(json.loads, options) return ir.Scan( schema, typ, - tuple(options), + reader_options, + cloud_options, node.paths, node.file_options, translate_named_expr(visitor, n=node.predicate) @@ -402,7 +410,7 @@ def _(node: pl_expr.Window, visitor: NodeTraverser, dtype: plc.DataType) -> expr @_translate_expr.register def _(node: pl_expr.Literal, visitor: NodeTraverser, dtype: plc.DataType) -> expr.Expr: if isinstance(node.value, plrs.PySeries): - return expr.LiteralColumn(dtype, node.value) + return expr.LiteralColumn(dtype, pl.Series._from_pyseries(node.value)) value = pa.scalar(node.value, type=plc.interop.to_arrow(dtype)) return expr.Literal(dtype, value) diff --git a/python/cudf_polars/tests/test_scan.py b/python/cudf_polars/tests/test_scan.py index f129cc7ca32..c41a94da14b 100644 --- a/python/cudf_polars/tests/test_scan.py +++ b/python/cudf_polars/tests/test_scan.py @@ -22,22 +22,22 @@ def row_index(request): @pytest.fixture( params=[ - (None, 0), + None, pytest.param( - (2, 1), marks=pytest.mark.xfail(reason="No handling of row limit in scan") + 2, marks=pytest.mark.xfail(reason="No handling of row limit in scan") ), pytest.param( - (3, 0), marks=pytest.mark.xfail(reason="No handling of row limit in scan") + 3, marks=pytest.mark.xfail(reason="No handling of row limit in scan") ), ], ids=["all-rows", "n_rows-with-skip", "n_rows-no-skip"], ) -def n_rows_skip_rows(request): +def n_rows(request): return request.param @pytest.fixture(params=["csv", "parquet"]) -def df(request, tmp_path, row_index, n_rows_skip_rows): +def df(request, tmp_path, row_index, n_rows): df = pl.DataFrame( { "a": [1, 2, 3, None], @@ -46,14 +46,12 @@ def df(request, tmp_path, row_index, n_rows_skip_rows): } ) name, offset = row_index - n_rows, skip_rows = n_rows_skip_rows if request.param == "csv": df.write_csv(tmp_path / "file.csv") return pl.scan_csv( tmp_path / "file.csv", row_index_name=name, row_index_offset=offset, - skip_rows_after_header=skip_rows, n_rows=n_rows, ) else: @@ -97,3 +95,98 @@ def test_scan_unsupported_raises(tmp_path): df.write_ndjson(tmp_path / "df.json") q = pl.scan_ndjson(tmp_path / "df.json") assert_ir_translation_raises(q, NotImplementedError) + + +def test_scan_row_index_projected_out(tmp_path): + df = pl.DataFrame({"a": [1, 2, 3]}) + + df.write_parquet(tmp_path / "df.pq") + + q = pl.scan_parquet(tmp_path / "df.pq").with_row_index().select(pl.col("a")) + + assert_gpu_result_equal(q) + + +def test_scan_csv_column_renames_projection_schema(tmp_path): + with (tmp_path / "test.csv").open("w") as f: + f.write("""foo,bar,baz\n1,2\n3,4,5""") + + q = pl.scan_csv( + tmp_path / "test.csv", + with_column_names=lambda names: [f"{n}_suffix" for n in names], + schema_overrides={ + "foo_suffix": pl.String(), + "bar_suffix": pl.Int8(), + "baz_suffix": pl.UInt16(), + }, + ) + + assert_gpu_result_equal(q) + + +def test_scan_csv_skip_after_header_not_implemented(tmp_path): + with (tmp_path / "test.csv").open("w") as f: + f.write("""foo,bar,baz\n1,2,3\n3,4,5""") + + q = pl.scan_csv(tmp_path / "test.csv", skip_rows_after_header=1) + + assert_ir_translation_raises(q, NotImplementedError) + + +def test_scan_csv_null_values_per_column_not_implemented(tmp_path): + with (tmp_path / "test.csv").open("w") as f: + f.write("""foo,bar,baz\n1,2,3\n3,4,5""") + + q = pl.scan_csv(tmp_path / "test.csv", null_values={"foo": "1", "baz": "5"}) + + assert_ir_translation_raises(q, NotImplementedError) + + +def test_scan_csv_comment_str_not_implemented(tmp_path): + with (tmp_path / "test.csv").open("w") as f: + f.write("""foo,bar,baz\n// 1,2,3\n3,4,5""") + + q = pl.scan_csv(tmp_path / "test.csv", comment_prefix="// ") + + assert_ir_translation_raises(q, NotImplementedError) + + +def test_scan_csv_comment_char(tmp_path): + with (tmp_path / "test.csv").open("w") as f: + f.write("""foo,bar,baz\n# 1,2,3\n3,4,5""") + + q = pl.scan_csv(tmp_path / "test.csv", comment_prefix="#") + + assert_gpu_result_equal(q) + + +@pytest.mark.parametrize("nulls", [None, "3", ["3", "5"]]) +def test_scan_csv_null_values(tmp_path, nulls): + with (tmp_path / "test.csv").open("w") as f: + f.write("""foo,bar,baz\n1,2,3\n3,4,5\n5,,2""") + + q = pl.scan_csv(tmp_path / "test.csv", null_values=nulls) + + assert_gpu_result_equal(q) + + +def test_scan_csv_decimal_comma(tmp_path): + with (tmp_path / "test.csv").open("w") as f: + f.write("""foo|bar|baz\n1,23|2,34|3,56\n1""") + + q = pl.scan_csv(tmp_path / "test.csv", separator="|", decimal_comma=True) + + assert_gpu_result_equal(q) + + +def test_scan_csv_skip_initial_empty_rows(tmp_path): + with (tmp_path / "test.csv").open("w") as f: + f.write("""\n\n\n\nfoo|bar|baz\n1|2|3\n1""") + + q = pl.scan_csv(tmp_path / "test.csv", separator="|", skip_rows=1, has_header=False) + + assert_ir_translation_raises(q, NotImplementedError) + + q = pl.scan_csv(tmp_path / "test.csv", separator="|", skip_rows=1) + + assert_gpu_result_equal(q) From 75966deef548754a5a7f5fb49f1cf5b1be991363 Mon Sep 17 00:00:00 2001 From: Thomas Li <47963215+lithomas1@users.noreply.github.com> Date: Tue, 9 Jul 2024 06:59:56 -0700 Subject: [PATCH 39/42] Publish cudf-polars nightlies (#16213) Publish nightlies for cudf-polars. Authors: - Thomas Li (https://github.com/lithomas1) Approvers: - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cudf/pull/16213 --- .github/workflows/build.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c5679cc5141..2e5959338b0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -108,6 +108,28 @@ jobs: sha: ${{ inputs.sha }} date: ${{ inputs.date }} package-name: dask_cudf + wheel-build-cudf-polars: + needs: wheel-publish-cudf + secrets: inherit + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.08 + with: + # This selects "ARCH=amd64 + the latest supported Python + CUDA". + matrix_filter: map(select(.ARCH == "amd64")) | group_by(.CUDA_VER|split(".")|map(tonumber)|.[0]) | map(max_by([(.PY_VER|split(".")|map(tonumber)), (.CUDA_VER|split(".")|map(tonumber))])) + build_type: ${{ inputs.build_type || 'branch' }} + branch: ${{ inputs.branch }} + sha: ${{ inputs.sha }} + date: ${{ inputs.date }} + script: ci/build_wheel_cudf_polars.sh + wheel-publish-cudf-polars: + needs: wheel-build-cudf-polars + secrets: inherit + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.08 + with: + build_type: ${{ inputs.build_type || 'branch' }} + branch: ${{ inputs.branch }} + sha: ${{ inputs.sha }} + date: ${{ inputs.date }} + package-name: cudf_polars trigger-pandas-tests: if: inputs.build_type == 'nightly' needs: wheel-build-cudf From 433e959deab26ccf1eb9b75b8ea3e21659da4f0a Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 9 Jul 2024 10:45:05 -0400 Subject: [PATCH 40/42] Free temp memory no longer needed in multibyte_split processing (#16091) Updates the `multibyte_split` logic to free temporary memory once the chars and offsets have been resolved. This gives room to the remaining processing if more temp memory is required. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Bradley Dice (https://github.com/bdice) - https://github.com/nvdbaranec URL: https://github.com/rapidsai/cudf/pull/16091 --- cpp/src/io/text/multibyte_split.cu | 324 ++++++++++++++--------------- 1 file changed, 162 insertions(+), 162 deletions(-) diff --git a/cpp/src/io/text/multibyte_split.cu b/cpp/src/io/text/multibyte_split.cu index 51dc0ca90af..be2e2b9a79c 100644 --- a/cpp/src/io/text/multibyte_split.cu +++ b/cpp/src/io/text/multibyte_split.cu @@ -55,6 +55,8 @@ #include #include +namespace cudf::io::text { +namespace detail { namespace { using cudf::io::text::detail::multistate; @@ -299,11 +301,6 @@ CUDF_KERNEL __launch_bounds__(THREADS_PER_TILE) void byte_split_kernel( } // namespace -namespace cudf { -namespace io { -namespace text { -namespace detail { - std::unique_ptr multibyte_split(cudf::io::text::data_chunk_source const& source, std::string const& delimiter, byte_range_info byte_range, @@ -336,173 +333,181 @@ std::unique_ptr multibyte_split(cudf::io::text::data_chunk_source CUDF_EXPECTS(delimiter.size() < multistate::max_segment_value, "delimiter contains too many total tokens to produce a deterministic result."); - auto const concurrency = 2; - - // must be at least 32 when using warp-reduce on partials - // must be at least 1 more than max possible concurrent tiles - // best when at least 32 more than max possible concurrent tiles, due to rolling `invalid`s - auto num_tile_states = std::max(32, TILES_PER_CHUNK * concurrency + 32); - auto tile_multistates = - scan_tile_state(num_tile_states, stream, rmm::mr::get_current_device_resource()); - auto tile_offsets = - scan_tile_state(num_tile_states, stream, rmm::mr::get_current_device_resource()); - - multibyte_split_init_kernel<<>>( // - -TILES_PER_CHUNK, - TILES_PER_CHUNK, - tile_multistates, - tile_offsets, - cudf::io::text::detail::scan_tile_status::oob); - - auto multistate_seed = multistate(); - multistate_seed.enqueue(0, 0); // this represents the first state in the pattern. - - // Seeding the tile state with an identity value allows the 0th tile to follow the same logic as - // the Nth tile, assuming it can look up an inclusive prefix. Without this seed, the 0th block - // would have to follow separate logic. - cudf::detail::device_single_thread( - [tm = scan_tile_state_view(tile_multistates), - to = scan_tile_state_view(tile_offsets), - multistate_seed] __device__() mutable { - tm.set_inclusive_prefix(-1, multistate_seed); - to.set_inclusive_prefix(-1, 0); - }, - stream); - - auto reader = source.create_reader(); - auto chunk_offset = std::max(0, byte_range.offset() - delimiter.size()); - auto const byte_range_end = byte_range.offset() + byte_range.size(); - reader->skip_bytes(chunk_offset); - // amortize output chunk allocations over 8 worst-case outputs. This limits the overallocation - constexpr auto max_growth = 8; - output_builder row_offset_storage(ITEMS_PER_CHUNK, max_growth, stream); - output_builder char_storage(ITEMS_PER_CHUNK, max_growth, stream); - - auto streams = cudf::detail::fork_streams(stream, concurrency); - - cudaEvent_t last_launch_event; - CUDF_CUDA_TRY(cudaEventCreate(&last_launch_event)); - - auto& read_stream = streams[0]; - auto& scan_stream = streams[1]; - auto chunk = reader->get_next_chunk(ITEMS_PER_CHUNK, read_stream); - int64_t base_tile_idx = 0; + auto chunk_offset = std::max(0, byte_range.offset() - delimiter.size()); std::optional first_row_offset; - std::optional last_row_offset; - bool found_last_offset = false; if (byte_range.offset() == 0) { first_row_offset = 0; } - std::swap(read_stream, scan_stream); - - while (chunk->size() > 0) { - // if we found the last delimiter, or didn't find delimiters inside the byte range at all: abort - if (last_row_offset.has_value() or - (not first_row_offset.has_value() and chunk_offset >= byte_range_end)) { - break; - } - - auto tiles_in_launch = - cudf::util::div_rounding_up_safe(chunk->size(), static_cast(ITEMS_PER_TILE)); - - auto row_offsets = row_offset_storage.next_output(scan_stream); + std::optional last_row_offset; - // reset the next chunk of tile state - multibyte_split_init_kernel<<(num_tile_states, stream, rmm::mr::get_current_device_resource()); + auto tile_offsets = scan_tile_state( + num_tile_states, stream, rmm::mr::get_current_device_resource()); + + multibyte_split_init_kernel<<>>( // - base_tile_idx, - tiles_in_launch, + stream.value()>>>( // + -TILES_PER_CHUNK, + TILES_PER_CHUNK, tile_multistates, - tile_offsets); + tile_offsets, + cudf::io::text::detail::scan_tile_status::oob); - CUDF_CUDA_TRY(cudaStreamWaitEvent(scan_stream.value(), last_launch_event)); + auto multistate_seed = multistate(); + multistate_seed.enqueue(0, 0); // this represents the first state in the pattern. - if (delimiter.size() == 1) { - // the single-byte case allows for a much more efficient kernel, so we special-case it - byte_split_kernel<<>>( // - base_tile_idx, - chunk_offset, - row_offset_storage.size(), - tile_offsets, - delimiter[0], - *chunk, - row_offsets); - } else { - multibyte_split_kernel<<>>( // + // Seeding the tile state with an identity value allows the 0th tile to follow the same logic as + // the Nth tile, assuming it can look up an inclusive prefix. Without this seed, the 0th block + // would have to follow separate logic. + cudf::detail::device_single_thread( + [tm = scan_tile_state_view(tile_multistates), + to = scan_tile_state_view(tile_offsets), + multistate_seed] __device__() mutable { + tm.set_inclusive_prefix(-1, multistate_seed); + to.set_inclusive_prefix(-1, 0); + }, + stream); + + auto reader = source.create_reader(); + auto const byte_range_end = byte_range.offset() + byte_range.size(); + reader->skip_bytes(chunk_offset); + // amortize output chunk allocations over 8 worst-case outputs. This limits the overallocation + constexpr auto max_growth = 8; + output_builder row_offset_storage(ITEMS_PER_CHUNK, max_growth, stream); + output_builder char_storage(ITEMS_PER_CHUNK, max_growth, stream); + + auto streams = cudf::detail::fork_streams(stream, concurrency); + + cudaEvent_t last_launch_event; + CUDF_CUDA_TRY(cudaEventCreate(&last_launch_event)); + + auto& read_stream = streams[0]; + auto& scan_stream = streams[1]; + auto chunk = reader->get_next_chunk(ITEMS_PER_CHUNK, read_stream); + int64_t base_tile_idx = 0; + bool found_last_offset = false; + std::swap(read_stream, scan_stream); + + while (chunk->size() > 0) { + // if we found the last delimiter, or didn't find delimiters inside the byte range at all: + // abort + if (last_row_offset.has_value() or + (not first_row_offset.has_value() and chunk_offset >= byte_range_end)) { + break; + } + + auto tiles_in_launch = + cudf::util::div_rounding_up_safe(chunk->size(), static_cast(ITEMS_PER_TILE)); + + auto row_offsets = row_offset_storage.next_output(scan_stream); + + // reset the next chunk of tile state + multibyte_split_init_kernel<<>>( // base_tile_idx, - chunk_offset, - row_offset_storage.size(), + tiles_in_launch, tile_multistates, - tile_offsets, - {device_delim.data(), static_cast(device_delim.size())}, - *chunk, - row_offsets); - } + tile_offsets); + + CUDF_CUDA_TRY(cudaStreamWaitEvent(scan_stream.value(), last_launch_event)); + + if (delimiter.size() == 1) { + // the single-byte case allows for a much more efficient kernel, so we special-case it + byte_split_kernel<<>>( // + base_tile_idx, + chunk_offset, + row_offset_storage.size(), + tile_offsets, + delimiter[0], + *chunk, + row_offsets); + } else { + multibyte_split_kernel<<>>( // + base_tile_idx, + chunk_offset, + row_offset_storage.size(), + tile_multistates, + tile_offsets, + {device_delim.data(), static_cast(device_delim.size())}, + *chunk, + row_offsets); + } - // load the next chunk - auto next_chunk = reader->get_next_chunk(ITEMS_PER_CHUNK, read_stream); - // while that is running, determine how many offsets we output (synchronizes) - auto const new_offsets = [&] { - auto const new_offsets_unclamped = - tile_offsets.get_inclusive_prefix(base_tile_idx + tiles_in_launch - 1, scan_stream) - - static_cast(row_offset_storage.size()); - // if we are not in the last chunk, we can use all offsets - if (chunk_offset + static_cast(chunk->size()) < byte_range_end) { - return new_offsets_unclamped; + // load the next chunk + auto next_chunk = reader->get_next_chunk(ITEMS_PER_CHUNK, read_stream); + // while that is running, determine how many offsets we output (synchronizes) + auto const new_offsets = [&] { + auto const new_offsets_unclamped = + tile_offsets.get_inclusive_prefix(base_tile_idx + tiles_in_launch - 1, scan_stream) - + static_cast(row_offset_storage.size()); + // if we are not in the last chunk, we can use all offsets + if (chunk_offset + static_cast(chunk->size()) < byte_range_end) { + return new_offsets_unclamped; + } + // if we are in the last chunk, we need to find the first out-of-bounds offset + auto const it = thrust::make_counting_iterator(output_offset{}); + auto const end_loc = + *thrust::find_if(rmm::exec_policy_nosync(scan_stream), + it, + it + new_offsets_unclamped, + [row_offsets, byte_range_end] __device__(output_offset i) { + return row_offsets[i] >= byte_range_end; + }); + // if we had no out-of-bounds offset, we copy all offsets + if (end_loc == new_offsets_unclamped) { return end_loc; } + // otherwise we copy only up to (including) the first out-of-bounds delimiter + found_last_offset = true; + return end_loc + 1; + }(); + row_offset_storage.advance_output(new_offsets, scan_stream); + // determine if we found the first or last field offset for the byte range + if (new_offsets > 0 and not first_row_offset) { + first_row_offset = row_offset_storage.front_element(scan_stream); + } + if (found_last_offset) { last_row_offset = row_offset_storage.back_element(scan_stream); } + // copy over the characters we need, if we already encountered the first field delimiter + if (first_row_offset.has_value()) { + auto const begin = + chunk->data() + std::max(0, *first_row_offset - chunk_offset); + auto const sentinel = last_row_offset.value_or(std::numeric_limits::max()); + auto const end = + chunk->data() + std::min(sentinel - chunk_offset, chunk->size()); + auto const output_size = end - begin; + auto char_output = char_storage.next_output(scan_stream); + thrust::copy(rmm::exec_policy_nosync(scan_stream), begin, end, char_output.begin()); + char_storage.advance_output(output_size, scan_stream); } - // if we are in the last chunk, we need to find the first out-of-bounds offset - auto const it = thrust::make_counting_iterator(output_offset{}); - auto const end_loc = - *thrust::find_if(rmm::exec_policy_nosync(scan_stream), - it, - it + new_offsets_unclamped, - [row_offsets, byte_range_end] __device__(output_offset i) { - return row_offsets[i] >= byte_range_end; - }); - // if we had no out-of-bounds offset, we copy all offsets - if (end_loc == new_offsets_unclamped) { return end_loc; } - // otherwise we copy only up to (including) the first out-of-bounds delimiter - found_last_offset = true; - return end_loc + 1; - }(); - row_offset_storage.advance_output(new_offsets, scan_stream); - // determine if we found the first or last field offset for the byte range - if (new_offsets > 0 and not first_row_offset) { - first_row_offset = row_offset_storage.front_element(scan_stream); - } - if (found_last_offset) { last_row_offset = row_offset_storage.back_element(scan_stream); } - // copy over the characters we need, if we already encountered the first field delimiter - if (first_row_offset.has_value()) { - auto const begin = chunk->data() + std::max(0, *first_row_offset - chunk_offset); - auto const sentinel = last_row_offset.value_or(std::numeric_limits::max()); - auto const end = - chunk->data() + std::min(sentinel - chunk_offset, chunk->size()); - auto const output_size = end - begin; - auto char_output = char_storage.next_output(scan_stream); - thrust::copy(rmm::exec_policy_nosync(scan_stream), begin, end, char_output.begin()); - char_storage.advance_output(output_size, scan_stream); - } - CUDF_CUDA_TRY(cudaEventRecord(last_launch_event, scan_stream.value())); + CUDF_CUDA_TRY(cudaEventRecord(last_launch_event, scan_stream.value())); - std::swap(read_stream, scan_stream); - base_tile_idx += tiles_in_launch; - chunk_offset += chunk->size(); - chunk = std::move(next_chunk); - } + std::swap(read_stream, scan_stream); + base_tile_idx += tiles_in_launch; + chunk_offset += chunk->size(); + chunk = std::move(next_chunk); + } + + CUDF_CUDA_TRY(cudaEventDestroy(last_launch_event)); - CUDF_CUDA_TRY(cudaEventDestroy(last_launch_event)); + cudf::detail::join_streams(streams, stream); - cudf::detail::join_streams(streams, stream); + auto chars = char_storage.gather(stream, mr); + auto global_offsets = row_offset_storage.gather(stream, mr); + return std::pair{std::move(global_offsets), std::move(chars)}; + }(); // if the input was empty, we didn't find a delimiter at all, // or the first delimiter was also the last: empty output @@ -511,9 +516,6 @@ std::unique_ptr multibyte_split(cudf::io::text::data_chunk_source return make_empty_column(type_id::STRING); } - auto chars = char_storage.gather(stream, mr); - auto global_offsets = row_offset_storage.gather(stream, mr); - // insert an offset at the beginning if we started at the beginning of the input bool const insert_begin = first_row_offset.value_or(0) == 0; // insert an offset at the end if we have not terminated the last row @@ -591,6 +593,4 @@ std::unique_ptr multibyte_split(cudf::io::text::data_chunk_source return result; } -} // namespace text -} // namespace io -} // namespace cudf +} // namespace cudf::io::text From 341e014ed22e7da1e4b8db66a1d7b6fd5fba98e9 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:47:38 -0400 Subject: [PATCH 41/42] Support `pd.read_pickle` and `pd.to_pickle` in `cudf.pandas` (#16105) Closes #15459 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) - Matthew Roeschke (https://github.com/mroeschke) - Lawrence Mitchell (https://github.com/wence-) URL: https://github.com/rapidsai/cudf/pull/16105 --- python/cudf/cudf/pandas/_wrappers/pandas.py | 6 ++++++ python/cudf/cudf_pandas_tests/test_cudf_pandas.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/python/cudf/cudf/pandas/_wrappers/pandas.py b/python/cudf/cudf/pandas/_wrappers/pandas.py index dd6f6fe76ba..3f94fc18980 100644 --- a/python/cudf/cudf/pandas/_wrappers/pandas.py +++ b/python/cudf/cudf/pandas/_wrappers/pandas.py @@ -919,6 +919,12 @@ def Index__new__(cls, *args, **kwargs): _eval_func = _FunctionProxy(_Unusable(), pd.eval) +register_proxy_func(pd.read_pickle)( + _FunctionProxy(_Unusable(), pd.read_pickle) +) + +register_proxy_func(pd.to_pickle)(_FunctionProxy(_Unusable(), pd.to_pickle)) + def _get_eval_locals_and_globals(level, local_dict=None, global_dict=None): frame = sys._getframe(level + 3) diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index b0aeaba3916..bc864a48e9d 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -1080,6 +1080,13 @@ def test_pickle(obj): tm.assert_equal(obj, copy) + with tempfile.TemporaryFile() as f: + xpd.to_pickle(obj, f) + f.seek(0) + copy = xpd.read_pickle(f) + + tm.assert_equal(obj, copy) + def test_dataframe_query(): cudf_pandas_df = xpd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) From 7cc01befa61d7957093bf32b99b4cac1364761f7 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:05:11 -0700 Subject: [PATCH 42/42] Parallelize `gpuInitStringDescriptors` for fixed length byte array data (#16109) Closes #14113 This PR parallelizes the `gpuInitStringDescriptors` function for the fixed length byte array (FLBA) data at either warp or thread block level via cooperative groups. The function continues to execute serially (thread rank 0 in the group) for variable length arrays. CC: @etseidl Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Vukasin Milovanovic (https://github.com/vuule) - Shruti Shivakumar (https://github.com/shrshi) URL: https://github.com/rapidsai/cudf/pull/16109 --- cpp/src/io/parquet/decode_preprocess.cu | 5 +- cpp/src/io/parquet/page_data.cu | 7 ++- cpp/src/io/parquet/page_decode.cuh | 69 +++++++++++++++--------- cpp/src/io/parquet/page_string_decode.cu | 10 +++- 4 files changed, 60 insertions(+), 31 deletions(-) diff --git a/cpp/src/io/parquet/decode_preprocess.cu b/cpp/src/io/parquet/decode_preprocess.cu index e49801e6172..62f1ee88036 100644 --- a/cpp/src/io/parquet/decode_preprocess.cu +++ b/cpp/src/io/parquet/decode_preprocess.cu @@ -26,6 +26,8 @@ namespace cudf::io::parquet::detail { +namespace cg = cooperative_groups; + namespace { // # of threads we're decoding with @@ -163,7 +165,8 @@ __device__ size_type gpuDecodeTotalPageStringSize(page_state_s* s, int t) // For V1, the choice is an overestimate (s->dict_size), or an exact number that's // expensive to compute. For now we're going with the latter. else { - str_len = gpuInitStringDescriptors(s, nullptr, target_pos, t); + str_len = gpuInitStringDescriptors( + s, nullptr, target_pos, cg::this_thread_block()); } break; diff --git a/cpp/src/io/parquet/page_data.cu b/cpp/src/io/parquet/page_data.cu index 7207173b82f..e0d50d7ccf9 100644 --- a/cpp/src/io/parquet/page_data.cu +++ b/cpp/src/io/parquet/page_data.cu @@ -23,6 +23,8 @@ namespace cudf::io::parquet::detail { +namespace cg = cooperative_groups; + namespace { constexpr int decode_block_size = 128; @@ -277,6 +279,7 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size) } // this needs to be here to prevent warp 3 modifying src_pos before all threads have read it __syncthreads(); + auto const tile_warp = cg::tiled_partition(cg::this_thread_block()); if (t < 32) { // decode repetition and definition levels. // - update validity vectors @@ -298,9 +301,9 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size) src_target_pos = gpuDecodeRleBooleans(s, sb, src_target_pos, t & 0x1f); } else if (s->col.physical_type == BYTE_ARRAY or s->col.physical_type == FIXED_LEN_BYTE_ARRAY) { - gpuInitStringDescriptors(s, sb, src_target_pos, t & 0x1f); + gpuInitStringDescriptors(s, sb, src_target_pos, tile_warp); } - if (t == 32) { s->dict_pos = src_target_pos; } + if (tile_warp.thread_rank() == 0) { s->dict_pos = src_target_pos; } } else { // WARP1..WARP3: Decode values int const dtype = s->col.physical_type; diff --git a/cpp/src/io/parquet/page_decode.cuh b/cpp/src/io/parquet/page_decode.cuh index b1f8e6dd5fe..a3f91f6859b 100644 --- a/cpp/src/io/parquet/page_decode.cuh +++ b/cpp/src/io/parquet/page_decode.cuh @@ -21,6 +21,7 @@ #include "parquet_gpu.hpp" #include "rle_stream.cuh" +#include #include #include @@ -420,46 +421,62 @@ inline __device__ int gpuDecodeRleBooleans(page_state_s* s, state_buf* sb, int t * @param[in,out] s Page state input/output * @param[out] sb Page state buffer output * @param[in] target_pos Target output position - * @param[in] t Thread ID + * @param[in] g Cooperative group (thread block or tile) * @tparam sizes_only True if only sizes are to be calculated * @tparam state_buf Typename of the `state_buf` (usually inferred) + * @tparam thread_group Typename of the cooperative group (inferred) * * @return Total length of strings processed */ -template -__device__ size_type -gpuInitStringDescriptors(page_state_s* s, [[maybe_unused]] state_buf* sb, int target_pos, int t) +template +__device__ size_type gpuInitStringDescriptors(page_state_s* s, + [[maybe_unused]] state_buf* sb, + int target_pos, + thread_group const& g) { - int pos = s->dict_pos; - int total_len = 0; + int const t = g.thread_rank(); + int const dict_size = s->dict_size; + int k = s->dict_val; + int pos = s->dict_pos; + int total_len = 0; + + // All group threads can participate for fixed len byte arrays. + if (s->col.physical_type == FIXED_LEN_BYTE_ARRAY) { + int const dtype_len_in = s->dtype_len_in; + total_len = min((target_pos - pos) * dtype_len_in, dict_size - s->dict_val); + if constexpr (!sizes_only) { + for (pos += t, k += t * dtype_len_in; pos < target_pos; pos += g.size()) { + sb->str_len[rolling_index(pos)] = + (k < dict_size) ? dtype_len_in : 0; + // dict_idx is upperbounded by dict_size. + sb->dict_idx[rolling_index(pos)] = k; + // Increment k if needed. + if (k < dict_size) { k = min(k + (g.size() * dtype_len_in), dict_size); } + } + } + // Only thread_rank = 0 updates the s->dict_val + if (!t) { s->dict_val += total_len; } + } + // This step is purely serial for byte arrays + else { + if (!t) { + uint8_t const* cur = s->data_start; - // This step is purely serial - if (!t) { - uint8_t const* cur = s->data_start; - int dict_size = s->dict_size; - int k = s->dict_val; - - while (pos < target_pos) { - int len = 0; - if (s->col.physical_type == FIXED_LEN_BYTE_ARRAY) { - if (k < dict_size) { len = s->dtype_len_in; } - } else { + for (int len = 0; pos < target_pos; pos++, len = 0) { if (k + 4 <= dict_size) { len = (cur[k]) | (cur[k + 1] << 8) | (cur[k + 2] << 16) | (cur[k + 3] << 24); k += 4; if (k + len > dict_size) { len = 0; } } + if constexpr (!sizes_only) { + sb->dict_idx[rolling_index(pos)] = k; + sb->str_len[rolling_index(pos)] = len; + } + k += len; + total_len += len; } - if constexpr (!sizes_only) { - sb->dict_idx[rolling_index(pos)] = k; - sb->str_len[rolling_index(pos)] = len; - } - k += len; - total_len += len; - pos++; + s->dict_val = k; } - s->dict_val = k; - __threadfence_block(); } return total_len; diff --git a/cpp/src/io/parquet/page_string_decode.cu b/cpp/src/io/parquet/page_string_decode.cu index 58e8a09d5b6..ca74a1c2ba0 100644 --- a/cpp/src/io/parquet/page_string_decode.cu +++ b/cpp/src/io/parquet/page_string_decode.cu @@ -31,6 +31,8 @@ namespace cudf::io::parquet::detail { +namespace cg = cooperative_groups; + namespace { constexpr int preprocess_block_size = 512; @@ -1006,6 +1008,10 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size) } // this needs to be here to prevent warp 1/2 modifying src_pos before all threads have read it __syncthreads(); + + // Create a warp sized thread block tile + auto const tile_warp = cg::tiled_partition(cg::this_thread_block()); + if (t < 32) { // decode repetition and definition levels. // - update validity vectors @@ -1020,9 +1026,9 @@ CUDF_KERNEL void __launch_bounds__(decode_block_size) if (s->dict_base) { src_target_pos = gpuDecodeDictionaryIndices(s, sb, src_target_pos, lane_id).first; } else { - gpuInitStringDescriptors(s, sb, src_target_pos, lane_id); + gpuInitStringDescriptors(s, sb, src_target_pos, tile_warp); } - if (t == 32) { s->dict_pos = src_target_pos; } + if (tile_warp.thread_rank() == 0) { s->dict_pos = src_target_pos; } } else { int const me = t - out_thread0;